diff --git a/src/Chart.tsx b/src/Chart.tsx index a4225a665..a501d81f8 100644 --- a/src/Chart.tsx +++ b/src/Chart.tsx @@ -13,7 +13,13 @@ import { FC, forwardRef, useEffect, useMemo, useRef, useState } from 'react'; import { EmptyState } from '@components/EmptyState'; import { LoadingState } from '@components/LoadingState'; -import { DEFAULT_BACKGROUND_COLOR, DEFAULT_COLOR_SCHEME, DEFAULT_LINE_TYPES, DEFAULT_LOCALE } from '@constants'; +import { + DEFAULT_BACKGROUND_COLOR, + DEFAULT_COLOR_SCHEME, + DEFAULT_LINE_TYPES, + DEFAULT_LOCALE, + MARK_ID, +} from '@constants'; import useChartHeight from '@hooks/useChartHeight'; import useChartImperativeHandle from '@hooks/useChartImperativeHandle'; import useChartWidth from '@hooks/useChartWidth'; @@ -51,6 +57,7 @@ export const Chart = forwardRef( height = 300, hiddenSeries = [], highlightedSeries, + idKey = MARK_ID, lineTypes = DEFAULT_LINE_TYPES as LineType[], lineWidths = ['M'], loading, @@ -124,6 +131,7 @@ export const Chart = forwardRef( debug, hiddenSeries, highlightedSeries, + idKey, lineTypes, lineWidths, locale, diff --git a/src/RscChart.tsx b/src/RscChart.tsx index 17fdad4cd..9251f55ee 100644 --- a/src/RscChart.tsx +++ b/src/RscChart.tsx @@ -16,7 +16,6 @@ import { FILTERED_TABLE, GROUP_DATA, LEGEND_TOOLTIP_DELAY, - MARK_ID, SELECTED_ITEM, SELECTED_SERIES, SERIES_ID, @@ -98,6 +97,7 @@ export const RscChart = forwardRef( chartWidth, UNSAFE_vegaSpec, chartId, + idKey, ...props }, forwardedRef @@ -191,7 +191,7 @@ export const RscChart = forwardRef( const tooltip = tooltips.find((t) => t.name === value[COMPONENT_NAME]); if (tooltip?.callback && !('index' in value)) { if (controlledHoveredIdSignal) { - chartView.current?.signal(controlledHoveredIdSignal.name, value?.[MARK_ID] ?? null); + chartView.current?.signal(controlledHoveredIdSignal.name, value?.[idKey] ?? null); } if (controlledHoveredGroupSignal) { const key = Object.keys(value).find((k) => k.endsWith('_highlightGroupId')); @@ -221,11 +221,11 @@ export const RscChart = forwardRef( if (legendIsToggleable) { signals.hiddenSeries = legendHiddenSeries; } - signals[SELECTED_ITEM] = selectedData?.[MARK_ID] ?? null; + signals[SELECTED_ITEM] = selectedData?.[idKey] ?? null; signals[SELECTED_SERIES] = selectedData?.[SERIES_ID] ?? null; return signals; - }, [colorScheme, legendHiddenSeries, legendIsToggleable]); + }, [colorScheme, idKey, legendHiddenSeries, legendIsToggleable]); return ( <> @@ -272,6 +272,7 @@ export const RscChart = forwardRef( view.signal('hiddenSeries', legendHiddenSeries); } setSelectedSignals({ + idKey, selectedData: selectedData.current, view, }); diff --git a/src/specBuilder/area/areaSpecBuilder.test.ts b/src/specBuilder/area/areaSpecBuilder.test.ts index f5d3aa05c..6f3b69136 100644 --- a/src/specBuilder/area/areaSpecBuilder.test.ts +++ b/src/specBuilder/area/areaSpecBuilder.test.ts @@ -47,6 +47,7 @@ const defaultAreaProps: AreaSpecProps = { colorScheme: DEFAULT_COLOR_SCHEME, color: DEFAULT_COLOR, dimension: DEFAULT_TIME_DIMENSION, + idKey: MARK_ID, index: 0, markType: 'area', metric: DEFAULT_METRIC, @@ -167,11 +168,11 @@ const defaultPointScale = { describe('areaSpecBuilder', () => { describe('addArea()', () => { test('should add area', () => { - expect(addArea(startingSpec, {})).toStrictEqual(defaultSpec); + expect(addArea(startingSpec, { idKey: MARK_ID })).toStrictEqual(defaultSpec); }); test('metricStart defined but valueEnd not defined, should default to value', () => { - expect(addArea(startingSpec, { metricStart: 'test' })).toStrictEqual(defaultSpec); + expect(addArea(startingSpec, { idKey: MARK_ID, metricStart: 'test' })).toStrictEqual(defaultSpec); }); }); diff --git a/src/specBuilder/area/areaSpecBuilder.ts b/src/specBuilder/area/areaSpecBuilder.ts index 5f8468862..3e9296019 100644 --- a/src/specBuilder/area/areaSpecBuilder.ts +++ b/src/specBuilder/area/areaSpecBuilder.ts @@ -18,7 +18,6 @@ import { DEFAULT_METRIC, DEFAULT_TIME_DIMENSION, FILTERED_TABLE, - MARK_ID, SELECTED_ITEM, SELECTED_SERIES, } from '@constants'; @@ -44,7 +43,7 @@ import { addTimeTransform, getFilteredTableData, getTableData, getTransformSort import { addContinuousDimensionScale, addFieldToFacetScaleDomain, addMetricScale } from '../scale/scaleSpecBuilder'; import { getAreaMark, getX } from './areaUtils'; -export const addArea = produce( +export const addArea = produce( ( spec, { @@ -123,7 +122,7 @@ export const addData = produce((data, props) => { if (children.length) { const areaHasPopover = hasPopover(children); - data.push(getAreaHighlightedData(name, areaHasPopover, isHighlightedByGroup(props))); + data.push(getAreaHighlightedData(name, props.idKey, areaHasPopover, isHighlightedByGroup(props))); if (areaHasPopover) { data.push({ name: `${name}_selectedDataSeries`, @@ -140,12 +139,17 @@ export const addData = produce((data, props) => { addTooltipData(data, props, false); }); -export const getAreaHighlightedData = (name: string, hasPopover: boolean, hasGroupId: boolean): SourceData => { +export const getAreaHighlightedData = ( + name: string, + idKey: string, + hasPopover: boolean, + hasGroupId: boolean +): SourceData => { const highlightedExpr = hasGroupId ? `${name}_controlledHoveredGroup === datum.${name}_highlightGroupId` - : `${name}_controlledHoveredId === datum.${MARK_ID}`; + : `${name}_controlledHoveredId === datum.${idKey}`; const expr = hasPopover - ? `${SELECTED_ITEM} && ${SELECTED_ITEM} === datum.${MARK_ID} || !${SELECTED_ITEM} && ${highlightedExpr}` + ? `${SELECTED_ITEM} && ${SELECTED_ITEM} === datum.${idKey} || !${SELECTED_ITEM} && ${highlightedExpr}` : highlightedExpr; return { name: `${name}_highlightedData`, diff --git a/src/specBuilder/bar/barSpecBuilder.test.ts b/src/specBuilder/bar/barSpecBuilder.test.ts index f6f96fccd..365edecb3 100644 --- a/src/specBuilder/bar/barSpecBuilder.test.ts +++ b/src/specBuilder/bar/barSpecBuilder.test.ts @@ -233,7 +233,7 @@ const defaultSpec: Spec = { describe('barSpecBuilder', () => { describe('addBar()', () => { test('no props', () => { - expect(addBar(startingSpec, {})).toStrictEqual(defaultSpec); + expect(addBar(startingSpec, { idKey: MARK_ID })).toStrictEqual(defaultSpec); }); }); diff --git a/src/specBuilder/bar/barSpecBuilder.ts b/src/specBuilder/bar/barSpecBuilder.ts index 4fa0ac8e2..62d801e06 100644 --- a/src/specBuilder/bar/barSpecBuilder.ts +++ b/src/specBuilder/bar/barSpecBuilder.ts @@ -48,7 +48,7 @@ import { getDodgedMark } from './dodgedBarUtils'; import { getDodgedAndStackedBarMark, getStackedBarMarks } from './stackedBarUtils'; import { addTrellisScale, getTrellisGroupMark, isTrellised } from './trellisedBarUtils'; -export const addBar = produce( +export const addBar = produce( ( spec, { @@ -107,7 +107,7 @@ export const addBar = produce((signals, props) => { - const { children, name, paddingRatio, paddingOuter: barPaddingOuter } = props; + const { children, idKey, name, paddingRatio, paddingOuter: barPaddingOuter } = props; // We use this value to calculate ReferenceLine positions. const { paddingInner } = getBarPadding(paddingRatio, barPaddingOuter); signals.push(getGenericValueSignal('paddingInner', paddingInner)); @@ -115,7 +115,7 @@ export const addSignals = produce((signals, props) => if (!children.length) { return; } - addHighlightedItemSignalEvents(signals, name, 1, getTooltipProps(children)?.excludeDataKeys); + addHighlightedItemSignalEvents(signals, name, idKey, 1, getTooltipProps(children)?.excludeDataKeys); addTooltipSignals(signals, props); setTrendlineSignals(signals, props); }); diff --git a/src/specBuilder/bar/barTestUtils.ts b/src/specBuilder/bar/barTestUtils.ts index eaac3b405..c26eb4133 100644 --- a/src/specBuilder/bar/barTestUtils.ts +++ b/src/specBuilder/bar/barTestUtils.ts @@ -19,6 +19,7 @@ import { DEFAULT_METRIC, DEFAULT_SECONDARY_COLOR, FILTERED_TABLE, + MARK_ID, PADDING_RATIO, STACK_ID, TRELLIS_PADDING, @@ -34,6 +35,7 @@ export const defaultBarProps: BarSpecProps = { dimension: DEFAULT_CATEGORICAL_DIMENSION, dimensionScaleType: 'band', hasSquareCorners: false, + idKey: MARK_ID, index: 0, interactiveMarkName: 'bar0', lineType: { value: 'solid' }, diff --git a/src/specBuilder/bar/barUtils.ts b/src/specBuilder/bar/barUtils.ts index 5d2abfca4..45706c2c0 100644 --- a/src/specBuilder/bar/barUtils.ts +++ b/src/specBuilder/bar/barUtils.ts @@ -9,15 +9,7 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -import { - CORNER_RADIUS, - DISCRETE_PADDING, - FILTERED_TABLE, - MARK_ID, - SELECTED_GROUP, - SELECTED_ITEM, - STACK_ID, -} from '@constants'; +import { CORNER_RADIUS, DISCRETE_PADDING, FILTERED_TABLE, SELECTED_GROUP, SELECTED_ITEM, STACK_ID } from '@constants'; import { getPopovers } from '@specBuilder/chartPopover/chartPopoverUtils'; import { getColorProductionRule, @@ -247,7 +239,13 @@ export const getBarUpdateEncodings = (props: BarSpecProps): EncodeEntry => ({ strokeWidth: getStrokeWidth(props), }); -export const getStroke = ({ name, children, color, colorScheme }: BarSpecProps): ProductionRule => { +export const getStroke = ({ + name, + children, + color, + colorScheme, + idKey, +}: BarSpecProps): ProductionRule => { const defaultProductionRule = getColorProductionRule(color, colorScheme); if (!hasPopover(children)) { return [defaultProductionRule]; @@ -255,7 +253,7 @@ export const getStroke = ({ name, children, color, colorScheme }: BarSpecProps): return [ { - test: `(${SELECTED_ITEM} && ${SELECTED_ITEM} === datum.${MARK_ID}) || (${SELECTED_GROUP} && ${SELECTED_GROUP} === datum.${name}_selectedGroupId)`, + test: `(${SELECTED_ITEM} && ${SELECTED_ITEM} === datum.${idKey}) || (${SELECTED_GROUP} && ${SELECTED_GROUP} === datum.${name}_selectedGroupId)`, value: getColorValue('static-blue', colorScheme), }, defaultProductionRule, @@ -299,17 +297,17 @@ export const getDimensionSelectionRing = (props: BarSpecProps): RectMark => { }; }; -export const getStrokeDash = ({ children, lineType }: BarSpecProps): ProductionRule => { +export const getStrokeDash = ({ children, idKey, lineType }: BarSpecProps): ProductionRule => { const defaultProductionRule = getStrokeDashProductionRule(lineType); if (!hasPopover(children)) { return [defaultProductionRule]; } - return [{ test: `${SELECTED_ITEM} && ${SELECTED_ITEM} === datum.${MARK_ID}`, value: [] }, defaultProductionRule]; + return [{ test: `${SELECTED_ITEM} && ${SELECTED_ITEM} === datum.${idKey}`, value: [] }, defaultProductionRule]; }; export const getStrokeWidth = (props: BarSpecProps): ProductionRule => { - const { lineWidth, name } = props; + const { idKey, lineWidth, name } = props; const lineWidthValue = getLineWidthPixelsFromLineWidth(lineWidth); const defaultProductionRule = { value: lineWidthValue }; const popovers = getPopovers(props); @@ -323,7 +321,7 @@ export const getStrokeWidth = (props: BarSpecProps): ProductionRule buildOrder.get(a.type) - buildOrder.get(b.type)) .reduce((acc: Spec, cur) => { @@ -141,31 +144,31 @@ export function buildSpec({ switch (cur.type.displayName) { case Area.displayName: areaCount++; - return addArea(acc, { ...(cur as AreaElement).props, colorScheme, index: areaCount }); + return addArea(acc, { ...(cur as AreaElement).props, ...specProps, index: areaCount }); case Axis.displayName: axisCount++; - return addAxis(acc, { ...(cur as AxisElement).props, colorScheme, index: axisCount }); + return addAxis(acc, { ...(cur as AxisElement).props, ...specProps, index: axisCount }); case Bar.displayName: barCount++; - return addBar(acc, { ...(cur as BarElement).props, colorScheme, index: barCount }); + return addBar(acc, { ...(cur as BarElement).props, ...specProps, index: barCount }); case Donut.displayName: donutCount++; - return addDonut(acc, { ...(cur as DonutElement).props, colorScheme, index: donutCount }); + return addDonut(acc, { ...(cur as DonutElement).props, ...specProps, index: donutCount }); case Legend.displayName: legendCount++; return addLegend(acc, { ...(cur as LegendElement).props, - colorScheme, + ...specProps, index: legendCount, hiddenSeries, highlightedSeries, }); case Line.displayName: lineCount++; - return addLine(acc, { ...(cur as LineElement).props, colorScheme, index: lineCount }); + return addLine(acc, { ...(cur as LineElement).props, ...specProps, index: lineCount }); case Scatter.displayName: scatterCount++; - return addScatter(acc, { ...(cur as ScatterElement).props, colorScheme, index: scatterCount }); + return addScatter(acc, { ...(cur as ScatterElement).props, ...specProps, index: scatterCount }); case Title.displayName: // No title count. There can only be one title. return addTitle(acc, { ...(cur as TitleElement).props }); @@ -173,7 +176,8 @@ export function buildSpec({ // Do nothing and do not throw an error return acc; case Combo.displayName: - return addCombo(acc, { ...(cur as ComboElement).props }); + comboCount++; + return addCombo(acc, { ...(cur as ComboElement).props, ...specProps, index: comboCount }); default: console.error(`Invalid component type: ${cur.type.displayName} is not a supported child`); return acc; @@ -206,6 +210,7 @@ const initializeComponentCounts = () => { areaCount: -1, axisCount: -1, barCount: -1, + comboCount: -1, donutCount: -1, legendCount: -1, lineCount: -1, diff --git a/src/specBuilder/chartTooltip/chartTooltipUtils.ts b/src/specBuilder/chartTooltip/chartTooltipUtils.ts index 5cc005a80..d7de10e0b 100644 --- a/src/specBuilder/chartTooltip/chartTooltipUtils.ts +++ b/src/specBuilder/chartTooltip/chartTooltipUtils.ts @@ -17,7 +17,6 @@ import { HIGHLIGHTED_ITEM, HIGHLIGHT_CONTRAST_RATIO, INTERACTION_MODE, - MARK_ID, SERIES_ID, } from '@constants'; import { getFilteredTableData } from '@specBuilder/data/dataUtils'; @@ -197,7 +196,7 @@ export const addTooltipMarkOpacityRules = ( markProps: TooltipParentProps ) => { opacityRules.unshift({ - test: `${HIGHLIGHTED_ITEM} && ${HIGHLIGHTED_ITEM} !== datum.${MARK_ID}`, + test: `${HIGHLIGHTED_ITEM} && ${HIGHLIGHTED_ITEM} !== datum.${markProps.idKey}`, value: 1 / HIGHLIGHT_CONTRAST_RATIO, }); if (isHighlightedByGroup(markProps)) { diff --git a/src/specBuilder/combo/comboSpecBuilder.test.ts b/src/specBuilder/combo/comboSpecBuilder.test.ts index 85910bdf6..3864ac3a6 100644 --- a/src/specBuilder/combo/comboSpecBuilder.test.ts +++ b/src/specBuilder/combo/comboSpecBuilder.test.ts @@ -14,6 +14,7 @@ import { createElement } from 'react'; import { Axis } from '@components/Axis'; import { Bar } from '@components/Bar'; import { Line } from '@components/Line'; +import { MARK_ID } from '@constants'; import { addBar } from '@specBuilder/bar/barSpecBuilder'; import { addLine } from '@specBuilder/line/lineSpecBuilder'; @@ -37,6 +38,7 @@ describe('comboSpecBuilder', () => { addCombo( {}, { + idKey: MARK_ID, children: [ createElement(Bar, { metric: 'people', @@ -61,6 +63,7 @@ describe('comboSpecBuilder', () => { addCombo( {}, { + idKey: MARK_ID, children: [createElement(Axis)], } ); @@ -70,7 +73,7 @@ describe('comboSpecBuilder', () => { }); it('should do nothing if no children', () => { - addCombo({}, {}); + addCombo({}, { idKey: MARK_ID }); expect(addBar).not.toHaveBeenCalled(); expect(addLine).not.toHaveBeenCalled(); diff --git a/src/specBuilder/combo/comboSpecBuilder.ts b/src/specBuilder/combo/comboSpecBuilder.ts index f81932177..2d5e38f4f 100644 --- a/src/specBuilder/combo/comboSpecBuilder.ts +++ b/src/specBuilder/combo/comboSpecBuilder.ts @@ -19,10 +19,17 @@ import { Spec } from 'vega'; import { BarElement, ChartChildElement, ColorScheme, ComboChildElement, ComboProps, LineElement } from '../../types'; -export const addCombo = produce( +export const addCombo = produce( ( spec, - { children = [], colorScheme = DEFAULT_COLOR_SCHEME, index = 0, name, dimension = DEFAULT_TIME_DIMENSION } + { + children = [], + colorScheme = DEFAULT_COLOR_SCHEME, + idKey, + index = 0, + name, + dimension = DEFAULT_TIME_DIMENSION, + } ) => { const buildOrder = new Map(); buildOrder.set(Bar, 0); @@ -44,6 +51,7 @@ export const addCombo = produce( +export const addDonut = produce( ( spec, { @@ -132,8 +132,8 @@ export const addMarks = produce((marks, props) => { }); export const addSignals = produce((signals, props) => { - const { name, children } = props; + const { children, idKey, name } = props; signals.push(...getDonutSummarySignals(props)); if (!hasInteractiveChildren(children)) return; - addHighlightedItemSignalEvents(signals, name, 1, getTooltipProps(children)?.excludeDataKeys); + addHighlightedItemSignalEvents(signals, name, idKey, 1, getTooltipProps(children)?.excludeDataKeys); }); diff --git a/src/specBuilder/donut/donutTestUtils.ts b/src/specBuilder/donut/donutTestUtils.ts index 4a4fbece4..35132ba97 100644 --- a/src/specBuilder/donut/donutTestUtils.ts +++ b/src/specBuilder/donut/donutTestUtils.ts @@ -9,17 +9,20 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ +import { MARK_ID } from '@constants'; + import { DonutSpecProps } from '../../types'; export const defaultDonutProps: DonutSpecProps = { - index: 0, + children: [], + color: 'testColor', colorScheme: 'light', + holeRatio: 0.85, + idKey: MARK_ID, + index: 0, + isBoolean: false, + name: 'testName', markType: 'donut', metric: 'testMetric', startAngle: 0, - name: 'testName', - isBoolean: false, - color: 'testColor', - holeRatio: 0.85, - children: [], }; diff --git a/src/specBuilder/donut/donutUtils.ts b/src/specBuilder/donut/donutUtils.ts index 346746b05..0af27a4c9 100644 --- a/src/specBuilder/donut/donutUtils.ts +++ b/src/specBuilder/donut/donutUtils.ts @@ -9,7 +9,7 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -import { DONUT_RADIUS, FILTERED_TABLE, MARK_ID, SELECTED_ITEM } from '@constants'; +import { DONUT_RADIUS, FILTERED_TABLE, SELECTED_ITEM } from '@constants'; import { getColorProductionRule, getCursor, getMarkOpacity, getTooltip } from '@specBuilder/marks/markUtils'; import { getColorValue } from '@specBuilder/specUtils'; import { ArcMark } from 'vega'; @@ -17,7 +17,7 @@ import { ArcMark } from 'vega'; import { DonutSpecProps } from '../../types'; export const getArcMark = (props: DonutSpecProps): ArcMark => { - const { color, colorScheme, name, holeRatio, children } = props; + const { children, color, colorScheme, holeRatio, idKey, name } = props; return { type: 'arc', name, @@ -38,7 +38,7 @@ export const getArcMark = (props: DonutSpecProps): ArcMark => { outerRadius: { signal: DONUT_RADIUS }, opacity: getMarkOpacity(props), cursor: getCursor(children), - strokeWidth: [{ test: `${SELECTED_ITEM} === datum.${MARK_ID}`, value: 2 }, { value: 0 }], + strokeWidth: [{ test: `${SELECTED_ITEM} === datum.${idKey}`, value: 2 }, { value: 0 }], }, }, }; diff --git a/src/specBuilder/line/lineDataUtils.test.ts b/src/specBuilder/line/lineDataUtils.test.ts index c7c5d6f08..2cb011058 100644 --- a/src/specBuilder/line/lineDataUtils.test.ts +++ b/src/specBuilder/line/lineDataUtils.test.ts @@ -9,25 +9,28 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -import { FILTERED_TABLE, HIGHLIGHTED_GROUP, SELECTED_ITEM } from '@constants'; +import { FILTERED_TABLE, HIGHLIGHTED_GROUP, MARK_ID, SELECTED_ITEM } from '@constants'; import { FilterTransform } from 'vega'; import { getLineHighlightedData } from './lineDataUtils'; describe('getLineHighlightedData()', () => { test('should include select signal if hasPopover', () => { - const expr = (getLineHighlightedData('line0', FILTERED_TABLE, true, false).transform?.[0] as FilterTransform) - .expr; + const expr = ( + getLineHighlightedData('line0', MARK_ID, FILTERED_TABLE, true, false).transform?.[0] as FilterTransform + ).expr; expect(expr.includes(SELECTED_ITEM)).toBeTruthy(); }); test('should not include select signal if does not hasPopover', () => { - const expr = (getLineHighlightedData('line0', FILTERED_TABLE, false, false).transform?.[0] as FilterTransform) - .expr; + const expr = ( + getLineHighlightedData('line0', MARK_ID, FILTERED_TABLE, false, false).transform?.[0] as FilterTransform + ).expr; expect(expr.includes(SELECTED_ITEM)).toBeFalsy(); }); test('should use groupId if hadGroupId', () => { - const expr = (getLineHighlightedData('line0', FILTERED_TABLE, true, true).transform?.[0] as FilterTransform) - .expr; + const expr = ( + getLineHighlightedData('line0', MARK_ID, FILTERED_TABLE, true, true).transform?.[0] as FilterTransform + ).expr; expect(expr.includes(HIGHLIGHTED_GROUP)).toBeTruthy(); }); }); diff --git a/src/specBuilder/line/lineDataUtils.ts b/src/specBuilder/line/lineDataUtils.ts index 1b29d2a21..a21752002 100644 --- a/src/specBuilder/line/lineDataUtils.ts +++ b/src/specBuilder/line/lineDataUtils.ts @@ -9,7 +9,7 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -import { HIGHLIGHTED_GROUP, HIGHLIGHTED_ITEM, MARK_ID, SELECTED_ITEM } from '@constants'; +import { HIGHLIGHTED_GROUP, HIGHLIGHTED_ITEM, SELECTED_ITEM } from '@constants'; import { SourceData } from 'vega'; /** @@ -20,15 +20,16 @@ import { SourceData } from 'vega'; */ export const getLineHighlightedData = ( name: string, + idKey: string, source: string, hasPopover: boolean, hasGroupId: boolean ): SourceData => { const highlightedExpr = hasGroupId ? `${HIGHLIGHTED_GROUP} === datum.${name}_highlightGroupId` - : `${HIGHLIGHTED_ITEM} === datum.${MARK_ID}`; + : `${HIGHLIGHTED_ITEM} === datum.${idKey}`; const expr = hasPopover - ? `${SELECTED_ITEM} && ${SELECTED_ITEM} === datum.${MARK_ID} || !${SELECTED_ITEM} && ${highlightedExpr}` + ? `${SELECTED_ITEM} && ${SELECTED_ITEM} === datum.${idKey} || !${SELECTED_ITEM} && ${highlightedExpr}` : highlightedExpr; return { name: `${name}_highlightedData`, @@ -51,17 +52,24 @@ export const getLineHighlightedData = ( * @param isMethodLast * @returns */ -export const getLineStaticPointData = (name: string, staticPoint: string | undefined, source: string, isSparkline: boolean | undefined, isMethodLast: boolean | undefined): SourceData => { - const expr = isSparkline && isMethodLast ? - 'datum === data(\'table\')[data(\'table\').length - 1]' : - `datum.${staticPoint} === true`; +export const getLineStaticPointData = ( + name: string, + staticPoint: string | undefined, + source: string, + isSparkline: boolean | undefined, + isMethodLast: boolean | undefined +): SourceData => { + const expr = + isSparkline && isMethodLast + ? "datum === data('table')[data('table').length - 1]" + : `datum.${staticPoint} === true`; return { name: `${name}_staticPointData`, source, transform: [ { type: 'filter', - expr + expr, }, ], }; diff --git a/src/specBuilder/line/linePointUtils.ts b/src/specBuilder/line/linePointUtils.ts index eb1fc461a..86e7257f4 100644 --- a/src/specBuilder/line/linePointUtils.ts +++ b/src/specBuilder/line/linePointUtils.ts @@ -13,7 +13,6 @@ import { BACKGROUND_COLOR, DEFAULT_SYMBOL_SIZE, DEFAULT_SYMBOL_STROKE_WIDTH, - MARK_ID, SELECTED_GROUP, SELECTED_ITEM, } from '@constants'; @@ -32,8 +31,8 @@ import { LineSpecProps, ProductionRuleTests } from '../../types'; import { LineMarkProps } from './lineUtils'; const staticPointTest = (staticPoint: string) => `datum.${staticPoint} && datum.${staticPoint} === true`; -const getSelectedTest = (name: string) => - `(${SELECTED_ITEM} && ${SELECTED_ITEM} === datum.${MARK_ID}) || (${SELECTED_GROUP} && ${SELECTED_GROUP} === datum.${name}_selectedGroupId)`; +const getSelectedTest = (name: string, idKey: string) => + `(${SELECTED_ITEM} && ${SELECTED_ITEM} === datum.${idKey}) || (${SELECTED_GROUP} && ${SELECTED_GROUP} === datum.${name}_selectedGroupId)`; /** * Gets the point mark for static points on a line chart. @@ -49,7 +48,7 @@ export const getLineStaticPoint = ({ scaleType, dimension, isSparkline, - pointSize = 125 + pointSize = 125, }: LineSpecProps): SymbolMark => { return { name: `${name}_staticPoints`, @@ -58,7 +57,7 @@ export const getLineStaticPoint = ({ interactive: false, encode: { enter: { - size: { value: pointSize } , + size: { value: pointSize }, fill: isSparkline ? { signal: BACKGROUND_COLOR } : getColorProductionRule(color, colorScheme), stroke: isSparkline ? getColorProductionRule(color, colorScheme) : { signal: BACKGROUND_COLOR }, y: getYProductionRule(metricAxis, metric), @@ -174,14 +173,15 @@ export const getSecondaryHighlightPoint = ( * @returns fill rule */ export const getHighlightPointFill = ({ - name, children, color, colorScheme, + idKey, + name, staticPoint, }: LineMarkProps): ProductionRuleTests => { const fillRules: ProductionRuleTests = []; - const selectedTest = getSelectedTest(name); + const selectedTest = getSelectedTest(name, idKey); if (staticPoint) { fillRules.push({ test: staticPointTest(staticPoint), ...getColorProductionRule(color, colorScheme) }); @@ -198,14 +198,15 @@ export const getHighlightPointFill = ({ * @returns stroke rule */ export const getHighlightPointStroke = ({ - name, children, color, colorScheme, + idKey, + name, staticPoint, }: LineMarkProps): ProductionRuleTests => { const strokeRules: ProductionRuleTests = []; - const selectedTest = getSelectedTest(name); + const selectedTest = getSelectedTest(name, idKey); if (staticPoint) { strokeRules.push({ test: staticPointTest(staticPoint), ...getColorProductionRule(color, colorScheme) }); @@ -277,8 +278,8 @@ export const getHighlightPointStrokeWidth = ({ staticPoint }: LineMarkProps): Pr * @returns SymbolMark */ export const getSelectRingPoint = (lineProps: LineMarkProps): SymbolMark => { - const { colorScheme, dimension, metric, metricAxis, name, scaleType } = lineProps; - const selectedTest = getSelectedTest(name); + const { colorScheme, dimension, idKey, metric, metricAxis, name, scaleType } = lineProps; + const selectedTest = getSelectedTest(name, idKey); return { name: `${name}_pointSelectRing`, diff --git a/src/specBuilder/line/lineSpecBuilder.test.ts b/src/specBuilder/line/lineSpecBuilder.test.ts index bc99d04aa..4782ccc76 100644 --- a/src/specBuilder/line/lineSpecBuilder.test.ts +++ b/src/specBuilder/line/lineSpecBuilder.test.ts @@ -44,6 +44,7 @@ const defaultLineProps: LineSpecProps = { children: [], name: 'line0', dimension: DEFAULT_TIME_DIMENSION, + idKey: MARK_ID, index: 0, markType: 'line', metric: DEFAULT_METRIC, @@ -292,7 +293,7 @@ const metricRangeWithDisplayPointMarks = [ scale: 'yLinear', field: 'value', }, - size: { + size: { value: 125, }, fill: { @@ -329,7 +330,7 @@ const displayPointMarks = [ scale: 'yLinear', field: 'value', }, - size: { + size: { value: 125, }, fill: { @@ -353,7 +354,7 @@ const displayPointMarks = [ describe('lineSpecBuilder', () => { describe('addLine()', () => { test('should add line', () => { - expect(addLine(startingSpec, { color: DEFAULT_COLOR })).toStrictEqual(defaultSpec); + expect(addLine(startingSpec, { idKey: MARK_ID, color: DEFAULT_COLOR })).toStrictEqual(defaultSpec); }); }); diff --git a/src/specBuilder/line/lineSpecBuilder.ts b/src/specBuilder/line/lineSpecBuilder.ts index cd98a3694..17e82f790 100644 --- a/src/specBuilder/line/lineSpecBuilder.ts +++ b/src/specBuilder/line/lineSpecBuilder.ts @@ -43,7 +43,7 @@ import { getLineHoverMarks, getLineMark } from './lineMarkUtils'; import { getLineStaticPoint } from './linePointUtils'; import { getInteractiveMarkName, getPopoverMarkName } from './lineUtils'; -export const addLine = produce( +export const addLine = produce( ( spec, { @@ -99,10 +99,13 @@ export const addData = produce((data, props) => { tableData.transform = addTimeTransform(tableData.transform ?? [], dimension); } if (hasInteractiveChildren(children)) { - data.push(getLineHighlightedData(name, FILTERED_TABLE, hasPopover(children), isHighlightedByGroup(props))); + data.push( + getLineHighlightedData(name, props.idKey, FILTERED_TABLE, hasPopover(children), isHighlightedByGroup(props)) + ); data.push(getFilteredTooltipData(children)); } - if (staticPoint || isSparkline) data.push(getLineStaticPointData(name, staticPoint, FILTERED_TABLE, isSparkline, isMethodLast)); + if (staticPoint || isSparkline) + data.push(getLineStaticPointData(name, staticPoint, FILTERED_TABLE, isSparkline, isMethodLast)); addTrendlineData(data, props); addTooltipData(data, props, false); addPopoverData(data, props); @@ -110,12 +113,12 @@ export const addData = produce((data, props) => { }); export const addSignals = produce((signals, props) => { - const { children, name } = props; + const { children, idKey, name } = props; setTrendlineSignals(signals, props); signals.push(...getMetricRangeSignals(props)); if (!hasInteractiveChildren(children)) return; - addHighlightedItemSignalEvents(signals, `${name}_voronoi`, 2); + addHighlightedItemSignalEvents(signals, `${name}_voronoi`, idKey, 2); addHighlightedSeriesSignalEvents(signals, `${name}_voronoi`, 2); addHoverSignals(signals, props); addTooltipSignals(signals, props); @@ -181,10 +184,10 @@ const getMetricKeys = (lineMetric: string, lineChildren: MarkChildElement[], lin }; const addHoverSignals = (signals: Signal[], props: LineSpecProps) => { - const { interactionMode, name } = props; + const { idKey, interactionMode, name } = props; if (interactionMode !== INTERACTION_MODE.ITEM) return; getHoverMarkNames(name).forEach((hoverMarkName) => { - addHighlightedItemSignalEvents(signals, hoverMarkName, 1); + addHighlightedItemSignalEvents(signals, hoverMarkName, idKey, 1); addHighlightedSeriesSignalEvents(signals, hoverMarkName, 1); }); }; diff --git a/src/specBuilder/line/lineTestUtils.ts b/src/specBuilder/line/lineTestUtils.ts index f636d3893..6999cd4c5 100644 --- a/src/specBuilder/line/lineTestUtils.ts +++ b/src/specBuilder/line/lineTestUtils.ts @@ -9,7 +9,7 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -import { DEFAULT_COLOR, DEFAULT_COLOR_SCHEME, DEFAULT_METRIC, DEFAULT_TIME_DIMENSION } from '@constants'; +import { DEFAULT_COLOR, DEFAULT_COLOR_SCHEME, DEFAULT_METRIC, DEFAULT_TIME_DIMENSION, MARK_ID } from '@constants'; import { LineMarkProps } from './lineUtils'; @@ -18,6 +18,7 @@ export const defaultLineMarkProps: LineMarkProps = { color: DEFAULT_COLOR, colorScheme: DEFAULT_COLOR_SCHEME, dimension: DEFAULT_TIME_DIMENSION, + idKey: MARK_ID, lineType: { value: 'solid' }, lineWidth: { value: 1 }, name: 'line0', diff --git a/src/specBuilder/line/lineUtils.ts b/src/specBuilder/line/lineUtils.ts index 914372e53..c8304b3e3 100644 --- a/src/specBuilder/line/lineUtils.ts +++ b/src/specBuilder/line/lineUtils.ts @@ -68,6 +68,7 @@ export interface LineMarkProps { colorScheme: ColorScheme; dimension: string; displayOnHover?: boolean; + idKey: string; interactiveMarkName?: string; // optional name of the mark that is used for hover and click interactions isHighlightedByDimension?: boolean; isHighlightedByGroup?: boolean; diff --git a/src/specBuilder/marks/markUtils.ts b/src/specBuilder/marks/markUtils.ts index 8d48a64a2..ef2a5063a 100644 --- a/src/specBuilder/marks/markUtils.ts +++ b/src/specBuilder/marks/markUtils.ts @@ -28,7 +28,6 @@ import { LINEAR_COLOR_SCALE, LINE_TYPE_SCALE, LINE_WIDTH_SCALE, - MARK_ID, OPACITY_SCALE, SELECTED_GROUP, SELECTED_ITEM, @@ -415,7 +414,7 @@ const getHoverSizeSignal = (size: number): SignalRef => ({ * @returns */ export const getMarkOpacity = (props: BarSpecProps | DonutSpecProps): ({ test?: string } & NumericValueRef)[] => { - const { children, name: markName } = props; + const { children, idKey, name: markName } = props; const rules: ({ test?: string } & NumericValueRef)[] = [DEFAULT_OPACITY_RULE]; // if there aren't any interactive components, then we don't need to add special opacity rules if (!hasInteractiveChildren(children)) { @@ -428,10 +427,10 @@ export const getMarkOpacity = (props: BarSpecProps | DonutSpecProps): ({ test?: if (hasPopover(children)) { return [ { - test: `!${SELECTED_GROUP} && ${SELECTED_ITEM} && ${SELECTED_ITEM} !== datum.${MARK_ID}`, + test: `!${SELECTED_GROUP} && ${SELECTED_ITEM} && ${SELECTED_ITEM} !== datum.${idKey}`, value: 1 / HIGHLIGHT_CONTRAST_RATIO, }, - { test: `${SELECTED_ITEM} && ${SELECTED_ITEM} === datum.${MARK_ID}`, ...DEFAULT_OPACITY_RULE }, + { test: `${SELECTED_ITEM} && ${SELECTED_ITEM} === datum.${idKey}`, ...DEFAULT_OPACITY_RULE }, { test: `${SELECTED_GROUP} && ${SELECTED_GROUP} === datum.${markName}_selectedGroupId`, value: 1, diff --git a/src/specBuilder/metricRange/metricRangeUtils.test.ts b/src/specBuilder/metricRange/metricRangeUtils.test.ts index aec101680..536bc3001 100644 --- a/src/specBuilder/metricRange/metricRangeUtils.test.ts +++ b/src/specBuilder/metricRange/metricRangeUtils.test.ts @@ -21,6 +21,7 @@ import { DEFAULT_TIME_DIMENSION, DEFAULT_TRANSFORMED_TIME_DIMENSION, FILTERED_TABLE, + MARK_ID, } from '@constants'; import { LineSpecProps, MetricRangeProps, MetricRangeSpecProps } from '../../types'; @@ -54,18 +55,19 @@ const defaultMetricRangeSpecProps: MetricRangeSpecProps = { const defaultLineProps: LineSpecProps = { children: [createElement(MetricRange, defaultMetricRangeProps)], - name: 'line0', + color: DEFAULT_COLOR, + colorScheme: DEFAULT_COLOR_SCHEME, dimension: DEFAULT_TIME_DIMENSION, + idKey: MARK_ID, index: 0, + interactiveMarkName: undefined, + lineType: { value: 'solid' }, markType: 'line', metric: DEFAULT_METRIC, - color: DEFAULT_COLOR, - scaleType: 'time', - lineType: { value: 'solid' }, + name: 'line0', opacity: { value: 1 }, - colorScheme: DEFAULT_COLOR_SCHEME, - interactiveMarkName: undefined, popoverMarkName: undefined, + scaleType: 'time', }; const basicMetricRangeMarks = [ diff --git a/src/specBuilder/scatter/scatterMarkUtils.ts b/src/specBuilder/scatter/scatterMarkUtils.ts index 1708bce85..dcb352b1b 100644 --- a/src/specBuilder/scatter/scatterMarkUtils.ts +++ b/src/specBuilder/scatter/scatterMarkUtils.ts @@ -9,7 +9,7 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -import { DEFAULT_OPACITY_RULE, FILTERED_TABLE, HIGHLIGHT_CONTRAST_RATIO, MARK_ID, SELECTED_ITEM } from '@constants'; +import { DEFAULT_OPACITY_RULE, FILTERED_TABLE, HIGHLIGHT_CONTRAST_RATIO, SELECTED_ITEM } from '@constants'; import { addTooltipMarkOpacityRules } from '@specBuilder/chartTooltip/chartTooltipUtils'; import { getColorProductionRule, @@ -102,7 +102,7 @@ export const getScatterMark = (props: ScatterSpecProps): SymbolMark => { * @returns opacity production rule */ export const getOpacity = (props: ScatterSpecProps): ({ test?: string } & NumericValueRef)[] => { - const { children } = props; + const { children, idKey } = props; if (!hasInteractiveChildren(children)) { return [DEFAULT_OPACITY_RULE]; } @@ -113,7 +113,7 @@ export const getOpacity = (props: ScatterSpecProps): ({ test?: string } & Numeri addTooltipMarkOpacityRules(rules, props); if (hasPopover(children)) { rules.push({ - test: `${SELECTED_ITEM} && ${SELECTED_ITEM} !== datum.${MARK_ID}`, + test: `${SELECTED_ITEM} && ${SELECTED_ITEM} !== datum.${idKey}`, value: fadedValue, }); } diff --git a/src/specBuilder/scatter/scatterSpecBuilder.ts b/src/specBuilder/scatter/scatterSpecBuilder.ts index 104ccadc6..e11f4f670 100644 --- a/src/specBuilder/scatter/scatterSpecBuilder.ts +++ b/src/specBuilder/scatter/scatterSpecBuilder.ts @@ -19,7 +19,6 @@ import { LINEAR_COLOR_SCALE, LINE_TYPE_SCALE, LINE_WIDTH_SCALE, - MARK_ID, OPACITY_SCALE, SELECTED_ITEM, SYMBOL_SIZE_SCALE, @@ -48,7 +47,7 @@ import { addScatterMarks } from './scatterMarkUtils'; * @param spec Spec * @param scatterProps ScatterProps */ -export const addScatter = produce( +export const addScatter = produce( ( spec, { @@ -98,7 +97,7 @@ export const addScatter = produce((data, props) => { - const { children, dimension, dimensionScaleType, name } = props; + const { children, dimension, dimensionScaleType, idKey, name } = props; if (dimensionScaleType === 'time') { const tableData = getTableData(data); tableData.transform = addTimeTransform(tableData.transform ?? [], dimension); @@ -115,7 +114,7 @@ export const addData = produce((data, props) => { transform: [ { type: 'filter', - expr: `${SELECTED_ITEM} === datum.${MARK_ID}`, + expr: `${SELECTED_ITEM} === datum.${idKey}`, }, ], }); @@ -130,13 +129,13 @@ export const addData = produce((data, props) => { * @param scatterProps ScatterSpecProps */ export const addSignals = produce((signals, props) => { - const { children, name } = props; + const { children, idKey, name } = props; // trendline signals setTrendlineSignals(signals, props); if (!hasInteractiveChildren(children)) return; // interactive signals - addHighlightedItemSignalEvents(signals, `${name}_voronoi`, 2); + addHighlightedItemSignalEvents(signals, `${name}_voronoi`, idKey, 2); addTooltipSignals(signals, props); }); diff --git a/src/specBuilder/scatter/scatterTestUtils.ts b/src/specBuilder/scatter/scatterTestUtils.ts index ca4589b75..e9442284d 100644 --- a/src/specBuilder/scatter/scatterTestUtils.ts +++ b/src/specBuilder/scatter/scatterTestUtils.ts @@ -14,6 +14,7 @@ import { DEFAULT_DIMENSION_SCALE_TYPE, DEFAULT_LINEAR_DIMENSION, DEFAULT_METRIC, + MARK_ID, } from '@constants'; import { ScatterSpecProps } from '../../types'; @@ -25,6 +26,7 @@ export const defaultScatterProps: ScatterSpecProps = { colorScheme: DEFAULT_COLOR_SCHEME, dimension: DEFAULT_LINEAR_DIMENSION, dimensionScaleType: DEFAULT_DIMENSION_SCALE_TYPE, + idKey: MARK_ID, index: 0, interactiveMarkName: 'scatter0', lineType: { value: 'solid' }, diff --git a/src/specBuilder/signal/signalSpecBuilder.test.ts b/src/specBuilder/signal/signalSpecBuilder.test.ts index 2e81a1186..35c29e0f3 100644 --- a/src/specBuilder/signal/signalSpecBuilder.test.ts +++ b/src/specBuilder/signal/signalSpecBuilder.test.ts @@ -9,7 +9,7 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -import { FILTERED_TABLE, HIGHLIGHTED_ITEM, HIGHLIGHTED_SERIES } from '@constants'; +import { FILTERED_TABLE, HIGHLIGHTED_ITEM, HIGHLIGHTED_SERIES, MARK_ID } from '@constants'; import { defaultHighlightedItemSignal, defaultHighlightedSeriesSignal, @@ -30,7 +30,7 @@ describe('signalSpecBuilder', () => { }); describe('addHighlightedItemSignalEvents()', () => { test('should add on events', () => { - addHighlightedItemSignalEvents(signals, 'line0'); + addHighlightedItemSignalEvents(signals, 'line0', MARK_ID); expect(signals).toHaveLength(defaultSignals.length); expect(signals[0]).toHaveProperty('name', HIGHLIGHTED_ITEM); expect(signals[0].on).toHaveLength(2); @@ -44,11 +44,11 @@ describe('signalSpecBuilder', () => { test('should not do anything if the highlight signal is not found', () => { const signals = JSON.parse(JSON.stringify([defaultHighlightedSeriesSignal])); const signalsCopy = JSON.parse(JSON.stringify(signals)); - addHighlightedItemSignalEvents(signals, 'line0'); + addHighlightedItemSignalEvents(signals, 'line0', MARK_ID); expect(signals).toEqual(signalsCopy); }); test('should include update condition if excludeDataKey is provided', () => { - addHighlightedItemSignalEvents(signals, 'bar0', 1, ['excludeFromTooltip']); + addHighlightedItemSignalEvents(signals, 'bar0', MARK_ID, 1, ['excludeFromTooltip']); expect(signals).toHaveLength(defaultSignals.length); expect(signals[0]).toHaveProperty('name', HIGHLIGHTED_ITEM); expect(signals[0].on).toHaveLength(2); diff --git a/src/specBuilder/signal/signalSpecBuilder.ts b/src/specBuilder/signal/signalSpecBuilder.ts index 7bc8449e1..3489a8f8b 100644 --- a/src/specBuilder/signal/signalSpecBuilder.ts +++ b/src/specBuilder/signal/signalSpecBuilder.ts @@ -9,14 +9,7 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -import { - FILTERED_TABLE, - HIGHLIGHTED_GROUP, - HIGHLIGHTED_ITEM, - HIGHLIGHTED_SERIES, - MARK_ID, - SERIES_ID, -} from '@constants'; +import { FILTERED_TABLE, HIGHLIGHTED_GROUP, HIGHLIGHTED_ITEM, HIGHLIGHTED_SERIES, SERIES_ID } from '@constants'; import { Signal } from 'vega'; /** @@ -124,6 +117,7 @@ export const getGenericUpdateSignal = (name: string, update: string): Signal => export const addHighlightedItemSignalEvents = ( signals: Signal[], markName: string, + idKey: string, datumOrder = 1, excludeDataKeys?: string[] ) => { @@ -142,8 +136,8 @@ export const addHighlightedItemSignalEvents = ( { events: `@${markName}:mouseover`, update: excludeDataKeys?.length - ? `(${excludeDataKeysCondition}) ? null : ${datum}${MARK_ID}` - : `${datum}${MARK_ID}`, + ? `(${excludeDataKeysCondition}) ? null : ${datum}${idKey}` + : `${datum}${idKey}`, }, { events: `@${markName}:mouseout`, update: 'null' }, ] diff --git a/src/specBuilder/trendline/trendlineDataUtils.ts b/src/specBuilder/trendline/trendlineDataUtils.ts index 5acb20b67..0430f438c 100644 --- a/src/specBuilder/trendline/trendlineDataUtils.ts +++ b/src/specBuilder/trendline/trendlineDataUtils.ts @@ -13,7 +13,6 @@ import { FILTERED_TABLE, HIGHLIGHTED_ITEM, HIGHLIGHTED_SERIES, - MARK_ID, SELECTED_ITEM, SELECTED_SERIES, SERIES_ID, @@ -66,7 +65,7 @@ export const addTrendlineData = (data: Data[], markProps: TrendlineParentProps) */ export const getTrendlineData = (markProps: TrendlineParentProps): SourceData[] => { const data: SourceData[] = []; - const { color, lineType, name: markName } = markProps; + const { color, idKey, lineType, name: markName } = markProps; const trendlines = getTrendlines(markProps); const concatenatedTrendlineData: { name: string; source: string[] } = { @@ -95,7 +94,7 @@ export const getTrendlineData = (markProps: TrendlineParentProps): SourceData[] if (trendlines.some((trendline) => hasInteractiveChildren(trendline.children))) { data.push(concatenatedTrendlineData); - data.push(getHighlightTrendlineData(markName)); + data.push(getHighlightTrendlineData(markName, idKey)); } return data; @@ -229,8 +228,8 @@ const getWindowTrendlineData = (markProps: TrendlineParentProps, trendlineProps: * @param trendlines * @returns Data */ -const getHighlightTrendlineData = (markName: string): SourceData => { - const expr = `${SELECTED_ITEM} === datum.${MARK_ID} || !${SELECTED_ITEM} && ${HIGHLIGHTED_ITEM} === datum.${MARK_ID}`; +const getHighlightTrendlineData = (markName: string, idKey: string): SourceData => { + const expr = `${SELECTED_ITEM} === datum.${idKey} || !${SELECTED_ITEM} && ${HIGHLIGHTED_ITEM} === datum.${idKey}`; return { name: `${markName}Trendline_highlightedData`, source: `${markName}_allTrendlineData`, diff --git a/src/specBuilder/trendline/trendlineMarkUtils.ts b/src/specBuilder/trendline/trendlineMarkUtils.ts index 3042d57e6..6d3bf92e6 100644 --- a/src/specBuilder/trendline/trendlineMarkUtils.ts +++ b/src/specBuilder/trendline/trendlineMarkUtils.ts @@ -329,7 +329,7 @@ const getLineMarkProps = ( { dimensionScaleType, displayOnHover, lineWidth, metric, name, opacity }: TrendlineSpecProps, override?: Partial ): LineMarkProps => { - const { children, color, colorScheme, dimension, interactiveMarkName, lineType } = markProps; + const { children, color, colorScheme, dimension, idKey, interactiveMarkName, lineType } = markProps; const popoverMarkName = 'popoverMarkName' in markProps ? markProps.popoverMarkName : undefined; const staticPoint = 'staticPoint' in markProps ? markProps.staticPoint : undefined; return { @@ -338,6 +338,7 @@ const getLineMarkProps = ( colorScheme, dimension, displayOnHover, + idKey, interactiveMarkName, lineType: getTrendlineLineTypeFromMarkProps(lineType), lineWidth: { value: lineWidth }, diff --git a/src/specBuilder/trendline/trendlineSignalUtils.ts b/src/specBuilder/trendline/trendlineSignalUtils.ts index 7ee8a972d..489f0b00f 100644 --- a/src/specBuilder/trendline/trendlineSignalUtils.ts +++ b/src/specBuilder/trendline/trendlineSignalUtils.ts @@ -19,11 +19,11 @@ import { Signal } from 'vega'; import { TrendlineParentProps, getTrendlines } from './trendlineUtils'; export const setTrendlineSignals = (signals: Signal[], markProps: TrendlineParentProps): void => { - const { name: markName } = markProps; + const { idKey, name: markName } = markProps; const trendlines = getTrendlines(markProps); if (trendlines.some((trendline) => hasTooltip(trendline.children))) { - addHighlightedItemSignalEvents(signals, `${markName}Trendline_voronoi`, 2); + addHighlightedItemSignalEvents(signals, `${markName}Trendline_voronoi`, idKey, 2); addHighlightedSeriesSignalEvents(signals, `${markName}Trendline_voronoi`, 2); } diff --git a/src/specBuilder/trendline/trendlineTestUtils.ts b/src/specBuilder/trendline/trendlineTestUtils.ts index 9c4938af7..67d7273e2 100644 --- a/src/specBuilder/trendline/trendlineTestUtils.ts +++ b/src/specBuilder/trendline/trendlineTestUtils.ts @@ -12,7 +12,7 @@ import { createElement } from 'react'; import { Trendline } from '@components/Trendline'; -import { DEFAULT_COLOR, DEFAULT_COLOR_SCHEME, DEFAULT_METRIC, DEFAULT_TIME_DIMENSION } from '@constants'; +import { DEFAULT_COLOR, DEFAULT_COLOR_SCHEME, DEFAULT_METRIC, DEFAULT_TIME_DIMENSION, MARK_ID } from '@constants'; import { LineSpecProps, TrendlineSpecProps } from '../../types'; @@ -21,6 +21,7 @@ export const defaultLineProps: LineSpecProps = { color: DEFAULT_COLOR, colorScheme: DEFAULT_COLOR_SCHEME, dimension: DEFAULT_TIME_DIMENSION, + idKey: MARK_ID, index: 0, lineType: { value: 'solid' }, markType: 'line', diff --git a/src/types/Chart.ts b/src/types/Chart.ts index 08b81704a..fda17043f 100644 --- a/src/types/Chart.ts +++ b/src/types/Chart.ts @@ -53,7 +53,7 @@ export type TrendlineAnnotationElement = ReactElement< export type TrendlineElement = ReactElement>; export type ComboElement = ReactElement>; -export type SimpleData = { [key: string]: unknown }; +export type SimpleData = Record; export type ChartData = SimpleData | Data; export interface SpecProps { @@ -87,6 +87,8 @@ export interface SpecProps { hiddenSeries?: string[]; /** Series name to highlight on the chart (controlled). */ highlightedSeries?: string; + /** Data key that contains a unique ID for each data point in the array. */ + idKey?: string; } export interface SanitizedSpecProps extends SpecProps { @@ -129,12 +131,13 @@ type ChartPropsWithDefaults = | 'colors' | 'colorScheme' | 'debug' + | 'hiddenSeries' + | 'idKey' + | 'lineTypes' + | 'lineWidths' | 'locale' | 'padding' | 'renderer' - | 'lineTypes' - | 'lineWidths' - | 'hiddenSeries' | 'tooltipAnchor' | 'tooltipPlacement'; diff --git a/src/types/specBuilderTypes.ts b/src/types/specBuilderTypes.ts index 7db4b324c..9536ee221 100644 --- a/src/types/specBuilderTypes.ts +++ b/src/types/specBuilderTypes.ts @@ -47,8 +47,10 @@ type PartiallyRequired = Omit & Required> type AreaPropsWithDefaults = 'name' | 'dimension' | 'metric' | 'color' | 'scaleType' | 'opacity'; -export interface AreaSpecProps - extends PartiallyRequired { +export interface AreaSpecProps extends PartiallyRequired { + colorScheme: ColorScheme; + idKey: string; + index: number; children: MarkChildElement[]; markType: 'area'; } @@ -82,11 +84,10 @@ export interface AxisSpecProps extends PartiallyRequired { + extends PartiallyRequired { + axisName: string; children: AxisAnnotationChildElement[]; + colorScheme: ColorScheme; } type BarPropsWithDefaults = @@ -104,12 +105,14 @@ type BarPropsWithDefaults = | 'trellisPadding' | 'type'; -export interface BarSpecProps - extends PartiallyRequired { - markType: 'bar'; +export interface BarSpecProps extends PartiallyRequired { children: MarkChildElement[]; - interactiveMarkName: string | undefined; + colorScheme: ColorScheme; dimensionScaleType: 'band'; + idKey: string; + index: number; + interactiveMarkName: string | undefined; + markType: 'bar'; } type AnnotationPropsWithDefaults = 'textKey'; @@ -135,9 +138,11 @@ export interface ChartPopoverSpecProps extends PartiallyRequired { +export interface DonutSpecProps extends PartiallyRequired { children: MarkChildElement[]; + colorScheme: ColorScheme; + idKey: string; + index: number; markType: 'donut'; } @@ -155,12 +160,12 @@ export interface SegmentLabelSpecProps extends PartiallyRequired { +export interface LegendSpecProps extends PartiallyRequired { color?: FacetRef; + colorScheme: ColorScheme; + hiddenSeries: string[]; + highlightedSeries?: string; + index: number; lineType?: FacetRef; lineWidth?: FacetRef; symbolShape?: FacetRef; @@ -171,6 +176,7 @@ type LinePropsWithDefaults = 'name' | 'dimension' | 'metric' | 'color' | 'scaleT export interface LineSpecProps extends PartiallyRequired { children: MarkChildElement[]; colorScheme: ColorScheme; + idKey: string; index: number; interactiveMarkName: string | undefined; isHighlightedByGroup?: boolean; @@ -195,6 +201,7 @@ type ScatterPropsWithDefaults = export interface ScatterSpecProps extends PartiallyRequired { children: MarkChildElement[]; colorScheme: ColorScheme; + idKey: string; index: number; interactiveMarkName: string | undefined; markType: 'scatter'; @@ -212,8 +219,9 @@ export interface ScatterPathSpecProps extends PartiallyRequired {} +export interface MetricRangeSpecProps extends PartiallyRequired { + name: string; +} type TrendlinePropsWithDefaults = | 'dimensionExtent' @@ -223,16 +231,15 @@ type TrendlinePropsWithDefaults = | 'lineType' | 'lineWidth' | 'method' - | 'metric' | 'opacity' | 'orientation'; -export interface TrendlineSpecProps - extends PartiallyRequired { +export interface TrendlineSpecProps extends PartiallyRequired { children: TrendlineChildElement[]; colorScheme: ColorScheme; dimensionScaleType: RscScaleType; isDimensionNormalized: boolean; + metric: string; name: string; trendlineColor: ColorFacet; trendlineDimension: string; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index dbda45da7..f6636281c 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -11,7 +11,7 @@ */ import { Fragment, ReactNode } from 'react'; -import { MARK_ID, SELECTED_GROUP, SELECTED_ITEM, SELECTED_SERIES, SERIES_ID } from '@constants'; +import { SELECTED_GROUP, SELECTED_ITEM, SELECTED_SERIES, SERIES_ID } from '@constants'; import { Annotation, Area, @@ -400,8 +400,16 @@ export function debugLog( * Sets the values of the selectedId and selectedSeries signals * @param param0 */ -export const setSelectedSignals = ({ selectedData, view }: { selectedData: Datum | null; view: View }) => { - view.signal(SELECTED_ITEM, selectedData?.[MARK_ID] ?? null); +export const setSelectedSignals = ({ + idKey, + selectedData, + view, +}: { + idKey: string; + selectedData: Datum | null; + view: View; +}) => { + view.signal(SELECTED_ITEM, selectedData?.[idKey] ?? null); view.signal(SELECTED_SERIES, selectedData?.[SERIES_ID] ?? null); const selectedGroupKey = Object.keys(selectedData ?? {}).find((k) => k.endsWith('_selectedGroupId'));