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..c1b2c7e26 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'; @@ -35,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/docs/docs/api/visualizations/Gauge.md b/packages/docs/docs/api/visualizations/Gauge.md new file mode 100644 index 000000000..6c8a60c5d --- /dev/null +++ b/packages/docs/docs/api/visualizations/Gauge.md @@ -0,0 +1,62 @@ +## 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. + +## Data visualization + +Unlike many other chart types, `Gauge` draws two marks (arcs) for a given series, and a mark needle for progression measurement and data tracking. The two arcs shown are the backgrounds arc and the filling color arc, showing the progress based on the current value. + + +## Props + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
nametypedefaultdescription
colorstring'series'Key in the data that is used as the color to current value.
currValnumber75The current value tracked and its progress in the gauge. Set to 75 out of 100 by default.
metricnumber'value'The key in the data that is used for the current value.
maxArcValuenumber100The maximum value of the arc in the gauge. Set to 100 by default.
minArcValuenumber0The minimum value of the arc in the gauge. Set to 0 by default.
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..f7e541287 --- /dev/null +++ b/packages/react-spectrum-charts/src/alpha/components/Gauge/Gauge.tsx @@ -0,0 +1,36 @@ +/* + * 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, +} 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 = ({ + name = 'gauge0', + metric = 'currentAmount', // CurrVal + minArcValue = DEFAULT_MIN_ARC_VALUE, // Min Arc Value + maxArcValue = DEFAULT_MAX_ARC_VALUE, // Max Arc Value +}) => { + 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..5e2d200b1 --- /dev/null +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.story.tsx @@ -0,0 +1,80 @@ +/* + * 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 } 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 ( + + + <Gauge {...args} /> + </Chart> + ); +}; + +// 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 GaugeVariation2 = bindWithProps(GaugeStory); +GaugeVariation2.args = { + metric: 'currentAmount', + color: 'red-900', +}; + +const GaugeVariation3 = bindWithProps(GaugeStory); +GaugeVariation3.args = { + metric: 'currentAmount', + color: 'fuchsia-900', +}; + + + +export { Basic, GaugeVariation2, GaugeVariation3 }; 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..0824494b7 --- /dev/null +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/Gauge.test.tsx @@ -0,0 +1,36 @@ +/* + * 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(<Gauge />); + }); + + test('Basic gauge renders properly', async () => { + render(<Basic {...Basic.args} />); + const chart = await findChart(); + expect(chart).toBeInTheDocument(); + + const backgroundArc = await findMarksByGroupName(chart, 'BackgroundArcRounded'); + expect(backgroundArc).toBeDefined(); + + const fillerArc = await findMarksByGroupName(chart, 'FillerArc'); + expect(fillerArc).toBeDefined(); + + const needleRule = await findMarksByGroupName(chart, 'Needle', 'line'); + expect(needleRule).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..8f527fbd3 --- /dev/null +++ b/packages/react-spectrum-charts/src/stories/components/Gauge/data.ts @@ -0,0 +1,19 @@ +/* + * 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 }, +]; + + 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..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/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..3b2cd2e4c --- /dev/null +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.test.ts @@ -0,0 +1,74 @@ +/* + * 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, +} 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('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(8); + }); +}); + + +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(1); + + expect(data.encode?.enter).toBeDefined(); + expect(Object.keys(data.encode?.enter ?? {}).length).toBe(7) + }); +}); + + +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(3) + }); +}); + 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..a196a84ab --- /dev/null +++ b/packages/vega-spec-builder/src/gauge/gaugeMarkUtils.ts @@ -0,0 +1,100 @@ +/* + * 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 } from '@spectrum-charts/constants'; + +import { GaugeSpecOptions } from '../types'; +import { spectrumColors } from '@spectrum-charts/themes'; + +export const addGaugeMarks = produce<Mark[], [GaugeSpecOptions]>((marks, opt) => { + const { + name, + 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)); + + // Filler arc (fills to clampedValue) + marks.push(getFillerArc(name, fillerColorSignal)); + + // Needle to clampedValue + marks.push(getNeedle(name)); +}); + +export function getBackgroundArc(name: string, fill: string, stroke: string): Mark { + return { + name: `${name}BackgroundArcRounded`, + description: 'Background Arc (Round Edge)', + 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 }, + stroke: { value: stroke } + } + } + }; +} + +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: fillerColorSignal } + }, + update: { + endAngle: { signal: "scale('angleScale', clampedVal)" } + } + } + }; +} + + export function getNeedle(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' } + } + } + }; +} 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..cc159950b --- /dev/null +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.test.ts @@ -0,0 +1,138 @@ +/* + * 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(19); + }); +}); + +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'); + }); + + 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[]; + + // 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'); + }); + +}); + 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..cc255b56e --- /dev/null +++ b/packages/vega-spec-builder/src/gauge/gaugeSpecBuilder.ts @@ -0,0 +1,102 @@ +/* + * 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, ScSpec } from '../types'; +import { getGaugeTableData } from './gaugeDataUtils'; + +const DEFAULT_COLOR = spectrumColors.light['blue-900']; + + +/** + * 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, + ...options + } + ) => { + const gaugeOptions: GaugeSpecOptions = { + backgroundFill: spectrumColors[colorScheme]['gray-200'], + backgroundStroke: spectrumColors[colorScheme]['gray-300'], + color: getColorValue(color, colorScheme), + fillerColorSignal: 'fillerColorToCurrVal', + colorScheme: colorScheme, + index, + maxArcValue: 100, + minArcValue: 0, + metric: 'currentAmount', + name: toCamelCase(name ?? `gauge${index}`), + ...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<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: '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: 'theta', update: "scale('angleScale', clampedVal)"}) +}); + +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 + }); +}); + +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/gauge/gaugeTestUtils.ts b/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts new file mode 100644 index 000000000..79787257f --- /dev/null +++ b/packages/vega-spec-builder/src/gauge/gaugeTestUtils.ts @@ -0,0 +1,29 @@ +/* + * 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', + 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' +}; 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..9467cd068 --- /dev/null +++ b/packages/vega-spec-builder/src/types/marks/gaugeSpec.types.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 { ColorScheme } from '../chartSpec.types'; +import { NumberFormat, PartiallyRequired } from '../specUtil.types'; + +export type ThresholdBackground = { thresholdMin?: number; thresholdMax?: number; fill?: string }; + +export interface GaugeOptions { + /** 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, 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; +} + +type GaugeOptionsWithDefaults = + | 'name' + | 'metric' + | 'color' + | 'minArcValue' + | 'maxArcValue' + | 'backgroundFill' + | 'backgroundStroke' + | 'fillerColorSignal' + +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'; 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==