Skip to content

Commit 43bcdb6

Browse files
authored
Plotting Improvements - Series color scale and color picker options (#1903)
### version 7.2.0 *Released*: 9 December 2025 - Chart builder updates for series color scale options - ChartColorInputs for single color geomOptions and series specific color and shape value map - ChartConfig measuresOptions to store per series mapping object - Hide and show color options based on selected chart type and measures - ColorPickerInput update to use fixed position by default and new DEFAULT_COLORS constant - Add LetterIcon for use with "Auto" set series values
1 parent 9af227b commit 43bcdb6

18 files changed

Lines changed: 1262 additions & 123 deletions

packages/components/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/components/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@labkey/components",
3-
"version": "7.1.1",
3+
"version": "7.2.0",
44
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
55
"sideEffects": false,
66
"files": [

packages/components/releaseNotes/components.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
# @labkey/components
22
Components, models, actions, and utility functions for LabKey applications and pages
33

4+
### version 7.2.0
5+
*Released*: 9 December 2025
6+
- Chart builder updates for series color scale options
7+
- ChartColorInputs for single color geomOptions and series specific color and shape value map
8+
- ChartConfig measuresOptions to store per series mapping object
9+
- Hide and show color options based on selected chart type and measures
10+
- ColorPickerInput update to use fixed position by default and new DEFAULT_COLORS constant
11+
- Add LetterIcon for use with "Auto" set series values
12+
413
### version 7.1.1
514
*Released*: 8 December 2025
615
- Sample Amount/Units polish: part 2

packages/components/src/internal/components/chart/ChartBuilderModal.test.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -117,16 +117,9 @@ describe('ChartBuilderModal', () => {
117117
expect(document.querySelectorAll('.chart-settings')).toHaveLength(1);
118118
expect(document.querySelectorAll('.chart-builder-modal__chart-preview')).toHaveLength(1);
119119
expect(document.querySelector('.modal-title').textContent).toBe(isNew ? 'Create Chart' : 'Edit Chart');
120-
expect(document.querySelectorAll('.btn')).toHaveLength(canDelete ? 3 : 2);
120+
expect(document.querySelectorAll('.btn:not(.color-picker__button)')).toHaveLength(canDelete ? 3 : 2);
121121
expect(document.querySelectorAll('.alert')).toHaveLength(0);
122-
123-
// TODO update this part of jest test
124-
// hidden chart types are filtered out
125-
// const chartTypeItems = document.querySelectorAll('.chart-builder-type');
126-
// expect(chartTypeItems).toHaveLength(3);
127-
// expect(chartTypeItems[0].textContent).toBe('Bar');
128-
// expect(chartTypeItems[1].textContent).toBe('Scatter');
129-
// expect(chartTypeItems[2].textContent).toBe('Line');
122+
expect(document.querySelectorAll('.chart-settings__chart-type')).toHaveLength(isNew ? 1 : 0);
130123

131124
expect(document.querySelectorAll('input[name="name"]')).toHaveLength(1);
132125
expect(document.querySelectorAll('input[name="shared"]')).toHaveLength(canShare ? 1 : 0);
@@ -291,7 +284,7 @@ describe('ChartBuilderModal', () => {
291284

292285
// click delete button and verify confirm text / buttons
293286
await userEvent.click(document.querySelector('.btn-danger'));
294-
const btnItems = document.querySelectorAll('.btn');
287+
const btnItems = document.querySelectorAll('.btn:not(.color-picker__button)');
295288
expect(btnItems).toHaveLength(2);
296289
expect(btnItems[0].textContent).toBe('Cancel');
297290
expect(btnItems[1].textContent).toBe('Delete');

packages/components/src/internal/components/chart/ChartBuilderModal.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import { RequiresModelAndActions } from '../../../public/QueryModel/withQueryMod
1414
import { useServerContext } from '../base/ServerContext';
1515
import { hasPermissions } from '../base/models/User';
1616

17-
import { Alert } from '../base/Alert';
1817
import { FormButtons } from '../../FormButtons';
1918

2019
import { getContainerFilterForFolder } from '../../query/api';
@@ -354,13 +353,13 @@ export const ChartBuilderModal: FC<ChartBuilderModalProps> = memo(({ actions, mo
354353
onCancel={onCancel}
355354
title={savedChartModel ? 'Edit Chart' : 'Create Chart'}
356355
>
357-
{error && <Alert>{error}</Alert>}
358356
<ChartSettingsPanel
359357
allowInherit={allowInherit}
360358
canShare={canShare}
361359
chartConfig={chartConfig}
362360
chartModel={chartModel}
363361
chartType={selectedType}
362+
error={error}
364363
isNew={savedChartModel !== undefined}
365364
model={model}
366365
setChartConfig={setChartConfig}
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import React from 'react';
2+
import { render } from '@testing-library/react';
3+
import { ChartConfig } from './models';
4+
import { LABKEY_VIS } from '../../constants';
5+
6+
import { ChartColorInputs, SeriesOptionRenderer, ShapeOptionRenderer, showColorOption } from './ChartColorInputs';
7+
import { makeTestQueryModel } from '../../../public/QueryModel/testUtils';
8+
import { SchemaQuery } from '../../../public/SchemaQuery';
9+
10+
LABKEY_VIS = {
11+
Scale: {
12+
ShapeMap: { circle: jest.fn },
13+
},
14+
};
15+
16+
describe('showColorOption', () => {
17+
test('boxFillColor', () => {
18+
expect(showColorOption({ renderType: 'bar_chart' } as ChartConfig, 'boxFillColor')).toBe(true);
19+
expect(showColorOption({ renderType: 'box_plot' } as ChartConfig, 'boxFillColor')).toBe(true);
20+
expect(showColorOption({ renderType: 'line_plot' } as ChartConfig, 'boxFillColor')).toBe(false);
21+
expect(showColorOption({ renderType: 'scatter_plot' } as ChartConfig, 'boxFillColor')).toBe(false);
22+
expect(showColorOption({ renderType: 'pie_chart' } as ChartConfig, 'boxFillColor')).toBe(false);
23+
24+
expect(
25+
showColorOption(
26+
{ renderType: 'bar_chart', measures: { xSub: { name: 'test' } } } as ChartConfig,
27+
'boxFillColor'
28+
)
29+
).toBe(false);
30+
});
31+
32+
test('colorPaletteScale', () => {
33+
expect(showColorOption({ renderType: 'bar_chart' } as ChartConfig, 'colorPaletteScale')).toBe(false);
34+
expect(showColorOption({ renderType: 'box_plot' } as ChartConfig, 'colorPaletteScale')).toBe(false);
35+
expect(showColorOption({ renderType: 'line_plot' } as ChartConfig, 'colorPaletteScale')).toBe(false);
36+
expect(showColorOption({ renderType: 'scatter_plot' } as ChartConfig, 'colorPaletteScale')).toBe(false);
37+
expect(showColorOption({ renderType: 'pie_chart' } as ChartConfig, 'colorPaletteScale')).toBe(true);
38+
39+
expect(
40+
showColorOption(
41+
{ renderType: 'line_plot', measures: { series: { name: 'test' } } } as ChartConfig,
42+
'colorPaletteScale'
43+
)
44+
).toBe(true);
45+
expect(
46+
showColorOption(
47+
{ renderType: 'bar_chart', measures: { xSub: { name: 'test' } } } as ChartConfig,
48+
'colorPaletteScale'
49+
)
50+
).toBe(true);
51+
expect(
52+
showColorOption(
53+
{ renderType: 'box_plot', measures: { color: { name: 'test' } } } as ChartConfig,
54+
'colorPaletteScale'
55+
)
56+
).toBe(true);
57+
expect(
58+
showColorOption(
59+
{ renderType: 'scatter_plot', measures: { color: { name: 'test' } } } as ChartConfig,
60+
'colorPaletteScale'
61+
)
62+
).toBe(true);
63+
});
64+
65+
test('lineColor', () => {
66+
expect(showColorOption({ renderType: 'bar_chart' } as ChartConfig, 'lineColor')).toBe(true);
67+
expect(showColorOption({ renderType: 'box_plot' } as ChartConfig, 'lineColor')).toBe(true);
68+
expect(showColorOption({ renderType: 'line_plot' } as ChartConfig, 'lineColor')).toBe(false);
69+
expect(showColorOption({ renderType: 'scatter_plot' } as ChartConfig, 'lineColor')).toBe(false);
70+
expect(showColorOption({ renderType: 'pie_chart' } as ChartConfig, 'lineColor')).toBe(false);
71+
72+
expect(
73+
showColorOption(
74+
{ renderType: 'bar_chart', measures: { xSub: { name: 'test' } } } as ChartConfig,
75+
'lineColor'
76+
)
77+
).toBe(false);
78+
});
79+
80+
test('pointFillColor', () => {
81+
expect(showColorOption({ renderType: 'bar_chart' } as ChartConfig, 'pointFillColor')).toBe(false);
82+
expect(showColorOption({ renderType: 'box_plot' } as ChartConfig, 'pointFillColor')).toBe(true);
83+
expect(showColorOption({ renderType: 'line_plot' } as ChartConfig, 'pointFillColor')).toBe(true);
84+
expect(showColorOption({ renderType: 'scatter_plot' } as ChartConfig, 'pointFillColor')).toBe(true);
85+
expect(showColorOption({ renderType: 'pie_chart' } as ChartConfig, 'pointFillColor')).toBe(false);
86+
87+
expect(
88+
showColorOption(
89+
{ renderType: 'line_plot', measures: { series: { name: 'test' } } } as ChartConfig,
90+
'pointFillColor'
91+
)
92+
).toBe(false);
93+
expect(
94+
showColorOption(
95+
{ renderType: 'box_plot', measures: { color: { name: 'test' } } } as ChartConfig,
96+
'pointFillColor'
97+
)
98+
).toBe(false);
99+
expect(
100+
showColorOption(
101+
{ renderType: 'scatter_plot', measures: { color: { name: 'test' } } } as ChartConfig,
102+
'pointFillColor'
103+
)
104+
).toBe(false);
105+
});
106+
});
107+
108+
describe('ShapeOptionRenderer', () => {
109+
test('isValueRenderer false', () => {
110+
render(<ShapeOptionRenderer isValueRenderer={false} name="circle" />);
111+
expect(document.querySelectorAll('.chart-builder-type-option')).toHaveLength(1);
112+
expect(document.querySelectorAll('.chart-builder-type-option--value')).toHaveLength(0);
113+
});
114+
115+
test('isValueRenderer true', () => {
116+
render(<ShapeOptionRenderer isValueRenderer name="circle" />);
117+
expect(document.querySelectorAll('.chart-builder-type-option')).toHaveLength(1);
118+
expect(document.querySelectorAll('.chart-builder-type-option--value')).toHaveLength(1);
119+
});
120+
});
121+
122+
describe('SeriesOptionRenderer', () => {
123+
test('isValueRenderer false', () => {
124+
render(<SeriesOptionRenderer isValueRenderer={false} name="series1" seriesOptionMap={{}} />);
125+
expect(document.querySelectorAll('.chart-builder-type-option')).toHaveLength(1);
126+
expect(document.querySelectorAll('.chart-builder-type-option--value')).toHaveLength(0);
127+
});
128+
129+
test('isValueRenderer true', () => {
130+
render(<SeriesOptionRenderer isValueRenderer name="series1" seriesOptionMap={{}} />);
131+
expect(document.querySelectorAll('.chart-builder-type-option')).toHaveLength(1);
132+
expect(document.querySelectorAll('.chart-builder-type-option--value')).toHaveLength(1);
133+
});
134+
135+
test('without seriesOptionMap value', () => {
136+
render(<SeriesOptionRenderer isValueRenderer name="series1" seriesOptionMap={{}} />);
137+
expect(document.querySelector('.chart-builder-type-option').textContent).toBe('A series1');
138+
expect(document.querySelectorAll('.color-icon__chip-small')).toHaveLength(0);
139+
expect(document.querySelectorAll('i')).toHaveLength(0);
140+
expect(document.querySelectorAll('.letter-icon')).toHaveLength(1);
141+
});
142+
143+
test('with seriesOptionMap value', () => {
144+
render(<SeriesOptionRenderer isValueRenderer name="series1" seriesOptionMap={{ series1: { color: 'red' } }} />);
145+
expect(document.querySelector('.chart-builder-type-option').textContent).toBe(' series1');
146+
expect(document.querySelectorAll('.color-icon__chip-small')).toHaveLength(1);
147+
expect(document.querySelectorAll('i')).toHaveLength(1);
148+
expect(document.querySelector('i').getAttribute('style')).toBe('background-color: red;');
149+
expect(document.querySelectorAll('.letter-icon')).toHaveLength(0);
150+
});
151+
});
152+
153+
describe('ChartColorInputs', () => {
154+
const model = makeTestQueryModel(new SchemaQuery('schema', 'query'), undefined, [], 0);
155+
156+
test('default bar chart', () => {
157+
render(
158+
<ChartColorInputs
159+
chartConfig={{ renderType: 'bar_chart', geomOptions: {} } as ChartConfig}
160+
model={model}
161+
setChartConfig={jest.fn()}
162+
/>
163+
);
164+
expect(document.querySelectorAll('.row')).toHaveLength(1);
165+
expect(document.querySelectorAll('.color-picker')).toHaveLength(2);
166+
expect(document.querySelectorAll('.select-input')).toHaveLength(0);
167+
});
168+
169+
test('default pie chart', () => {
170+
render(
171+
<ChartColorInputs
172+
chartConfig={{ renderType: 'pie_chart', geomOptions: {} } as ChartConfig}
173+
model={model}
174+
setChartConfig={jest.fn()}
175+
/>
176+
);
177+
expect(document.querySelectorAll('.row')).toHaveLength(2);
178+
expect(document.querySelectorAll('.color-picker')).toHaveLength(0);
179+
expect(document.querySelectorAll('.select-input')).toHaveLength(1);
180+
});
181+
182+
test('default box plot', () => {
183+
render(
184+
<ChartColorInputs
185+
chartConfig={{ renderType: 'box_plot', geomOptions: {} } as ChartConfig}
186+
model={model}
187+
setChartConfig={jest.fn()}
188+
/>
189+
);
190+
expect(document.querySelectorAll('.row')).toHaveLength(1);
191+
expect(document.querySelectorAll('.color-picker')).toHaveLength(3);
192+
expect(document.querySelectorAll('.select-input')).toHaveLength(0);
193+
});
194+
195+
test('default scatter plot', () => {
196+
render(
197+
<ChartColorInputs
198+
chartConfig={{ renderType: 'scatter_plot', geomOptions: {} } as ChartConfig}
199+
model={model}
200+
setChartConfig={jest.fn()}
201+
/>
202+
);
203+
expect(document.querySelectorAll('.row')).toHaveLength(1);
204+
expect(document.querySelectorAll('.color-picker')).toHaveLength(1);
205+
expect(document.querySelectorAll('.select-input')).toHaveLength(0);
206+
});
207+
208+
test('scatter plot with color', () => {
209+
render(
210+
<ChartColorInputs
211+
chartConfig={
212+
{
213+
renderType: 'scatter_plot',
214+
geomOptions: {},
215+
measures: { color: { name: 'test' } },
216+
} as ChartConfig
217+
}
218+
model={model}
219+
setChartConfig={jest.fn()}
220+
/>
221+
);
222+
expect(document.querySelectorAll('.row')).toHaveLength(2);
223+
expect(document.querySelectorAll('.color-picker')).toHaveLength(0);
224+
expect(document.querySelectorAll('.select-input')).toHaveLength(1);
225+
});
226+
227+
test('default line plot', () => {
228+
render(
229+
<ChartColorInputs
230+
chartConfig={{ renderType: 'line_plot', geomOptions: {} } as ChartConfig}
231+
model={model}
232+
setChartConfig={jest.fn()}
233+
/>
234+
);
235+
expect(document.querySelectorAll('.row')).toHaveLength(1);
236+
expect(document.querySelectorAll('.color-picker')).toHaveLength(1);
237+
expect(document.querySelectorAll('.select-input')).toHaveLength(0);
238+
});
239+
240+
test('line plot with series', () => {
241+
render(
242+
<ChartColorInputs
243+
chartConfig={
244+
{
245+
renderType: 'line_plot',
246+
geomOptions: {},
247+
measures: { series: { name: 'test' } },
248+
} as ChartConfig
249+
}
250+
model={model}
251+
setChartConfig={jest.fn()}
252+
/>
253+
);
254+
expect(document.querySelectorAll('.row')).toHaveLength(2);
255+
expect(document.querySelectorAll('.color-picker')).toHaveLength(0);
256+
expect(document.querySelectorAll('.select-input')).toHaveLength(1);
257+
});
258+
});

0 commit comments

Comments
 (0)