@automattic/charts 2.0.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/TOKENS.md +109 -50
- package/dist/index.cjs +529 -233
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +118 -55
- package/dist/index.d.cts +90 -48
- package/dist/index.d.ts +90 -48
- package/dist/index.js +527 -235
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.module.scss +8 -3
- package/src/charts/area-chart/area-chart.tsx +25 -25
- package/src/charts/area-chart/private/overlays.tsx +3 -3
- package/src/charts/area-chart/test/area-chart.test.tsx +301 -11
- package/src/charts/area-chart/types.ts +4 -3
- package/src/charts/bar-chart/bar-chart.module.scss +14 -6
- package/src/charts/bar-chart/bar-chart.tsx +21 -15
- package/src/charts/bar-chart/test/bar-chart.test.tsx +255 -2
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +23 -18
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +107 -86
- package/src/charts/conversion-funnel-chart/test/chart-scope.test.tsx +36 -0
- package/src/charts/geo-chart/geo-chart.module.scss +8 -0
- package/src/charts/geo-chart/geo-chart.tsx +12 -4
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +22 -9
- package/src/charts/heatmap-chart/heatmap-chart.tsx +43 -13
- package/src/charts/heatmap-chart/private/build-calendar-data.ts +65 -5
- package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +125 -0
- package/src/charts/heatmap-chart/types.ts +8 -0
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +13 -7
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +17 -22
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +107 -1
- package/src/charts/leaderboard-chart/types.ts +0 -2
- package/src/charts/line-chart/line-chart.module.scss +12 -7
- package/src/charts/line-chart/line-chart.tsx +32 -28
- package/src/charts/line-chart/private/line-chart-annotation.tsx +4 -4
- package/src/charts/line-chart/private/line-chart-annotations-overlay.tsx +2 -2
- package/src/charts/line-chart/test/line-chart.test.tsx +262 -5
- package/src/charts/line-chart/types.ts +4 -3
- package/src/charts/pie-chart/pie-chart.tsx +12 -14
- package/src/charts/pie-chart/test/pie-chart.test.tsx +73 -1
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +2 -2
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +12 -14
- package/src/charts/private/{single-chart-context/single-chart-context.tsx → chart-instance-context/chart-instance-context.ts} +1 -5
- package/src/charts/private/chart-instance-context/index.ts +3 -0
- package/src/charts/private/{single-chart-context/use-single-chart-context.ts → chart-instance-context/use-chart-instance-context.ts} +1 -3
- package/src/charts/private/svg-empty-state/get-all-hidden-message.ts +41 -0
- package/src/charts/private/svg-empty-state/index.ts +1 -0
- package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
- package/src/charts/private/svg-empty-state/test/get-all-hidden-message.test.ts +15 -0
- package/src/charts/private/with-responsive/test/with-responsive.test.tsx +16 -0
- package/src/charts/private/with-responsive/with-responsive.tsx +18 -11
- package/src/charts/private/x-zoom/test/x-zoom.test.tsx +2 -2
- package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
- package/src/charts/private/x-zoom/x-zoom.tsx +2 -2
- package/src/components/legend/legend.tsx +4 -4
- package/src/components/legend/private/base-legend.module.scss +1 -1
- package/src/components/legend/private/base-legend.tsx +70 -19
- package/src/components/legend/test/legend.test.tsx +100 -4
- package/src/components/tooltip/accessible-tooltip.tsx +17 -4
- package/src/components/tooltip/base-tooltip.tsx +8 -1
- package/src/components/trend-indicator/test/trend-indicator.test.tsx +20 -0
- package/src/components/trend-indicator/trend-indicator.module.scss +4 -7
- package/src/components/trend-indicator/trend-indicator.tsx +3 -0
- package/src/hooks/index.ts +1 -1
- package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +28 -0
- package/src/hooks/test/use-xychart-theme-style-reads.test.tsx +19 -13
- package/src/hooks/test/use-xychart-theme.test.tsx +25 -0
- package/src/hooks/{use-interactive-legend-data.ts → use-legend-visibility-data.ts} +29 -30
- package/src/hooks/use-xychart-theme.ts +6 -4
- package/src/index.ts +12 -3
- package/src/providers/chart-context/global-charts-provider.tsx +127 -30
- package/src/providers/chart-context/hooks/use-default-hidden-series.ts +47 -0
- package/src/providers/chart-context/private/theme-override-vars.ts +87 -0
- package/src/providers/chart-context/private/with-catalog-pointers.ts +50 -0
- package/src/providers/chart-context/test/chart-context.test.tsx +142 -40
- package/src/providers/chart-context/test/chart-scope-class.test.tsx +19 -0
- package/src/providers/chart-context/test/sparse-theme.test.ts +35 -0
- package/src/providers/chart-context/test/theme-override-vars-guard.test.tsx +29 -0
- package/src/providers/chart-context/test/theme-override-vars.test.ts +118 -0
- package/src/providers/chart-context/test/use-default-hidden-series-paint.test.tsx +56 -0
- package/src/providers/chart-context/test/use-default-hidden-series.ssr.test.tsx +39 -0
- package/src/providers/chart-context/test/use-default-hidden-series.test.tsx +222 -0
- package/src/providers/chart-context/test/with-catalog-pointers.test.ts +47 -0
- package/src/providers/chart-context/themes.ts +18 -27
- package/src/providers/chart-context/types.ts +7 -4
- package/src/providers/chart-scope/chart-scope-context.ts +8 -0
- package/src/providers/chart-scope/index.ts +3 -0
- package/src/providers/chart-scope/test/chart-scope.test.tsx +24 -0
- package/src/providers/chart-scope/test/use-standalone-scope-class.test.tsx +20 -0
- package/src/providers/chart-scope/use-chart-scope-element.ts +9 -0
- package/src/providers/chart-scope/use-standalone-scope-class.ts +13 -0
- package/src/providers/index.ts +1 -0
- package/src/styles/chart-scope-class.ts +6 -0
- package/src/styles/chart-scope.scss +83 -0
- package/src/styles/test/chart-scope.test.ts +165 -0
- package/src/styles/test/wpds-fallbacks.test.ts +131 -0
- package/src/types.ts +12 -34
- package/src/charts/private/grid-control/grid-control.module.scss +0 -8
- package/src/charts/private/grid-control/grid-control.tsx +0 -26
- package/src/charts/private/grid-control/index.ts +0 -1
- package/src/charts/private/grid-control/test/grid-control.test.tsx +0 -56
- package/src/charts/private/single-chart-context/index.ts +0 -8
|
@@ -10,6 +10,36 @@ export type CalendarHeatmapResult = {
|
|
|
10
10
|
/** Rows that get a weekday label (Mon, Wed, Fri with a Monday week start). */
|
|
11
11
|
const LABELLED_ROWS = [ 0, 2, 4 ];
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Resolve one grid bound: an unparseable or narrowing value falls back to the
|
|
15
|
+
* series' own bound, so the grid can only ever be widened.
|
|
16
|
+
*
|
|
17
|
+
* @param bound - Requested bound as `yyyy-MM-dd`, if any.
|
|
18
|
+
* @param fallback - The series' own bound on this side.
|
|
19
|
+
* @param direction - Which way the bound is allowed to move.
|
|
20
|
+
* @return The bound to draw to.
|
|
21
|
+
*/
|
|
22
|
+
const widenTo = (
|
|
23
|
+
bound: string | undefined,
|
|
24
|
+
fallback: Date,
|
|
25
|
+
direction: 'earlier' | 'later'
|
|
26
|
+
): Date => {
|
|
27
|
+
if ( ! bound ) {
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const parsed = parseISO( bound );
|
|
32
|
+
if ( isNaN( parsed.getTime() ) ) {
|
|
33
|
+
return fallback;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if ( direction === 'earlier' ) {
|
|
37
|
+
return parsed < fallback ? parsed : fallback;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return parsed > fallback ? parsed : fallback;
|
|
41
|
+
};
|
|
42
|
+
|
|
13
43
|
const toDate = ( point: DataPointDate ): Date | null => {
|
|
14
44
|
if ( point.date instanceof Date && ! isNaN( point.date.getTime() ) ) {
|
|
15
45
|
return point.date;
|
|
@@ -34,6 +64,16 @@ export const buildCalendarHeatmapData = (
|
|
|
34
64
|
* blank cells even when the series has no entry for them.
|
|
35
65
|
*/
|
|
36
66
|
hideOutOfRangeDays?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Draw the grid over this span (`yyyy-MM-dd` bounds) instead of the
|
|
69
|
+
* series' own. Days inside the grid but outside the series become
|
|
70
|
+
* placeholder cells: painted as empty slots so a short series still
|
|
71
|
+
* fills its container, but reporting nothing, since they were never
|
|
72
|
+
* measured. A start bound is drawn from the beginning of its week, so
|
|
73
|
+
* the grid always opens on a whole column. Bounds narrower than the
|
|
74
|
+
* series are ignored — the grid never drops a day that carries data.
|
|
75
|
+
*/
|
|
76
|
+
gridSpan?: { start?: string; end?: string };
|
|
37
77
|
} = {}
|
|
38
78
|
): CalendarHeatmapResult => {
|
|
39
79
|
const weekStartsOn = options.weekStartsOn ?? 1;
|
|
@@ -60,14 +100,27 @@ export const buildCalendarHeatmapData = (
|
|
|
60
100
|
}
|
|
61
101
|
}
|
|
62
102
|
|
|
63
|
-
const gridStart = startOfWeek( minDate, { weekStartsOn } );
|
|
64
|
-
const weekCount = differenceInCalendarWeeks( maxDate, gridStart, { weekStartsOn } ) + 1;
|
|
65
|
-
|
|
66
103
|
// Day-key bounds for the ragged-edge option: calendar-day comparison, so
|
|
67
104
|
// entries carrying a time of day can't shift the span.
|
|
68
105
|
const minDayKey = format( minDate, 'yyyy-MM-dd' );
|
|
69
106
|
const maxDayKey = format( maxDate, 'yyyy-MM-dd' );
|
|
70
107
|
|
|
108
|
+
// The grid may run wider than the series, never narrower: a bound that would
|
|
109
|
+
// cut into the data is dropped rather than honoured.
|
|
110
|
+
const gridMinDate = widenTo( options.gridSpan?.start, minDate, 'earlier' );
|
|
111
|
+
const gridMaxDate = widenTo( options.gridSpan?.end, maxDate, 'later' );
|
|
112
|
+
|
|
113
|
+
const gridMaxDayKey = format( gridMaxDate, 'yyyy-MM-dd' );
|
|
114
|
+
|
|
115
|
+
const gridStart = startOfWeek( gridMinDate, { weekStartsOn } );
|
|
116
|
+
|
|
117
|
+
// A start bound rarely lands on a week start, and `gridStart` rounds it down.
|
|
118
|
+
// The days it rounds past were no more measured than the rest of the widened
|
|
119
|
+
// span, so they are filler too; treating them as a ragged edge instead would
|
|
120
|
+
// notch the first column by however far the bound sat into its week.
|
|
121
|
+
const gridMinDayKey = format( gridMinDate < minDate ? gridStart : gridMinDate, 'yyyy-MM-dd' );
|
|
122
|
+
const weekCount = differenceInCalendarWeeks( gridMaxDate, gridStart, { weekStartsOn } ) + 1;
|
|
123
|
+
|
|
71
124
|
const rowLabels = Array.from( { length: 7 }, ( _, row ) =>
|
|
72
125
|
LABELLED_ROWS.includes( row ) ? format( addDays( gridStart, row ), 'EEE' ) : ''
|
|
73
126
|
);
|
|
@@ -104,8 +157,15 @@ export const buildCalendarHeatmapData = (
|
|
|
104
157
|
label: format( day, 'EEE, MMM d, yyyy' ),
|
|
105
158
|
value: valueByDay.has( key ) ? ( valueByDay.get( key ) as number | null ) : null,
|
|
106
159
|
};
|
|
107
|
-
if (
|
|
108
|
-
|
|
160
|
+
if ( key < gridMinDayKey || key > gridMaxDayKey ) {
|
|
161
|
+
// The days completing the grid's first/last week: the calendar's
|
|
162
|
+
// ragged edge.
|
|
163
|
+
if ( hideOutOfRangeDays ) {
|
|
164
|
+
cell.hidden = true;
|
|
165
|
+
}
|
|
166
|
+
} else if ( key < minDayKey || key > maxDayKey ) {
|
|
167
|
+
// Inside the grid the caller asked for, outside the series: filler.
|
|
168
|
+
cell.placeholder = true;
|
|
109
169
|
}
|
|
110
170
|
cells.push( cell );
|
|
111
171
|
}
|
|
@@ -142,3 +142,128 @@ describe( 'buildCalendarHeatmapData', () => {
|
|
|
142
142
|
expect( rowLabels[ 4 ] ).toBe( 'Thu' );
|
|
143
143
|
} );
|
|
144
144
|
} );
|
|
145
|
+
|
|
146
|
+
describe( 'buildCalendarHeatmapData with a grid wider than the series', () => {
|
|
147
|
+
// Mon Jan 1 2024 through Sun Jan 7 — one whole week of data.
|
|
148
|
+
const oneWeek: DataPointDate[] = [
|
|
149
|
+
{ dateString: '2024-01-01', value: 3 },
|
|
150
|
+
{ dateString: '2024-01-07', value: 4 },
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
test( 'draws the extra weeks and marks them placeholders, not blank cells', () => {
|
|
154
|
+
const { data } = buildCalendarHeatmapData( oneWeek, {
|
|
155
|
+
weekStartsOn: 1,
|
|
156
|
+
gridSpan: { start: '2023-12-04' }, // 4 weeks earlier
|
|
157
|
+
} );
|
|
158
|
+
|
|
159
|
+
expect( data ).toHaveLength( 5 );
|
|
160
|
+
|
|
161
|
+
// The filler weeks stand for days nothing was measured for.
|
|
162
|
+
expect( data[ 0 ].data.every( cell => cell.placeholder === true ) ).toBe( true );
|
|
163
|
+
expect( data[ 3 ].data.every( cell => cell.placeholder === true ) ).toBe( true );
|
|
164
|
+
|
|
165
|
+
// The data week is untouched: real values, no placeholder flag.
|
|
166
|
+
expect( data[ 4 ].data[ 0 ] ).toMatchObject( { value: 3 } );
|
|
167
|
+
expect( data[ 4 ].data[ 0 ].placeholder ).toBeUndefined();
|
|
168
|
+
expect( data[ 4 ].data[ 6 ] ).toMatchObject( { value: 4 } );
|
|
169
|
+
} );
|
|
170
|
+
|
|
171
|
+
test( 'labels the filler columns so the grid still reads as a calendar', () => {
|
|
172
|
+
const { data } = buildCalendarHeatmapData( oneWeek, {
|
|
173
|
+
weekStartsOn: 1,
|
|
174
|
+
gridSpan: { start: '2023-12-04' },
|
|
175
|
+
} );
|
|
176
|
+
|
|
177
|
+
expect( data.map( column => column.label ) ).toEqual( [ 'Dec', '', '', '', 'Jan' ] );
|
|
178
|
+
} );
|
|
179
|
+
|
|
180
|
+
test( 'ignores a bound that would cut into the series', () => {
|
|
181
|
+
const { data } = buildCalendarHeatmapData( oneWeek, {
|
|
182
|
+
weekStartsOn: 1,
|
|
183
|
+
gridSpan: { start: '2024-01-04', end: '2024-01-05' },
|
|
184
|
+
} );
|
|
185
|
+
|
|
186
|
+
expect( data ).toHaveLength( 1 );
|
|
187
|
+
expect( data[ 0 ].data[ 0 ].value ).toBe( 3 );
|
|
188
|
+
expect( data[ 0 ].data[ 6 ].value ).toBe( 4 );
|
|
189
|
+
} );
|
|
190
|
+
|
|
191
|
+
test( 'falls back to the series span for an unparseable bound', () => {
|
|
192
|
+
const { data } = buildCalendarHeatmapData( oneWeek, {
|
|
193
|
+
weekStartsOn: 1,
|
|
194
|
+
gridSpan: { start: 'not-a-date' },
|
|
195
|
+
} );
|
|
196
|
+
|
|
197
|
+
expect( data ).toHaveLength( 1 );
|
|
198
|
+
} );
|
|
199
|
+
|
|
200
|
+
test( 'separates the ragged edge from the filler', () => {
|
|
201
|
+
// The grid opens Mon Dec 25 and closes with the series on Wed Jan 3, so
|
|
202
|
+
// Thu Jan 4 onward only completes the last week.
|
|
203
|
+
const { data } = buildCalendarHeatmapData( [ { dateString: '2024-01-03', value: 5 } ], {
|
|
204
|
+
weekStartsOn: 1,
|
|
205
|
+
gridSpan: { start: '2023-12-25' },
|
|
206
|
+
} );
|
|
207
|
+
|
|
208
|
+
const lastColumn = data[ data.length - 1 ];
|
|
209
|
+
|
|
210
|
+
// Inside the requested grid, before the data: filler.
|
|
211
|
+
expect( lastColumn.data[ 0 ].placeholder ).toBe( true );
|
|
212
|
+
expect( lastColumn.data[ 0 ].hidden ).toBeUndefined();
|
|
213
|
+
|
|
214
|
+
expect( lastColumn.data[ 2 ].value ).toBe( 5 );
|
|
215
|
+
|
|
216
|
+
// Past the grid's end: the calendar's ragged edge, painted as nothing.
|
|
217
|
+
expect( lastColumn.data[ 3 ].hidden ).toBe( true );
|
|
218
|
+
expect( lastColumn.data[ 3 ].placeholder ).toBeUndefined();
|
|
219
|
+
} );
|
|
220
|
+
|
|
221
|
+
test( 'is a no-op when no grid span is given', () => {
|
|
222
|
+
expect( buildCalendarHeatmapData( oneWeek, { weekStartsOn: 1 } ) ).toEqual(
|
|
223
|
+
buildCalendarHeatmapData( oneWeek, { weekStartsOn: 1, gridSpan: {} } )
|
|
224
|
+
);
|
|
225
|
+
} );
|
|
226
|
+
|
|
227
|
+
// The caller sizes the grid in whole weeks back from the period's last day, so
|
|
228
|
+
// the bound lands on that weekday — a Monday only one year in seven.
|
|
229
|
+
test( 'opens on a whole column when the start bound falls mid-week', () => {
|
|
230
|
+
const { data } = buildCalendarHeatmapData( oneWeek, {
|
|
231
|
+
weekStartsOn: 1,
|
|
232
|
+
gridSpan: { start: '2023-12-07' }, // a Thursday
|
|
233
|
+
} );
|
|
234
|
+
|
|
235
|
+
expect( data ).toHaveLength( 5 );
|
|
236
|
+
|
|
237
|
+
// Mon Dec 4 through Wed Dec 6 precede the bound; they are as unmeasured as
|
|
238
|
+
// the rest of the filler, so they fill the column rather than notching it.
|
|
239
|
+
expect( data[ 0 ].data.every( cell => cell.placeholder === true ) ).toBe( true );
|
|
240
|
+
expect( data[ 0 ].data.some( cell => cell.hidden ) ).toBe( false );
|
|
241
|
+
} );
|
|
242
|
+
|
|
243
|
+
test( 'still draws the ragged edge past the grid end', () => {
|
|
244
|
+
// Wed Jan 3 ends the series, so Thu Jan 4 onward only completes the week.
|
|
245
|
+
const { data } = buildCalendarHeatmapData( [ { dateString: '2024-01-03', value: 5 } ], {
|
|
246
|
+
weekStartsOn: 1,
|
|
247
|
+
gridSpan: { start: '2023-12-20' }, // a Wednesday
|
|
248
|
+
} );
|
|
249
|
+
|
|
250
|
+
expect( data[ 0 ].data.every( cell => cell.placeholder === true ) ).toBe( true );
|
|
251
|
+
|
|
252
|
+
const lastColumn = data[ data.length - 1 ];
|
|
253
|
+
expect( lastColumn.data[ 2 ].value ).toBe( 5 );
|
|
254
|
+
expect( lastColumn.data[ 3 ].hidden ).toBe( true );
|
|
255
|
+
} );
|
|
256
|
+
|
|
257
|
+
test( 'keeps the ragged start edge when the bound is ignored', () => {
|
|
258
|
+
// A narrowing bound never applies, so the series' own first week stays
|
|
259
|
+
// ragged rather than being filled out.
|
|
260
|
+
const { data } = buildCalendarHeatmapData( [ { dateString: '2024-01-03', value: 5 } ], {
|
|
261
|
+
weekStartsOn: 1,
|
|
262
|
+
gridSpan: { start: '2024-01-05' },
|
|
263
|
+
} );
|
|
264
|
+
|
|
265
|
+
expect( data ).toHaveLength( 1 );
|
|
266
|
+
expect( data[ 0 ].data[ 0 ].hidden ).toBe( true );
|
|
267
|
+
expect( data[ 0 ].data[ 0 ].placeholder ).toBeUndefined();
|
|
268
|
+
} );
|
|
269
|
+
} );
|
|
@@ -13,6 +13,14 @@ export type HeatmapCell = {
|
|
|
13
13
|
* days completing the first/last week fall outside the covered range.
|
|
14
14
|
*/
|
|
15
15
|
hidden?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Paint the cell as a faded empty slot that carries no claim about the day:
|
|
18
|
+
* it is set apart from a measured day that scored zero, and skipped by
|
|
19
|
+
* hover, tooltips, keyboard navigation and the accessibility tree. For grid
|
|
20
|
+
* filler — slots drawn only so a short range still fills its container,
|
|
21
|
+
* which were never measured.
|
|
22
|
+
*/
|
|
23
|
+
placeholder?: boolean;
|
|
16
24
|
};
|
|
17
25
|
|
|
18
26
|
/** A heatmap column (rendered left→right); its cells render top→bottom. */
|
|
@@ -60,20 +60,26 @@
|
|
|
60
60
|
// row's fixed-pixel hover inset from collapsing a narrow bar to nothing.
|
|
61
61
|
min-width: var(--wpds-dimension-size-5xs, 4px);
|
|
62
62
|
|
|
63
|
-
border-radius: var(--a8c-charts-border-radius-leaderboard-bar
|
|
63
|
+
border-radius: var(--a8c-charts-border-radius-leaderboard-bar);
|
|
64
64
|
z-index: -1; /* places it behind the label */
|
|
65
65
|
|
|
66
|
+
// Keep the collapsed state in `from` and never on the element: a dropped
|
|
67
|
+
// animation then fails visible. `both`, not `forwards`, for the
|
|
68
|
+
// backwards fill.
|
|
66
69
|
&--animated {
|
|
67
70
|
transform-origin: left;
|
|
68
71
|
transform-box: fill-box;
|
|
69
|
-
transform: scaleX(0);
|
|
70
72
|
animation:
|
|
71
|
-
stretch var(--
|
|
72
|
-
var(--
|
|
73
|
+
stretch var(--a8c-charts-motion-duration-entrance)
|
|
74
|
+
var(--a8c-charts-motion-easing-entrance) both;
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
@keyframes stretch {
|
|
76
78
|
|
|
79
|
+
from {
|
|
80
|
+
transform: scaleX(0);
|
|
81
|
+
}
|
|
82
|
+
|
|
77
83
|
to {
|
|
78
84
|
transform: scaleX(1);
|
|
79
85
|
}
|
|
@@ -104,7 +110,7 @@
|
|
|
104
110
|
.emptyState {
|
|
105
111
|
padding: var(--wpds-dimension-padding-3xl, 32px) var(--wpds-dimension-padding-lg, 16px);
|
|
106
112
|
text-align: center;
|
|
107
|
-
color: var(--a8c-charts-color-label-secondary
|
|
113
|
+
color: var(--a8c-charts-color-label-secondary);
|
|
108
114
|
font-size: var(--wpds-typography-font-size-md, 13px);
|
|
109
115
|
font-style: italic;
|
|
110
116
|
}
|
|
@@ -205,7 +211,7 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
205
211
|
border-radius: var(--wpds-border-radius-sm, 2px);
|
|
206
212
|
outline:
|
|
207
213
|
$focus-ring-width solid
|
|
208
|
-
var(--
|
|
214
|
+
var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9));
|
|
209
215
|
// Inset by its own width so the outer edge hugs the row boundary: the
|
|
210
216
|
// outline then sits in the shared row padding, clear of the bar, and stays
|
|
211
217
|
// inside the overflow:auto container that would clip an outset ring.
|
|
@@ -220,7 +226,7 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
220
226
|
bottom: 0;
|
|
221
227
|
margin-block: auto;
|
|
222
228
|
opacity: 0;
|
|
223
|
-
color: var(--a8c-charts-color-label-secondary
|
|
229
|
+
color: var(--a8c-charts-color-label-secondary);
|
|
224
230
|
transition:
|
|
225
231
|
opacity var(--wpds-motion-duration-sm, 100ms)
|
|
226
232
|
var(--wpds-motion-easing-subtle, cubic-bezier(0.15, 0, 0.15, 1));
|
|
@@ -17,8 +17,9 @@ import {
|
|
|
17
17
|
} from '../../providers';
|
|
18
18
|
import { formatMetricValue, attachSubComponents } from '../../utils';
|
|
19
19
|
import { useChartChildren } from '../private/chart-composition';
|
|
20
|
+
import { ChartInstanceContext } from '../private/chart-instance-context';
|
|
20
21
|
import { ChartLayout } from '../private/chart-layout';
|
|
21
|
-
import {
|
|
22
|
+
import { getAllHiddenMessage } from '../private/svg-empty-state';
|
|
22
23
|
import { withResponsive } from '../private/with-responsive';
|
|
23
24
|
import { useFittedRowCount, useLeaderboardLegendItems } from './hooks';
|
|
24
25
|
import styles from './leaderboard-chart.module.scss';
|
|
@@ -225,35 +226,28 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
225
226
|
legendLabels,
|
|
226
227
|
} );
|
|
227
228
|
|
|
228
|
-
// Track visibility of primary and comparison series
|
|
229
|
+
// Track visibility of primary and comparison series from the shared legend state.
|
|
229
230
|
const isPrimaryVisible = useMemo( () => {
|
|
230
|
-
if (
|
|
231
|
+
if ( legendItems.length === 0 ) {
|
|
231
232
|
return true;
|
|
232
233
|
}
|
|
233
234
|
return isSeriesVisible( chartId, legendItems[ 0 ].label );
|
|
234
|
-
}, [ chartId,
|
|
235
|
+
}, [ chartId, legendItems, isSeriesVisible ] );
|
|
235
236
|
|
|
236
237
|
const isComparisonVisible = useMemo( () => {
|
|
237
|
-
if (
|
|
238
|
+
if ( legendItems.length < 2 ) {
|
|
238
239
|
return true;
|
|
239
240
|
}
|
|
240
241
|
return isSeriesVisible( chartId, legendItems[ 1 ].label );
|
|
241
|
-
}, [ chartId,
|
|
242
|
+
}, [ chartId, legendItems, isSeriesVisible ] );
|
|
242
243
|
|
|
243
244
|
// Check if all series are hidden
|
|
244
245
|
const allSeriesHidden = useMemo( () => {
|
|
245
|
-
if ( ! legendInteractive ) return false;
|
|
246
246
|
if ( withComparison && ! withOverlayLabel ) {
|
|
247
247
|
return ! isPrimaryVisible && ! isComparisonVisible;
|
|
248
248
|
}
|
|
249
249
|
return ! isPrimaryVisible;
|
|
250
|
-
}, [
|
|
251
|
-
legendInteractive,
|
|
252
|
-
isPrimaryVisible,
|
|
253
|
-
isComparisonVisible,
|
|
254
|
-
withComparison,
|
|
255
|
-
withOverlayLabel,
|
|
256
|
-
] );
|
|
250
|
+
}, [ isPrimaryVisible, isComparisonVisible, withComparison, withOverlayLabel ] );
|
|
257
251
|
|
|
258
252
|
// Validate data
|
|
259
253
|
const isDataValid = Boolean( data && data.length > 0 );
|
|
@@ -278,9 +272,10 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
278
272
|
|
|
279
273
|
const prefersReducedMotion = usePrefersReducedMotion();
|
|
280
274
|
|
|
281
|
-
// There are no rows to measure while
|
|
282
|
-
//
|
|
283
|
-
// again, re-runs the effect
|
|
275
|
+
// There are no rows to measure while every series is hidden, whether from an
|
|
276
|
+
// interactive legend click or a programmatic toggle. Pausing fitting restores
|
|
277
|
+
// the full row count and, when a series is shown again, re-runs the effect
|
|
278
|
+
// against the newly mounted grid.
|
|
284
279
|
const { contentRef, fittedCount, isMeasurable } = useFittedRowCount(
|
|
285
280
|
fitRows && ! allSeriesHidden,
|
|
286
281
|
data?.length ?? 0,
|
|
@@ -291,7 +286,7 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
291
286
|
// Handle empty or undefined data
|
|
292
287
|
if ( ! data || data.length === 0 ) {
|
|
293
288
|
return (
|
|
294
|
-
<
|
|
289
|
+
<ChartInstanceContext.Provider value={ { chartId } }>
|
|
295
290
|
<ChartLayout
|
|
296
291
|
legendPosition={ legendPosition }
|
|
297
292
|
legendElement={ false }
|
|
@@ -315,7 +310,7 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
315
310
|
: __( 'No data available', 'jetpack-charts' ) }
|
|
316
311
|
</div>
|
|
317
312
|
</ChartLayout>
|
|
318
|
-
</
|
|
313
|
+
</ChartInstanceContext.Provider>
|
|
319
314
|
);
|
|
320
315
|
}
|
|
321
316
|
|
|
@@ -335,7 +330,7 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
335
330
|
);
|
|
336
331
|
|
|
337
332
|
return (
|
|
338
|
-
<
|
|
333
|
+
<ChartInstanceContext.Provider value={ { chartId } }>
|
|
339
334
|
<ChartLayout
|
|
340
335
|
legendPosition={ legendPosition }
|
|
341
336
|
legendElement={ legendElement }
|
|
@@ -372,7 +367,7 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
372
367
|
) }
|
|
373
368
|
{ allSeriesHidden ? (
|
|
374
369
|
<div className={ styles.emptyState }>
|
|
375
|
-
{
|
|
370
|
+
{ getAllHiddenMessage( legendInteractive, 'series' ) }
|
|
376
371
|
</div>
|
|
377
372
|
) : (
|
|
378
373
|
<Grid
|
|
@@ -476,7 +471,7 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
476
471
|
) }
|
|
477
472
|
</div>
|
|
478
473
|
</ChartLayout>
|
|
479
|
-
</
|
|
474
|
+
</ChartInstanceContext.Provider>
|
|
480
475
|
);
|
|
481
476
|
};
|
|
482
477
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { render, screen } from '@testing-library/react';
|
|
1
|
+
import { render, screen, act } from '@testing-library/react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { GlobalChartsProvider } from '../../../providers';
|
|
4
|
+
import { useGlobalChartsContext } from '../../../providers/chart-context/hooks/use-global-charts-context';
|
|
3
5
|
import LeaderboardChart from '../leaderboard-chart';
|
|
6
|
+
import type { GlobalChartsContextValue } from '../../../providers/chart-context/types';
|
|
4
7
|
import type { LeaderboardEntry } from '../../../types';
|
|
5
8
|
|
|
6
9
|
const mockDefaultParentSize = () => ( {
|
|
@@ -418,6 +421,109 @@ describe( 'LeaderboardChart', () => {
|
|
|
418
421
|
} );
|
|
419
422
|
} );
|
|
420
423
|
|
|
424
|
+
describe( 'Programmatic visibility', () => {
|
|
425
|
+
it( 'hides the primary series programmatically when the legend is not interactive', () => {
|
|
426
|
+
let context: GlobalChartsContextValue;
|
|
427
|
+
const Grab = () => {
|
|
428
|
+
context = useGlobalChartsContext();
|
|
429
|
+
return null;
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
render(
|
|
433
|
+
<GlobalChartsProvider>
|
|
434
|
+
<Grab />
|
|
435
|
+
<LeaderboardChart
|
|
436
|
+
width={ 500 }
|
|
437
|
+
height={ 300 }
|
|
438
|
+
showLegend={ true }
|
|
439
|
+
legend={ { interactive: false } }
|
|
440
|
+
chartId="test-programmatic-leaderboard"
|
|
441
|
+
data={ mockData }
|
|
442
|
+
/>
|
|
443
|
+
</GlobalChartsProvider>
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
const primaryLabel = screen.getAllByTestId( 'legend-item' )[ 0 ].textContent;
|
|
447
|
+
|
|
448
|
+
act( () => {
|
|
449
|
+
context.toggleSeriesVisibility( 'test-programmatic-leaderboard', primaryLabel );
|
|
450
|
+
} );
|
|
451
|
+
|
|
452
|
+
expect( screen.getByText( /all series are hidden/i ) ).toBeInTheDocument();
|
|
453
|
+
} );
|
|
454
|
+
|
|
455
|
+
it( 'omits the click instruction when the legend cannot be clicked', () => {
|
|
456
|
+
let context: GlobalChartsContextValue;
|
|
457
|
+
const Grab = () => {
|
|
458
|
+
context = useGlobalChartsContext();
|
|
459
|
+
return null;
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
render(
|
|
463
|
+
<GlobalChartsProvider>
|
|
464
|
+
<Grab />
|
|
465
|
+
<LeaderboardChart
|
|
466
|
+
width={ 500 }
|
|
467
|
+
height={ 300 }
|
|
468
|
+
showLegend={ true }
|
|
469
|
+
legend={ { interactive: false } }
|
|
470
|
+
chartId="test-empty-copy-leaderboard"
|
|
471
|
+
data={ mockData }
|
|
472
|
+
/>
|
|
473
|
+
</GlobalChartsProvider>
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
const primaryLabel = screen.getAllByTestId( 'legend-item' )[ 0 ].textContent;
|
|
477
|
+
|
|
478
|
+
act( () => {
|
|
479
|
+
context.toggleSeriesVisibility( 'test-empty-copy-leaderboard', primaryLabel );
|
|
480
|
+
} );
|
|
481
|
+
|
|
482
|
+
expect( screen.getByText( 'All series are hidden.' ) ).toBeInTheDocument();
|
|
483
|
+
expect( screen.queryByText( /click legend items/i ) ).not.toBeInTheDocument();
|
|
484
|
+
} );
|
|
485
|
+
|
|
486
|
+
it( 'still renders the primary series when only the comparison is hidden programmatically', () => {
|
|
487
|
+
// Hiding every series routes through allSeriesHidden to the empty state and
|
|
488
|
+
// never exercises the per-series render path. Hiding only the comparison
|
|
489
|
+
// series exercises that path for the leaderboard's primary/comparison split.
|
|
490
|
+
let context: GlobalChartsContextValue;
|
|
491
|
+
const Grab = () => {
|
|
492
|
+
context = useGlobalChartsContext();
|
|
493
|
+
return null;
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
render(
|
|
497
|
+
<GlobalChartsProvider>
|
|
498
|
+
<Grab />
|
|
499
|
+
<LeaderboardChart
|
|
500
|
+
width={ 500 }
|
|
501
|
+
height={ 300 }
|
|
502
|
+
showLegend={ true }
|
|
503
|
+
withComparison={ true }
|
|
504
|
+
legend={ { interactive: false } }
|
|
505
|
+
chartId="test-programmatic-comparison-leaderboard"
|
|
506
|
+
data={ mockData }
|
|
507
|
+
/>
|
|
508
|
+
</GlobalChartsProvider>
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
const comparisonLabel = screen.getAllByTestId( 'legend-item' )[ 1 ].textContent;
|
|
512
|
+
|
|
513
|
+
act( () => {
|
|
514
|
+
context.toggleSeriesVisibility(
|
|
515
|
+
'test-programmatic-comparison-leaderboard',
|
|
516
|
+
comparisonLabel
|
|
517
|
+
);
|
|
518
|
+
} );
|
|
519
|
+
|
|
520
|
+
expect( screen.queryByText( /all series are hidden/i ) ).not.toBeInTheDocument();
|
|
521
|
+
expect( screen.getByText( 'Direct' ) ).toBeInTheDocument();
|
|
522
|
+
expect( screen.getByText( '12.5K' ) ).toBeInTheDocument();
|
|
523
|
+
expect( screen.queryByText( '+25%' ) ).not.toBeInTheDocument();
|
|
524
|
+
} );
|
|
525
|
+
} );
|
|
526
|
+
|
|
421
527
|
describe( 'Responsive wrapper', () => {
|
|
422
528
|
it( 'applies explicit width and height to chart container', () => {
|
|
423
529
|
const { useParentSize } = jest.requireMock( '@visx/responsive' );
|
|
@@ -65,8 +65,6 @@ export interface LeaderboardChartProps
|
|
|
65
65
|
*/
|
|
66
66
|
style?: React.CSSProperties & {
|
|
67
67
|
'--a8c-charts-border-radius-leaderboard-bar'?: string;
|
|
68
|
-
/** @deprecated Use `--a8c-charts-border-radius-leaderboard-bar`. */
|
|
69
|
-
'--a8c--charts--leaderboard--bar--border-radius'?: string;
|
|
70
68
|
};
|
|
71
69
|
|
|
72
70
|
/**
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
.line-chart {
|
|
2
2
|
position: relative;
|
|
3
3
|
|
|
4
|
+
// Keep the collapsed state in `from` and never on the element: a dropped
|
|
5
|
+
// animation then fails visible. `both`, not `forwards`, for backwards fill.
|
|
4
6
|
&--animated {
|
|
5
7
|
|
|
6
8
|
path {
|
|
7
9
|
transform-origin: 0 95%;
|
|
8
|
-
transform: scaleY(0);
|
|
9
10
|
animation:
|
|
10
|
-
rise var(--
|
|
11
|
-
var(--
|
|
11
|
+
rise var(--a8c-charts-motion-duration-entrance)
|
|
12
|
+
var(--a8c-charts-motion-easing-entrance) both;
|
|
12
13
|
}
|
|
13
14
|
}
|
|
14
15
|
|
|
@@ -18,13 +19,13 @@
|
|
|
18
19
|
|
|
19
20
|
&__tooltip,
|
|
20
21
|
&__annotation-label-popover {
|
|
21
|
-
background: var(--a8c-charts-color-
|
|
22
|
-
color: var(--a8c-charts-color-label
|
|
22
|
+
background: var(--a8c-charts-color-surface);
|
|
23
|
+
color: var(--a8c-charts-color-label);
|
|
23
24
|
padding: var(--wpds-dimension-padding-sm, 8px);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
&__tooltip-date {
|
|
27
|
-
font-weight: var(--wpds-typography-font-weight-emphasis,
|
|
28
|
+
font-weight: var(--wpds-typography-font-weight-emphasis, 600);
|
|
28
29
|
padding-bottom: var(--wpds-dimension-padding-md, 12px);
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -33,7 +34,7 @@
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
&__tooltip-label {
|
|
36
|
-
font-weight: var(--wpds-typography-font-weight-emphasis,
|
|
37
|
+
font-weight: var(--wpds-typography-font-weight-emphasis, 600);
|
|
37
38
|
padding-right: var(--wpds-dimension-padding-lg, 16px);
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -110,6 +111,10 @@
|
|
|
110
111
|
|
|
111
112
|
@keyframes rise {
|
|
112
113
|
|
|
114
|
+
from {
|
|
115
|
+
transform: scaleY(0);
|
|
116
|
+
}
|
|
117
|
+
|
|
113
118
|
to {
|
|
114
119
|
transform: scaleY(1);
|
|
115
120
|
}
|