diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 783022bee8..e26368360d 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.4.0", + "version": "7.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.4.0", + "version": "7.5.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index 89f51e1006..2b42d0392b 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -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": [ diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index b18da88e8f..111de213b5 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -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 diff --git a/packages/components/src/internal/components/chart/ChartBuilderModal.test.tsx b/packages/components/src/internal/components/chart/ChartBuilderModal.test.tsx index 17d689796c..031644ac82 100644 --- a/packages/components/src/internal/components/chart/ChartBuilderModal.test.tsx +++ b/packages/components/src/internal/components/chart/ChartBuilderModal.test.tsx @@ -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 = { @@ -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); @@ -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); @@ -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); diff --git a/packages/components/src/internal/components/chart/ChartColorInputs.test.tsx b/packages/components/src/internal/components/chart/ChartColorInputs.test.tsx index 6da5532218..066b44f62b 100644 --- a/packages/components/src/internal/components/chart/ChartColorInputs.test.tsx +++ b/packages/components/src/internal/components/chart/ChartColorInputs.test.tsx @@ -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'; @@ -150,6 +156,34 @@ describe('SeriesOptionRenderer', () => { }); }); +describe('LineTypeOptionRenderer', () => { + test('isValueRenderer false', () => { + render(); + 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(); + 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(); + 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(); + 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); diff --git a/packages/components/src/internal/components/chart/ChartColorInputs.tsx b/packages/components/src/internal/components/chart/ChartColorInputs.tsx index 6ad57a187a..757a3c8cf7 100644 --- a/packages/components/src/internal/components/chart/ChartColorInputs.tsx +++ b/packages/components/src/internal/components/chart/ChartColorInputs.tsx @@ -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'; @@ -83,6 +83,42 @@ function shapeValueRenderer(option) { return ; } +interface LineTypeOptionRendererProps { + isValueRenderer: boolean; + label: string; + value: string; +} + +// export for jest testing +export const LineTypeOptionRenderer: FC = 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 ( + + + + + + ); +}); +LineTypeOptionRenderer.displayName = 'LineTypeOptionRenderer'; + +function lineTypeOptionRenderer(option) { + return ; +} + +function lineTypeValueRenderer(option) { + return ; +} + interface SeriesOptionRendererProps { isValueRenderer: boolean; name: string; @@ -97,7 +133,7 @@ export const SeriesOptionRenderer: FC = memo( 'chart-builder-type-option--value': isValueRenderer, }); return ( - + {value && ( <> {name} @@ -280,11 +316,15 @@ const SeriesLineStyleInput: FC = 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 @@ -346,6 +386,17 @@ const SeriesLineStyleInput: FC = 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); @@ -383,9 +434,9 @@ const SeriesLineStyleInput: FC = memo(({ chartConfig, {selectedSeries && ( -
-
-
Color
+
+
+ = memo(({ chartConfig, value={seriesOptionMap[selectedSeries]?.color} />
-
-
Shape
+ {!chartConfig.geomOptions?.hideDataPoints && ( +
+ + + {seriesOptionMap[selectedSeries]?.shape && ( + + )} +
+ )} +
+ - {seriesOptionMap[selectedSeries]?.shape && ( - + {seriesOptionMap[selectedSeries]?.lineType !== undefined && ( + )}
diff --git a/packages/components/src/internal/components/chart/ChartSettingsPanel.tsx b/packages/components/src/internal/components/chart/ChartSettingsPanel.tsx index aadf27baa9..dd2364b07a 100644 --- a/packages/components/src/internal/components/chart/ChartSettingsPanel.tsx +++ b/packages/components/src/internal/components/chart/ChartSettingsPanel.tsx @@ -280,7 +280,6 @@ export const ChartSettingsPanel: FC = memo(props => { setChartConfig, setChartModel, } = props; - const legendPos = chartConfig.legendPos; const showTrendline = hasTrendline(chartType); const fields = chartType.fields.filter(f => f.name !== 'trendline'); @@ -330,17 +329,41 @@ export const ChartSettingsPanel: FC = 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 (
@@ -410,16 +433,33 @@ export const ChartSettingsPanel: FC = memo(props => { {showLegendPos && ( -
- - -
- +
+
+ +
+ +
+
+
+ )} + + {showPointsOption && ( +
+
+ +
+ +
)} diff --git a/packages/components/src/internal/components/chart/constants.ts b/packages/components/src/internal/components/chart/constants.ts index eeb55ea396..dc4718fce7 100644 --- a/packages/components/src/internal/components/chart/constants.ts +++ b/packages/components/src/internal/components/chart/constants.ts @@ -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'], diff --git a/packages/components/src/internal/components/chart/models.ts b/packages/components/src/internal/components/chart/models.ts index a315673b75..ce5d00a1d4 100644 --- a/packages/components/src/internal/components/chart/models.ts +++ b/packages/components/src/internal/components/chart/models.ts @@ -9,8 +9,8 @@ export interface ChartLabels { export interface MeasureOption { color?: string; + lineType?: string; shape?: string; - // lineType?: string; } export interface ChartConfig { diff --git a/packages/components/src/theme/charts.scss b/packages/components/src/theme/charts.scss index 41375a5898..ea070e47da 100644 --- a/packages/components/src/theme/charts.scss +++ b/packages/components/src/theme/charts.scss @@ -219,21 +219,31 @@ overflow-y: auto; } +.chart-color-inputs { + display: flex; +} +.chart-color-input { + flex: 1; +} +.chart-color-input label { + display: block; +} + .chart-builder-modal__chart-preview h4, .chart-settings h4 { margin: 0 0 8px; } -.chart-settings__legend-pos-values { +.chart-settings__radio-group-values { display: flex; gap: 8px; } -.chart-settings__legend-pos-values .radio-input-wrapper input { +.chart-settings__radio-group-values .radio-input-wrapper input { margin-right: 4px; } -.chart-settings__legend-pos-values .radioinput-label { +.chart-settings__radio-group-values .radioinput-label { cursor: pointer; }