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

Commit dfeb89e

Browse files
committed
Add new LineChartCumulative chart
1 parent 1e89662 commit dfeb89e

File tree

28 files changed

+963
-1
lines changed

28 files changed

+963
-1
lines changed

.storybook/preview.js

Lines changed: 1 addition & 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 = {

packages/polaris-viz/src/components/Labels/hooks/useLabels.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ interface Props {
2121
targetWidth: number;
2222
onHeightChange?: Dispatch<SetStateAction<number>> | (() => void);
2323
align?: 'center' | 'left';
24+
activeIndex?: number;
25+
fillColor?: string;
2426
}
2527

2628
export function useLabels({
@@ -29,6 +31,8 @@ export function useLabels({
2931
labels,
3032
onHeightChange = () => {},
3133
targetWidth,
34+
activeIndex,
35+
fillColor,
3236
}: Props) {
3337
const {characterWidths} = useChartContext();
3438

@@ -77,6 +81,8 @@ export function useLabels({
7781
targetWidth,
7882
targetHeight: HORIZONTAL_LABEL_TARGET_HEIGHT,
7983
characterWidths,
84+
activeIndex,
85+
fillColor,
8086
});
8187
}
8288
case shouldDrawDiagonal: {
@@ -86,6 +92,8 @@ export function useLabels({
8692
longestLabelWidth,
8793
targetHeight: LINE_HEIGHT,
8894
targetWidth,
95+
activeIndex,
96+
fillColor,
8997
});
9098
}
9199
case shouldDrawVertical: {
@@ -94,6 +102,8 @@ export function useLabels({
94102
labels: preparedLabels,
95103
longestLabelWidth,
96104
targetWidth,
105+
activeIndex,
106+
fillColor,
97107
});
98108
}
99109
default: {
@@ -111,6 +121,8 @@ export function useLabels({
111121
characterWidths,
112122
preparedLabels,
113123
longestLabelWidth,
124+
activeIndex,
125+
fillColor,
114126
]);
115127

116128
useEffect(() => {

packages/polaris-viz/src/components/Labels/utilities/getDiagonalLabels.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ interface Props {
1717
targetHeight: number;
1818
targetWidth: number;
1919
characterWidths: CharacterWidths;
20+
activeIndex?: number;
21+
fillColor?: string;
2022
}
2123

2224
export function getDiagonalLabels({
@@ -25,6 +27,8 @@ export function getDiagonalLabels({
2527
longestLabelWidth,
2628
targetHeight,
2729
targetWidth,
30+
activeIndex,
31+
fillColor,
2832
}: Props) {
2933
const clampedTargetWidth = clamp({
3034
amount: longestLabelWidth,
@@ -44,6 +48,7 @@ export function getDiagonalLabels({
4448
const centerPoint = targetWidth / 2 - LINE_HEIGHT / 2;
4549

4650
for (let i = 0; i < labels.length; i++) {
51+
const isActiveIndex = activeIndex === i;
4752
lines[i] = [];
4853
lines[i].push({
4954
truncatedText: truncatedLabels[i].truncatedName,
@@ -56,6 +61,8 @@ export function getDiagonalLabels({
5661
height: LINE_HEIGHT,
5762
textAnchor: 'end',
5863
transform: `rotate(-45)`,
64+
style: isActiveIndex ? {fontWeight: '600'} : undefined,
65+
fill: isActiveIndex ? fillColor : undefined,
5966
});
6067
}
6168

packages/polaris-viz/src/components/Labels/utilities/getHorizontalLabels.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ interface Props {
1616
targetHeight: number;
1717
targetWidth: number;
1818
characterWidths: CharacterWidths;
19+
activeIndex?: number;
20+
fillColor?: string;
1921
}
2022

2123
export function getHorizontalLabels({
@@ -25,6 +27,8 @@ export function getHorizontalLabels({
2527
targetHeight,
2628
targetWidth,
2729
characterWidths,
30+
activeIndex,
31+
fillColor,
2832
}: Props) {
2933
const truncatedLabels = truncateLabels({
3034
labels,
@@ -42,6 +46,7 @@ export function getHorizontalLabels({
4246
let line = '';
4347
let lineNumber = 0;
4448
const words = label.truncatedWords;
49+
const isActiveIndex = activeIndex === index;
4550

4651
// The reason we use a for loop here is we want
4752
// to be able to advance the loop below if we
@@ -84,6 +89,8 @@ export function getHorizontalLabels({
8489
height: LINE_HEIGHT,
8590
textAnchor: align === 'left' ? 'start' : 'middle',
8691
dominantBaseline: 'hanging',
92+
style: isActiveIndex ? {fontWeight: '600'} : undefined,
93+
fill: isActiveIndex ? fillColor : undefined,
8794
});
8895

8996
lineNumber += 1;

packages/polaris-viz/src/components/Labels/utilities/getVerticalLabels.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,17 @@ interface Props {
1313
longestLabelWidth: number;
1414
labels: PreparedLabels[];
1515
characterWidths: CharacterWidths;
16+
activeIndex?: number;
17+
fillColor?: string;
1618
}
1719

1820
export function getVerticalLabels({
1921
labels,
2022
characterWidths,
2123
longestLabelWidth,
2224
targetWidth,
25+
activeIndex,
26+
fillColor,
2327
}: Props) {
2428
const clampedTargetWidth = clamp({
2529
amount: longestLabelWidth,
@@ -38,6 +42,7 @@ export function getVerticalLabels({
3842
let longestString = 0;
3943

4044
for (let i = 0; i < labels.length; i++) {
45+
const isActiveIndex = activeIndex === i;
4146
lines[i] = [];
4247
lines[i].push({
4348
truncatedText: truncatedLabels[i].truncatedName,
@@ -49,6 +54,8 @@ export function getVerticalLabels({
4954
height: LINE_HEIGHT,
5055
textAnchor: 'end',
5156
transform: `translate(${targetWidth / 2}) rotate(-90)`,
57+
style: isActiveIndex ? {fontWeight: '600'} : undefined,
58+
fill: isActiveIndex ? fillColor : undefined,
5259
});
5360

5461
if (truncatedLabels[i].truncatedWidth > longestString) {
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
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 - 1],
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 {longestSeriesLength, longestSeriesIndex} = useFormatData(data);
84+
85+
const {
86+
drawableWidth,
87+
drawableHeight,
88+
chartXPosition,
89+
chartYPosition,
90+
xAxisBounds,
91+
} = useChartPositions({
92+
annotationsHeight: 0,
93+
height,
94+
width,
95+
xAxisHeight,
96+
yAxisWidth: 0,
97+
});
98+
99+
const {xAxisDetails, xScale, labels} = useLinearLabelsAndDimensions({
100+
data,
101+
drawableWidth,
102+
hideXAxis: false,
103+
labels: formattedLabels,
104+
longestSeriesLength,
105+
});
106+
107+
const {yScale} = useYScale({
108+
...yScaleOptions,
109+
drawableHeight,
110+
verticalOverflow: selectedTheme.grid.verticalOverflow,
111+
});
112+
113+
if (xScale == null || drawableWidth == null) {
114+
return null;
115+
}
116+
117+
const halfXAxisLabelWidth = xAxisDetails.labelWidth / 2;
118+
const fillColor = data[data.length - 1].color as string;
119+
120+
return (
121+
<Fragment>
122+
<ChartElements.Svg
123+
emptyState={emptyState}
124+
emptyStateText={emptyStateText}
125+
height={height}
126+
role="table"
127+
width={width}
128+
>
129+
<XAxis
130+
allowLineWrap={xAxisOptions.allowLineWrap}
131+
ariaHidden
132+
labels={labels}
133+
labelWidth={xAxisDetails.labelWidth}
134+
onHeightChange={setXAxisHeight}
135+
x={xAxisBounds.x - halfXAxisLabelWidth}
136+
xScale={xScale}
137+
y={xAxisBounds.y}
138+
activeIndex={fixedActiveIndex}
139+
fillColor={fillColor}
140+
/>
141+
142+
<g transform={`translate(${chartXPosition},${chartYPosition})`}>
143+
{data.map((singleSeries, index) => {
144+
return (
145+
<LineSeries
146+
activeLineIndex={-1}
147+
data={singleSeries}
148+
index={index}
149+
key={`${name}-${index}`}
150+
svgDimensions={{height: drawableHeight, width: drawableWidth}}
151+
theme={theme}
152+
xScale={xScale}
153+
yScale={yScale}
154+
type="default"
155+
/>
156+
);
157+
})}
158+
159+
<PointsAndCrosshair
160+
activeIndex={fixedActiveIndex}
161+
data={data}
162+
drawableHeight={drawableHeight}
163+
emptyState={emptyState}
164+
longestSeriesIndex={longestSeriesIndex}
165+
theme={theme}
166+
tooltipId={uniqueId('lineChartCumulative')}
167+
xScale={xScale}
168+
yScale={yScale}
169+
/>
170+
</g>
171+
</ChartElements.Svg>
172+
</Fragment>
173+
);
174+
}

0 commit comments

Comments
 (0)