diff --git a/package.json b/package.json
index 4696cd8e0..e953461b1 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",
@@ -175,5 +175,6 @@
"React",
"Spectrum",
"Charts"
- ]
+ ],
+ "dependencies": {}
}
diff --git a/packages/constants/constants.ts b/packages/constants/constants.ts
index 804518ac8..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';
@@ -35,6 +36,10 @@ 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_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/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
+
+
+
+
+ | name |
+ type |
+ default |
+ description |
+
+
+
+
+ | metric |
+ number |
+ 'value' |
+ The data that is used for the current value. |
+
+
+ | color |
+ string |
+ 'series' |
+ The data that is used as the color to current value. |
+
+
+ | name |
+ string |
+ 'gauge0' |
+ Sets the name of the component. |
+
+
+ | graphLabel |
+ string |
+ 'graphLabel' |
+ The data that is used as the graph label. |
+
+
+ | showLabel |
+ boolean |
+ false |
+ Sets to show the label or not. |
+
+
+ | showsAsPercent |
+ boolean |
+ false |
+ Sets to show the current value as a percentage or not. |
+
+
+ | minArcValue |
+ number |
+ 0 |
+ Minimum value for the scale. This value must be greater than zero, and less than maxArcValue. |
+
+
+ | maxArcValue |
+ number |
+ 100 |
+ Maximum value for the scale. This value must be greater than zero, and greater than minArcValue. |
+
+
+ | currVal |
+ number |
+ 75 |
+ The current value tracked and its progress in the gauge. Set to 75 out of 100 by default. |
+
+
+ | backgroundFill |
+ string |
+ - |
+ The color of the background arc. |
+
+
+ | backgroundStroke |
+ string |
+ - |
+ The color of the background stroke. |
+
+
+ | fillerColorSignal |
+ string |
+ - |
+ The color of the filler color arc. |
+
+
+ | needle |
+ boolean |
+ false |
+ The needle mark for tracking progress towards a goal. |
+
+
+ | target |
+ string |
+ 'target' |
+ The data that is used as the target. |
+
+
+ | targetLine |
+ boolean |
+ false |
+ Shows a line for target tracking. |
+
+
+ | performanceRanges |
+ - |
+ false |
+ Array of performance ranges to be rendered as filled bands on the gauge. |
+
+
+ | showPerformanceRanges |
+ number |
+ 0 |
+ If true, show banded performance ranges instead of a colored filler arc. |
+
+
+
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..000d09e32
--- /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_MAX_ARC_VALUE,
+ DEFAULT_MIN_ARC_VALUE,
+ DEFAULT_LABEL,
+ DEFAULT_NEEDLE,
+ DEFAULT_TARGET_LINE,
+} from '@spectrum-charts/constants';
+
+import { GaugeProps } from '../../../types';
+
+const Gauge: FC = ({
+ name = 'gauge0',
+ graphLabel = 'graphLabel',
+ showLabel = DEFAULT_LABEL,
+ showsAsPercent = false,
+ metric = 'currentAmount',
+ minArcValue = DEFAULT_MIN_ARC_VALUE,
+ maxArcValue = DEFAULT_MAX_ARC_VALUE,
+ needle = DEFAULT_NEEDLE,
+ targetLine = DEFAULT_TARGET_LINE,
+ target = 'target',
+ showPerformanceRanges = 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..ea2dcf0e4 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/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 =
const { width, height, ...bulletProps } = args;
const chartProps = useChartProps({ ...defaultChartProps, width: width ?? 350, height: height ?? 350 });
return (
-
+
);
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..cc91755ed
--- /dev/null
+++ b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx
@@ -0,0 +1,133 @@
+/*
+ * 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';
+// 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 { GaugeProps, ChartProps } from '../../../types';
+import { basicGaugeData, coloredPerformanceData } from './data';
+
+export default {
+ title: 'RSC/Gauge (alpha)',
+ component: Gauge,
+};
+
+// Default chart properties
+const defaultChartProps: ChartProps = {
+ data: basicGaugeData,
+ width: 500,
+ height: 600,
+};
+
+// Basic Gauge chart story
+const GaugeStory: StoryFn = (args): ReactElement => {
+ const { width, height, ...gaugeProps } = args;
+ const chartProps = useChartProps({ ...defaultChartProps, width: width ?? 500, height: height ?? 500 });
+ return (
+
+
+
+ );
+};
+
+// Gauge with Title
+const GaugeTitleStory: StoryFn = (args): ReactElement => {
+ const chartProps = useChartProps({ ...defaultChartProps, width: 400 });
+ return (
+
+
+
+
+ );
+};
+
+// Basic Gauge chart story. All the ones below it are variations of the Gauge chart.
+const Basic = bindWithProps(GaugeStory);
+Basic.args = {
+ metric: 'currentAmount',
+ color: 'blue-900',
+};
+
+const PerformanceRange = bindWithProps(GaugeStory);
+PerformanceRange.args = {
+ metric: 'currentAmount',
+ color: 'red-900',
+ showPerformanceRanges: true,
+ performanceRanges: coloredPerformanceData,
+};
+
+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',
+ 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,
+ Empty,
+ Full,
+ PerformanceRange,
+ GaugeLabelNoNeedleNoPercent,
+ GaugeLabelNeedleNoPercent,
+ GaugeLabelNoNeedlePercent,
+ GaugeLabelNeedlePercent
+};
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..f6fd8de0e
--- /dev/null
+++ b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.test.tsx
@@ -0,0 +1,40 @@
+/*
+ * 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 { Gauge } from '../../../alpha';
+import { findChart, findMarksByGroupName, render } from '../../../test-utils';
+import { Basic } from './Gauge.story';
+
+describe('Gauge', () => {
+ // Gauge is not a real React component. This test provides test coverage for sonarqube
+ test('Gauge pseudo element', () => {
+ render();
+ });
+
+ test('Basic gauge renders properly', async () => {
+ render();
+ const chart = await findChart();
+ expect(chart).toBeInTheDocument();
+
+ const backgroundArc = await findMarksByGroupName(chart, 'gauge0BackgroundArc');
+ expect(backgroundArc).toBeDefined();
+
+ const fillerArc = await findMarksByGroupName(chart, 'gauge0FillerArc');
+ expect(fillerArc).toBeDefined();
+
+ const startCap = await findMarksByGroupName(chart, 'gauge0StartCap');
+ expect(startCap).toBeDefined();
+
+ const endCap = await findMarksByGroupName(chart, 'gauge0EndCap');
+ expect(endCap).toBeDefined();
+
+ });
+});
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..edfacf019
--- /dev/null
+++ b/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts
@@ -0,0 +1,23 @@
+/*
+ * 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 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/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/gauge.types.ts b/packages/react-spectrum-charts/src/types/marks/gauge.types.ts
new file mode 100644
index 000000000..238c371c3
--- /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';
+
+export interface GaugeProps extends Omit {}
+
+export type GaugeElement = ReactElement>;
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 a61db2215..928287b7c 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';
+import { addGauge } from './gauge/gaugeSpecBuilder';
import { addCombo } from './combo/comboSpecBuilder';
import { getSeriesIdTransform } from './data/dataUtils';
import { addDonut } from './donut/donutSpecBuilder';
@@ -128,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) => {
@@ -148,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 });
@@ -217,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.test.ts b/packages/vega-spec-builder/src/gauge/gaugeDataUtils.test.ts
new file mode 100644
index 000000000..6fb00aaa4
--- /dev/null
+++ b/packages/vega-spec-builder/src/gauge/gaugeDataUtils.test.ts
@@ -0,0 +1,39 @@
+/*
+ * 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 { getGaugeTableData } from './gaugeDataUtils';
+
+describe('getGaugeTableData', () => {
+ it('Should create a new table data if it does not exist', () => {
+ const data: Data[] = [];
+
+ const result = getGaugeTableData(data);
+
+ expect(result.name).toBe('table');
+ expect(result.values).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: [4],
+ };
+ const data: Data[] = [existingTableData];
+
+ const result = getGaugeTableData(data);
+
+ expect(result).toEqual(existingTableData);
+ });
+});
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..8bc25995f
--- /dev/null
+++ b/packages/vega-spec-builder/src/gauge/gaugeDataUtils.ts
@@ -0,0 +1,50 @@
+/*
+ * 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, ValuesData } from 'vega';
+
+import { TABLE } from '@spectrum-charts/constants';
+
+import { getTableData } from '../data/dataUtils';
+
+/**
+ * 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: [],
+ };
+ 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.
+ */
+
+/**
+ * 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 defaultColor The default color to use if no thresholds are met.
+ * @returns A string representing the Vega expression for the color.
+ */
+
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..19c0ed420
--- /dev/null
+++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.test.ts
@@ -0,0 +1,368 @@
+/*
+ * 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 {
+ addGaugeMarks,
+ getBackgroundArc,
+ getFillerArc,
+ getNeedle,
+ 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);
+ expect(data).toBeDefined();
+ expect(Array.isArray(data)).toBe(true);
+ expect(data).toHaveLength(4);
+ expect(data[0].type).toBe('arc');
+ expect(data[1].type).toBe('arc');
+ });
+});
+
+// getGaugeBackgroundArc
+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?.enter).toBeDefined();
+
+ // Expect the correct amount of fields in the update object
+ expect(Object.keys(data.encode?.enter ?? {}).length).toBe(7);
+ });
+});
+
+// getFillerArc
+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);
+
+ expect(data.encode?.enter).toBeDefined();
+ expect(Object.keys(data.encode?.enter ?? {}).length).toBe(7)
+ });
+});
+
+// getGaugeNeedle
+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(data.encode?.enter).toBeDefined();
+ expect(Object.keys(data.encode?.enter ?? {}).length).toBe(2)
+ });
+});
+
+// getDefaultPerformanceRanges
+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']);
+ });
+});
+
+// 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', () => {
+ 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);
+ });
+});
+
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..9c0e8b63f
--- /dev/null
+++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts
@@ -0,0 +1,378 @@
+/*
+ * 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 { Mark } from 'vega';
+
+import { DEFAULT_COLOR_SCHEME, BACKGROUND_COLOR } from '@spectrum-charts/constants';
+
+import { GaugeSpecOptions, PerformanceRanges } from '../types';
+import { spectrumColors, getColorValue } from '@spectrum-charts/themes';
+import { defaultGaugeOptions } from './gaugeTestUtils';
+
+export const addGaugeMarks = produce((marks, opt) => {
+ const {
+ name,
+ colorScheme = DEFAULT_COLOR_SCHEME,
+ needle,
+ showLabel,
+ targetLine
+ } = opt;
+ const backgroundFill = spectrumColors[colorScheme]['gray-200'];
+ const backgroundStroke = spectrumColors[colorScheme]['gray-300'];
+ const fillerColorSignal = 'fillerColorToCurrVal';
+
+ // Performance ranges
+ if (opt.showPerformanceRanges) {
+ marks.push(...getPerformanceRangeMarks(name, opt.performanceRanges));
+ 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));
+ // Filler arc (fills to clampedValue)
+ marks.push(getFillerArc(name, fillerColorSignal));
+ // Add caps
+ marks.push(getStartCap(name, fillerColorSignal, backgroundFill));
+ marks.push(getEndCap(name, fillerColorSignal, backgroundFill));
+ }
+
+ // Needle to clampedValue
+if ((needle || opt.showPerformanceRanges) && showLabel) {
+ marks.push(getNeedle(name));
+ marks.push(getNeedleHole(name, BACKGROUND_COLOR));
+ const yOffset = 140;
+ const fontSize = 28;
+ marks.push(getLabel(name, fontSize, yOffset));
+
+ const labelYOffset = 100;
+ const labelFontSize = 60;
+ marks.push(getValueLabel(name, labelFontSize, labelYOffset));
+ } else if (needle || opt.showPerformanceRanges){
+ marks.push(getNeedle(name));
+ marks.push(getNeedleHole(name, BACKGROUND_COLOR));
+ } else if (showLabel){
+ const yOffset = 70;
+ const fontSize = 32;
+ marks.push(getLabel(name, fontSize, yOffset));
+
+ const labelYOffset = 0;
+ const labelFontSize = 84;
+ marks.push(getValueLabel(name, labelFontSize, labelYOffset));
+ }
+ if (targetLine){
+ marks.push(getTargetLine(name));
+ }
+});
+
+export function getBackgroundArc(name: string, fill: string): Mark {
+ return {
+ name: `${name}BackgroundArc`,
+ description: 'Background Arc',
+ type: 'arc',
+ encode: {
+ enter: {
+ x: { signal: 'centerX' },
+ y: { signal: 'centerY' },
+ innerRadius: { signal: 'innerRadius' },
+ outerRadius: { signal: 'outerRadius' },
+ startAngle: { signal: 'startAngle' },
+ endAngle: { signal: 'endAngle' },
+ fill: { value: fill },
+ }
+ }
+ };
+}
+
+export function getFillerArc(name: string, fillerColorSignal: string): Mark {
+ return {
+ name: `${name}FillerArc`,
+ description: 'Filler Arc',
+ type: 'arc',
+ encode: {
+ enter: {
+ x: { signal: 'centerX' },
+ y: { signal: 'centerY' },
+ innerRadius: { signal: 'innerRadius' },
+ outerRadius: { signal: 'outerRadius' },
+ startAngle: { signal: 'startAngle' },
+ endAngle: { signal: 'endAngle' },
+ fill: { signal: 'fillerColorToCurrVal' }
+ },
+ update: {
+ endAngle: { signal: "scale('angleScale', clampedVal)" },
+ stroke: { signal: `currVal > arcMinVal ? ${fillerColorSignal} : ""`}
+ }
+ }
+ };
+}
+
+export function getNeedle(name: string): Mark {
+ return {
+ name: `${name}Needle`,
+ 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: "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 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 {
+ name: `${name}NeedleHole`,
+ 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 },
+ }
+ }
+ };
+}
+
+export function getTargetLine(name: string): Mark {
+ return {
+ name: `${name}TargetLine`,
+ description: 'Target Line',
+ type: 'rule',
+ encode: {
+ 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" }
+ }
+ }
+ }
+};
+
+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}StartCap`,
+ description: `Start Cap`,
+ type: `arc`,
+ encode: {
+ enter: {
+ "x": { signal: xOffset },
+ "y": { signal: yOffset },
+ "innerRadius": { signal: '0' },
+ "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}`},
+ "stroke": { signal: `currVal <= arcMinVal ? ${backgroundColor} : ${fillColor}`}
+ }
+ }
+ }
+}
+
+export function getEndCap(name: string, fillColor: string, backgroundColor: string): Mark {
+ const xOffset = 'centerX+(sin(endAngle)*((outerRadius+innerRadius)/2))'
+ const yOffset = 'centerY-(cos(endAngle)*((outerRadius+innerRadius)/2))'
+ return {
+ name: `${name}EndCap`,
+ description: `End Cap`,
+ type: `arc`,
+ encode: {
+ enter: {
+ "x": { signal: xOffset },
+ "y": { signal: yOffset },
+ "innerRadius": { signal: '0' },
+ "outerRadius": { signal: "((outerRadius - innerRadius) / 2) - 1" },
+ "startAngle": { signal: "endAngle" },
+ "endAngle": { signal: "endAngle+PI"},
+ "strokeWidth": { signal: "2" },
+ },
+ update: {
+ "fill": {signal: `currVal >= arcMaxVal ? ${fillColor} : ${backgroundColor}`},
+ "stroke": { signal: `currVal >= arcMaxVal ? ${fillColor} : ${backgroundColor}` }
+ }
+ }
+ }
+}
+
+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: { signal: 'labelTextColor' }
+ },
+ update: {
+ x: { signal: "centerX" },
+ y: { signal: `centerY + ${yOffset}` },
+ text: { signal: "graphLabel" }
+ }
+ }
+ }
+};
+
+export function getValueLabel(name: string, fontSize, yOffset): Mark {
+ return {
+ name: `${name}graphLabelCurrentValueText`,
+ description: `graph current value label`,
+ type: `text`,
+ encode: {
+ enter: {
+ align: { value: "center" },
+ baseline: { value: "middle" },
+ fontSize: { value: fontSize },
+ fill: { signal: 'valueTextColor' }
+ },
+ update: {
+ x: { signal: "centerX" },
+ y: { signal: `centerY + ${yOffset}` },
+ text: { signal: "textSignal" }
+ }
+ }
+ }
+};
\ No newline at end of file
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..90ce04b79
--- /dev/null
+++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts
@@ -0,0 +1,235 @@
+/*
+ * 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 { GaugeOptions, ScSpec } from '../types';
+import { addGauge, addData, addScales, addSignals } from './gaugeSpecBuilder';
+import { defaultGaugeOptions } from './gaugeTestUtils';
+
+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 create a spec with gauge chart properties', () => {
+ const newSpec = addGauge(spec, defaultGaugeOptions);
+
+ expect(newSpec).toBeDefined();
+ expect(newSpec).toHaveProperty('data');
+ expect(newSpec).toHaveProperty('marks');
+ expect(newSpec).toHaveProperty('scales');
+ expect(newSpec).toHaveProperty('signals');
+ });
+});
+
+describe('getGaugeScales', () => {
+ test('Should return the correct scale object', () => {
+ const data = addScales([], defaultGaugeOptions);
+ expect(data).toBeDefined();
+ 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');
+ }
+ });
+});
+
+describe('getGaugeSignals', () => {
+ test('Should return the correct signals object', () => {
+ const data = addSignals([], defaultGaugeOptions);
+ expect(data).toBeDefined();
+ expect(data).toHaveLength(55);
+ });
+});
+
+describe('getGaugeData', () => {
+ test('Should return the data object', () => {
+ const data = addData([], defaultGaugeOptions);
+ expect(data).toHaveLength(1);
+ });
+});
+
+describe('addGauge (defaults & overrides for gaugeOptions)', () => {
+ let spec: ScSpec;
+
+ beforeEach(() => {
+ spec = { data: [], marks: [], scales: [], usermeta: {} };
+ });
+
+ test('uses defaults when no overrides are provided', () => {
+ const newSpec = addGauge(spec, defaultGaugeOptions);
+
+ expect(newSpec).toBeDefined();
+ expect(newSpec.signals).toBeDefined();
+
+ const signals = newSpec.signals as any[];
+
+ // min/max come from defaults in gaugeOptions
+ expect(byName(signals, 'arcMaxVal')?.value).toBe(100);
+ expect(byName(signals, 'arcMinVal')?.value).toBe(0);
+
+ // default angles: -120° .. +120°
+ expect(byName(signals, 'startAngle')?.update).toBe('-PI * 2 / 3');
+ expect(byName(signals, 'endAngle')?.update).toBe('PI * 2 / 3');
+
+ // default metric is 'currentAmount'
+ expect(byName(signals, 'currVal')?.update).toBe("data('table')[0].currentAmount");
+
+ // background fill from DEFAULT_COLOR_SCHEME
+ const scheme = DEFAULT_COLOR_SCHEME;
+ const expectedBgFill = spectrumColors[scheme]['gray-200'];
+ expect(byName(signals, 'backgroundfillColor')?.value).toBe(expectedBgFill);
+
+ // 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, '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)', () => {
+ 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 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);
+ expect(byName(signals, 'arcMaxVal')?.value).toBe(500);
+
+ // 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');
+ });
+
+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);
+ });
+ });
+
+
+});
+
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..bc6994689
--- /dev/null
+++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts
@@ -0,0 +1,166 @@
+/*
+ * 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 { Signal, Scale, Data } from 'vega';
+import { addGaugeMarks } from './gaugeMarkUtils';
+
+
+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 { ColorScheme, GaugeOptions, GaugeSpecOptions, PerformanceRanges, ScSpec } from '../types';
+import { getGaugeTableData } from './gaugeDataUtils';
+
+const DEFAULT_COLOR = spectrumColors.light['blue-900'];
+
+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'] },
+];
+
+
+
+/**
+ * Adds a simple Gauge chart to the spec
+ *
+ */
+export const addGauge = produce<
+ ScSpec,
+ [GaugeOptions & { colorScheme?: ColorScheme; index?: number; idKey: string }]
+>(
+ (
+ spec,
+ {
+ colorScheme = DEFAULT_COLOR_SCHEME,
+ index = 0,
+ color = DEFAULT_COLOR,
+ performanceRanges,
+ showPerformanceRanges = false,
+ 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'],
+ color: getColorValue(color, colorScheme),
+ fillerColorSignal: 'fillerColorToCurrVal',
+ graphLabel: 'graphLabel',
+ showsAsPercent: false,
+ showLabel: false,
+ colorScheme: colorScheme,
+ index,
+ maxArcValue: 100,
+ minArcValue: 0,
+ metric: 'currentAmount',
+ target: 'target',
+ name: toCamelCase(name ?? `gauge${index}`),
+ needle: false,
+ targetLine: false,
+ performanceRanges: resolvedPerformanceRanges,
+ showPerformanceRanges: showPerformanceRanges ?? false,
+ ...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);
+
+ }
+);
+
+export const addSignals = produce((signals, options) => {
+ const ranges = options.performanceRanges ?? DEFAULT_PERFORMANCE_RANGES;
+
+ 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" })
+ 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"})
+ 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: '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: '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)"})
+ 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')"})
+
+ 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)" });
+
+ 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((scales, options) => {
+ scales.push({
+ name: 'angleScale',
+ type: 'linear',
+ domain: [{ signal: 'arcMinVal' }, { signal: 'arcMaxVal' }],
+ range: [{ signal: 'startAngle' }, { signal: 'endAngle' }],
+ clamp: true
+ });
+});
+
+export const addData = produce((data, options) => {
+ const tableData = getGaugeTableData(data);
+});
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..a1683f8f2
--- /dev/null
+++ b/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 { 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: DEFAULT_COLOR_SCHEME,
+ idKey: MARK_ID,
+ index: 5,
+ name: 'gaugeTestName',
+ graphLabel: 'graphLabel',
+ showLabel: false,
+ showsAsPercent: false,
+ metric: 'currentAmount',
+ color: DEFAULT_COLOR_SCHEME,
+ minArcValue: 0,
+ maxArcValue: 100,
+ backgroundFill: spectrumColors[DEFAULT_COLOR_SCHEME]['gray-200'],
+ backgroundStroke: spectrumColors[DEFAULT_COLOR_SCHEME]['gray-300'],
+ fillerColorSignal: 'light',
+ needle: false,
+ target: 'target',
+ targetLine: false,
+ showPerformanceRanges: false,
+ 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'] },
+ ]
+};
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
new file mode 100644
index 000000000..671340729
--- /dev/null
+++ b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.ts
@@ -0,0 +1,75 @@
+/*
+ * 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 PerformanceRanges = { bandEndPct: number; fill: string };
+
+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;
+ /** 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 */
+ color?: string;
+ /** 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, and greater than minArcValue */
+ maxArcValue?: number;
+ /** Color of the background arc */
+ backgroundFill?: string;
+ /** Color of the background stroke */
+ backgroundStroke?: string;
+ /** Color of the filler color arc */
+ 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;
+ /** 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;
+
+}
+
+type GaugeOptionsWithDefaults =
+ | 'name'
+ | 'graphLabel'
+ | 'showLabel'
+ | 'showsAsPercent'
+ | 'metric'
+ | 'color'
+ | 'minArcValue'
+ | 'maxArcValue'
+ | 'backgroundFill'
+ | 'backgroundStroke'
+ | 'fillerColorSignal'
+ | 'needle'
+ | 'target'
+ | 'targetLine'
+ | 'performanceRanges'
+ | 'showPerformanceRanges'
+
+export interface GaugeSpecOptions extends PartiallyRequired {
+ 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';
diff --git a/yarn.lock b/yarn.lock
index 5664a7126..132dd5a3f 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"
@@ -8181,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"
@@ -8815,14 +8815,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==