Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f17713d
Chart builder updates for series line type option
cnathe Dec 11, 2025
cc83aaf
7.3.0-chartSeriesLineType.0
cnathe Dec 11, 2025
f7cb592
Merge remote-tracking branch 'origin/develop' into fb_chartSeriesLine…
cnathe Dec 15, 2025
171107d
7.3.2-chartSeriesLineType.0
cnathe Dec 15, 2025
381aff7
LineTypeOptionRenderer for solid/dashed/dotted
cnathe Dec 15, 2025
769a05f
Chart builder option for show/hide data points for line chart type
cnathe Dec 15, 2025
2c7a199
misc styling and layout/spacing updates for chart settings panel
cnathe Dec 15, 2025
7262c3f
7.3.2-chartSeriesLineType.1
cnathe Dec 15, 2025
11b1fdf
jest test fixes for ChartBuilderModal.test.tsx
cnathe Dec 15, 2025
a31b44b
update data attribute for SeriesOptionRenderer
cnathe Dec 16, 2025
9163fca
7.3.2-chartSeriesLineType.2
cnathe Dec 16, 2025
1bad69a
ChartColorInputs.tsx per series options to use <label>
cnathe Dec 16, 2025
95c4f31
7.3.2-chartSeriesLineType.3
cnathe Dec 16, 2025
f26c69c
linting
cnathe Dec 16, 2025
96e8d68
jest test updates for LineTypeOptionRenderer
cnathe Dec 16, 2025
e619c61
SeriesLineStyleInput select distinct series to account for lookup col…
cnathe Dec 16, 2025
a0120d8
7.3.2-chartSeriesLineType.4
cnathe Dec 16, 2025
cbe8e4b
CR feedback: pass stroke-dasharray value instead of dashed/dotted lab…
cnathe Dec 16, 2025
c3db526
just fix for LineTypeOptionRenderer refactor
cnathe Dec 16, 2025
f528b8e
7.3.2-chartSeriesLineType.5
cnathe Dec 16, 2025
dbeb39f
update LineTypeOptionRenderer data-series-linetype
cnathe Dec 16, 2025
1c5a212
7.3.2-chartSeriesLineType.6
cnathe Dec 16, 2025
100f435
ChartColorInputs line type dashed to use 6,6 and dotted to be 0.1,6 w…
cnathe Dec 18, 2025
35b3058
7.3.2-chartSeriesLineType.7
cnathe Dec 18, 2025
8c9deb5
Don't show per series shape selector if hide data points is selected
cnathe Dec 22, 2025
67a52b5
7.3.2-fb-chartSeriesLineType.0
cnathe Dec 22, 2025
c81c584
npm run lint-branch-fix
cnathe Dec 22, 2025
0f9681f
Merge remote-tracking branch 'origin/develop' into fb_chartSeriesLine…
cnathe Dec 22, 2025
fe691d0
Update release notes with version number and release date
cnathe Dec 22, 2025
9ca7970
7.5.0
cnathe Dec 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/components/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@labkey/components",
"version": "7.4.0",
"version": "7.5.0",
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
"sideEffects": false,
"files": [
Expand Down
5 changes: 5 additions & 0 deletions packages/components/releaseNotes/components.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# @labkey/components
Components, models, actions, and utility functions for LabKey applications and pages

### 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

### version 7.4.0
*Released*: 22 December 2025
- Add support for moving jobs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {

import { ChartBuilderModal, getChartBuilderQueryConfig, getChartRenderMsg } from './ChartBuilderModal';
import { MAX_POINT_DISPLAY, MAX_ROWS_PREVIEW } from './constants';
import { ChartConfig, ChartQueryConfig, ChartTypeInfo, GenericChartModel, VisualizationConfigModel } from './models';
import { ChartConfig, ChartQueryConfig, ChartTypeInfo, GenericChartModel } from './models';
import { deepCopyChartConfig } from './utils';

const BAR_CHART_TYPE = {
Expand Down Expand Up @@ -272,7 +272,7 @@ describe('ChartBuilderModal', () => {
await userEvent.click(typeDropdown);
const lineOption = screen.getByText('Line');
await userEvent.click(lineOption);
expect(document.querySelectorAll('input')).toHaveLength(17);
expect(document.querySelectorAll('input')).toHaveLength(19);
LINE_PLOT_TYPE.fields.forEach(field => {
if (field.name !== 'trendline') {
expect(document.querySelectorAll(`input[name="${field.name}"]`)).toHaveLength(1);
Expand Down Expand Up @@ -421,7 +421,7 @@ describe('ChartBuilderModal', () => {
);

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

validate(false, true, true);
expect(document.querySelectorAll('input')).toHaveLength(17);
expect(document.querySelectorAll('input')).toHaveLength(19);
expect(document.querySelector('input[name=x]').getAttribute('value')).toBe('field1');
expect(document.querySelector('input[name=y]').getAttribute('value')).toBe('field2');
expect(document.querySelectorAll('input[name=aggregate-method]')).toHaveLength(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import { render } from '@testing-library/react';
import { ChartConfig } from './models';
import { LABKEY_VIS } from '../../constants';

import { ChartColorInputs, SeriesOptionRenderer, ShapeOptionRenderer, showColorOption } from './ChartColorInputs';
import {
ChartColorInputs,
LineTypeOptionRenderer,
SeriesOptionRenderer,
ShapeOptionRenderer,
showColorOption,
} from './ChartColorInputs';
import { makeTestQueryModel } from '../../../public/QueryModel/testUtils';
import { SchemaQuery } from '../../../public/SchemaQuery';

Expand Down Expand Up @@ -150,6 +156,34 @@ describe('SeriesOptionRenderer', () => {
});
});

describe('LineTypeOptionRenderer', () => {
test('isValueRenderer false', () => {
render(<LineTypeOptionRenderer isValueRenderer={false} label="Solid" value="" />);
expect(document.querySelectorAll('.chart-builder-type-option')).toHaveLength(1);
expect(document.querySelectorAll('.chart-builder-type-option--value')).toHaveLength(0);
expect(document.querySelector('svg path').getAttribute('stroke-dasharray')).toBe(null);
});

test('isValueRenderer true', () => {
render(<LineTypeOptionRenderer isValueRenderer label="Solid" value="" />);
expect(document.querySelectorAll('.chart-builder-type-option')).toHaveLength(1);
expect(document.querySelectorAll('.chart-builder-type-option--value')).toHaveLength(1);
expect(document.querySelector('svg path').getAttribute('stroke-dasharray')).toBe(null);
});

test('dashed line type', () => {
render(<LineTypeOptionRenderer isValueRenderer label="Dashed" value="dashed" />);
expect(document.querySelector('svg path').getAttribute('stroke-dasharray')).toBe('6,6');
expect(document.querySelector('svg path').getAttribute('stroke-linecap')).toBe(null);
});

test('dotted line type', () => {
render(<LineTypeOptionRenderer isValueRenderer label="Dotted" value="dotted" />);
expect(document.querySelector('svg path').getAttribute('stroke-dasharray')).toBe('0.1,6');
expect(document.querySelector('svg path').getAttribute('stroke-linecap')).toBe('round');
});
});

describe('ChartColorInputs', () => {
const model = makeTestQueryModel(new SchemaQuery('schema', 'query'), undefined, [], 0);

Expand Down
101 changes: 86 additions & 15 deletions packages/components/src/internal/components/chart/ChartColorInputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import classNames from 'classnames';
import { Utils } from '@labkey/api';
import { ChartConfig, ChartConfigSetter, MeasureOption } from './models';
import { ColorPickerInput } from '../forms/input/ColorPickerInput';
import { COLOR_OPTIONS_PER_TYPE, COLOR_PALETTE_OPTIONS, SHAPE_OPTIONS } from './constants';
import { COLOR_OPTIONS_PER_TYPE, COLOR_PALETTE_OPTIONS, LINE_TYPE_OPTIONS, SHAPE_OPTIONS } from './constants';
import { SelectInput } from '../forms/input/SelectInput';
import { selectDistinctRows } from '../../query/api';
import { QueryModel } from '../../../public/QueryModel/QueryModel';
Expand Down Expand Up @@ -83,6 +83,42 @@ function shapeValueRenderer(option) {
return <ShapeOptionRenderer isValueRenderer name={option.data.value} />;
}

interface LineTypeOptionRendererProps {
isValueRenderer: boolean;
label: string;
value: string;
}

// export for jest testing
export const LineTypeOptionRenderer: FC<LineTypeOptionRendererProps> = memo(({ label, value, isValueRenderer }) => {
const className = classNames('chart-builder-type-option', { 'chart-builder-type-option--value': isValueRenderer });
const strokeValue = value === 'dashed' ? '6,6' : value === 'dotted' ? '0.1,6' : undefined;
const strokeLineCap = value === 'dotted' ? 'round' : undefined;
return (
<span className={className} data-series-linetype={label}>
<svg height="10" width="25">
<path
d="M 5 5 H 25"
fill="none"
stroke="#000000"
strokeDasharray={strokeValue}
strokeLinecap={strokeLineCap}
strokeWidth="3"
/>
</svg>
</span>
);
});
LineTypeOptionRenderer.displayName = 'LineTypeOptionRenderer';

function lineTypeOptionRenderer(option) {
return <LineTypeOptionRenderer isValueRenderer={false} label={option.data.label} value={option.data.value} />;
}

function lineTypeValueRenderer(option) {
return <LineTypeOptionRenderer isValueRenderer label={option.data.label} value={option.data.value} />;
}

interface SeriesOptionRendererProps {
isValueRenderer: boolean;
name: string;
Expand All @@ -97,7 +133,7 @@ export const SeriesOptionRenderer: FC<SeriesOptionRendererProps> = memo(
'chart-builder-type-option--value': isValueRenderer,
});
return (
<span className={className} data-series-shape={name}>
<span className={className} data-series-option={name}>
{value && (
<>
<ColorIcon asSquare cls="color-icon__chip-small" value={value} /> {name}
Expand Down Expand Up @@ -280,11 +316,15 @@ const SeriesLineStyleInput: FC<SeriesLineStyleInputProps> = memo(({ chartConfig,

if (chartConfig.measures?.series) {
try {
const seriesColumn = model.getColumn(chartConfig.measures?.series.fieldKey);
const response = await selectDistinctRows({
schemaName: model.schemaQuery.schemaName,
queryName: model.schemaQuery.queryName,
viewName: model.schemaQuery.viewName,
column: chartConfig.measures?.series.fieldKey,
// if the series measure is a lookup, we need to get distinct values from the display column
column:
chartConfig.measures?.series.fieldKey +
(seriesColumn?.isLookup() ? '/' + seriesColumn.lookup.displayColumnFieldKey : ''),
});

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

const onSeriesLineTypeChange = useCallback(
(_: never, value: string) => {
onSeriesOptionChange(selectedSeries, 'lineType', value);
},
[onSeriesOptionChange, selectedSeries]
);

const onSeriesLineTypeRemove = useCallback(() => {
onSeriesOptionChange(selectedSeries, 'lineType', undefined);
}, [onSeriesOptionChange, selectedSeries]);

const onSeriesShapeChange = useCallback(
(_: never, value: string) => {
onSeriesOptionChange(selectedSeries, 'shape', value);
Expand Down Expand Up @@ -383,9 +434,9 @@ const SeriesLineStyleInput: FC<SeriesLineStyleInputProps> = memo(({ chartConfig,
</div>
</div>
{selectedSeries && (
<div className="row">
<div className="col-xs-4">
<div>Color</div>
<div className="chart-color-inputs">
<div className="chart-color-input">
<label className="label-weight-normal">Color</label>
<ColorPickerInput
allowRemove
name="seriesColor"
Expand All @@ -394,22 +445,42 @@ const SeriesLineStyleInput: FC<SeriesLineStyleInputProps> = memo(({ chartConfig,
value={seriesOptionMap[selectedSeries]?.color}
/>
</div>
<div className="col-xs-8">
<div>Shape</div>
{!chartConfig.geomOptions?.hideDataPoints && (
<div className="chart-color-input">
<label className="label-weight-normal">Shape</label>
<SelectInput
clearable={false}
containerClass="inline-block"
inputClass=""
menuPlacement="top"
onChange={onSeriesShapeChange}
optionRenderer={shapeOptionRenderer}
options={SHAPE_OPTIONS}
placeholder="Auto"
value={seriesOptionMap[selectedSeries]?.shape}
valueRenderer={shapeValueRenderer}
/>
{seriesOptionMap[selectedSeries]?.shape && (
<RemoveEntityButton labelClass="color-picker__remove" onClick={onSeriesShapeRemove} />
)}
</div>
)}
<div className="chart-color-input">
<label className="label-weight-normal">Line Type</label>
<SelectInput
clearable={false}
containerClass="inline-block"
inputClass=""
menuPlacement="top"
onChange={onSeriesShapeChange}
optionRenderer={shapeOptionRenderer}
options={SHAPE_OPTIONS}
onChange={onSeriesLineTypeChange}
optionRenderer={lineTypeOptionRenderer}
options={LINE_TYPE_OPTIONS}
placeholder="Auto"
value={seriesOptionMap[selectedSeries]?.shape}
valueRenderer={shapeValueRenderer}
value={seriesOptionMap[selectedSeries]?.lineType}
valueRenderer={lineTypeValueRenderer}
/>
{seriesOptionMap[selectedSeries]?.shape && (
<RemoveEntityButton labelClass="color-picker__remove" onClick={onSeriesShapeRemove} />
{seriesOptionMap[selectedSeries]?.lineType !== undefined && (
<RemoveEntityButton labelClass="color-picker__remove" onClick={onSeriesLineTypeRemove} />
)}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,6 @@ export const ChartSettingsPanel: FC<Props> = memo(props => {
setChartConfig,
setChartModel,
} = props;
const legendPos = chartConfig.legendPos;
const showTrendline = hasTrendline(chartType);
const fields = chartType.fields.filter(f => f.name !== 'trendline');

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

const legendOptions = useMemo(() => {
return [
{ label: 'Right', selected: !legendPos || legendPos === 'right', value: 'right' },
{ label: 'Bottom', selected: legendPos === 'bottom', value: 'bottom' },
{ label: 'Right', selected: !chartConfig.legendPos || chartConfig.legendPos === 'right', value: 'right' },
{ label: 'Bottom', selected: chartConfig.legendPos === 'bottom', value: 'bottom' },
];
}, [legendPos]);
}, [chartConfig.legendPos]);

const onLegendPosChange = useCallback(
value => setChartConfig(current => ({ ...current, legendPos: value })),
[setChartConfig]
);

const hideDataPointsOptions = useMemo(
() => [
{
label: 'Show',
selected:
chartConfig.geomOptions.hideDataPoints === undefined ||
chartConfig.geomOptions.hideDataPoints === false,
value: 'false',
},
{ label: 'Hide', selected: chartConfig.geomOptions.hideDataPoints === true, value: 'true' },
],
[chartConfig.geomOptions.hideDataPoints]
);

const onHideDataPointsChange = useCallback(
(value: string) =>
setChartConfig(current => ({
...current,
geomOptions: { ...current.geomOptions, hideDataPoints: value === 'true' },
})),
[setChartConfig]
);

const showLegendPos = chartType.name !== 'pie_chart';
const showPointsOption = chartType.name === 'line_plot';

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

{showLegendPos && (
<div className="chart-settings__legend-pos">
<label>Legend Position</label>

<div className="chart-settings__legend-pos-values">
<RadioGroupInput
formsy={false}
name="legendPos"
onValueChange={onLegendPosChange}
options={legendOptions}
/>
<div className="chart-settings__radio-group form-group row">
<div className="col-xs-12">
<label>Legend Position</label>
<div className="chart-settings__radio-group-values">
<RadioGroupInput
formsy={false}
name="legendPos"
onValueChange={onLegendPosChange}
options={legendOptions}
/>
</div>
</div>
</div>
)}

{showPointsOption && (
<div className="chart-settings__radio-group form-group row">
<div className="col-xs-12">
<label>Points</label>
<div className="chart-settings__radio-group-values">
<RadioGroupInput
formsy={false}
name="hideDataPoints"
onValueChange={onHideDataPointsChange}
options={hideDataPointsOptions}
/>
</div>
</div>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ export const SHAPE_OPTIONS = [
{ label: 'Cross', value: 'x' },
];

export const LINE_TYPE_OPTIONS = [
{ label: 'Solid', value: '' },
{ label: 'Dashed', value: 'dashed' },
{ label: 'Dotted', value: 'dotted' },
];

export const COLOR_OPTIONS_PER_TYPE = {
boxFillColor: ['bar_chart', 'box_plot'],
colorPaletteScale: ['bar_chart', 'box_plot', 'line_plot', 'scatter_plot', 'pie_chart'],
Expand Down
Loading