diff --git a/chartlets.js/CHANGES.md b/chartlets.js/CHANGES.md
index 71d3544..43cafa8 100644
--- a/chartlets.js/CHANGES.md
+++ b/chartlets.js/CHANGES.md
@@ -1,3 +1,9 @@
+## Version 0.1.5 (in development)
+
+* Add `multiple` property for `Select` component to enable the selection
+ of multiple elements. The `default` mode is supported at the moment.
+
+
## Version 0.1.4 (from 2025/03/06)
* In `chartlets.js` we no longer emit warnings and errors in common
diff --git a/chartlets.js/package-lock.json b/chartlets.js/package-lock.json
index 3b8e04f..ae12139 100644
--- a/chartlets.js/package-lock.json
+++ b/chartlets.js/package-lock.json
@@ -8322,4 +8322,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/chartlets.js/packages/lib/src/plugins/mui/Select.test.tsx b/chartlets.js/packages/lib/src/plugins/mui/Select.test.tsx
index 258e00c..fd1b5c8 100644
--- a/chartlets.js/packages/lib/src/plugins/mui/Select.test.tsx
+++ b/chartlets.js/packages/lib/src/plugins/mui/Select.test.tsx
@@ -105,6 +105,43 @@ describe("Select", () => {
});
});
+ it("should fire 'value' property with an array of multiple values", () => {
+ const { recordedEvents, onChange } = createChangeHandler();
+ render(
+ ,
+ );
+ // open the Select component's list box
+ // note, we must use "mouseDown" as "click" doesn't work
+ fireEvent.mouseDown(screen.getByRole("combobox"));
+ // click item in the Select component's list box
+ const listBox = within(screen.getByRole("listbox"));
+ fireEvent.click(listBox.getByText(/11/i));
+ fireEvent.click(listBox.getByText(/12/i));
+ expect(recordedEvents.length).toBe(2);
+ expect(recordedEvents).toEqual([
+ {
+ componentType: "Select",
+ id: "sel",
+ property: "value",
+ value: [11],
+ },
+ {
+ componentType: "Select",
+ id: "sel",
+ property: "value",
+ value: [12],
+ },
+ ]);
+ });
+
it("should fire 'value' property with object options", () => {
const { recordedEvents, onChange } = createChangeHandler();
render(
diff --git a/chartlets.js/packages/lib/src/plugins/mui/Select.tsx b/chartlets.js/packages/lib/src/plugins/mui/Select.tsx
index 1cae59f..71b6245 100644
--- a/chartlets.js/packages/lib/src/plugins/mui/Select.tsx
+++ b/chartlets.js/packages/lib/src/plugins/mui/Select.tsx
@@ -16,6 +16,7 @@ export type SelectOption =
interface SelectState extends ComponentState {
options?: SelectOption[];
+ multiple?: boolean;
}
interface SelectProps extends ComponentProps, SelectState {}
@@ -30,14 +31,19 @@ export function Select({
style,
tooltip,
label,
+ multiple = false,
onChange,
}: SelectProps) {
- const handleChange = (event: SelectChangeEvent) => {
+ const handleChange = (event: SelectChangeEvent) => {
if (id) {
- let newValue: string | number = event.target.value;
- if (typeof value == "number") {
- newValue = Number.parseInt(newValue);
+ let newValue: string | number | (string | number)[] = multiple
+ ? (event.target.value as (string | number)[])
+ : (event.target.value as string | number);
+
+ if (!multiple && typeof value === "number") {
+ newValue = Number.parseInt(newValue as string);
}
+
onChange({
componentType: type,
id: id,
@@ -54,16 +60,18 @@ export function Select({
labelId={`${id}-label`}
id={id}
name={name}
- value={`${value}`}
+ value={value}
disabled={disabled}
- onChange={handleChange}
- >
+ multiple={multiple}
+ onChange={handleChange}>
{Array.isArray(options) &&
- options.map(normalizeSelectOption).map(([value, text], index) => (
-
- {text}
-
- ))}
+ options
+ .map(normalizeSelectOption)
+ .map(([optionValue, optionLabel], index) => (
+
+ {optionLabel}
+
+ ))}
@@ -71,7 +79,7 @@ export function Select({
}
function normalizeSelectOption(
- option: SelectOption,
+ option: SelectOption
): [string | number, string] {
if (isString(option)) {
return [option, option];
diff --git a/chartlets.py/CHANGES.md b/chartlets.py/CHANGES.md
index 2e18cbe..70fe2ac 100644
--- a/chartlets.py/CHANGES.md
+++ b/chartlets.py/CHANGES.md
@@ -1,3 +1,9 @@
+## Version 0.1.5 (in development)
+
+* Add `multiple` property for `Select` component to enable the
+ of multiple elements.
+
+
## Version 0.1.4 (from 2025/03/06)
* New (MUI) components
@@ -39,6 +45,7 @@
- `Switch`
- `Slider`
- `Tabs` and `Tab`
+
## Version 0.0.29 (from 2024/11/26)
diff --git a/chartlets.py/chartlets/component.py b/chartlets.py/chartlets/component.py
index 32be433..08dbf1d 100644
--- a/chartlets.py/chartlets/component.py
+++ b/chartlets.py/chartlets/component.py
@@ -19,7 +19,7 @@ class Component(ABC):
name: str | None = None
"""HTML `name` attribute. Optional."""
- value: bool | int | float | str | None = None
+ value: bool | int | float | str | list[bool | int | float | str] | None = None
"""HTML `value` attribute. Required for specific components."""
style: dict[str, Any] | None = None
diff --git a/chartlets.py/chartlets/components/select.py b/chartlets.py/chartlets/components/select.py
index c5e0be7..5b45d40 100644
--- a/chartlets.py/chartlets/components/select.py
+++ b/chartlets.py/chartlets/components/select.py
@@ -13,6 +13,11 @@ class Select(Component):
"""Select components are used for collecting user provided
information from a list of options."""
+ multiple: bool | None = None
+ """Allows for multiple selection in Select Menu. If `true`, value
+ must be an array.
+ """
+
options: list[SelectOption] = field(default_factory=list)
"""The options given as a list of number or text values or a list
of (value, label) pairs.
diff --git a/chartlets.py/demo/my_extension/__init__.py b/chartlets.py/demo/my_extension/__init__.py
index 265f76a..5c9f47d 100644
--- a/chartlets.py/demo/my_extension/__init__.py
+++ b/chartlets.py/demo/my_extension/__init__.py
@@ -5,6 +5,8 @@
from .my_panel_4 import panel as my_panel_4
from .my_panel_5 import panel as my_panel_5
from .my_panel_6 import panel as my_panel_6
+from .my_panel_7 import panel as my_panel_7
+
ext = Extension(__name__)
ext.add(my_panel_1)
@@ -13,3 +15,4 @@
ext.add(my_panel_4)
ext.add(my_panel_5)
ext.add(my_panel_6)
+ext.add(my_panel_7)
diff --git a/chartlets.py/demo/my_extension/my_panel_7.py b/chartlets.py/demo/my_extension/my_panel_7.py
new file mode 100644
index 0000000..dccc158
--- /dev/null
+++ b/chartlets.py/demo/my_extension/my_panel_7.py
@@ -0,0 +1,93 @@
+import altair as alt
+import pandas as pd
+from chartlets import Component, Input, State, Output
+from chartlets.components import VegaChart, Box, Select, Typography
+
+from server.context import Context
+from server.panel import Panel
+
+
+panel = Panel(__name__, title="Panel G")
+
+
+@panel.layout(State("@app", "selectedDatasetId"))
+def render_panel(
+ ctx: Context,
+ selected_dataset_id: str = "",
+) -> Component:
+ dataset = ctx.datasets.get(selected_dataset_id)
+ variable_names, selected_var_names = get_variable_names(dataset)
+
+ select = Select(
+ id="selected_variable_name",
+ value=[],
+ label="Variable",
+ options=[(v, v) for v in variable_names],
+ style={"flexGrow": 0, "minWidth": 120},
+ multiple=True,
+ tooltip="Select the variables of the test dataset",
+ )
+ control_group = Box(
+ style={
+ "display": "flex",
+ "flexDirection": "row",
+ "padding": 4,
+ "justifyContent": "center",
+ "gap": 4,
+ },
+ children=[select],
+ )
+
+ text = update_info_text(ctx, selected_dataset_id)
+ info_text = Typography(id="info_text", children=text)
+
+ return Box(
+ style={
+ "display": "flex",
+ "flexDirection": "column",
+ "width": "100%",
+ "height": "100%",
+ },
+ children=[info_text, control_group],
+ )
+
+
+def get_variable_names(
+ dataset: pd.DataFrame,
+ prev_var_name: str | None = None,
+) -> tuple[list[str], list[str]]:
+ """Get the variable names and the selected variable name
+ for the given dataset and previously selected variable name.
+ """
+
+ if dataset is not None:
+ var_names = [v for v in dataset.keys() if v != "x"]
+ else:
+ var_names = []
+
+ if prev_var_name and prev_var_name in var_names:
+ var_name = prev_var_name
+ elif var_names:
+ var_name = var_names[0]
+ else:
+ var_name = ""
+
+ return var_names, var_name
+
+
+@panel.callback(
+ Input("@app", "selectedDatasetId"),
+ Input("selected_variable_name", "value"),
+ Output("info_text", "children"),
+)
+def update_info_text(
+ ctx: Context,
+ dataset_id: str = "",
+ selected_var_names: list[str] | None = None,
+) -> list[str]:
+
+ if selected_var_names is not None:
+ text = ", ".join(map(str, selected_var_names))
+ return [f"The dataset is {dataset_id} and the selected variables are: {text}"]
+ else:
+ return [f"The dataset is {dataset_id} and no variables are selected."]