From f97880f70d9b76c6027f395f8ee10a2f79687682 Mon Sep 17 00:00:00 2001 From: Brady Shimanek Date: Tue, 7 Oct 2025 16:54:21 -0600 Subject: [PATCH 01/66] Initial gauge component setup - copied and renamed bullet files --- packages/constants/constants.ts | 1 + .../src/alpha/components/Gauge/Gauge.tsx | 45 ++ .../src/alpha/components/Gauge/index.ts | 13 + .../src/alpha/components/index.ts | 1 + .../stories/components/Gauge/Gauge.story.tsx | 201 ++++++++ .../stories/components/Gauge/Gauge.test.tsx | 44 ++ .../src/stories/components/Gauge/data.ts | 28 ++ .../src/types/marks/gauge.types.ts | 18 + .../vega-spec-builder/src/chartSpecBuilder.ts | 1 + .../src/gauge/gaugeDataUtils.test.ts | 199 ++++++++ .../src/gauge/gaugeDataUtils.ts | 115 +++++ .../src/gauge/gaugeMarkUtils.test.ts | 475 ++++++++++++++++++ .../src/gauge/gaugeMarkUtils.ts | 360 +++++++++++++ .../src/gauge/gaugeSpecBuilder.test.ts | 192 +++++++ .../src/gauge/gaugeSpecBuilder.ts | 181 +++++++ .../src/gauge/gaugeTestUtils.ts | 54 ++ .../src/types/marks/gaugeSpec.types.ts | 95 ++++ .../src/types/marks/index.ts | 1 + 18 files changed, 2024 insertions(+) create mode 100644 packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx create mode 100644 packages/react-spectrum-charts/src/alpha/components/Gauge/index.ts create mode 100644 packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx create mode 100644 packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.test.tsx create mode 100644 packages/react-spectrum-charts/src/stories/components/Gauge/data.ts create mode 100644 packages/react-spectrum-charts/src/types/marks/gauge.types.ts create mode 100644 packages/vega-spec-builder/src/gauge/gaugeDataUtils.test.ts create mode 100644 packages/vega-spec-builder/src/gauge/gaugeDataUtils.ts create mode 100644 packages/vega-spec-builder/src/gauge/gaugeMarkUtils.test.ts create mode 100644 packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts create mode 100644 packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts create mode 100644 packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts create mode 100644 packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts create mode 100644 packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts diff --git a/packages/constants/constants.ts b/packages/constants/constants.ts index 804518ac8..89629936e 100644 --- a/packages/constants/constants.ts +++ b/packages/constants/constants.ts @@ -18,6 +18,7 @@ export const DEFAULT_AXIS_ANNOTATION_COLOR = 'gray-600'; export const DEFAULT_AXIS_ANNOTATION_OFFSET = 80; export const DEFAULT_BACKGROUND_COLOR = 'transparent'; export const DEFAULT_BULLET_DIRECTION = 'column'; +export const DEFAULT_GAUGE_DIRECTION = 'column'; export const DEFAULT_CATEGORICAL_DIMENSION = 'category'; export const DEFAULT_COLOR = 'series'; export const DEFAULT_COLOR_SCHEME = 'light'; diff --git a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx new file mode 100644 index 000000000..0c5d007b1 --- /dev/null +++ b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx @@ -0,0 +1,45 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { FC } from 'react'; + +import { + DEFAULT_GAUGE_DIRECTION, + DEFAULT_LABEL_POSITION, + DEFAULT_SCALE_TYPE, + DEFAULT_SCALE_VALUE, +} from '@spectrum-charts/constants'; + +import { GaugeProps } from '../../../types'; + +const Gauge: FC = ({ + name = 'bullet0', + metric = 'currentAmount', + dimension = 'graphLabel', + target = 'target', + direction = DEFAULT_GAUGE_DIRECTION, + numberFormat = '', + showTarget = true, + showTargetValue = false, + labelPosition = DEFAULT_LABEL_POSITION, + scaleType = DEFAULT_SCALE_TYPE, + maxScaleValue = DEFAULT_SCALE_VALUE, + thresholdBarColor = false, +}) => { + return null; +}; + +// displayName is used to validate the component type in the spec builder +Gauge.displayName = 'Gauge'; + +export { Gauge }; diff --git a/packages/react-spectrum-charts/src/alpha/components/Gauge/index.ts b/packages/react-spectrum-charts/src/alpha/components/Gauge/index.ts new file mode 100644 index 000000000..72f28c6a1 --- /dev/null +++ b/packages/react-spectrum-charts/src/alpha/components/Gauge/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +export * from './Gauge'; diff --git a/packages/react-spectrum-charts/src/alpha/components/index.ts b/packages/react-spectrum-charts/src/alpha/components/index.ts index fedea5918..83326d4d2 100644 --- a/packages/react-spectrum-charts/src/alpha/components/index.ts +++ b/packages/react-spectrum-charts/src/alpha/components/index.ts @@ -13,3 +13,4 @@ export * from './Bullet'; export * from './Combo'; export * from './Venn'; +export * from './Gauge'; diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx new file mode 100644 index 000000000..350e9ac3f --- /dev/null +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx @@ -0,0 +1,201 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { ReactElement } from 'react'; + +import { StoryFn } from '@storybook/react'; + +import { Chart } from '../../../Chart'; +// Assuming Bullet chart is a component in the @rsc/alpha export +import { Bullet } from '../../../alpha'; +import { Title } from '../../../components'; +import useChartProps from '../../../hooks/useChartProps'; +import { bindWithProps } from '../../../test-utils'; +import { BulletProps, ChartProps } from '../../../types'; +import { basicBulletData, basicThresholdsData, coloredThresholdsData } from './data'; + +export default { + title: 'RSC/Bullet (alpha)', + component: Bullet, +}; + +// Default chart properties +const defaultChartProps: ChartProps = { + data: basicBulletData, + width: 350, + height: 350, +}; + +// Basic Bullet chart story +const BulletStory: StoryFn = (args): ReactElement => { + const { width, height, ...bulletProps } = args; + const chartProps = useChartProps({ ...defaultChartProps, width: width ?? 350, height: height ?? 350 }); + return ( + + + + ); +}; + +// Bullet with Title +const BulletTitleStory: StoryFn = (args): ReactElement => { + const chartProps = useChartProps({ ...defaultChartProps, width: 400 }); + return ( + + + <Bullet {...args} /> + </Chart> + ); +}; + +const Basic = bindWithProps(BulletStory); +Basic.args = { + metric: 'currentAmount', + dimension: 'graphLabel', + target: 'target', + color: 'blue-900', + direction: 'column', + numberFormat: '$,.2f', + showTarget: true, + showTargetValue: false, + labelPosition: 'top', + scaleType: 'normal', + maxScaleValue: 100, + track: false, + thresholdBarColor: false, + metricAxis: false, +}; + +const Thresholds = bindWithProps(BulletStory); +Thresholds.args = { + metric: 'currentAmount', + dimension: 'graphLabel', + target: 'target', + color: 'blue-900', + direction: 'column', + numberFormat: '$,.2f', + showTarget: true, + showTargetValue: false, + labelPosition: 'top', + scaleType: 'normal', + maxScaleValue: 100, + thresholds: basicThresholdsData, + thresholdBarColor: false, + track: false, + metricAxis: false, +}; + +const ColoredMetric = bindWithProps(BulletStory); +ColoredMetric.args = { + metric: 'currentAmount', + dimension: 'graphLabel', + target: 'target', + color: 'blue-900', + direction: 'column', + numberFormat: '$,.2f', + showTarget: true, + showTargetValue: false, + labelPosition: 'top', + scaleType: 'normal', + maxScaleValue: 100, + track: false, + thresholdBarColor: true, + thresholds: coloredThresholdsData, + metricAxis: false, +}; + +const Track = bindWithProps(BulletStory); +Track.args = { + metric: 'currentAmount', + dimension: 'graphLabel', + target: 'target', + color: 'blue-900', + direction: 'column', + numberFormat: '$,.2f', + showTarget: true, + showTargetValue: false, + labelPosition: 'top', + scaleType: 'normal', + maxScaleValue: 100, + track: true, + metricAxis: false, +}; + +const RowMode = bindWithProps(BulletStory); +RowMode.args = { + metric: 'currentAmount', + dimension: 'graphLabel', + target: 'target', + color: 'blue-900', + direction: 'row', + numberFormat: '$,.2f', + showTarget: true, + showTargetValue: false, + labelPosition: 'top', + scaleType: 'normal', + maxScaleValue: 100, + thresholds: coloredThresholdsData, + thresholdBarColor: true, + track: false, + metricAxis: false, +}; + +const WithTitle = bindWithProps(BulletTitleStory); +WithTitle.args = { + metric: 'currentAmount', + dimension: 'graphLabel', + target: 'target', + color: 'blue-900', + numberFormat: '$,.2f', + labelPosition: 'top', + scaleType: 'normal', + maxScaleValue: 100, + track: false, + direction: 'column', + metricAxis: false, +}; + +const FixedScale = bindWithProps(BulletStory); +FixedScale.args = { + metric: 'currentAmount', + dimension: 'graphLabel', + target: 'target', + color: 'blue-900', + direction: 'column', + numberFormat: '$,.2f', + showTarget: true, + showTargetValue: false, + labelPosition: 'top', + scaleType: 'fixed', + maxScaleValue: 250, + thresholds: basicThresholdsData, + track: false, + metricAxis: false, +}; + +const MetricAxis = bindWithProps(BulletStory); +MetricAxis.args = { + metric: 'currentAmount', + dimension: 'graphLabel', + target: 'target', + color: 'blue-900', + direction: 'column', + numberFormat: '$,.2f', + showTarget: true, + showTargetValue: false, + labelPosition: 'top', + scaleType: 'normal', + maxScaleValue: 250, + track: false, + metricAxis: true, +}; + +export { Basic, Thresholds, ColoredMetric, Track, RowMode, WithTitle, FixedScale, MetricAxis }; diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.test.tsx b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.test.tsx new file mode 100644 index 000000000..bd4b03284 --- /dev/null +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { Bullet } from '../../../alpha'; +import { findAllMarksByGroupName, findChart, render } from '../../../test-utils'; +import { Basic } from './Gauge.story'; + +describe('Bullet', () => { + // Bullet is not a real React component. This is test just provides test coverage for sonarqube + test('Bullet pseudo element', () => { + render(<Bullet />); + }); + + test('Basic bullet renders properly', async () => { + render(<Basic {...Basic.args} />); + const chart = await findChart(); + expect(chart).toBeInTheDocument(); + + const rects = await findAllMarksByGroupName(chart, 'bullet0Rect'); + expect(rects.length).toEqual(2); + + rects.forEach((rect) => { + // Expect blue-900 color + expect(rect).toHaveAttribute('fill', 'rgb(2, 101, 220)'); + }); + + const barLabels = await findAllMarksByGroupName(chart, 'bullet0Label', 'text'); + expect(barLabels.length).toEqual(2); + + const amountLabels = await findAllMarksByGroupName(chart, 'bullet0ValueLabel', 'text'); + expect(amountLabels.length).toEqual(2); + + const rules = await findAllMarksByGroupName(chart, 'bullet0Target', 'line'); + expect(rules.length).toEqual(2); + }); +}); diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts b/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts new file mode 100644 index 000000000..549d87dce --- /dev/null +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +export const basicBulletData = [ + { graphLabel: 'Customers', currentAmount: 150, target: 50 }, + { graphLabel: 'Revenue', currentAmount: 350, target: 450 }, +]; + +export const basicThresholdsData = [ + { thresholdMax: 120, fill: 'rgb(0, 0, 0)' }, + { thresholdMin: 120, thresholdMax: 235, fill: 'rgb(109, 109, 109)' }, + { thresholdMin: 235, fill: 'rgb(177, 177, 177)' }, +]; + +export const coloredThresholdsData = [ + { thresholdMax: 120, fill: 'rgb(234, 56, 41)' }, + { thresholdMin: 120, thresholdMax: 235, fill: 'rgb(249, 137, 23)' }, + { thresholdMin: 235, fill: 'rgb(21, 164, 110)' }, +]; diff --git a/packages/react-spectrum-charts/src/types/marks/gauge.types.ts b/packages/react-spectrum-charts/src/types/marks/gauge.types.ts new file mode 100644 index 000000000..d9af24e5e --- /dev/null +++ b/packages/react-spectrum-charts/src/types/marks/gauge.types.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { JSXElementConstructor, ReactElement } from 'react'; + +import { GaugeOptions } from '@spectrum-charts/vega-spec-builder'; // TODO: update this when GaugeOptions is added + +export interface GaugeProps extends Omit<GaugeOptions, 'markType'> {} + +export type GaugeElement = ReactElement<GaugeProps, JSXElementConstructor<GaugeProps>>; diff --git a/packages/vega-spec-builder/src/chartSpecBuilder.ts b/packages/vega-spec-builder/src/chartSpecBuilder.ts index a61db2215..041cda880 100644 --- a/packages/vega-spec-builder/src/chartSpecBuilder.ts +++ b/packages/vega-spec-builder/src/chartSpecBuilder.ts @@ -41,6 +41,7 @@ import { addArea } from './area/areaSpecBuilder'; import { addAxis } from './axis/axisSpecBuilder'; import { addBar } from './bar/barSpecBuilder'; import { addBullet } from './bullet/bulletSpecBuilder'; +// add import addGauge here import { addCombo } from './combo/comboSpecBuilder'; import { getSeriesIdTransform } from './data/dataUtils'; import { addDonut } from './donut/donutSpecBuilder'; diff --git a/packages/vega-spec-builder/src/gauge/gaugeDataUtils.test.ts b/packages/vega-spec-builder/src/gauge/gaugeDataUtils.test.ts new file mode 100644 index 000000000..fd7aef1cc --- /dev/null +++ b/packages/vega-spec-builder/src/gauge/gaugeDataUtils.test.ts @@ -0,0 +1,199 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { Data } from 'vega'; + +import { BulletSpecOptions, ThresholdBackground } from '../types'; +import { generateThresholdColorExpr, getBulletTableData, getBulletTransforms } from './bulletDataUtils'; +import { sampleOptionsColumn } from './bulletTestUtils'; + +describe('getBulletTableData', () => { + it('Should create a new table data if it does not exist', () => { + const data: Data[] = []; + + const result = getBulletTableData(data); + + expect(result.name).toBe('table'); + expect(result.values).toEqual([]); + expect(result.transform).toEqual([]); + + expect(data.length).toBe(1); + expect(data[0]).toEqual(result); + }); + + it('Should return the existing table data if it exists', () => { + const existingTableData: Data = { + name: 'table', + values: [], + transform: [], + }; + const data: Data[] = [existingTableData]; + + const result = getBulletTableData(data); + + expect(result).toEqual(existingTableData); + }); +}); + +describe('getBulletTransforms', () => { + it('Should return a formula transform using the target property', () => { + const Options: BulletSpecOptions = { + ...sampleOptionsColumn, + target: 'target', + }; + + const result = getBulletTransforms(Options); + + expect(result).toHaveLength(1); + + expect(result).toEqual([ + { + type: 'formula', + expr: 'isValid(datum.target) ? round(datum.target * 1.05) : 0', + as: 'xPaddingForTarget', + }, + ]); + }); + it('Should return a formula transform using the maxScaleValue property', () => { + const Options: BulletSpecOptions = { + ...sampleOptionsColumn, + target: 'target', + scaleType: 'flexible', + maxScaleValue: 100, + }; + + const result = getBulletTransforms(Options); + + expect(result).toHaveLength(2); + + expect(result[1]).toEqual({ + type: 'formula', + expr: '100', + as: 'flexibleScaleValue', + }); + }); + + it('Should include a barColor transform when thresholdBarColor is true and thresholds are provided', () => { + const thresholds: ThresholdBackground[] = [ + { fill: 'rgb(234, 56, 41)' }, + { thresholdMin: 120, thresholdMax: 235, fill: 'rgb(249, 137, 23)' }, + { thresholdMin: 235, fill: 'rgb(21, 164, 110)' }, + ]; + const Options: BulletSpecOptions = { + ...sampleOptionsColumn, + target: 'target', + thresholdBarColor: true, + thresholds, + color: 'blue', // default color + metric: 'currentAmount', + }; + const result = getBulletTransforms(Options); + expect(result.length).toBeGreaterThanOrEqual(2); + const barColorTransform = result.find((t) => t.as === 'barColor'); + expect(barColorTransform).toBeDefined(); + expect(barColorTransform?.type).toBe('formula'); + expect(typeof barColorTransform?.expr).toBe('string'); + }); + + it('Should not include a barColor transform when thresholds is empty', () => { + // test + const Options: BulletSpecOptions = { + ...sampleOptionsColumn, + target: 'target', + thresholdBarColor: true, + thresholds: [], + color: 'blue', + metric: 'currentAmount', + }; + const result = getBulletTransforms(Options); + const barColorTransform = result.find((t) => t.as === 'barColor'); + expect(barColorTransform).toBeUndefined(); + }); + + it('Should not include a barColor transform when thresholdBarColor is false', () => { + const Options: BulletSpecOptions = { + ...sampleOptionsColumn, + target: 'target', + thresholdBarColor: false, + thresholds: [], + color: 'blue', + metric: 'currentAmount', + }; + const result = getBulletTransforms(Options); + const barColorTransform = result.find((t) => t.as === 'barColor'); + expect(barColorTransform).toBeUndefined(); + }); +}); + +describe('generateThresholdColorExpr', () => { + const metricField = 'currentAmount'; + + it('Should return default color if no thresholds provided', () => { + const expr = generateThresholdColorExpr([], metricField, 'blue-900'); + expect(expr).toBe(`'blue-900'`); + }); + + it('Should generate correct expression for complete thresholds', () => { + const thresholds: ThresholdBackground[] = [ + { fill: 'rgb(234, 56, 41)' }, // first threshold, no thresholdMin → treated as -1e12 + { thresholdMin: 120, thresholdMax: 235, fill: 'rgb(249, 137, 23)' }, + { thresholdMin: 235, fill: 'rgb(21, 164, 110)' }, + ]; + + const expected = + `(datum.${metricField} < -1000000000000) ? 'blue' : ` + + `(datum.${metricField} < 120) ? 'rgb(234, 56, 41)' : ` + + `(datum.${metricField} < 235) ? 'rgb(249, 137, 23)' : ` + + `'rgb(21, 164, 110)'`; + const expr = generateThresholdColorExpr(thresholds, metricField, 'blue'); + expect(expr).toBe(expected); + }); + + it('Should returns proper expression when one threshold is removed', () => { + // Only two thresholds provided. + const thresholds: ThresholdBackground[] = [ + { fill: 'rgb(234, 56, 41)' }, // covers below 120 + { thresholdMin: 120, fill: 'rgb(249, 137, 23)' }, // covers from 120 upward + ]; + + const expected = + `(datum.${metricField} < -1000000000000) ? 'blue' : ` + + `(datum.${metricField} < 120) ? 'rgb(234, 56, 41)' : ` + + `'rgb(249, 137, 23)'`; + const expr = generateThresholdColorExpr(thresholds, metricField, 'blue'); + expect(expr).toBe(expected); + }); + + it('Should sort thresholds correctly when thresholdMin is not provided', () => { + const thresholds: ThresholdBackground[] = [ + { fill: 'rgb(234, 56, 41)' }, // covers below 120 + { thresholdMax: 235, fill: 'rgb(249, 137, 23)' }, // covers from 120 upward + ]; + + const expected = + `(datum.${metricField} < -1000000000000) ? 'blue' : ` + + `(datum.${metricField} < -1000000000000) ? 'rgb(234, 56, 41)' : ` + + `'rgb(249, 137, 23)'`; + const expr = generateThresholdColorExpr(thresholds, metricField, 'blue'); + expect(expr).toBe(expected); + }); + + it('Should return proper expression when two thresholds are removed', () => { + // Only one threshold provided. + const thresholds: ThresholdBackground[] = [ + { fill: 'rgb(234, 56, 41)' }, // covers below 120 + ]; + + const expected = `(datum.${metricField} < -1000000000000) ? 'blue' : 'rgb(234, 56, 41)'`; + const expr = generateThresholdColorExpr(thresholds, metricField, 'blue'); + expect(expr).toBe(expected); + }); +}); diff --git a/packages/vega-spec-builder/src/gauge/gaugeDataUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeDataUtils.ts new file mode 100644 index 000000000..c331fbc7b --- /dev/null +++ b/packages/vega-spec-builder/src/gauge/gaugeDataUtils.ts @@ -0,0 +1,115 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { Data, FormulaTransform, ValuesData } from 'vega'; + +import { TABLE } from '@spectrum-charts/constants'; + +import { getTableData } from '../data/dataUtils'; +import { GaugeSpecOptions, ThresholdBackground } from '../types'; + +/** + * Retrieves the gauge table data from the provided data array. + * If it doesn't exist, creates and pushes a new one. + * @param data The data array. + * @returns The gauge table data. + */ +export const getGaugeTableData = (data: Data[]): ValuesData => { + let tableData = getTableData(data); + if (!tableData) { + tableData = { + name: TABLE, + values: [], + transform: [], + }; + data.push(tableData); + } + return tableData; +}; + +/** + * Generates the necessary formula transforms for the gauge chart. + * It calculates the xPaddingForTarget and, if in flexible scale mode, adds the flexibleScaleValue. + * It also generates a color expression for the threshold bars if applicable. + * @param gaugeOptions The gauge spec properties. + * @returns An array of formula transforms. + */ +export const getGaugeTransforms = (gaugeOptions: GaugeSpecOptions): FormulaTransform[] => { + const transforms: FormulaTransform[] = [ + { + type: 'formula', + expr: `isValid(datum.${gaugeOptions.target}) ? round(datum.${gaugeOptions.target} * 1.05) : 0`, + as: 'xPaddingForTarget', + }, + ]; + + if (gaugeOptions.scaleType === 'flexible') { + transforms.push({ + type: 'formula', + expr: `${gaugeOptions.maxScaleValue}`, + as: 'flexibleScaleValue', + }); + } + + if (gaugeOptions.thresholdBarColor && (gaugeOptions.thresholds?.length ?? 0) > 0) { + transforms.push({ + type: 'formula', + expr: generateThresholdColorExpr(gaugeOptions.thresholds ?? [], gaugeOptions.metric, gaugeOptions.color), + as: 'barColor', + }); + } + + return transforms; +}; + +/** + * Generates a Vega expression for the color of the gauge chart based on the provided thresholds. + * The expression checks the value of the metric field against the thresholds and assigns the appropriate color. + * @param thresholds An array of threshold objects. + * @param metricField The name of the metric field in the data. + * @param defaultColor The default color to use if no thresholds are met. + * @returns A string representing the Vega expression for the color. + */ +export function generateThresholdColorExpr( + thresholds: ThresholdBackground[], + metricField: string, + defaultColor: string +): string { + if (!thresholds || thresholds.length === 0) return `'${defaultColor}'`; + + const sorted: ThresholdBackground[] = thresholds.slice().sort((a, b) => { + const aMin = a.thresholdMin !== undefined ? a.thresholdMin : -1e12; + const bMin = b.thresholdMin !== undefined ? b.thresholdMin : -1e12; + return aMin - bMin; + }); + + const exprParts: string[] = []; + + // For values below the first threshold's lower bound, use the default color. + exprParts.push( + `(datum.${metricField} < ${ + sorted[0].thresholdMin !== undefined ? sorted[0].thresholdMin : -1e12 + }) ? '${defaultColor}' : ` + ); + + // For each threshold, check if the metric field is within the range defined by the thresholdMin and thresholdMax values. + // If it is, use the corresponding fill color. + for (let i = 0; i < sorted.length - 1; i++) { + const nextLower = sorted[i + 1].thresholdMin !== undefined ? sorted[i + 1].thresholdMin : -1e12; + exprParts.push(`(datum.${metricField} < ${nextLower}) ? '${sorted[i].fill}' : `); + } + + // For values above the last threshold's upper bound, use the last threshold's fill color. + exprParts.push(`'${sorted[sorted.length - 1].fill}'`); + + const expr = exprParts.join(''); + return expr; +} diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.test.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.test.ts new file mode 100644 index 000000000..745f4b6e1 --- /dev/null +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.test.ts @@ -0,0 +1,475 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { GroupMark } from 'vega'; + +import { + addAxes, + addMarks, + getBulletMarkLabel, + getBulletMarkRect, + getBulletMarkTarget, + getBulletMarkThreshold, + getBulletMarkValueLabel, + getBulletTrack, +} from './bulletMarkUtils'; +import { sampleOptionsColumn, sampleOptionsRow } from './bulletTestUtils'; + +describe('getBulletMarks', () => { + test('Should return the correct marks object for column mode', () => { + const data = addMarks([], sampleOptionsColumn)[0] as GroupMark; + expect(data).toBeDefined; + expect(data?.marks).toHaveLength(4); + expect(data?.marks?.[0]?.type).toBe('rect'); + expect(data?.marks?.[1]?.type).toBe('rule'); + expect(data?.marks?.[2]?.type).toBe('text'); + expect(data?.marks?.[3]?.type).toBe('text'); + + //Make sure the object that defines the orientation contains the correct key + expect(Object.keys(data?.encode?.update || {})).toContain('y'); + }); + + test('Should return the correct marks object for row mode', () => { + const data = addMarks([], sampleOptionsRow)[0] as GroupMark; + expect(data).toBeDefined; + expect(data?.marks).toHaveLength(4); + expect(data?.marks?.[0]?.type).toBe('rect'); + expect(data?.marks?.[1]?.type).toBe('rule'); + expect(data?.marks?.[2]?.type).toBe('text'); + expect(data?.marks?.[3]?.type).toBe('text'); + expect(Object.keys(data?.encode?.update || {})).toContain('x'); + }); + + test('Should not include target marks when showTarget is false', () => { + const options = { ...sampleOptionsColumn, showTarget: false, showTargetValue: true }; + const marksGroup = addMarks([], options)[0] as GroupMark; + expect(marksGroup.marks).toHaveLength(3); + marksGroup.marks?.forEach((mark) => { + expect(mark.description).not.toContain('Target'); + }); + }); + + test('Should include target value label when showTargetValue is true', () => { + const options = { ...sampleOptionsColumn, showTarget: true, showTargetValue: true }; + const marksGroup = addMarks([], options)[0] as GroupMark; + expect(marksGroup.marks).toHaveLength(5); + const targetValueMark = marksGroup.marks?.find((mark) => mark.name?.includes('TargetValueLabel')); + expect(targetValueMark).toBeDefined(); + }); + + test('Should include label marks when axis labels are enabled', () => { + const options = { ...sampleOptionsColumn, showTarget: true, showTargetValue: true }; + const marksGroup = addMarks([], options)[0] as GroupMark; + expect(marksGroup.marks).toHaveLength(5); + const targetValueMark = marksGroup.marks?.find((mark) => mark.name?.includes('TargetValueLabel')); + expect(targetValueMark).toBeDefined(); + }); + + test('Should include bullet track when track is set to true and threshold is set to false.', () => { + const options = { ...sampleOptionsColumn, threshold: false, track: true }; + const marksGroup = addMarks([], options)[0] as GroupMark; + expect(marksGroup.marks).toHaveLength(5); + const bulletTrackMark = marksGroup.marks?.find((mark) => mark.name?.includes('Track')); + expect(bulletTrackMark).toBeDefined(); + + // Threshold mark should not be present + const bulletThresholdMark = marksGroup.marks?.find((mark) => mark.name?.includes('Threshold')); + expect(bulletThresholdMark).toBeUndefined(); + }); +}); + +describe('getBulletMarkRect', () => { + test('Should return the correct rect mark object', () => { + const data = getBulletMarkRect(sampleOptionsColumn); + expect(data).toBeDefined(); + expect(data.encode?.update).toBeDefined(); + + // Expect the correct amount of fields in the update object + expect(Object.keys(data.encode?.update ?? {}).length).toBe(4); + }); + + describe('getBulletMarkRect threshold color logic', () => { + test('Uses barColor field when thresholdBarColor is enabled and thresholds exist', () => { + const optionsWithThresholdColor = { + ...sampleOptionsColumn, + thresholdBarColor: true, + thresholds: [ + { thresholdMax: 120, fill: 'rgb(234, 56, 41)' }, + { thresholdMin: 120, thresholdMax: 235, fill: 'rgb(249, 137, 23)' }, + { thresholdMin: 235, fill: 'rgb(21, 164, 110)' }, + ], + }; + + const rectMark = getBulletMarkRect(optionsWithThresholdColor); + expect(rectMark.encode?.enter?.fill).toEqual([{ field: 'barColor' }]); + }); + + test('Uses default color field when thresholdBarColor is disabled or no thresholds exist', () => { + const optionsNoThresholds = { + ...sampleOptionsColumn, + thresholdBarColor: true, + thresholds: [], + }; + + const rectMark = getBulletMarkRect(optionsNoThresholds); + expect(rectMark.encode?.enter?.fill).toEqual([{ value: optionsNoThresholds.color }]); + }); + + test('Uses default color field when thresholdBarColor is disabled', () => { + const optionsNoThresholds = { + ...sampleOptionsColumn, + thresholdBarColor: false, + thresholds: [ + { thresholdMax: 120, fill: 'rgb(234, 56, 41)' }, + { thresholdMin: 120, thresholdMax: 235, fill: 'rgb(249, 137, 23)' }, + { thresholdMin: 235, fill: 'rgb(21, 164, 110)' }, + ], + }; + + const rectMark = getBulletMarkRect(optionsNoThresholds); + expect(rectMark.encode?.enter?.fill).toEqual([{ value: optionsNoThresholds.color }]); + }); + + test('Uses default color field when thresholdBarColor is disabled and no thresholds exist', () => { + const optionsNoThresholds = { + ...sampleOptionsColumn, + thresholdBarColor: false, + thresholds: [], + }; + + const rectMark = getBulletMarkRect(optionsNoThresholds); + expect(rectMark.encode?.enter?.fill).toEqual([{ value: optionsNoThresholds.color }]); + }); + }); +}); + +describe('getBulletMarkTarget', () => { + test('Should return the correct target mark object', () => { + const data = getBulletMarkTarget(sampleOptionsColumn); + expect(data).toBeDefined(); + expect(data.encode?.update).toBeDefined(); + expect(Object.keys(data.encode?.update ?? {}).length).toBe(3); + }); +}); + +describe('getBulletMarkLabel', () => { + test('Should return the correct label mark object', () => { + const data = getBulletMarkLabel(sampleOptionsColumn); + expect(data).toBeDefined(); + expect(data.encode?.update).toBeDefined(); + expect(Object.keys(data.encode?.update ?? {}).length).toBe(2); + }); +}); + +describe('getBulletMarkValueLabel', () => { + test('Should return the correct value label mark object in column mode', () => { + const data = getBulletMarkValueLabel(sampleOptionsColumn); + expect(data).toBeDefined(); + expect(data.encode?.update).toBeDefined(); + expect(Object.keys(data.encode?.update ?? {}).length).toBe(2); + }); + + test('Should apply numberFormat specifier to metric and target values', () => { + const options = { ...sampleOptionsColumn, showTarget: true, showTargetValue: true, numberFormat: '$,.2f' }; + const marksGroup = addMarks([], options)[0] as GroupMark; + + const metricValueLabel = marksGroup.marks?.find((mark) => mark.name === `${options.name}ValueLabel`); + expect(metricValueLabel).toBeDefined(); + + if (metricValueLabel?.encode?.enter?.text) { + const textEncode = metricValueLabel.encode.enter.text; + if (typeof textEncode === 'object' && 'signal' in textEncode) { + expect(textEncode.signal).toContain(`format(datum.${options.metric}, '$,.2f')`); + } + } + + const TargetValueLabel = marksGroup.marks?.find((mark) => mark.name?.includes('TargetValueLabel')); + expect(TargetValueLabel).toBeDefined(); + + if (TargetValueLabel?.encode?.enter?.text) { + const textEncode = TargetValueLabel.encode.enter.text; + if (typeof textEncode === 'object' && 'signal' in textEncode) { + expect(textEncode.signal).toContain(`format(datum.${options.target}, '$,.2f')`); + } + } + }); + + describe('getBulletMarkValueLabel threshold color logic', () => { + test('Uses barColor field for label when thresholdBarColor is true', () => { + const options = { + ...sampleOptionsColumn, + thresholdBarColor: true, + thresholds: [{ thresholdMax: 200, fill: 'rgb(249, 137, 23)' }], + }; + const labelMark = getBulletMarkValueLabel(options); + expect(labelMark.encode?.enter?.fill).toEqual({ + signal: "datum.barColor === 'green' ? 'rgb(0, 0, 0)' : datum.barColor", + }); + }); + + test('Falls back to neutral when thresholdBarColor is false', () => { + const options = { + ...sampleOptionsColumn, + thresholdBarColor: false, + }; + const labelMark = getBulletMarkValueLabel(options); + + expect(labelMark.encode?.enter?.fill).toEqual({ signal: "'rgb(0, 0, 0)'" }); + }); + + test('Uses default color when no thresholds are provided', () => { + const options = { + ...sampleOptionsColumn, + thresholdBarColor: true, + thresholds: [], + }; + const labelMark = getBulletMarkValueLabel(options); + expect(labelMark.encode?.enter?.fill).toEqual({ signal: "'rgb(0, 0, 0)'" }); + }); + + test('Uses default color when thresholdBarColor is false and no thresholds are provided', () => { + const options = { + ...sampleOptionsColumn, + thresholdBarColor: false, + thresholds: [], + }; + const labelMark = getBulletMarkValueLabel(options); + expect(labelMark.encode?.enter?.fill).toEqual({ signal: "'rgb(0, 0, 0)'" }); + }); + + test('Uses default color when thresholdBarColor is true and no thresholds are provided', () => { + const options = { + ...sampleOptionsColumn, + thresholdBarColor: true, + thresholds: [], + }; + const labelMark = getBulletMarkValueLabel(options); + expect(labelMark.encode?.enter?.fill).toEqual({ signal: "'rgb(0, 0, 0)'" }); + }); + }); +}); + +describe('getBulletMarkSideLabel', () => { + test('Should not return label marks when side label mode is enabled', () => { + const options = { ...sampleOptionsColumn, labelPosition: 'side' as 'side' | 'top' }; + const marks = addMarks([], options)[0] as GroupMark; + expect(marks.marks).toBeDefined(); + expect(marks.marks).toHaveLength(2); + }); +}); + +describe('getBulletAxes', () => { + test('Should return the correct axes object when side label mode is enabled', () => { + const options = { ...sampleOptionsColumn, labelPosition: 'side' as 'side' | 'top' }; + const axes = addAxes([], options); + expect(axes).toHaveLength(2); + expect(axes[0].labelOffset).toBe(2); + }); + + test('Should return the correct axes object when side label mode is enabled and target label is shown', () => { + const options = { ...sampleOptionsColumn, labelPosition: 'side' as 'side' | 'top', showTargetValue: true }; + const axes = addAxes([], options); + expect(axes).toHaveLength(2); + expect(axes[0].labelOffset).toBe(-8); + }); + + test('Should return an empty list when top label mode is enabled', () => { + const options = { ...sampleOptionsColumn }; + const axes = addAxes([], options); + expect(axes).toStrictEqual([]); + }); + + test('Should return the scale axis when axis is true, row mode is enabled, and showtarget is false', () => { + const options = { ...sampleOptionsColumn, metricAxis: true }; + const axes = addAxes([], options); + expect(axes).toStrictEqual([ + { + labelOffset: 2, + scale: 'xscale', + orient: 'bottom', + ticks: false, + labelColor: 'gray', + domain: false, + tickCount: 5, + offset: { signal: 'axisOffset' }, + }, + ]); + }); + + test('Should not return scale axis when showtarget and showtargetValue are true', () => { + const options = { ...sampleOptionsColumn, showTarget: true, showTargetValue: true, axis: true }; + const axes = addAxes([], options); + expect(axes).toStrictEqual([]); + }); + + test('Should return scale axis and label axes when both are enabled', () => { + const options = { ...sampleOptionsColumn, labelPosition: 'side' as 'side' | 'top', metricAxis: true }; + const axes = addAxes([], options); + expect(axes).toStrictEqual([ + { + labelOffset: 2, + scale: 'xscale', + orient: 'bottom', + ticks: false, + labelColor: 'gray', + domain: false, + tickCount: 5, + offset: { signal: 'axisOffset' }, + }, + { + scale: 'groupScale', + orient: 'left', + tickSize: 0, + labelOffset: 2, + labelPadding: 10, + labelColor: '#797979', + domain: false, + }, + { + scale: 'groupScale', + orient: 'right', + tickSize: 0, + labelOffset: 2, + labelPadding: 10, + domain: false, + encode: { + labels: { + update: { + text: { + signal: + "info(data('table')[datum.index * (length(data('table')) - 1)].currentAmount) != null ? format(info(data('table')[datum.index * (length(data('table')) - 1)].currentAmount), '') : ''", + }, + }, + }, + }, + }, + ]); + }); +}); + +describe('Threshold functionality', () => { + describe('Data generation', () => { + test('Should add threshold data and mark when thresholds are provided', () => { + const detailedThresholds = [ + { thresholdMax: 120, fill: 'rgb(234, 56, 41)' }, + { thresholdMin: 120, thresholdMax: 235, fill: 'rgb(249, 137, 23)' }, + { thresholdMin: 235, fill: 'rgb(21, 164, 110)' }, + ]; + const options = { + ...sampleOptionsRow, + name: 'testBullet', + thresholds: detailedThresholds, + }; + + const marksGroup = addMarks([], options)[0] as GroupMark; + expect(marksGroup.data).toBeDefined(); + expect(marksGroup.data?.[0].name).toBe('thresholds'); + + // Ensure that the generated values match the detailed thresholds. + const dataItem = marksGroup.data?.[0]; + expect(dataItem).toHaveProperty('values'); + const values = (dataItem as { values: unknown[] }).values; + expect(values).toEqual(detailedThresholds); + + const thresholdMark = marksGroup.marks?.find((mark) => mark.name === `${options.name}Threshold`); + expect(thresholdMark).toBeDefined(); + }); + }); + + describe('Y encoding', () => { + test('Should adjust y encoding when showTarget and showTargetValue is enabled', () => { + const options = { + ...sampleOptionsRow, + name: 'testBullet', + showTarget: true, + showTargetValue: true, + }; + expect(options.showTarget).toBe(true); + expect(options.showTargetValue).toBe(true); + + const thresholdMark = getBulletMarkThreshold(options); + expect(thresholdMark).toBeDefined(); + expect(thresholdMark.encode).toBeDefined(); + expect(thresholdMark.encode?.update).toBeDefined(); + + const yEncoding = thresholdMark.encode?.update?.y; + if (yEncoding && 'signal' in yEncoding) { + expect(yEncoding.signal).toContain('targetValueLabelHeight'); + const expectedSignal = 'bulletGroupHeight - 3 - bulletThresholdHeight - targetValueLabelHeight'; + expect(yEncoding.signal).toBe(expectedSignal); + } + }); + + test('Should compute y encoding without subtracting targetValueLabelHeight when showTargetValue is false', () => { + const options = { + ...sampleOptionsRow, + name: 'testBullet', + showTarget: true, + showTargetValue: false, + }; + const thresholdMark = getBulletMarkThreshold(options); + expect(thresholdMark).toBeDefined(); + expect(thresholdMark.encode).toBeDefined(); + expect(thresholdMark.encode?.update).toBeDefined(); + + const yEncoding = thresholdMark.encode?.update?.y; + if (yEncoding && 'signal' in yEncoding) { + expect(yEncoding.signal).not.toContain('targetValueLabelHeight'); + const expectedSignal = 'bulletGroupHeight - 3 - bulletThresholdHeight'; + expect(yEncoding.signal).toBe(expectedSignal); + } + }); + }); +}); + +describe('getBulletMarkTrack', () => { + test('Should return the correct track mark object in column mode', () => { + const options = { + ...sampleOptionsColumn, + name: 'testBullet', + threshold: false, + track: true, + }; + const data = getBulletTrack(options); + expect(data).toBeDefined(); + expect(data.encode?.update).toBeDefined(); + expect(Object.keys(data.encode?.update ?? {}).length).toBe(4); + expect(Object.keys(data.encode?.enter ?? {}).length).toBe(5); + expect(data.encode?.update?.width).toBeDefined(); + expect(data.encode?.update?.width).toStrictEqual({ signal: 'width' }); + }); + + test('Should return the correct track mark object in row mode', () => { + const options = { + ...sampleOptionsRow, + name: 'testBullet', + threshold: false, + track: true, + }; + const data = getBulletTrack(options); + expect(data.encode?.update?.width).toBeDefined(); + expect(data.encode?.update?.width).toStrictEqual({ signal: 'bulletGroupWidth' }); + }); + + test('Should return the correct track mark object when the target label is enabled', () => { + const options = { + ...sampleOptionsRow, + name: 'testBullet', + threshold: false, + track: true, + showTarget: true, + showTargetValue: true, + }; + const data = getBulletTrack(options); + expect(data.encode?.update?.y).toBeDefined(); + expect(data.encode?.update?.y).toStrictEqual({ signal: 'bulletGroupHeight - 3 - 2 * bulletHeight - 20' }); + }); +}); diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts new file mode 100644 index 000000000..4583c1192 --- /dev/null +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -0,0 +1,360 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { produce } from 'immer'; +import { Axis, GroupMark, Mark } from 'vega'; + +import { getColorValue } from '@spectrum-charts/themes'; + +import { BulletSpecOptions } from '../types'; + +export const addMarks = produce<Mark[], [BulletSpecOptions]>((marks, bulletOptions) => { + const markGroupEncodeUpdateDirection = bulletOptions.direction === 'column' ? 'y' : 'x'; + const bulletGroupWidth = bulletOptions.direction === 'column' ? 'width' : 'bulletGroupWidth'; + + const bulletMark: GroupMark = { + name: 'bulletGroup', + type: 'group', + from: { + facet: { data: 'table', name: 'bulletGroups', groupby: `${bulletOptions.dimension}` }, + }, + encode: { + update: { + [markGroupEncodeUpdateDirection]: { scale: 'groupScale', field: `${bulletOptions.dimension}` }, + height: { signal: 'bulletGroupHeight' }, + width: { signal: bulletGroupWidth }, + }, + }, + marks: [], + }; + + const thresholds = bulletOptions.thresholds; + + if (Array.isArray(thresholds) && thresholds.length > 0) { + bulletMark.data = [ + { + name: 'thresholds', + values: thresholds, + transform: [{ type: 'identifier', as: 'id' }], + }, + ]; + bulletMark.marks?.push(getBulletMarkThreshold(bulletOptions)); + } else if (bulletOptions.track) { + bulletMark.marks?.push(getBulletTrack(bulletOptions)); + } + + bulletMark.marks?.push(getBulletMarkRect(bulletOptions)); + if (bulletOptions.target && bulletOptions.showTarget !== false) { + bulletMark.marks?.push(getBulletMarkTarget(bulletOptions)); + if (bulletOptions.showTargetValue) { + bulletMark.marks?.push(getBulletMarkTargetValueLabel(bulletOptions)); + } + } + + if (bulletOptions.labelPosition === 'top' || bulletOptions.direction === 'row') { + bulletMark.marks?.push(getBulletMarkLabel(bulletOptions)); + bulletMark.marks?.push(getBulletMarkValueLabel(bulletOptions)); + } + + marks.push(bulletMark); +}); + +export function getBulletMarkRect(bulletOptions: BulletSpecOptions): Mark { + //The vertical positioning is calculated starting at the bulletgroupheight + //and then subtracting two times the bullet height to center the bullet bar + //in the middle of the threshold. The 3 is subtracted because the bulletgroup height + //starts the bullet below the threshold area. + //Additionally, the value of the targetValueLabelHeight is subtracted if the target value label is shown + //to make sure that the bullet bar is not drawn over the target value label. + const bulletMarkRectEncodeUpdateYSignal = + bulletOptions.showTarget && bulletOptions.showTargetValue + ? 'bulletGroupHeight - targetValueLabelHeight - 3 - 2 * bulletHeight' + : 'bulletGroupHeight - 3 - 2 * bulletHeight'; + + const fillColor = + bulletOptions.thresholdBarColor && (bulletOptions.thresholds?.length ?? 0) > 0 + ? [{ field: 'barColor' }] + : [{ value: bulletOptions.color }]; + + const bulletMarkRect: Mark = { + name: `${bulletOptions.name}Rect`, + description: `${bulletOptions.name}Rect`, + type: 'rect', + from: { data: 'bulletGroups' }, + encode: { + enter: { + cornerRadiusTopLeft: [{ test: `datum.${bulletOptions.metric} < 0`, value: 3 }], + cornerRadiusBottomLeft: [{ test: `datum.${bulletOptions.metric} < 0`, value: 3 }], + cornerRadiusTopRight: [{ test: `datum.${bulletOptions.metric} > 0`, value: 3 }], + cornerRadiusBottomRight: [{ test: `datum.${bulletOptions.metric} > 0`, value: 3 }], + fill: fillColor, + }, + update: { + x: { scale: 'xscale', value: 0 }, + x2: { scale: 'xscale', field: `${bulletOptions.metric}` }, + height: { signal: 'bulletHeight' }, + y: { signal: bulletMarkRectEncodeUpdateYSignal }, + }, + }, + }; + + return bulletMarkRect; +} + +export function getBulletMarkTarget(bulletOptions: BulletSpecOptions): Mark { + const solidColor = getColorValue('gray-900', bulletOptions.colorScheme); + + //When the target value label is shown, we must subtract the height of the target value label + //to make sure that the target line is not drawn over the target value label + const bulletMarkTargetEncodeUpdateY = + bulletOptions.showTarget && bulletOptions.showTargetValue + ? 'bulletGroupHeight - targetValueLabelHeight - targetHeight' + : 'bulletGroupHeight - targetHeight'; + const bulletMarkTargetEncodeUpdateY2 = + bulletOptions.showTarget && bulletOptions.showTargetValue + ? 'bulletGroupHeight - targetValueLabelHeight' + : 'bulletGroupHeight'; + + const bulletMarkTarget: Mark = { + name: `${bulletOptions.name}Target`, + description: `${bulletOptions.name}Target`, + type: 'rule', + from: { data: 'bulletGroups' }, + encode: { + enter: { + stroke: { value: `${solidColor}` }, + strokeWidth: { value: 2 }, + }, + update: { + x: { scale: 'xscale', field: `${bulletOptions.target}` }, + y: { signal: bulletMarkTargetEncodeUpdateY }, + y2: { signal: bulletMarkTargetEncodeUpdateY2 }, + }, + }, + }; + + return bulletMarkTarget; +} + +export function getBulletMarkLabel(bulletOptions: BulletSpecOptions): Mark { + const barLabelColor = getColorValue('gray-600', bulletOptions.colorScheme); + + const bulletMarkLabel: Mark = { + name: `${bulletOptions.name}Label`, + description: `${bulletOptions.name}Label`, + type: 'text', + from: { data: 'bulletGroups' }, + encode: { + enter: { + text: { signal: `datum.${bulletOptions.dimension}` }, + align: { value: 'left' }, + baseline: { value: 'top' }, + fill: { value: `${barLabelColor}` }, + }, + update: { x: { value: 0 }, y: { value: 0 } }, + }, + }; + + return bulletMarkLabel; +} + +export function getBulletMarkValueLabel(bulletOptions: BulletSpecOptions): Mark { + const defaultColor = getColorValue(bulletOptions.color, bulletOptions.colorScheme); + const solidColor = getColorValue('gray-900', bulletOptions.colorScheme); + const encodeUpdateSignalWidth = bulletOptions.direction === 'column' ? 'width' : 'bulletGroupWidth'; + const fillExpr = + bulletOptions.thresholdBarColor && (bulletOptions.thresholds?.length ?? 0) > 0 + ? `datum.barColor === '${defaultColor}' ? '${solidColor}' : datum.barColor` + : `'${solidColor}'`; + + const bulletMarkValueLabel: Mark = { + name: `${bulletOptions.name}ValueLabel`, + description: `${bulletOptions.name}ValueLabel`, + type: 'text', + from: { data: 'bulletGroups' }, + encode: { + enter: { + text: { + signal: `datum.${bulletOptions.metric} != null ? format(datum.${bulletOptions.metric}, '${ + bulletOptions.numberFormat || '' + }') : ''`, + }, + align: { value: 'right' }, + baseline: { value: 'top' }, + fill: { signal: fillExpr }, + }, + update: { x: { signal: encodeUpdateSignalWidth }, y: { value: 0 } }, + }, + }; + + return bulletMarkValueLabel; +} + +export function getBulletMarkTargetValueLabel(bulletOptions: BulletSpecOptions): Mark { + const solidColor = getColorValue('gray-900', bulletOptions.colorScheme); + + const bulletMarkTargetValueLabel: Mark = { + name: `${bulletOptions.name}TargetValueLabel`, + description: `${bulletOptions.name}TargetValueLabel`, + type: 'text', + from: { data: 'bulletGroups' }, + encode: { + enter: { + text: { + signal: `datum.${bulletOptions.target} != null ? 'Target: ' + format(datum.${bulletOptions.target}, '$,.2f') : 'No Target'`, + }, + align: { value: 'center' }, + baseline: { value: 'top' }, + fill: { value: `${solidColor}` }, + }, + update: { + x: { scale: 'xscale', field: `${bulletOptions.target}` }, + y: { signal: 'bulletGroupHeight - targetValueLabelHeight + 6' }, + }, + }, + }; + + return bulletMarkTargetValueLabel; +} + +export function getBulletMarkThreshold(bulletOptions: BulletSpecOptions): Mark { + // Vertically center the threshold bar by offsetting from bulletGroupHeight. + // Subtract 3 for alignment and targetValueLabelHeight if the label is shown. + const baseHeightSignal = 'bulletGroupHeight - 3 - bulletThresholdHeight'; + const encodeUpdateYSignal = + bulletOptions.showTarget && bulletOptions.showTargetValue + ? `${baseHeightSignal} - targetValueLabelHeight` + : baseHeightSignal; + + const bulletMarkThreshold: Mark = { + name: `${bulletOptions.name}Threshold`, + description: `${bulletOptions.name}Threshold`, + type: 'rect', + from: { data: 'thresholds' }, + clip: true, + encode: { + enter: { + cornerRadiusTopLeft: [{ test: `!isDefined(datum.thresholdMin) && domain('xscale')[0] !== 0`, value: 3 }], + cornerRadiusBottomLeft: [{ test: `!isDefined(datum.thresholdMin) && domain('xscale')[0] !== 0`, value: 3 }], + cornerRadiusTopRight: [{ test: `!isDefined(datum.thresholdMax) && domain('xscale')[1] !== 0`, value: 3 }], + cornerRadiusBottomRight: [{ test: `!isDefined(datum.thresholdMax) && domain('xscale')[1] !== 0`, value: 3 }], + fill: { field: 'fill' }, + fillOpacity: { value: 0.2 }, + }, + update: { + x: { + signal: "isDefined(datum.thresholdMin) ? scale('xscale', datum.thresholdMin) : 0", + }, + x2: { + signal: "isDefined(datum.thresholdMax) ? scale('xscale', datum.thresholdMax) : width", + }, + height: { signal: 'bulletThresholdHeight' }, + y: { signal: encodeUpdateYSignal }, + }, + }, + }; + return bulletMarkThreshold; +} + +export function getBulletTrack(bulletOptions: BulletSpecOptions): Mark { + const trackColor = getColorValue('gray-200', bulletOptions.colorScheme); + const trackWidth = bulletOptions.direction === 'column' ? 'width' : 'bulletGroupWidth'; + // Subtracting 20 accounts for the space used by the target value label + const trackY = + bulletOptions.showTarget && bulletOptions.showTargetValue + ? 'bulletGroupHeight - 3 - 2 * bulletHeight - 20' + : 'bulletGroupHeight - 3 - 2 * bulletHeight'; + + const bulletTrack: Mark = { + name: `${bulletOptions.name}Track`, + description: `${bulletOptions.name}Track`, + type: 'rect', + from: { data: 'bulletGroups' }, + encode: { + enter: { + fill: { value: trackColor }, + cornerRadiusTopRight: [{ test: "domain('xscale')[1] !== 0", value: 3 }], + cornerRadiusBottomRight: [{ test: "domain('xscale')[1] !== 0", value: 3 }], + cornerRadiusTopLeft: [{ test: "domain('xscale')[0] !== 0", value: 3 }], + cornerRadiusBottomLeft: [{ test: "domain('xscale')[0] !== 0", value: 3 }], + }, + update: { + x: { value: 0 }, + width: { signal: trackWidth }, + height: { signal: 'bulletHeight' }, + y: { signal: trackY }, + }, + }, + }; + + return bulletTrack; +} + +export function getBulletLabelAxesLeft(labelOffset): Axis { + return { + scale: 'groupScale', + orient: 'left', + tickSize: 0, + labelOffset: labelOffset, + labelPadding: 10, + labelColor: '#797979', + domain: false, + }; +} + +export function getBulletLabelAxesRight(bulletOptions: BulletSpecOptions, labelOffset): Axis { + return { + scale: 'groupScale', + orient: 'right', + tickSize: 0, + labelOffset: labelOffset, + labelPadding: 10, + domain: false, + encode: { + labels: { + update: { + text: { + signal: `info(data('table')[datum.index * (length(data('table')) - 1)].${ + bulletOptions.metric + }) != null ? format(info(data('table')[datum.index * (length(data('table')) - 1)].${ + bulletOptions.metric + }), '${bulletOptions.numberFormat || ''}') : ''`, + }, + }, + }, + }, + }; +} + +export function getBulletScaleAxes(): Axis { + return { + labelOffset: 2, + scale: 'xscale', + orient: 'bottom', + ticks: false, + labelColor: 'gray', + domain: false, + tickCount: 5, + offset: { signal: 'axisOffset' }, + }; +} + +export const addAxes = produce<Axis[], [BulletSpecOptions]>((axes, bulletOptions) => { + if (bulletOptions.metricAxis && bulletOptions.direction === 'column' && !bulletOptions.showTargetValue) { + axes.push(getBulletScaleAxes()); + } + + if (bulletOptions.labelPosition === 'side' && bulletOptions.direction === 'column') { + const labelOffset = bulletOptions.showTargetValue && bulletOptions.showTarget ? -8 : 2; + axes.push(getBulletLabelAxesLeft(labelOffset)); + axes.push(getBulletLabelAxesRight(bulletOptions, labelOffset)); + } +}); diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts new file mode 100644 index 000000000..7e71c84f4 --- /dev/null +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts @@ -0,0 +1,192 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { BulletOptions, ScSpec } from '../types'; +import { addBullet, addData, addScales, addSignals } from './bulletSpecBuilder'; +import { sampleOptionsColumn, sampleOptionsRow } from './bulletTestUtils'; + +describe('addBullet', () => { + let spec: ScSpec; + + beforeEach(() => { + spec = { data: [], marks: [], scales: [], usermeta: {} }; + }); + + test('should modify spec with bullet chart properties', () => { + const bulletOptions: BulletOptions & { idKey: string } = { + markType: 'bullet', + name: 'testBullet', + metric: 'revenue', + dimension: 'region', + target: 'goal', + idKey: 'rscMarkId', + }; + + const newSpec = addBullet(spec, bulletOptions); + + expect(newSpec).toBeDefined(); + expect(newSpec).toHaveProperty('data'); + expect(newSpec).toHaveProperty('marks'); + expect(newSpec).toHaveProperty('scales'); + expect(newSpec).toHaveProperty('signals'); + }); +}); + +describe('getBulletScales', () => { + test('Should return the correct scales object for column mode', () => { + const data = addScales([], sampleOptionsColumn); + expect(data).toBeDefined(); + expect(data).toHaveLength(2); + expect('range' in data[0] && data[0].range && data[0].range[1]).toBeTruthy(); + if ('range' in data[0] && data[0].range && data[0].range[1]) { + expect(data[0].range[1].signal).toBe('bulletChartHeight'); + } + }); + + test('Should return the correct scales object for row mode', () => { + const data = addScales([], sampleOptionsRow); + expect(data).toBeDefined(); + expect(data).toHaveLength(2); + expect('range' in data[0] && data[0].range && data[0].range[1]).toBeTruthy(); + if ('range' in data[0] && data[0].range && data[0].range[1]) { + expect(data[0].range[1].signal).toBe('width'); + } + }); + + test('Should return the correct scales object for flexible scale mode', () => { + const options = { ...sampleOptionsColumn, scaleType: 'flexible' as 'normal' | 'flexible' | 'fixed' }; + const data = addScales([], options); + expect(data).toBeDefined(); + expect(data[1].domain).toBeDefined(); + expect(data[1].domain).toStrictEqual({ + data: 'table', + fields: ['xPaddingForTarget', options.metric, 'flexibleScaleValue'], + }); + }); + + test('Should return the correct scales object for fixed scale mode', () => { + const options = { ...sampleOptionsColumn, scaleType: 'fixed' as 'normal' | 'flexible' | 'fixed' }; + const data = addScales([], options); + expect(data).toBeDefined(); + expect(data[1].domain).toBeDefined(); + expect(data[1].domain).toStrictEqual([0, `${options.maxScaleValue}`]); + }); + + test('Should return the correct scales object for normal scale mode', () => { + const options = { ...sampleOptionsColumn, scaleType: 'normal' as 'normal' | 'flexible' | 'fixed' }; + const data = addScales([], options); + expect(data).toBeDefined(); + expect(data[1].domain).toBeDefined(); + expect(data[1].domain).toStrictEqual({ data: 'table', fields: ['xPaddingForTarget', options.metric] }); + }); + + test('Should return the correct scales object when a negative value is passed for maxScaleValue', () => { + const options = { + ...sampleOptionsColumn, + scaleType: 'fixed' as 'normal' | 'flexible' | 'fixed', + maxScaleValue: -100, + }; + const data = addScales([], options); + expect(data).toBeDefined(); + expect(data[1].domain).toBeDefined(); + + // Expect normal scale mode to be used + expect(data[1].domain).toStrictEqual({ data: 'table', fields: ['xPaddingForTarget', options.metric] }); + }); +}); + +describe('getBulletSignals', () => { + test('Should return the correct signals object in column mode', () => { + const data = addSignals([], sampleOptionsColumn); + expect(data).toBeDefined(); + expect(data).toHaveLength(7); + }); + + test('Should return the correct signals object in row mode', () => { + const data = addSignals([], sampleOptionsRow); + expect(data).toBeDefined(); + expect(data).toHaveLength(8); + }); + + test('Should include targetValueLabelHeight signal when showTargetValue is true', () => { + const options = { ...sampleOptionsColumn, showTarget: true, showTargetValue: true }; + const signals = addSignals([], options); + expect(signals.find((signal) => signal.name === 'targetValueLabelHeight')).toBeDefined(); + }); + + test('Should include correct targetValueLabelHeight signal when showTargetValue is true', () => { + const options = { + ...sampleOptionsColumn, + showTarget: true, + showTargetValue: true, + labelPosition: 'side' as 'side' | 'top', + }; + const signals = addSignals([], options); + expect(signals.find((signal) => signal.name === 'bulletGroupHeight')).toStrictEqual({ + name: 'bulletGroupHeight', + update: 'bulletThresholdHeight + targetValueLabelHeight + 10', + }); + }); + + test('Should include correct targetValueLabelHeight signal when showTargetValue is true', () => { + const options = { + ...sampleOptionsColumn, + showTarget: true, + showTargetValue: true, + labelPosition: 'top' as 'side' | 'top', + }; + const signals = addSignals([], options); + expect(signals.find((signal) => signal.name === 'bulletGroupHeight')).toStrictEqual({ + name: 'bulletGroupHeight', + update: 'bulletThresholdHeight + targetValueLabelHeight + 24', + }); + }); + + test('Should include correct targetValueLabelHeight signal when showTargetValue is true', () => { + const options = { + ...sampleOptionsColumn, + showTarget: true, + showTargetValue: false, + labelPosition: 'side' as 'side' | 'top', + }; + const signals = addSignals([], options); + expect(signals.find((signal) => signal.name === 'bulletGroupHeight')).toStrictEqual({ + name: 'bulletGroupHeight', + update: 'bulletThresholdHeight + 10', + }); + }); + + test('Should include correct bulletChartHeight signal when options.axis is true and showTargetValue is false', () => { + const options = { + ...sampleOptionsColumn, + showTargetValue: false, + metricAxis: true, + }; + const signals = addSignals([], options); + expect(signals.find((signal) => signal.name === 'bulletChartHeight')).toStrictEqual({ + name: 'bulletChartHeight', + update: "length(data('table')) * bulletGroupHeight + (length(data('table')) - 1) * gap + 10", + }); + }); +}); + +describe('getBulletData', () => { + test('Should return the data object', () => { + const data = addData([], sampleOptionsColumn); + expect(data).toHaveLength(1); + }); + + test('Should return the correct data object in flexible scale mode', () => { + const options = { ...sampleOptionsColumn, scaleType: 'flexible' as 'normal' | 'flexible' | 'fixed' }; + const data = addData([], options); + expect(data[0].transform).toHaveLength(2); + }); +}); diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts new file mode 100644 index 000000000..3534a5d8c --- /dev/null +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -0,0 +1,181 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { produce } from 'immer'; +import { Data, Scale, Signal } from 'vega'; + +import { + DEFAULT_GAUGE_DIRECTION, + DEFAULT_COLOR_SCHEME, + DEFAULT_LABEL_POSITION, + DEFAULT_SCALE_TYPE, + DEFAULT_SCALE_VALUE, +} from '@spectrum-charts/constants'; +import { getColorValue, spectrumColors } from '@spectrum-charts/themes'; +import { toCamelCase } from '@spectrum-charts/utils'; + +import { GaugeOptions, GaugeSpecOptions, ColorScheme, ScSpec } from '../types'; +import { getGaugeTableData, getGaugeTransforms } from './gaugeDataUtils'; +import { addAxes, addMarks } from './gaugeMarkUtils'; + +const DEFAULT_COLOR = spectrumColors.light['static-blue']; + +export const addGauge = produce< + ScSpec, + [GaugeOptions & { colorScheme?: ColorScheme; index?: number; idKey: string }] +>( + ( + spec, + { + colorScheme = DEFAULT_COLOR_SCHEME, + index = 0, + name, + metric, + dimension, + target, + color = DEFAULT_COLOR, + direction = DEFAULT_GAUGE_DIRECTION, + numberFormat, + showTarget = true, + showTargetValue = false, + labelPosition = DEFAULT_LABEL_POSITION, + scaleType = DEFAULT_SCALE_TYPE, + maxScaleValue = DEFAULT_SCALE_VALUE, + thresholds = [], + track = false, + thresholdBarColor = false, + metricAxis = false, + ...options + } + ) => { + const gaugeOptions: GaugeSpecOptions = { + colorScheme: colorScheme, + index, + color: getColorValue(color, colorScheme), + metric: metric ?? 'currentAmount', + dimension: dimension ?? 'graphLabel', + target: target ?? 'target', + name: toCamelCase(name ?? `gauge${index}`), + direction: direction, + numberFormat: numberFormat ?? '', + showTarget: showTarget, + showTargetValue: showTargetValue, + labelPosition: labelPosition, + scaleType: scaleType, + maxScaleValue: maxScaleValue, + track: track, + thresholds: thresholds, + thresholdBarColor: thresholdBarColor, + metricAxis: metricAxis, + ...options, + }; + + spec.data = addData(spec.data ?? [], gaugeOptions); + spec.marks = addMarks(spec.marks ?? [], gaugeOptions); + spec.scales = addScales(spec.scales ?? [], gaugeOptions); + spec.signals = addSignals(spec.signals ?? [], gaugeOptions); + spec.axes = addAxes(spec.axes ?? [], gaugeOptions); + } +); + +export const addScales = produce<Scale[], [GaugeSpecOptions]>((scales, options) => { + const groupScaleRangeSignal = options.direction === 'column' ? 'gaugeChartHeight' : 'width'; + const xRange = options.direction === 'column' ? 'width' : [0, { signal: 'gaugeGroupWidth' }]; + let domainFields; + + if (options.scaleType === 'flexible' && options.maxScaleValue > 0) { + domainFields = { data: 'table', fields: ['xPaddingForTarget', options.metric, 'flexibleScaleValue'] }; + } else if (options.scaleType === 'fixed' && options.maxScaleValue > 0) { + domainFields = [0, `${options.maxScaleValue}`]; + } else { + domainFields = { data: 'table', fields: ['xPaddingForTarget', options.metric] }; + } + + scales.push( + { + name: 'groupScale', + type: 'band', + domain: { data: 'table', field: options.dimension }, + range: [0, { signal: groupScaleRangeSignal }], + paddingInner: { signal: 'paddingRatio' }, + }, + { + name: 'xscale', + type: 'linear', + domain: domainFields, + range: xRange, + round: true, + clamp: true, + zero: true, + } + ); +}); + +export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, options) => { + signals.push({ name: 'gap', value: 12 }); + signals.push({ name: 'gaugeHeight', value: 8 }); + signals.push({ name: 'gaugeThresholdHeight', update: 'gaugeHeight * 3' }); + signals.push({ name: 'targetHeight', update: 'gaugeThresholdHeight + 6' }); + + if (options.showTargetValue && options.showTarget) { + signals.push({ name: 'targetValueLabelHeight', update: '20' }); + } + + signals.push({ + name: 'gaugeGroupHeight', + update: getGaugeGroupHeightExpression(options), + }); + + if (options.direction === 'column') { + signals.push({ name: 'paddingRatio', update: 'gap / (gap + gaugeGroupHeight)' }); + + if (options.metricAxis && !options.showTargetValue) { + signals.push({ + name: 'gaugeChartHeight', + update: "length(data('table')) * gaugeGroupHeight + (length(data('table')) - 1) * gap + 10", + }); + signals.push({ + name: 'axisOffset', + update: 'gaugeChartHeight - height - 10', + }); + } else { + signals.push({ + name: 'gaugeChartHeight', + update: "length(data('table')) * gaugeGroupHeight + (length(data('table')) - 1) * gap", + }); + } + } else { + signals.push({ name: 'gaugeGroupWidth', update: "(width / length(data('table'))) - gap" }); + signals.push({ name: 'paddingRatio', update: 'gap / (gap + gaugeGroupWidth)' }); + signals.push({ name: 'gaugeChartHeight', update: 'gaugeGroupHeight' }); + } +}); + +/** + * Returns the height of the bullet group based on the options + * @param options the bullet spec options + * @returns the height of the bullet group + */ +function getGaugeGroupHeightExpression(options: GaugeSpecOptions): string { + if (options.showTargetValue && options.showTarget) { + return options.labelPosition === 'side' && options.direction === 'column' + ? 'gaugeThresholdHeight + targetValueLabelHeight + 10' + : 'gaugeThresholdHeight + targetValueLabelHeight + 24'; + } else if (options.labelPosition === 'side' && options.direction === 'column') { + return 'gaugeThresholdHeight + 10'; + } + return 'gaugeThresholdHeight + 24'; +} + +export const addData = produce<Data[], [GaugeSpecOptions]>((data, options) => { + const tableData = getGaugeTableData(data); + tableData.transform = getGaugeTransforms(options); +}); diff --git a/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts new file mode 100644 index 000000000..b401dae75 --- /dev/null +++ b/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts @@ -0,0 +1,54 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { BulletSpecOptions } from '../types'; + +export const sampleOptionsColumn: BulletSpecOptions = { + markType: 'bullet', + colorScheme: 'light', + index: 0, + color: 'green', + metric: 'currentAmount', + dimension: 'graphLabel', + target: 'target', + name: 'bullet0', + idKey: 'rscMarkId', + direction: 'column', + showTarget: true, + showTargetValue: false, + labelPosition: 'top', + scaleType: 'normal', + maxScaleValue: 100, + metricAxis: false, + track: false, + thresholdBarColor: false, +}; + +export const sampleOptionsRow: BulletSpecOptions = { + markType: 'bullet', + colorScheme: 'light', + index: 0, + color: 'green', + metric: 'currentAmount', + dimension: 'graphLabel', + target: 'target', + name: 'bullet0', + idKey: 'rscMarkId', + direction: 'row', + showTarget: true, + showTargetValue: false, + labelPosition: 'top', + scaleType: 'normal', + maxScaleValue: 100, + track: false, + thresholdBarColor: false, + metricAxis: false, +}; diff --git a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts new file mode 100644 index 000000000..7ba04d4b6 --- /dev/null +++ b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts @@ -0,0 +1,95 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { ColorScheme } from '../chartSpec.types'; +import { NumberFormat, PartiallyRequired } from '../specUtil.types'; + +export type ThresholdBackground = { thresholdMin?: number; thresholdMax?: number; fill?: string }; + +export interface GaugeOptions { + markType: 'gauge'; + + /** Key in the data that is used as the color facet */ + color?: string; + /** Data field that the metric is trended against (x-axis for horizontal orientation) */ + dimension?: string; + /** Specifies the direction the bars should be ordered (row/column) */ + direction?: 'row' | 'column'; + /** Specifies if the labels should be in top of the bullet chart or to the side. Side labels are not supported in row mode. */ + labelPosition?: 'side' | 'top'; + /** Maximum value for the scale. This value must be greater than zero. */ + maxScaleValue?: number; + /** Key in the data that is used as the metric */ + metric?: string; + /** Adds an axis that follows the max target in basic mode */ + metricAxis?: boolean; + /** Sets the name of the component. */ + name?: string; + /** d3 number format specifier. + * Sets the number format for the summary value. + * + * see {@link https://d3js.org/d3-format#locale_format} + */ + numberFormat?: NumberFormat; + /** Specifies if the scale should be normal, fixed, or flexible. + * + * In normal mode the maximum scale value will be calculated using the maximum value of the metric and target data fields. + * + * In fixed mode the maximum scale value will be set as the maxScaleValue prop. + * + * In flexible mode the maximum scale value will be calculated using the maximum value of either the maxScaleValue prop or maximum value of the metric and target data fields. + * This means that the scale max will be set to be the maxScaleValue prop until the data values overtake it. + */ + scaleType?: 'normal' | 'fixed' | 'flexible'; + /** Flag to control whether the target is shown */ + showTarget?: boolean; + /** Flag to control whether the target value is shown. */ + showTargetValue?: boolean; + /** Target line */ + target?: string; + /** changes color based on threshold */ + /** If true, the metric bar will be colored according to the thresholds. */ + thresholdBarColor?: boolean; + /** Array of threshold definitions to be rendered as background bands on the bullet chart. + * + * Each threshold object supports: + * `thresholdMin` (optional): The lower bound of the threshold. If undefined, the threshold starts from the beginning of the x-scale. + * + * `thresholdMax` (optional): The upper bound of the threshold. If undefined, the threshold extends to the end of the x-scale. + * + * `fill` : The fill color to use for the threshold background. + */ + thresholds?: ThresholdBackground[]; + /** Color regions that sit behind the bullet bar */ + track?: boolean; +} + +type GaugeOptionsWithDefaults = + | 'name' + | 'metric' + | 'dimension' + | 'target' + | 'color' + | 'direction' + | 'showTarget' + | 'showTargetValue' + | 'labelPosition' + | 'scaleType' + | 'maxScaleValue' + | 'track' + | 'metricAxis' + | 'thresholdBarColor'; + +export interface GaugeSpecOptions extends PartiallyRequired<GaugeOptions, GaugeOptionsWithDefaults> { + colorScheme: ColorScheme; + idKey: string; + index: number; +} diff --git a/packages/vega-spec-builder/src/types/marks/index.ts b/packages/vega-spec-builder/src/types/marks/index.ts index b0bc1076f..d84369eae 100644 --- a/packages/vega-spec-builder/src/types/marks/index.ts +++ b/packages/vega-spec-builder/src/types/marks/index.ts @@ -14,6 +14,7 @@ export * from './areaSpec.types'; export * from './barSpec.types'; export * from './bigNumberSpec.types'; export * from './bulletSpec.types'; +export * from './gaugeSpec.types'; export * from './comboSpec.types'; export * from './donutSpec.types'; export * from './lineSpec.types'; From 7ace04dbc6cf9d9dc21de6f5de3935fcf75b6e71 Mon Sep 17 00:00:00 2001 From: JarekSmith <105569335+JarekSmith@users.noreply.github.com> Date: Thu, 9 Oct 2025 14:22:44 -0600 Subject: [PATCH 02/66] Make gauge appear in Storybook with Bullet graph --- .../src/alpha/components/Gauge/Gauge.tsx | 10 +++++----- .../src/stories/components/Gauge/Gauge.story.tsx | 2 +- .../react-spectrum-charts/src/types/marks/index.ts | 1 + 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx index 0c5d007b1..c85ab17b6 100644 --- a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx +++ b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx @@ -14,20 +14,20 @@ import { FC } from 'react'; import { - DEFAULT_GAUGE_DIRECTION, + DEFAULT_BULLET_DIRECTION, DEFAULT_LABEL_POSITION, DEFAULT_SCALE_TYPE, DEFAULT_SCALE_VALUE, } from '@spectrum-charts/constants'; -import { GaugeProps } from '../../../types'; +import { BulletProps } from '../../../types'; -const Gauge: FC<GaugeProps> = ({ - name = 'bullet0', +const Gauge: FC<BulletProps> = ({ + name = 'gauge0', metric = 'currentAmount', dimension = 'graphLabel', target = 'target', - direction = DEFAULT_GAUGE_DIRECTION, + direction = DEFAULT_BULLET_DIRECTION, numberFormat = '', showTarget = true, showTargetValue = false, diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx index 350e9ac3f..62fec39c2 100644 --- a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx @@ -23,7 +23,7 @@ import { BulletProps, ChartProps } from '../../../types'; import { basicBulletData, basicThresholdsData, coloredThresholdsData } from './data'; export default { - title: 'RSC/Bullet (alpha)', + title: 'RSC/Gauge (alpha)', component: Bullet, }; diff --git a/packages/react-spectrum-charts/src/types/marks/index.ts b/packages/react-spectrum-charts/src/types/marks/index.ts index 05e62d712..3b71c1ac2 100644 --- a/packages/react-spectrum-charts/src/types/marks/index.ts +++ b/packages/react-spectrum-charts/src/types/marks/index.ts @@ -16,6 +16,7 @@ export * from './bigNumber.types'; export * from './bullet.types'; export * from './combo.types'; export * from './donut.types'; +export * from './gauge.types'; export * from './line.types'; export * from './scatter.types'; export * from './venn.types'; From 58581f5c57246240048a265f2722bf68c92bec92 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Thu, 9 Oct 2025 21:03:06 -0600 Subject: [PATCH 03/66] Found things we need to ask questions about --- .../src/alpha/components/Gauge/Gauge.tsx | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx index c85ab17b6..a62788e75 100644 --- a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx +++ b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx @@ -23,18 +23,27 @@ import { import { BulletProps } from '../../../types'; const Gauge: FC<BulletProps> = ({ - name = 'gauge0', - metric = 'currentAmount', - dimension = 'graphLabel', - target = 'target', - direction = DEFAULT_BULLET_DIRECTION, - numberFormat = '', - showTarget = true, - showTargetValue = false, - labelPosition = DEFAULT_LABEL_POSITION, - scaleType = DEFAULT_SCALE_TYPE, - maxScaleValue = DEFAULT_SCALE_VALUE, - thresholdBarColor = false, + name = 'gauge0', // Why the zero? + metric = 'currentAmount', // CurrVal + dimension = 'graphLabel', // Graph Title ? + target = 'target', // Yes + direction = DEFAULT_BULLET_DIRECTION, // Left to right Note to selves: Do this + numberFormat = '', // ints or floats ??? Help Mr Almighty Wizard ??? + showTarget = true, // Where you want + showTargetValue = false, // Number of what you want + labelPosition = DEFAULT_LABEL_POSITION, // Above gauge to label the needle + scaleType = DEFAULT_SCALE_TYPE, // Need Angle and Tick + maxScaleValue = DEFAULT_SCALE_VALUE, // Max Arc Value + thresholdBarColor = false, // filler color + // Things to add: + // ticksNumber + // clamping + // needle (on or off, sizing of it) + // arcMinVal and arcMaxVal + // size of tick lines + // needle length + // arc thickness + // label ticks? Enable and disable? }) => { return null; }; From 6678a60fbd167d0d832411d3a51aecdf683cf1ba Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Fri, 10 Oct 2025 18:40:21 -0600 Subject: [PATCH 04/66] adding case for gauge on chartSpecBuilder.ts --- .../src/alpha/components/Gauge/Gauge.tsx | 6 +++--- packages/vega-spec-builder/src/chartSpecBuilder.ts | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx index a62788e75..0f2099f92 100644 --- a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx +++ b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx @@ -23,11 +23,11 @@ import { import { BulletProps } from '../../../types'; const Gauge: FC<BulletProps> = ({ - name = 'gauge0', // Why the zero? + name = 'gauge0', metric = 'currentAmount', // CurrVal dimension = 'graphLabel', // Graph Title ? - target = 'target', // Yes - direction = DEFAULT_BULLET_DIRECTION, // Left to right Note to selves: Do this + target = 'target', + direction = DEFAULT_BULLET_DIRECTION, // Left to right Note to selves: Not today numberFormat = '', // ints or floats ??? Help Mr Almighty Wizard ??? showTarget = true, // Where you want showTargetValue = false, // Number of what you want diff --git a/packages/vega-spec-builder/src/chartSpecBuilder.ts b/packages/vega-spec-builder/src/chartSpecBuilder.ts index 041cda880..c4fcf9125 100644 --- a/packages/vega-spec-builder/src/chartSpecBuilder.ts +++ b/packages/vega-spec-builder/src/chartSpecBuilder.ts @@ -129,7 +129,7 @@ export function buildSpec({ spec.signals = getDefaultSignals(options); spec.scales = getDefaultScales(colors, colorScheme, lineTypes, lineWidths, opacities, symbolShapes, symbolSizes); - let { areaCount, barCount, bulletCount, comboCount, donutCount, lineCount, scatterCount, vennCount } = + let { areaCount, barCount, bulletCount, comboCount, donutCount, gaugeCount, lineCount, scatterCount, vennCount } = initializeComponentCounts(); const specOptions = { colorScheme, idKey, highlightedItem }; spec = [...marks].reduce((acc: ScSpec, mark) => { @@ -149,6 +149,9 @@ export function buildSpec({ case 'donut': donutCount++; return addDonut(acc, { ...mark, ...specOptions, index: donutCount }); + case 'gauge': + gaugeCount++; + return addGauge(acc, { ...mark, ...specOptions, index: gaugeCount }); case 'line': lineCount++; return addLine(acc, { ...mark, ...specOptions, index: lineCount }); From 491fdd89d075f89f6dccd326fc1dddaaca0d7adb Mon Sep 17 00:00:00 2001 From: Brady Shimanek <bradyshimanek33@gmail.com> Date: Thu, 23 Oct 2025 21:03:35 -0600 Subject: [PATCH 05/66] Began adding Signals --- .../src/alpha/components/Gauge/Gauge.tsx | 5 +- .../src/alpha/components/index.ts | 2 +- .../src/rscToSbAdapter/childrenAdapter.ts | 7 +- .../components/Bullet/Bullet.story.tsx | 2 +- .../stories/components/Gauge/Gauge.story.tsx | 212 +++++++++--------- .../src/stories/components/Gauge/data.ts | 6 +- .../src/types/chart.types.ts | 2 + .../react-spectrum-charts/src/utils/utils.ts | 10 +- .../vega-spec-builder/src/chartSpecBuilder.ts | 4 +- .../src/gauge/gaugeSpecBuilder.test.ts | 2 +- .../src/gauge/gaugeSpecBuilder.ts | 209 ++++++----------- .../src/types/chartSpec.types.ts | 2 + 12 files changed, 212 insertions(+), 251 deletions(-) diff --git a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx index 0f2099f92..5840589a5 100644 --- a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx +++ b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx @@ -20,9 +20,10 @@ import { DEFAULT_SCALE_VALUE, } from '@spectrum-charts/constants'; -import { BulletProps } from '../../../types'; +import { GaugeProps } from '../../../types'; -const Gauge: FC<BulletProps> = ({ +// I assume this houses all the props for all variations of a Gauge chart? +const Gauge: FC<GaugeProps> = ({ name = 'gauge0', metric = 'currentAmount', // CurrVal dimension = 'graphLabel', // Graph Title ? diff --git a/packages/react-spectrum-charts/src/alpha/components/index.ts b/packages/react-spectrum-charts/src/alpha/components/index.ts index 83326d4d2..ea2dcf0e4 100644 --- a/packages/react-spectrum-charts/src/alpha/components/index.ts +++ b/packages/react-spectrum-charts/src/alpha/components/index.ts @@ -13,4 +13,4 @@ export * from './Bullet'; export * from './Combo'; export * from './Venn'; -export * from './Gauge'; +export * from './Gauge/Gauge'; diff --git a/packages/react-spectrum-charts/src/rscToSbAdapter/childrenAdapter.ts b/packages/react-spectrum-charts/src/rscToSbAdapter/childrenAdapter.ts index 37283ebba..6a4b33744 100644 --- a/packages/react-spectrum-charts/src/rscToSbAdapter/childrenAdapter.ts +++ b/packages/react-spectrum-charts/src/rscToSbAdapter/childrenAdapter.ts @@ -18,6 +18,7 @@ import { ChartPopoverOptions, ChartTooltipOptions, DonutSummaryOptions, + GaugeOptions, LegendOptions, LineOptions, MarkOptions, @@ -30,7 +31,7 @@ import { TrendlineOptions, } from '@spectrum-charts/vega-spec-builder'; -import { Bullet, Combo, Venn } from '../alpha'; +import { Bullet, Combo, Gauge, Venn } from '../alpha'; import { Annotation } from '../components/Annotation'; import { Area } from '../components/Area'; import { Axis } from '../components/Axis'; @@ -158,6 +159,10 @@ export const childrenToOptions = ( marks.push({ ...child.props, markType: 'bullet' } as BulletOptions); break; + case Gauge.displayName: + marks.push({ ...child.props, markType: 'gauge' } as GaugeOptions); + break; + case ChartPopover.displayName: chartPopovers.push(getChartPopoverOptions(child.props as ChartPopoverProps)); break; diff --git a/packages/react-spectrum-charts/src/stories/components/Bullet/Bullet.story.tsx b/packages/react-spectrum-charts/src/stories/components/Bullet/Bullet.story.tsx index 350e9ac3f..81c4b7536 100644 --- a/packages/react-spectrum-charts/src/stories/components/Bullet/Bullet.story.tsx +++ b/packages/react-spectrum-charts/src/stories/components/Bullet/Bullet.story.tsx @@ -39,7 +39,7 @@ const BulletStory: StoryFn<BulletProps & { width?: number; height?: number }> = const { width, height, ...bulletProps } = args; const chartProps = useChartProps({ ...defaultChartProps, width: width ?? 350, height: height ?? 350 }); return ( - <Chart {...chartProps}> + <Chart {...chartProps} debug> <Bullet {...bulletProps} /> </Chart> ); diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx index 62fec39c2..5d28aca07 100644 --- a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx @@ -14,49 +14,50 @@ import { ReactElement } from 'react'; import { StoryFn } from '@storybook/react'; import { Chart } from '../../../Chart'; -// Assuming Bullet chart is a component in the @rsc/alpha export -import { Bullet } from '../../../alpha'; +// Gauge chart component from alpha export +import { Gauge } from '../../../alpha'; import { Title } from '../../../components'; import useChartProps from '../../../hooks/useChartProps'; import { bindWithProps } from '../../../test-utils'; -import { BulletProps, ChartProps } from '../../../types'; -import { basicBulletData, basicThresholdsData, coloredThresholdsData } from './data'; +import { GaugeProps, ChartProps } from '../../../types'; +import { basicGaugeData, basicThresholdsData, coloredThresholdsData } from './data'; export default { title: 'RSC/Gauge (alpha)', - component: Bullet, + component: Gauge, }; // Default chart properties const defaultChartProps: ChartProps = { - data: basicBulletData, + data: basicGaugeData, width: 350, height: 350, }; -// Basic Bullet chart story -const BulletStory: StoryFn<BulletProps & { width?: number; height?: number }> = (args): ReactElement => { - const { width, height, ...bulletProps } = args; - const chartProps = useChartProps({ ...defaultChartProps, width: width ?? 350, height: height ?? 350 }); +// Basic Gauge chart story +const GaugeStory: StoryFn<GaugeProps & { width?: number; height?: number }> = (args): ReactElement => { + const { width, height, ...gaugeProps } = args; + const chartProps = useChartProps({ ...defaultChartProps, width: width ?? 500, height: height ?? 500 }); return ( - <Chart {...chartProps}> - <Bullet {...bulletProps} /> + <Chart {...chartProps} debug> + <Gauge {...gaugeProps} /> </Chart> ); }; -// Bullet with Title -const BulletTitleStory: StoryFn<typeof Bullet> = (args): ReactElement => { +// Gauge with Title +const GaugeTitleStory: StoryFn<typeof Gauge> = (args): ReactElement => { const chartProps = useChartProps({ ...defaultChartProps, width: 400 }); return ( <Chart {...chartProps}> - <Title text={'Title Bullet'} position={'start'} orient={'top'} /> - <Bullet {...args} /> + <Title text={'Title Gauge'} position={'start'} orient={'top'} /> + <Gauge {...args} /> </Chart> ); }; -const Basic = bindWithProps(BulletStory); +// Basic Gauge chart story. All the ones below it are variations of the Gauge chart. +const Basic = bindWithProps(GaugeStory); Basic.args = { metric: 'currentAmount', dimension: 'graphLabel', @@ -74,12 +75,12 @@ Basic.args = { metricAxis: false, }; -const Thresholds = bindWithProps(BulletStory); -Thresholds.args = { +const GaugeVariation2 = bindWithProps(GaugeStory); +GaugeVariation2.args = { metric: 'currentAmount', dimension: 'graphLabel', target: 'target', - color: 'blue-900', + color: 'red-900', direction: 'column', numberFormat: '$,.2f', showTarget: true, @@ -93,12 +94,12 @@ Thresholds.args = { metricAxis: false, }; -const ColoredMetric = bindWithProps(BulletStory); -ColoredMetric.args = { +const GaugeVariation3 = bindWithProps(GaugeStory); +GaugeVariation3.args = { metric: 'currentAmount', dimension: 'graphLabel', target: 'target', - color: 'blue-900', + color: 'fuchsia-900', direction: 'column', numberFormat: '$,.2f', showTarget: true, @@ -112,90 +113,91 @@ ColoredMetric.args = { metricAxis: false, }; -const Track = bindWithProps(BulletStory); -Track.args = { - metric: 'currentAmount', - dimension: 'graphLabel', - target: 'target', - color: 'blue-900', - direction: 'column', - numberFormat: '$,.2f', - showTarget: true, - showTargetValue: false, - labelPosition: 'top', - scaleType: 'normal', - maxScaleValue: 100, - track: true, - metricAxis: false, -}; +// const Track = bindWithProps(GaugeStory); +// Track.args = { +// metric: 'currentAmount', +// dimension: 'graphLabel', +// target: 'target', +// color: 'blue-900', +// direction: 'column', +// numberFormat: '$,.2f', +// showTarget: true, +// showTargetValue: false, +// labelPosition: 'top', +// scaleType: 'normal', +// maxScaleValue: 100, +// track: true, +// metricAxis: false, +// }; -const RowMode = bindWithProps(BulletStory); -RowMode.args = { - metric: 'currentAmount', - dimension: 'graphLabel', - target: 'target', - color: 'blue-900', - direction: 'row', - numberFormat: '$,.2f', - showTarget: true, - showTargetValue: false, - labelPosition: 'top', - scaleType: 'normal', - maxScaleValue: 100, - thresholds: coloredThresholdsData, - thresholdBarColor: true, - track: false, - metricAxis: false, -}; +// const RowMode = bindWithProps(GaugeStory); +// RowMode.args = { +// metric: 'currentAmount', +// dimension: 'graphLabel', +// target: 'target', +// color: 'blue-900', +// direction: 'row', +// numberFormat: '$,.2f', +// showTarget: true, +// showTargetValue: false, +// labelPosition: 'top', +// scaleType: 'normal', +// maxScaleValue: 100, +// thresholds: coloredThresholdsData, +// thresholdBarColor: true, +// track: false, +// metricAxis: false, +// }; -const WithTitle = bindWithProps(BulletTitleStory); -WithTitle.args = { - metric: 'currentAmount', - dimension: 'graphLabel', - target: 'target', - color: 'blue-900', - numberFormat: '$,.2f', - labelPosition: 'top', - scaleType: 'normal', - maxScaleValue: 100, - track: false, - direction: 'column', - metricAxis: false, -}; +// const WithTitle = bindWithProps(GaugeTitleStory); +// WithTitle.args = { +// metric: 'currentAmount', +// dimension: 'graphLabel', +// target: 'target', +// color: 'blue-900', +// numberFormat: '$,.2f', +// labelPosition: 'top', +// scaleType: 'normal', +// maxScaleValue: 100, +// track: false, +// direction: 'column', +// metricAxis: false, +// }; -const FixedScale = bindWithProps(BulletStory); -FixedScale.args = { - metric: 'currentAmount', - dimension: 'graphLabel', - target: 'target', - color: 'blue-900', - direction: 'column', - numberFormat: '$,.2f', - showTarget: true, - showTargetValue: false, - labelPosition: 'top', - scaleType: 'fixed', - maxScaleValue: 250, - thresholds: basicThresholdsData, - track: false, - metricAxis: false, -}; +// const FixedScale = bindWithProps(GaugeStory); +// FixedScale.args = { +// metric: 'currentAmount', +// dimension: 'graphLabel', +// target: 'target', +// color: 'blue-900', +// direction: 'column', +// numberFormat: '$,.2f', +// showTarget: true, +// showTargetValue: false, +// labelPosition: 'top', +// scaleType: 'fixed', +// maxScaleValue: 250, +// thresholds: basicThresholdsData, +// track: false, +// metricAxis: false, +// }; -const MetricAxis = bindWithProps(BulletStory); -MetricAxis.args = { - metric: 'currentAmount', - dimension: 'graphLabel', - target: 'target', - color: 'blue-900', - direction: 'column', - numberFormat: '$,.2f', - showTarget: true, - showTargetValue: false, - labelPosition: 'top', - scaleType: 'normal', - maxScaleValue: 250, - track: false, - metricAxis: true, -}; +// const MetricAxis = bindWithProps(GaugeStory); +// MetricAxis.args = { +// metric: 'currentAmount', +// dimension: 'graphLabel', +// target: 'target', +// color: 'blue-900', +// direction: 'column', +// numberFormat: '$,.2f', +// showTarget: true, +// showTargetValue: false, +// labelPosition: 'top', +// scaleType: 'normal', +// maxScaleValue: 250, +// track: false, +// metricAxis: true, +// }; -export { Basic, Thresholds, ColoredMetric, Track, RowMode, WithTitle, FixedScale, MetricAxis }; +export { Basic, GaugeVariation2, GaugeVariation3 }; +// export { Basic, GaugeVariation2, GaugeVariation3, Track, RowMode, WithTitle, FixedScale, MetricAxis }; diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts b/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts index 549d87dce..8ed75a9c0 100644 --- a/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts @@ -10,7 +10,11 @@ * governing permissions and limitations under the License. */ -export const basicBulletData = [ + +// ok this is the actual data for the bullet chart. +// We can keep it for now once we get a working Gauge chart to render. + +export const basicGaugeData = [ { graphLabel: 'Customers', currentAmount: 150, target: 50 }, { graphLabel: 'Revenue', currentAmount: 350, target: 450 }, ]; diff --git a/packages/react-spectrum-charts/src/types/chart.types.ts b/packages/react-spectrum-charts/src/types/chart.types.ts index 2cd21f23c..69cb33b5d 100644 --- a/packages/react-spectrum-charts/src/types/chart.types.ts +++ b/packages/react-spectrum-charts/src/types/chart.types.ts @@ -36,6 +36,7 @@ import { ComboElement, DonutElement, DonutSummaryElement, + GaugeElement, LineElement, MetricRangeElement, ScatterElement, @@ -54,6 +55,7 @@ export type ChartChildElement = | BigNumberElement | DonutElement | ComboElement + | GaugeElement | LegendElement | LineElement | ScatterElement diff --git a/packages/react-spectrum-charts/src/utils/utils.ts b/packages/react-spectrum-charts/src/utils/utils.ts index 712e1084e..ebafc9843 100644 --- a/packages/react-spectrum-charts/src/utils/utils.ts +++ b/packages/react-spectrum-charts/src/utils/utils.ts @@ -17,7 +17,7 @@ import { SELECTED_GROUP, SELECTED_ITEM, SELECTED_SERIES, SERIES_ID } from '@spec import { combineNames, toCamelCase } from '@spectrum-charts/utils'; import { Datum } from '@spectrum-charts/vega-spec-builder'; -import { Bullet, Combo, Venn } from '../alpha'; +import { Bullet, Combo, Gauge, Venn } from '../alpha'; import { Annotation, Area, @@ -56,6 +56,7 @@ import { ComboElement, DonutElement, DonutSummaryElement, + GaugeElement, LegendElement, LineElement, MetricRangeElement, @@ -102,6 +103,7 @@ type ElementCounts = { scatter: number; combo: number; bullet: number; + gauge: number; venn: number; }; @@ -134,6 +136,7 @@ export const sanitizeChildren = (children: unknown): (ChartChildElement | MarkCh AxisThumbnail.displayName, Bar.displayName, Bullet.displayName, + Gauge.displayName, ChartPopover.displayName, ChartTooltip.displayName, Combo.displayName, @@ -165,6 +168,7 @@ export const sanitizeRscChartChildren = (children: unknown): ChartChildElement[] Axis.displayName, Bar.displayName, Donut.displayName, + Gauge.displayName, Legend.displayName, Line.displayName, Scatter.displayName, @@ -409,6 +413,9 @@ const getElementName = (element: unknown, elementCounts: ElementCounts) => { case Bullet.displayName: elementCounts.bullet++; return getComponentName(element as BulletElement, `bullet${elementCounts.bullet}`); + case Gauge.displayName: + elementCounts.gauge++; + return getComponentName(element as GaugeElement, `gauge${elementCounts.gauge}`); case Legend.displayName: elementCounts.legend++; return getComponentName(element as LegendElement, `legend${elementCounts.legend}`); @@ -449,6 +456,7 @@ const initElementCounts = (): ElementCounts => ({ line: -1, scatter: -1, combo: -1, + gauge: -1, venn: -1, }); diff --git a/packages/vega-spec-builder/src/chartSpecBuilder.ts b/packages/vega-spec-builder/src/chartSpecBuilder.ts index c4fcf9125..928287b7c 100644 --- a/packages/vega-spec-builder/src/chartSpecBuilder.ts +++ b/packages/vega-spec-builder/src/chartSpecBuilder.ts @@ -41,7 +41,7 @@ import { addArea } from './area/areaSpecBuilder'; import { addAxis } from './axis/axisSpecBuilder'; import { addBar } from './bar/barSpecBuilder'; import { addBullet } from './bullet/bulletSpecBuilder'; -// add import addGauge here +import { addGauge } from './gauge/gaugeSpecBuilder'; import { addCombo } from './combo/comboSpecBuilder'; import { getSeriesIdTransform } from './data/dataUtils'; import { addDonut } from './donut/donutSpecBuilder'; @@ -129,6 +129,7 @@ export function buildSpec({ spec.signals = getDefaultSignals(options); spec.scales = getDefaultScales(colors, colorScheme, lineTypes, lineWidths, opacities, symbolShapes, symbolSizes); + // added gaugeCount below let { areaCount, barCount, bulletCount, comboCount, donutCount, gaugeCount, lineCount, scatterCount, vennCount } = initializeComponentCounts(); const specOptions = { colorScheme, idKey, highlightedItem }; @@ -221,6 +222,7 @@ const initializeComponentCounts = () => { comboCount: -1, donutCount: -1, bulletCount: -1, + gaugeCount: -1, lineCount: -1, scatterCount: -1, vennCount: -1, diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts index 7e71c84f4..638833665 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.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 { BulletOptions, ScSpec } from '../types'; +import { GaugeOptions, ScSpec } from '../types'; import { addBullet, addData, addScales, addSignals } from './bulletSpecBuilder'; import { sampleOptionsColumn, sampleOptionsRow } from './bulletTestUtils'; diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index 3534a5d8c..2177c4919 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -10,24 +10,34 @@ * governing permissions and limitations under the License. */ import { produce } from 'immer'; -import { Data, Scale, Signal } from 'vega'; +import { Mark, Signal } from 'vega'; -import { - DEFAULT_GAUGE_DIRECTION, - DEFAULT_COLOR_SCHEME, - DEFAULT_LABEL_POSITION, - DEFAULT_SCALE_TYPE, - DEFAULT_SCALE_VALUE, -} from '@spectrum-charts/constants'; -import { getColorValue, spectrumColors } from '@spectrum-charts/themes'; +// import { +// DEFAULT_GAUGE_DIRECTION, +// DEFAULT_COLOR_SCHEME, +// DEFAULT_LABEL_POSITION, +// DEFAULT_SCALE_TYPE, +// DEFAULT_SCALE_VALUE, +// } from '@spectrum-charts/constants'; + + +import { DEFAULT_COLOR_SCHEME } from '@spectrum-charts/constants'; // this is the only constant i needed to get a simple blue rect to render + +// import { getColorValue, spectrumColors } from '@spectrum-charts/themes'; +import { getColorValue, spectrumColors } from '@spectrum-charts/themes'; import { toCamelCase } from '@spectrum-charts/utils'; -import { GaugeOptions, GaugeSpecOptions, ColorScheme, ScSpec } from '../types'; -import { getGaugeTableData, getGaugeTransforms } from './gaugeDataUtils'; -import { addAxes, addMarks } from './gaugeMarkUtils'; +import { ColorScheme, GaugeOptions, GaugeSpecOptions, ScSpec } from '../types'; + +const DEFAULT_COLOR = spectrumColors.light['blue-900']; // can't figure out why this doesnt change if i leave it blank +// might be because this sets the default color for the gauge component for the user when they use a <Gauge /> component. +// the color I see in Storybook is what is hardcoded in Gauge.story.tsx -const DEFAULT_COLOR = spectrumColors.light['static-blue']; +/** + * Adds a simple Gauge chart to the spec + * This is a simplified version that just renders a blue rectangle as proof of concept + */ export const addGauge = produce< ScSpec, [GaugeOptions & { colorScheme?: ColorScheme; index?: number; idKey: string }] @@ -38,144 +48,69 @@ export const addGauge = produce< colorScheme = DEFAULT_COLOR_SCHEME, index = 0, name, - metric, - dimension, - target, color = DEFAULT_COLOR, - direction = DEFAULT_GAUGE_DIRECTION, - numberFormat, - showTarget = true, - showTargetValue = false, - labelPosition = DEFAULT_LABEL_POSITION, - scaleType = DEFAULT_SCALE_TYPE, - maxScaleValue = DEFAULT_SCALE_VALUE, - thresholds = [], - track = false, - thresholdBarColor = false, - metricAxis = false, ...options } ) => { const gaugeOptions: GaugeSpecOptions = { colorScheme: colorScheme, index, - color: getColorValue(color, colorScheme), - metric: metric ?? 'currentAmount', - dimension: dimension ?? 'graphLabel', - target: target ?? 'target', name: toCamelCase(name ?? `gauge${index}`), - direction: direction, - numberFormat: numberFormat ?? '', - showTarget: showTarget, - showTargetValue: showTargetValue, - labelPosition: labelPosition, - scaleType: scaleType, - maxScaleValue: maxScaleValue, - track: track, - thresholds: thresholds, - thresholdBarColor: thresholdBarColor, - metricAxis: metricAxis, + metric: 'currentAmount', + dimension: 'graphLabel', + target: 'target', + color: getColorValue(color, colorScheme), // Convert spectrum color to RGB + direction: 'column', + numberFormat: '', + showTarget: true, + showTargetValue: false, + labelPosition: 'top', + scaleType: 'normal', + maxScaleValue: 100, + track: false, + thresholds: [], + thresholdBarColor: false, + metricAxis: false, ...options, }; - spec.data = addData(spec.data ?? [], gaugeOptions); - spec.marks = addMarks(spec.marks ?? [], gaugeOptions); - spec.scales = addScales(spec.scales ?? [], gaugeOptions); - spec.signals = addSignals(spec.signals ?? [], gaugeOptions); - spec.axes = addAxes(spec.axes ?? [], gaugeOptions); - } -); - -export const addScales = produce<Scale[], [GaugeSpecOptions]>((scales, options) => { - const groupScaleRangeSignal = options.direction === 'column' ? 'gaugeChartHeight' : 'width'; - const xRange = options.direction === 'column' ? 'width' : [0, { signal: 'gaugeGroupWidth' }]; - let domainFields; - - if (options.scaleType === 'flexible' && options.maxScaleValue > 0) { - domainFields = { data: 'table', fields: ['xPaddingForTarget', options.metric, 'flexibleScaleValue'] }; - } else if (options.scaleType === 'fixed' && options.maxScaleValue > 0) { - domainFields = [0, `${options.maxScaleValue}`]; - } else { - domainFields = { data: 'table', fields: ['xPaddingForTarget', options.metric] }; - } - - scales.push( - { - name: 'groupScale', - type: 'band', - domain: { data: 'table', field: options.dimension }, - range: [0, { signal: groupScaleRangeSignal }], - paddingInner: { signal: 'paddingRatio' }, - }, - { - name: 'xscale', - type: 'linear', - domain: domainFields, - range: xRange, - round: true, - clamp: true, - zero: true, + // Initialize marks array if it doesn't exist + if (!spec.marks) { + spec.marks = []; } - ); -}); - -export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, options) => { - signals.push({ name: 'gap', value: 12 }); - signals.push({ name: 'gaugeHeight', value: 8 }); - signals.push({ name: 'gaugeThresholdHeight', update: 'gaugeHeight * 3' }); - signals.push({ name: 'targetHeight', update: 'gaugeThresholdHeight + 6' }); - - if (options.showTargetValue && options.showTarget) { - signals.push({ name: 'targetValueLabelHeight', update: '20' }); - } - - signals.push({ - name: 'gaugeGroupHeight', - update: getGaugeGroupHeightExpression(options), - }); - if (options.direction === 'column') { - signals.push({ name: 'paddingRatio', update: 'gap / (gap + gaugeGroupHeight)' }); - - if (options.metricAxis && !options.showTargetValue) { - signals.push({ - name: 'gaugeChartHeight', - update: "length(data('table')) * gaugeGroupHeight + (length(data('table')) - 1) * gap + 10", - }); - signals.push({ - name: 'axisOffset', - update: 'gaugeChartHeight - height - 10', - }); - } else { - signals.push({ - name: 'gaugeChartHeight', - update: "length(data('table')) * gaugeGroupHeight + (length(data('table')) - 1) * gap", - }); - } - } else { - signals.push({ name: 'gaugeGroupWidth', update: "(width / length(data('table'))) - gap" }); - signals.push({ name: 'paddingRatio', update: 'gap / (gap + gaugeGroupWidth)' }); - signals.push({ name: 'gaugeChartHeight', update: 'gaugeGroupHeight' }); - } -}); + spec.signals = addSignals(spec.signals ?? [], gaugeOptions); -/** - * Returns the height of the bullet group based on the options - * @param options the bullet spec options - * @returns the height of the bullet group - */ -function getGaugeGroupHeightExpression(options: GaugeSpecOptions): string { - if (options.showTargetValue && options.showTarget) { - return options.labelPosition === 'side' && options.direction === 'column' - ? 'gaugeThresholdHeight + targetValueLabelHeight + 10' - : 'gaugeThresholdHeight + targetValueLabelHeight + 24'; - } else if (options.labelPosition === 'side' && options.direction === 'column') { - return 'gaugeThresholdHeight + 10'; + // Add a simple blue rectangle mark as proof of concept + // spec.marks.push({ + // type: 'rect', + // name: `${gaugeOptions.name}_test_rectangle`, + // encode: { + // enter: { + // x: { value: 50 }, + // y: { value: 50 }, + // width: { value: 200 }, + // height: { value: 100 }, + // fill: { value: gaugeOptions.color }, + // }, + // }, + // }); + } - return 'gaugeThresholdHeight + 24'; -} +); -export const addData = produce<Data[], [GaugeSpecOptions]>((data, options) => { - const tableData = getGaugeTableData(data); - tableData.transform = getGaugeTransforms(options); +export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, options) => { + // Hardcoded range values + signals.push({ name: 'arcMinVal', value: 0 }); + signals.push({ name: 'arcMaxVal', value: 100 }); + + // Hardcoded angles + signals.push({ name: 'startAngle', value: -Math.PI / 2 }); // -90 degrees + signals.push({ name: 'endAngle', value: Math.PI / 2 }); // 90 degrees + + // Get current value from first row of table data + signals.push({ name: 'currVal', update: "data('table')[0].currentAmount" }); + signals.push({ name: 'target', update: "data('table')[0].target" }); + + }); diff --git a/packages/vega-spec-builder/src/types/chartSpec.types.ts b/packages/vega-spec-builder/src/types/chartSpec.types.ts index 70f415a82..408065b52 100644 --- a/packages/vega-spec-builder/src/types/chartSpec.types.ts +++ b/packages/vega-spec-builder/src/types/chartSpec.types.ts @@ -20,6 +20,7 @@ import { BulletOptions, ComboOptions, DonutOptions, + GaugeOptions, LineOptions, ScatterOptions, VennOptions, @@ -58,6 +59,7 @@ export type MarkOptions = | BulletOptions | ComboOptions | DonutOptions + | GaugeOptions | LineOptions | ScatterOptions | VennOptions; From 711440d799194be15804ea67b0d76bf188635615 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Thu, 23 Oct 2025 21:16:04 -0600 Subject: [PATCH 06/66] Messed with the signals --- package.json | 2 +- .../src/gauge/gaugeMarkUtils.ts | 2 -- .../src/gauge/gaugeSpecBuilder.ts | 8 ++++---- yarn.lock | 18 ++++++++++++------ 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 4696cd8e0..d23e09def 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "chalk": "4.1.2", "clean-webpack-plugin": "^4.0.0", "concurrently": "^8.0.0", - "cross-env": "^7.0.3", + "cross-env": "^10.1.0", "css-loader": "^7.1.2", "eslint": "^8.29.0", "eslint-config-prettier": "^9.0.0", diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 4583c1192..6377966c0 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -17,8 +17,6 @@ import { getColorValue } from '@spectrum-charts/themes'; import { BulletSpecOptions } from '../types'; export const addMarks = produce<Mark[], [BulletSpecOptions]>((marks, bulletOptions) => { - const markGroupEncodeUpdateDirection = bulletOptions.direction === 'column' ? 'y' : 'x'; - const bulletGroupWidth = bulletOptions.direction === 'column' ? 'width' : 'bulletGroupWidth'; const bulletMark: GroupMark = { name: 'bulletGroup', diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index 2177c4919..cbf0a857a 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -105,12 +105,12 @@ export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, option signals.push({ name: 'arcMaxVal', value: 100 }); // Hardcoded angles - signals.push({ name: 'startAngle', value: -Math.PI / 2 }); // -90 degrees - signals.push({ name: 'endAngle', value: Math.PI / 2 }); // 90 degrees + signals.push({ name: 'startAngle', value: "PI / 2" }); // -90 degrees + signals.push({ name: 'endAngle', value: "PI / 2" }); // 90 degrees // Get current value from first row of table data - signals.push({ name: 'currVal', update: "data('table')[0].currentAmount" }); - signals.push({ name: 'target', update: "data('table')[0].target" }); + signals.push({ name: 'currVal', value: "30" }); + signals.push({ name: 'target', value: "80" }); }); diff --git a/yarn.lock b/yarn.lock index 5664a7126..283be820e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3078,6 +3078,11 @@ utility-types "^3.10.0" webpack "^5.88.1" +"@epic-web/invariant@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@epic-web/invariant/-/invariant-1.0.0.tgz#1073e5dee6dd540410784990eb73e4acd25c9813" + integrity sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA== + "@es-joy/jsdoccomment@~0.49.0": version "0.49.0" resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.49.0.tgz#e5ec1eda837c802eca67d3b29e577197f14ba1db" @@ -8815,14 +8820,15 @@ create-jest@^29.7.0: jest-util "^29.7.0" prompts "^2.0.1" -cross-env@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" - integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== +cross-env@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-10.1.0.tgz#cfd2a6200df9ed75bfb9cb3d7ce609c13ea21783" + integrity sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw== dependencies: - cross-spawn "^7.0.1" + "@epic-web/invariant" "^1.0.0" + cross-spawn "^7.0.6" -cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== From c51e8f6c30eafc16494ddb81a0e09d47f960ee8c Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Thu, 23 Oct 2025 21:33:31 -0600 Subject: [PATCH 07/66] Progress on Mark --- .../src/gauge/gaugeMarkUtils.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 6377966c0..102648318 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -10,23 +10,20 @@ * governing permissions and limitations under the License. */ import { produce } from 'immer'; -import { Axis, GroupMark, Mark } from 'vega'; +import { Mark } from 'vega'; import { getColorValue } from '@spectrum-charts/themes'; -import { BulletSpecOptions } from '../types'; +import { GaugeSpecOptions } from '../types'; -export const addMarks = produce<Mark[], [BulletSpecOptions]>((marks, bulletOptions) => { +export const addMarks = produce<Mark[], [GaugeSpecOptions]>((marks, GaugeOptions) => { - const bulletMark: GroupMark = { - name: 'bulletGroup', - type: 'group', - from: { - facet: { data: 'table', name: 'bulletGroups', groupby: `${bulletOptions.dimension}` }, - }, + const gaugeMark: GroupMark = { + name: 'backgroundArcRoundEdge', + type: 'arc', encode: { - update: { - [markGroupEncodeUpdateDirection]: { scale: 'groupScale', field: `${bulletOptions.dimension}` }, + enter: { + { scale: 'groupScale', field: `${gaugeOptions.dimension}` }, height: { signal: 'bulletGroupHeight' }, width: { signal: bulletGroupWidth }, }, From 1b92348e28bb2067c272570f4d2fdcc200ed735e Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Thu, 23 Oct 2025 22:14:30 -0600 Subject: [PATCH 08/66] Working on Mark Util --- .../src/gauge/gaugeMarkUtils.ts | 422 +++++------------- .../src/types/marks/gaugeSpec.types.ts | 58 +-- 2 files changed, 134 insertions(+), 346 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 102648318..aa1430d4c 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -12,344 +12,168 @@ import { produce } from 'immer'; import { Mark } from 'vega'; -import { getColorValue } from '@spectrum-charts/themes'; +// import { getColorValue } from '@spectrum-charts/themes'; import { GaugeSpecOptions } from '../types'; export const addMarks = produce<Mark[], [GaugeSpecOptions]>((marks, GaugeOptions) => { - const gaugeMark: GroupMark = { - name: 'backgroundArcRoundEdge', - type: 'arc', - encode: { - enter: { - { scale: 'groupScale', field: `${gaugeOptions.dimension}` }, - height: { signal: 'bulletGroupHeight' }, - width: { signal: bulletGroupWidth }, - }, - }, - marks: [], - }; - - const thresholds = bulletOptions.thresholds; - - if (Array.isArray(thresholds) && thresholds.length > 0) { - bulletMark.data = [ - { - name: 'thresholds', - values: thresholds, - transform: [{ type: 'identifier', as: 'id' }], - }, - ]; - bulletMark.marks?.push(getBulletMarkThreshold(bulletOptions)); - } else if (bulletOptions.track) { - bulletMark.marks?.push(getBulletTrack(bulletOptions)); - } - - bulletMark.marks?.push(getBulletMarkRect(bulletOptions)); - if (bulletOptions.target && bulletOptions.showTarget !== false) { - bulletMark.marks?.push(getBulletMarkTarget(bulletOptions)); - if (bulletOptions.showTargetValue) { - bulletMark.marks?.push(getBulletMarkTargetValueLabel(bulletOptions)); - } - } - - if (bulletOptions.labelPosition === 'top' || bulletOptions.direction === 'row') { - bulletMark.marks?.push(getBulletMarkLabel(bulletOptions)); - bulletMark.marks?.push(getBulletMarkValueLabel(bulletOptions)); - } - - marks.push(bulletMark); + export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => { + const { + name, + backgroundFill, + backgroundStroke, + fillerColorSignal, + straightEdgeOffsetExpr, + labelColor, + labelSize, + } = opt; + + // Background arcs (rounded, then straight overlay) + marks.push(getBackgroundArcRounded(name, backgroundFill, backgroundStroke)); + marks.push(getBackgroundArcStraight(name, backgroundFill, backgroundStroke, straightEdgeOffsetExpr)); + + // Text labels: max, target, min + marks.push(getMaxValueText(name, labelColor, labelSize)); + marks.push(getTargetValueText(name, labelColor, labelSize)); + marks.push(getMinValueText(name, labelColor, labelSize)); + + // Filler arc (value fill) + marks.push(getFillerArc(name, fillerColorSignal)); +} }); -export function getBulletMarkRect(bulletOptions: BulletSpecOptions): Mark { - //The vertical positioning is calculated starting at the bulletgroupheight - //and then subtracting two times the bullet height to center the bullet bar - //in the middle of the threshold. The 3 is subtracted because the bulletgroup height - //starts the bullet below the threshold area. - //Additionally, the value of the targetValueLabelHeight is subtracted if the target value label is shown - //to make sure that the bullet bar is not drawn over the target value label. - const bulletMarkRectEncodeUpdateYSignal = - bulletOptions.showTarget && bulletOptions.showTargetValue - ? 'bulletGroupHeight - targetValueLabelHeight - 3 - 2 * bulletHeight' - : 'bulletGroupHeight - 3 - 2 * bulletHeight'; - - const fillColor = - bulletOptions.thresholdBarColor && (bulletOptions.thresholds?.length ?? 0) > 0 - ? [{ field: 'barColor' }] - : [{ value: bulletOptions.color }]; - - const bulletMarkRect: Mark = { - name: `${bulletOptions.name}Rect`, - description: `${bulletOptions.name}Rect`, - type: 'rect', - from: { data: 'bulletGroups' }, +function getBackgroundArcRounded(name: string, fill: string, stroke: string): Mark { + return { + name: `${name}BackgroundArcRounded`, + description: 'Background Arc (Round Edge)', + type: 'arc', encode: { enter: { - cornerRadiusTopLeft: [{ test: `datum.${bulletOptions.metric} < 0`, value: 3 }], - cornerRadiusBottomLeft: [{ test: `datum.${bulletOptions.metric} < 0`, value: 3 }], - cornerRadiusTopRight: [{ test: `datum.${bulletOptions.metric} > 0`, value: 3 }], - cornerRadiusBottomRight: [{ test: `datum.${bulletOptions.metric} > 0`, value: 3 }], - fill: fillColor, - }, - update: { - x: { scale: 'xscale', value: 0 }, - x2: { scale: 'xscale', field: `${bulletOptions.metric}` }, - height: { signal: 'bulletHeight' }, - y: { signal: bulletMarkRectEncodeUpdateYSignal }, - }, - }, + x: { signal: 'centerX' }, + y: { signal: 'centerY' }, + innerRadius: { signal: 'innerRadius' }, + outerRadius: { signal: 'outerRadius' }, + startAngle: { signal: 'startAngle' }, + endAngle: { signal: 'endAngle' }, + cornerRadius:{ signal: 'cornerR' }, + fill: { value: fill }, + stroke: { value: stroke } + } + } }; - - return bulletMarkRect; } -export function getBulletMarkTarget(bulletOptions: BulletSpecOptions): Mark { - const solidColor = getColorValue('gray-900', bulletOptions.colorScheme); - - //When the target value label is shown, we must subtract the height of the target value label - //to make sure that the target line is not drawn over the target value label - const bulletMarkTargetEncodeUpdateY = - bulletOptions.showTarget && bulletOptions.showTargetValue - ? 'bulletGroupHeight - targetValueLabelHeight - targetHeight' - : 'bulletGroupHeight - targetHeight'; - const bulletMarkTargetEncodeUpdateY2 = - bulletOptions.showTarget && bulletOptions.showTargetValue - ? 'bulletGroupHeight - targetValueLabelHeight' - : 'bulletGroupHeight'; - - const bulletMarkTarget: Mark = { - name: `${bulletOptions.name}Target`, - description: `${bulletOptions.name}Target`, - type: 'rule', - from: { data: 'bulletGroups' }, +function getBackgroundArcStraight( + name: string, + fill: string, + stroke: string, + offsetExpr: string +): Mark { + return { + name: `${name}BackgroundArcStraight`, + description: 'Background Arc (Straight Edge)', + type: 'arc', encode: { enter: { - stroke: { value: `${solidColor}` }, - strokeWidth: { value: 2 }, - }, - update: { - x: { scale: 'xscale', field: `${bulletOptions.target}` }, - y: { signal: bulletMarkTargetEncodeUpdateY }, - y2: { signal: bulletMarkTargetEncodeUpdateY2 }, - }, - }, + x: { signal: 'centerX' }, + y: { signal: 'centerY' }, + innerRadius: { signal: 'innerRadius' }, + outerRadius: { signal: 'outerRadius' }, + // startAngle offset to flatten the left edge + startAngle: { signal: `startAngle + (${offsetExpr})` }, + endAngle: { signal: 'endAngle' }, + fill: { value: fill }, + stroke: { value: stroke } + } + } }; - - return bulletMarkTarget; } -export function getBulletMarkLabel(bulletOptions: BulletSpecOptions): Mark { - const barLabelColor = getColorValue('gray-600', bulletOptions.colorScheme); - - const bulletMarkLabel: Mark = { - name: `${bulletOptions.name}Label`, - description: `${bulletOptions.name}Label`, +function getMaxValueText(name: string, color: string, fontSize: number): Mark { + return { + name: `${name}MaxValText`, + description: 'Max Val Text', type: 'text', - from: { data: 'bulletGroups' }, encode: { enter: { - text: { signal: `datum.${bulletOptions.dimension}` }, - align: { value: 'left' }, - baseline: { value: 'top' }, - fill: { value: `${barLabelColor}` }, + x: { signal: 'MaxTextX' }, + y: { signal: 'MaxTextY' }, + text: { signal: "format(arcMaxVal, '.0f')" }, + align: { value: 'center' }, + baseline: { value: 'middle' }, + fontSize: { value: fontSize }, + fill: { value: color }, + fontWeight:{ value: 'bold' } }, - update: { x: { value: 0 }, y: { value: 0 } }, - }, + // Keeping parity with your example; this update prop is harmless for text + update: { + endAngle: { signal: "scale('angleScale', arcMaxVal)" } + } + } }; - - return bulletMarkLabel; } -export function getBulletMarkValueLabel(bulletOptions: BulletSpecOptions): Mark { - const defaultColor = getColorValue(bulletOptions.color, bulletOptions.colorScheme); - const solidColor = getColorValue('gray-900', bulletOptions.colorScheme); - const encodeUpdateSignalWidth = bulletOptions.direction === 'column' ? 'width' : 'bulletGroupWidth'; - const fillExpr = - bulletOptions.thresholdBarColor && (bulletOptions.thresholds?.length ?? 0) > 0 - ? `datum.barColor === '${defaultColor}' ? '${solidColor}' : datum.barColor` - : `'${solidColor}'`; - - const bulletMarkValueLabel: Mark = { - name: `${bulletOptions.name}ValueLabel`, - description: `${bulletOptions.name}ValueLabel`, +function getTargetValueText(name: string, color: string, fontSize: number): Mark { + return { + name: `${name}TargetValText`, + description: 'Target Val Text', type: 'text', - from: { data: 'bulletGroups' }, encode: { enter: { - text: { - signal: `datum.${bulletOptions.metric} != null ? format(datum.${bulletOptions.metric}, '${ - bulletOptions.numberFormat || '' - }') : ''`, - }, - align: { value: 'right' }, - baseline: { value: 'top' }, - fill: { signal: fillExpr }, - }, - update: { x: { signal: encodeUpdateSignalWidth }, y: { value: 0 } }, - }, + x: { signal: 'targetTextX' }, + y: { signal: 'targetTextY' }, + text: { signal: "format(target, '.0f')" }, + align: { value: 'center' }, + baseline: { value: 'middle' }, + fontSize: { value: fontSize }, + fontWeight:{ value: 'bold' }, + fill: { value: color } + } + } }; - - return bulletMarkValueLabel; } -export function getBulletMarkTargetValueLabel(bulletOptions: BulletSpecOptions): Mark { - const solidColor = getColorValue('gray-900', bulletOptions.colorScheme); - - const bulletMarkTargetValueLabel: Mark = { - name: `${bulletOptions.name}TargetValueLabel`, - description: `${bulletOptions.name}TargetValueLabel`, +function getMinValueText(name: string, color: string, fontSize: number): Mark { + return { + name: `${name}MinValText`, + description: 'Min Val Text', type: 'text', - from: { data: 'bulletGroups' }, - encode: { - enter: { - text: { - signal: `datum.${bulletOptions.target} != null ? 'Target: ' + format(datum.${bulletOptions.target}, '$,.2f') : 'No Target'`, - }, - align: { value: 'center' }, - baseline: { value: 'top' }, - fill: { value: `${solidColor}` }, - }, - update: { - x: { scale: 'xscale', field: `${bulletOptions.target}` }, - y: { signal: 'bulletGroupHeight - targetValueLabelHeight + 6' }, - }, - }, - }; - - return bulletMarkTargetValueLabel; -} - -export function getBulletMarkThreshold(bulletOptions: BulletSpecOptions): Mark { - // Vertically center the threshold bar by offsetting from bulletGroupHeight. - // Subtract 3 for alignment and targetValueLabelHeight if the label is shown. - const baseHeightSignal = 'bulletGroupHeight - 3 - bulletThresholdHeight'; - const encodeUpdateYSignal = - bulletOptions.showTarget && bulletOptions.showTargetValue - ? `${baseHeightSignal} - targetValueLabelHeight` - : baseHeightSignal; - - const bulletMarkThreshold: Mark = { - name: `${bulletOptions.name}Threshold`, - description: `${bulletOptions.name}Threshold`, - type: 'rect', - from: { data: 'thresholds' }, - clip: true, encode: { enter: { - cornerRadiusTopLeft: [{ test: `!isDefined(datum.thresholdMin) && domain('xscale')[0] !== 0`, value: 3 }], - cornerRadiusBottomLeft: [{ test: `!isDefined(datum.thresholdMin) && domain('xscale')[0] !== 0`, value: 3 }], - cornerRadiusTopRight: [{ test: `!isDefined(datum.thresholdMax) && domain('xscale')[1] !== 0`, value: 3 }], - cornerRadiusBottomRight: [{ test: `!isDefined(datum.thresholdMax) && domain('xscale')[1] !== 0`, value: 3 }], - fill: { field: 'fill' }, - fillOpacity: { value: 0.2 }, - }, - update: { - x: { - signal: "isDefined(datum.thresholdMin) ? scale('xscale', datum.thresholdMin) : 0", - }, - x2: { - signal: "isDefined(datum.thresholdMax) ? scale('xscale', datum.thresholdMax) : width", - }, - height: { signal: 'bulletThresholdHeight' }, - y: { signal: encodeUpdateYSignal }, - }, - }, + x: { signal: 'MinTextX' }, + y: { signal: 'MinTextY' }, + text: { signal: "format(arcMinVal, '.0f')" }, + align: { value: 'center' }, + baseline: { value: 'middle' }, + fontSize: { value: fontSize }, + fontWeight:{ value: 'bold' }, + fill: { value: color } + } + } }; - return bulletMarkThreshold; } -export function getBulletTrack(bulletOptions: BulletSpecOptions): Mark { - const trackColor = getColorValue('gray-200', bulletOptions.colorScheme); - const trackWidth = bulletOptions.direction === 'column' ? 'width' : 'bulletGroupWidth'; - // Subtracting 20 accounts for the space used by the target value label - const trackY = - bulletOptions.showTarget && bulletOptions.showTargetValue - ? 'bulletGroupHeight - 3 - 2 * bulletHeight - 20' - : 'bulletGroupHeight - 3 - 2 * bulletHeight'; - - const bulletTrack: Mark = { - name: `${bulletOptions.name}Track`, - description: `${bulletOptions.name}Track`, - type: 'rect', - from: { data: 'bulletGroups' }, +function getFillerArc(name: string, fillerColorSignal: string): Mark { + return { + name: `${name}FillerArc`, + description: 'Filler Arc', + type: 'arc', encode: { enter: { - fill: { value: trackColor }, - cornerRadiusTopRight: [{ test: "domain('xscale')[1] !== 0", value: 3 }], - cornerRadiusBottomRight: [{ test: "domain('xscale')[1] !== 0", value: 3 }], - cornerRadiusTopLeft: [{ test: "domain('xscale')[0] !== 0", value: 3 }], - cornerRadiusBottomLeft: [{ test: "domain('xscale')[0] !== 0", value: 3 }], + x: { signal: 'centerX' }, + y: { signal: 'centerY' }, + innerRadius: { signal: 'innerRadius' }, + outerRadius: { signal: 'outerRadius' }, + startAngle: { signal: 'startAngle' }, + endAngle: { signal: 'endAngle' }, + fill: { signal: fillerColorSignal } }, update: { - x: { value: 0 }, - width: { signal: trackWidth }, - height: { signal: 'bulletHeight' }, - y: { signal: trackY }, - }, - }, - }; - - return bulletTrack; -} - -export function getBulletLabelAxesLeft(labelOffset): Axis { - return { - scale: 'groupScale', - orient: 'left', - tickSize: 0, - labelOffset: labelOffset, - labelPadding: 10, - labelColor: '#797979', - domain: false, - }; -} - -export function getBulletLabelAxesRight(bulletOptions: BulletSpecOptions, labelOffset): Axis { - return { - scale: 'groupScale', - orient: 'right', - tickSize: 0, - labelOffset: labelOffset, - labelPadding: 10, - domain: false, - encode: { - labels: { - update: { - text: { - signal: `info(data('table')[datum.index * (length(data('table')) - 1)].${ - bulletOptions.metric - }) != null ? format(info(data('table')[datum.index * (length(data('table')) - 1)].${ - bulletOptions.metric - }), '${bulletOptions.numberFormat || ''}') : ''`, - }, - }, - }, - }, - }; -} - -export function getBulletScaleAxes(): Axis { - return { - labelOffset: 2, - scale: 'xscale', - orient: 'bottom', - ticks: false, - labelColor: 'gray', - domain: false, - tickCount: 5, - offset: { signal: 'axisOffset' }, + endAngle: { signal: "scale('angleScale', clampedVal)" }, + // Square end normally; rounded when “full” + cornerRadius: { signal: "!isFull ? cornerR : 0" } + } + } }; } - -export const addAxes = produce<Axis[], [BulletSpecOptions]>((axes, bulletOptions) => { - if (bulletOptions.metricAxis && bulletOptions.direction === 'column' && !bulletOptions.showTargetValue) { - axes.push(getBulletScaleAxes()); - } - - if (bulletOptions.labelPosition === 'side' && bulletOptions.direction === 'column') { - const labelOffset = bulletOptions.showTargetValue && bulletOptions.showTarget ? -8 : 2; - axes.push(getBulletLabelAxesLeft(labelOffset)); - axes.push(getBulletLabelAxesRight(bulletOptions, labelOffset)); - } -}); diff --git a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts index 7ba04d4b6..bea44b1a2 100644 --- a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts +++ b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts @@ -19,18 +19,10 @@ export interface GaugeOptions { /** Key in the data that is used as the color facet */ color?: string; - /** Data field that the metric is trended against (x-axis for horizontal orientation) */ - dimension?: string; - /** Specifies the direction the bars should be ordered (row/column) */ - direction?: 'row' | 'column'; - /** Specifies if the labels should be in top of the bullet chart or to the side. Side labels are not supported in row mode. */ - labelPosition?: 'side' | 'top'; /** Maximum value for the scale. This value must be greater than zero. */ - maxScaleValue?: number; + maxArcValue?: number; /** Key in the data that is used as the metric */ metric?: string; - /** Adds an axis that follows the max target in basic mode */ - metricAxis?: boolean; /** Sets the name of the component. */ name?: string; /** d3 number format specifier. @@ -39,57 +31,29 @@ export interface GaugeOptions { * see {@link https://d3js.org/d3-format#locale_format} */ numberFormat?: NumberFormat; - /** Specifies if the scale should be normal, fixed, or flexible. - * - * In normal mode the maximum scale value will be calculated using the maximum value of the metric and target data fields. - * - * In fixed mode the maximum scale value will be set as the maxScaleValue prop. - * - * In flexible mode the maximum scale value will be calculated using the maximum value of either the maxScaleValue prop or maximum value of the metric and target data fields. - * This means that the scale max will be set to be the maxScaleValue prop until the data values overtake it. - */ - scaleType?: 'normal' | 'fixed' | 'flexible'; /** Flag to control whether the target is shown */ - showTarget?: boolean; - /** Flag to control whether the target value is shown. */ - showTargetValue?: boolean; - /** Target line */ target?: string; - /** changes color based on threshold */ - /** If true, the metric bar will be colored according to the thresholds. */ - thresholdBarColor?: boolean; - /** Array of threshold definitions to be rendered as background bands on the bullet chart. - * - * Each threshold object supports: - * `thresholdMin` (optional): The lower bound of the threshold. If undefined, the threshold starts from the beginning of the x-scale. - * - * `thresholdMax` (optional): The upper bound of the threshold. If undefined, the threshold extends to the end of the x-scale. - * - * `fill` : The fill color to use for the threshold background. - */ - thresholds?: ThresholdBackground[]; - /** Color regions that sit behind the bullet bar */ + /** Color regions that fill the gauge bar to the metric value */ track?: boolean; } type GaugeOptionsWithDefaults = | 'name' | 'metric' - | 'dimension' | 'target' | 'color' - | 'direction' - | 'showTarget' - | 'showTargetValue' - | 'labelPosition' - | 'scaleType' - | 'maxScaleValue' - | 'track' - | 'metricAxis' - | 'thresholdBarColor'; + | 'maxArcValue'; export interface GaugeSpecOptions extends PartiallyRequired<GaugeOptions, GaugeOptionsWithDefaults> { + // What does this do? colorScheme: ColorScheme; idKey: string; index: number; + name: string; + backgroundFill: '#eee', + backgroundStroke: '#999', + fillerColorSignal: 'fillerColorToCurrVal', + straightEdgeOffsetExpr: 'PI/15', + labelColor: '#333', + labelSize: 40, } From 8a8668ecb9d0f95a4d84b4375c060c07becaf444 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Thu, 23 Oct 2025 22:18:44 -0600 Subject: [PATCH 09/66] Marks Util Progress --- .../src/gauge/gaugeMarkUtils.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index aa1430d4c..2b268a1a5 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -16,17 +16,15 @@ import { Mark } from 'vega'; import { GaugeSpecOptions } from '../types'; -export const addMarks = produce<Mark[], [GaugeSpecOptions]>((marks, GaugeOptions) => { - - export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => { +export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => { const { name, - backgroundFill, - backgroundStroke, - fillerColorSignal, - straightEdgeOffsetExpr, - labelColor, - labelSize, + backgroundFill = '#eee', + backgroundStroke = '#999', + fillerColorSignal = 'fillerColorToCurrVal', + straightEdgeOffsetExpr = 'PI/15', + labelColor = '#333', + labelSize = 40, } = opt; // Background arcs (rounded, then straight overlay) @@ -40,7 +38,7 @@ export const addMarks = produce<Mark[], [GaugeSpecOptions]>((marks, GaugeOptions // Filler arc (value fill) marks.push(getFillerArc(name, fillerColorSignal)); -} + }); function getBackgroundArcRounded(name: string, fill: string, stroke: string): Mark { From 14cc5e30ad03614ca5b4f7f1be44f0fc39d283ad Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Thu, 23 Oct 2025 22:22:24 -0600 Subject: [PATCH 10/66] Marks Util Progress --- .../vega-spec-builder/src/gauge/gaugeMarkUtils.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 2b268a1a5..b354cfa1d 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -62,12 +62,7 @@ function getBackgroundArcRounded(name: string, fill: string, stroke: string): Ma }; } -function getBackgroundArcStraight( - name: string, - fill: string, - stroke: string, - offsetExpr: string -): Mark { +function getBackgroundArcStraight(name: string, fill: string, stroke: string, offsetExpr: string): Mark { return { name: `${name}BackgroundArcStraight`, description: 'Background Arc (Straight Edge)', @@ -78,7 +73,7 @@ function getBackgroundArcStraight( y: { signal: 'centerY' }, innerRadius: { signal: 'innerRadius' }, outerRadius: { signal: 'outerRadius' }, - // startAngle offset to flatten the left edge + // startAngle offset to not flatten the right edge startAngle: { signal: `startAngle + (${offsetExpr})` }, endAngle: { signal: 'endAngle' }, fill: { value: fill }, @@ -104,7 +99,6 @@ function getMaxValueText(name: string, color: string, fontSize: number): Mark { fill: { value: color }, fontWeight:{ value: 'bold' } }, - // Keeping parity with your example; this update prop is harmless for text update: { endAngle: { signal: "scale('angleScale', arcMaxVal)" } } @@ -170,7 +164,7 @@ function getFillerArc(name: string, fillerColorSignal: string): Mark { update: { endAngle: { signal: "scale('angleScale', clampedVal)" }, // Square end normally; rounded when “full” - cornerRadius: { signal: "!isFull ? cornerR : 0" } + cornerRadius: { signal: "isFull ? cornerR : 0" } } } }; From dd128deafe08b765c0de781f9fd0cdc760bf3633 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Thu, 23 Oct 2025 23:26:02 -0600 Subject: [PATCH 11/66] Mark Updates --- .../src/alpha/components/Gauge/Gauge.tsx | 20 +--------- .../stories/components/Gauge/Gauge.story.tsx | 39 +++---------------- .../src/gauge/gaugeSpecBuilder.ts | 5 +-- 3 files changed, 9 insertions(+), 55 deletions(-) diff --git a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx index 5840589a5..ba2acffe0 100644 --- a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx +++ b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx @@ -26,25 +26,9 @@ import { GaugeProps } from '../../../types'; const Gauge: FC<GaugeProps> = ({ name = 'gauge0', metric = 'currentAmount', // CurrVal - dimension = 'graphLabel', // Graph Title ? target = 'target', - direction = DEFAULT_BULLET_DIRECTION, // Left to right Note to selves: Not today - numberFormat = '', // ints or floats ??? Help Mr Almighty Wizard ??? - showTarget = true, // Where you want - showTargetValue = false, // Number of what you want - labelPosition = DEFAULT_LABEL_POSITION, // Above gauge to label the needle - scaleType = DEFAULT_SCALE_TYPE, // Need Angle and Tick - maxScaleValue = DEFAULT_SCALE_VALUE, // Max Arc Value - thresholdBarColor = false, // filler color - // Things to add: - // ticksNumber - // clamping - // needle (on or off, sizing of it) - // arcMinVal and arcMaxVal - // size of tick lines - // needle length - // arc thickness - // label ticks? Enable and disable? + numberFormat = '', // ints or floats + maxArcValue = DEFAULT_SCALE_VALUE, // Max Arc Value }) => { return null; }; diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx index 5d28aca07..4afa99e12 100644 --- a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx @@ -30,8 +30,8 @@ export default { // Default chart properties const defaultChartProps: ChartProps = { data: basicGaugeData, - width: 350, - height: 350, + width: 500, + height: 600, }; // Basic Gauge chart story @@ -60,57 +60,28 @@ const GaugeTitleStory: StoryFn<typeof Gauge> = (args): ReactElement => { const Basic = bindWithProps(GaugeStory); Basic.args = { metric: 'currentAmount', - dimension: 'graphLabel', target: 'target', color: 'blue-900', - direction: 'column', numberFormat: '$,.2f', - showTarget: true, - showTargetValue: false, - labelPosition: 'top', - scaleType: 'normal', - maxScaleValue: 100, - track: false, - thresholdBarColor: false, - metricAxis: false, + maxArcValue: 100 }; const GaugeVariation2 = bindWithProps(GaugeStory); GaugeVariation2.args = { metric: 'currentAmount', - dimension: 'graphLabel', target: 'target', color: 'red-900', - direction: 'column', numberFormat: '$,.2f', - showTarget: true, - showTargetValue: false, - labelPosition: 'top', - scaleType: 'normal', - maxScaleValue: 100, - thresholds: basicThresholdsData, - thresholdBarColor: false, - track: false, - metricAxis: false, + maxArcValue: 100 }; const GaugeVariation3 = bindWithProps(GaugeStory); GaugeVariation3.args = { metric: 'currentAmount', - dimension: 'graphLabel', target: 'target', color: 'fuchsia-900', - direction: 'column', numberFormat: '$,.2f', - showTarget: true, - showTargetValue: false, - labelPosition: 'top', - scaleType: 'normal', - maxScaleValue: 100, - track: false, - thresholdBarColor: true, - thresholds: coloredThresholdsData, - metricAxis: false, + maxArcValue: 100 }; // const Track = bindWithProps(GaugeStory); diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index cbf0a857a..917cdf9f4 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -66,7 +66,7 @@ export const addGauge = produce< showTargetValue: false, labelPosition: 'top', scaleType: 'normal', - maxScaleValue: 100, + maxArcValue: 100, track: false, thresholds: [], thresholdBarColor: false, @@ -105,12 +105,11 @@ export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, option signals.push({ name: 'arcMaxVal', value: 100 }); // Hardcoded angles - signals.push({ name: 'startAngle', value: "PI / 2" }); // -90 degrees + signals.push({ name: 'startAngle', value: "-PI / 2" }); // -90 degrees signals.push({ name: 'endAngle', value: "PI / 2" }); // 90 degrees // Get current value from first row of table data signals.push({ name: 'currVal', value: "30" }); signals.push({ name: 'target', value: "80" }); - }); From d1b0c9064b9f1227a3dd88ba74c3bdd82c900516 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Thu, 23 Oct 2025 23:55:38 -0600 Subject: [PATCH 12/66] Adding Needle Rule Mark. --- .../src/gauge/gaugeMarkUtils.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index b354cfa1d..7df7566c8 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -164,8 +164,28 @@ function getFillerArc(name: string, fillerColorSignal: string): Mark { update: { endAngle: { signal: "scale('angleScale', clampedVal)" }, // Square end normally; rounded when “full” - cornerRadius: { signal: "isFull ? cornerR : 0" } + cornerRadius: { signal: "!isFull ? cornerR : 0" } } } }; + function getNeedleRule(name: string): Mark { + return { + name: `${name}Needle`, + description: 'Needle (rule)', + type: 'rule', + encode: { + enter: { + stroke: { value: '#333' }, + strokeWidth: { value: 3 }, + strokeCap: { value: 'round' } + }, + update: { + x: { signal: 'centerX' }, + y: { signal: 'centerY' }, + x2: { signal: 'needleTipX' }, + y2: { signal: 'needleTipY' } + } + } + }; + } From 084003f4fca19ba7064da6bf7638f5465420b169 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Thu, 23 Oct 2025 23:56:15 -0600 Subject: [PATCH 13/66] Fixed paranthesis --- packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 7df7566c8..50d84d0b6 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -168,6 +168,8 @@ function getFillerArc(name: string, fillerColorSignal: string): Mark { } } }; +} + function getNeedleRule(name: string): Mark { return { name: `${name}Needle`, @@ -187,5 +189,4 @@ function getFillerArc(name: string, fillerColorSignal: string): Mark { } } }; - } From b9ba2c3da6f83f35e93f60e51a6ca0e3d4335af4 Mon Sep 17 00:00:00 2001 From: Brady Shimanek <bradyshimanek33@gmail.com> Date: Fri, 24 Oct 2025 00:24:00 -0600 Subject: [PATCH 14/66] Got gauge to render --- .../src/gauge/gaugeSpecBuilder.ts | 78 +++++++++++++------ 1 file changed, 54 insertions(+), 24 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index 917cdf9f4..e982d0bce 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -10,7 +10,8 @@ * governing permissions and limitations under the License. */ import { produce } from 'immer'; -import { Mark, Signal } from 'vega'; +import { Mark, Signal, Scale } from 'vega'; +import { addGaugeMarks } from './gaugeMarkUtils'; // import { // DEFAULT_GAUGE_DIRECTION, @@ -57,29 +58,23 @@ export const addGauge = produce< index, name: toCamelCase(name ?? `gauge${index}`), metric: 'currentAmount', - dimension: 'graphLabel', target: 'target', color: getColorValue(color, colorScheme), // Convert spectrum color to RGB - direction: 'column', numberFormat: '', - showTarget: true, - showTargetValue: false, - labelPosition: 'top', - scaleType: 'normal', maxArcValue: 100, - track: false, - thresholds: [], - thresholdBarColor: false, - metricAxis: false, + backgroundFill: '#eee', + backgroundStroke: '#999', + fillerColorSignal: 'fillerColorToCurrVal', + straightEdgeOffsetExpr: 'PI/15', + labelColor: '#333', + labelSize: 40, ...options, }; - // Initialize marks array if it doesn't exist - if (!spec.marks) { - spec.marks = []; - } spec.signals = addSignals(spec.signals ?? [], gaugeOptions); + spec.scales = addScales(spec.scales ?? [], gaugeOptions); + spec.marks = addGaugeMarks(spec.marks ?? [], gaugeOptions); // Add a simple blue rectangle mark as proof of concept // spec.marks.push({ @@ -100,16 +95,51 @@ export const addGauge = produce< ); export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, options) => { - // Hardcoded range values - signals.push({ name: 'arcMinVal', value: 0 }); - signals.push({ name: 'arcMaxVal', value: 100 }); - // Hardcoded angles - signals.push({ name: 'startAngle', value: "-PI / 2" }); // -90 degrees - signals.push({ name: 'endAngle', value: "PI / 2" }); // 90 degrees - - // Get current value from first row of table data + signals.push({ name: 'arcMinVal', value: 0 }); + signals.push({ name: 'arcMaxVal', value: 100 }); + signals.push({ name: 'startAngle', update: "-PI / 2" }); // -90 degrees + signals.push({ name: 'endAngle', update: "PI / 2" }); // 90 degrees signals.push({ name: 'currVal', value: "30" }); signals.push({ name: 'target', value: "80" }); - + signals.push({ name: 'backgroundfillColor', value: "77A7FB"}) + signals.push({ name: 'fillerColorToCurrVal', value: "89CFF0"}) + signals.push({ name: 'TargetTextTheta', update: "scale('angleScale', target)"}) + signals.push({ name: 'targetTextX', update: "centerX + (outerRadius + 40) * sin(TargetTextTheta)"}) + signals.push({ name: 'targetTextY', update: "centerY - (outerRadius + 40) * cos(TargetTextTheta)"}) + signals.push({ name: 'StartTextTheta', update: "scale('angleScale', arcMinVal)"}) + signals.push({ name: 'EndTextTheta', update: "scale('angleScale', arcMaxVal)"}) + signals.push({ name: 'MinTextX', update: "centerX + (innerRadius + (outerRadius - innerRadius) / 2) * sin(StartTextTheta)"}) + signals.push({ name: 'MinTextY', update: "centerY + 40"}) + signals.push({ name: 'MaxTextX', update: "centerX + (innerRadius + (outerRadius - innerRadius) / 2) * sin(EndTextTheta)"}) + signals.push({ name: 'MaxTextY', update: "centerY + 40"}) + signals.push({ name: 'cornerR', update: "(outerRadius - innerRadius) * 0.45"}) + signals.push({ name: 'capSpan', update: "(cornerR / outerRadius) * 2"}) + signals.push({ name: 'valueEnd', update: "scale('angleScale', clampedVal)"}) + signals.push({ name: 'isFull', update: "clampedVal >= arcMaxVal"}) + signals.push({ name: 'capEnd', update: "min(valueEnd, startAngle + capSpan)"}) + signals.push({ name: 'mainStart', update: "isFull ? startAngle : capEnd"}) + signals.push({ name: 'radiusRef', update: "min(width/2, height/2)"}) + signals.push({ name: 'outerRadius', update: "radiusRef * 0.95"}) + signals.push({ name: 'innerRadius', update: "outerRadius - (radiusRef * 0.25)"}) + signals.push({ name: 'theta', update: "scale('angleScale', clampedVal)"}) + signals.push({ name: 'centerX', update: "width/2"}) + signals.push({ name: 'centerY', update: "height/2 + outerRadius/2"}) + signals.push({ name: 'clampedVal', update: "min(max(arcMinVal, currVal), arcMaxVal)"}) + signals.push({ name: 'needleAngleRaw', update: "scale('angleScale', clampedVal)"}) + signals.push({ name: 'needleAngle', update: "needleAngleRaw - PI/2"}) + signals.push({ name: 'needleLength', update: "innerRadius"}) + signals.push({ name: 'needleTipX', update: "centerX + needleLength * cos(needleAngle)"}) + signals.push({ name: 'needleTipY', update: "centerY + needleLength * sin(needleAngle)"}) + +}); + +export const addScales = produce<Scale[], [GaugeSpecOptions]>((scales, options) => { + scales.push({ + name: 'angleScale', + type: 'linear', + domain: [{ signal: 'arcMinVal' }, { signal: 'arcMaxVal' }], + range: [{ signal: 'startAngle' }, { signal: 'endAngle' }], + clamp: true + }); }); From 565b2fca55c7072b20a4a6f27a016c751986f5a7 Mon Sep 17 00:00:00 2001 From: Brady Shimanek <bradyshimanek33@gmail.com> Date: Fri, 24 Oct 2025 10:10:33 -0600 Subject: [PATCH 15/66] Removed some comments --- .../src/gauge/gaugeSpecBuilder.ts | 27 ++----------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index e982d0bce..4388eb4ca 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -13,16 +13,8 @@ import { produce } from 'immer'; import { Mark, Signal, Scale } from 'vega'; import { addGaugeMarks } from './gaugeMarkUtils'; -// import { -// DEFAULT_GAUGE_DIRECTION, -// DEFAULT_COLOR_SCHEME, -// DEFAULT_LABEL_POSITION, -// DEFAULT_SCALE_TYPE, -// DEFAULT_SCALE_VALUE, -// } from '@spectrum-charts/constants'; - -import { DEFAULT_COLOR_SCHEME } from '@spectrum-charts/constants'; // this is the only constant i needed to get a simple blue rect to render +import { DEFAULT_COLOR_SCHEME } from '@spectrum-charts/constants'; // import { getColorValue, spectrumColors } from '@spectrum-charts/themes'; import { getColorValue, spectrumColors } from '@spectrum-charts/themes'; @@ -37,7 +29,7 @@ const DEFAULT_COLOR = spectrumColors.light['blue-900']; // can't figure out why /** * Adds a simple Gauge chart to the spec - * This is a simplified version that just renders a blue rectangle as proof of concept + * */ export const addGauge = produce< ScSpec, @@ -75,21 +67,6 @@ export const addGauge = produce< spec.signals = addSignals(spec.signals ?? [], gaugeOptions); spec.scales = addScales(spec.scales ?? [], gaugeOptions); spec.marks = addGaugeMarks(spec.marks ?? [], gaugeOptions); - - // Add a simple blue rectangle mark as proof of concept - // spec.marks.push({ - // type: 'rect', - // name: `${gaugeOptions.name}_test_rectangle`, - // encode: { - // enter: { - // x: { value: 50 }, - // y: { value: 50 }, - // width: { value: 200 }, - // height: { value: 100 }, - // fill: { value: gaugeOptions.color }, - // }, - // }, - // }); } ); From 42d2cc236450bec8a5d7d7f79ad6fc156ed37614 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Fri, 24 Oct 2025 15:03:47 -0600 Subject: [PATCH 16/66] Stinking # symbols --- packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index 4388eb4ca..99fdd0c90 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -79,8 +79,8 @@ export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, option signals.push({ name: 'endAngle', update: "PI / 2" }); // 90 degrees signals.push({ name: 'currVal', value: "30" }); signals.push({ name: 'target', value: "80" }); - signals.push({ name: 'backgroundfillColor', value: "77A7FB"}) - signals.push({ name: 'fillerColorToCurrVal', value: "89CFF0"}) + signals.push({ name: 'backgroundfillColor', value: "#77A7FB"}) + signals.push({ name: 'fillerColorToCurrVal', value: "#89CFF0"}) signals.push({ name: 'TargetTextTheta', update: "scale('angleScale', target)"}) signals.push({ name: 'targetTextX', update: "centerX + (outerRadius + 40) * sin(TargetTextTheta)"}) signals.push({ name: 'targetTextY', update: "centerY - (outerRadius + 40) * cos(TargetTextTheta)"}) From a3385dd7517c8e9df9b76bc8f56382617d7f9cf4 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Fri, 24 Oct 2025 15:07:10 -0600 Subject: [PATCH 17/66] WIP: gauge work --- packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index 99fdd0c90..4388eb4ca 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -79,8 +79,8 @@ export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, option signals.push({ name: 'endAngle', update: "PI / 2" }); // 90 degrees signals.push({ name: 'currVal', value: "30" }); signals.push({ name: 'target', value: "80" }); - signals.push({ name: 'backgroundfillColor', value: "#77A7FB"}) - signals.push({ name: 'fillerColorToCurrVal', value: "#89CFF0"}) + signals.push({ name: 'backgroundfillColor', value: "77A7FB"}) + signals.push({ name: 'fillerColorToCurrVal', value: "89CFF0"}) signals.push({ name: 'TargetTextTheta', update: "scale('angleScale', target)"}) signals.push({ name: 'targetTextX', update: "centerX + (outerRadius + 40) * sin(TargetTextTheta)"}) signals.push({ name: 'targetTextY', update: "centerY - (outerRadius + 40) * cos(TargetTextTheta)"}) From ebe0bb52d61d14cecc00089e34279882ba2d59db Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Fri, 24 Oct 2025 15:07:42 -0600 Subject: [PATCH 18/66] Fixing the color problems. --- packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index 4388eb4ca..99fdd0c90 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -79,8 +79,8 @@ export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, option signals.push({ name: 'endAngle', update: "PI / 2" }); // 90 degrees signals.push({ name: 'currVal', value: "30" }); signals.push({ name: 'target', value: "80" }); - signals.push({ name: 'backgroundfillColor', value: "77A7FB"}) - signals.push({ name: 'fillerColorToCurrVal', value: "89CFF0"}) + signals.push({ name: 'backgroundfillColor', value: "#77A7FB"}) + signals.push({ name: 'fillerColorToCurrVal', value: "#89CFF0"}) signals.push({ name: 'TargetTextTheta', update: "scale('angleScale', target)"}) signals.push({ name: 'targetTextX', update: "centerX + (outerRadius + 40) * sin(TargetTextTheta)"}) signals.push({ name: 'targetTextY', update: "centerY - (outerRadius + 40) * cos(TargetTextTheta)"}) From 2c4a18608f84957853a5920635b72c3963fb18c1 Mon Sep 17 00:00:00 2001 From: Brady Shimanek <bradyshimanek33@gmail.com> Date: Mon, 27 Oct 2025 19:10:02 -0600 Subject: [PATCH 19/66] Added data-driven basics, color scheme corrections, and flexible props --- .../stories/components/Gauge/Gauge.story.tsx | 4 +-- .../src/stories/components/Gauge/data.ts | 4 +-- .../src/gauge/gaugeSpecBuilder.ts | 31 ++++++++++++------- .../src/types/marks/gaugeSpec.types.ts | 25 ++++++++++----- 4 files changed, 40 insertions(+), 24 deletions(-) diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx index 4afa99e12..5f10ce3ee 100644 --- a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx @@ -72,7 +72,7 @@ GaugeVariation2.args = { target: 'target', color: 'red-900', numberFormat: '$,.2f', - maxArcValue: 100 + maxArcValue: 150 }; const GaugeVariation3 = bindWithProps(GaugeStory); @@ -81,7 +81,7 @@ GaugeVariation3.args = { target: 'target', color: 'fuchsia-900', numberFormat: '$,.2f', - maxArcValue: 100 + maxArcValue: 90 }; // const Track = bindWithProps(GaugeStory); diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts b/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts index 8ed75a9c0..c6f30fdac 100644 --- a/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts @@ -11,11 +11,9 @@ */ -// ok this is the actual data for the bullet chart. -// We can keep it for now once we get a working Gauge chart to render. export const basicGaugeData = [ - { graphLabel: 'Customers', currentAmount: 150, target: 50 }, + { graphLabel: 'Customers', currentAmount: 60, target: 80 }, { graphLabel: 'Revenue', currentAmount: 350, target: 450 }, ]; diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index 99fdd0c90..b57a031a2 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ import { produce } from 'immer'; -import { Mark, Signal, Scale } from 'vega'; +import { Mark, Signal, Scale, Data } from 'vega'; import { addGaugeMarks } from './gaugeMarkUtils'; @@ -21,6 +21,7 @@ import { getColorValue, spectrumColors } from '@spectrum-charts/themes'; import { toCamelCase } from '@spectrum-charts/utils'; import { ColorScheme, GaugeOptions, GaugeSpecOptions, ScSpec } from '../types'; +import { getGaugeTableData } from './gaugeDataUtils'; const DEFAULT_COLOR = spectrumColors.light['blue-900']; // can't figure out why this doesnt change if i leave it blank // might be because this sets the default color for the gauge component for the user when they use a <Gauge /> component. @@ -53,20 +54,22 @@ export const addGauge = produce< target: 'target', color: getColorValue(color, colorScheme), // Convert spectrum color to RGB numberFormat: '', - maxArcValue: 100, - backgroundFill: '#eee', - backgroundStroke: '#999', + maxArcValue: 100, // sets the DEFAULT max value for the gauge + backgroundFill: spectrumColors[colorScheme]['gray-200'], + backgroundStroke: spectrumColors[colorScheme]['gray-300'], fillerColorSignal: 'fillerColorToCurrVal', straightEdgeOffsetExpr: 'PI/15', - labelColor: '#333', + labelColor: spectrumColors[colorScheme]['gray-800'], labelSize: 40, - ...options, + ...options, // this overrides the default if the user passes a prop when using the <Gauge /> component }; spec.signals = addSignals(spec.signals ?? [], gaugeOptions); spec.scales = addScales(spec.scales ?? [], gaugeOptions); spec.marks = addGaugeMarks(spec.marks ?? [], gaugeOptions); + spec.data = addData(spec.data ?? [], gaugeOptions); + } ); @@ -74,13 +77,13 @@ export const addGauge = produce< export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, options) => { signals.push({ name: 'arcMinVal', value: 0 }); - signals.push({ name: 'arcMaxVal', value: 100 }); + signals.push({ name: 'arcMaxVal', value: options.maxArcValue }); signals.push({ name: 'startAngle', update: "-PI / 2" }); // -90 degrees signals.push({ name: 'endAngle', update: "PI / 2" }); // 90 degrees - signals.push({ name: 'currVal', value: "30" }); - signals.push({ name: 'target', value: "80" }); - signals.push({ name: 'backgroundfillColor', value: "#77A7FB"}) - signals.push({ name: 'fillerColorToCurrVal', value: "#89CFF0"}) + signals.push({ name: 'currVal', update: `data('table')[0].${options.metric}` }); + signals.push({ name: 'target', update: `data('table')[0].${options.target}` }); + signals.push({ name: 'backgroundfillColor', value: `${options.backgroundFill}`}) + signals.push({ name: 'fillerColorToCurrVal', value: `${options.color}`}) signals.push({ name: 'TargetTextTheta', update: "scale('angleScale', target)"}) signals.push({ name: 'targetTextX', update: "centerX + (outerRadius + 40) * sin(TargetTextTheta)"}) signals.push({ name: 'targetTextY', update: "centerY - (outerRadius + 40) * cos(TargetTextTheta)"}) @@ -120,3 +123,9 @@ export const addScales = produce<Scale[], [GaugeSpecOptions]>((scales, options) clamp: true }); }); + +export const addData = produce<Data[], [GaugeSpecOptions]>((data, options) => { + const tableData = getGaugeTableData(data); + // We can add Transforms here if we need to. + tableData.transform = []; // Or add gauge-specific transforms here +}); \ No newline at end of file diff --git a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts index bea44b1a2..01c080150 100644 --- a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts +++ b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts @@ -35,6 +35,16 @@ export interface GaugeOptions { target?: string; /** Color regions that fill the gauge bar to the metric value */ track?: boolean; + /** Color of the background fill */ + backgroundFill?: string; + /** Color of the background stroke */ + backgroundStroke?: string; + /** Color of the filler color signal */ + fillerColorSignal?: string; + /** Color of the label text */ + labelColor?: string; + /** Size of the label text */ + labelSize?: number; } type GaugeOptionsWithDefaults = @@ -42,18 +52,17 @@ type GaugeOptionsWithDefaults = | 'metric' | 'target' | 'color' - | 'maxArcValue'; + | 'maxArcValue' + | 'backgroundFill' + | 'backgroundStroke' + | 'fillerColorSignal' + | 'labelColor' + | 'labelSize'; export interface GaugeSpecOptions extends PartiallyRequired<GaugeOptions, GaugeOptionsWithDefaults> { // What does this do? colorScheme: ColorScheme; idKey: string; index: number; - name: string; - backgroundFill: '#eee', - backgroundStroke: '#999', - fillerColorSignal: 'fillerColorToCurrVal', - straightEdgeOffsetExpr: 'PI/15', - labelColor: '#333', - labelSize: 40, + straightEdgeOffsetExpr: 'PI/15'; // hardcoded. change to number or string? } From 1952a7614d6f6077273fe74e3a2f9d771fffe885 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:05:00 -0600 Subject: [PATCH 20/66] Fixed the filler arc to have the one straight edge --- .../src/gauge/gaugeMarkUtils.ts | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 50d84d0b6..11ef88f7a 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -29,7 +29,7 @@ export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => // Background arcs (rounded, then straight overlay) marks.push(getBackgroundArcRounded(name, backgroundFill, backgroundStroke)); - marks.push(getBackgroundArcStraight(name, backgroundFill, backgroundStroke, straightEdgeOffsetExpr)); + marks.push(getFillerArcStraight(name, fillerColorSignal, backgroundStroke, straightEdgeOffsetExpr)); // Text labels: max, target, min marks.push(getMaxValueText(name, labelColor, labelSize)); @@ -62,27 +62,6 @@ function getBackgroundArcRounded(name: string, fill: string, stroke: string): Ma }; } -function getBackgroundArcStraight(name: string, fill: string, stroke: string, offsetExpr: string): Mark { - return { - name: `${name}BackgroundArcStraight`, - description: 'Background Arc (Straight Edge)', - type: 'arc', - encode: { - enter: { - x: { signal: 'centerX' }, - y: { signal: 'centerY' }, - innerRadius: { signal: 'innerRadius' }, - outerRadius: { signal: 'outerRadius' }, - // startAngle offset to not flatten the right edge - startAngle: { signal: `startAngle + (${offsetExpr})` }, - endAngle: { signal: 'endAngle' }, - fill: { value: fill }, - stroke: { value: stroke } - } - } - }; -} - function getMaxValueText(name: string, color: string, fontSize: number): Mark { return { name: `${name}MaxValText`, @@ -170,6 +149,30 @@ function getFillerArc(name: string, fillerColorSignal: string): Mark { }; } +function getFillerArcStraight(name: string, fillerColorSignal: string, stroke: string, offsetExpr: string): Mark { + return { + name: `${name}FillerArcStraight`, + description: 'Filler Arc (Straight Edge)', + type: 'arc', + encode: { + enter: { + x: { signal: 'centerX' }, + y: { signal: 'centerY' }, + innerRadius: { signal: 'innerRadius' }, + outerRadius: { signal: 'outerRadius' }, + // startAngle offset to not flatten the right edge + startAngle: { signal: `startAngle + (${offsetExpr})` }, + endAngle: { signal: 'endAngle' }, + fill: { signal: fillerColorSignal }, + stroke: { value: stroke } + }, + update: { + endAngle: { signal: "scale('angleScale', clampedVal)" }, + } + } + }; +} + function getNeedleRule(name: string): Mark { return { name: `${name}Needle`, From 38cff119d28647500eaf6f8643e32c06864b0f01 Mon Sep 17 00:00:00 2001 From: JarekSmith <105569335+JarekSmith@users.noreply.github.com> Date: Thu, 30 Oct 2025 16:31:18 -0600 Subject: [PATCH 21/66] Remove `track` from gaugeSpec --- packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts index 01c080150..c540b5e2f 100644 --- a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts +++ b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts @@ -34,7 +34,7 @@ export interface GaugeOptions { /** Flag to control whether the target is shown */ target?: string; /** Color regions that fill the gauge bar to the metric value */ - track?: boolean; + //track?: boolean; /** Color of the background fill */ backgroundFill?: string; /** Color of the background stroke */ From 83cee9f1a3efb1ffcfb0912047842764e7c04d86 Mon Sep 17 00:00:00 2001 From: Brady Shimanek <bradyshimanek33@gmail.com> Date: Thu, 30 Oct 2025 20:13:23 -0600 Subject: [PATCH 22/66] Touched up currVal to be dynamic --- .../src/stories/components/Gauge/Gauge.story.tsx | 2 +- packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts | 3 ++- .../vega-spec-builder/src/types/marks/gaugeSpec.types.ts | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx index 5f10ce3ee..8d77356b3 100644 --- a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx @@ -68,7 +68,7 @@ Basic.args = { const GaugeVariation2 = bindWithProps(GaugeStory); GaugeVariation2.args = { - metric: 'currentAmount', + metric: 50, target: 'target', color: 'red-900', numberFormat: '$,.2f', diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index b57a031a2..86af8723b 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -80,7 +80,8 @@ export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, option signals.push({ name: 'arcMaxVal', value: options.maxArcValue }); signals.push({ name: 'startAngle', update: "-PI / 2" }); // -90 degrees signals.push({ name: 'endAngle', update: "PI / 2" }); // 90 degrees - signals.push({ name: 'currVal', update: `data('table')[0].${options.metric}` }); + // signals.push({ name: 'currVal', update: `data('table')[0].${options.metric}` }); + signals.push({ name: 'currVal', value: options.metric }); signals.push({ name: 'target', update: `data('table')[0].${options.target}` }); signals.push({ name: 'backgroundfillColor', value: `${options.backgroundFill}`}) signals.push({ name: 'fillerColorToCurrVal', value: `${options.color}`}) diff --git a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts index c540b5e2f..d9b0b0319 100644 --- a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts +++ b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts @@ -22,7 +22,7 @@ export interface GaugeOptions { /** Maximum value for the scale. This value must be greater than zero. */ maxArcValue?: number; /** Key in the data that is used as the metric */ - metric?: string; + metric?: string | number; /** Sets the name of the component. */ name?: string; /** d3 number format specifier. @@ -64,5 +64,6 @@ export interface GaugeSpecOptions extends PartiallyRequired<GaugeOptions, GaugeO colorScheme: ColorScheme; idKey: string; index: number; - straightEdgeOffsetExpr: 'PI/15'; // hardcoded. change to number or string? + straightEdgeOffsetExpr: 'PI/15' + // hardcoded. change to number or string? } From a370f90e8f9dd2c5edf1566d5af4998e89cc47cc Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Thu, 6 Nov 2025 12:16:43 -0700 Subject: [PATCH 23/66] making the minimalistic skeleton for the gauge --- .../src/gauge/gaugeMarkUtils.ts | 115 ++---------------- .../src/gauge/gaugeSpecBuilder.ts | 9 +- 2 files changed, 9 insertions(+), 115 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 11ef88f7a..41354a835 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -12,8 +12,6 @@ import { produce } from 'immer'; import { Mark } from 'vega'; -// import { getColorValue } from '@spectrum-charts/themes'; - import { GaugeSpecOptions } from '../types'; export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => { @@ -22,26 +20,19 @@ export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => backgroundFill = '#eee', backgroundStroke = '#999', fillerColorSignal = 'fillerColorToCurrVal', - straightEdgeOffsetExpr = 'PI/15', - labelColor = '#333', - labelSize = 40, } = opt; - // Background arcs (rounded, then straight overlay) - marks.push(getBackgroundArcRounded(name, backgroundFill, backgroundStroke)); - marks.push(getFillerArcStraight(name, fillerColorSignal, backgroundStroke, straightEdgeOffsetExpr)); - - // Text labels: max, target, min - marks.push(getMaxValueText(name, labelColor, labelSize)); - marks.push(getTargetValueText(name, labelColor, labelSize)); - marks.push(getMinValueText(name, labelColor, labelSize)); + // Background arc + marks.push(getBackgroundArc(name, backgroundFill, backgroundStroke)); - // Filler arc (value fill) + // Filler arc (fills to clampedValue) marks.push(getFillerArc(name, fillerColorSignal)); + // Needle to clampedValue + marks.push(getNeedle(name)); }); -function getBackgroundArcRounded(name: string, fill: string, stroke: string): Mark { +function getBackgroundArc(name: string, fill: string, stroke: string): Mark { return { name: `${name}BackgroundArcRounded`, description: 'Background Arc (Round Edge)', @@ -54,7 +45,6 @@ function getBackgroundArcRounded(name: string, fill: string, stroke: string): Ma outerRadius: { signal: 'outerRadius' }, startAngle: { signal: 'startAngle' }, endAngle: { signal: 'endAngle' }, - cornerRadius:{ signal: 'cornerR' }, fill: { value: fill }, stroke: { value: stroke } } @@ -62,69 +52,6 @@ function getBackgroundArcRounded(name: string, fill: string, stroke: string): Ma }; } -function getMaxValueText(name: string, color: string, fontSize: number): Mark { - return { - name: `${name}MaxValText`, - description: 'Max Val Text', - type: 'text', - encode: { - enter: { - x: { signal: 'MaxTextX' }, - y: { signal: 'MaxTextY' }, - text: { signal: "format(arcMaxVal, '.0f')" }, - align: { value: 'center' }, - baseline: { value: 'middle' }, - fontSize: { value: fontSize }, - fill: { value: color }, - fontWeight:{ value: 'bold' } - }, - update: { - endAngle: { signal: "scale('angleScale', arcMaxVal)" } - } - } - }; -} - -function getTargetValueText(name: string, color: string, fontSize: number): Mark { - return { - name: `${name}TargetValText`, - description: 'Target Val Text', - type: 'text', - encode: { - enter: { - x: { signal: 'targetTextX' }, - y: { signal: 'targetTextY' }, - text: { signal: "format(target, '.0f')" }, - align: { value: 'center' }, - baseline: { value: 'middle' }, - fontSize: { value: fontSize }, - fontWeight:{ value: 'bold' }, - fill: { value: color } - } - } - }; -} - -function getMinValueText(name: string, color: string, fontSize: number): Mark { - return { - name: `${name}MinValText`, - description: 'Min Val Text', - type: 'text', - encode: { - enter: { - x: { signal: 'MinTextX' }, - y: { signal: 'MinTextY' }, - text: { signal: "format(arcMinVal, '.0f')" }, - align: { value: 'center' }, - baseline: { value: 'middle' }, - fontSize: { value: fontSize }, - fontWeight:{ value: 'bold' }, - fill: { value: color } - } - } - }; -} - function getFillerArc(name: string, fillerColorSignal: string): Mark { return { name: `${name}FillerArc`, @@ -141,39 +68,13 @@ function getFillerArc(name: string, fillerColorSignal: string): Mark { fill: { signal: fillerColorSignal } }, update: { - endAngle: { signal: "scale('angleScale', clampedVal)" }, - // Square end normally; rounded when “full” - cornerRadius: { signal: "!isFull ? cornerR : 0" } - } - } - }; -} - -function getFillerArcStraight(name: string, fillerColorSignal: string, stroke: string, offsetExpr: string): Mark { - return { - name: `${name}FillerArcStraight`, - description: 'Filler Arc (Straight Edge)', - type: 'arc', - encode: { - enter: { - x: { signal: 'centerX' }, - y: { signal: 'centerY' }, - innerRadius: { signal: 'innerRadius' }, - outerRadius: { signal: 'outerRadius' }, - // startAngle offset to not flatten the right edge - startAngle: { signal: `startAngle + (${offsetExpr})` }, - endAngle: { signal: 'endAngle' }, - fill: { signal: fillerColorSignal }, - stroke: { value: stroke } - }, - update: { - endAngle: { signal: "scale('angleScale', clampedVal)" }, + endAngle: { signal: "scale('angleScale', clampedVal)" } } } }; } - function getNeedleRule(name: string): Mark { + function getNeedle(name: string): Mark { return { name: `${name}Needle`, description: 'Needle (rule)', diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index 86af8723b..4bc097607 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -80,8 +80,7 @@ export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, option signals.push({ name: 'arcMaxVal', value: options.maxArcValue }); signals.push({ name: 'startAngle', update: "-PI / 2" }); // -90 degrees signals.push({ name: 'endAngle', update: "PI / 2" }); // 90 degrees - // signals.push({ name: 'currVal', update: `data('table')[0].${options.metric}` }); - signals.push({ name: 'currVal', value: options.metric }); + signals.push({ name: 'currVal', update: `data('table')[0].${options.metric}` }); signals.push({ name: 'target', update: `data('table')[0].${options.target}` }); signals.push({ name: 'backgroundfillColor', value: `${options.backgroundFill}`}) signals.push({ name: 'fillerColorToCurrVal', value: `${options.color}`}) @@ -90,14 +89,8 @@ export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, option signals.push({ name: 'targetTextY', update: "centerY - (outerRadius + 40) * cos(TargetTextTheta)"}) signals.push({ name: 'StartTextTheta', update: "scale('angleScale', arcMinVal)"}) signals.push({ name: 'EndTextTheta', update: "scale('angleScale', arcMaxVal)"}) - signals.push({ name: 'MinTextX', update: "centerX + (innerRadius + (outerRadius - innerRadius) / 2) * sin(StartTextTheta)"}) - signals.push({ name: 'MinTextY', update: "centerY + 40"}) - signals.push({ name: 'MaxTextX', update: "centerX + (innerRadius + (outerRadius - innerRadius) / 2) * sin(EndTextTheta)"}) - signals.push({ name: 'MaxTextY', update: "centerY + 40"}) - signals.push({ name: 'cornerR', update: "(outerRadius - innerRadius) * 0.45"}) signals.push({ name: 'capSpan', update: "(cornerR / outerRadius) * 2"}) signals.push({ name: 'valueEnd', update: "scale('angleScale', clampedVal)"}) - signals.push({ name: 'isFull', update: "clampedVal >= arcMaxVal"}) signals.push({ name: 'capEnd', update: "min(valueEnd, startAngle + capSpan)"}) signals.push({ name: 'mainStart', update: "isFull ? startAngle : capEnd"}) signals.push({ name: 'radiusRef', update: "min(width/2, height/2)"}) From b0bb8e6ba41bd4f164e27b501c33fa9a306dbc62 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Thu, 6 Nov 2025 21:02:53 -0700 Subject: [PATCH 24/66] Simpler implementation --- .../src/gauge/gaugeDataUtils.ts | 25 ---------- .../src/gauge/gaugeSpecBuilder.ts | 48 +++++++------------ .../src/types/marks/gaugeSpec.types.ts | 6 +-- 3 files changed, 21 insertions(+), 58 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeDataUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeDataUtils.ts index c331fbc7b..b9665bd06 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeDataUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeDataUtils.ts @@ -43,31 +43,6 @@ export const getGaugeTableData = (data: Data[]): ValuesData => { * @returns An array of formula transforms. */ export const getGaugeTransforms = (gaugeOptions: GaugeSpecOptions): FormulaTransform[] => { - const transforms: FormulaTransform[] = [ - { - type: 'formula', - expr: `isValid(datum.${gaugeOptions.target}) ? round(datum.${gaugeOptions.target} * 1.05) : 0`, - as: 'xPaddingForTarget', - }, - ]; - - if (gaugeOptions.scaleType === 'flexible') { - transforms.push({ - type: 'formula', - expr: `${gaugeOptions.maxScaleValue}`, - as: 'flexibleScaleValue', - }); - } - - if (gaugeOptions.thresholdBarColor && (gaugeOptions.thresholds?.length ?? 0) > 0) { - transforms.push({ - type: 'formula', - expr: generateThresholdColorExpr(gaugeOptions.thresholds ?? [], gaugeOptions.metric, gaugeOptions.color), - as: 'barColor', - }); - } - - return transforms; }; /** diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index 4bc097607..33ff21ebb 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -52,16 +52,16 @@ export const addGauge = produce< name: toCamelCase(name ?? `gauge${index}`), metric: 'currentAmount', target: 'target', - color: getColorValue(color, colorScheme), // Convert spectrum color to RGB + color: getColorValue(color, colorScheme), numberFormat: '', - maxArcValue: 100, // sets the DEFAULT max value for the gauge + minArcValue: 0, + maxArcValue: 100, backgroundFill: spectrumColors[colorScheme]['gray-200'], backgroundStroke: spectrumColors[colorScheme]['gray-300'], fillerColorSignal: 'fillerColorToCurrVal', - straightEdgeOffsetExpr: 'PI/15', labelColor: spectrumColors[colorScheme]['gray-800'], labelSize: 40, - ...options, // this overrides the default if the user passes a prop when using the <Gauge /> component + ...options, }; @@ -75,37 +75,26 @@ export const addGauge = produce< ); export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, options) => { - - signals.push({ name: 'arcMinVal', value: 0 }); signals.push({ name: 'arcMaxVal', value: options.maxArcValue }); - signals.push({ name: 'startAngle', update: "-PI / 2" }); // -90 degrees - signals.push({ name: 'endAngle', update: "PI / 2" }); // 90 degrees - signals.push({ name: 'currVal', update: `data('table')[0].${options.metric}` }); - signals.push({ name: 'target', update: `data('table')[0].${options.target}` }); - signals.push({ name: 'backgroundfillColor', value: `${options.backgroundFill}`}) - signals.push({ name: 'fillerColorToCurrVal', value: `${options.color}`}) - signals.push({ name: 'TargetTextTheta', update: "scale('angleScale', target)"}) - signals.push({ name: 'targetTextX', update: "centerX + (outerRadius + 40) * sin(TargetTextTheta)"}) - signals.push({ name: 'targetTextY', update: "centerY - (outerRadius + 40) * cos(TargetTextTheta)"}) - signals.push({ name: 'StartTextTheta', update: "scale('angleScale', arcMinVal)"}) - signals.push({ name: 'EndTextTheta', update: "scale('angleScale', arcMaxVal)"}) - signals.push({ name: 'capSpan', update: "(cornerR / outerRadius) * 2"}) - signals.push({ name: 'valueEnd', update: "scale('angleScale', clampedVal)"}) - signals.push({ name: 'capEnd', update: "min(valueEnd, startAngle + capSpan)"}) - signals.push({ name: 'mainStart', update: "isFull ? startAngle : capEnd"}) - signals.push({ name: 'radiusRef', update: "min(width/2, height/2)"}) - signals.push({ name: 'outerRadius', update: "radiusRef * 0.95"}) - signals.push({ name: 'innerRadius', update: "outerRadius - (radiusRef * 0.25)"}) - signals.push({ name: 'theta', update: "scale('angleScale', clampedVal)"}) + signals.push({ name: 'arcMinVal', value: options.minArcValue }); + signals.push({ name: 'backgroundfillColor', value: `${options.backgroundFill}`}); signals.push({ name: 'centerX', update: "width/2"}) signals.push({ name: 'centerY', update: "height/2 + outerRadius/2"}) signals.push({ name: 'clampedVal', update: "min(max(arcMinVal, currVal), arcMaxVal)"}) - signals.push({ name: 'needleAngleRaw', update: "scale('angleScale', clampedVal)"}) - signals.push({ name: 'needleAngle', update: "needleAngleRaw - PI/2"}) + signals.push({ name: 'currVal', update: `data('table')[0].${options.metric}` }); + signals.push({ name: 'endAngle', update: "PI * 2 / 3" }); // 120 degrees + signals.push({ name: 'fillerColorToCurrVal', value: `${options.color}`}) + signals.push({ name: 'innerRadius', update: "outerRadius - (radiusRef * 0.25)"}) + signals.push({ name: 'needleAngle', update: "needleAngleOriginal - PI/2"}) + signals.push({ name: 'needleAngleOriginal', update: "scale('angleScale', clampedVal)"}) signals.push({ name: 'needleLength', update: "innerRadius"}) signals.push({ name: 'needleTipX', update: "centerX + needleLength * cos(needleAngle)"}) signals.push({ name: 'needleTipY', update: "centerY + needleLength * sin(needleAngle)"}) - + signals.push({ name: 'outerRadius', update: "radiusRef * 0.95"}) + signals.push({ name: 'radiusRef', update: "min(width/2, height/2)"}) + signals.push({ name: 'startAngle', update: "-PI * 2 / 3" }); // -120 degrees + signals.push({ name: 'target', update: `data('table')[0].${options.target}` }); + signals.push({ name: 'theta', update: "scale('angleScale', clampedVal)"}) }); export const addScales = produce<Scale[], [GaugeSpecOptions]>((scales, options) => { @@ -120,6 +109,5 @@ export const addScales = produce<Scale[], [GaugeSpecOptions]>((scales, options) export const addData = produce<Data[], [GaugeSpecOptions]>((data, options) => { const tableData = getGaugeTableData(data); - // We can add Transforms here if we need to. - tableData.transform = []; // Or add gauge-specific transforms here + tableData.transform = []; }); \ No newline at end of file diff --git a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts index d9b0b0319..40494a522 100644 --- a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts +++ b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts @@ -19,6 +19,8 @@ export interface GaugeOptions { /** Key in the data that is used as the color facet */ color?: string; + /** Minimum value for the scale. This value must be greater than zero. */ + minArcValue?: number; /** Maximum value for the scale. This value must be greater than zero. */ maxArcValue?: number; /** Key in the data that is used as the metric */ @@ -52,6 +54,7 @@ type GaugeOptionsWithDefaults = | 'metric' | 'target' | 'color' + | 'minArcValue' | 'maxArcValue' | 'backgroundFill' | 'backgroundStroke' @@ -60,10 +63,7 @@ type GaugeOptionsWithDefaults = | 'labelSize'; export interface GaugeSpecOptions extends PartiallyRequired<GaugeOptions, GaugeOptionsWithDefaults> { - // What does this do? colorScheme: ColorScheme; idKey: string; index: number; - straightEdgeOffsetExpr: 'PI/15' - // hardcoded. change to number or string? } From 73cab9c92ffb6953deb190cc47280ffe49f06df5 Mon Sep 17 00:00:00 2001 From: Brady Shimanek <bradyshimanek33@gmail.com> Date: Thu, 6 Nov 2025 22:08:09 -0700 Subject: [PATCH 25/66] Cleaned up basic Gauge --- .../src/alpha/components/Gauge/Gauge.tsx | 3 - .../stories/components/Gauge/Gauge.story.tsx | 89 +------------------ .../src/stories/components/Gauge/data.ts | 10 --- .../src/gauge/gaugeDataUtils.ts | 42 +-------- .../src/gauge/gaugeSpecBuilder.ts | 19 ++-- .../src/types/marks/gaugeSpec.types.ts | 4 +- 6 files changed, 12 insertions(+), 155 deletions(-) diff --git a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx index ba2acffe0..8174d9bd1 100644 --- a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx +++ b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx @@ -14,9 +14,6 @@ import { FC } from 'react'; import { - DEFAULT_BULLET_DIRECTION, - DEFAULT_LABEL_POSITION, - DEFAULT_SCALE_TYPE, DEFAULT_SCALE_VALUE, } from '@spectrum-charts/constants'; diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx index 8d77356b3..0f7348c92 100644 --- a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx @@ -20,7 +20,7 @@ import { Title } from '../../../components'; import useChartProps from '../../../hooks/useChartProps'; import { bindWithProps } from '../../../test-utils'; import { GaugeProps, ChartProps } from '../../../types'; -import { basicGaugeData, basicThresholdsData, coloredThresholdsData } from './data'; +import { basicGaugeData } from './data'; export default { title: 'RSC/Gauge (alpha)', @@ -68,7 +68,7 @@ Basic.args = { const GaugeVariation2 = bindWithProps(GaugeStory); GaugeVariation2.args = { - metric: 50, + metric: 'currentAmount', target: 'target', color: 'red-900', numberFormat: '$,.2f', @@ -84,91 +84,6 @@ GaugeVariation3.args = { maxArcValue: 90 }; -// const Track = bindWithProps(GaugeStory); -// Track.args = { -// metric: 'currentAmount', -// dimension: 'graphLabel', -// target: 'target', -// color: 'blue-900', -// direction: 'column', -// numberFormat: '$,.2f', -// showTarget: true, -// showTargetValue: false, -// labelPosition: 'top', -// scaleType: 'normal', -// maxScaleValue: 100, -// track: true, -// metricAxis: false, -// }; - -// const RowMode = bindWithProps(GaugeStory); -// RowMode.args = { -// metric: 'currentAmount', -// dimension: 'graphLabel', -// target: 'target', -// color: 'blue-900', -// direction: 'row', -// numberFormat: '$,.2f', -// showTarget: true, -// showTargetValue: false, -// labelPosition: 'top', -// scaleType: 'normal', -// maxScaleValue: 100, -// thresholds: coloredThresholdsData, -// thresholdBarColor: true, -// track: false, -// metricAxis: false, -// }; - -// const WithTitle = bindWithProps(GaugeTitleStory); -// WithTitle.args = { -// metric: 'currentAmount', -// dimension: 'graphLabel', -// target: 'target', -// color: 'blue-900', -// numberFormat: '$,.2f', -// labelPosition: 'top', -// scaleType: 'normal', -// maxScaleValue: 100, -// track: false, -// direction: 'column', -// metricAxis: false, -// }; - -// const FixedScale = bindWithProps(GaugeStory); -// FixedScale.args = { -// metric: 'currentAmount', -// dimension: 'graphLabel', -// target: 'target', -// color: 'blue-900', -// direction: 'column', -// numberFormat: '$,.2f', -// showTarget: true, -// showTargetValue: false, -// labelPosition: 'top', -// scaleType: 'fixed', -// maxScaleValue: 250, -// thresholds: basicThresholdsData, -// track: false, -// metricAxis: false, -// }; -// const MetricAxis = bindWithProps(GaugeStory); -// MetricAxis.args = { -// metric: 'currentAmount', -// dimension: 'graphLabel', -// target: 'target', -// color: 'blue-900', -// direction: 'column', -// numberFormat: '$,.2f', -// showTarget: true, -// showTargetValue: false, -// labelPosition: 'top', -// scaleType: 'normal', -// maxScaleValue: 250, -// track: false, -// metricAxis: true, -// }; export { Basic, GaugeVariation2, GaugeVariation3 }; -// export { Basic, GaugeVariation2, GaugeVariation3, Track, RowMode, WithTitle, FixedScale, MetricAxis }; diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts b/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts index c6f30fdac..bdd0ed37c 100644 --- a/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts @@ -17,14 +17,4 @@ export const basicGaugeData = [ { graphLabel: 'Revenue', currentAmount: 350, target: 450 }, ]; -export const basicThresholdsData = [ - { thresholdMax: 120, fill: 'rgb(0, 0, 0)' }, - { thresholdMin: 120, thresholdMax: 235, fill: 'rgb(109, 109, 109)' }, - { thresholdMin: 235, fill: 'rgb(177, 177, 177)' }, -]; -export const coloredThresholdsData = [ - { thresholdMax: 120, fill: 'rgb(234, 56, 41)' }, - { thresholdMin: 120, thresholdMax: 235, fill: 'rgb(249, 137, 23)' }, - { thresholdMin: 235, fill: 'rgb(21, 164, 110)' }, -]; diff --git a/packages/vega-spec-builder/src/gauge/gaugeDataUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeDataUtils.ts index b9665bd06..8bc25995f 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeDataUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeDataUtils.ts @@ -9,12 +9,11 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -import { Data, FormulaTransform, ValuesData } from 'vega'; +import { Data, ValuesData } from 'vega'; import { TABLE } from '@spectrum-charts/constants'; import { getTableData } from '../data/dataUtils'; -import { GaugeSpecOptions, ThresholdBackground } from '../types'; /** * Retrieves the gauge table data from the provided data array. @@ -28,7 +27,6 @@ export const getGaugeTableData = (data: Data[]): ValuesData => { tableData = { name: TABLE, values: [], - transform: [], }; data.push(tableData); } @@ -42,49 +40,11 @@ export const getGaugeTableData = (data: Data[]): ValuesData => { * @param gaugeOptions The gauge spec properties. * @returns An array of formula transforms. */ -export const getGaugeTransforms = (gaugeOptions: GaugeSpecOptions): FormulaTransform[] => { -}; /** * Generates a Vega expression for the color of the gauge chart based on the provided thresholds. * The expression checks the value of the metric field against the thresholds and assigns the appropriate color. - * @param thresholds An array of threshold objects. - * @param metricField The name of the metric field in the data. * @param defaultColor The default color to use if no thresholds are met. * @returns A string representing the Vega expression for the color. */ -export function generateThresholdColorExpr( - thresholds: ThresholdBackground[], - metricField: string, - defaultColor: string -): string { - if (!thresholds || thresholds.length === 0) return `'${defaultColor}'`; - - const sorted: ThresholdBackground[] = thresholds.slice().sort((a, b) => { - const aMin = a.thresholdMin !== undefined ? a.thresholdMin : -1e12; - const bMin = b.thresholdMin !== undefined ? b.thresholdMin : -1e12; - return aMin - bMin; - }); - - const exprParts: string[] = []; - - // For values below the first threshold's lower bound, use the default color. - exprParts.push( - `(datum.${metricField} < ${ - sorted[0].thresholdMin !== undefined ? sorted[0].thresholdMin : -1e12 - }) ? '${defaultColor}' : ` - ); - - // For each threshold, check if the metric field is within the range defined by the thresholdMin and thresholdMax values. - // If it is, use the corresponding fill color. - for (let i = 0; i < sorted.length - 1; i++) { - const nextLower = sorted[i + 1].thresholdMin !== undefined ? sorted[i + 1].thresholdMin : -1e12; - exprParts.push(`(datum.${metricField} < ${nextLower}) ? '${sorted[i].fill}' : `); - } - - // For values above the last threshold's upper bound, use the last threshold's fill color. - exprParts.push(`'${sorted[sorted.length - 1].fill}'`); - const expr = exprParts.join(''); - return expr; -} diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index 33ff21ebb..eda3ba17a 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -47,20 +47,18 @@ export const addGauge = produce< } ) => { const gaugeOptions: GaugeSpecOptions = { + backgroundFill: spectrumColors[colorScheme]['gray-200'], + backgroundStroke: spectrumColors[colorScheme]['gray-300'], + color: getColorValue(color, colorScheme), colorScheme: colorScheme, + fillerColorSignal: 'fillerColorToCurrVal', index, - name: toCamelCase(name ?? `gauge${index}`), + maxArcValue: 100, + minArcValue: 0, metric: 'currentAmount', - target: 'target', - color: getColorValue(color, colorScheme), + name: toCamelCase(name ?? `gauge${index}`), numberFormat: '', - minArcValue: 0, - maxArcValue: 100, - backgroundFill: spectrumColors[colorScheme]['gray-200'], - backgroundStroke: spectrumColors[colorScheme]['gray-300'], - fillerColorSignal: 'fillerColorToCurrVal', - labelColor: spectrumColors[colorScheme]['gray-800'], - labelSize: 40, + target: 'target', ...options, }; @@ -109,5 +107,4 @@ export const addScales = produce<Scale[], [GaugeSpecOptions]>((scales, options) export const addData = produce<Data[], [GaugeSpecOptions]>((data, options) => { const tableData = getGaugeTableData(data); - tableData.transform = []; }); \ No newline at end of file diff --git a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts index 40494a522..377b9ae09 100644 --- a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts +++ b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts @@ -58,9 +58,7 @@ type GaugeOptionsWithDefaults = | 'maxArcValue' | 'backgroundFill' | 'backgroundStroke' - | 'fillerColorSignal' - | 'labelColor' - | 'labelSize'; + | 'fillerColorSignal'; export interface GaugeSpecOptions extends PartiallyRequired<GaugeOptions, GaugeOptionsWithDefaults> { colorScheme: ColorScheme; From 31dd0e7b42ad1db1e874ede630843c11a30a3acb Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Thu, 6 Nov 2025 22:11:31 -0700 Subject: [PATCH 26/66] Deleting a comment --- packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index eda3ba17a..2f43ba76e 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -23,9 +23,7 @@ import { toCamelCase } from '@spectrum-charts/utils'; import { ColorScheme, GaugeOptions, GaugeSpecOptions, ScSpec } from '../types'; import { getGaugeTableData } from './gaugeDataUtils'; -const DEFAULT_COLOR = spectrumColors.light['blue-900']; // can't figure out why this doesnt change if i leave it blank -// might be because this sets the default color for the gauge component for the user when they use a <Gauge /> component. -// the color I see in Storybook is what is hardcoded in Gauge.story.tsx +const DEFAULT_COLOR = spectrumColors.light['blue-900']; /** From 77d4b353bce8b4d60709038b049360d1eb3a05d4 Mon Sep 17 00:00:00 2001 From: JarekSmith <105569335+JarekSmith@users.noreply.github.com> Date: Thu, 9 Oct 2025 14:22:44 -0600 Subject: [PATCH 27/66] Make gauge appear in Storybook with Bullet graph Found things we need to ask questions about adding case for gauge on chartSpecBuilder.ts Began adding Signals Messed with the signals Progress on Mark Working on Mark Util Marks Util Progress Marks Util Progress Mark Updates Adding Needle Rule Mark. Fixed paranthesis Got gauge to render Removed some comments Stinking # symbols WIP: gauge work Fixing the color problems. Added data-driven basics, color scheme corrections, and flexible props Fixed the filler arc to have the one straight edge Remove `track` from gaugeSpec Touched up currVal to be dynamic making the minimalistic skeleton for the gauge Simpler implementation Cleaned up basic Gauge Deleting a comment --- package.json | 2 +- .../src/alpha/components/Gauge/Gauge.tsx | 21 +- .../src/alpha/components/index.ts | 2 +- .../src/rscToSbAdapter/childrenAdapter.ts | 7 +- .../components/Bullet/Bullet.story.tsx | 2 +- .../stories/components/Gauge/Gauge.story.tsx | 174 ++------ .../src/stories/components/Gauge/data.ts | 16 +- .../src/types/chart.types.ts | 2 + .../src/types/marks/index.ts | 1 + .../react-spectrum-charts/src/utils/utils.ts | 10 +- .../vega-spec-builder/src/chartSpecBuilder.ts | 9 +- .../src/gauge/gaugeDataUtils.ts | 67 +-- .../src/gauge/gaugeMarkUtils.ts | 382 +++--------------- .../src/gauge/gaugeSpecBuilder.test.ts | 2 +- .../src/gauge/gaugeSpecBuilder.ts | 191 +++------ .../src/types/chartSpec.types.ts | 2 + .../src/types/marks/gaugeSpec.types.ts | 70 +--- yarn.lock | 18 +- 18 files changed, 224 insertions(+), 754 deletions(-) diff --git a/package.json b/package.json index 4696cd8e0..d23e09def 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "chalk": "4.1.2", "clean-webpack-plugin": "^4.0.0", "concurrently": "^8.0.0", - "cross-env": "^7.0.3", + "cross-env": "^10.1.0", "css-loader": "^7.1.2", "eslint": "^8.29.0", "eslint-config-prettier": "^9.0.0", diff --git a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx index 0c5d007b1..8174d9bd1 100644 --- a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx +++ b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx @@ -14,27 +14,18 @@ import { FC } from 'react'; import { - DEFAULT_GAUGE_DIRECTION, - DEFAULT_LABEL_POSITION, - DEFAULT_SCALE_TYPE, DEFAULT_SCALE_VALUE, } from '@spectrum-charts/constants'; import { GaugeProps } from '../../../types'; +// I assume this houses all the props for all variations of a Gauge chart? const Gauge: FC<GaugeProps> = ({ - name = 'bullet0', - metric = 'currentAmount', - dimension = 'graphLabel', - target = 'target', - direction = DEFAULT_GAUGE_DIRECTION, - numberFormat = '', - showTarget = true, - showTargetValue = false, - labelPosition = DEFAULT_LABEL_POSITION, - scaleType = DEFAULT_SCALE_TYPE, - maxScaleValue = DEFAULT_SCALE_VALUE, - thresholdBarColor = false, + name = 'gauge0', + metric = 'currentAmount', // CurrVal + target = 'target', + numberFormat = '', // ints or floats + maxArcValue = DEFAULT_SCALE_VALUE, // Max Arc Value }) => { return null; }; diff --git a/packages/react-spectrum-charts/src/alpha/components/index.ts b/packages/react-spectrum-charts/src/alpha/components/index.ts index 83326d4d2..ea2dcf0e4 100644 --- a/packages/react-spectrum-charts/src/alpha/components/index.ts +++ b/packages/react-spectrum-charts/src/alpha/components/index.ts @@ -13,4 +13,4 @@ export * from './Bullet'; export * from './Combo'; export * from './Venn'; -export * from './Gauge'; +export * from './Gauge/Gauge'; diff --git a/packages/react-spectrum-charts/src/rscToSbAdapter/childrenAdapter.ts b/packages/react-spectrum-charts/src/rscToSbAdapter/childrenAdapter.ts index 37283ebba..6a4b33744 100644 --- a/packages/react-spectrum-charts/src/rscToSbAdapter/childrenAdapter.ts +++ b/packages/react-spectrum-charts/src/rscToSbAdapter/childrenAdapter.ts @@ -18,6 +18,7 @@ import { ChartPopoverOptions, ChartTooltipOptions, DonutSummaryOptions, + GaugeOptions, LegendOptions, LineOptions, MarkOptions, @@ -30,7 +31,7 @@ import { TrendlineOptions, } from '@spectrum-charts/vega-spec-builder'; -import { Bullet, Combo, Venn } from '../alpha'; +import { Bullet, Combo, Gauge, Venn } from '../alpha'; import { Annotation } from '../components/Annotation'; import { Area } from '../components/Area'; import { Axis } from '../components/Axis'; @@ -158,6 +159,10 @@ export const childrenToOptions = ( marks.push({ ...child.props, markType: 'bullet' } as BulletOptions); break; + case Gauge.displayName: + marks.push({ ...child.props, markType: 'gauge' } as GaugeOptions); + break; + case ChartPopover.displayName: chartPopovers.push(getChartPopoverOptions(child.props as ChartPopoverProps)); break; diff --git a/packages/react-spectrum-charts/src/stories/components/Bullet/Bullet.story.tsx b/packages/react-spectrum-charts/src/stories/components/Bullet/Bullet.story.tsx index 350e9ac3f..81c4b7536 100644 --- a/packages/react-spectrum-charts/src/stories/components/Bullet/Bullet.story.tsx +++ b/packages/react-spectrum-charts/src/stories/components/Bullet/Bullet.story.tsx @@ -39,7 +39,7 @@ const BulletStory: StoryFn<BulletProps & { width?: number; height?: number }> = const { width, height, ...bulletProps } = args; const chartProps = useChartProps({ ...defaultChartProps, width: width ?? 350, height: height ?? 350 }); return ( - <Chart {...chartProps}> + <Chart {...chartProps} debug> <Bullet {...bulletProps} /> </Chart> ); diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx index 350e9ac3f..0f7348c92 100644 --- a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx @@ -14,188 +14,76 @@ import { ReactElement } from 'react'; import { StoryFn } from '@storybook/react'; import { Chart } from '../../../Chart'; -// Assuming Bullet chart is a component in the @rsc/alpha export -import { Bullet } from '../../../alpha'; +// Gauge chart component from alpha export +import { Gauge } from '../../../alpha'; import { Title } from '../../../components'; import useChartProps from '../../../hooks/useChartProps'; import { bindWithProps } from '../../../test-utils'; -import { BulletProps, ChartProps } from '../../../types'; -import { basicBulletData, basicThresholdsData, coloredThresholdsData } from './data'; +import { GaugeProps, ChartProps } from '../../../types'; +import { basicGaugeData } from './data'; export default { - title: 'RSC/Bullet (alpha)', - component: Bullet, + title: 'RSC/Gauge (alpha)', + component: Gauge, }; // Default chart properties const defaultChartProps: ChartProps = { - data: basicBulletData, - width: 350, - height: 350, + data: basicGaugeData, + width: 500, + height: 600, }; -// Basic Bullet chart story -const BulletStory: StoryFn<BulletProps & { width?: number; height?: number }> = (args): ReactElement => { - const { width, height, ...bulletProps } = args; - const chartProps = useChartProps({ ...defaultChartProps, width: width ?? 350, height: height ?? 350 }); +// Basic Gauge chart story +const GaugeStory: StoryFn<GaugeProps & { width?: number; height?: number }> = (args): ReactElement => { + const { width, height, ...gaugeProps } = args; + const chartProps = useChartProps({ ...defaultChartProps, width: width ?? 500, height: height ?? 500 }); return ( - <Chart {...chartProps}> - <Bullet {...bulletProps} /> + <Chart {...chartProps} debug> + <Gauge {...gaugeProps} /> </Chart> ); }; -// Bullet with Title -const BulletTitleStory: StoryFn<typeof Bullet> = (args): ReactElement => { +// Gauge with Title +const GaugeTitleStory: StoryFn<typeof Gauge> = (args): ReactElement => { const chartProps = useChartProps({ ...defaultChartProps, width: 400 }); return ( <Chart {...chartProps}> - <Title text={'Title Bullet'} position={'start'} orient={'top'} /> - <Bullet {...args} /> + <Title text={'Title Gauge'} position={'start'} orient={'top'} /> + <Gauge {...args} /> </Chart> ); }; -const Basic = bindWithProps(BulletStory); +// Basic Gauge chart story. All the ones below it are variations of the Gauge chart. +const Basic = bindWithProps(GaugeStory); Basic.args = { metric: 'currentAmount', - dimension: 'graphLabel', - target: 'target', - color: 'blue-900', - direction: 'column', - numberFormat: '$,.2f', - showTarget: true, - showTargetValue: false, - labelPosition: 'top', - scaleType: 'normal', - maxScaleValue: 100, - track: false, - thresholdBarColor: false, - metricAxis: false, -}; - -const Thresholds = bindWithProps(BulletStory); -Thresholds.args = { - metric: 'currentAmount', - dimension: 'graphLabel', target: 'target', color: 'blue-900', - direction: 'column', numberFormat: '$,.2f', - showTarget: true, - showTargetValue: false, - labelPosition: 'top', - scaleType: 'normal', - maxScaleValue: 100, - thresholds: basicThresholdsData, - thresholdBarColor: false, - track: false, - metricAxis: false, + maxArcValue: 100 }; -const ColoredMetric = bindWithProps(BulletStory); -ColoredMetric.args = { +const GaugeVariation2 = bindWithProps(GaugeStory); +GaugeVariation2.args = { metric: 'currentAmount', - dimension: 'graphLabel', target: 'target', - color: 'blue-900', - direction: 'column', + color: 'red-900', numberFormat: '$,.2f', - showTarget: true, - showTargetValue: false, - labelPosition: 'top', - scaleType: 'normal', - maxScaleValue: 100, - track: false, - thresholdBarColor: true, - thresholds: coloredThresholdsData, - metricAxis: false, + maxArcValue: 150 }; -const Track = bindWithProps(BulletStory); -Track.args = { +const GaugeVariation3 = bindWithProps(GaugeStory); +GaugeVariation3.args = { metric: 'currentAmount', - dimension: 'graphLabel', target: 'target', - color: 'blue-900', - direction: 'column', + color: 'fuchsia-900', numberFormat: '$,.2f', - showTarget: true, - showTargetValue: false, - labelPosition: 'top', - scaleType: 'normal', - maxScaleValue: 100, - track: true, - metricAxis: false, + maxArcValue: 90 }; -const RowMode = bindWithProps(BulletStory); -RowMode.args = { - metric: 'currentAmount', - dimension: 'graphLabel', - target: 'target', - color: 'blue-900', - direction: 'row', - numberFormat: '$,.2f', - showTarget: true, - showTargetValue: false, - labelPosition: 'top', - scaleType: 'normal', - maxScaleValue: 100, - thresholds: coloredThresholdsData, - thresholdBarColor: true, - track: false, - metricAxis: false, -}; -const WithTitle = bindWithProps(BulletTitleStory); -WithTitle.args = { - metric: 'currentAmount', - dimension: 'graphLabel', - target: 'target', - color: 'blue-900', - numberFormat: '$,.2f', - labelPosition: 'top', - scaleType: 'normal', - maxScaleValue: 100, - track: false, - direction: 'column', - metricAxis: false, -}; - -const FixedScale = bindWithProps(BulletStory); -FixedScale.args = { - metric: 'currentAmount', - dimension: 'graphLabel', - target: 'target', - color: 'blue-900', - direction: 'column', - numberFormat: '$,.2f', - showTarget: true, - showTargetValue: false, - labelPosition: 'top', - scaleType: 'fixed', - maxScaleValue: 250, - thresholds: basicThresholdsData, - track: false, - metricAxis: false, -}; - -const MetricAxis = bindWithProps(BulletStory); -MetricAxis.args = { - metric: 'currentAmount', - dimension: 'graphLabel', - target: 'target', - color: 'blue-900', - direction: 'column', - numberFormat: '$,.2f', - showTarget: true, - showTargetValue: false, - labelPosition: 'top', - scaleType: 'normal', - maxScaleValue: 250, - track: false, - metricAxis: true, -}; -export { Basic, Thresholds, ColoredMetric, Track, RowMode, WithTitle, FixedScale, MetricAxis }; +export { Basic, GaugeVariation2, GaugeVariation3 }; diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts b/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts index 549d87dce..bdd0ed37c 100644 --- a/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts @@ -10,19 +10,11 @@ * governing permissions and limitations under the License. */ -export const basicBulletData = [ - { graphLabel: 'Customers', currentAmount: 150, target: 50 }, + + +export const basicGaugeData = [ + { graphLabel: 'Customers', currentAmount: 60, target: 80 }, { graphLabel: 'Revenue', currentAmount: 350, target: 450 }, ]; -export const basicThresholdsData = [ - { thresholdMax: 120, fill: 'rgb(0, 0, 0)' }, - { thresholdMin: 120, thresholdMax: 235, fill: 'rgb(109, 109, 109)' }, - { thresholdMin: 235, fill: 'rgb(177, 177, 177)' }, -]; -export const coloredThresholdsData = [ - { thresholdMax: 120, fill: 'rgb(234, 56, 41)' }, - { thresholdMin: 120, thresholdMax: 235, fill: 'rgb(249, 137, 23)' }, - { thresholdMin: 235, fill: 'rgb(21, 164, 110)' }, -]; diff --git a/packages/react-spectrum-charts/src/types/chart.types.ts b/packages/react-spectrum-charts/src/types/chart.types.ts index 2cd21f23c..69cb33b5d 100644 --- a/packages/react-spectrum-charts/src/types/chart.types.ts +++ b/packages/react-spectrum-charts/src/types/chart.types.ts @@ -36,6 +36,7 @@ import { ComboElement, DonutElement, DonutSummaryElement, + GaugeElement, LineElement, MetricRangeElement, ScatterElement, @@ -54,6 +55,7 @@ export type ChartChildElement = | BigNumberElement | DonutElement | ComboElement + | GaugeElement | LegendElement | LineElement | ScatterElement diff --git a/packages/react-spectrum-charts/src/types/marks/index.ts b/packages/react-spectrum-charts/src/types/marks/index.ts index 05e62d712..3b71c1ac2 100644 --- a/packages/react-spectrum-charts/src/types/marks/index.ts +++ b/packages/react-spectrum-charts/src/types/marks/index.ts @@ -16,6 +16,7 @@ export * from './bigNumber.types'; export * from './bullet.types'; export * from './combo.types'; export * from './donut.types'; +export * from './gauge.types'; export * from './line.types'; export * from './scatter.types'; export * from './venn.types'; diff --git a/packages/react-spectrum-charts/src/utils/utils.ts b/packages/react-spectrum-charts/src/utils/utils.ts index 712e1084e..ebafc9843 100644 --- a/packages/react-spectrum-charts/src/utils/utils.ts +++ b/packages/react-spectrum-charts/src/utils/utils.ts @@ -17,7 +17,7 @@ import { SELECTED_GROUP, SELECTED_ITEM, SELECTED_SERIES, SERIES_ID } from '@spec import { combineNames, toCamelCase } from '@spectrum-charts/utils'; import { Datum } from '@spectrum-charts/vega-spec-builder'; -import { Bullet, Combo, Venn } from '../alpha'; +import { Bullet, Combo, Gauge, Venn } from '../alpha'; import { Annotation, Area, @@ -56,6 +56,7 @@ import { ComboElement, DonutElement, DonutSummaryElement, + GaugeElement, LegendElement, LineElement, MetricRangeElement, @@ -102,6 +103,7 @@ type ElementCounts = { scatter: number; combo: number; bullet: number; + gauge: number; venn: number; }; @@ -134,6 +136,7 @@ export const sanitizeChildren = (children: unknown): (ChartChildElement | MarkCh AxisThumbnail.displayName, Bar.displayName, Bullet.displayName, + Gauge.displayName, ChartPopover.displayName, ChartTooltip.displayName, Combo.displayName, @@ -165,6 +168,7 @@ export const sanitizeRscChartChildren = (children: unknown): ChartChildElement[] Axis.displayName, Bar.displayName, Donut.displayName, + Gauge.displayName, Legend.displayName, Line.displayName, Scatter.displayName, @@ -409,6 +413,9 @@ const getElementName = (element: unknown, elementCounts: ElementCounts) => { case Bullet.displayName: elementCounts.bullet++; return getComponentName(element as BulletElement, `bullet${elementCounts.bullet}`); + case Gauge.displayName: + elementCounts.gauge++; + return getComponentName(element as GaugeElement, `gauge${elementCounts.gauge}`); case Legend.displayName: elementCounts.legend++; return getComponentName(element as LegendElement, `legend${elementCounts.legend}`); @@ -449,6 +456,7 @@ const initElementCounts = (): ElementCounts => ({ line: -1, scatter: -1, combo: -1, + gauge: -1, venn: -1, }); diff --git a/packages/vega-spec-builder/src/chartSpecBuilder.ts b/packages/vega-spec-builder/src/chartSpecBuilder.ts index 041cda880..928287b7c 100644 --- a/packages/vega-spec-builder/src/chartSpecBuilder.ts +++ b/packages/vega-spec-builder/src/chartSpecBuilder.ts @@ -41,7 +41,7 @@ import { addArea } from './area/areaSpecBuilder'; import { addAxis } from './axis/axisSpecBuilder'; import { addBar } from './bar/barSpecBuilder'; import { addBullet } from './bullet/bulletSpecBuilder'; -// add import addGauge here +import { addGauge } from './gauge/gaugeSpecBuilder'; import { addCombo } from './combo/comboSpecBuilder'; import { getSeriesIdTransform } from './data/dataUtils'; import { addDonut } from './donut/donutSpecBuilder'; @@ -129,7 +129,8 @@ export function buildSpec({ spec.signals = getDefaultSignals(options); spec.scales = getDefaultScales(colors, colorScheme, lineTypes, lineWidths, opacities, symbolShapes, symbolSizes); - let { areaCount, barCount, bulletCount, comboCount, donutCount, lineCount, scatterCount, vennCount } = + // added gaugeCount below + let { areaCount, barCount, bulletCount, comboCount, donutCount, gaugeCount, lineCount, scatterCount, vennCount } = initializeComponentCounts(); const specOptions = { colorScheme, idKey, highlightedItem }; spec = [...marks].reduce((acc: ScSpec, mark) => { @@ -149,6 +150,9 @@ export function buildSpec({ case 'donut': donutCount++; return addDonut(acc, { ...mark, ...specOptions, index: donutCount }); + case 'gauge': + gaugeCount++; + return addGauge(acc, { ...mark, ...specOptions, index: gaugeCount }); case 'line': lineCount++; return addLine(acc, { ...mark, ...specOptions, index: lineCount }); @@ -218,6 +222,7 @@ const initializeComponentCounts = () => { comboCount: -1, donutCount: -1, bulletCount: -1, + gaugeCount: -1, lineCount: -1, scatterCount: -1, vennCount: -1, diff --git a/packages/vega-spec-builder/src/gauge/gaugeDataUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeDataUtils.ts index c331fbc7b..8bc25995f 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeDataUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeDataUtils.ts @@ -9,12 +9,11 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -import { Data, FormulaTransform, ValuesData } from 'vega'; +import { Data, ValuesData } from 'vega'; import { TABLE } from '@spectrum-charts/constants'; import { getTableData } from '../data/dataUtils'; -import { GaugeSpecOptions, ThresholdBackground } from '../types'; /** * Retrieves the gauge table data from the provided data array. @@ -28,7 +27,6 @@ export const getGaugeTableData = (data: Data[]): ValuesData => { tableData = { name: TABLE, values: [], - transform: [], }; data.push(tableData); } @@ -42,74 +40,11 @@ export const getGaugeTableData = (data: Data[]): ValuesData => { * @param gaugeOptions The gauge spec properties. * @returns An array of formula transforms. */ -export const getGaugeTransforms = (gaugeOptions: GaugeSpecOptions): FormulaTransform[] => { - const transforms: FormulaTransform[] = [ - { - type: 'formula', - expr: `isValid(datum.${gaugeOptions.target}) ? round(datum.${gaugeOptions.target} * 1.05) : 0`, - as: 'xPaddingForTarget', - }, - ]; - - if (gaugeOptions.scaleType === 'flexible') { - transforms.push({ - type: 'formula', - expr: `${gaugeOptions.maxScaleValue}`, - as: 'flexibleScaleValue', - }); - } - - if (gaugeOptions.thresholdBarColor && (gaugeOptions.thresholds?.length ?? 0) > 0) { - transforms.push({ - type: 'formula', - expr: generateThresholdColorExpr(gaugeOptions.thresholds ?? [], gaugeOptions.metric, gaugeOptions.color), - as: 'barColor', - }); - } - - return transforms; -}; /** * Generates a Vega expression for the color of the gauge chart based on the provided thresholds. * The expression checks the value of the metric field against the thresholds and assigns the appropriate color. - * @param thresholds An array of threshold objects. - * @param metricField The name of the metric field in the data. * @param defaultColor The default color to use if no thresholds are met. * @returns A string representing the Vega expression for the color. */ -export function generateThresholdColorExpr( - thresholds: ThresholdBackground[], - metricField: string, - defaultColor: string -): string { - if (!thresholds || thresholds.length === 0) return `'${defaultColor}'`; - - const sorted: ThresholdBackground[] = thresholds.slice().sort((a, b) => { - const aMin = a.thresholdMin !== undefined ? a.thresholdMin : -1e12; - const bMin = b.thresholdMin !== undefined ? b.thresholdMin : -1e12; - return aMin - bMin; - }); - - const exprParts: string[] = []; - - // For values below the first threshold's lower bound, use the default color. - exprParts.push( - `(datum.${metricField} < ${ - sorted[0].thresholdMin !== undefined ? sorted[0].thresholdMin : -1e12 - }) ? '${defaultColor}' : ` - ); - - // For each threshold, check if the metric field is within the range defined by the thresholdMin and thresholdMax values. - // If it is, use the corresponding fill color. - for (let i = 0; i < sorted.length - 1; i++) { - const nextLower = sorted[i + 1].thresholdMin !== undefined ? sorted[i + 1].thresholdMin : -1e12; - exprParts.push(`(datum.${metricField} < ${nextLower}) ? '${sorted[i].fill}' : `); - } - - // For values above the last threshold's upper bound, use the last threshold's fill color. - exprParts.push(`'${sorted[sorted.length - 1].fill}'`); - const expr = exprParts.join(''); - return expr; -} diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 4583c1192..41354a835 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -10,351 +10,87 @@ * governing permissions and limitations under the License. */ import { produce } from 'immer'; -import { Axis, GroupMark, Mark } from 'vega'; +import { Mark } from 'vega'; -import { getColorValue } from '@spectrum-charts/themes'; +import { GaugeSpecOptions } from '../types'; -import { BulletSpecOptions } from '../types'; +export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => { + const { + name, + backgroundFill = '#eee', + backgroundStroke = '#999', + fillerColorSignal = 'fillerColorToCurrVal', + } = opt; -export const addMarks = produce<Mark[], [BulletSpecOptions]>((marks, bulletOptions) => { - const markGroupEncodeUpdateDirection = bulletOptions.direction === 'column' ? 'y' : 'x'; - const bulletGroupWidth = bulletOptions.direction === 'column' ? 'width' : 'bulletGroupWidth'; + // Background arc + marks.push(getBackgroundArc(name, backgroundFill, backgroundStroke)); - const bulletMark: GroupMark = { - name: 'bulletGroup', - type: 'group', - from: { - facet: { data: 'table', name: 'bulletGroups', groupby: `${bulletOptions.dimension}` }, - }, - encode: { - update: { - [markGroupEncodeUpdateDirection]: { scale: 'groupScale', field: `${bulletOptions.dimension}` }, - height: { signal: 'bulletGroupHeight' }, - width: { signal: bulletGroupWidth }, - }, - }, - marks: [], - }; - - const thresholds = bulletOptions.thresholds; + // Filler arc (fills to clampedValue) + marks.push(getFillerArc(name, fillerColorSignal)); - if (Array.isArray(thresholds) && thresholds.length > 0) { - bulletMark.data = [ - { - name: 'thresholds', - values: thresholds, - transform: [{ type: 'identifier', as: 'id' }], - }, - ]; - bulletMark.marks?.push(getBulletMarkThreshold(bulletOptions)); - } else if (bulletOptions.track) { - bulletMark.marks?.push(getBulletTrack(bulletOptions)); - } - - bulletMark.marks?.push(getBulletMarkRect(bulletOptions)); - if (bulletOptions.target && bulletOptions.showTarget !== false) { - bulletMark.marks?.push(getBulletMarkTarget(bulletOptions)); - if (bulletOptions.showTargetValue) { - bulletMark.marks?.push(getBulletMarkTargetValueLabel(bulletOptions)); - } - } - - if (bulletOptions.labelPosition === 'top' || bulletOptions.direction === 'row') { - bulletMark.marks?.push(getBulletMarkLabel(bulletOptions)); - bulletMark.marks?.push(getBulletMarkValueLabel(bulletOptions)); - } - - marks.push(bulletMark); + // Needle to clampedValue + marks.push(getNeedle(name)); }); -export function getBulletMarkRect(bulletOptions: BulletSpecOptions): Mark { - //The vertical positioning is calculated starting at the bulletgroupheight - //and then subtracting two times the bullet height to center the bullet bar - //in the middle of the threshold. The 3 is subtracted because the bulletgroup height - //starts the bullet below the threshold area. - //Additionally, the value of the targetValueLabelHeight is subtracted if the target value label is shown - //to make sure that the bullet bar is not drawn over the target value label. - const bulletMarkRectEncodeUpdateYSignal = - bulletOptions.showTarget && bulletOptions.showTargetValue - ? 'bulletGroupHeight - targetValueLabelHeight - 3 - 2 * bulletHeight' - : 'bulletGroupHeight - 3 - 2 * bulletHeight'; - - const fillColor = - bulletOptions.thresholdBarColor && (bulletOptions.thresholds?.length ?? 0) > 0 - ? [{ field: 'barColor' }] - : [{ value: bulletOptions.color }]; - - const bulletMarkRect: Mark = { - name: `${bulletOptions.name}Rect`, - description: `${bulletOptions.name}Rect`, - type: 'rect', - from: { data: 'bulletGroups' }, - encode: { - enter: { - cornerRadiusTopLeft: [{ test: `datum.${bulletOptions.metric} < 0`, value: 3 }], - cornerRadiusBottomLeft: [{ test: `datum.${bulletOptions.metric} < 0`, value: 3 }], - cornerRadiusTopRight: [{ test: `datum.${bulletOptions.metric} > 0`, value: 3 }], - cornerRadiusBottomRight: [{ test: `datum.${bulletOptions.metric} > 0`, value: 3 }], - fill: fillColor, - }, - update: { - x: { scale: 'xscale', value: 0 }, - x2: { scale: 'xscale', field: `${bulletOptions.metric}` }, - height: { signal: 'bulletHeight' }, - y: { signal: bulletMarkRectEncodeUpdateYSignal }, - }, - }, - }; - - return bulletMarkRect; -} - -export function getBulletMarkTarget(bulletOptions: BulletSpecOptions): Mark { - const solidColor = getColorValue('gray-900', bulletOptions.colorScheme); - - //When the target value label is shown, we must subtract the height of the target value label - //to make sure that the target line is not drawn over the target value label - const bulletMarkTargetEncodeUpdateY = - bulletOptions.showTarget && bulletOptions.showTargetValue - ? 'bulletGroupHeight - targetValueLabelHeight - targetHeight' - : 'bulletGroupHeight - targetHeight'; - const bulletMarkTargetEncodeUpdateY2 = - bulletOptions.showTarget && bulletOptions.showTargetValue - ? 'bulletGroupHeight - targetValueLabelHeight' - : 'bulletGroupHeight'; - - const bulletMarkTarget: Mark = { - name: `${bulletOptions.name}Target`, - description: `${bulletOptions.name}Target`, - type: 'rule', - from: { data: 'bulletGroups' }, - encode: { - enter: { - stroke: { value: `${solidColor}` }, - strokeWidth: { value: 2 }, - }, - update: { - x: { scale: 'xscale', field: `${bulletOptions.target}` }, - y: { signal: bulletMarkTargetEncodeUpdateY }, - y2: { signal: bulletMarkTargetEncodeUpdateY2 }, - }, - }, - }; - - return bulletMarkTarget; -} - -export function getBulletMarkLabel(bulletOptions: BulletSpecOptions): Mark { - const barLabelColor = getColorValue('gray-600', bulletOptions.colorScheme); - - const bulletMarkLabel: Mark = { - name: `${bulletOptions.name}Label`, - description: `${bulletOptions.name}Label`, - type: 'text', - from: { data: 'bulletGroups' }, - encode: { - enter: { - text: { signal: `datum.${bulletOptions.dimension}` }, - align: { value: 'left' }, - baseline: { value: 'top' }, - fill: { value: `${barLabelColor}` }, - }, - update: { x: { value: 0 }, y: { value: 0 } }, - }, - }; - - return bulletMarkLabel; -} - -export function getBulletMarkValueLabel(bulletOptions: BulletSpecOptions): Mark { - const defaultColor = getColorValue(bulletOptions.color, bulletOptions.colorScheme); - const solidColor = getColorValue('gray-900', bulletOptions.colorScheme); - const encodeUpdateSignalWidth = bulletOptions.direction === 'column' ? 'width' : 'bulletGroupWidth'; - const fillExpr = - bulletOptions.thresholdBarColor && (bulletOptions.thresholds?.length ?? 0) > 0 - ? `datum.barColor === '${defaultColor}' ? '${solidColor}' : datum.barColor` - : `'${solidColor}'`; - - const bulletMarkValueLabel: Mark = { - name: `${bulletOptions.name}ValueLabel`, - description: `${bulletOptions.name}ValueLabel`, - type: 'text', - from: { data: 'bulletGroups' }, - encode: { - enter: { - text: { - signal: `datum.${bulletOptions.metric} != null ? format(datum.${bulletOptions.metric}, '${ - bulletOptions.numberFormat || '' - }') : ''`, - }, - align: { value: 'right' }, - baseline: { value: 'top' }, - fill: { signal: fillExpr }, - }, - update: { x: { signal: encodeUpdateSignalWidth }, y: { value: 0 } }, - }, - }; - - return bulletMarkValueLabel; -} - -export function getBulletMarkTargetValueLabel(bulletOptions: BulletSpecOptions): Mark { - const solidColor = getColorValue('gray-900', bulletOptions.colorScheme); - - const bulletMarkTargetValueLabel: Mark = { - name: `${bulletOptions.name}TargetValueLabel`, - description: `${bulletOptions.name}TargetValueLabel`, - type: 'text', - from: { data: 'bulletGroups' }, +function getBackgroundArc(name: string, fill: string, stroke: string): Mark { + return { + name: `${name}BackgroundArcRounded`, + description: 'Background Arc (Round Edge)', + type: 'arc', encode: { enter: { - text: { - signal: `datum.${bulletOptions.target} != null ? 'Target: ' + format(datum.${bulletOptions.target}, '$,.2f') : 'No Target'`, - }, - align: { value: 'center' }, - baseline: { value: 'top' }, - fill: { value: `${solidColor}` }, - }, - update: { - x: { scale: 'xscale', field: `${bulletOptions.target}` }, - y: { signal: 'bulletGroupHeight - targetValueLabelHeight + 6' }, - }, - }, + x: { signal: 'centerX' }, + y: { signal: 'centerY' }, + innerRadius: { signal: 'innerRadius' }, + outerRadius: { signal: 'outerRadius' }, + startAngle: { signal: 'startAngle' }, + endAngle: { signal: 'endAngle' }, + fill: { value: fill }, + stroke: { value: stroke } + } + } }; - - return bulletMarkTargetValueLabel; } -export function getBulletMarkThreshold(bulletOptions: BulletSpecOptions): Mark { - // Vertically center the threshold bar by offsetting from bulletGroupHeight. - // Subtract 3 for alignment and targetValueLabelHeight if the label is shown. - const baseHeightSignal = 'bulletGroupHeight - 3 - bulletThresholdHeight'; - const encodeUpdateYSignal = - bulletOptions.showTarget && bulletOptions.showTargetValue - ? `${baseHeightSignal} - targetValueLabelHeight` - : baseHeightSignal; - - const bulletMarkThreshold: Mark = { - name: `${bulletOptions.name}Threshold`, - description: `${bulletOptions.name}Threshold`, - type: 'rect', - from: { data: 'thresholds' }, - clip: true, +function getFillerArc(name: string, fillerColorSignal: string): Mark { + return { + name: `${name}FillerArc`, + description: 'Filler Arc', + type: 'arc', encode: { enter: { - cornerRadiusTopLeft: [{ test: `!isDefined(datum.thresholdMin) && domain('xscale')[0] !== 0`, value: 3 }], - cornerRadiusBottomLeft: [{ test: `!isDefined(datum.thresholdMin) && domain('xscale')[0] !== 0`, value: 3 }], - cornerRadiusTopRight: [{ test: `!isDefined(datum.thresholdMax) && domain('xscale')[1] !== 0`, value: 3 }], - cornerRadiusBottomRight: [{ test: `!isDefined(datum.thresholdMax) && domain('xscale')[1] !== 0`, value: 3 }], - fill: { field: 'fill' }, - fillOpacity: { value: 0.2 }, + x: { signal: 'centerX' }, + y: { signal: 'centerY' }, + innerRadius: { signal: 'innerRadius' }, + outerRadius: { signal: 'outerRadius' }, + startAngle: { signal: 'startAngle' }, + endAngle: { signal: 'endAngle' }, + fill: { signal: fillerColorSignal } }, update: { - x: { - signal: "isDefined(datum.thresholdMin) ? scale('xscale', datum.thresholdMin) : 0", - }, - x2: { - signal: "isDefined(datum.thresholdMax) ? scale('xscale', datum.thresholdMax) : width", - }, - height: { signal: 'bulletThresholdHeight' }, - y: { signal: encodeUpdateYSignal }, - }, - }, + endAngle: { signal: "scale('angleScale', clampedVal)" } + } + } }; - return bulletMarkThreshold; } -export function getBulletTrack(bulletOptions: BulletSpecOptions): Mark { - const trackColor = getColorValue('gray-200', bulletOptions.colorScheme); - const trackWidth = bulletOptions.direction === 'column' ? 'width' : 'bulletGroupWidth'; - // Subtracting 20 accounts for the space used by the target value label - const trackY = - bulletOptions.showTarget && bulletOptions.showTargetValue - ? 'bulletGroupHeight - 3 - 2 * bulletHeight - 20' - : 'bulletGroupHeight - 3 - 2 * bulletHeight'; - - const bulletTrack: Mark = { - name: `${bulletOptions.name}Track`, - description: `${bulletOptions.name}Track`, - type: 'rect', - from: { data: 'bulletGroups' }, + function getNeedle(name: string): Mark { + return { + name: `${name}Needle`, + description: 'Needle (rule)', + type: 'rule', encode: { enter: { - fill: { value: trackColor }, - cornerRadiusTopRight: [{ test: "domain('xscale')[1] !== 0", value: 3 }], - cornerRadiusBottomRight: [{ test: "domain('xscale')[1] !== 0", value: 3 }], - cornerRadiusTopLeft: [{ test: "domain('xscale')[0] !== 0", value: 3 }], - cornerRadiusBottomLeft: [{ test: "domain('xscale')[0] !== 0", value: 3 }], + stroke: { value: '#333' }, + strokeWidth: { value: 3 }, + strokeCap: { value: 'round' } }, update: { - x: { value: 0 }, - width: { signal: trackWidth }, - height: { signal: 'bulletHeight' }, - y: { signal: trackY }, - }, - }, - }; - - return bulletTrack; -} - -export function getBulletLabelAxesLeft(labelOffset): Axis { - return { - scale: 'groupScale', - orient: 'left', - tickSize: 0, - labelOffset: labelOffset, - labelPadding: 10, - labelColor: '#797979', - domain: false, - }; -} - -export function getBulletLabelAxesRight(bulletOptions: BulletSpecOptions, labelOffset): Axis { - return { - scale: 'groupScale', - orient: 'right', - tickSize: 0, - labelOffset: labelOffset, - labelPadding: 10, - domain: false, - encode: { - labels: { - update: { - text: { - signal: `info(data('table')[datum.index * (length(data('table')) - 1)].${ - bulletOptions.metric - }) != null ? format(info(data('table')[datum.index * (length(data('table')) - 1)].${ - bulletOptions.metric - }), '${bulletOptions.numberFormat || ''}') : ''`, - }, - }, - }, - }, - }; -} - -export function getBulletScaleAxes(): Axis { - return { - labelOffset: 2, - scale: 'xscale', - orient: 'bottom', - ticks: false, - labelColor: 'gray', - domain: false, - tickCount: 5, - offset: { signal: 'axisOffset' }, + x: { signal: 'centerX' }, + y: { signal: 'centerY' }, + x2: { signal: 'needleTipX' }, + y2: { signal: 'needleTipY' } + } + } }; } - -export const addAxes = produce<Axis[], [BulletSpecOptions]>((axes, bulletOptions) => { - if (bulletOptions.metricAxis && bulletOptions.direction === 'column' && !bulletOptions.showTargetValue) { - axes.push(getBulletScaleAxes()); - } - - if (bulletOptions.labelPosition === 'side' && bulletOptions.direction === 'column') { - const labelOffset = bulletOptions.showTargetValue && bulletOptions.showTarget ? -8 : 2; - axes.push(getBulletLabelAxesLeft(labelOffset)); - axes.push(getBulletLabelAxesRight(bulletOptions, labelOffset)); - } -}); diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts index 7e71c84f4..638833665 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.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 { BulletOptions, ScSpec } from '../types'; +import { GaugeOptions, ScSpec } from '../types'; import { addBullet, addData, addScales, addSignals } from './bulletSpecBuilder'; import { sampleOptionsColumn, sampleOptionsRow } from './bulletTestUtils'; diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index 3534a5d8c..2f43ba76e 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -10,24 +10,26 @@ * governing permissions and limitations under the License. */ import { produce } from 'immer'; -import { Data, Scale, Signal } from 'vega'; +import { Mark, Signal, Scale, Data } from 'vega'; +import { addGaugeMarks } from './gaugeMarkUtils'; -import { - DEFAULT_GAUGE_DIRECTION, - DEFAULT_COLOR_SCHEME, - DEFAULT_LABEL_POSITION, - DEFAULT_SCALE_TYPE, - DEFAULT_SCALE_VALUE, -} from '@spectrum-charts/constants'; -import { getColorValue, spectrumColors } from '@spectrum-charts/themes'; + +import { DEFAULT_COLOR_SCHEME } from '@spectrum-charts/constants'; + +// import { getColorValue, spectrumColors } from '@spectrum-charts/themes'; +import { getColorValue, spectrumColors } from '@spectrum-charts/themes'; import { toCamelCase } from '@spectrum-charts/utils'; -import { GaugeOptions, GaugeSpecOptions, ColorScheme, ScSpec } from '../types'; -import { getGaugeTableData, getGaugeTransforms } from './gaugeDataUtils'; -import { addAxes, addMarks } from './gaugeMarkUtils'; +import { ColorScheme, GaugeOptions, GaugeSpecOptions, ScSpec } from '../types'; +import { getGaugeTableData } from './gaugeDataUtils'; + +const DEFAULT_COLOR = spectrumColors.light['blue-900']; -const DEFAULT_COLOR = spectrumColors.light['static-blue']; +/** + * Adds a simple Gauge chart to the spec + * + */ export const addGauge = produce< ScSpec, [GaugeOptions & { colorScheme?: ColorScheme; index?: number; idKey: string }] @@ -38,144 +40,69 @@ export const addGauge = produce< colorScheme = DEFAULT_COLOR_SCHEME, index = 0, name, - metric, - dimension, - target, color = DEFAULT_COLOR, - direction = DEFAULT_GAUGE_DIRECTION, - numberFormat, - showTarget = true, - showTargetValue = false, - labelPosition = DEFAULT_LABEL_POSITION, - scaleType = DEFAULT_SCALE_TYPE, - maxScaleValue = DEFAULT_SCALE_VALUE, - thresholds = [], - track = false, - thresholdBarColor = false, - metricAxis = false, ...options } ) => { const gaugeOptions: GaugeSpecOptions = { + backgroundFill: spectrumColors[colorScheme]['gray-200'], + backgroundStroke: spectrumColors[colorScheme]['gray-300'], + color: getColorValue(color, colorScheme), colorScheme: colorScheme, + fillerColorSignal: 'fillerColorToCurrVal', index, - color: getColorValue(color, colorScheme), - metric: metric ?? 'currentAmount', - dimension: dimension ?? 'graphLabel', - target: target ?? 'target', + maxArcValue: 100, + minArcValue: 0, + metric: 'currentAmount', name: toCamelCase(name ?? `gauge${index}`), - direction: direction, - numberFormat: numberFormat ?? '', - showTarget: showTarget, - showTargetValue: showTargetValue, - labelPosition: labelPosition, - scaleType: scaleType, - maxScaleValue: maxScaleValue, - track: track, - thresholds: thresholds, - thresholdBarColor: thresholdBarColor, - metricAxis: metricAxis, + numberFormat: '', + target: 'target', ...options, }; - spec.data = addData(spec.data ?? [], gaugeOptions); - spec.marks = addMarks(spec.marks ?? [], gaugeOptions); - spec.scales = addScales(spec.scales ?? [], gaugeOptions); - spec.signals = addSignals(spec.signals ?? [], gaugeOptions); - spec.axes = addAxes(spec.axes ?? [], gaugeOptions); - } -); -export const addScales = produce<Scale[], [GaugeSpecOptions]>((scales, options) => { - const groupScaleRangeSignal = options.direction === 'column' ? 'gaugeChartHeight' : 'width'; - const xRange = options.direction === 'column' ? 'width' : [0, { signal: 'gaugeGroupWidth' }]; - let domainFields; + spec.signals = addSignals(spec.signals ?? [], gaugeOptions); + spec.scales = addScales(spec.scales ?? [], gaugeOptions); + spec.marks = addGaugeMarks(spec.marks ?? [], gaugeOptions); + spec.data = addData(spec.data ?? [], gaugeOptions); - if (options.scaleType === 'flexible' && options.maxScaleValue > 0) { - domainFields = { data: 'table', fields: ['xPaddingForTarget', options.metric, 'flexibleScaleValue'] }; - } else if (options.scaleType === 'fixed' && options.maxScaleValue > 0) { - domainFields = [0, `${options.maxScaleValue}`]; - } else { - domainFields = { data: 'table', fields: ['xPaddingForTarget', options.metric] }; + } - - scales.push( - { - name: 'groupScale', - type: 'band', - domain: { data: 'table', field: options.dimension }, - range: [0, { signal: groupScaleRangeSignal }], - paddingInner: { signal: 'paddingRatio' }, - }, - { - name: 'xscale', - type: 'linear', - domain: domainFields, - range: xRange, - round: true, - clamp: true, - zero: true, - } - ); -}); +); export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, options) => { - signals.push({ name: 'gap', value: 12 }); - signals.push({ name: 'gaugeHeight', value: 8 }); - signals.push({ name: 'gaugeThresholdHeight', update: 'gaugeHeight * 3' }); - signals.push({ name: 'targetHeight', update: 'gaugeThresholdHeight + 6' }); - - if (options.showTargetValue && options.showTarget) { - signals.push({ name: 'targetValueLabelHeight', update: '20' }); - } + signals.push({ name: 'arcMaxVal', value: options.maxArcValue }); + signals.push({ name: 'arcMinVal', value: options.minArcValue }); + signals.push({ name: 'backgroundfillColor', value: `${options.backgroundFill}`}); + signals.push({ name: 'centerX', update: "width/2"}) + signals.push({ name: 'centerY', update: "height/2 + outerRadius/2"}) + signals.push({ name: 'clampedVal', update: "min(max(arcMinVal, currVal), arcMaxVal)"}) + signals.push({ name: 'currVal', update: `data('table')[0].${options.metric}` }); + signals.push({ name: 'endAngle', update: "PI * 2 / 3" }); // 120 degrees + signals.push({ name: 'fillerColorToCurrVal', value: `${options.color}`}) + signals.push({ name: 'innerRadius', update: "outerRadius - (radiusRef * 0.25)"}) + signals.push({ name: 'needleAngle', update: "needleAngleOriginal - PI/2"}) + signals.push({ name: 'needleAngleOriginal', update: "scale('angleScale', clampedVal)"}) + signals.push({ name: 'needleLength', update: "innerRadius"}) + signals.push({ name: 'needleTipX', update: "centerX + needleLength * cos(needleAngle)"}) + signals.push({ name: 'needleTipY', update: "centerY + needleLength * sin(needleAngle)"}) + signals.push({ name: 'outerRadius', update: "radiusRef * 0.95"}) + signals.push({ name: 'radiusRef', update: "min(width/2, height/2)"}) + signals.push({ name: 'startAngle', update: "-PI * 2 / 3" }); // -120 degrees + signals.push({ name: 'target', update: `data('table')[0].${options.target}` }); + signals.push({ name: 'theta', update: "scale('angleScale', clampedVal)"}) +}); - signals.push({ - name: 'gaugeGroupHeight', - update: getGaugeGroupHeightExpression(options), +export const addScales = produce<Scale[], [GaugeSpecOptions]>((scales, options) => { + scales.push({ + name: 'angleScale', + type: 'linear', + domain: [{ signal: 'arcMinVal' }, { signal: 'arcMaxVal' }], + range: [{ signal: 'startAngle' }, { signal: 'endAngle' }], + clamp: true }); - - if (options.direction === 'column') { - signals.push({ name: 'paddingRatio', update: 'gap / (gap + gaugeGroupHeight)' }); - - if (options.metricAxis && !options.showTargetValue) { - signals.push({ - name: 'gaugeChartHeight', - update: "length(data('table')) * gaugeGroupHeight + (length(data('table')) - 1) * gap + 10", - }); - signals.push({ - name: 'axisOffset', - update: 'gaugeChartHeight - height - 10', - }); - } else { - signals.push({ - name: 'gaugeChartHeight', - update: "length(data('table')) * gaugeGroupHeight + (length(data('table')) - 1) * gap", - }); - } - } else { - signals.push({ name: 'gaugeGroupWidth', update: "(width / length(data('table'))) - gap" }); - signals.push({ name: 'paddingRatio', update: 'gap / (gap + gaugeGroupWidth)' }); - signals.push({ name: 'gaugeChartHeight', update: 'gaugeGroupHeight' }); - } }); -/** - * Returns the height of the bullet group based on the options - * @param options the bullet spec options - * @returns the height of the bullet group - */ -function getGaugeGroupHeightExpression(options: GaugeSpecOptions): string { - if (options.showTargetValue && options.showTarget) { - return options.labelPosition === 'side' && options.direction === 'column' - ? 'gaugeThresholdHeight + targetValueLabelHeight + 10' - : 'gaugeThresholdHeight + targetValueLabelHeight + 24'; - } else if (options.labelPosition === 'side' && options.direction === 'column') { - return 'gaugeThresholdHeight + 10'; - } - return 'gaugeThresholdHeight + 24'; -} - export const addData = produce<Data[], [GaugeSpecOptions]>((data, options) => { const tableData = getGaugeTableData(data); - tableData.transform = getGaugeTransforms(options); -}); +}); \ No newline at end of file diff --git a/packages/vega-spec-builder/src/types/chartSpec.types.ts b/packages/vega-spec-builder/src/types/chartSpec.types.ts index 70f415a82..408065b52 100644 --- a/packages/vega-spec-builder/src/types/chartSpec.types.ts +++ b/packages/vega-spec-builder/src/types/chartSpec.types.ts @@ -20,6 +20,7 @@ import { BulletOptions, ComboOptions, DonutOptions, + GaugeOptions, LineOptions, ScatterOptions, VennOptions, @@ -58,6 +59,7 @@ export type MarkOptions = | BulletOptions | ComboOptions | DonutOptions + | GaugeOptions | LineOptions | ScatterOptions | VennOptions; diff --git a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts index 7ba04d4b6..377b9ae09 100644 --- a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts +++ b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts @@ -19,18 +19,12 @@ export interface GaugeOptions { /** Key in the data that is used as the color facet */ color?: string; - /** Data field that the metric is trended against (x-axis for horizontal orientation) */ - dimension?: string; - /** Specifies the direction the bars should be ordered (row/column) */ - direction?: 'row' | 'column'; - /** Specifies if the labels should be in top of the bullet chart or to the side. Side labels are not supported in row mode. */ - labelPosition?: 'side' | 'top'; + /** Minimum value for the scale. This value must be greater than zero. */ + minArcValue?: number; /** Maximum value for the scale. This value must be greater than zero. */ - maxScaleValue?: number; + maxArcValue?: number; /** Key in the data that is used as the metric */ - metric?: string; - /** Adds an axis that follows the max target in basic mode */ - metricAxis?: boolean; + metric?: string | number; /** Sets the name of the component. */ name?: string; /** d3 number format specifier. @@ -39,54 +33,32 @@ export interface GaugeOptions { * see {@link https://d3js.org/d3-format#locale_format} */ numberFormat?: NumberFormat; - /** Specifies if the scale should be normal, fixed, or flexible. - * - * In normal mode the maximum scale value will be calculated using the maximum value of the metric and target data fields. - * - * In fixed mode the maximum scale value will be set as the maxScaleValue prop. - * - * In flexible mode the maximum scale value will be calculated using the maximum value of either the maxScaleValue prop or maximum value of the metric and target data fields. - * This means that the scale max will be set to be the maxScaleValue prop until the data values overtake it. - */ - scaleType?: 'normal' | 'fixed' | 'flexible'; /** Flag to control whether the target is shown */ - showTarget?: boolean; - /** Flag to control whether the target value is shown. */ - showTargetValue?: boolean; - /** Target line */ target?: string; - /** changes color based on threshold */ - /** If true, the metric bar will be colored according to the thresholds. */ - thresholdBarColor?: boolean; - /** Array of threshold definitions to be rendered as background bands on the bullet chart. - * - * Each threshold object supports: - * `thresholdMin` (optional): The lower bound of the threshold. If undefined, the threshold starts from the beginning of the x-scale. - * - * `thresholdMax` (optional): The upper bound of the threshold. If undefined, the threshold extends to the end of the x-scale. - * - * `fill` : The fill color to use for the threshold background. - */ - thresholds?: ThresholdBackground[]; - /** Color regions that sit behind the bullet bar */ - track?: boolean; + /** Color regions that fill the gauge bar to the metric value */ + //track?: boolean; + /** Color of the background fill */ + backgroundFill?: string; + /** Color of the background stroke */ + backgroundStroke?: string; + /** Color of the filler color signal */ + fillerColorSignal?: string; + /** Color of the label text */ + labelColor?: string; + /** Size of the label text */ + labelSize?: number; } type GaugeOptionsWithDefaults = | 'name' | 'metric' - | 'dimension' | 'target' | 'color' - | 'direction' - | 'showTarget' - | 'showTargetValue' - | 'labelPosition' - | 'scaleType' - | 'maxScaleValue' - | 'track' - | 'metricAxis' - | 'thresholdBarColor'; + | 'minArcValue' + | 'maxArcValue' + | 'backgroundFill' + | 'backgroundStroke' + | 'fillerColorSignal'; export interface GaugeSpecOptions extends PartiallyRequired<GaugeOptions, GaugeOptionsWithDefaults> { colorScheme: ColorScheme; diff --git a/yarn.lock b/yarn.lock index 5664a7126..283be820e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3078,6 +3078,11 @@ utility-types "^3.10.0" webpack "^5.88.1" +"@epic-web/invariant@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@epic-web/invariant/-/invariant-1.0.0.tgz#1073e5dee6dd540410784990eb73e4acd25c9813" + integrity sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA== + "@es-joy/jsdoccomment@~0.49.0": version "0.49.0" resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.49.0.tgz#e5ec1eda837c802eca67d3b29e577197f14ba1db" @@ -8815,14 +8820,15 @@ create-jest@^29.7.0: jest-util "^29.7.0" prompts "^2.0.1" -cross-env@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" - integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== +cross-env@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-10.1.0.tgz#cfd2a6200df9ed75bfb9cb3d7ce609c13ea21783" + integrity sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw== dependencies: - cross-spawn "^7.0.1" + "@epic-web/invariant" "^1.0.0" + cross-spawn "^7.0.6" -cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== From b1efcae06fa05e45f3a9b79b66ff40b0656696aa Mon Sep 17 00:00:00 2001 From: Brady Shimanek <bradyshimanek33@gmail.com> Date: Tue, 11 Nov 2025 22:48:15 -0700 Subject: [PATCH 28/66] Modified Basic Gauge Props --- packages/constants/constants.ts | 2 ++ .../src/alpha/components/Gauge/Gauge.tsx | 8 ++--- .../stories/components/Gauge/Gauge.story.tsx | 9 ----- .../src/stories/components/Gauge/data.ts | 1 - .../src/gauge/gaugeMarkUtils.ts | 10 ++++-- .../src/gauge/gaugeSpecBuilder.ts | 8 +---- .../src/types/marks/gaugeSpec.types.ts | 35 +++++-------------- 7 files changed, 23 insertions(+), 50 deletions(-) diff --git a/packages/constants/constants.ts b/packages/constants/constants.ts index 89629936e..c1b2c7e26 100644 --- a/packages/constants/constants.ts +++ b/packages/constants/constants.ts @@ -36,6 +36,8 @@ export const DEFAULT_LINE_WIDTHS = ['M']; export const DEFAULT_LINEAR_DIMENSION = 'x'; export const DEFAULT_LOCALE = 'en-US'; export const DEFAULT_METRIC = 'value'; +export const DEFAULT_MAX_ARC_VALUE = 100; +export const DEFAULT_MIN_ARC_VALUE = 0; export const DEFAULT_SCALE_TYPE = 'normal'; export const DEFAULT_SCALE_VALUE = 100; export const DEFAULT_SECONDARY_COLOR = 'subSeries'; diff --git a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx index 8174d9bd1..f7e541287 100644 --- a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx +++ b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx @@ -14,7 +14,8 @@ import { FC } from 'react'; import { - DEFAULT_SCALE_VALUE, + DEFAULT_MAX_ARC_VALUE, + DEFAULT_MIN_ARC_VALUE, } from '@spectrum-charts/constants'; import { GaugeProps } from '../../../types'; @@ -23,9 +24,8 @@ import { GaugeProps } from '../../../types'; const Gauge: FC<GaugeProps> = ({ name = 'gauge0', metric = 'currentAmount', // CurrVal - target = 'target', - numberFormat = '', // ints or floats - maxArcValue = DEFAULT_SCALE_VALUE, // Max Arc Value + minArcValue = DEFAULT_MIN_ARC_VALUE, // Min Arc Value + maxArcValue = DEFAULT_MAX_ARC_VALUE, // Max Arc Value }) => { return null; }; diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx index 0f7348c92..5e2d200b1 100644 --- a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx @@ -60,28 +60,19 @@ const GaugeTitleStory: StoryFn<typeof Gauge> = (args): ReactElement => { const Basic = bindWithProps(GaugeStory); Basic.args = { metric: 'currentAmount', - target: 'target', color: 'blue-900', - numberFormat: '$,.2f', - maxArcValue: 100 }; const GaugeVariation2 = bindWithProps(GaugeStory); GaugeVariation2.args = { metric: 'currentAmount', - target: 'target', color: 'red-900', - numberFormat: '$,.2f', - maxArcValue: 150 }; const GaugeVariation3 = bindWithProps(GaugeStory); GaugeVariation3.args = { metric: 'currentAmount', - target: 'target', color: 'fuchsia-900', - numberFormat: '$,.2f', - maxArcValue: 90 }; diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts b/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts index bdd0ed37c..8f527fbd3 100644 --- a/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts @@ -14,7 +14,6 @@ export const basicGaugeData = [ { graphLabel: 'Customers', currentAmount: 60, target: 80 }, - { graphLabel: 'Revenue', currentAmount: 350, target: 450 }, ]; diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 41354a835..2a66227d0 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -12,15 +12,19 @@ import { produce } from 'immer'; import { Mark } from 'vega'; +import { DEFAULT_COLOR_SCHEME } from '@spectrum-charts/constants'; + import { GaugeSpecOptions } from '../types'; +import { spectrumColors } from '@spectrum-charts/themes'; export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => { const { name, - backgroundFill = '#eee', - backgroundStroke = '#999', - fillerColorSignal = 'fillerColorToCurrVal', + colorScheme = DEFAULT_COLOR_SCHEME, } = opt; + const backgroundFill = spectrumColors[colorScheme]['gray-200']; + const backgroundStroke = spectrumColors[colorScheme]['gray-300']; + const fillerColorSignal = 'fillerColorToCurrVal'; // Background arc marks.push(getBackgroundArc(name, backgroundFill, backgroundStroke)); diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index 2f43ba76e..2c6206a7a 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -39,7 +39,6 @@ export const addGauge = produce< { colorScheme = DEFAULT_COLOR_SCHEME, index = 0, - name, color = DEFAULT_COLOR, ...options } @@ -48,24 +47,20 @@ export const addGauge = produce< backgroundFill: spectrumColors[colorScheme]['gray-200'], backgroundStroke: spectrumColors[colorScheme]['gray-300'], color: getColorValue(color, colorScheme), - colorScheme: colorScheme, fillerColorSignal: 'fillerColorToCurrVal', + colorScheme: colorScheme, index, maxArcValue: 100, minArcValue: 0, metric: 'currentAmount', name: toCamelCase(name ?? `gauge${index}`), - numberFormat: '', - target: 'target', ...options, }; - spec.signals = addSignals(spec.signals ?? [], gaugeOptions); spec.scales = addScales(spec.scales ?? [], gaugeOptions); spec.marks = addGaugeMarks(spec.marks ?? [], gaugeOptions); spec.data = addData(spec.data ?? [], gaugeOptions); - } ); @@ -89,7 +84,6 @@ export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, option signals.push({ name: 'outerRadius', update: "radiusRef * 0.95"}) signals.push({ name: 'radiusRef', update: "min(width/2, height/2)"}) signals.push({ name: 'startAngle', update: "-PI * 2 / 3" }); // -120 degrees - signals.push({ name: 'target', update: `data('table')[0].${options.target}` }); signals.push({ name: 'theta', update: "scale('angleScale', clampedVal)"}) }); diff --git a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts index 377b9ae09..9467cd068 100644 --- a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts +++ b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts @@ -15,50 +15,33 @@ import { NumberFormat, PartiallyRequired } from '../specUtil.types'; export type ThresholdBackground = { thresholdMin?: number; thresholdMax?: number; fill?: string }; export interface GaugeOptions { - markType: 'gauge'; - + /** Sets the name of the component. */ + name?: string; + /** Key in the data that is used as the metric */ + metric?: string; /** Key in the data that is used as the color facet */ color?: string; - /** Minimum value for the scale. This value must be greater than zero. */ + /** Minimum value for the scale. This value must be greater than zero, and less than maxArcValue */ minArcValue?: number; - /** Maximum value for the scale. This value must be greater than zero. */ + /** Maximum value for the scale. This value must be greater than zero, and greater than minArcValue */ maxArcValue?: number; - /** Key in the data that is used as the metric */ - metric?: string | number; - /** Sets the name of the component. */ - name?: string; - /** d3 number format specifier. - * Sets the number format for the summary value. - * - * see {@link https://d3js.org/d3-format#locale_format} - */ - numberFormat?: NumberFormat; - /** Flag to control whether the target is shown */ - target?: string; - /** Color regions that fill the gauge bar to the metric value */ - //track?: boolean; - /** Color of the background fill */ + /** Color of the background arc */ backgroundFill?: string; /** Color of the background stroke */ backgroundStroke?: string; - /** Color of the filler color signal */ + /** Color of the filler color arc */ fillerColorSignal?: string; - /** Color of the label text */ - labelColor?: string; - /** Size of the label text */ - labelSize?: number; } type GaugeOptionsWithDefaults = | 'name' | 'metric' - | 'target' | 'color' | 'minArcValue' | 'maxArcValue' | 'backgroundFill' | 'backgroundStroke' - | 'fillerColorSignal'; + | 'fillerColorSignal' export interface GaugeSpecOptions extends PartiallyRequired<GaugeOptions, GaugeOptionsWithDefaults> { colorScheme: ColorScheme; From 4981a62b418426fc958d182a3485867e7e090ee5 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Tue, 11 Nov 2025 22:42:40 -0700 Subject: [PATCH 29/66] Stashing to get rid of it --- package.json | 3 ++- yarn.lock | 13 ++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index d23e09def..e953461b1 100644 --- a/package.json +++ b/package.json @@ -175,5 +175,6 @@ "React", "Spectrum", "Charts" - ] + ], + "dependencies": {} } diff --git a/yarn.lock b/yarn.lock index 283be820e..132dd5a3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8186,15 +8186,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001716: - version "1.0.30001718" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz#dae13a9c80d517c30c6197515a96131c194d8f82" - integrity sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw== - -caniuse-lite@^1.0.30001669: - version "1.0.30001677" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001677.tgz#27c2e2c637e007cfa864a16f7dfe7cde66b38b5f" - integrity sha512-fmfjsOlJUpMWu+mAAtZZZHz7UEwsUxIIvu1TJfO1HqFQvB/B+ii0xr9B5HpbZY/mC4XZ8SvjHJqtAY6pDPQEog== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001669, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001716: + version "1.0.30001754" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz" + integrity sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg== capital-case@^1.0.4: version "1.0.4" From 5f069a832a8ba1ef67d650e2a28e2243767c8d55 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Wed, 12 Nov 2025 01:01:00 -0700 Subject: [PATCH 30/66] Made gauge tests for data util and mark util. --- .../src/gauge/gaugeDataUtils.test.ts | 170 +------ .../src/gauge/gaugeMarkUtils.test.ts | 473 ++---------------- .../src/gauge/gaugeMarkUtils.ts | 6 +- .../src/gauge/gaugeTestUtils.ts | 53 +- 4 files changed, 59 insertions(+), 643 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeDataUtils.test.ts b/packages/vega-spec-builder/src/gauge/gaugeDataUtils.test.ts index fd7aef1cc..6fb00aaa4 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeDataUtils.test.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeDataUtils.test.ts @@ -10,20 +10,16 @@ * governing permissions and limitations under the License. */ import { Data } from 'vega'; +import { getGaugeTableData } from './gaugeDataUtils'; -import { BulletSpecOptions, ThresholdBackground } from '../types'; -import { generateThresholdColorExpr, getBulletTableData, getBulletTransforms } from './bulletDataUtils'; -import { sampleOptionsColumn } from './bulletTestUtils'; - -describe('getBulletTableData', () => { +describe('getGaugeTableData', () => { it('Should create a new table data if it does not exist', () => { const data: Data[] = []; - const result = getBulletTableData(data); + const result = getGaugeTableData(data); expect(result.name).toBe('table'); expect(result.values).toEqual([]); - expect(result.transform).toEqual([]); expect(data.length).toBe(1); expect(data[0]).toEqual(result); @@ -32,168 +28,12 @@ describe('getBulletTableData', () => { it('Should return the existing table data if it exists', () => { const existingTableData: Data = { name: 'table', - values: [], - transform: [], + values: [4], }; const data: Data[] = [existingTableData]; - const result = getBulletTableData(data); + const result = getGaugeTableData(data); expect(result).toEqual(existingTableData); }); }); - -describe('getBulletTransforms', () => { - it('Should return a formula transform using the target property', () => { - const Options: BulletSpecOptions = { - ...sampleOptionsColumn, - target: 'target', - }; - - const result = getBulletTransforms(Options); - - expect(result).toHaveLength(1); - - expect(result).toEqual([ - { - type: 'formula', - expr: 'isValid(datum.target) ? round(datum.target * 1.05) : 0', - as: 'xPaddingForTarget', - }, - ]); - }); - it('Should return a formula transform using the maxScaleValue property', () => { - const Options: BulletSpecOptions = { - ...sampleOptionsColumn, - target: 'target', - scaleType: 'flexible', - maxScaleValue: 100, - }; - - const result = getBulletTransforms(Options); - - expect(result).toHaveLength(2); - - expect(result[1]).toEqual({ - type: 'formula', - expr: '100', - as: 'flexibleScaleValue', - }); - }); - - it('Should include a barColor transform when thresholdBarColor is true and thresholds are provided', () => { - const thresholds: ThresholdBackground[] = [ - { fill: 'rgb(234, 56, 41)' }, - { thresholdMin: 120, thresholdMax: 235, fill: 'rgb(249, 137, 23)' }, - { thresholdMin: 235, fill: 'rgb(21, 164, 110)' }, - ]; - const Options: BulletSpecOptions = { - ...sampleOptionsColumn, - target: 'target', - thresholdBarColor: true, - thresholds, - color: 'blue', // default color - metric: 'currentAmount', - }; - const result = getBulletTransforms(Options); - expect(result.length).toBeGreaterThanOrEqual(2); - const barColorTransform = result.find((t) => t.as === 'barColor'); - expect(barColorTransform).toBeDefined(); - expect(barColorTransform?.type).toBe('formula'); - expect(typeof barColorTransform?.expr).toBe('string'); - }); - - it('Should not include a barColor transform when thresholds is empty', () => { - // test - const Options: BulletSpecOptions = { - ...sampleOptionsColumn, - target: 'target', - thresholdBarColor: true, - thresholds: [], - color: 'blue', - metric: 'currentAmount', - }; - const result = getBulletTransforms(Options); - const barColorTransform = result.find((t) => t.as === 'barColor'); - expect(barColorTransform).toBeUndefined(); - }); - - it('Should not include a barColor transform when thresholdBarColor is false', () => { - const Options: BulletSpecOptions = { - ...sampleOptionsColumn, - target: 'target', - thresholdBarColor: false, - thresholds: [], - color: 'blue', - metric: 'currentAmount', - }; - const result = getBulletTransforms(Options); - const barColorTransform = result.find((t) => t.as === 'barColor'); - expect(barColorTransform).toBeUndefined(); - }); -}); - -describe('generateThresholdColorExpr', () => { - const metricField = 'currentAmount'; - - it('Should return default color if no thresholds provided', () => { - const expr = generateThresholdColorExpr([], metricField, 'blue-900'); - expect(expr).toBe(`'blue-900'`); - }); - - it('Should generate correct expression for complete thresholds', () => { - const thresholds: ThresholdBackground[] = [ - { fill: 'rgb(234, 56, 41)' }, // first threshold, no thresholdMin → treated as -1e12 - { thresholdMin: 120, thresholdMax: 235, fill: 'rgb(249, 137, 23)' }, - { thresholdMin: 235, fill: 'rgb(21, 164, 110)' }, - ]; - - const expected = - `(datum.${metricField} < -1000000000000) ? 'blue' : ` + - `(datum.${metricField} < 120) ? 'rgb(234, 56, 41)' : ` + - `(datum.${metricField} < 235) ? 'rgb(249, 137, 23)' : ` + - `'rgb(21, 164, 110)'`; - const expr = generateThresholdColorExpr(thresholds, metricField, 'blue'); - expect(expr).toBe(expected); - }); - - it('Should returns proper expression when one threshold is removed', () => { - // Only two thresholds provided. - const thresholds: ThresholdBackground[] = [ - { fill: 'rgb(234, 56, 41)' }, // covers below 120 - { thresholdMin: 120, fill: 'rgb(249, 137, 23)' }, // covers from 120 upward - ]; - - const expected = - `(datum.${metricField} < -1000000000000) ? 'blue' : ` + - `(datum.${metricField} < 120) ? 'rgb(234, 56, 41)' : ` + - `'rgb(249, 137, 23)'`; - const expr = generateThresholdColorExpr(thresholds, metricField, 'blue'); - expect(expr).toBe(expected); - }); - - it('Should sort thresholds correctly when thresholdMin is not provided', () => { - const thresholds: ThresholdBackground[] = [ - { fill: 'rgb(234, 56, 41)' }, // covers below 120 - { thresholdMax: 235, fill: 'rgb(249, 137, 23)' }, // covers from 120 upward - ]; - - const expected = - `(datum.${metricField} < -1000000000000) ? 'blue' : ` + - `(datum.${metricField} < -1000000000000) ? 'rgb(234, 56, 41)' : ` + - `'rgb(249, 137, 23)'`; - const expr = generateThresholdColorExpr(thresholds, metricField, 'blue'); - expect(expr).toBe(expected); - }); - - it('Should return proper expression when two thresholds are removed', () => { - // Only one threshold provided. - const thresholds: ThresholdBackground[] = [ - { fill: 'rgb(234, 56, 41)' }, // covers below 120 - ]; - - const expected = `(datum.${metricField} < -1000000000000) ? 'blue' : 'rgb(234, 56, 41)'`; - const expr = generateThresholdColorExpr(thresholds, metricField, 'blue'); - expect(expr).toBe(expected); - }); -}); diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.test.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.test.ts index 745f4b6e1..3b2cd2e4c 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.test.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.test.ts @@ -12,464 +12,63 @@ import { GroupMark } from 'vega'; import { - addAxes, - addMarks, - getBulletMarkLabel, - getBulletMarkRect, - getBulletMarkTarget, - getBulletMarkThreshold, - getBulletMarkValueLabel, - getBulletTrack, -} from './bulletMarkUtils'; -import { sampleOptionsColumn, sampleOptionsRow } from './bulletTestUtils'; - -describe('getBulletMarks', () => { - test('Should return the correct marks object for column mode', () => { - const data = addMarks([], sampleOptionsColumn)[0] as GroupMark; - expect(data).toBeDefined; - expect(data?.marks).toHaveLength(4); - expect(data?.marks?.[0]?.type).toBe('rect'); - expect(data?.marks?.[1]?.type).toBe('rule'); - expect(data?.marks?.[2]?.type).toBe('text'); - expect(data?.marks?.[3]?.type).toBe('text'); - - //Make sure the object that defines the orientation contains the correct key - expect(Object.keys(data?.encode?.update || {})).toContain('y'); - }); - - test('Should return the correct marks object for row mode', () => { - const data = addMarks([], sampleOptionsRow)[0] as GroupMark; - expect(data).toBeDefined; - expect(data?.marks).toHaveLength(4); - expect(data?.marks?.[0]?.type).toBe('rect'); - expect(data?.marks?.[1]?.type).toBe('rule'); - expect(data?.marks?.[2]?.type).toBe('text'); - expect(data?.marks?.[3]?.type).toBe('text'); - expect(Object.keys(data?.encode?.update || {})).toContain('x'); - }); - - test('Should not include target marks when showTarget is false', () => { - const options = { ...sampleOptionsColumn, showTarget: false, showTargetValue: true }; - const marksGroup = addMarks([], options)[0] as GroupMark; - expect(marksGroup.marks).toHaveLength(3); - marksGroup.marks?.forEach((mark) => { - expect(mark.description).not.toContain('Target'); - }); - }); - - test('Should include target value label when showTargetValue is true', () => { - const options = { ...sampleOptionsColumn, showTarget: true, showTargetValue: true }; - const marksGroup = addMarks([], options)[0] as GroupMark; - expect(marksGroup.marks).toHaveLength(5); - const targetValueMark = marksGroup.marks?.find((mark) => mark.name?.includes('TargetValueLabel')); - expect(targetValueMark).toBeDefined(); - }); - - test('Should include label marks when axis labels are enabled', () => { - const options = { ...sampleOptionsColumn, showTarget: true, showTargetValue: true }; - const marksGroup = addMarks([], options)[0] as GroupMark; - expect(marksGroup.marks).toHaveLength(5); - const targetValueMark = marksGroup.marks?.find((mark) => mark.name?.includes('TargetValueLabel')); - expect(targetValueMark).toBeDefined(); - }); - - test('Should include bullet track when track is set to true and threshold is set to false.', () => { - const options = { ...sampleOptionsColumn, threshold: false, track: true }; - const marksGroup = addMarks([], options)[0] as GroupMark; - expect(marksGroup.marks).toHaveLength(5); - const bulletTrackMark = marksGroup.marks?.find((mark) => mark.name?.includes('Track')); - expect(bulletTrackMark).toBeDefined(); - - // Threshold mark should not be present - const bulletThresholdMark = marksGroup.marks?.find((mark) => mark.name?.includes('Threshold')); - expect(bulletThresholdMark).toBeUndefined(); + addGaugeMarks, + getBackgroundArc, + getFillerArc, + getNeedle, +} from './gaugeMarkUtils'; + +import { defaultGaugeOptions } from './gaugeTestUtils'; +import { spectrumColors } from '../../../themes'; + +describe('getGaugeMarks', () => { + test('Should return the correct marks object', () => { + const data = addGaugeMarks([], defaultGaugeOptions); + expect(data).toBeDefined(); + expect(Array.isArray(data)).toBe(true); + expect(data).toHaveLength(3); + expect(data[0].type).toBe('arc'); + expect(data[1].type).toBe('arc'); + expect(data[2].type).toBe('rule'); }); }); -describe('getBulletMarkRect', () => { - test('Should return the correct rect mark object', () => { - const data = getBulletMarkRect(sampleOptionsColumn); + +describe('getGaugeBackgroundArc', () => { + test('Should return the correct background arc mark object', () => { + const data = getBackgroundArc("backgroundTestName", spectrumColors['light']['blue-200'], spectrumColors['light']['blue-300']); expect(data).toBeDefined(); - expect(data.encode?.update).toBeDefined(); + expect(data.encode?.enter).toBeDefined(); // Expect the correct amount of fields in the update object - expect(Object.keys(data.encode?.update ?? {}).length).toBe(4); - }); - - describe('getBulletMarkRect threshold color logic', () => { - test('Uses barColor field when thresholdBarColor is enabled and thresholds exist', () => { - const optionsWithThresholdColor = { - ...sampleOptionsColumn, - thresholdBarColor: true, - thresholds: [ - { thresholdMax: 120, fill: 'rgb(234, 56, 41)' }, - { thresholdMin: 120, thresholdMax: 235, fill: 'rgb(249, 137, 23)' }, - { thresholdMin: 235, fill: 'rgb(21, 164, 110)' }, - ], - }; - - const rectMark = getBulletMarkRect(optionsWithThresholdColor); - expect(rectMark.encode?.enter?.fill).toEqual([{ field: 'barColor' }]); - }); - - test('Uses default color field when thresholdBarColor is disabled or no thresholds exist', () => { - const optionsNoThresholds = { - ...sampleOptionsColumn, - thresholdBarColor: true, - thresholds: [], - }; - - const rectMark = getBulletMarkRect(optionsNoThresholds); - expect(rectMark.encode?.enter?.fill).toEqual([{ value: optionsNoThresholds.color }]); - }); - - test('Uses default color field when thresholdBarColor is disabled', () => { - const optionsNoThresholds = { - ...sampleOptionsColumn, - thresholdBarColor: false, - thresholds: [ - { thresholdMax: 120, fill: 'rgb(234, 56, 41)' }, - { thresholdMin: 120, thresholdMax: 235, fill: 'rgb(249, 137, 23)' }, - { thresholdMin: 235, fill: 'rgb(21, 164, 110)' }, - ], - }; - - const rectMark = getBulletMarkRect(optionsNoThresholds); - expect(rectMark.encode?.enter?.fill).toEqual([{ value: optionsNoThresholds.color }]); - }); - - test('Uses default color field when thresholdBarColor is disabled and no thresholds exist', () => { - const optionsNoThresholds = { - ...sampleOptionsColumn, - thresholdBarColor: false, - thresholds: [], - }; - - const rectMark = getBulletMarkRect(optionsNoThresholds); - expect(rectMark.encode?.enter?.fill).toEqual([{ value: optionsNoThresholds.color }]); - }); + expect(Object.keys(data.encode?.enter ?? {}).length).toBe(8); }); }); -describe('getBulletMarkTarget', () => { - test('Should return the correct target mark object', () => { - const data = getBulletMarkTarget(sampleOptionsColumn); - expect(data).toBeDefined(); - expect(data.encode?.update).toBeDefined(); - expect(Object.keys(data.encode?.update ?? {}).length).toBe(3); - }); -}); -describe('getBulletMarkLabel', () => { - test('Should return the correct label mark object', () => { - const data = getBulletMarkLabel(sampleOptionsColumn); +describe('getFillerArc', () => { + test('Should return the correct filler arc mark object', () => { + const data = getFillerArc("fillerTestName", spectrumColors['light']['magenta-900']); expect(data).toBeDefined(); - expect(data.encode?.update).toBeDefined(); - expect(Object.keys(data.encode?.update ?? {}).length).toBe(2); - }); -}); -describe('getBulletMarkValueLabel', () => { - test('Should return the correct value label mark object in column mode', () => { - const data = getBulletMarkValueLabel(sampleOptionsColumn); - expect(data).toBeDefined(); expect(data.encode?.update).toBeDefined(); - expect(Object.keys(data.encode?.update ?? {}).length).toBe(2); - }); - - test('Should apply numberFormat specifier to metric and target values', () => { - const options = { ...sampleOptionsColumn, showTarget: true, showTargetValue: true, numberFormat: '$,.2f' }; - const marksGroup = addMarks([], options)[0] as GroupMark; - - const metricValueLabel = marksGroup.marks?.find((mark) => mark.name === `${options.name}ValueLabel`); - expect(metricValueLabel).toBeDefined(); - - if (metricValueLabel?.encode?.enter?.text) { - const textEncode = metricValueLabel.encode.enter.text; - if (typeof textEncode === 'object' && 'signal' in textEncode) { - expect(textEncode.signal).toContain(`format(datum.${options.metric}, '$,.2f')`); - } - } - - const TargetValueLabel = marksGroup.marks?.find((mark) => mark.name?.includes('TargetValueLabel')); - expect(TargetValueLabel).toBeDefined(); + expect(Object.keys(data.encode?.update ?? {}).length).toBe(1); - if (TargetValueLabel?.encode?.enter?.text) { - const textEncode = TargetValueLabel.encode.enter.text; - if (typeof textEncode === 'object' && 'signal' in textEncode) { - expect(textEncode.signal).toContain(`format(datum.${options.target}, '$,.2f')`); - } - } - }); - - describe('getBulletMarkValueLabel threshold color logic', () => { - test('Uses barColor field for label when thresholdBarColor is true', () => { - const options = { - ...sampleOptionsColumn, - thresholdBarColor: true, - thresholds: [{ thresholdMax: 200, fill: 'rgb(249, 137, 23)' }], - }; - const labelMark = getBulletMarkValueLabel(options); - expect(labelMark.encode?.enter?.fill).toEqual({ - signal: "datum.barColor === 'green' ? 'rgb(0, 0, 0)' : datum.barColor", - }); - }); - - test('Falls back to neutral when thresholdBarColor is false', () => { - const options = { - ...sampleOptionsColumn, - thresholdBarColor: false, - }; - const labelMark = getBulletMarkValueLabel(options); - - expect(labelMark.encode?.enter?.fill).toEqual({ signal: "'rgb(0, 0, 0)'" }); - }); - - test('Uses default color when no thresholds are provided', () => { - const options = { - ...sampleOptionsColumn, - thresholdBarColor: true, - thresholds: [], - }; - const labelMark = getBulletMarkValueLabel(options); - expect(labelMark.encode?.enter?.fill).toEqual({ signal: "'rgb(0, 0, 0)'" }); - }); - - test('Uses default color when thresholdBarColor is false and no thresholds are provided', () => { - const options = { - ...sampleOptionsColumn, - thresholdBarColor: false, - thresholds: [], - }; - const labelMark = getBulletMarkValueLabel(options); - expect(labelMark.encode?.enter?.fill).toEqual({ signal: "'rgb(0, 0, 0)'" }); - }); - - test('Uses default color when thresholdBarColor is true and no thresholds are provided', () => { - const options = { - ...sampleOptionsColumn, - thresholdBarColor: true, - thresholds: [], - }; - const labelMark = getBulletMarkValueLabel(options); - expect(labelMark.encode?.enter?.fill).toEqual({ signal: "'rgb(0, 0, 0)'" }); - }); - }); -}); - -describe('getBulletMarkSideLabel', () => { - test('Should not return label marks when side label mode is enabled', () => { - const options = { ...sampleOptionsColumn, labelPosition: 'side' as 'side' | 'top' }; - const marks = addMarks([], options)[0] as GroupMark; - expect(marks.marks).toBeDefined(); - expect(marks.marks).toHaveLength(2); - }); -}); - -describe('getBulletAxes', () => { - test('Should return the correct axes object when side label mode is enabled', () => { - const options = { ...sampleOptionsColumn, labelPosition: 'side' as 'side' | 'top' }; - const axes = addAxes([], options); - expect(axes).toHaveLength(2); - expect(axes[0].labelOffset).toBe(2); - }); - - test('Should return the correct axes object when side label mode is enabled and target label is shown', () => { - const options = { ...sampleOptionsColumn, labelPosition: 'side' as 'side' | 'top', showTargetValue: true }; - const axes = addAxes([], options); - expect(axes).toHaveLength(2); - expect(axes[0].labelOffset).toBe(-8); - }); - - test('Should return an empty list when top label mode is enabled', () => { - const options = { ...sampleOptionsColumn }; - const axes = addAxes([], options); - expect(axes).toStrictEqual([]); - }); - - test('Should return the scale axis when axis is true, row mode is enabled, and showtarget is false', () => { - const options = { ...sampleOptionsColumn, metricAxis: true }; - const axes = addAxes([], options); - expect(axes).toStrictEqual([ - { - labelOffset: 2, - scale: 'xscale', - orient: 'bottom', - ticks: false, - labelColor: 'gray', - domain: false, - tickCount: 5, - offset: { signal: 'axisOffset' }, - }, - ]); - }); - - test('Should not return scale axis when showtarget and showtargetValue are true', () => { - const options = { ...sampleOptionsColumn, showTarget: true, showTargetValue: true, axis: true }; - const axes = addAxes([], options); - expect(axes).toStrictEqual([]); - }); - - test('Should return scale axis and label axes when both are enabled', () => { - const options = { ...sampleOptionsColumn, labelPosition: 'side' as 'side' | 'top', metricAxis: true }; - const axes = addAxes([], options); - expect(axes).toStrictEqual([ - { - labelOffset: 2, - scale: 'xscale', - orient: 'bottom', - ticks: false, - labelColor: 'gray', - domain: false, - tickCount: 5, - offset: { signal: 'axisOffset' }, - }, - { - scale: 'groupScale', - orient: 'left', - tickSize: 0, - labelOffset: 2, - labelPadding: 10, - labelColor: '#797979', - domain: false, - }, - { - scale: 'groupScale', - orient: 'right', - tickSize: 0, - labelOffset: 2, - labelPadding: 10, - domain: false, - encode: { - labels: { - update: { - text: { - signal: - "info(data('table')[datum.index * (length(data('table')) - 1)].currentAmount) != null ? format(info(data('table')[datum.index * (length(data('table')) - 1)].currentAmount), '') : ''", - }, - }, - }, - }, - }, - ]); + expect(data.encode?.enter).toBeDefined(); + expect(Object.keys(data.encode?.enter ?? {}).length).toBe(7) }); }); -describe('Threshold functionality', () => { - describe('Data generation', () => { - test('Should add threshold data and mark when thresholds are provided', () => { - const detailedThresholds = [ - { thresholdMax: 120, fill: 'rgb(234, 56, 41)' }, - { thresholdMin: 120, thresholdMax: 235, fill: 'rgb(249, 137, 23)' }, - { thresholdMin: 235, fill: 'rgb(21, 164, 110)' }, - ]; - const options = { - ...sampleOptionsRow, - name: 'testBullet', - thresholds: detailedThresholds, - }; - - const marksGroup = addMarks([], options)[0] as GroupMark; - expect(marksGroup.data).toBeDefined(); - expect(marksGroup.data?.[0].name).toBe('thresholds'); - - // Ensure that the generated values match the detailed thresholds. - const dataItem = marksGroup.data?.[0]; - expect(dataItem).toHaveProperty('values'); - const values = (dataItem as { values: unknown[] }).values; - expect(values).toEqual(detailedThresholds); - - const thresholdMark = marksGroup.marks?.find((mark) => mark.name === `${options.name}Threshold`); - expect(thresholdMark).toBeDefined(); - }); - }); - - describe('Y encoding', () => { - test('Should adjust y encoding when showTarget and showTargetValue is enabled', () => { - const options = { - ...sampleOptionsRow, - name: 'testBullet', - showTarget: true, - showTargetValue: true, - }; - expect(options.showTarget).toBe(true); - expect(options.showTargetValue).toBe(true); - - const thresholdMark = getBulletMarkThreshold(options); - expect(thresholdMark).toBeDefined(); - expect(thresholdMark.encode).toBeDefined(); - expect(thresholdMark.encode?.update).toBeDefined(); - const yEncoding = thresholdMark.encode?.update?.y; - if (yEncoding && 'signal' in yEncoding) { - expect(yEncoding.signal).toContain('targetValueLabelHeight'); - const expectedSignal = 'bulletGroupHeight - 3 - bulletThresholdHeight - targetValueLabelHeight'; - expect(yEncoding.signal).toBe(expectedSignal); - } - }); - - test('Should compute y encoding without subtracting targetValueLabelHeight when showTargetValue is false', () => { - const options = { - ...sampleOptionsRow, - name: 'testBullet', - showTarget: true, - showTargetValue: false, - }; - const thresholdMark = getBulletMarkThreshold(options); - expect(thresholdMark).toBeDefined(); - expect(thresholdMark.encode).toBeDefined(); - expect(thresholdMark.encode?.update).toBeDefined(); - - const yEncoding = thresholdMark.encode?.update?.y; - if (yEncoding && 'signal' in yEncoding) { - expect(yEncoding.signal).not.toContain('targetValueLabelHeight'); - const expectedSignal = 'bulletGroupHeight - 3 - bulletThresholdHeight'; - expect(yEncoding.signal).toBe(expectedSignal); - } - }); - }); -}); - -describe('getBulletMarkTrack', () => { - test('Should return the correct track mark object in column mode', () => { - const options = { - ...sampleOptionsColumn, - name: 'testBullet', - threshold: false, - track: true, - }; - const data = getBulletTrack(options); +describe('getGaugeNeedle', () => { + test('Should return the needle mark object', () => { + const data = getNeedle("needleTestName"); expect(data).toBeDefined(); expect(data.encode?.update).toBeDefined(); expect(Object.keys(data.encode?.update ?? {}).length).toBe(4); - expect(Object.keys(data.encode?.enter ?? {}).length).toBe(5); - expect(data.encode?.update?.width).toBeDefined(); - expect(data.encode?.update?.width).toStrictEqual({ signal: 'width' }); - }); - - test('Should return the correct track mark object in row mode', () => { - const options = { - ...sampleOptionsRow, - name: 'testBullet', - threshold: false, - track: true, - }; - const data = getBulletTrack(options); - expect(data.encode?.update?.width).toBeDefined(); - expect(data.encode?.update?.width).toStrictEqual({ signal: 'bulletGroupWidth' }); - }); - test('Should return the correct track mark object when the target label is enabled', () => { - const options = { - ...sampleOptionsRow, - name: 'testBullet', - threshold: false, - track: true, - showTarget: true, - showTargetValue: true, - }; - const data = getBulletTrack(options); - expect(data.encode?.update?.y).toBeDefined(); - expect(data.encode?.update?.y).toStrictEqual({ signal: 'bulletGroupHeight - 3 - 2 * bulletHeight - 20' }); + expect(data.encode?.enter).toBeDefined(); + expect(Object.keys(data.encode?.enter ?? {}).length).toBe(3) }); }); + diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 2a66227d0..a196a84ab 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -36,7 +36,7 @@ export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => marks.push(getNeedle(name)); }); -function getBackgroundArc(name: string, fill: string, stroke: string): Mark { +export function getBackgroundArc(name: string, fill: string, stroke: string): Mark { return { name: `${name}BackgroundArcRounded`, description: 'Background Arc (Round Edge)', @@ -56,7 +56,7 @@ function getBackgroundArc(name: string, fill: string, stroke: string): Mark { }; } -function getFillerArc(name: string, fillerColorSignal: string): Mark { +export function getFillerArc(name: string, fillerColorSignal: string): Mark { return { name: `${name}FillerArc`, description: 'Filler Arc', @@ -78,7 +78,7 @@ function getFillerArc(name: string, fillerColorSignal: string): Mark { }; } - function getNeedle(name: string): Mark { + export function getNeedle(name: string): Mark { return { name: `${name}Needle`, description: 'Needle (rule)', diff --git a/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts index b401dae75..fc53ae909 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts @@ -9,46 +9,23 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -import { BulletSpecOptions } from '../types'; +import { GaugeSpecOptions } from '../types'; +import { spectrumColors } from '@spectrum-charts/themes'; +import { DEFAULT_COLOR_SCHEME } from '@spectrum-charts/constants'; -export const sampleOptionsColumn: BulletSpecOptions = { - markType: 'bullet', - colorScheme: 'light', - index: 0, - color: 'green', - metric: 'currentAmount', - dimension: 'graphLabel', - target: 'target', - name: 'bullet0', - idKey: 'rscMarkId', - direction: 'column', - showTarget: true, - showTargetValue: false, - labelPosition: 'top', - scaleType: 'normal', - maxScaleValue: 100, - metricAxis: false, - track: false, - thresholdBarColor: false, -}; -export const sampleOptionsRow: BulletSpecOptions = { - markType: 'bullet', +import { MARK_ID } from '@spectrum-charts/constants'; + +export const defaultGaugeOptions: GaugeSpecOptions = { colorScheme: 'light', + idKey: MARK_ID, index: 0, - color: 'green', - metric: 'currentAmount', - dimension: 'graphLabel', - target: 'target', - name: 'bullet0', - idKey: 'rscMarkId', - direction: 'row', - showTarget: true, - showTargetValue: false, - labelPosition: 'top', - scaleType: 'normal', - maxScaleValue: 100, - track: false, - thresholdBarColor: false, - metricAxis: false, + name: '', + metric: '', + color: DEFAULT_COLOR_SCHEME, + minArcValue: 0, + maxArcValue: 0, + backgroundFill: spectrumColors['light']['gray-200'], + backgroundStroke: spectrumColors['light']['gray-300'], + fillerColorSignal: '' }; From 156746acbf8033d58a051b3e6b6f27c6f4debbc0 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Wed, 12 Nov 2025 22:42:52 -0700 Subject: [PATCH 31/66] Fixed gaugeSpecBuilder test --- .../src/gauge/gaugeSpecBuilder.test.ts | 222 +++++++----------- .../src/gauge/gaugeSpecBuilder.ts | 2 +- .../src/gauge/gaugeTestUtils.ts | 18 +- 3 files changed, 93 insertions(+), 149 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts index 638833665..cc159950b 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts @@ -10,27 +10,24 @@ * governing permissions and limitations under the License. */ import { GaugeOptions, ScSpec } from '../types'; -import { addBullet, addData, addScales, addSignals } from './bulletSpecBuilder'; -import { sampleOptionsColumn, sampleOptionsRow } from './bulletTestUtils'; +import { addGauge, addData, addScales, addSignals } from './gaugeSpecBuilder'; +import { defaultGaugeOptions } from './gaugeTestUtils'; -describe('addBullet', () => { +import { getColorValue, spectrumColors } from '../../../themes'; + +import { DEFAULT_COLOR_SCHEME } from '@spectrum-charts/constants'; + +const byName = (signals: any[], name: string) => signals.find(s => s.name === name); + +describe('addGauge', () => { let spec: ScSpec; beforeEach(() => { spec = { data: [], marks: [], scales: [], usermeta: {} }; }); - test('should modify spec with bullet chart properties', () => { - const bulletOptions: BulletOptions & { idKey: string } = { - markType: 'bullet', - name: 'testBullet', - metric: 'revenue', - dimension: 'region', - target: 'goal', - idKey: 'rscMarkId', - }; - - const newSpec = addBullet(spec, bulletOptions); + test('should create a spec with gauge chart properties', () => { + const newSpec = addGauge(spec, defaultGaugeOptions); expect(newSpec).toBeDefined(); expect(newSpec).toHaveProperty('data'); @@ -40,153 +37,102 @@ describe('addBullet', () => { }); }); -describe('getBulletScales', () => { - test('Should return the correct scales object for column mode', () => { - const data = addScales([], sampleOptionsColumn); +describe('getGaugeScales', () => { + test('Should return the correct scale object', () => { + const data = addScales([], defaultGaugeOptions); expect(data).toBeDefined(); - expect(data).toHaveLength(2); - expect('range' in data[0] && data[0].range && data[0].range[1]).toBeTruthy(); - if ('range' in data[0] && data[0].range && data[0].range[1]) { - expect(data[0].range[1].signal).toBe('bulletChartHeight'); + expect(data).toHaveLength(1); + expect('range' in data[0] && data[0].range).toBeTruthy(); + if ('range' in data[0] && data[0].range) { + expect(data[0].range[1].signal).toBe('endAngle'); } }); +}); - test('Should return the correct scales object for row mode', () => { - const data = addScales([], sampleOptionsRow); +describe('getGaugeSignals', () => { + test('Should return the correct signals object', () => { + const data = addSignals([], defaultGaugeOptions); expect(data).toBeDefined(); - expect(data).toHaveLength(2); - expect('range' in data[0] && data[0].range && data[0].range[1]).toBeTruthy(); - if ('range' in data[0] && data[0].range && data[0].range[1]) { - expect(data[0].range[1].signal).toBe('width'); - } + expect(data).toHaveLength(19); }); +}); - test('Should return the correct scales object for flexible scale mode', () => { - const options = { ...sampleOptionsColumn, scaleType: 'flexible' as 'normal' | 'flexible' | 'fixed' }; - const data = addScales([], options); - expect(data).toBeDefined(); - expect(data[1].domain).toBeDefined(); - expect(data[1].domain).toStrictEqual({ - data: 'table', - fields: ['xPaddingForTarget', options.metric, 'flexibleScaleValue'], - }); +describe('getGaugeData', () => { + test('Should return the data object', () => { + const data = addData([], defaultGaugeOptions); + expect(data).toHaveLength(1); }); +}); - test('Should return the correct scales object for fixed scale mode', () => { - const options = { ...sampleOptionsColumn, scaleType: 'fixed' as 'normal' | 'flexible' | 'fixed' }; - const data = addScales([], options); - expect(data).toBeDefined(); - expect(data[1].domain).toBeDefined(); - expect(data[1].domain).toStrictEqual([0, `${options.maxScaleValue}`]); - }); +describe('addGauge (defaults & overrides for gaugeOptions)', () => { + let spec: ScSpec; - test('Should return the correct scales object for normal scale mode', () => { - const options = { ...sampleOptionsColumn, scaleType: 'normal' as 'normal' | 'flexible' | 'fixed' }; - const data = addScales([], options); - expect(data).toBeDefined(); - expect(data[1].domain).toBeDefined(); - expect(data[1].domain).toStrictEqual({ data: 'table', fields: ['xPaddingForTarget', options.metric] }); + beforeEach(() => { + spec = { data: [], marks: [], scales: [], usermeta: {} }; }); - test('Should return the correct scales object when a negative value is passed for maxScaleValue', () => { - const options = { - ...sampleOptionsColumn, - scaleType: 'fixed' as 'normal' | 'flexible' | 'fixed', - maxScaleValue: -100, - }; - const data = addScales([], options); - expect(data).toBeDefined(); - expect(data[1].domain).toBeDefined(); + test('uses defaults when no overrides are provided', () => { + const newSpec = addGauge(spec, defaultGaugeOptions); - // Expect normal scale mode to be used - expect(data[1].domain).toStrictEqual({ data: 'table', fields: ['xPaddingForTarget', options.metric] }); - }); -}); + expect(newSpec).toBeDefined(); + expect(newSpec.signals).toBeDefined(); -describe('getBulletSignals', () => { - test('Should return the correct signals object in column mode', () => { - const data = addSignals([], sampleOptionsColumn); - expect(data).toBeDefined(); - expect(data).toHaveLength(7); - }); + const signals = newSpec.signals as any[]; - test('Should return the correct signals object in row mode', () => { - const data = addSignals([], sampleOptionsRow); - expect(data).toBeDefined(); - expect(data).toHaveLength(8); - }); + // min/max come from defaults in gaugeOptions + expect(byName(signals, 'arcMaxVal')?.value).toBe(100); + expect(byName(signals, 'arcMinVal')?.value).toBe(0); - test('Should include targetValueLabelHeight signal when showTargetValue is true', () => { - const options = { ...sampleOptionsColumn, showTarget: true, showTargetValue: true }; - const signals = addSignals([], options); - expect(signals.find((signal) => signal.name === 'targetValueLabelHeight')).toBeDefined(); - }); + // default angles: -120° .. +120° + expect(byName(signals, 'startAngle')?.update).toBe('-PI * 2 / 3'); + expect(byName(signals, 'endAngle')?.update).toBe('PI * 2 / 3'); - test('Should include correct targetValueLabelHeight signal when showTargetValue is true', () => { - const options = { - ...sampleOptionsColumn, - showTarget: true, - showTargetValue: true, - labelPosition: 'side' as 'side' | 'top', - }; - const signals = addSignals([], options); - expect(signals.find((signal) => signal.name === 'bulletGroupHeight')).toStrictEqual({ - name: 'bulletGroupHeight', - update: 'bulletThresholdHeight + targetValueLabelHeight + 10', - }); - }); + // default metric is 'currentAmount' + expect(byName(signals, 'currVal')?.update).toBe("data('table')[0].currentAmount"); - test('Should include correct targetValueLabelHeight signal when showTargetValue is true', () => { - const options = { - ...sampleOptionsColumn, - showTarget: true, - showTargetValue: true, - labelPosition: 'top' as 'side' | 'top', - }; - const signals = addSignals([], options); - expect(signals.find((signal) => signal.name === 'bulletGroupHeight')).toStrictEqual({ - name: 'bulletGroupHeight', - update: 'bulletThresholdHeight + targetValueLabelHeight + 24', - }); - }); + // background fill from DEFAULT_COLOR_SCHEME + const scheme = DEFAULT_COLOR_SCHEME; + const expectedBgFill = spectrumColors[scheme]['gray-200']; + expect(byName(signals, 'backgroundfillColor')?.value).toBe(expectedBgFill); - test('Should include correct targetValueLabelHeight signal when showTargetValue is true', () => { - const options = { - ...sampleOptionsColumn, - showTarget: true, - showTargetValue: false, - labelPosition: 'side' as 'side' | 'top', - }; - const signals = addSignals([], options); - expect(signals.find((signal) => signal.name === 'bulletGroupHeight')).toStrictEqual({ - name: 'bulletGroupHeight', - update: 'bulletThresholdHeight + 10', - }); + // fillerColorToCurrVal uses light + expect(byName(signals, 'fillerColorToCurrVal')?.value).toBe('light'); }); - test('Should include correct bulletChartHeight signal when options.axis is true and showTargetValue is false', () => { - const options = { - ...sampleOptionsColumn, - showTargetValue: false, - metricAxis: true, + test('applies user overrides (colorScheme, color, min/max, metric, name, index)', () => { + const overrides = { + ...defaultGaugeOptions, + colorScheme: 'dark' as const, + color: spectrumColors.dark['yellow-900'], + backgroundFill: spectrumColors.dark['gray-200'], + minArcValue: 50, + maxArcValue: 500, + metric: 'myMetric', + name: 'Revenue Gauge', + index: 2, }; - const signals = addSignals([], options); - expect(signals.find((signal) => signal.name === 'bulletChartHeight')).toStrictEqual({ - name: 'bulletChartHeight', - update: "length(data('table')) * bulletGroupHeight + (length(data('table')) - 1) * gap + 10", - }); - }); -}); -describe('getBulletData', () => { - test('Should return the data object', () => { - const data = addData([], sampleOptionsColumn); - expect(data).toHaveLength(1); - }); + const newSpec = addGauge(spec, overrides); + const signals = newSpec.signals as any[]; + + // min/max should reflect overrides + expect(byName(signals, 'arcMinVal')?.value).toBe(50); + expect(byName(signals, 'arcMaxVal')?.value).toBe(500); - test('Should return the correct data object in flexible scale mode', () => { - const options = { ...sampleOptionsColumn, scaleType: 'flexible' as 'normal' | 'flexible' | 'fixed' }; - const data = addData([], options); - expect(data[0].transform).toHaveLength(2); + // metric override reflected in currVal + expect(byName(signals, 'currVal')?.update).toBe("data('table')[0].myMetric"); + + // background fill should read from dark scheme + expect(byName(signals, 'backgroundfillColor')?.value).toBe(spectrumColors.dark['gray-200']); + + // filler color should be computed via getColorValue with dark scheme + const expectedFillerDark = getColorValue(spectrumColors.dark['yellow-900'], 'dark'); + expect(byName(signals, 'fillerColorToCurrVal')?.value).toBe(expectedFillerDark); + + // sanity: start/end angles remain the same defaults + expect(byName(signals, 'startAngle')?.update).toBe('-PI * 2 / 3'); + expect(byName(signals, 'endAngle')?.update).toBe('PI * 2 / 3'); }); + }); + diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index 2c6206a7a..cc255b56e 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ import { produce } from 'immer'; -import { Mark, Signal, Scale, Data } from 'vega'; +import { Signal, Scale, Data } from 'vega'; import { addGaugeMarks } from './gaugeMarkUtils'; diff --git a/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts index fc53ae909..79787257f 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts @@ -12,20 +12,18 @@ import { GaugeSpecOptions } from '../types'; import { spectrumColors } from '@spectrum-charts/themes'; import { DEFAULT_COLOR_SCHEME } from '@spectrum-charts/constants'; - - import { MARK_ID } from '@spectrum-charts/constants'; export const defaultGaugeOptions: GaugeSpecOptions = { - colorScheme: 'light', + colorScheme: DEFAULT_COLOR_SCHEME, idKey: MARK_ID, - index: 0, - name: '', - metric: '', + index: 5, + name: 'gaugeTestName', + metric: 'currentAmount', color: DEFAULT_COLOR_SCHEME, minArcValue: 0, - maxArcValue: 0, - backgroundFill: spectrumColors['light']['gray-200'], - backgroundStroke: spectrumColors['light']['gray-300'], - fillerColorSignal: '' + maxArcValue: 100, + backgroundFill: spectrumColors[DEFAULT_COLOR_SCHEME]['gray-200'], + backgroundStroke: spectrumColors[DEFAULT_COLOR_SCHEME]['gray-300'], + fillerColorSignal: 'light' }; From e5f2f0c63d9e025c6dc7c369f6d39f94ae4d9555 Mon Sep 17 00:00:00 2001 From: JarekSmith <105569335+JarekSmith@users.noreply.github.com> Date: Wed, 12 Nov 2025 23:29:44 -0700 Subject: [PATCH 32/66] Implement Gauge.test.tsx --- .../stories/components/Gauge/Gauge.test.tsx | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.test.tsx b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.test.tsx index bd4b03284..fbd539d8f 100644 --- a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.test.tsx +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.test.tsx @@ -9,36 +9,30 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -import { Bullet } from '../../../alpha'; -import { findAllMarksByGroupName, findChart, render } from '../../../test-utils'; +import { spectrumColors } from '@spectrum-charts/themes'; +import { Gauge } from '../../../alpha'; +import { findAllMarksByGroupName, findChart, findMarksByGroupName, render } from '../../../test-utils'; import { Basic } from './Gauge.story'; +import { DEFAULT_COLOR_SCHEME } from '@spectrum-charts/constants'; -describe('Bullet', () => { - // Bullet is not a real React component. This is test just provides test coverage for sonarqube - test('Bullet pseudo element', () => { - render(<Bullet />); +describe('Gauge', () => { + // Gauge is not a real React component. This test provides test coverage for sonarqube + test('Gauge pseudo element', () => { + render(<Gauge />); }); - test('Basic bullet renders properly', async () => { + test('Basic gauge renders properly', async () => { render(<Basic {...Basic.args} />); const chart = await findChart(); expect(chart).toBeInTheDocument(); - const rects = await findAllMarksByGroupName(chart, 'bullet0Rect'); - expect(rects.length).toEqual(2); + const backgroundArc = await findMarksByGroupName(chart, 'BackgroundArcRounded'); + expect(backgroundArc).toBeDefined(); - rects.forEach((rect) => { - // Expect blue-900 color - expect(rect).toHaveAttribute('fill', 'rgb(2, 101, 220)'); - }); + const fillerArc = await findMarksByGroupName(chart, 'FillerArc'); + expect(fillerArc).toBeDefined(); - const barLabels = await findAllMarksByGroupName(chart, 'bullet0Label', 'text'); - expect(barLabels.length).toEqual(2); - - const amountLabels = await findAllMarksByGroupName(chart, 'bullet0ValueLabel', 'text'); - expect(amountLabels.length).toEqual(2); - - const rules = await findAllMarksByGroupName(chart, 'bullet0Target', 'line'); - expect(rules.length).toEqual(2); + const needleRule = await findMarksByGroupName(chart, 'Needle', 'line'); + expect(needleRule).toBeDefined(); }); }); From 95734fc15c4b5aee15b50377324526658bb43c24 Mon Sep 17 00:00:00 2001 From: JarekSmith <105569335+JarekSmith@users.noreply.github.com> Date: Wed, 12 Nov 2025 23:33:37 -0700 Subject: [PATCH 33/66] Remove unnecessary imports --- .../src/stories/components/Gauge/Gauge.test.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.test.tsx b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.test.tsx index fbd539d8f..0824494b7 100644 --- a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.test.tsx +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.test.tsx @@ -9,11 +9,9 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -import { spectrumColors } from '@spectrum-charts/themes'; import { Gauge } from '../../../alpha'; -import { findAllMarksByGroupName, findChart, findMarksByGroupName, render } from '../../../test-utils'; +import { findChart, findMarksByGroupName, render } from '../../../test-utils'; import { Basic } from './Gauge.story'; -import { DEFAULT_COLOR_SCHEME } from '@spectrum-charts/constants'; describe('Gauge', () => { // Gauge is not a real React component. This test provides test coverage for sonarqube From 09f6ead9bf19da305b9f7a1e7b6246e5e7ec071d Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Thu, 13 Nov 2025 22:07:57 -0700 Subject: [PATCH 34/66] Create skeleton for optional needle --- packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts | 5 ++++- .../vega-spec-builder/src/types/marks/gaugeSpec.types.ts | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index a196a84ab..96c16597f 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -16,6 +16,7 @@ import { DEFAULT_COLOR_SCHEME } from '@spectrum-charts/constants'; import { GaugeSpecOptions } from '../types'; import { spectrumColors } from '@spectrum-charts/themes'; +import { defaultGaugeOptions } from './gaugeTestUtils'; export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => { const { @@ -33,7 +34,9 @@ export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => marks.push(getFillerArc(name, fillerColorSignal)); // Needle to clampedValue - marks.push(getNeedle(name)); + if (gaugeOptions.needle) { + marks?.push(getNeedle(name)); + } }); export function getBackgroundArc(name: string, fill: string, stroke: string): Mark { diff --git a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts index 9467cd068..bad71ff96 100644 --- a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts +++ b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts @@ -31,6 +31,8 @@ export interface GaugeOptions { backgroundStroke?: string; /** Color of the filler color arc */ fillerColorSignal?: string; + /** Showing the needle mark */ + needle?: boolean; } type GaugeOptionsWithDefaults = From 11ec94f7c61a66181147cded2b058423eca39c23 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Fri, 14 Nov 2025 01:23:32 -0700 Subject: [PATCH 35/66] Trash --- .../src/gauge/gaugeMarkUtils.ts | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 96c16597f..1f4680d21 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -36,7 +36,7 @@ export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => // Needle to clampedValue if (gaugeOptions.needle) { marks?.push(getNeedle(name)); - } + } }); export function getBackgroundArc(name: string, fill: string, stroke: string): Mark { @@ -85,19 +85,21 @@ export function getFillerArc(name: string, fillerColorSignal: string): Mark { return { name: `${name}Needle`, description: 'Needle (rule)', - type: 'rule', + type: 'symbol', encode: { - enter: { - stroke: { value: '#333' }, - strokeWidth: { value: 3 }, - strokeCap: { value: 'round' } - }, - update: { - x: { signal: 'centerX' }, - y: { signal: 'centerY' }, - x2: { signal: 'needleTipX' }, - y2: { signal: 'needleTipY' } - } + enter: { + x: { signal: "centerX" }, + y: { signal: "centerY" } + }, + update: { + shape: { + signal: + "'M -5 0 Q -5 5 0 5 Q 5 5 5 0' + 'L 2.5 -30 ' + 'Q 2.5 -35 0 -35 Q -2.5 -35 -2.5 -30' + 'L -5 0 Z'" + }, + angle: { signal: "needleAngleDeg" }, + fill: { signal: "fillerColorToCurrVal" }, + stroke: { signal: "fillerColorToCurrVal" }, } + } }; } From ce4e44df2605b3a0afdd39487a07c65952f02601 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Fri, 14 Nov 2025 01:37:37 -0700 Subject: [PATCH 36/66] Implement basic needle --- packages/constants/constants.ts | 1 + .../react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx | 2 ++ packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts | 3 ++- packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts | 2 ++ packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts | 1 + 5 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/constants/constants.ts b/packages/constants/constants.ts index c1b2c7e26..3093f9d63 100644 --- a/packages/constants/constants.ts +++ b/packages/constants/constants.ts @@ -38,6 +38,7 @@ export const DEFAULT_LOCALE = 'en-US'; export const DEFAULT_METRIC = 'value'; export const DEFAULT_MAX_ARC_VALUE = 100; export const DEFAULT_MIN_ARC_VALUE = 0; +export const DEFAULT_NEEDLE = false; export const DEFAULT_SCALE_TYPE = 'normal'; export const DEFAULT_SCALE_VALUE = 100; export const DEFAULT_SECONDARY_COLOR = 'subSeries'; diff --git a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx index f7e541287..f50bf0352 100644 --- a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx +++ b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx @@ -16,6 +16,7 @@ import { FC } from 'react'; import { DEFAULT_MAX_ARC_VALUE, DEFAULT_MIN_ARC_VALUE, + DEFAULT_NEEDLE } from '@spectrum-charts/constants'; import { GaugeProps } from '../../../types'; @@ -26,6 +27,7 @@ const Gauge: FC<GaugeProps> = ({ metric = 'currentAmount', // CurrVal minArcValue = DEFAULT_MIN_ARC_VALUE, // Min Arc Value maxArcValue = DEFAULT_MAX_ARC_VALUE, // Max Arc Value + needle = DEFAULT_NEEDLE, }) => { return null; }; diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 1f4680d21..835a3eda0 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -22,6 +22,7 @@ export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => const { name, colorScheme = DEFAULT_COLOR_SCHEME, + needle } = opt; const backgroundFill = spectrumColors[colorScheme]['gray-200']; const backgroundStroke = spectrumColors[colorScheme]['gray-300']; @@ -34,7 +35,7 @@ export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => marks.push(getFillerArc(name, fillerColorSignal)); // Needle to clampedValue - if (gaugeOptions.needle) { + if (needle) { marks?.push(getNeedle(name)); } }); diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index cc255b56e..edb0a15e3 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -54,6 +54,7 @@ export const addGauge = produce< minArcValue: 0, metric: 'currentAmount', name: toCamelCase(name ?? `gauge${index}`), + needle: false, ...options, }; @@ -85,6 +86,7 @@ export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, option signals.push({ name: 'radiusRef', update: "min(width/2, height/2)"}) signals.push({ name: 'startAngle', update: "-PI * 2 / 3" }); // -120 degrees signals.push({ name: 'theta', update: "scale('angleScale', clampedVal)"}) + signals.push({ name: 'needleAngleDeg', update: "needleAngleOriginal * 180 / PI"}) }); export const addScales = produce<Scale[], [GaugeSpecOptions]>((scales, options) => { diff --git a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts index bad71ff96..1bf69a4d2 100644 --- a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts +++ b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts @@ -44,6 +44,7 @@ type GaugeOptionsWithDefaults = | 'backgroundFill' | 'backgroundStroke' | 'fillerColorSignal' + | 'needle' export interface GaugeSpecOptions extends PartiallyRequired<GaugeOptions, GaugeOptionsWithDefaults> { colorScheme: ColorScheme; From c8f33a2c10e3fae2f9015df0a784cfcd8538b3c3 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Tue, 18 Nov 2025 22:59:05 -0700 Subject: [PATCH 37/66] Fix needle and implement needle hole. aka needhole --- .../src/gauge/gaugeMarkUtils.ts | 33 ++++++++++++++++--- .../src/gauge/gaugeSpecBuilder.ts | 2 +- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 835a3eda0..d9654bf11 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -12,7 +12,7 @@ import { produce } from 'immer'; import { Mark } from 'vega'; -import { DEFAULT_COLOR_SCHEME } from '@spectrum-charts/constants'; +import { DEFAULT_COLOR_SCHEME, BACKGROUND_COLOR } from '@spectrum-charts/constants'; import { GaugeSpecOptions } from '../types'; import { spectrumColors } from '@spectrum-charts/themes'; @@ -36,7 +36,8 @@ export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => // Needle to clampedValue if (needle) { - marks?.push(getNeedle(name)); + marks.push(getNeedle(name)); + marks.push(getNeedleHole(name, BACKGROUND_COLOR)); } }); @@ -82,7 +83,7 @@ export function getFillerArc(name: string, fillerColorSignal: string): Mark { }; } - export function getNeedle(name: string): Mark { +export function getNeedle(name: string): Mark { return { name: `${name}Needle`, description: 'Needle (rule)', @@ -95,7 +96,7 @@ export function getFillerArc(name: string, fillerColorSignal: string): Mark { update: { shape: { signal: - "'M -5 0 Q -5 5 0 5 Q 5 5 5 0' + 'L 2.5 -30 ' + 'Q 2.5 -35 0 -35 Q -2.5 -35 -2.5 -30' + 'L -5 0 Z'" + "'M -4 0 A 4 4 0 1 0 4 0 L 2 -' + needleLength + 'A 2 2 0 1 0 -2 -' + needleLength + ' ' + 'L -4 0 Z'" }, angle: { signal: "needleAngleDeg" }, fill: { signal: "fillerColorToCurrVal" }, @@ -104,3 +105,27 @@ export function getFillerArc(name: string, fillerColorSignal: string): Mark { } }; } + +export function getNeedleHole(name: string, backgroundColor): Mark { + return { + name: `${name}Needle Hole`, + description: 'Needle Hole (rule)', + type: 'symbol', + encode: { + enter: { + x: { signal: "centerX" }, + y: { signal: "centerY" } + }, + update: { + shape: { + value: + "circle" + }, + angle: { signal: "needleAngleDeg" }, + size: {"value": 750}, + fill: { signal: backgroundColor }, + stroke: { signal: backgroundColor }, + } + } + }; +} \ No newline at end of file diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index edb0a15e3..90dd04baf 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -79,7 +79,7 @@ export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, option signals.push({ name: 'innerRadius', update: "outerRadius - (radiusRef * 0.25)"}) signals.push({ name: 'needleAngle', update: "needleAngleOriginal - PI/2"}) signals.push({ name: 'needleAngleOriginal', update: "scale('angleScale', clampedVal)"}) - signals.push({ name: 'needleLength', update: "innerRadius"}) + signals.push({ name: 'needleLength', update: "30"}) signals.push({ name: 'needleTipX', update: "centerX + needleLength * cos(needleAngle)"}) signals.push({ name: 'needleTipY', update: "centerY + needleLength * sin(needleAngle)"}) signals.push({ name: 'outerRadius', update: "radiusRef * 0.95"}) From 824a0dab72bcec231b59321bf5251eaa7397f7c1 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Tue, 18 Nov 2025 23:31:19 -0700 Subject: [PATCH 38/66] Implement skeleton target goal line --- packages/constants/constants.ts | 2 +- .../src/alpha/components/Gauge/Gauge.tsx | 4 ++- .../src/gauge/gaugeMarkUtils.ts | 35 ++++++++++++++++--- .../src/gauge/gaugeSpecBuilder.ts | 7 ++-- .../src/types/marks/gaugeSpec.types.ts | 3 ++ 5 files changed, 42 insertions(+), 9 deletions(-) diff --git a/packages/constants/constants.ts b/packages/constants/constants.ts index 3093f9d63..e93ef8275 100644 --- a/packages/constants/constants.ts +++ b/packages/constants/constants.ts @@ -18,7 +18,6 @@ export const DEFAULT_AXIS_ANNOTATION_COLOR = 'gray-600'; export const DEFAULT_AXIS_ANNOTATION_OFFSET = 80; export const DEFAULT_BACKGROUND_COLOR = 'transparent'; export const DEFAULT_BULLET_DIRECTION = 'column'; -export const DEFAULT_GAUGE_DIRECTION = 'column'; export const DEFAULT_CATEGORICAL_DIMENSION = 'category'; export const DEFAULT_COLOR = 'series'; export const DEFAULT_COLOR_SCHEME = 'light'; @@ -39,6 +38,7 @@ export const DEFAULT_METRIC = 'value'; export const DEFAULT_MAX_ARC_VALUE = 100; export const DEFAULT_MIN_ARC_VALUE = 0; export const DEFAULT_NEEDLE = false; +export const DEFAULT_TARGET_LINE = false; export const DEFAULT_SCALE_TYPE = 'normal'; export const DEFAULT_SCALE_VALUE = 100; export const DEFAULT_SECONDARY_COLOR = 'subSeries'; diff --git a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx index f50bf0352..4acc4363b 100644 --- a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx +++ b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx @@ -16,7 +16,8 @@ import { FC } from 'react'; import { DEFAULT_MAX_ARC_VALUE, DEFAULT_MIN_ARC_VALUE, - DEFAULT_NEEDLE + DEFAULT_NEEDLE, + DEFAULT_TARGET_LINE } from '@spectrum-charts/constants'; import { GaugeProps } from '../../../types'; @@ -28,6 +29,7 @@ const Gauge: FC<GaugeProps> = ({ minArcValue = DEFAULT_MIN_ARC_VALUE, // Min Arc Value maxArcValue = DEFAULT_MAX_ARC_VALUE, // Max Arc Value needle = DEFAULT_NEEDLE, + targetLine = DEFAULT_TARGET_LINE }) => { return null; }; diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index d9654bf11..7afdfc7e3 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -22,7 +22,8 @@ export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => const { name, colorScheme = DEFAULT_COLOR_SCHEME, - needle + needle, + targetLine } = opt; const backgroundFill = spectrumColors[colorScheme]['gray-200']; const backgroundStroke = spectrumColors[colorScheme]['gray-300']; @@ -39,6 +40,9 @@ export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => marks.push(getNeedle(name)); marks.push(getNeedleHole(name, BACKGROUND_COLOR)); } + if (targetLine){ + marks.push(getTargetLine(name)); + } }); export function getBackgroundArc(name: string, fill: string, stroke: string): Mark { @@ -86,7 +90,7 @@ export function getFillerArc(name: string, fillerColorSignal: string): Mark { export function getNeedle(name: string): Mark { return { name: `${name}Needle`, - description: 'Needle (rule)', + description: 'Needle', type: 'symbol', encode: { enter: { @@ -109,7 +113,7 @@ export function getNeedle(name: string): Mark { export function getNeedleHole(name: string, backgroundColor): Mark { return { name: `${name}Needle Hole`, - description: 'Needle Hole (rule)', + description: 'Needle Hole', type: 'symbol', encode: { enter: { @@ -122,10 +126,31 @@ export function getNeedleHole(name: string, backgroundColor): Mark { "circle" }, angle: { signal: "needleAngleDeg" }, - size: {"value": 750}, + size: {value: 750}, fill: { signal: backgroundColor }, stroke: { signal: backgroundColor }, } } }; -} \ No newline at end of file +} + +export function getTargetLine(name: string): Mark { + return { + name: `${name}Target Line`, + description: 'Target Line', + type: 'rule', + encode: { + enter: { + stroke: { value: "black" }, + strokeWidth: { value: 6 }, + strokeCap: { value: "round" } + }, + update: { + x: { signal: "needleTipX" }, + y: { signal: "needleTipY" }, + x2: { signal: "needleTipX2" }, + y2: { signal: "needleTipY2" } + } + } + } +}; diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index 90dd04baf..905bc4bd5 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -55,6 +55,7 @@ export const addGauge = produce< metric: 'currentAmount', name: toCamelCase(name ?? `gauge${index}`), needle: false, + targetLine: false, ...options, }; @@ -80,8 +81,10 @@ export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, option signals.push({ name: 'needleAngle', update: "needleAngleOriginal - PI/2"}) signals.push({ name: 'needleAngleOriginal', update: "scale('angleScale', clampedVal)"}) signals.push({ name: 'needleLength', update: "30"}) - signals.push({ name: 'needleTipX', update: "centerX + needleLength * cos(needleAngle)"}) - signals.push({ name: 'needleTipY', update: "centerY + needleLength * sin(needleAngle)"}) + signals.push({ name: 'needleTipX', update: "centerX + ( innerRadius - 5) * cos(needleAngle)"}) + signals.push({ name: 'needleTipY', update: "centerY + ( innerRadius - 5) * sin(needleAngle)"}) + signals.push({ name: 'needleTipX2', update: "centerX + ( outerRadius + 5) * cos(needleAngle)"}) + signals.push({ name: 'needleTipY2', update: "centerY + ( outerRadius + 5) * sin(needleAngle)"}) signals.push({ name: 'outerRadius', update: "radiusRef * 0.95"}) signals.push({ name: 'radiusRef', update: "min(width/2, height/2)"}) signals.push({ name: 'startAngle', update: "-PI * 2 / 3" }); // -120 degrees diff --git a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts index 1bf69a4d2..ed417a121 100644 --- a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts +++ b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts @@ -33,6 +33,8 @@ export interface GaugeOptions { fillerColorSignal?: string; /** Showing the needle mark */ needle?: boolean; + /** Showing the target line */ + targetLine?: boolean; } type GaugeOptionsWithDefaults = @@ -45,6 +47,7 @@ type GaugeOptionsWithDefaults = | 'backgroundStroke' | 'fillerColorSignal' | 'needle' + | 'targetLine' export interface GaugeSpecOptions extends PartiallyRequired<GaugeOptions, GaugeOptionsWithDefaults> { colorScheme: ColorScheme; From 544d3335c1b4189e099d97c3e376d8c8e0c7c113 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Thu, 20 Nov 2025 20:46:48 -0700 Subject: [PATCH 39/66] Race to see who fixes X2 and Y2 for TargetLine first. Good luck --- .../src/alpha/components/Gauge/Gauge.tsx | 11 +++---- .../src/gauge/gaugeMarkUtils.ts | 11 ++++--- .../src/gauge/gaugeSpecBuilder.ts | 29 ++++++++++--------- .../src/types/marks/gaugeSpec.types.ts | 4 +++ 4 files changed, 31 insertions(+), 24 deletions(-) diff --git a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx index 4acc4363b..1eb5e189b 100644 --- a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx +++ b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx @@ -17,7 +17,7 @@ import { DEFAULT_MAX_ARC_VALUE, DEFAULT_MIN_ARC_VALUE, DEFAULT_NEEDLE, - DEFAULT_TARGET_LINE + DEFAULT_TARGET_LINE, } from '@spectrum-charts/constants'; import { GaugeProps } from '../../../types'; @@ -25,11 +25,12 @@ import { GaugeProps } from '../../../types'; // I assume this houses all the props for all variations of a Gauge chart? const Gauge: FC<GaugeProps> = ({ name = 'gauge0', - metric = 'currentAmount', // CurrVal - minArcValue = DEFAULT_MIN_ARC_VALUE, // Min Arc Value - maxArcValue = DEFAULT_MAX_ARC_VALUE, // Max Arc Value + metric = 'currentAmount', + minArcValue = DEFAULT_MIN_ARC_VALUE, + maxArcValue = DEFAULT_MAX_ARC_VALUE, needle = DEFAULT_NEEDLE, - targetLine = DEFAULT_TARGET_LINE + targetLine = DEFAULT_TARGET_LINE, + target = 'target', }) => { return null; }; diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 7afdfc7e3..9fcfbf2d1 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -106,7 +106,7 @@ export function getNeedle(name: string): Mark { fill: { signal: "fillerColorToCurrVal" }, stroke: { signal: "fillerColorToCurrVal" }, } - } + } }; } @@ -125,7 +125,6 @@ export function getNeedleHole(name: string, backgroundColor): Mark { value: "circle" }, - angle: { signal: "needleAngleDeg" }, size: {value: 750}, fill: { signal: backgroundColor }, stroke: { signal: backgroundColor }, @@ -146,10 +145,10 @@ export function getTargetLine(name: string): Mark { strokeCap: { value: "round" } }, update: { - x: { signal: "needleTipX" }, - y: { signal: "needleTipY" }, - x2: { signal: "needleTipX2" }, - y2: { signal: "needleTipY2" } + x: { signal: "targetLineX" }, + y: { signal: "targetLineY" }, + x2: { signal: "targetLineX2" }, + y2: { signal: "targetLineY2" } } } } diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index 905bc4bd5..a556a3b8f 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -53,6 +53,7 @@ export const addGauge = produce< maxArcValue: 100, minArcValue: 0, metric: 'currentAmount', + target: 'target', name: toCamelCase(name ?? `gauge${index}`), needle: false, targetLine: false, @@ -68,28 +69,30 @@ export const addGauge = produce< ); export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, options) => { - signals.push({ name: 'arcMaxVal', value: options.maxArcValue }); - signals.push({ name: 'arcMinVal', value: options.minArcValue }); - signals.push({ name: 'backgroundfillColor', value: `${options.backgroundFill}`}); + signals.push({ name: 'arcMaxVal', value: options.maxArcValue }) + signals.push({ name: 'arcMinVal', value: options.minArcValue }) + signals.push({ name: 'backgroundfillColor', value: `${options.backgroundFill}`}) signals.push({ name: 'centerX', update: "width/2"}) signals.push({ name: 'centerY', update: "height/2 + outerRadius/2"}) signals.push({ name: 'clampedVal', update: "min(max(arcMinVal, currVal), arcMaxVal)"}) - signals.push({ name: 'currVal', update: `data('table')[0].${options.metric}` }); - signals.push({ name: 'endAngle', update: "PI * 2 / 3" }); // 120 degrees + signals.push({ name: 'currVal', update: `data('table')[0].${options.metric}` }) + signals.push({ name: 'endAngle', update: "PI * 2 / 3" }); signals.push({ name: 'fillerColorToCurrVal', value: `${options.color}`}) signals.push({ name: 'innerRadius', update: "outerRadius - (radiusRef * 0.25)"}) - signals.push({ name: 'needleAngle', update: "needleAngleOriginal - PI/2"}) - signals.push({ name: 'needleAngleOriginal', update: "scale('angleScale', clampedVal)"}) + signals.push({ name: 'needleAngleTarget', update: "needleAngleTargetVal - PI/2"}) + signals.push({ name: 'needleAngleDeg', update: "needleAngleClampedVal * 180 / PI"}) + signals.push({ name: 'needleAngleClampedVal', update: "scale('angleScale', clampedVal)"}) + signals.push({ name: 'needleAngleTargetVal', update: "scale('angleScale', target)"}) signals.push({ name: 'needleLength', update: "30"}) - signals.push({ name: 'needleTipX', update: "centerX + ( innerRadius - 5) * cos(needleAngle)"}) - signals.push({ name: 'needleTipY', update: "centerY + ( innerRadius - 5) * sin(needleAngle)"}) - signals.push({ name: 'needleTipX2', update: "centerX + ( outerRadius + 5) * cos(needleAngle)"}) - signals.push({ name: 'needleTipY2', update: "centerY + ( outerRadius + 5) * sin(needleAngle)"}) + signals.push({ name: 'target', value: `data('table')[0].${options.target}`}) + signals.push({ name: 'targetLineX', update: "centerX + ( innerRadius - 5) * cos(needleAngleTarget)"}) + signals.push({ name: 'targetLineY', update: "centerY + ( innerRadius - 5) * sin(needleAngleTarget)"}) + signals.push({ name: 'targetLineX2', update: "centerX + ( outerRadius + 5) * cos(needleAngleTarget)"}) + signals.push({ name: 'targetLineY2', update: "centerY + ( outerRadius + 5) * sin(needleAngleTarget)"}) signals.push({ name: 'outerRadius', update: "radiusRef * 0.95"}) signals.push({ name: 'radiusRef', update: "min(width/2, height/2)"}) - signals.push({ name: 'startAngle', update: "-PI * 2 / 3" }); // -120 degrees + signals.push({ name: 'startAngle', update: "-PI * 2 / 3" }) signals.push({ name: 'theta', update: "scale('angleScale', clampedVal)"}) - signals.push({ name: 'needleAngleDeg', update: "needleAngleOriginal * 180 / PI"}) }); export const addScales = produce<Scale[], [GaugeSpecOptions]>((scales, options) => { diff --git a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts index ed417a121..6a45d8db7 100644 --- a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts +++ b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts @@ -33,8 +33,11 @@ export interface GaugeOptions { fillerColorSignal?: string; /** Showing the needle mark */ needle?: boolean; + /** Key in the data that is used as the target */ + target?: string; /** Showing the target line */ targetLine?: boolean; + } type GaugeOptionsWithDefaults = @@ -47,6 +50,7 @@ type GaugeOptionsWithDefaults = | 'backgroundStroke' | 'fillerColorSignal' | 'needle' + | 'target' | 'targetLine' export interface GaugeSpecOptions extends PartiallyRequired<GaugeOptions, GaugeOptionsWithDefaults> { From 977b0fd2313e90a0a7d951f4ac79872ee68cf742 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Thu, 20 Nov 2025 20:54:29 -0700 Subject: [PATCH 40/66] Fixed it. I won -Dani --- .../vega-spec-builder/src/gauge/gaugeMarkUtils.ts | 4 ++-- .../vega-spec-builder/src/gauge/gaugeSpecBuilder.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 9fcfbf2d1..df45cef1a 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -47,8 +47,8 @@ export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => export function getBackgroundArc(name: string, fill: string, stroke: string): Mark { return { - name: `${name}BackgroundArcRounded`, - description: 'Background Arc (Round Edge)', + name: `${name}BackgroundArc`, + description: 'Background Arc', type: 'arc', encode: { enter: { diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index a556a3b8f..63ab741b5 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -76,7 +76,7 @@ export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, option signals.push({ name: 'centerY', update: "height/2 + outerRadius/2"}) signals.push({ name: 'clampedVal', update: "min(max(arcMinVal, currVal), arcMaxVal)"}) signals.push({ name: 'currVal', update: `data('table')[0].${options.metric}` }) - signals.push({ name: 'endAngle', update: "PI * 2 / 3" }); + signals.push({ name: 'endAngle', update: "PI * 2 / 3" }) signals.push({ name: 'fillerColorToCurrVal', value: `${options.color}`}) signals.push({ name: 'innerRadius', update: "outerRadius - (radiusRef * 0.25)"}) signals.push({ name: 'needleAngleTarget', update: "needleAngleTargetVal - PI/2"}) @@ -84,15 +84,15 @@ export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, option signals.push({ name: 'needleAngleClampedVal', update: "scale('angleScale', clampedVal)"}) signals.push({ name: 'needleAngleTargetVal', update: "scale('angleScale', target)"}) signals.push({ name: 'needleLength', update: "30"}) - signals.push({ name: 'target', value: `data('table')[0].${options.target}`}) - signals.push({ name: 'targetLineX', update: "centerX + ( innerRadius - 5) * cos(needleAngleTarget)"}) - signals.push({ name: 'targetLineY', update: "centerY + ( innerRadius - 5) * sin(needleAngleTarget)"}) - signals.push({ name: 'targetLineX2', update: "centerX + ( outerRadius + 5) * cos(needleAngleTarget)"}) - signals.push({ name: 'targetLineY2', update: "centerY + ( outerRadius + 5) * sin(needleAngleTarget)"}) signals.push({ name: 'outerRadius', update: "radiusRef * 0.95"}) signals.push({ name: 'radiusRef', update: "min(width/2, height/2)"}) signals.push({ name: 'startAngle', update: "-PI * 2 / 3" }) signals.push({ name: 'theta', update: "scale('angleScale', clampedVal)"}) + signals.push({ name: 'target', update: `data('table')[0].${options.target}`}) + signals.push({ name: 'targetLineX', update: "centerX + ( innerRadius - 5) * cos(needleAngleTarget)"}) + signals.push({ name: 'targetLineY', update: "centerY + ( innerRadius - 5) * sin(needleAngleTarget)"}) + signals.push({ name: 'targetLineX2', update: "centerX + ( outerRadius + 5) * cos(needleAngleTarget)"}) + signals.push({ name: 'targetLineY2', update: "centerY + ( outerRadius + 5) * sin(needleAngleTarget)"}) }); export const addScales = produce<Scale[], [GaugeSpecOptions]>((scales, options) => { From e2abec1eb51e6eb3102f874510a1313bd93607b0 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Thu, 20 Nov 2025 21:17:50 -0700 Subject: [PATCH 41/66] Temp color for target line --- packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index df45cef1a..4dabf6b89 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -145,6 +145,7 @@ export function getTargetLine(name: string): Mark { strokeCap: { value: "round" } }, update: { + stroke: { signal: "fillerColorToCurrVal" }, x: { signal: "targetLineX" }, y: { signal: "targetLineY" }, x2: { signal: "targetLineX2" }, From bdc0848e9936c33802cb9bb3c1b0326f36d78e70 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Fri, 21 Nov 2025 01:11:40 -0700 Subject: [PATCH 42/66] Make the target line change color depending on colorScheme --- .../src/gauge/gaugeMarkUtils.ts | 76 +++++++++---------- .../src/gauge/gaugeSpecBuilder.ts | 1 + .../src/gauge/gaugeTestUtils.ts | 5 +- 3 files changed, 42 insertions(+), 40 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 4dabf6b89..bba3df021 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -15,7 +15,7 @@ import { Mark } from 'vega'; import { DEFAULT_COLOR_SCHEME, BACKGROUND_COLOR } from '@spectrum-charts/constants'; import { GaugeSpecOptions } from '../types'; -import { spectrumColors } from '@spectrum-charts/themes'; +import { spectrumColors, getColorValue } from '@spectrum-charts/themes'; import { defaultGaugeOptions } from './gaugeTestUtils'; export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => { @@ -93,19 +93,19 @@ export function getNeedle(name: string): Mark { description: 'Needle', type: 'symbol', encode: { - enter: { - x: { signal: "centerX" }, - y: { signal: "centerY" } - }, - update: { - shape: { - signal: - "'M -4 0 A 4 4 0 1 0 4 0 L 2 -' + needleLength + 'A 2 2 0 1 0 -2 -' + needleLength + ' ' + 'L -4 0 Z'" - }, - angle: { signal: "needleAngleDeg" }, - fill: { signal: "fillerColorToCurrVal" }, - stroke: { signal: "fillerColorToCurrVal" }, - } + enter: { + x: { signal: "centerX" }, + y: { signal: "centerY" } + }, + update: { + shape: { + signal: + "'M -4 0 A 4 4 0 1 0 4 0 L 2 -' + needleLength + 'A 2 2 0 1 0 -2 -' + needleLength + ' ' + 'L -4 0 Z'" + }, + angle: { signal: "needleAngleDeg" }, + fill: { signal: "fillerColorToCurrVal" }, + stroke: { signal: "fillerColorToCurrVal" }, + } } }; } @@ -116,41 +116,39 @@ export function getNeedleHole(name: string, backgroundColor): Mark { description: 'Needle Hole', type: 'symbol', encode: { - enter: { - x: { signal: "centerX" }, - y: { signal: "centerY" } - }, - update: { - shape: { - value: - "circle" - }, - size: {value: 750}, - fill: { signal: backgroundColor }, - stroke: { signal: backgroundColor }, - } + enter: { + x: { signal: "centerX" }, + y: { signal: "centerY" } + }, + update: { + shape: { value: "circle" }, + size: { value: 750 }, + fill: { signal: backgroundColor }, + stroke: { signal: backgroundColor }, + } } }; } export function getTargetLine(name: string): Mark { + const targetColor = getColorValue('gray-900', defaultGaugeOptions.colorScheme); return { name: `${name}Target Line`, description: 'Target Line', type: 'rule', encode: { - enter: { - stroke: { value: "black" }, - strokeWidth: { value: 6 }, - strokeCap: { value: "round" } - }, - update: { - stroke: { signal: "fillerColorToCurrVal" }, - x: { signal: "targetLineX" }, - y: { signal: "targetLineY" }, - x2: { signal: "targetLineX2" }, - y2: { signal: "targetLineY2" } + enter: { + stroke: { signal: 'targetLineStroke' }, + strokeWidth: { value: 6 }, + strokeCap: { value: "round" } + }, + update: { + stroke: { signal: 'targetLineStroke' }, + x: { signal: "targetLineX" }, + y: { signal: "targetLineY" }, + x2: { signal: "targetLineX2" }, + y2: { signal: "targetLineY2" } + } } } - } }; diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index 63ab741b5..9018e7215 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -89,6 +89,7 @@ export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, option signals.push({ name: 'startAngle', update: "-PI * 2 / 3" }) signals.push({ name: 'theta', update: "scale('angleScale', clampedVal)"}) signals.push({ name: 'target', update: `data('table')[0].${options.target}`}) + signals.push({ name: 'targetLineStroke', value: getColorValue('gray-900', options.colorScheme)}) signals.push({ name: 'targetLineX', update: "centerX + ( innerRadius - 5) * cos(needleAngleTarget)"}) signals.push({ name: 'targetLineY', update: "centerY + ( innerRadius - 5) * sin(needleAngleTarget)"}) signals.push({ name: 'targetLineX2', update: "centerX + ( outerRadius + 5) * cos(needleAngleTarget)"}) diff --git a/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts index 79787257f..17ae574aa 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts @@ -25,5 +25,8 @@ export const defaultGaugeOptions: GaugeSpecOptions = { maxArcValue: 100, backgroundFill: spectrumColors[DEFAULT_COLOR_SCHEME]['gray-200'], backgroundStroke: spectrumColors[DEFAULT_COLOR_SCHEME]['gray-300'], - fillerColorSignal: 'light' + fillerColorSignal: 'light', + needle: false, + target: 'target', + targetLine: false }; From 54532f0fe21c3f15e74aa341d59e3d09f817b7d4 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Mon, 24 Nov 2025 15:12:27 -0700 Subject: [PATCH 43/66] Add dynamic label --- packages/constants/constants.ts | 1 + .../src/alpha/components/Gauge/Gauge.tsx | 4 +- .../src/gauge/gaugeMarkUtils.ts | 65 ++++++++++++++++++- .../src/gauge/gaugeSpecBuilder.ts | 3 + .../src/gauge/gaugeTestUtils.ts | 2 + .../src/types/marks/gaugeSpec.types.ts | 6 ++ 6 files changed, 79 insertions(+), 2 deletions(-) diff --git a/packages/constants/constants.ts b/packages/constants/constants.ts index e93ef8275..bd52d76a9 100644 --- a/packages/constants/constants.ts +++ b/packages/constants/constants.ts @@ -26,6 +26,7 @@ export const DEFAULT_FONT_SIZE = 14; export const DEFAULT_FONT_COLOR = 'gray-800'; export const DEFAULT_GRANULARITY = 'day'; export const DEFAULT_HIDDEN_SERIES = []; +export const DEFAULT_LABEL = false; export const DEFAULT_LABEL_ALIGN = 'center'; export const DEFAULT_LABEL_FONT_WEIGHT = 'normal'; export const DEFAULT_LABEL_ORIENTATION = 'horizontal'; diff --git a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx index 1eb5e189b..0e776e1d1 100644 --- a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx +++ b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx @@ -16,15 +16,17 @@ import { FC } from 'react'; import { DEFAULT_MAX_ARC_VALUE, DEFAULT_MIN_ARC_VALUE, + DEFAULT_LABEL, DEFAULT_NEEDLE, DEFAULT_TARGET_LINE, } from '@spectrum-charts/constants'; import { GaugeProps } from '../../../types'; -// I assume this houses all the props for all variations of a Gauge chart? const Gauge: FC<GaugeProps> = ({ name = 'gauge0', + graphLabel = 'graphLabel', + showLabel = DEFAULT_LABEL, metric = 'currentAmount', minArcValue = DEFAULT_MIN_ARC_VALUE, maxArcValue = DEFAULT_MAX_ARC_VALUE, diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index bba3df021..b1ec43b44 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -23,6 +23,7 @@ export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => name, colorScheme = DEFAULT_COLOR_SCHEME, needle, + showLabel, targetLine } = opt; const backgroundFill = spectrumColors[colorScheme]['gray-200']; @@ -36,9 +37,27 @@ export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => marks.push(getFillerArc(name, fillerColorSignal)); // Needle to clampedValue - if (needle) { + if (needle && showLabel) { marks.push(getNeedle(name)); marks.push(getNeedleHole(name, BACKGROUND_COLOR)); + const yOffset = 120; + const fontSize = 28; + marks.push(getLabel(name, fontSize, yOffset)); + + const labelYOffset = 80; + const labelFontSize = 36; + marks.push(getValueLabel(name, labelFontSize, labelYOffset)); + } else if (needle){ + marks.push(getNeedle(name)); + marks.push(getNeedleHole(name, BACKGROUND_COLOR)); + } else if (showLabel){ + const yOffset = 40; + const fontSize = 32; + marks.push(getLabel(name, fontSize, yOffset)); + + const labelYOffset = 0; + const labelFontSize = 48; + marks.push(getValueLabel(name, labelFontSize, labelYOffset)); } if (targetLine){ marks.push(getTargetLine(name)); @@ -152,3 +171,47 @@ export function getTargetLine(name: string): Mark { } } }; + +export function getLabel(name: string, fontSize, yOffset): Mark { + const targetColor = getColorValue('gray-600', defaultGaugeOptions.colorScheme); + return { + name: `${name}graphLabelText`, + description: `graph label`, + type: `text`, + encode: { + enter: { + align: { value: "center" }, + baseline: { value: "middle" }, + fontSize: { value: fontSize }, + fill: { value: targetColor } + }, + update: { + x: { signal: "centerX" }, + y: { signal: `centerY + ${yOffset}` }, + text: { signal: "graphLabel" } + } + } + } +}; + +export function getValueLabel(name: string, fontSize, yOffset): Mark { + const targetColor = getColorValue('gray-900', defaultGaugeOptions.colorScheme); + return { + name: `${name}graphLabelCurrentValueText`, + description: `graph current value label`, + type: `text`, + encode: { + enter: { + align: { value: "center" }, + baseline: { value: "middle" }, + fontSize: { value: fontSize }, + fill: { value: targetColor } + }, + update: { + x: { signal: "centerX" }, + y: { signal: `centerY + ${yOffset}` }, + text: { signal: "currVal" } + } + } + } +}; \ No newline at end of file diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index 9018e7215..a8e9c3a01 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -48,6 +48,8 @@ export const addGauge = produce< backgroundStroke: spectrumColors[colorScheme]['gray-300'], color: getColorValue(color, colorScheme), fillerColorSignal: 'fillerColorToCurrVal', + graphLabel: 'graphLabel', + showLabel: false, colorScheme: colorScheme, index, maxArcValue: 100, @@ -78,6 +80,7 @@ export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, option signals.push({ name: 'currVal', update: `data('table')[0].${options.metric}` }) signals.push({ name: 'endAngle', update: "PI * 2 / 3" }) signals.push({ name: 'fillerColorToCurrVal', value: `${options.color}`}) + signals.push({ name: 'graphLabel', update: `data('table')[0].${options.graphLabel}` }) signals.push({ name: 'innerRadius', update: "outerRadius - (radiusRef * 0.25)"}) signals.push({ name: 'needleAngleTarget', update: "needleAngleTargetVal - PI/2"}) signals.push({ name: 'needleAngleDeg', update: "needleAngleClampedVal * 180 / PI"}) diff --git a/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts index 17ae574aa..50904399c 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts @@ -19,6 +19,8 @@ export const defaultGaugeOptions: GaugeSpecOptions = { idKey: MARK_ID, index: 5, name: 'gaugeTestName', + graphLabel: 'graphLabel', + showLabel: false, metric: 'currentAmount', color: DEFAULT_COLOR_SCHEME, minArcValue: 0, diff --git a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts index 6a45d8db7..b125c4dd0 100644 --- a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts +++ b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts @@ -17,6 +17,10 @@ export type ThresholdBackground = { thresholdMin?: number; thresholdMax?: number export interface GaugeOptions { /** Sets the name of the component. */ name?: string; + /** Key in the data that is used as the graph label */ + graphLabel?: string; + /** Sets to show the label or not */ + showLabel?: boolean; /** Key in the data that is used as the metric */ metric?: string; /** Key in the data that is used as the color facet */ @@ -42,6 +46,8 @@ export interface GaugeOptions { type GaugeOptionsWithDefaults = | 'name' + | 'graphLabel' + | 'showLabel' | 'metric' | 'color' | 'minArcValue' From a784373aec58e2bef11ba53f19a880a6bdbe3e58 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Mon, 24 Nov 2025 15:32:34 -0700 Subject: [PATCH 44/66] Implement percentage option --- .../react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx | 1 + packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts | 2 +- packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts | 3 +++ packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts | 1 + packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts | 3 +++ 5 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx index 0e776e1d1..4ad300073 100644 --- a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx +++ b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx @@ -27,6 +27,7 @@ const Gauge: FC<GaugeProps> = ({ name = 'gauge0', graphLabel = 'graphLabel', showLabel = DEFAULT_LABEL, + showsAsPercent = false, metric = 'currentAmount', minArcValue = DEFAULT_MIN_ARC_VALUE, maxArcValue = DEFAULT_MAX_ARC_VALUE, diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index b1ec43b44..6d199321f 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -210,7 +210,7 @@ export function getValueLabel(name: string, fontSize, yOffset): Mark { update: { x: { signal: "centerX" }, y: { signal: `centerY + ${yOffset}` }, - text: { signal: "currVal" } + text: { signal: "textSignal" } } } } diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index a8e9c3a01..90db7d58e 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -49,6 +49,7 @@ export const addGauge = produce< color: getColorValue(color, colorScheme), fillerColorSignal: 'fillerColorToCurrVal', graphLabel: 'graphLabel', + showsAsPercent: false, showLabel: false, colorScheme: colorScheme, index, @@ -97,6 +98,8 @@ export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, option signals.push({ name: 'targetLineY', update: "centerY + ( innerRadius - 5) * sin(needleAngleTarget)"}) signals.push({ name: 'targetLineX2', update: "centerX + ( outerRadius + 5) * cos(needleAngleTarget)"}) signals.push({ name: 'targetLineY2', update: "centerY + ( outerRadius + 5) * sin(needleAngleTarget)"}) + signals.push({ name: 'showAsPercent', update: `${options.showsAsPercent}`}) + signals.push({ name: 'textSignal', update: "showAsPercent ? format((currVal / arcMaxVal) * 100, '.2f') + '%' : format(currVal, '.0f')"}) }); export const addScales = produce<Scale[], [GaugeSpecOptions]>((scales, options) => { diff --git a/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts index 50904399c..34b8cc44a 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts @@ -21,6 +21,7 @@ export const defaultGaugeOptions: GaugeSpecOptions = { name: 'gaugeTestName', graphLabel: 'graphLabel', showLabel: false, + showsAsPercent: false, metric: 'currentAmount', color: DEFAULT_COLOR_SCHEME, minArcValue: 0, diff --git a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts index b125c4dd0..b99ef3796 100644 --- a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts +++ b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts @@ -21,6 +21,8 @@ export interface GaugeOptions { graphLabel?: string; /** Sets to show the label or not */ showLabel?: boolean; + /** Sets to show the current value as a percentage or not */ + showsAsPercent?: boolean; /** Key in the data that is used as the metric */ metric?: string; /** Key in the data that is used as the color facet */ @@ -48,6 +50,7 @@ type GaugeOptionsWithDefaults = | 'name' | 'graphLabel' | 'showLabel' + | 'showsAsPercent' | 'metric' | 'color' | 'minArcValue' From 7834fe5caa01ab06223952f15cc14b91fb5e1531 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Mon, 24 Nov 2025 15:45:15 -0700 Subject: [PATCH 45/66] Dynamic color for labels --- packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts | 6 ++---- packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts | 2 ++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 6d199321f..0083d988c 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -150,7 +150,6 @@ export function getNeedleHole(name: string, backgroundColor): Mark { } export function getTargetLine(name: string): Mark { - const targetColor = getColorValue('gray-900', defaultGaugeOptions.colorScheme); return { name: `${name}Target Line`, description: 'Target Line', @@ -183,7 +182,7 @@ export function getLabel(name: string, fontSize, yOffset): Mark { align: { value: "center" }, baseline: { value: "middle" }, fontSize: { value: fontSize }, - fill: { value: targetColor } + fill: { signal: 'labelTextColor' } }, update: { x: { signal: "centerX" }, @@ -195,7 +194,6 @@ export function getLabel(name: string, fontSize, yOffset): Mark { }; export function getValueLabel(name: string, fontSize, yOffset): Mark { - const targetColor = getColorValue('gray-900', defaultGaugeOptions.colorScheme); return { name: `${name}graphLabelCurrentValueText`, description: `graph current value label`, @@ -205,7 +203,7 @@ export function getValueLabel(name: string, fontSize, yOffset): Mark { align: { value: "center" }, baseline: { value: "middle" }, fontSize: { value: fontSize }, - fill: { value: targetColor } + fill: { signal: 'valueTextColor' } }, update: { x: { signal: "centerX" }, diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index 90db7d58e..2a60f8e5c 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -99,6 +99,8 @@ export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, option signals.push({ name: 'targetLineX2', update: "centerX + ( outerRadius + 5) * cos(needleAngleTarget)"}) signals.push({ name: 'targetLineY2', update: "centerY + ( outerRadius + 5) * sin(needleAngleTarget)"}) signals.push({ name: 'showAsPercent', update: `${options.showsAsPercent}`}) + signals.push({ name: 'valueTextColor', value: getColorValue('gray-900', options.colorScheme) }) + signals.push({ name: 'labelTextColor', value: getColorValue('gray-600', options.colorScheme) }) signals.push({ name: 'textSignal', update: "showAsPercent ? format((currVal / arcMaxVal) * 100, '.2f') + '%' : format(currVal, '.0f')"}) }); From 5833aabfce36072021fdb55442f1ca2547f1e2c3 Mon Sep 17 00:00:00 2001 From: JarekSmith <105569335+JarekSmith@users.noreply.github.com> Date: Mon, 24 Nov 2025 19:29:46 -0700 Subject: [PATCH 46/66] Add start and end caps to gauge --- .../src/gauge/gaugeMarkUtils.ts | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 9fcfbf2d1..170a9ccbe 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -34,6 +34,8 @@ export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => // Filler arc (fills to clampedValue) marks.push(getFillerArc(name, fillerColorSignal)); + marks.push(getStartCap(name, fillerColorSignal, backgroundFill)); + marks.push(getEndCap(name, fillerColorSignal, backgroundFill)); // Needle to clampedValue if (needle) { @@ -153,3 +155,45 @@ export function getTargetLine(name: string): Mark { } } }; + +export function getStartCap(name: string, fillColor: string, backgroundColor: string): Mark { + return { + name: `${name}Start Cap`, + description: `Start Cap`, + type: `arc`, + encode: { + enter: { + "x": { signal: '69' }, + "y": { signal: '468'}, + "innerRadius": { signal: '0' }, + "outerRadius": { signal: "(outerRadius - innerRadius) / 2" }, + "startAngle": { signal: "startAngle" }, + "endAngle": { signal: "startAngle-PI"}, + }, + update: { + "fill": {signal: `currVal <= arcMinVal ? ${backgroundColor} : ${fillColor}`} + } + } + } +} + +export function getEndCap(name: string, fillColor: string, backgroundColor: string): Mark { + return { + name: `${name}End Cap`, + description: `End Cap`, + type: `arc`, + encode: { + enter: { + "x": { signal: '500-69' }, + "y": { signal: '468'}, + "innerRadius": { signal: '0' }, + "outerRadius": { signal: "(outerRadius - innerRadius) / 2" }, + "startAngle": { signal: "endAngle" }, + "endAngle": { signal: "endAngle+PI"}, + }, + update: { + "fill": {signal: `currVal >= arcMaxVal ? ${fillColor} : ${backgroundColor}`} + } + } + } +} From 37073852f835edea73c94206ed7728763468b472 Mon Sep 17 00:00:00 2001 From: JarekSmith <105569335+JarekSmith@users.noreply.github.com> Date: Mon, 24 Nov 2025 20:46:37 -0700 Subject: [PATCH 47/66] Adjust x/y calculations --- .../src/gauge/gaugeMarkUtils.ts | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 170a9ccbe..25c7a7782 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -80,10 +80,12 @@ export function getFillerArc(name: string, fillerColorSignal: string): Mark { outerRadius: { signal: 'outerRadius' }, startAngle: { signal: 'startAngle' }, endAngle: { signal: 'endAngle' }, - fill: { signal: fillerColorSignal } + fill: { signal: fillerColorSignal }, + stroke: { signal: fillerColorSignal } }, update: { - endAngle: { signal: "scale('angleScale', clampedVal)" } + endAngle: { signal: "scale('angleScale', clampedVal)" }, + stroke: { signal: `currVal > arcMinVal ? ${fillerColorSignal} : ""`} } } }; @@ -157,35 +159,41 @@ export function getTargetLine(name: string): Mark { }; export function getStartCap(name: string, fillColor: string, backgroundColor: string): Mark { + const xOffset = 'centerX+(sin(startAngle)*((outerRadius+innerRadius)/2))' + const yOffset = 'centerY-(cos(startAngle)*((outerRadius+innerRadius)/2))' return { name: `${name}Start Cap`, description: `Start Cap`, type: `arc`, encode: { enter: { - "x": { signal: '69' }, - "y": { signal: '468'}, + "x": { signal: xOffset }, + "y": { signal: yOffset }, "innerRadius": { signal: '0' }, "outerRadius": { signal: "(outerRadius - innerRadius) / 2" }, "startAngle": { signal: "startAngle" }, "endAngle": { signal: "startAngle-PI"}, + "stroke": { signal: fillColor }, }, update: { - "fill": {signal: `currVal <= arcMinVal ? ${backgroundColor} : ${fillColor}`} + "fill": {signal: `currVal <= arcMinVal ? ${backgroundColor} : ${fillColor}`}, + "stroke": { signal: `currVal <= arcMinVal ? ${backgroundColor} : ${fillColor}`} } } } } export function getEndCap(name: string, fillColor: string, backgroundColor: string): Mark { + const xOffset = 'centerX+(sin(startAngle)*((outerRadius+innerRadius)/2*-1))' + const yOffset = 'centerY-(cos(startAngle)*((outerRadius+innerRadius)/2))' return { name: `${name}End Cap`, description: `End Cap`, type: `arc`, encode: { enter: { - "x": { signal: '500-69' }, - "y": { signal: '468'}, + "x": { signal: xOffset }, + "y": { signal: yOffset }, "innerRadius": { signal: '0' }, "outerRadius": { signal: "(outerRadius - innerRadius) / 2" }, "startAngle": { signal: "endAngle" }, From 898da7670e622fd5674a20e782c14e9f8a8bb066 Mon Sep 17 00:00:00 2001 From: JarekSmith <105569335+JarekSmith@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:03:23 -0700 Subject: [PATCH 48/66] Removed backgroundArc stroke to match design --- packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 25c7a7782..88a650f4f 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -61,7 +61,6 @@ export function getBackgroundArc(name: string, fill: string, stroke: string): Ma startAngle: { signal: 'startAngle' }, endAngle: { signal: 'endAngle' }, fill: { value: fill }, - stroke: { value: stroke } } } }; From 67496004391097a1c7482b0fe3603f086a240a23 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:18:41 -0700 Subject: [PATCH 49/66] Adding stories --- .../stories/components/Gauge/Gauge.story.tsx | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx index 5e2d200b1..01ba0cdcd 100644 --- a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx @@ -75,6 +75,42 @@ GaugeVariation3.args = { color: 'fuchsia-900', }; +const GaugeLabelNoNeedleNoPercent = bindWithProps(GaugeStory); +GaugeLabelNoNeedleNoPercent.args = { + metric: 'currentAmount', + color: 'indigo-1200', + showLabel: true, + needle: false, + showsAsPercent: false +}; + +const GaugeLabelNoNeedlePercent = bindWithProps(GaugeStory); +GaugeLabelNoNeedlePercent.args = { + metric: 'currentAmount', + color: 'celery-800', + maxArcValue: 151, + showLabel: true, + needle: false, + showsAsPercent: true +}; +const GaugeLabelNeedleNoPercent = bindWithProps(GaugeStory); +GaugeLabelNeedleNoPercent.args = { + metric: 'currentAmount', + color: 'cyan-700', + showLabel: true, + needle: true, + showsAsPercent: false +}; + +const GaugeLabelNeedlePercent = bindWithProps(GaugeStory); +GaugeLabelNeedlePercent.args = { + metric: 'currentAmount', + color: 'magenta-1000', + maxArcValue: 151, + showLabel: true, + needle: true, + showsAsPercent: true +}; -export { Basic, GaugeVariation2, GaugeVariation3 }; +export { Basic, GaugeVariation2, GaugeVariation3, GaugeLabelNoNeedleNoPercent, GaugeLabelNoNeedlePercent, GaugeLabelNeedleNoPercent, GaugeLabelNeedlePercent }; From 9c96edcba061484021af0c6cdfdae6e980422a9b Mon Sep 17 00:00:00 2001 From: JarekSmith <105569335+JarekSmith@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:45:32 -0700 Subject: [PATCH 50/66] Fix gaps between caps and backgroundArc --- .../src/stories/components/Gauge/Gauge.story.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx index 5e2d200b1..605c5a857 100644 --- a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx @@ -75,6 +75,16 @@ GaugeVariation3.args = { color: 'fuchsia-900', }; +const Empty = bindWithProps(GaugeStory); +Empty.args = { + metric: 'currentAmount-60' +} +const Full = bindWithProps(GaugeStory); +Full.args = { + metric: 'currentAmount+40', + color: 'static-pruple-900' +} -export { Basic, GaugeVariation2, GaugeVariation3 }; + +export { Basic, GaugeVariation2, GaugeVariation3, Empty, Full }; From cf993808dacae802942cfb6c064bb87216153720 Mon Sep 17 00:00:00 2001 From: JarekSmith <105569335+JarekSmith@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:46:53 -0700 Subject: [PATCH 51/66] Add Full and Empty stories --- .../src/gauge/gaugeMarkUtils.ts | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 88a650f4f..c96194c5b 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -30,7 +30,7 @@ export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => const fillerColorSignal = 'fillerColorToCurrVal'; // Background arc - marks.push(getBackgroundArc(name, backgroundFill, backgroundStroke)); + marks.push(getBackgroundArc(name, backgroundFill)); // Filler arc (fills to clampedValue) marks.push(getFillerArc(name, fillerColorSignal)); @@ -47,7 +47,7 @@ export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => } }); -export function getBackgroundArc(name: string, fill: string, stroke: string): Mark { +export function getBackgroundArc(name: string, fill: string): Mark { return { name: `${name}BackgroundArcRounded`, description: 'Background Arc (Round Edge)', @@ -142,19 +142,19 @@ export function getTargetLine(name: string): Mark { description: 'Target Line', type: 'rule', encode: { - enter: { - stroke: { value: "black" }, - strokeWidth: { value: 6 }, - strokeCap: { value: "round" } - }, - update: { - x: { signal: "targetLineX" }, - y: { signal: "targetLineY" }, - x2: { signal: "targetLineX2" }, - y2: { signal: "targetLineY2" } + enter: { + stroke: { value: "black" }, + strokeWidth: { value: 6 }, + strokeCap: { value: "round" } + }, + update: { + x: { signal: "targetLineX" }, + y: { signal: "targetLineY" }, + x2: { signal: "targetLineX2" }, + y2: { signal: "targetLineY2" } + } } } - } }; export function getStartCap(name: string, fillColor: string, backgroundColor: string): Mark { @@ -169,10 +169,11 @@ export function getStartCap(name: string, fillColor: string, backgroundColor: st "x": { signal: xOffset }, "y": { signal: yOffset }, "innerRadius": { signal: '0' }, - "outerRadius": { signal: "(outerRadius - innerRadius) / 2" }, + "outerRadius": { signal: "(outerRadius - innerRadius) / 2 - 1" }, "startAngle": { signal: "startAngle" }, "endAngle": { signal: "startAngle-PI"}, "stroke": { signal: fillColor }, + "strokeWidth": { signal: "2" }, }, update: { "fill": {signal: `currVal <= arcMinVal ? ${backgroundColor} : ${fillColor}`}, @@ -194,12 +195,14 @@ export function getEndCap(name: string, fillColor: string, backgroundColor: stri "x": { signal: xOffset }, "y": { signal: yOffset }, "innerRadius": { signal: '0' }, - "outerRadius": { signal: "(outerRadius - innerRadius) / 2" }, + "outerRadius": { signal: "((outerRadius - innerRadius) / 2)-1" }, "startAngle": { signal: "endAngle" }, "endAngle": { signal: "endAngle+PI"}, + "strokeWidth": { signal: "2" }, }, update: { - "fill": {signal: `currVal >= arcMaxVal ? ${fillColor} : ${backgroundColor}`} + "fill": {signal: `currVal >= arcMaxVal ? ${fillColor} : ${backgroundColor}`}, + "stroke": { signal: `currVal >= arcMaxVal ? ${fillColor} : ${backgroundColor}` } } } } From b60007d86d650cdab143d0338ea76b7ec11b4808 Mon Sep 17 00:00:00 2001 From: JarekSmith <105569335+JarekSmith@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:54:33 -0700 Subject: [PATCH 52/66] Calculate x/y from endAngle for endCap --- packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index c96194c5b..085c22ffa 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -184,8 +184,8 @@ export function getStartCap(name: string, fillColor: string, backgroundColor: st } export function getEndCap(name: string, fillColor: string, backgroundColor: string): Mark { - const xOffset = 'centerX+(sin(startAngle)*((outerRadius+innerRadius)/2*-1))' - const yOffset = 'centerY-(cos(startAngle)*((outerRadius+innerRadius)/2))' + const xOffset = 'centerX+(sin(endAngle)*((outerRadius+innerRadius)/2))' + const yOffset = 'centerY-(cos(endAngle)*((outerRadius+innerRadius)/2))' return { name: `${name}End Cap`, description: `End Cap`, From 8edb8a96094c0d32c9aa22ec3b89bd3cfc4c3b13 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:09:00 -0700 Subject: [PATCH 53/66] Arranged stories to show dynamic labeling in a better way --- .../src/stories/components/Gauge/Gauge.story.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx index 01ba0cdcd..8ea179ced 100644 --- a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx @@ -113,4 +113,12 @@ GaugeLabelNeedlePercent.args = { showsAsPercent: true }; -export { Basic, GaugeVariation2, GaugeVariation3, GaugeLabelNoNeedleNoPercent, GaugeLabelNoNeedlePercent, GaugeLabelNeedleNoPercent, GaugeLabelNeedlePercent }; +export { + Basic, + GaugeVariation2, + GaugeVariation3, + GaugeLabelNoNeedleNoPercent, + GaugeLabelNeedleNoPercent, + GaugeLabelNoNeedlePercent, + GaugeLabelNeedlePercent +}; From f7851e5f6cd636cccaed517dc452959ccab798fd Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:12:52 -0700 Subject: [PATCH 54/66] Make the text bigger --- packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 0083d988c..9bf25805e 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -40,23 +40,23 @@ export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => if (needle && showLabel) { marks.push(getNeedle(name)); marks.push(getNeedleHole(name, BACKGROUND_COLOR)); - const yOffset = 120; + const yOffset = 140; const fontSize = 28; marks.push(getLabel(name, fontSize, yOffset)); - const labelYOffset = 80; - const labelFontSize = 36; + const labelYOffset = 100; + const labelFontSize = 60; marks.push(getValueLabel(name, labelFontSize, labelYOffset)); } else if (needle){ marks.push(getNeedle(name)); marks.push(getNeedleHole(name, BACKGROUND_COLOR)); } else if (showLabel){ - const yOffset = 40; + const yOffset = 70; const fontSize = 32; marks.push(getLabel(name, fontSize, yOffset)); const labelYOffset = 0; - const labelFontSize = 48; + const labelFontSize = 84; marks.push(getValueLabel(name, labelFontSize, labelYOffset)); } if (targetLine){ From 58a95902b26d8fd2412fb30e5cd6e978faebee6d Mon Sep 17 00:00:00 2001 From: Brady Shimanek <bradyshimanek33@gmail.com> Date: Wed, 26 Nov 2025 20:18:11 -0700 Subject: [PATCH 55/66] Implement basic performance range gauge variation --- .../stories/components/Gauge/Gauge.story.tsx | 10 +-- .../src/stories/components/Gauge/data.ts | 6 +- .../src/types/marks/gauge.types.ts | 2 +- .../src/gauge/gaugeMarkUtils.ts | 69 +++++++++++++++++-- .../src/gauge/gaugeSpecBuilder.ts | 51 +++++++++++++- .../src/types/marks/gaugeSpec.types.ts | 11 ++- 6 files changed, 135 insertions(+), 14 deletions(-) diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx index 5e2d200b1..4abf8d098 100644 --- a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx @@ -20,7 +20,7 @@ import { Title } from '../../../components'; import useChartProps from '../../../hooks/useChartProps'; import { bindWithProps } from '../../../test-utils'; import { GaugeProps, ChartProps } from '../../../types'; -import { basicGaugeData } from './data'; +import { basicGaugeData, coloredPerformanceData } from './data'; export default { title: 'RSC/Gauge (alpha)', @@ -63,10 +63,12 @@ Basic.args = { color: 'blue-900', }; -const GaugeVariation2 = bindWithProps(GaugeStory); -GaugeVariation2.args = { +const PerformanceRange = bindWithProps(GaugeStory); +PerformanceRange.args = { metric: 'currentAmount', color: 'red-900', + showPerformanceRanges: true, + performanceRanges: coloredPerformanceData, }; const GaugeVariation3 = bindWithProps(GaugeStory); @@ -77,4 +79,4 @@ GaugeVariation3.args = { -export { Basic, GaugeVariation2, GaugeVariation3 }; +export { Basic, PerformanceRange, GaugeVariation3 }; diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts b/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts index 8f527fbd3..edfacf019 100644 --- a/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts @@ -16,4 +16,8 @@ export const basicGaugeData = [ { graphLabel: 'Customers', currentAmount: 60, target: 80 }, ]; - +export const coloredPerformanceData = [ + { bandEndPct: 0.55, fill: 'red-900' }, + { bandEndPct: 0.8, fill: 'yellow-400' }, + { bandEndPct: 1, fill: 'green-700' }, +]; diff --git a/packages/react-spectrum-charts/src/types/marks/gauge.types.ts b/packages/react-spectrum-charts/src/types/marks/gauge.types.ts index d9af24e5e..238c371c3 100644 --- a/packages/react-spectrum-charts/src/types/marks/gauge.types.ts +++ b/packages/react-spectrum-charts/src/types/marks/gauge.types.ts @@ -11,7 +11,7 @@ */ import { JSXElementConstructor, ReactElement } from 'react'; -import { GaugeOptions } from '@spectrum-charts/vega-spec-builder'; // TODO: update this when GaugeOptions is added +import { GaugeOptions } from '@spectrum-charts/vega-spec-builder'; export interface GaugeProps extends Omit<GaugeOptions, 'markType'> {} diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index bba3df021..8ffb8611b 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -14,7 +14,7 @@ import { Mark } from 'vega'; import { DEFAULT_COLOR_SCHEME, BACKGROUND_COLOR } from '@spectrum-charts/constants'; -import { GaugeSpecOptions } from '../types'; +import { GaugeSpecOptions, PerformanceRanges } from '../types'; import { spectrumColors, getColorValue } from '@spectrum-charts/themes'; import { defaultGaugeOptions } from './gaugeTestUtils'; @@ -32,6 +32,11 @@ export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => // Background arc marks.push(getBackgroundArc(name, backgroundFill, backgroundStroke)); + // Performance ranges + if (opt.showPerformanceRanges) { + marks.push(...getPerformanceRangeMarks(name, opt.performanceRanges)); + } + // Filler arc (fills to clampedValue) marks.push(getFillerArc(name, fillerColorSignal)); @@ -78,7 +83,7 @@ export function getFillerArc(name: string, fillerColorSignal: string): Mark { outerRadius: { signal: 'outerRadius' }, startAngle: { signal: 'startAngle' }, endAngle: { signal: 'endAngle' }, - fill: { signal: fillerColorSignal } + fill: { signal: 'fillerColorToCurrVal' } }, update: { endAngle: { signal: "scale('angleScale', clampedVal)" } @@ -103,13 +108,69 @@ export function getNeedle(name: string): Mark { "'M -4 0 A 4 4 0 1 0 4 0 L 2 -' + needleLength + 'A 2 2 0 1 0 -2 -' + needleLength + ' ' + 'L -4 0 Z'" }, angle: { signal: "needleAngleDeg" }, - fill: { signal: "fillerColorToCurrVal" }, - stroke: { signal: "fillerColorToCurrVal" }, + fill: { signal: "needleColor" }, + stroke: { signal: "needleColor" }, } } }; } +export function getPerformanceRangeMarks( + name: string, + performanceRanges: PerformanceRanges[] +): Mark[] { + const [band1, band2, band3] = performanceRanges; + return [ + { + name: `${name}Band1Arc`, + type: 'arc', + encode: { + enter: { + x: { signal: 'centerX' }, + y: { signal: 'centerY' }, + innerRadius: { signal: 'innerRadius' }, + outerRadius: { signal: 'outerRadius' }, + startAngle: { signal: 'band1StartAngle' }, + endAngle: { signal: 'band1EndAngle' }, + fill: { value: band1.fill }, + }, + }, + }, + { + name: `${name}Band2Arc`, + type: 'arc', + encode: { + enter: { + x: { signal: 'centerX' }, + y: { signal: 'centerY' }, + innerRadius: { signal: 'innerRadius' }, + outerRadius: { signal: 'outerRadius' }, + startAngle: { signal: 'band2StartAngle' }, + endAngle: { signal: 'band2EndAngle' }, + fill: { value: band2.fill }, + }, + }, + }, + { + name: `${name}Band3Arc`, + type: 'arc', + encode: { + enter: { + x: { signal: 'centerX' }, + y: { signal: 'centerY' }, + innerRadius: { signal: 'innerRadius' }, + outerRadius: { signal: 'outerRadius' }, + startAngle: { signal: 'band3StartAngle' }, + endAngle: { signal: 'band3EndAngle' }, + fill: { value: band3.fill }, + }, + }, + }, + ]; +} + + + export function getNeedleHole(name: string, backgroundColor): Mark { return { name: `${name}Needle Hole`, diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index 9018e7215..c15ccafe9 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -20,11 +20,18 @@ import { DEFAULT_COLOR_SCHEME } from '@spectrum-charts/constants'; import { getColorValue, spectrumColors } from '@spectrum-charts/themes'; import { toCamelCase } from '@spectrum-charts/utils'; -import { ColorScheme, GaugeOptions, GaugeSpecOptions, ScSpec } from '../types'; +import { ColorScheme, GaugeOptions, GaugeSpecOptions, PerformanceRanges, ScSpec } from '../types'; import { getGaugeTableData } from './gaugeDataUtils'; const DEFAULT_COLOR = spectrumColors.light['blue-900']; +const DEFAULT_PERFORMANCE_RANGES: PerformanceRanges[] = [ + { bandEndPct: 0.55, fill: spectrumColors.light['red-900'] }, + { bandEndPct: 0.8, fill: spectrumColors.light['yellow-900'] }, + { bandEndPct: 1, fill: spectrumColors.light['green-700'] }, +]; + + /** * Adds a simple Gauge chart to the spec @@ -40,9 +47,17 @@ export const addGauge = produce< colorScheme = DEFAULT_COLOR_SCHEME, index = 0, color = DEFAULT_COLOR, + performanceRanges, + showPerformanceRanges, + name, ...options } ) => { + const resolvedPerformanceRanges = + (performanceRanges ?? DEFAULT_PERFORMANCE_RANGES).map(range => ({ + ...range, + fill: getColorValue(range.fill, DEFAULT_COLOR_SCHEME), + })); const gaugeOptions: GaugeSpecOptions = { backgroundFill: spectrumColors[colorScheme]['gray-200'], backgroundStroke: spectrumColors[colorScheme]['gray-300'], @@ -57,6 +72,8 @@ export const addGauge = produce< name: toCamelCase(name ?? `gauge${index}`), needle: false, targetLine: false, + performanceRanges: resolvedPerformanceRanges, + showPerformanceRanges: showPerformanceRanges ?? false, ...options, }; @@ -69,6 +86,8 @@ export const addGauge = produce< ); export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, options) => { + const ranges = options.performanceRanges; + signals.push({ name: 'arcMaxVal', value: options.maxArcValue }) signals.push({ name: 'arcMinVal', value: options.minArcValue }) signals.push({ name: 'backgroundfillColor', value: `${options.backgroundFill}`}) @@ -77,7 +96,16 @@ export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, option signals.push({ name: 'clampedVal', update: "min(max(arcMinVal, currVal), arcMaxVal)"}) signals.push({ name: 'currVal', update: `data('table')[0].${options.metric}` }) signals.push({ name: 'endAngle', update: "PI * 2 / 3" }) - signals.push({ name: 'fillerColorToCurrVal', value: `${options.color}`}) + signals.push({ + name: 'fillerColorToCurrVal', + update: options.showPerformanceRanges ? 'null' : `"${options.color}"`, + }); + signals.push({ + name: 'needleColor', + update: options.showPerformanceRanges + ? `"${getColorValue('gray-900', options.colorScheme)}"` + : `"${options.color}"`, + }); signals.push({ name: 'innerRadius', update: "outerRadius - (radiusRef * 0.25)"}) signals.push({ name: 'needleAngleTarget', update: "needleAngleTargetVal - PI/2"}) signals.push({ name: 'needleAngleDeg', update: "needleAngleClampedVal * 180 / PI"}) @@ -94,6 +122,23 @@ export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, option signals.push({ name: 'targetLineY', update: "centerY + ( innerRadius - 5) * sin(needleAngleTarget)"}) signals.push({ name: 'targetLineX2', update: "centerX + ( outerRadius + 5) * cos(needleAngleTarget)"}) signals.push({ name: 'targetLineY2', update: "centerY + ( outerRadius + 5) * sin(needleAngleTarget)"}) + + signals.push({ name: 'band1EndPct', value: ranges[0].bandEndPct }); + signals.push({ name: 'band2EndPct', value: ranges[1].bandEndPct }); + signals.push({ name: 'band3EndPct', value: ranges[2].bandEndPct }); + signals.push({ name: 'bandRange', update: 'arcMaxVal - arcMinVal' }); + signals.push({ name: 'band1StartVal', update: 'arcMinVal' }); + signals.push({ name: 'band1EndVal', update: 'arcMinVal + bandRange * band1EndPct' }); + signals.push({ name: 'band2StartVal', update: 'band1EndVal' }); + signals.push({ name: 'band2EndVal', update: 'arcMinVal + bandRange * band2EndPct' }); + signals.push({ name: 'band3StartVal', update: 'band2EndVal' }); + signals.push({ name: 'band3EndVal', update: 'arcMaxVal' }); + signals.push({ name: 'band1StartAngle', update: "scale('angleScale', band1StartVal)" }); + signals.push({ name: 'band1EndAngle', update: "scale('angleScale', band1EndVal)" }); + signals.push({ name: 'band2StartAngle', update: "scale('angleScale', band2StartVal)" }); + signals.push({ name: 'band2EndAngle', update: "scale('angleScale', band2EndVal)" }); + signals.push({ name: 'band3StartAngle', update: "scale('angleScale', band3StartVal)" }); + signals.push({ name: 'band3EndAngle', update: "scale('angleScale', band3EndVal)" }); }); export const addScales = produce<Scale[], [GaugeSpecOptions]>((scales, options) => { @@ -108,4 +153,4 @@ export const addScales = produce<Scale[], [GaugeSpecOptions]>((scales, options) export const addData = produce<Data[], [GaugeSpecOptions]>((data, options) => { const tableData = getGaugeTableData(data); -}); \ No newline at end of file +}); diff --git a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts index 6a45d8db7..648329eda 100644 --- a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts +++ b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts @@ -12,7 +12,7 @@ import { ColorScheme } from '../chartSpec.types'; import { NumberFormat, PartiallyRequired } from '../specUtil.types'; -export type ThresholdBackground = { thresholdMin?: number; thresholdMax?: number; fill?: string }; +export type PerformanceRanges = { bandEndPct: number; fill: string }; export interface GaugeOptions { /** Sets the name of the component. */ @@ -37,6 +37,14 @@ export interface GaugeOptions { target?: string; /** Showing the target line */ targetLine?: boolean; + /** Performance ranges + * + * Array of performance ranges to be rendered as filled bands on the gauge. + * + */ + performanceRanges?: PerformanceRanges[]; + /** if true, show banded performance ranges instead of a colored filler arc */ + showPerformanceRanges?: boolean; } @@ -52,6 +60,7 @@ type GaugeOptionsWithDefaults = | 'needle' | 'target' | 'targetLine' + | 'performanceRanges' export interface GaugeSpecOptions extends PartiallyRequired<GaugeOptions, GaugeOptionsWithDefaults> { colorScheme: ColorScheme; From 241e56e7137103cd611c84d44823b98779050d52 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Wed, 26 Nov 2025 20:39:33 -0700 Subject: [PATCH 56/66] adding strokes on the three colored gauge --- packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts | 10 ++++++++++ .../vega-spec-builder/src/gauge/gaugeSpecBuilder.ts | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 8ffb8611b..83d45fbff 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -35,10 +35,14 @@ export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => // Performance ranges if (opt.showPerformanceRanges) { marks.push(...getPerformanceRangeMarks(name, opt.performanceRanges)); + const endCapColor = opt.performanceRanges[0]; + const startCapColor = opt.performanceRanges[2]; } // Filler arc (fills to clampedValue) marks.push(getFillerArc(name, fillerColorSignal)); + const endCapColor = fillerColorSignal; + const startCapColor = fillerColorSignal; // Needle to clampedValue if (needle) { @@ -133,6 +137,8 @@ export function getPerformanceRangeMarks( startAngle: { signal: 'band1StartAngle' }, endAngle: { signal: 'band1EndAngle' }, fill: { value: band1.fill }, + stroke: { signal: BACKGROUND_COLOR }, + strokeWidth: { value: 6 }, }, }, }, @@ -148,6 +154,8 @@ export function getPerformanceRangeMarks( startAngle: { signal: 'band2StartAngle' }, endAngle: { signal: 'band2EndAngle' }, fill: { value: band2.fill }, + stroke: { signal: BACKGROUND_COLOR }, + strokeWidth: { value: 6 }, }, }, }, @@ -163,6 +171,8 @@ export function getPerformanceRangeMarks( startAngle: { signal: 'band3StartAngle' }, endAngle: { signal: 'band3EndAngle' }, fill: { value: band3.fill }, + stroke: { signal: BACKGROUND_COLOR }, + strokeWidth: { value: 6 }, }, }, }, diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index c15ccafe9..42169001d 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -27,7 +27,7 @@ const DEFAULT_COLOR = spectrumColors.light['blue-900']; const DEFAULT_PERFORMANCE_RANGES: PerformanceRanges[] = [ { bandEndPct: 0.55, fill: spectrumColors.light['red-900'] }, - { bandEndPct: 0.8, fill: spectrumColors.light['yellow-900'] }, + { bandEndPct: 0.8, fill: spectrumColors.light['yellow-400'] }, { bandEndPct: 1, fill: spectrumColors.light['green-700'] }, ]; From b084aa33c1d003f97967fb4925881338c1367594 Mon Sep 17 00:00:00 2001 From: Brady Shimanek <bradyshimanek33@gmail.com> Date: Wed, 26 Nov 2025 22:06:41 -0700 Subject: [PATCH 57/66] Implement *correct* gaps @danimanderson -_- --- .../src/gauge/gaugeMarkUtils.ts | 71 ++++++++++++++----- .../src/gauge/gaugeSpecBuilder.ts | 10 +++ 2 files changed, 65 insertions(+), 16 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 83d45fbff..93db75701 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -29,23 +29,24 @@ export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => const backgroundStroke = spectrumColors[colorScheme]['gray-300']; const fillerColorSignal = 'fillerColorToCurrVal'; - // Background arc - marks.push(getBackgroundArc(name, backgroundFill, backgroundStroke)); - // Performance ranges if (opt.showPerformanceRanges) { marks.push(...getPerformanceRangeMarks(name, opt.performanceRanges)); const endCapColor = opt.performanceRanges[0]; const startCapColor = opt.performanceRanges[2]; + marks.push(getBandGap1(name)); + marks.push(getBandGap2(name)); + } else { + // Background arc + marks.push(getBackgroundArc(name, backgroundFill, backgroundStroke)); + // Filler arc (fills to clampedValue) + marks.push(getFillerArc(name, fillerColorSignal)); + const endCapColor = fillerColorSignal; + const startCapColor = fillerColorSignal; } - // Filler arc (fills to clampedValue) - marks.push(getFillerArc(name, fillerColorSignal)); - const endCapColor = fillerColorSignal; - const startCapColor = fillerColorSignal; - // Needle to clampedValue - if (needle) { + if (needle || opt.showPerformanceRanges) { marks.push(getNeedle(name)); marks.push(getNeedleHole(name, BACKGROUND_COLOR)); } @@ -136,9 +137,7 @@ export function getPerformanceRangeMarks( outerRadius: { signal: 'outerRadius' }, startAngle: { signal: 'band1StartAngle' }, endAngle: { signal: 'band1EndAngle' }, - fill: { value: band1.fill }, - stroke: { signal: BACKGROUND_COLOR }, - strokeWidth: { value: 6 }, + fill: { value: band1.fill }, }, }, }, @@ -154,8 +153,6 @@ export function getPerformanceRangeMarks( startAngle: { signal: 'band2StartAngle' }, endAngle: { signal: 'band2EndAngle' }, fill: { value: band2.fill }, - stroke: { signal: BACKGROUND_COLOR }, - strokeWidth: { value: 6 }, }, }, }, @@ -171,15 +168,57 @@ export function getPerformanceRangeMarks( startAngle: { signal: 'band3StartAngle' }, endAngle: { signal: 'band3EndAngle' }, fill: { value: band3.fill }, - stroke: { signal: BACKGROUND_COLOR }, - strokeWidth: { value: 6 }, }, }, }, ]; } +export function getBandGap1(name: string): Mark { + + return { + name: `${name}Band1Gap`, + description: 'Band 1 Gap', + type: 'rule', + encode: { + enter: { + stroke: { signal: BACKGROUND_COLOR }, + strokeWidth: { value: 6 }, + strokeCap: { value: "round" } + }, + update: { + stroke: { signal: BACKGROUND_COLOR }, + x: { signal: "band1GapX" }, + y: { signal: "band1GapY" }, + x2: { signal: "band1GapX2" }, + y2: { signal: "band1GapY2" } + } + } + } +}; + +export function getBandGap2(name: string): Mark { + return { + name: `${name}Band2Gap`, + description: 'Band 2 Gap', + type: 'rule', + encode: { + enter: { + stroke: { signal: BACKGROUND_COLOR }, + strokeWidth: { value: 6 }, + strokeCap: { value: "round" } + }, + update: { + stroke: { signal: BACKGROUND_COLOR }, + x: { signal: "band2GapX" }, + y: { signal: "band2GapY" }, + x2: { signal: "band2GapX2" }, + y2: { signal: "band2GapY2" } + } + } + } +}; export function getNeedleHole(name: string, backgroundColor): Mark { return { diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index 42169001d..a3de614d4 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -139,6 +139,16 @@ export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, option signals.push({ name: 'band2EndAngle', update: "scale('angleScale', band2EndVal)" }); signals.push({ name: 'band3StartAngle', update: "scale('angleScale', band3StartVal)" }); signals.push({ name: 'band3EndAngle', update: "scale('angleScale', band3EndVal)" }); + + signals.push({ name: 'band1GapX', update: "centerX + ( innerRadius - 5 ) * cos(band1EndAngle - PI/2)"}) + signals.push({ name: 'band1GapY', update: "centerY + ( innerRadius - 5 ) * sin(band1EndAngle - PI/2)"}) + signals.push({ name: 'band1GapX2', update: "centerX + ( outerRadius + 5 ) * cos(band1EndAngle - PI/2)"}) + signals.push({ name: 'band1GapY2', update: "centerY + ( outerRadius + 5 ) * sin(band1EndAngle - PI/2)"}) + + signals.push({ name: 'band2GapX', update: "centerX + ( innerRadius - 5 ) * cos(band2EndAngle - PI/2)"}) + signals.push({ name: 'band2GapY', update: "centerY + ( innerRadius - 5 ) * sin(band2EndAngle - PI/2)"}) + signals.push({ name: 'band2GapX2', update: "centerX + ( outerRadius + 5 ) * cos(band2EndAngle - PI/2)"}) + signals.push({ name: 'band2GapY2', update: "centerY + ( outerRadius + 5 ) * sin(band2EndAngle - PI/2)"}) }); export const addScales = produce<Scale[], [GaugeSpecOptions]>((scales, options) => { From 23df3417efb9d575baef976f199cd54949964279 Mon Sep 17 00:00:00 2001 From: JarekSmith <105569335+JarekSmith@users.noreply.github.com> Date: Wed, 26 Nov 2025 22:41:38 -0700 Subject: [PATCH 58/66] Implement three color rounded edges --- .../vega-spec-builder/src/gauge/gaugeMarkUtils.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 9b20a62ea..49ad929b6 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -32,17 +32,19 @@ export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => // Performance ranges if (opt.showPerformanceRanges) { marks.push(...getPerformanceRangeMarks(name, opt.performanceRanges)); - const endCapColor = opt.performanceRanges[0]; - const startCapColor = opt.performanceRanges[2]; marks.push(getBandGap1(name)); marks.push(getBandGap2(name)); + // Add caps + marks.push(getStartCap(name, opt.performanceRanges[0].fill, opt.performanceRanges[0].fill)); + marks.push(getEndCap(name, opt.performanceRanges[2].fill, opt.performanceRanges[2].fill)); } else { // Background arc - marks.push(getBackgroundArc(name, backgroundFill, backgroundStroke)); + marks.push(getBackgroundArc(name, backgroundFill)); // Filler arc (fills to clampedValue) marks.push(getFillerArc(name, fillerColorSignal)); - const endCapColor = fillerColorSignal; - const startCapColor = fillerColorSignal; + // Add caps + marks.push(getStartCap(name, fillerColorSignal, backgroundFill)); + marks.push(getEndCap(name, fillerColorSignal, backgroundFill)); } // Needle to clampedValue @@ -301,7 +303,7 @@ export function getEndCap(name: string, fillColor: string, backgroundColor: stri "x": { signal: xOffset }, "y": { signal: yOffset }, "innerRadius": { signal: '0' }, - "outerRadius": { signal: "((outerRadius - innerRadius) / 2)-1" }, + "outerRadius": { signal: "((outerRadius - innerRadius) / 2) - 1" }, "startAngle": { signal: "endAngle" }, "endAngle": { signal: "endAngle+PI"}, "strokeWidth": { signal: "2" }, From 09b1d0f7a10ea8181bb082142976e5f40058cca1 Mon Sep 17 00:00:00 2001 From: JarekSmith <105569335+JarekSmith@users.noreply.github.com> Date: Wed, 26 Nov 2025 22:52:05 -0700 Subject: [PATCH 59/66] Merge label with endcaps and three color gauge --- .../stories/components/Gauge/Gauge.story.tsx | 17 ++++++++++++----- .../src/gauge/gaugeMarkUtils.ts | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx index ea422fd84..cc91755ed 100644 --- a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx @@ -71,12 +71,18 @@ PerformanceRange.args = { performanceRanges: coloredPerformanceData, }; -const GaugeVariation3 = bindWithProps(GaugeStory); -GaugeVariation3.args = { - metric: 'currentAmount', +const Empty = bindWithProps(GaugeStory); +Empty.args = { + metric: 'currentAmount-60', color: 'fuchsia-900', }; +const Full = bindWithProps(GaugeStory); +Full.args = { + metric: 'currentAmount+40', + color: 'static-pruple-900', +}; + const GaugeLabelNoNeedleNoPercent = bindWithProps(GaugeStory); GaugeLabelNoNeedleNoPercent.args = { metric: 'currentAmount', @@ -117,8 +123,9 @@ GaugeLabelNeedlePercent.args = { export { Basic, - GaugeVariation2, - GaugeVariation3, + Empty, + Full, + PerformanceRange, GaugeLabelNoNeedleNoPercent, GaugeLabelNeedleNoPercent, GaugeLabelNoNeedlePercent, diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 9eb9d76f7..b3a423028 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -59,7 +59,7 @@ export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => const labelYOffset = 100; const labelFontSize = 60; marks.push(getValueLabel(name, labelFontSize, labelYOffset)); - } else if (needle){ + } else if (needle || opt.showPerformanceRanges){ marks.push(getNeedle(name)); marks.push(getNeedleHole(name, BACKGROUND_COLOR)); } else if (showLabel){ From 776cc7dcd80af8eff615e935a632631026fa2c1e Mon Sep 17 00:00:00 2001 From: H-bot-hash <H-bot-hash@users.noreply.github.com> Date: Tue, 2 Dec 2025 21:25:21 -0700 Subject: [PATCH 60/66] Fixing tests --- .../src/gauge/gaugeMarkUtils.test.ts | 9 ++++----- .../vega-spec-builder/src/gauge/gaugeSpecBuilder.ts | 12 ++---------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.test.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.test.ts index 3b2cd2e4c..ef58d0a78 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.test.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.test.ts @@ -26,10 +26,9 @@ describe('getGaugeMarks', () => { const data = addGaugeMarks([], defaultGaugeOptions); expect(data).toBeDefined(); expect(Array.isArray(data)).toBe(true); - expect(data).toHaveLength(3); + expect(data).toHaveLength(4); expect(data[0].type).toBe('arc'); expect(data[1].type).toBe('arc'); - expect(data[2].type).toBe('rule'); }); }); @@ -41,7 +40,7 @@ describe('getGaugeBackgroundArc', () => { expect(data.encode?.enter).toBeDefined(); // Expect the correct amount of fields in the update object - expect(Object.keys(data.encode?.enter ?? {}).length).toBe(8); + expect(Object.keys(data.encode?.enter ?? {}).length).toBe(7); }); }); @@ -52,7 +51,7 @@ describe('getFillerArc', () => { expect(data).toBeDefined(); expect(data.encode?.update).toBeDefined(); - expect(Object.keys(data.encode?.update ?? {}).length).toBe(1); + expect(Object.keys(data.encode?.update ?? {}).length).toBe(2); expect(data.encode?.enter).toBeDefined(); expect(Object.keys(data.encode?.enter ?? {}).length).toBe(7) @@ -68,7 +67,7 @@ describe('getGaugeNeedle', () => { expect(Object.keys(data.encode?.update ?? {}).length).toBe(4); expect(data.encode?.enter).toBeDefined(); - expect(Object.keys(data.encode?.enter ?? {}).length).toBe(3) + expect(Object.keys(data.encode?.enter ?? {}).length).toBe(2) }); }); diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index 40cb03278..4ab2734a9 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -99,16 +99,8 @@ export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, option signals.push({ name: 'clampedVal', update: "min(max(arcMinVal, currVal), arcMaxVal)"}) signals.push({ name: 'currVal', update: `data('table')[0].${options.metric}` }) signals.push({ name: 'endAngle', update: "PI * 2 / 3" }) - signals.push({ - name: 'fillerColorToCurrVal', - update: options.showPerformanceRanges ? 'null' : `"${options.color}"`, - }); - signals.push({ - name: 'needleColor', - update: options.showPerformanceRanges - ? `"${getColorValue('gray-900', options.colorScheme)}"` - : `"${options.color}"`, - }); + signals.push({ name: 'fillerColorToCurrVal', value: `${options.color}`}); + signals.push({ name: 'needleColor', update: options.showPerformanceRanges? `"${getColorValue('gray-900', options.colorScheme)}"`: `"${options.color}"`}); signals.push({ name: 'graphLabel', update: `data('table')[0].${options.graphLabel}` }) signals.push({ name: 'innerRadius', update: "outerRadius - (radiusRef * 0.25)"}) signals.push({ name: 'needleAngleTarget', update: "needleAngleTargetVal - PI/2"}) From fec35c84dd5128473939cd405df14b9b3f7d691f Mon Sep 17 00:00:00 2001 From: H-bot-hash <H-bot-hash@users.noreply.github.com> Date: Tue, 2 Dec 2025 23:14:32 -0700 Subject: [PATCH 61/66] Adding tests --- .../stories/components/Gauge/Gauge.test.tsx | 12 +- .../src/gauge/gaugeMarkUtils.test.ts | 233 +++++++++++++++++- .../src/gauge/gaugeMarkUtils.ts | 8 +- .../src/gauge/gaugeSpecBuilder.test.ts | 2 +- .../src/gauge/gaugeSpecBuilder.ts | 4 +- 5 files changed, 244 insertions(+), 15 deletions(-) diff --git a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.test.tsx b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.test.tsx index 0824494b7..f6fd8de0e 100644 --- a/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.test.tsx +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.test.tsx @@ -24,13 +24,17 @@ describe('Gauge', () => { const chart = await findChart(); expect(chart).toBeInTheDocument(); - const backgroundArc = await findMarksByGroupName(chart, 'BackgroundArcRounded'); + const backgroundArc = await findMarksByGroupName(chart, 'gauge0BackgroundArc'); expect(backgroundArc).toBeDefined(); - const fillerArc = await findMarksByGroupName(chart, 'FillerArc'); + const fillerArc = await findMarksByGroupName(chart, 'gauge0FillerArc'); expect(fillerArc).toBeDefined(); - const needleRule = await findMarksByGroupName(chart, 'Needle', 'line'); - expect(needleRule).toBeDefined(); + const startCap = await findMarksByGroupName(chart, 'gauge0StartCap'); + expect(startCap).toBeDefined(); + + const endCap = await findMarksByGroupName(chart, 'gauge0EndCap'); + expect(endCap).toBeDefined(); + }); }); diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.test.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.test.ts index ef58d0a78..173395d8f 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.test.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.test.ts @@ -16,11 +16,25 @@ import { getBackgroundArc, getFillerArc, getNeedle, -} from './gaugeMarkUtils'; + getPerformanceRangeMarks, + getBandGap1, + getBandGap2, + getNeedleHole, + getTargetLine, + getStartCap, + getEndCap, + getLabel, + getValueLabel, + } from './gaugeMarkUtils'; import { defaultGaugeOptions } from './gaugeTestUtils'; import { spectrumColors } from '../../../themes'; +import { DEFAULT_PERFORMANCE_RANGES } from './gaugeSpecBuilder'; + + + +// getGaugeMarks describe('getGaugeMarks', () => { test('Should return the correct marks object', () => { const data = addGaugeMarks([], defaultGaugeOptions); @@ -32,7 +46,7 @@ describe('getGaugeMarks', () => { }); }); - +// getGaugeBackgroundArc describe('getGaugeBackgroundArc', () => { test('Should return the correct background arc mark object', () => { const data = getBackgroundArc("backgroundTestName", spectrumColors['light']['blue-200'], spectrumColors['light']['blue-300']); @@ -44,7 +58,7 @@ describe('getGaugeBackgroundArc', () => { }); }); - +// getFillerArc describe('getFillerArc', () => { test('Should return the correct filler arc mark object', () => { const data = getFillerArc("fillerTestName", spectrumColors['light']['magenta-900']); @@ -58,7 +72,7 @@ describe('getFillerArc', () => { }); }); - +// getGaugeNeedle describe('getGaugeNeedle', () => { test('Should return the needle mark object', () => { const data = getNeedle("needleTestName"); @@ -71,3 +85,214 @@ describe('getGaugeNeedle', () => { }); }); +// getPerformanceRangeMarks +describe('DEFAULT_PERFORMANCE_RANGES', () => { + test('Should use the correct bandEndPct and fill colors for red, yellow, green', () => { + expect(DEFAULT_PERFORMANCE_RANGES).toBeDefined(); + expect(DEFAULT_PERFORMANCE_RANGES).toHaveLength(3); + + const [band1, band2, band3] = DEFAULT_PERFORMANCE_RANGES; + + // Band 1: red + expect(band1.bandEndPct).toBe(0.55); + expect(band1.fill).toBe(spectrumColors.light['red-900']); + + // Band 2: yellow + expect(band2.bandEndPct).toBe(0.8); + expect(band2.fill).toBe(spectrumColors.light['yellow-400']); + + // Band 3: green + expect(band3.bandEndPct).toBe(1); + expect(band3.fill).toBe(spectrumColors.light['green-700']); + }); +}); + + +// getBandGap1 +describe('getGaugeBandGap1', () => { + test('Should return the band 1 gap mark object', () => { + const data = getBandGap1('bandGapTestName'); + expect(data).toBeDefined(); + + expect(data.encode?.enter).toBeDefined(); + expect(Object.keys(data.encode?.enter ?? {}).length).toBe(3); + + expect(data.encode?.update).toBeDefined(); + expect(Object.keys(data.encode?.update ?? {}).length).toBe(5); + }); +}); + + +// getBandGap2 +describe('getGaugeBandGap2', () => { + test('Should return the band 2 gap mark object', () => { + const data = getBandGap2('bandGapTestName'); + expect(data).toBeDefined(); + + expect(data.encode?.enter).toBeDefined(); + expect(Object.keys(data.encode?.enter ?? {}).length).toBe(3); + + expect(data.encode?.update).toBeDefined(); + expect(Object.keys(data.encode?.update ?? {}).length).toBe(5); + }); +}); + +// getNeedleHole +describe('getGaugeNeedleHole', () => { + test('Should return the needle hole mark object', () => { + const backgroundColorSignal = 'chartBackgroundColor'; + const data = getNeedleHole('needleTestName', backgroundColorSignal); + expect(data).toBeDefined(); + expect(data.encode?.enter).toBeDefined(); + expect(Object.keys(data.encode?.enter ?? {}).length).toBe(2); + + expect(data.encode?.enter?.x).toBeDefined(); + expect(data.encode?.enter?.y).toBeDefined(); + + expect(data.encode?.update).toBeDefined(); + expect(Object.keys(data.encode?.update ?? {}).length).toBe(4); + }); +}); + +// getTargetLine +describe('getGaugeTargetLine', () => { + test('Should return the target line mark object', () => { + const data = getTargetLine('targetTestName'); + expect(data).toBeDefined(); + + + expect(data.encode?.enter).toBeDefined(); + expect(Object.keys(data.encode?.enter ?? {}).length).toBe(3); + + + expect(data.encode?.update).toBeDefined(); + expect(Object.keys(data.encode?.update ?? {}).length).toBe(5); + }); +}); + +// getStartCap +describe('getGaugeStartCap', () => { + test('Should return the start cap mark object', () => { + const data = getStartCap('startCapTestName', 'fillerColorToCurrVal', 'chartBackgroundColor'); + expect(data).toBeDefined(); + + expect(data.encode?.enter).toBeDefined(); + expect(Object.keys(data.encode?.enter ?? {}).length).toBe(8); + + expect(data.encode?.update).toBeDefined(); + expect(Object.keys(data.encode?.update ?? {}).length).toBe(2); + }); +}); + + +// getEndCap +describe('getGaugeEndCap', () => { + test('Should return the end cap mark object', () => { + const data = getEndCap('endCapTestName', 'fillerColorToCurrVal', 'chartBackgroundColor'); + expect(data).toBeDefined(); + + expect(data.encode?.enter).toBeDefined(); + expect(Object.keys(data.encode?.enter ?? {}).length).toBe(7); + + expect(data.encode?.update).toBeDefined(); + expect(Object.keys(data.encode?.update ?? {}).length).toBe(2); + }); +}); + +// getLabel +describe('getGaugeLabel', () => { + test('Should return the label mark object', () => { + const data = getLabel('labelTestName', 14, 20); + expect(data).toBeDefined(); + + expect(data.encode?.enter).toBeDefined(); + expect(Object.keys(data.encode?.enter ?? {}).length).toBe(4); + + expect(data.encode?.update).toBeDefined(); + expect(Object.keys(data.encode?.update ?? {}).length).toBe(3); + }); +}); + +// getValueLabel +describe('getGaugeValueLabel', () => { + test('Should return the value label mark object', () => { + const data = getValueLabel('valueLabelTestName', 16, 30); + expect(data).toBeDefined(); + + expect(data.encode?.enter).toBeDefined(); + expect(Object.keys(data.encode?.enter ?? {}).length).toBe(4); + + expect(data.encode?.update).toBeDefined(); + expect(Object.keys(data.encode?.update ?? {}).length).toBe(3); + }); +}); + + +// getGaugeNeeldeAndLabelTests +describe('getGaugeNeedleAndLabelTests', () => { + test('Needle and Label Both True', () => { + const data = addGaugeMarks([], { + ...defaultGaugeOptions, + needle: true, + showLabel: true + }); + expect(data).toBeDefined(); + expect(Array.isArray(data)).toBe(true); + expect(data).toHaveLength(8); + + // Should have 4 arcs to start + expect(data.filter(m => m.type === 'arc')).toHaveLength(4); + // Needle should be present + expect(data.some(m => m.type === 'symbol' && (m.name ?? '').includes('Needle'))).toBe(true); + // Needle holde should be present + expect(data.some(m => m.type === 'symbol' && (m.name ?? '').includes('NeedleHole'))).toBe(true); + // Graph Label present + expect(data.some(m => m.type === 'text' && (m.name ?? '').includes('graphLabelText'))).toBe(true); + // Value Label present + expect(data.some(m => m.type === 'text' && (m.name ?? '').includes('graphLabelCurrentValueText'))).toBe(true); + }); + + test('Needle True and Label False', () => { + const data = addGaugeMarks([], { + ...defaultGaugeOptions, + needle: true, + showLabel: false + }); + expect(data).toBeDefined(); + expect(Array.isArray(data)).toBe(true); + expect(data).toHaveLength(6); + + // Still have 4 arcs to start + expect(data.filter(m => m.type === 'arc')).toHaveLength(4); + // Needle present + expect(data.some(m => m.type === 'symbol' && (m.name ?? '').includes('Needle'))).toBe(true); + // Needle holde should be present + expect(data.some(m => m.type === 'symbol' && (m.name ?? '').includes('NeedleHole'))).toBe(true); + // Graph Label present + expect(data.some(m => m.type === 'text' && (m.name ?? '').includes('graphLabelText'))).toBe(false); + // Value Label present + expect(data.some(m => m.type === 'text' && (m.name ?? '').includes('graphLabelCurrentValueText'))).toBe(false); + }); + + test('Needle False and Label True', () => { + const data = addGaugeMarks([], { + ...defaultGaugeOptions, + needle: false, + showLabel: true + }); + expect(data).toBeDefined(); + expect(Array.isArray(data)).toBe(true); + expect(data).toHaveLength(6); + + // Still have 4 arcs to start + expect(data.filter(m => m.type === 'arc')).toHaveLength(4); + // No needle mark + expect(data.some(m => m.type === 'symbol' && (m.name ?? '').includes('Needle'))).toBe(false); + // Needle holde should be present + expect(data.some(m => m.type === 'symbol' && (m.name ?? '').includes('NeedleHole'))).toBe(false); + // Graph Label present + expect(data.some(m => m.type === 'text' && (m.name ?? '').includes('graphLabelText'))).toBe(true); + // Value Label present + expect(data.some(m => m.type === 'text' && (m.name ?? '').includes('graphLabelCurrentValueText'))).toBe(true); + }); +}); \ No newline at end of file diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index b3a423028..345b30f99 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -243,7 +243,7 @@ export function getBandGap2(name: string): Mark { export function getNeedleHole(name: string, backgroundColor): Mark { return { - name: `${name}Needle Hole`, + name: `${name}NeedleHole`, description: 'Needle Hole', type: 'symbol', encode: { @@ -263,7 +263,7 @@ export function getNeedleHole(name: string, backgroundColor): Mark { export function getTargetLine(name: string): Mark { return { - name: `${name}Target Line`, + name: `${name}TargetLine`, description: 'Target Line', type: 'rule', encode: { @@ -287,7 +287,7 @@ export function getStartCap(name: string, fillColor: string, backgroundColor: st const xOffset = 'centerX+(sin(startAngle)*((outerRadius+innerRadius)/2))' const yOffset = 'centerY-(cos(startAngle)*((outerRadius+innerRadius)/2))' return { - name: `${name}Start Cap`, + name: `${name}StartCap`, description: `Start Cap`, type: `arc`, encode: { @@ -313,7 +313,7 @@ export function getEndCap(name: string, fillColor: string, backgroundColor: stri const xOffset = 'centerX+(sin(endAngle)*((outerRadius+innerRadius)/2))' const yOffset = 'centerY-(cos(endAngle)*((outerRadius+innerRadius)/2))' return { - name: `${name}End Cap`, + name: `${name}EndCap`, description: `End Cap`, type: `arc`, encode: { diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts index cc159950b..970bbecae 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts @@ -53,7 +53,7 @@ describe('getGaugeSignals', () => { test('Should return the correct signals object', () => { const data = addSignals([], defaultGaugeOptions); expect(data).toBeDefined(); - expect(data).toHaveLength(19); + expect(data).toHaveLength(55); }); }); diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index 4ab2734a9..64b30324f 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -25,7 +25,7 @@ import { getGaugeTableData } from './gaugeDataUtils'; const DEFAULT_COLOR = spectrumColors.light['blue-900']; -const DEFAULT_PERFORMANCE_RANGES: PerformanceRanges[] = [ +export const DEFAULT_PERFORMANCE_RANGES: PerformanceRanges[] = [ { bandEndPct: 0.55, fill: spectrumColors.light['red-900'] }, { bandEndPct: 0.8, fill: spectrumColors.light['yellow-400'] }, { bandEndPct: 1, fill: spectrumColors.light['green-700'] }, @@ -89,7 +89,7 @@ export const addGauge = produce< ); export const addSignals = produce<Signal[], [GaugeSpecOptions]>((signals, options) => { - const ranges = options.performanceRanges; + const ranges = options.performanceRanges ?? DEFAULT_PERFORMANCE_RANGES; signals.push({ name: 'arcMaxVal', value: options.maxArcValue }) signals.push({ name: 'arcMinVal', value: options.minArcValue }) From aa4220ae18eec9848711a98fc1b247e9b04f7701 Mon Sep 17 00:00:00 2001 From: H-bot-hash <H-bot-hash@users.noreply.github.com> Date: Tue, 2 Dec 2025 23:38:37 -0700 Subject: [PATCH 62/66] Adding more tests --- .../src/gauge/gaugeMarkUtils.test.ts | 80 +++++++++++++++++-- .../src/gauge/gaugeMarkUtils.ts | 2 +- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.test.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.test.ts index 173395d8f..19c0ed420 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.test.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.test.ts @@ -32,8 +32,6 @@ import { spectrumColors } from '../../../themes'; import { DEFAULT_PERFORMANCE_RANGES } from './gaugeSpecBuilder'; - - // getGaugeMarks describe('getGaugeMarks', () => { test('Should return the correct marks object', () => { @@ -85,7 +83,7 @@ describe('getGaugeNeedle', () => { }); }); -// getPerformanceRangeMarks +// getDefaultPerformanceRanges describe('DEFAULT_PERFORMANCE_RANGES', () => { test('Should use the correct bandEndPct and fill colors for red, yellow, green', () => { expect(DEFAULT_PERFORMANCE_RANGES).toBeDefined(); @@ -107,6 +105,78 @@ describe('DEFAULT_PERFORMANCE_RANGES', () => { }); }); +// getPerformanceRangeTests +describe('getPerformanceRangesTests', () => { + const performanceRanges = [ + { bandEndPct: 0.55, fill: 'red-900' }, + { bandEndPct: 0.8, fill: 'yellow-900' }, + { bandEndPct: 1, fill: 'green-700' }, + ]; + + test('Performance ranges enabled adds band arcs, gaps, and caps', () => { + const data = addGaugeMarks([], { + ...defaultGaugeOptions, + showPerformanceRanges: true, + performanceRanges, + }); + expect(data).toHaveLength(9); + expect(data.map(mark => mark.type)).toEqual([ + 'arc', + 'arc', + 'arc', + 'rule', + 'rule', + 'arc', + 'arc', + 'symbol', + 'symbol', + ]); + }); + + test('Performance ranges disabled keeps background, filler, and caps', () => { + const data = addGaugeMarks([], { + ...defaultGaugeOptions, + showPerformanceRanges: false }); + expect(data).toHaveLength(4); + expect(data.every(mark => mark.type === 'arc')).toBe(true); + }); +});describe('getPerformanceRangesTests', () => { + const performanceRanges = [ + { bandEndPct: 0.55, fill: 'red-900' }, + { bandEndPct: 0.8, fill: 'yellow-900' }, + { bandEndPct: 1, fill: 'green-700' }, + ]; + + test('Performance ranges enabled adds band arcs, gaps, and caps', () => { + const data = addGaugeMarks([], { + ...defaultGaugeOptions, + showPerformanceRanges: true, + performanceRanges, + }); + expect(data).toHaveLength(9); + expect(data.map(mark => mark.type)).toEqual([ + 'arc', + 'arc', + 'arc', + 'rule', + 'rule', + 'arc', + 'arc', + 'symbol', + 'symbol', + ]); + }); + + test('Performance ranges disabled keeps background, filler, and caps', () => { + const data = addGaugeMarks([], { + ...defaultGaugeOptions, + showPerformanceRanges: false }); + expect(data).toHaveLength(4); + expect(data.every(mark => mark.type === 'arc')).toBe(true); + }); +}); + + // getBandGap1 describe('getGaugeBandGap1', () => { @@ -227,7 +297,6 @@ describe('getGaugeValueLabel', () => { }); }); - // getGaugeNeeldeAndLabelTests describe('getGaugeNeedleAndLabelTests', () => { test('Needle and Label Both True', () => { @@ -295,4 +364,5 @@ describe('getGaugeNeedleAndLabelTests', () => { // Value Label present expect(data.some(m => m.type === 'text' && (m.name ?? '').includes('graphLabelCurrentValueText'))).toBe(true); }); -}); \ No newline at end of file +}); + diff --git a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts index 345b30f99..9c0e8b63f 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -49,7 +49,7 @@ export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => } // Needle to clampedValue - if (needle && showLabel) { +if ((needle || opt.showPerformanceRanges) && showLabel) { marks.push(getNeedle(name)); marks.push(getNeedleHole(name, BACKGROUND_COLOR)); const yOffset = 140; From 9ccd5a6d657ecfd63f6036221c914fca4e4c99a4 Mon Sep 17 00:00:00 2001 From: H-bot-hash <H-bot-hash@users.noreply.github.com> Date: Thu, 4 Dec 2025 21:32:32 -0700 Subject: [PATCH 63/66] Adding signal tests --- .../src/gauge/gaugeSpecBuilder.test.ts | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts index 970bbecae..bdb172c66 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts @@ -97,6 +97,69 @@ describe('addGauge (defaults & overrides for gaugeOptions)', () => { // fillerColorToCurrVal uses light expect(byName(signals, 'fillerColorToCurrVal')?.value).toBe('light'); + + // geometry defaults + expect(byName(signals, 'radiusRef')?.update).toBe('min(width/2, height/2)'); + expect(byName(signals, 'outerRadius')?.update).toBe('radiusRef * 0.95'); + expect(byName(signals, 'innerRadius')?.update).toBe('outerRadius - (radiusRef * 0.25)'); + expect(byName(signals, 'centerX')?.update).toBe('width/2'); + expect(byName(signals, 'centerY')?.update).toBe('height/2 + outerRadius/2'); + + + // target value uses default target field from options + expect(byName(signals, 'target')?.update).toBe("data('table')[0].target"); + + // clamped value and angle mapping are wired as expressions + expect(byName(signals, 'clampedVal')?.update).toBe('min(max(arcMinVal, currVal), arcMaxVal)'); + expect(byName(signals, 'theta')?.update).toBe("scale('angleScale', clampedVal)"); + expect(byName(signals, 'needleAngleClampedVal')?.update).toBe("scale('angleScale', clampedVal)"); + expect(byName(signals, 'needleAngleRaw')?.update).toBeUndefined(); // (if you don't have it anymore; or remove this line) + + expect(byName(signals, 'needleAngleDeg')?.update).toBe('needleAngleClampedVal * 180 / PI'); + expect(byName(signals, 'needleLength')?.update).toBe('30'); + + // label/value text colors from scheme + const expectedValueColor = getColorValue('gray-900', DEFAULT_COLOR_SCHEME); + const expectedLabelColor = getColorValue('gray-600', DEFAULT_COLOR_SCHEME); + + expect(byName(signals, 'valueTextColor')?.value).toBe(expectedValueColor); + expect(byName(signals, 'labelTextColor')?.value).toBe(expectedLabelColor); + + // showAsPercent default wiring + expect(byName(signals, 'showAsPercent')?.update).toBe(`${defaultGaugeOptions.showsAsPercent}`); + + // textSignal formatting expression + expect(byName(signals, 'textSignal')?.update).toBe("showAsPercent ? format((currVal / arcMaxVal) * 100, '.2f') + '%' : format(currVal, '.0f')"); + + // --- band ranges: default formulas --- + expect(byName(signals, 'bandRange')?.update).toBe('arcMaxVal - arcMinVal'); + expect(byName(signals, 'band1StartVal')?.update).toBe('arcMinVal'); + expect(byName(signals, 'band1EndVal')?.update).toBe('arcMinVal + bandRange * band1EndPct'); + expect(byName(signals, 'band2StartVal')?.update).toBe('band1EndVal'); + expect(byName(signals, 'band2EndVal')?.update).toBe('arcMinVal + bandRange * band2EndPct'); + expect(byName(signals, 'band3StartVal')?.update).toBe('band2EndVal'); + expect(byName(signals, 'band3EndVal')?.update).toBe('arcMaxVal'); + + // --- band angle mapping formulas --- + expect(byName(signals, 'band1StartAngle')?.update).toBe("scale('angleScale', band1StartVal)"); + expect(byName(signals, 'band1EndAngle')?.update).toBe("scale('angleScale', band1EndVal)"); + expect(byName(signals, 'band2StartAngle')?.update).toBe("scale('angleScale', band2StartVal)"); + expect(byName(signals, 'band2EndAngle')?.update).toBe("scale('angleScale', band2EndVal)"); + expect(byName(signals, 'band3StartAngle')?.update).toBe("scale('angleScale', band3StartVal)"); + expect(byName(signals, 'band3EndAngle')?.update).toBe("scale('angleScale', band3EndVal)"); + + // --- band gap geometry formulas (band 1) --- + expect(byName(signals, 'band1GapX')?.update).toBe("centerX + ( innerRadius - 5 ) * cos(band1EndAngle - PI/2)"); + expect(byName(signals, 'band1GapY')?.update).toBe("centerY + ( innerRadius - 5 ) * sin(band1EndAngle - PI/2)"); + expect(byName(signals, 'band1GapX2')?.update).toBe("centerX + ( outerRadius + 5 ) * cos(band1EndAngle - PI/2)"); + expect(byName(signals, 'band1GapY2')?.update).toBe("centerY + ( outerRadius + 5 ) * sin(band1EndAngle - PI/2)"); + + // --- band gap geometry formulas (band 2) --- + expect(byName(signals, 'band2GapX')?.update).toBe("centerX + ( innerRadius - 5 ) * cos(band2EndAngle - PI/2)"); + expect(byName(signals, 'band2GapY')?.update).toBe("centerY + ( innerRadius - 5 ) * sin(band2EndAngle - PI/2)"); + expect(byName(signals, 'band2GapX2')?.update).toBe("centerX + ( outerRadius + 5 ) * cos(band2EndAngle - PI/2)"); + expect(byName(signals, 'band2GapY2')?.update).toBe("centerY + ( outerRadius + 5 ) * sin(band2EndAngle - PI/2)"); + }); test('applies user overrides (colorScheme, color, min/max, metric, name, index)', () => { @@ -114,6 +177,13 @@ describe('addGauge (defaults & overrides for gaugeOptions)', () => { const newSpec = addGauge(spec, overrides); const signals = newSpec.signals as any[]; + const expectedTargetStrokeDark = getColorValue('gray-900', 'dark'); + const expectedValueColorDark = getColorValue('gray-900', 'dark'); + const expectedLabelColorDark = getColorValue('gray-600', 'dark'); + + expect(byName(signals, 'targetLineStroke')?.value).toBe(expectedTargetStrokeDark); + expect(byName(signals, 'valueTextColor')?.value).toBe(expectedValueColorDark); + expect(byName(signals, 'labelTextColor')?.value).toBe(expectedLabelColorDark); // min/max should reflect overrides expect(byName(signals, 'arcMinVal')?.value).toBe(50); @@ -134,5 +204,34 @@ describe('addGauge (defaults & overrides for gaugeOptions)', () => { expect(byName(signals, 'endAngle')?.update).toBe('PI * 2 / 3'); }); +describe('getGaugeSignals – performanceRanges overrides', () => { + let spec: ScSpec; + + beforeEach(() => { + spec = { data: [], marks: [], scales: [], usermeta: {} }; + }); + + test('binds band pct signals to options.performanceRanges', () => { + const performanceRanges = [ + { bandEndPct: 0.4, fill: 'red' }, + { bandEndPct: 0.75, fill: 'yellow' }, + { bandEndPct: 1, fill: 'green' }, + ]; + + const overrides = { + ...defaultGaugeOptions, + performanceRanges, + }; + + const newSpec = addGauge(spec, overrides); + const signals = newSpec.signals as any[]; + + expect(byName(signals, 'band1EndPct')?.value).toBe(0.4); + expect(byName(signals, 'band2EndPct')?.value).toBe(0.75); + expect(byName(signals, 'band3EndPct')?.value).toBe(1); + }); + }); + + }); From c0bc461d1f771eaff1058a1db9809c0fd33619c7 Mon Sep 17 00:00:00 2001 From: danipeterson <112648333+danipeterson@users.noreply.github.com> Date: Thu, 4 Dec 2025 22:31:14 -0700 Subject: [PATCH 64/66] Add default setting for showPerformanceRanges --- .../src/alpha/components/Gauge/Gauge.tsx | 1 + packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts | 4 ++-- packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts | 4 +++- .../vega-spec-builder/src/types/marks/gaugeSpec.types.ts | 9 +++------ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx index 4ad300073..000d09e32 100644 --- a/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx +++ b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx @@ -34,6 +34,7 @@ const Gauge: FC<GaugeProps> = ({ needle = DEFAULT_NEEDLE, targetLine = DEFAULT_TARGET_LINE, target = 'target', + showPerformanceRanges = false, }) => { return null; }; diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index 64b30324f..e326688b6 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -48,7 +48,7 @@ export const addGauge = produce< index = 0, color = DEFAULT_COLOR, performanceRanges, - showPerformanceRanges, + showPerformanceRanges = false, name, ...options } @@ -76,7 +76,7 @@ export const addGauge = produce< needle: false, targetLine: false, performanceRanges: resolvedPerformanceRanges, - showPerformanceRanges: showPerformanceRanges ?? false, + showPerformanceRanges: false, ...options, }; diff --git a/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts index 34b8cc44a..4682117d5 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts @@ -31,5 +31,7 @@ export const defaultGaugeOptions: GaugeSpecOptions = { fillerColorSignal: 'light', needle: false, target: 'target', - targetLine: false + targetLine: false, + showPerformanceRanges: false, + performanceRanges: [] }; diff --git a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts index b3dc9ac3b..671340729 100644 --- a/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts +++ b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts @@ -43,13 +43,9 @@ export interface GaugeOptions { target?: string; /** Showing the target line */ targetLine?: boolean; - /** Performance ranges - * - * Array of performance ranges to be rendered as filled bands on the gauge. - * - */ + /** Array of performance ranges to be rendered as filled bands on the gauge. */ performanceRanges?: PerformanceRanges[]; - /** if true, show banded performance ranges instead of a colored filler arc */ + /** If true, show banded performance ranges instead of a colored filler arc */ showPerformanceRanges?: boolean; } @@ -70,6 +66,7 @@ type GaugeOptionsWithDefaults = | 'target' | 'targetLine' | 'performanceRanges' + | 'showPerformanceRanges' export interface GaugeSpecOptions extends PartiallyRequired<GaugeOptions, GaugeOptionsWithDefaults> { colorScheme: ColorScheme; From 68e18af62f18a2076f19ef99711c61386c41bd57 Mon Sep 17 00:00:00 2001 From: H-bot-hash <H-bot-hash@users.noreply.github.com> Date: Thu, 4 Dec 2025 23:10:34 -0700 Subject: [PATCH 65/66] Updating documentation --- .../docs/docs/api/visualizations/Gauge.md | 140 ++++++++++++++++++ .../src/gauge/gaugeSpecBuilder.test.ts | 2 - 2 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 packages/docs/docs/api/visualizations/Gauge.md diff --git a/packages/docs/docs/api/visualizations/Gauge.md b/packages/docs/docs/api/visualizations/Gauge.md new file mode 100644 index 000000000..9bcca8d66 --- /dev/null +++ b/packages/docs/docs/api/visualizations/Gauge.md @@ -0,0 +1,140 @@ +## ALPHA RELEASE + +Gauge is currently in alpha. This means that the component, behavior and API are all subject to change. + +``` +import { Chart, ChartProps } from '@adobe/react-spectrum-charts'; +import { Gauge, GaugeSummary, SegmentLabel } from '@adobe/react-spectrum-charts/rc'; +``` + +# Gauge +The `Gauge` component is used to display data in a dashboard gauge style. + +## Needle +The gauge can draw a mark needle for progression measurement and data tracking. Disabled by default. + +## Target Line +The target line shows a line representing a goal value for the metric. Disabled by default. + +## Performance Ranges +The gauge can draw `performance ranges` marks for a given series of target ranges, defining color for detailed data tracking. Disabled by default. + +## Dynamic Labeling with unit options +The guage label can be represented as a percentage or numeric value and resizes in relation to the needle being present. + +## Props + +<table> + <thead> + <tr> + <th>name</th> + <th>type</th> + <th>default</th> + <th>description</th> + </tr> + </thead> + <tbody> + <tr> + <td>metric</td> + <td>number</td> + <td>'value'</td> + <td>The data that is used for the current value.</td> + </tr> + <tr> + <td>color</td> + <td>string</td> + <td>'series'</td> + <td>The data that is used as the color to current value.</td> + </tr> + <tr> + <td>name</td> + <td>string</td> + <td>'gauge0'</td> + <td>Sets the name of the component.</td> + </tr> + <tr> + <td>graphLabel</td> + <td>string</td> + <td>'graphLabel'</td> + <td>The data that is used as the graph label.</td> + </tr> + <tr> + <td>showLabel</td> + <td>boolean</td> + <td>false</td> + <td>Sets to show the label or not.</td> + </tr> + <tr> + <td>showsAsPercent</td> + <td>boolean</td> + <td>false</td> + <td>Sets to show the current value as a percentage or not.</td> + </tr> + <tr> + <td>minArcValue</td> + <td>number</td> + <td>0</td> + <td>Minimum value for the scale. This value must be greater than zero, and less than maxArcValue.</td> + </tr> + <tr> + <td>maxArcValue</td> + <td>number</td> + <td>100</td> + <td>Maximum value for the scale. This value must be greater than zero, and greater than minArcValue.</td> + </tr> + <tr> + <td>currVal</td> + <td>number</td> + <td>75</td> + <td>The current value tracked and its progress in the gauge. Set to 75 out of 100 by default.</td> + </tr> + <tr> + <td>backgroundFill</td> + <td>string</td> + <td>-</td> + <td>The color of the background arc.</td> + </tr> + <tr> + <td>backgroundStroke</td> + <td>string</td> + <td>-</td> + <td>The color of the background stroke.</td> + </tr> + <tr> + <td>fillerColorSignal</td> + <td>string</td> + <td>-</td> + <td>The color of the filler color arc.</td> + </tr> + <tr> + <td>needle</td> + <td>boolean</td> + <td>false</td> + <td>The needle mark for tracking progress towards a goal.</td> + </tr> + <tr> + <td>target</td> + <td>string</td> + <td>'target'</td> + <td>The data that is used as the target.</td> + </tr> + <tr> + <td>targetLine</td> + <td>boolean</td> + <td>false</td> + <td>Shows a line for target tracking.</td> + </tr> + <tr> + <td>performanceRanges</td> + <td>-</td> + <td>false</td> + <td>Array of performance ranges to be rendered as filled bands on the gauge.</td> + </tr> + <tr> + <td>showPerformanceRanges</td> + <td>number</td> + <td>0</td> + <td>If true, show banded performance ranges instead of a colored filler arc.</td> + </tr> + </tbody> +</table> diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts index bdb172c66..90ce04b79 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts @@ -113,8 +113,6 @@ describe('addGauge (defaults & overrides for gaugeOptions)', () => { expect(byName(signals, 'clampedVal')?.update).toBe('min(max(arcMinVal, currVal), arcMaxVal)'); expect(byName(signals, 'theta')?.update).toBe("scale('angleScale', clampedVal)"); expect(byName(signals, 'needleAngleClampedVal')?.update).toBe("scale('angleScale', clampedVal)"); - expect(byName(signals, 'needleAngleRaw')?.update).toBeUndefined(); // (if you don't have it anymore; or remove this line) - expect(byName(signals, 'needleAngleDeg')?.update).toBe('needleAngleClampedVal * 180 / PI'); expect(byName(signals, 'needleLength')?.update).toBe('30'); From ccefd8915dd5d454b81ab54f512ad4e7171dd232 Mon Sep 17 00:00:00 2001 From: Brady Shimanek <bradyshimanek33@gmail.com> Date: Mon, 8 Dec 2025 16:33:46 -0700 Subject: [PATCH 66/66] Fix performance ranges not rendering + added values for it's test. --- packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts | 2 +- packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts index e326688b6..bc6994689 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -76,7 +76,7 @@ export const addGauge = produce< needle: false, targetLine: false, performanceRanges: resolvedPerformanceRanges, - showPerformanceRanges: false, + showPerformanceRanges: showPerformanceRanges ?? false, ...options, }; diff --git a/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts index 4682117d5..a1683f8f2 100644 --- a/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts +++ b/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts @@ -33,5 +33,9 @@ export const defaultGaugeOptions: GaugeSpecOptions = { target: 'target', targetLine: false, showPerformanceRanges: false, - performanceRanges: [] + performanceRanges: [ + { bandEndPct: 0.55, fill: spectrumColors[DEFAULT_COLOR_SCHEME]['red-900'] }, + { bandEndPct: 0.8, fill: spectrumColors[DEFAULT_COLOR_SCHEME]['yellow-400'] }, + { bandEndPct: 1, fill: spectrumColors[DEFAULT_COLOR_SCHEME]['green-700'] }, + ] };