Skip to content

Commit e18dc5c

Browse files
clarasbb-yogesh
andauthored
Select component: add prop multiple (#102)
* add multiple to select component * update changes.md and formatting * update * add demo panel E * add demo panel E * update demo panel E * update Changes * update Changes * update Changes * update Changes * Update chartlets.py/chartlets/components/select.py Co-authored-by: b-yogesh <[email protected]> * update formatting * add test for select.tsx regarding `multiple` property * update test for Select.tsx * resolved conflicts and new panel G --------- Co-authored-by: b-yogesh <[email protected]>
1 parent 64990f7 commit e18dc5c

File tree

9 files changed

+174
-15
lines changed

9 files changed

+174
-15
lines changed

chartlets.js/CHANGES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## Version 0.1.5 (in development)
2+
3+
* Add `multiple` property for `Select` component to enable the selection
4+
of multiple elements. The `default` mode is supported at the moment.
5+
6+
17
## Version 0.1.4 (from 2025/03/06)
28

39
* In `chartlets.js` we no longer emit warnings and errors in common

chartlets.js/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

chartlets.js/packages/lib/src/plugins/mui/Select.test.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,43 @@ describe("Select", () => {
105105
});
106106
});
107107

108+
it("should fire 'value' property with an array of multiple values", () => {
109+
const { recordedEvents, onChange } = createChangeHandler();
110+
render(
111+
<Select
112+
id="sel"
113+
type={"Select"}
114+
label={"Colors"}
115+
options={[10, 11, 12]}
116+
value={[]}
117+
onChange={onChange}
118+
multiple={true}
119+
/>,
120+
);
121+
// open the Select component's list box
122+
// note, we must use "mouseDown" as "click" doesn't work
123+
fireEvent.mouseDown(screen.getByRole("combobox"));
124+
// click item in the Select component's list box
125+
const listBox = within(screen.getByRole("listbox"));
126+
fireEvent.click(listBox.getByText(/11/i));
127+
fireEvent.click(listBox.getByText(/12/i));
128+
expect(recordedEvents.length).toBe(2);
129+
expect(recordedEvents).toEqual([
130+
{
131+
componentType: "Select",
132+
id: "sel",
133+
property: "value",
134+
value: [11],
135+
},
136+
{
137+
componentType: "Select",
138+
id: "sel",
139+
property: "value",
140+
value: [12],
141+
},
142+
]);
143+
});
144+
108145
it("should fire 'value' property with object options", () => {
109146
const { recordedEvents, onChange } = createChangeHandler();
110147
render(

chartlets.js/packages/lib/src/plugins/mui/Select.tsx

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type SelectOption =
1616

1717
interface SelectState extends ComponentState {
1818
options?: SelectOption[];
19+
multiple?: boolean;
1920
}
2021

2122
interface SelectProps extends ComponentProps, SelectState {}
@@ -30,14 +31,19 @@ export function Select({
3031
style,
3132
tooltip,
3233
label,
34+
multiple = false,
3335
onChange,
3436
}: SelectProps) {
35-
const handleChange = (event: SelectChangeEvent) => {
37+
const handleChange = (event: SelectChangeEvent<unknown>) => {
3638
if (id) {
37-
let newValue: string | number = event.target.value;
38-
if (typeof value == "number") {
39-
newValue = Number.parseInt(newValue);
39+
let newValue: string | number | (string | number)[] = multiple
40+
? (event.target.value as (string | number)[])
41+
: (event.target.value as string | number);
42+
43+
if (!multiple && typeof value === "number") {
44+
newValue = Number.parseInt(newValue as string);
4045
}
46+
4147
onChange({
4248
componentType: type,
4349
id: id,
@@ -54,24 +60,26 @@ export function Select({
5460
labelId={`${id}-label`}
5561
id={id}
5662
name={name}
57-
value={`${value}`}
63+
value={value}
5864
disabled={disabled}
59-
onChange={handleChange}
60-
>
65+
multiple={multiple}
66+
onChange={handleChange}>
6167
{Array.isArray(options) &&
62-
options.map(normalizeSelectOption).map(([value, text], index) => (
63-
<MuiMenuItem key={index} value={value}>
64-
{text}
65-
</MuiMenuItem>
66-
))}
68+
options
69+
.map(normalizeSelectOption)
70+
.map(([optionValue, optionLabel], index) => (
71+
<MuiMenuItem key={index} value={optionValue}>
72+
{optionLabel}
73+
</MuiMenuItem>
74+
))}
6775
</MuiSelect>
6876
</MuiFormControl>
6977
</Tooltip>
7078
);
7179
}
7280

7381
function normalizeSelectOption(
74-
option: SelectOption,
82+
option: SelectOption
7583
): [string | number, string] {
7684
if (isString(option)) {
7785
return [option, option];

chartlets.py/CHANGES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## Version 0.1.5 (in development)
2+
3+
* Add `multiple` property for `Select` component to enable the
4+
of multiple elements.
5+
6+
17
## Version 0.1.4 (from 2025/03/06)
28

39
* New (MUI) components
@@ -39,6 +45,7 @@
3945
- `Switch`
4046
- `Slider`
4147
- `Tabs` and `Tab`
48+
4249

4350
## Version 0.0.29 (from 2024/11/26)
4451

chartlets.py/chartlets/component.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class Component(ABC):
1919
name: str | None = None
2020
"""HTML `name` attribute. Optional."""
2121

22-
value: bool | int | float | str | None = None
22+
value: bool | int | float | str | list[bool | int | float | str] | None = None
2323
"""HTML `value` attribute. Required for specific components."""
2424

2525
style: dict[str, Any] | None = None

chartlets.py/chartlets/components/select.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ class Select(Component):
1313
"""Select components are used for collecting user provided
1414
information from a list of options."""
1515

16+
multiple: bool | None = None
17+
"""Allows for multiple selection in Select Menu. If `true`, value
18+
must be an array.
19+
"""
20+
1621
options: list[SelectOption] = field(default_factory=list)
1722
"""The options given as a list of number or text values or a list
1823
of (value, label) pairs.

chartlets.py/demo/my_extension/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from .my_panel_4 import panel as my_panel_4
66
from .my_panel_5 import panel as my_panel_5
77
from .my_panel_6 import panel as my_panel_6
8+
from .my_panel_7 import panel as my_panel_7
9+
810

911
ext = Extension(__name__)
1012
ext.add(my_panel_1)
@@ -13,3 +15,4 @@
1315
ext.add(my_panel_4)
1416
ext.add(my_panel_5)
1517
ext.add(my_panel_6)
18+
ext.add(my_panel_7)
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import altair as alt
2+
import pandas as pd
3+
from chartlets import Component, Input, State, Output
4+
from chartlets.components import VegaChart, Box, Select, Typography
5+
6+
from server.context import Context
7+
from server.panel import Panel
8+
9+
10+
panel = Panel(__name__, title="Panel G")
11+
12+
13+
@panel.layout(State("@app", "selectedDatasetId"))
14+
def render_panel(
15+
ctx: Context,
16+
selected_dataset_id: str = "",
17+
) -> Component:
18+
dataset = ctx.datasets.get(selected_dataset_id)
19+
variable_names, selected_var_names = get_variable_names(dataset)
20+
21+
select = Select(
22+
id="selected_variable_name",
23+
value=[],
24+
label="Variable",
25+
options=[(v, v) for v in variable_names],
26+
style={"flexGrow": 0, "minWidth": 120},
27+
multiple=True,
28+
tooltip="Select the variables of the test dataset",
29+
)
30+
control_group = Box(
31+
style={
32+
"display": "flex",
33+
"flexDirection": "row",
34+
"padding": 4,
35+
"justifyContent": "center",
36+
"gap": 4,
37+
},
38+
children=[select],
39+
)
40+
41+
text = update_info_text(ctx, selected_dataset_id)
42+
info_text = Typography(id="info_text", children=text)
43+
44+
return Box(
45+
style={
46+
"display": "flex",
47+
"flexDirection": "column",
48+
"width": "100%",
49+
"height": "100%",
50+
},
51+
children=[info_text, control_group],
52+
)
53+
54+
55+
def get_variable_names(
56+
dataset: pd.DataFrame,
57+
prev_var_name: str | None = None,
58+
) -> tuple[list[str], list[str]]:
59+
"""Get the variable names and the selected variable name
60+
for the given dataset and previously selected variable name.
61+
"""
62+
63+
if dataset is not None:
64+
var_names = [v for v in dataset.keys() if v != "x"]
65+
else:
66+
var_names = []
67+
68+
if prev_var_name and prev_var_name in var_names:
69+
var_name = prev_var_name
70+
elif var_names:
71+
var_name = var_names[0]
72+
else:
73+
var_name = ""
74+
75+
return var_names, var_name
76+
77+
78+
@panel.callback(
79+
Input("@app", "selectedDatasetId"),
80+
Input("selected_variable_name", "value"),
81+
Output("info_text", "children"),
82+
)
83+
def update_info_text(
84+
ctx: Context,
85+
dataset_id: str = "",
86+
selected_var_names: list[str] | None = None,
87+
) -> list[str]:
88+
89+
if selected_var_names is not None:
90+
text = ", ".join(map(str, selected_var_names))
91+
return [f"The dataset is {dataset_id} and the selected variables are: {text}"]
92+
else:
93+
return [f"The dataset is {dataset_id} and no variables are selected."]

0 commit comments

Comments
 (0)