Skip to content

Commit 0951e0c

Browse files
authored
Plotting Improvements - Series line type scale and UI (#1906)
### version 7.5.0 *Released*: 22 December 2025 - Chart builder updates for per-series line type option - Chart builder option for show/hide data points for line chart type
1 parent 32fa827 commit 0951e0c

10 files changed

Lines changed: 207 additions & 41 deletions

File tree

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.4.0",
3+
"version": "7.5.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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# @labkey/components
22
Components, models, actions, and utility functions for LabKey applications and pages
33

4+
### version 7.5.0
5+
*Released*: 22 December 2025
6+
- Chart builder updates for per-series line type option
7+
- Chart builder option for show/hide data points for line chart type
8+
49
### version 7.4.0
510
*Released*: 22 December 2025
611
- Add support for moving jobs

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020

2121
import { ChartBuilderModal, getChartBuilderQueryConfig, getChartRenderMsg } from './ChartBuilderModal';
2222
import { MAX_POINT_DISPLAY, MAX_ROWS_PREVIEW } from './constants';
23-
import { ChartConfig, ChartQueryConfig, ChartTypeInfo, GenericChartModel, VisualizationConfigModel } from './models';
23+
import { ChartConfig, ChartQueryConfig, ChartTypeInfo, GenericChartModel } from './models';
2424
import { deepCopyChartConfig } from './utils';
2525

2626
const BAR_CHART_TYPE = {
@@ -272,7 +272,7 @@ describe('ChartBuilderModal', () => {
272272
await userEvent.click(typeDropdown);
273273
const lineOption = screen.getByText('Line');
274274
await userEvent.click(lineOption);
275-
expect(document.querySelectorAll('input')).toHaveLength(17);
275+
expect(document.querySelectorAll('input')).toHaveLength(19);
276276
LINE_PLOT_TYPE.fields.forEach(field => {
277277
if (field.name !== 'trendline') {
278278
expect(document.querySelectorAll(`input[name="${field.name}"]`)).toHaveLength(1);
@@ -421,7 +421,7 @@ describe('ChartBuilderModal', () => {
421421
);
422422

423423
validate(false, true, true);
424-
expect(document.querySelectorAll('input')).toHaveLength(17);
424+
expect(document.querySelectorAll('input')).toHaveLength(19);
425425
expect(document.querySelector('input[name=x]').getAttribute('value')).toBe('field1');
426426
expect(document.querySelector('input[name=y]').getAttribute('value')).toBe('field2');
427427
expect(document.querySelectorAll('input[name=aggregate-method]')).toHaveLength(0);
@@ -459,7 +459,7 @@ describe('ChartBuilderModal', () => {
459459
);
460460

461461
validate(false, true, true);
462-
expect(document.querySelectorAll('input')).toHaveLength(17);
462+
expect(document.querySelectorAll('input')).toHaveLength(19);
463463
expect(document.querySelector('input[name=x]').getAttribute('value')).toBe('field1');
464464
expect(document.querySelector('input[name=y]').getAttribute('value')).toBe('field2');
465465
expect(document.querySelectorAll('input[name=aggregate-method]')).toHaveLength(0);

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

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ import { render } from '@testing-library/react';
33
import { ChartConfig } from './models';
44
import { LABKEY_VIS } from '../../constants';
55

6-
import { ChartColorInputs, SeriesOptionRenderer, ShapeOptionRenderer, showColorOption } from './ChartColorInputs';
6+
import {
7+
ChartColorInputs,
8+
LineTypeOptionRenderer,
9+
SeriesOptionRenderer,
10+
ShapeOptionRenderer,
11+
showColorOption,
12+
} from './ChartColorInputs';
713
import { makeTestQueryModel } from '../../../public/QueryModel/testUtils';
814
import { SchemaQuery } from '../../../public/SchemaQuery';
915

@@ -150,6 +156,34 @@ describe('SeriesOptionRenderer', () => {
150156
});
151157
});
152158

159+
describe('LineTypeOptionRenderer', () => {
160+
test('isValueRenderer false', () => {
161+
render(<LineTypeOptionRenderer isValueRenderer={false} label="Solid" value="" />);
162+
expect(document.querySelectorAll('.chart-builder-type-option')).toHaveLength(1);
163+
expect(document.querySelectorAll('.chart-builder-type-option--value')).toHaveLength(0);
164+
expect(document.querySelector('svg path').getAttribute('stroke-dasharray')).toBe(null);
165+
});
166+
167+
test('isValueRenderer true', () => {
168+
render(<LineTypeOptionRenderer isValueRenderer label="Solid" value="" />);
169+
expect(document.querySelectorAll('.chart-builder-type-option')).toHaveLength(1);
170+
expect(document.querySelectorAll('.chart-builder-type-option--value')).toHaveLength(1);
171+
expect(document.querySelector('svg path').getAttribute('stroke-dasharray')).toBe(null);
172+
});
173+
174+
test('dashed line type', () => {
175+
render(<LineTypeOptionRenderer isValueRenderer label="Dashed" value="dashed" />);
176+
expect(document.querySelector('svg path').getAttribute('stroke-dasharray')).toBe('6,6');
177+
expect(document.querySelector('svg path').getAttribute('stroke-linecap')).toBe(null);
178+
});
179+
180+
test('dotted line type', () => {
181+
render(<LineTypeOptionRenderer isValueRenderer label="Dotted" value="dotted" />);
182+
expect(document.querySelector('svg path').getAttribute('stroke-dasharray')).toBe('0.1,6');
183+
expect(document.querySelector('svg path').getAttribute('stroke-linecap')).toBe('round');
184+
});
185+
});
186+
153187
describe('ChartColorInputs', () => {
154188
const model = makeTestQueryModel(new SchemaQuery('schema', 'query'), undefined, [], 0);
155189

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

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import classNames from 'classnames';
33
import { Utils } from '@labkey/api';
44
import { ChartConfig, ChartConfigSetter, MeasureOption } from './models';
55
import { ColorPickerInput } from '../forms/input/ColorPickerInput';
6-
import { COLOR_OPTIONS_PER_TYPE, COLOR_PALETTE_OPTIONS, SHAPE_OPTIONS } from './constants';
6+
import { COLOR_OPTIONS_PER_TYPE, COLOR_PALETTE_OPTIONS, LINE_TYPE_OPTIONS, SHAPE_OPTIONS } from './constants';
77
import { SelectInput } from '../forms/input/SelectInput';
88
import { selectDistinctRows } from '../../query/api';
99
import { QueryModel } from '../../../public/QueryModel/QueryModel';
@@ -83,6 +83,42 @@ function shapeValueRenderer(option) {
8383
return <ShapeOptionRenderer isValueRenderer name={option.data.value} />;
8484
}
8585

86+
interface LineTypeOptionRendererProps {
87+
isValueRenderer: boolean;
88+
label: string;
89+
value: string;
90+
}
91+
92+
// export for jest testing
93+
export const LineTypeOptionRenderer: FC<LineTypeOptionRendererProps> = memo(({ label, value, isValueRenderer }) => {
94+
const className = classNames('chart-builder-type-option', { 'chart-builder-type-option--value': isValueRenderer });
95+
const strokeValue = value === 'dashed' ? '6,6' : value === 'dotted' ? '0.1,6' : undefined;
96+
const strokeLineCap = value === 'dotted' ? 'round' : undefined;
97+
return (
98+
<span className={className} data-series-linetype={label}>
99+
<svg height="10" width="25">
100+
<path
101+
d="M 5 5 H 25"
102+
fill="none"
103+
stroke="#000000"
104+
strokeDasharray={strokeValue}
105+
strokeLinecap={strokeLineCap}
106+
strokeWidth="3"
107+
/>
108+
</svg>
109+
</span>
110+
);
111+
});
112+
LineTypeOptionRenderer.displayName = 'LineTypeOptionRenderer';
113+
114+
function lineTypeOptionRenderer(option) {
115+
return <LineTypeOptionRenderer isValueRenderer={false} label={option.data.label} value={option.data.value} />;
116+
}
117+
118+
function lineTypeValueRenderer(option) {
119+
return <LineTypeOptionRenderer isValueRenderer label={option.data.label} value={option.data.value} />;
120+
}
121+
86122
interface SeriesOptionRendererProps {
87123
isValueRenderer: boolean;
88124
name: string;
@@ -97,7 +133,7 @@ export const SeriesOptionRenderer: FC<SeriesOptionRendererProps> = memo(
97133
'chart-builder-type-option--value': isValueRenderer,
98134
});
99135
return (
100-
<span className={className} data-series-shape={name}>
136+
<span className={className} data-series-option={name}>
101137
{value && (
102138
<>
103139
<ColorIcon asSquare cls="color-icon__chip-small" value={value} /> {name}
@@ -280,11 +316,15 @@ const SeriesLineStyleInput: FC<SeriesLineStyleInputProps> = memo(({ chartConfig,
280316

281317
if (chartConfig.measures?.series) {
282318
try {
319+
const seriesColumn = model.getColumn(chartConfig.measures?.series.fieldKey);
283320
const response = await selectDistinctRows({
284321
schemaName: model.schemaQuery.schemaName,
285322
queryName: model.schemaQuery.queryName,
286323
viewName: model.schemaQuery.viewName,
287-
column: chartConfig.measures?.series.fieldKey,
324+
// if the series measure is a lookup, we need to get distinct values from the display column
325+
column:
326+
chartConfig.measures?.series.fieldKey +
327+
(seriesColumn?.isLookup() ? '/' + seriesColumn.lookup.displayColumnFieldKey : ''),
288328
});
289329

290330
// map response.values to SelectOption format
@@ -346,6 +386,17 @@ const SeriesLineStyleInput: FC<SeriesLineStyleInputProps> = memo(({ chartConfig,
346386
[onSeriesOptionChange, selectedSeries]
347387
);
348388

389+
const onSeriesLineTypeChange = useCallback(
390+
(_: never, value: string) => {
391+
onSeriesOptionChange(selectedSeries, 'lineType', value);
392+
},
393+
[onSeriesOptionChange, selectedSeries]
394+
);
395+
396+
const onSeriesLineTypeRemove = useCallback(() => {
397+
onSeriesOptionChange(selectedSeries, 'lineType', undefined);
398+
}, [onSeriesOptionChange, selectedSeries]);
399+
349400
const onSeriesShapeChange = useCallback(
350401
(_: never, value: string) => {
351402
onSeriesOptionChange(selectedSeries, 'shape', value);
@@ -383,9 +434,9 @@ const SeriesLineStyleInput: FC<SeriesLineStyleInputProps> = memo(({ chartConfig,
383434
</div>
384435
</div>
385436
{selectedSeries && (
386-
<div className="row">
387-
<div className="col-xs-4">
388-
<div>Color</div>
437+
<div className="chart-color-inputs">
438+
<div className="chart-color-input">
439+
<label className="label-weight-normal">Color</label>
389440
<ColorPickerInput
390441
allowRemove
391442
name="seriesColor"
@@ -394,22 +445,42 @@ const SeriesLineStyleInput: FC<SeriesLineStyleInputProps> = memo(({ chartConfig,
394445
value={seriesOptionMap[selectedSeries]?.color}
395446
/>
396447
</div>
397-
<div className="col-xs-8">
398-
<div>Shape</div>
448+
{!chartConfig.geomOptions?.hideDataPoints && (
449+
<div className="chart-color-input">
450+
<label className="label-weight-normal">Shape</label>
451+
<SelectInput
452+
clearable={false}
453+
containerClass="inline-block"
454+
inputClass=""
455+
menuPlacement="top"
456+
onChange={onSeriesShapeChange}
457+
optionRenderer={shapeOptionRenderer}
458+
options={SHAPE_OPTIONS}
459+
placeholder="Auto"
460+
value={seriesOptionMap[selectedSeries]?.shape}
461+
valueRenderer={shapeValueRenderer}
462+
/>
463+
{seriesOptionMap[selectedSeries]?.shape && (
464+
<RemoveEntityButton labelClass="color-picker__remove" onClick={onSeriesShapeRemove} />
465+
)}
466+
</div>
467+
)}
468+
<div className="chart-color-input">
469+
<label className="label-weight-normal">Line Type</label>
399470
<SelectInput
400471
clearable={false}
401472
containerClass="inline-block"
402473
inputClass=""
403474
menuPlacement="top"
404-
onChange={onSeriesShapeChange}
405-
optionRenderer={shapeOptionRenderer}
406-
options={SHAPE_OPTIONS}
475+
onChange={onSeriesLineTypeChange}
476+
optionRenderer={lineTypeOptionRenderer}
477+
options={LINE_TYPE_OPTIONS}
407478
placeholder="Auto"
408-
value={seriesOptionMap[selectedSeries]?.shape}
409-
valueRenderer={shapeValueRenderer}
479+
value={seriesOptionMap[selectedSeries]?.lineType}
480+
valueRenderer={lineTypeValueRenderer}
410481
/>
411-
{seriesOptionMap[selectedSeries]?.shape && (
412-
<RemoveEntityButton labelClass="color-picker__remove" onClick={onSeriesShapeRemove} />
482+
{seriesOptionMap[selectedSeries]?.lineType !== undefined && (
483+
<RemoveEntityButton labelClass="color-picker__remove" onClick={onSeriesLineTypeRemove} />
413484
)}
414485
</div>
415486
</div>

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

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,6 @@ export const ChartSettingsPanel: FC<Props> = memo(props => {
280280
setChartConfig,
281281
setChartModel,
282282
} = props;
283-
const legendPos = chartConfig.legendPos;
284283
const showTrendline = hasTrendline(chartType);
285284
const fields = chartType.fields.filter(f => f.name !== 'trendline');
286285

@@ -330,17 +329,41 @@ export const ChartSettingsPanel: FC<Props> = memo(props => {
330329

331330
const legendOptions = useMemo(() => {
332331
return [
333-
{ label: 'Right', selected: !legendPos || legendPos === 'right', value: 'right' },
334-
{ label: 'Bottom', selected: legendPos === 'bottom', value: 'bottom' },
332+
{ label: 'Right', selected: !chartConfig.legendPos || chartConfig.legendPos === 'right', value: 'right' },
333+
{ label: 'Bottom', selected: chartConfig.legendPos === 'bottom', value: 'bottom' },
335334
];
336-
}, [legendPos]);
335+
}, [chartConfig.legendPos]);
337336

338337
const onLegendPosChange = useCallback(
339338
value => setChartConfig(current => ({ ...current, legendPos: value })),
340339
[setChartConfig]
341340
);
342341

342+
const hideDataPointsOptions = useMemo(
343+
() => [
344+
{
345+
label: 'Show',
346+
selected:
347+
chartConfig.geomOptions.hideDataPoints === undefined ||
348+
chartConfig.geomOptions.hideDataPoints === false,
349+
value: 'false',
350+
},
351+
{ label: 'Hide', selected: chartConfig.geomOptions.hideDataPoints === true, value: 'true' },
352+
],
353+
[chartConfig.geomOptions.hideDataPoints]
354+
);
355+
356+
const onHideDataPointsChange = useCallback(
357+
(value: string) =>
358+
setChartConfig(current => ({
359+
...current,
360+
geomOptions: { ...current.geomOptions, hideDataPoints: value === 'true' },
361+
})),
362+
[setChartConfig]
363+
);
364+
343365
const showLegendPos = chartType.name !== 'pie_chart';
366+
const showPointsOption = chartType.name === 'line_plot';
344367

345368
return (
346369
<div className="chart-settings">
@@ -410,16 +433,33 @@ export const ChartSettingsPanel: FC<Props> = memo(props => {
410433
<SizeInputs height={chartConfig.height} setChartConfig={setChartConfig} width={chartConfig.width} />
411434

412435
{showLegendPos && (
413-
<div className="chart-settings__legend-pos">
414-
<label>Legend Position</label>
415-
416-
<div className="chart-settings__legend-pos-values">
417-
<RadioGroupInput
418-
formsy={false}
419-
name="legendPos"
420-
onValueChange={onLegendPosChange}
421-
options={legendOptions}
422-
/>
436+
<div className="chart-settings__radio-group form-group row">
437+
<div className="col-xs-12">
438+
<label>Legend Position</label>
439+
<div className="chart-settings__radio-group-values">
440+
<RadioGroupInput
441+
formsy={false}
442+
name="legendPos"
443+
onValueChange={onLegendPosChange}
444+
options={legendOptions}
445+
/>
446+
</div>
447+
</div>
448+
</div>
449+
)}
450+
451+
{showPointsOption && (
452+
<div className="chart-settings__radio-group form-group row">
453+
<div className="col-xs-12">
454+
<label>Points</label>
455+
<div className="chart-settings__radio-group-values">
456+
<RadioGroupInput
457+
formsy={false}
458+
name="hideDataPoints"
459+
onValueChange={onHideDataPointsChange}
460+
options={hideDataPointsOptions}
461+
/>
462+
</div>
423463
</div>
424464
</div>
425465
)}

packages/components/src/internal/components/chart/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ export const SHAPE_OPTIONS = [
3535
{ label: 'Cross', value: 'x' },
3636
];
3737

38+
export const LINE_TYPE_OPTIONS = [
39+
{ label: 'Solid', value: '' },
40+
{ label: 'Dashed', value: 'dashed' },
41+
{ label: 'Dotted', value: 'dotted' },
42+
];
43+
3844
export const COLOR_OPTIONS_PER_TYPE = {
3945
boxFillColor: ['bar_chart', 'box_plot'],
4046
colorPaletteScale: ['bar_chart', 'box_plot', 'line_plot', 'scatter_plot', 'pie_chart'],

0 commit comments

Comments
 (0)