Skip to content
This repository was archived by the owner on Jun 6, 2025. It is now read-only.

Commit 2a31549

Browse files
committed
Add new LineChartCumulative chart
1 parent 827c6a3 commit 2a31549

File tree

23 files changed

+975
-2
lines changed

23 files changed

+975
-2
lines changed

.storybook/preview.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {themes} from '@storybook/theming';
22
import {PolarisVizProvider} from '@shopify/polaris-viz';
33
import {DARK_THEME, LIGHT_THEME} from '../packages/polaris-viz/src/constants';
44
import {useTheme} from '../packages/polaris-viz/src/hooks';
5+
import variables from '../packages/polaris-viz-core/src/styles/shared/_variables.scss';
56

67
// https://github.com/storybookjs/storybook/issues/548
78
const storiesOrder = {
@@ -163,6 +164,20 @@ export const decorators = [
163164
color: 'rgb(0, 164, 124)',
164165
},
165166
},
167+
LinePositive: {
168+
seriesColors: {
169+
comparison: variables.colorGray40,
170+
single: 'rgba(4, 123, 93, 1)',
171+
all: [variables.colorGray40, 'rgba(4, 123, 93, 1)'],
172+
},
173+
},
174+
LineNegative: {
175+
seriesColors: {
176+
comparison: variables.colorGray40,
177+
single: variables.colorGray120,
178+
all: [variables.colorGray40, variables.colorGray120],
179+
},
180+
},
166181
}}
167182
>
168183
<Container theme={context.args.theme}>
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import {useState, Fragment} from 'react';
2+
import {
3+
useYScale,
4+
LineSeries,
5+
DEFAULT_THEME_NAME,
6+
useChartPositions,
7+
uniqueId,
8+
} from '@shopify/polaris-viz-core';
9+
import type {
10+
XAxisOptions,
11+
YAxisOptions,
12+
LineChartDataSeriesWithDefaults,
13+
LabelFormatter,
14+
} from '@shopify/polaris-viz-core';
15+
16+
import {useIndexForLabels} from '../../hooks/useIndexForLabels';
17+
import {XAxis} from '../XAxis';
18+
import {useLegend} from '../LegendContainer';
19+
import {useTheme, useLinearLabelsAndDimensions} from '../../hooks';
20+
import {ChartElements} from '../ChartElements';
21+
22+
import {PointsAndCrosshair} from './components';
23+
import {useFormatData, useFormattedLabels} from './hooks';
24+
import {yAxisMinMax} from './utilities';
25+
26+
export interface ChartProps {
27+
data: LineChartDataSeriesWithDefaults[];
28+
emptyStateText?: string;
29+
seriesNameFormatter: LabelFormatter;
30+
theme?: string;
31+
xAxisOptions: Required<XAxisOptions>;
32+
yAxisOptions: Required<YAxisOptions>;
33+
fixedActiveIndex: number;
34+
}
35+
36+
export function Chart({
37+
emptyStateText,
38+
data,
39+
seriesNameFormatter,
40+
theme = DEFAULT_THEME_NAME,
41+
xAxisOptions,
42+
yAxisOptions,
43+
fixedActiveIndex,
44+
}: ChartProps) {
45+
const selectedTheme = useTheme(theme);
46+
47+
const [xAxisHeight, setXAxisHeight] = useState(0);
48+
49+
const {height, width} = useLegend({
50+
data: [
51+
{
52+
shape: 'Line',
53+
series: data,
54+
},
55+
],
56+
showLegend: false,
57+
seriesNameFormatter,
58+
});
59+
60+
const indexForLabels = useIndexForLabels(data);
61+
62+
const {formattedLabels} = useFormattedLabels({
63+
data: [data[indexForLabels]],
64+
labelFormatter: xAxisOptions.labelFormatter,
65+
indexToKeep: [0, fixedActiveIndex, data[indexForLabels]?.data.length - 2],
66+
});
67+
68+
const emptyState =
69+
data.length === 0 || data.every((series) => series.data.length === 0);
70+
71+
const {minY, maxY} = yAxisMinMax(data);
72+
73+
const yScaleOptions = {
74+
formatYAxisLabel: yAxisOptions.labelFormatter,
75+
integersOnly: yAxisOptions.integersOnly,
76+
fixedWidth: yAxisOptions.fixedWidth,
77+
maxYOverride: yAxisOptions.maxYOverride,
78+
max: maxY,
79+
min: minY,
80+
ticksOverride: yAxisOptions.ticksOverride,
81+
};
82+
83+
const {yAxisLabelWidth} = useYScale({
84+
...yScaleOptions,
85+
drawableHeight: height,
86+
verticalOverflow: selectedTheme.grid.verticalOverflow,
87+
});
88+
89+
const {longestSeriesLength, longestSeriesIndex} = useFormatData(data);
90+
91+
const {
92+
drawableWidth,
93+
drawableHeight,
94+
chartXPosition,
95+
chartYPosition,
96+
xAxisBounds,
97+
} = useChartPositions({
98+
annotationsHeight: 0,
99+
height,
100+
width,
101+
xAxisHeight,
102+
yAxisWidth: yAxisLabelWidth,
103+
});
104+
105+
const {xAxisDetails, xScale, labels} = useLinearLabelsAndDimensions({
106+
data,
107+
drawableWidth,
108+
hideXAxis: false,
109+
labels: formattedLabels,
110+
longestSeriesLength,
111+
});
112+
113+
const {yScale} = useYScale({
114+
...yScaleOptions,
115+
drawableHeight,
116+
verticalOverflow: selectedTheme.grid.verticalOverflow,
117+
});
118+
119+
if (xScale == null || drawableWidth == null || yAxisLabelWidth == null) {
120+
return null;
121+
}
122+
123+
const halfXAxisLabelWidth = xAxisDetails.labelWidth / 2;
124+
125+
return (
126+
<Fragment>
127+
<ChartElements.Svg
128+
emptyState={emptyState}
129+
emptyStateText={emptyStateText}
130+
height={height}
131+
role="table"
132+
width={width}
133+
>
134+
<XAxis
135+
allowLineWrap={xAxisOptions.allowLineWrap}
136+
ariaHidden
137+
isLinearChart
138+
labels={labels}
139+
labelWidth={xAxisDetails.labelWidth}
140+
onHeightChange={setXAxisHeight}
141+
x={xAxisBounds.x - halfXAxisLabelWidth}
142+
xScale={xScale}
143+
y={xAxisBounds.y}
144+
activeIndex={{
145+
index: fixedActiveIndex,
146+
color: selectedTheme.seriesColors.single as string,
147+
}}
148+
/>
149+
150+
<g transform={`translate(${chartXPosition},${chartYPosition})`}>
151+
<LineSeries
152+
activeLineIndex={-1}
153+
data={data[1]}
154+
index={2}
155+
key={`placeholder-${2}`}
156+
svgDimensions={{height: drawableHeight, width: drawableWidth}}
157+
theme={theme}
158+
xScale={xScale}
159+
yScale={yScale}
160+
type="default"
161+
shouldShowArea={false}
162+
/>
163+
164+
{data.map((singleSeries, index) => {
165+
if (singleSeries.metadata?.isVisuallyHidden === true) {
166+
return null;
167+
}
168+
169+
return (
170+
<LineSeries
171+
activeLineIndex={-1}
172+
data={singleSeries}
173+
index={index}
174+
key={`${name}-${index}`}
175+
svgDimensions={{height: drawableHeight, width: drawableWidth}}
176+
theme={theme}
177+
xScale={xScale}
178+
yScale={yScale}
179+
type="default"
180+
/>
181+
);
182+
})}
183+
184+
<PointsAndCrosshair
185+
activeIndex={fixedActiveIndex}
186+
data={data}
187+
drawableHeight={drawableHeight}
188+
emptyState={emptyState}
189+
longestSeriesIndex={longestSeriesIndex}
190+
theme={theme}
191+
tooltipId={uniqueId('lineChartCumulative')}
192+
xScale={xScale}
193+
yScale={yScale}
194+
/>
195+
</g>
196+
</ChartElements.Svg>
197+
</Fragment>
198+
);
199+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {Fragment} from 'react';
2+
import type {
3+
XAxisOptions,
4+
YAxisOptions,
5+
ChartProps,
6+
} from '@shopify/polaris-viz-core';
7+
import {
8+
InternalChartType,
9+
ChartState,
10+
DEFAULT_CHART_PROPS,
11+
usePolarisVizContext,
12+
useThemeSeriesColors,
13+
} from '@shopify/polaris-viz-core';
14+
15+
import {fillMissingDataPoints} from '../../utilities/fillMissingDataPoints';
16+
import {getLineChartDataWithDefaults} from '../../utilities/getLineChartDataWithDefaults';
17+
import {ChartContainer} from '../ChartContainer';
18+
import {ChartSkeleton} from '../ChartSkeleton';
19+
import {
20+
getXAxisOptionsWithDefaults,
21+
getYAxisOptionsWithDefaults,
22+
} from '../../utilities';
23+
import {useTheme} from '../../hooks';
24+
25+
import {Chart} from './Chart';
26+
27+
export type LineChartCumulativeProps = {
28+
errorText?: string;
29+
emptyStateText?: string;
30+
xAxisOptions?: Partial<XAxisOptions>;
31+
yAxisOptions?: Partial<YAxisOptions>;
32+
scrollContainer?: Element | null;
33+
fixedActiveIndex: number;
34+
} & ChartProps;
35+
36+
export function LineChartCumulative(props: LineChartCumulativeProps) {
37+
const {defaultTheme} = usePolarisVizContext();
38+
39+
const {
40+
data: dataSeries,
41+
emptyStateText,
42+
errorText,
43+
id,
44+
isAnimated,
45+
onError,
46+
seriesNameFormatter = (value) => `${value}`,
47+
state,
48+
theme = defaultTheme,
49+
xAxisOptions,
50+
yAxisOptions,
51+
scrollContainer,
52+
fixedActiveIndex,
53+
} = {
54+
...DEFAULT_CHART_PROPS,
55+
...props,
56+
};
57+
58+
const data = fillMissingDataPoints(dataSeries, true);
59+
60+
const selectedTheme = useTheme(theme);
61+
const seriesColors = useThemeSeriesColors(data, selectedTheme);
62+
63+
const xAxisOptionsWithDefaults = getXAxisOptionsWithDefaults(xAxisOptions);
64+
const yAxisOptionsWithDefaults = getYAxisOptionsWithDefaults(yAxisOptions);
65+
66+
const dataWithDefaults = getLineChartDataWithDefaults(data, seriesColors);
67+
68+
return (
69+
<Fragment>
70+
<ChartContainer
71+
id={id}
72+
data={data}
73+
theme={theme}
74+
isAnimated={isAnimated}
75+
type={InternalChartType.Line}
76+
onError={onError}
77+
scrollContainer={scrollContainer}
78+
>
79+
{state !== ChartState.Success ? (
80+
<ChartSkeleton state={state} errorText={errorText} theme={theme} />
81+
) : (
82+
<Chart
83+
data={dataWithDefaults}
84+
emptyStateText={emptyStateText}
85+
seriesNameFormatter={seriesNameFormatter}
86+
theme={theme}
87+
xAxisOptions={xAxisOptionsWithDefaults}
88+
yAxisOptions={yAxisOptionsWithDefaults}
89+
fixedActiveIndex={fixedActiveIndex}
90+
/>
91+
)}
92+
</ChartContainer>
93+
</Fragment>
94+
);
95+
}

0 commit comments

Comments
 (0)