Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/__stories__/Gauge/Gauge.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type {Meta, StoryObj} from '@storybook/react';

import {Chart} from '../../components';
import {ChartStory} from '../ChartStory';
import {gaugeBasicData, gaugeGradientData, gaugeNeedleData, gaugeSolidData} from '../__data__';

const meta: Meta<typeof Chart> = {
title: 'Gauge',
render: ChartStory,
component: Chart,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component: `Gauge chart displays a single value on an arc with optional threshold zones, a pointer, and a target marker.`,
},
},
},
};

export default meta;

type Story = StoryObj<typeof ChartStory>;

export const GaugeBasic = {
name: 'Basic (marker, 3 thresholds, target)',
args: {
data: gaugeBasicData,
},
} satisfies Story;

export const GaugeNeedle = {
name: 'Needle pointer',
args: {
data: gaugeNeedleData,
},
} satisfies Story;

export const GaugeSolid = {
name: 'Solid pointer',
args: {
data: gaugeSolidData,
},
} satisfies Story;

export const GaugeGradient = {
name: 'Gradient track (continuous)',
args: {
data: gaugeGradientData,
},
} satisfies Story;
83 changes: 83 additions & 0 deletions src/__stories__/__data__/gauge/basic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type {ChartData} from '../../../types';

export const gaugeBasicData: ChartData = {
series: {
data: [
{
type: 'gauge',
name: 'Performance',
value: 65,
min: 0,
max: 100,
unit: '%',
thresholds: [
{value: 40, color: '#FF3D64', label: 'Critical'},
{value: 70, color: '#FFC636', label: 'Warning'},
{value: 100, color: '#8AD554', label: 'Good'},
],
target: 80,
pointer: {type: 'marker'},
},
],
},
};

export const gaugeNeedleData: ChartData = {
series: {
data: [
{
type: 'gauge',
name: 'Temperature',
value: 72,
min: 0,
max: 120,
unit: '°C',
thresholds: [
{value: 40, color: '#8AD554', label: 'Normal'},
{value: 80, color: '#FFC636', label: 'Hot'},
{value: 120, color: '#FF3D64', label: 'Critical'},
],
pointer: {type: 'needle'},
},
],
},
};

export const gaugeSolidData: ChartData = {
series: {
data: [
{
type: 'gauge',
name: 'Progress',
value: 42,
min: 0,
max: 100,
unit: '%',
color: '#4DA2F1',
pointer: {type: 'solid'},
},
],
},
};

export const gaugeGradientData: ChartData = {
series: {
data: [
{
type: 'gauge',
name: 'CPU Load',
value: 68,
min: 0,
max: 100,
unit: '%',
thresholds: [
{value: 33, color: '#8AD554', label: 'Low'},
{value: 66, color: '#FFC636', label: 'Medium'},
{value: 100, color: '#FF3D64', label: 'High'},
],
pointer: {type: 'marker'},
arc: {trackStyle: 'continuous'},
},
],
},
};
1 change: 1 addition & 0 deletions src/__stories__/__data__/gauge/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {gaugeBasicData, gaugeGradientData, gaugeNeedleData, gaugeSolidData} from './basic';
1 change: 1 addition & 0 deletions src/__stories__/__data__/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './area';
export * from './bar-x';
export * from './bar-y';
export * from './gauge';
export * from './other';
export * from './line';
export * from './pie';
Expand Down
33 changes: 33 additions & 0 deletions src/__tests__/gauge-series.visual.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';

import {expect, test} from '@playwright/experimental-ct-react';

import {ChartTestStory} from '../../playwright/components/ChartTestStory';
import {
gaugeBasicData,
gaugeGradientData,
gaugeNeedleData,
gaugeSolidData,
} from '../__stories__/__data__';

test.describe('Gauge series', () => {
test('Basic gauge: value=65, marker pointer, 3 thresholds, target=80', async ({mount}) => {
const component = await mount(<ChartTestStory data={gaugeBasicData} />);
await expect(component.locator('svg')).toHaveScreenshot();
});

test('Needle pointer', async ({mount}) => {
const component = await mount(<ChartTestStory data={gaugeNeedleData} />);
await expect(component.locator('svg')).toHaveScreenshot();
});

test('Solid pointer', async ({mount}) => {
const component = await mount(<ChartTestStory data={gaugeSolidData} />);
await expect(component.locator('svg')).toHaveScreenshot();
});

test('Gradient track (continuous)', async ({mount}) => {
const component = await mount(<ChartTestStory data={gaugeGradientData} />);
await expect(component.locator('svg')).toHaveScreenshot();
});
});
6 changes: 3 additions & 3 deletions src/__tests__/x-range.visual.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import cloneDeep from 'lodash/cloneDeep';
import merge from 'lodash/merge';

import {xRangeBasicData, xRangeContinuousLegendData} from 'src/__stories__/__data__';
import type {ChartData, DeepPartial, XRangeSeriesData} from 'src/types';
import type {ChartData, DeepPartial, XRangeSeries, XRangeSeriesData} from 'src/types';

import {ChartTestStory} from '../../playwright/components/ChartTestStory';

Expand Down Expand Up @@ -119,9 +119,9 @@ test.describe('X-Range series', () => {
...xRangeBasicData,
series: {
data: xRangeBasicData.series.data.map((s) => ({
...s,
...(s as XRangeSeries),
dataLabels: {enabled: true, html: true},
data: s.data.map((d) => ({
data: (s as XRangeSeries).data.map((d) => ({
...d,
label: `<pre>${String((d as XRangeSeriesData).label)}</pre>`,
})),
Expand Down
6 changes: 5 additions & 1 deletion src/components/ChartInner/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ export function hasAtLeastOneSeriesDataPerPlot(
const plotIndex = yAxis?.plotIndex ?? 0;

if (!hasDataMap.get(plotIndex)) {
if (Array.isArray(seriesDataChunk.data) && seriesDataChunk.data.length > 0) {
if (
'data' in seriesDataChunk &&
Array.isArray(seriesDataChunk.data) &&
seriesDataChunk.data.length > 0
) {
hasDataMap.set(plotIndex, true);
}
}
Expand Down
48 changes: 48 additions & 0 deletions src/components/Tooltip/DefaultTooltipContent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
ChartXAxis,
ChartYAxis,
TooltipDataChunk,
TooltipDataChunkGauge,
TooltipDataChunkSankey,
TooltipDataChunkWaterfall,
TreemapSeriesData,
Expand Down Expand Up @@ -315,6 +316,53 @@ export const DefaultTooltipContent = ({
series,
});
}
case 'gauge': {
const gaugeData = (seriesItem as TooltipDataChunkGauge).data;
const zoneColor = gaugeData.zoneColor ?? get(series, 'color');
const valueWithUnit = gaugeData.unit
? `${gaugeData.value} ${gaugeData.unit}`
: String(gaugeData.value);
// Empty spacer keeps the color column consistent across all gauge rows
const emptyColorCell = (
<span style={{display: 'inline-block', width: 8, height: 8}} />
);

return (
<React.Fragment key={id}>
{renderRow({
id,
color: zoneColor,
name: series.name,
value: gaugeData.value,
formattedValue: valueWithUnit,
series,
})}
{gaugeData.zoneLabel && (
<Row
colorSymbol={emptyColorCell}
label={gaugeData.zoneLabel}
value={
gaugeData.zoneMin !== undefined &&
gaugeData.zoneMax !== undefined
? `${gaugeData.zoneMin} – ${gaugeData.zoneMax}`
: undefined
}
/>
)}
{gaugeData.distanceToTarget !== undefined && (
<Row
colorSymbol={emptyColorCell}
label="vs target"
value={
gaugeData.distanceToTarget > 0
? `+${gaugeData.distanceToTarget}`
: String(gaugeData.distanceToTarget)
}
/>
)}
</React.Fragment>
);
}
default: {
return null;
}
Expand Down
5 changes: 4 additions & 1 deletion src/components/Tooltip/DefaultTooltipContent/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const getMeasureValue = ({
}) => {
if (
data.every((item) =>
['pie', 'treemap', 'waterfall', 'sankey', 'heatmap', 'funnel'].includes(
['gauge', 'pie', 'treemap', 'waterfall', 'sankey', 'heatmap', 'funnel'].includes(
item.series.type,
),
)
Expand Down Expand Up @@ -141,6 +141,9 @@ export function getHoveredValues(args: {
case 'bar-y': {
return getXRowData(data, xAxis);
}
case 'gauge': {
return (data as {value: number}).value;
}
case 'pie':
case 'radar':
case 'heatmap':
Expand Down
1 change: 1 addition & 0 deletions src/core/constants/chart-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export const SERIES_TYPE = {
Area: 'area',
BarX: 'bar-x',
BarY: 'bar-y',
Gauge: 'gauge',
Line: 'line',
Pie: 'pie',
Scatter: 'scatter',
Expand Down
72 changes: 72 additions & 0 deletions src/core/series/prepare-gauge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type {ScaleOrdinal} from 'd3-scale';

import type {GaugeSeries} from '../../types';
import type {PreparedGaugeSeries} from '../shapes/gauge/types';
import {GAUGE_DEFAULTS} from '../types/chart/gauge';
import {getUniqId} from '../utils';

import type {PreparedLegend, PreparedSeries} from './types';
import {prepareLegendSymbol} from './utils';

type PrepareGaugeSeriesArgs = {
series: GaugeSeries;
legend: PreparedLegend;
colorScale: ScaleOrdinal<string, string>;
};

export function prepareGaugeSeries({series, colorScale}: PrepareGaugeSeriesArgs): PreparedSeries[] {
const id = getUniqId();
const name = series.name ?? 'Gauge';
const color = series.color ?? colorScale(name);

const arcConfig = {
sweepAngle: GAUGE_DEFAULTS.arc.sweepAngle,
trackStyle: GAUGE_DEFAULTS.arc.trackStyle,
thickness: GAUGE_DEFAULTS.arc.thickness as number | string,
cornerRadius: GAUGE_DEFAULTS.arc.cornerRadius,
...series.arc,
} as Required<{
sweepAngle: number;
trackStyle: 'discrete' | 'continuous';
thickness: number | string;
cornerRadius: number;
}>;

const pointerConfig = {
type: GAUGE_DEFAULTS.pointer.type,
size: GAUGE_DEFAULTS.pointer.size,
...series.pointer,
} as {
type: 'marker' | 'needle' | 'solid';
color?: string;
size: number;
};

const result: PreparedGaugeSeries = {
type: 'gauge',
id,
color,
name,
visible: series.visible !== false,
legend: {
enabled: series.legend?.enabled ?? false,
symbol: prepareLegendSymbol({}),
groupId: getUniqId(),
itemText: name,
},
cursor: series.cursor ?? null,
tooltip: series.tooltip,
value: series.value,
min: series.min ?? GAUGE_DEFAULTS.min,
max: series.max ?? GAUGE_DEFAULTS.max,
thresholds: series.thresholds ?? [],
target: series.target,
unit: series.unit,
customContent: series.customContent,
arc: arcConfig,
pointer: pointerConfig,
overflow: series.overflow ?? GAUGE_DEFAULTS.overflow,
};

return [result];
}
Loading
Loading