@automattic/charts 3.0.0 → 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 +13 -0
- package/TOKENS.md +3 -3
- package/dist/index.cjs +235 -123
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +23 -14
- package/dist/index.d.cts +51 -14
- package/dist/index.d.ts +51 -14
- package/dist/index.js +236 -125
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.tsx +24 -24
- 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.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 +6 -6
- package/src/charts/geo-chart/geo-chart.module.scss +8 -0
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +9 -0
- package/src/charts/heatmap-chart/heatmap-chart.tsx +31 -8
- 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.tsx +17 -22
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +107 -1
- package/src/charts/line-chart/line-chart.module.scss +2 -2
- package/src/charts/line-chart/line-chart.tsx +31 -27
- 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 +8 -13
- 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 +8 -13
- 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/test/get-all-hidden-message.test.ts +15 -0
- package/src/charts/private/x-zoom/test/x-zoom.test.tsx +2 -2
- 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 +67 -18
- package/src/components/legend/test/legend.test.tsx +100 -4
- package/src/components/tooltip/accessible-tooltip.tsx +13 -2
- package/src/components/trend-indicator/trend-indicator.module.scss +1 -1
- package/src/hooks/index.ts +1 -1
- package/src/hooks/{use-interactive-legend-data.ts → use-legend-visibility-data.ts} +29 -30
- package/src/index.ts +3 -1
- package/src/providers/chart-context/global-charts-provider.tsx +92 -18
- package/src/providers/chart-context/hooks/use-default-hidden-series.ts +47 -0
- package/src/providers/chart-context/test/chart-context.test.tsx +142 -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/types.ts +7 -1
- package/src/styles/chart-scope.scss +2 -2
- package/src/styles/test/wpds-fallbacks.test.ts +131 -0
- package/src/types.ts +12 -0
- package/src/charts/private/single-chart-context/index.ts +0 -8
|
@@ -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. */
|
|
@@ -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' );
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
&__tooltip-date {
|
|
28
|
-
font-weight: var(--wpds-typography-font-weight-emphasis,
|
|
28
|
+
font-weight: var(--wpds-typography-font-weight-emphasis, 600);
|
|
29
29
|
padding-bottom: var(--wpds-dimension-padding-md, 12px);
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
&__tooltip-label {
|
|
37
|
-
font-weight: var(--wpds-typography-font-weight-emphasis,
|
|
37
|
+
font-weight: var(--wpds-typography-font-weight-emphasis, 600);
|
|
38
38
|
padding-right: var(--wpds-dimension-padding-lg, 16px);
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -29,12 +29,13 @@ import {
|
|
|
29
29
|
useGlobalChartsContext,
|
|
30
30
|
useGlobalChartsTheme,
|
|
31
31
|
} from '../../providers';
|
|
32
|
+
import { useDefaultHiddenSeries } from '../../providers/chart-context/hooks/use-default-hidden-series';
|
|
32
33
|
import { attachSubComponents } from '../../utils';
|
|
33
34
|
import { useChartChildren } from '../private/chart-composition';
|
|
35
|
+
import { ChartInstanceContext, type ChartInstanceRef } from '../private/chart-instance-context';
|
|
34
36
|
import { ChartLayout } from '../private/chart-layout';
|
|
35
37
|
import { DefaultGlyph } from '../private/default-glyph';
|
|
36
|
-
import {
|
|
37
|
-
import { SvgEmptyState } from '../private/svg-empty-state';
|
|
38
|
+
import { getAllHiddenMessage, SvgEmptyState } from '../private/svg-empty-state';
|
|
38
39
|
import { getCurveType, getFormatter, guessOptimalNumTicks } from '../private/time-axis';
|
|
39
40
|
import { withResponsive } from '../private/with-responsive';
|
|
40
41
|
import { useXZoom, ZoomResetButton, ZoomSelectionRect, ZoomClip } from '../private/x-zoom';
|
|
@@ -119,7 +120,7 @@ const validateData = ( data: SeriesData[] ) => {
|
|
|
119
120
|
|
|
120
121
|
// Inner component to access DataContext and provide scale data to ref
|
|
121
122
|
const LineChartScalesRef: FC< {
|
|
122
|
-
chartRef?: Ref<
|
|
123
|
+
chartRef?: Ref< ChartInstanceRef >;
|
|
123
124
|
width: number;
|
|
124
125
|
height: number;
|
|
125
126
|
margin?: { top?: number; right?: number; bottom?: number; left?: number };
|
|
@@ -150,7 +151,7 @@ const LineChartScalesRef: FC< {
|
|
|
150
151
|
return null; // This component only provides the ref interface
|
|
151
152
|
};
|
|
152
153
|
|
|
153
|
-
const LineChartInternal = forwardRef<
|
|
154
|
+
const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
154
155
|
(
|
|
155
156
|
{
|
|
156
157
|
data,
|
|
@@ -180,6 +181,7 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
|
|
|
180
181
|
onPointerOut = undefined,
|
|
181
182
|
zoomable = false,
|
|
182
183
|
rescaleYOnVisibilityChange = true,
|
|
184
|
+
defaultHiddenSeries,
|
|
183
185
|
children,
|
|
184
186
|
gridVisibility,
|
|
185
187
|
gap = 'md',
|
|
@@ -196,10 +198,15 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
|
|
|
196
198
|
// Gradient stops apply this as an SVG attribute, where CSS var() cannot resolve. useXYChartTheme has already resolved the same role inside its memo, against the chart's scope element, so read it back rather than paying another getComputedStyle on every render.
|
|
197
199
|
const resolvedBackgroundColor = theme.backgroundColor ?? providerTheme.backgroundColor;
|
|
198
200
|
const chartId = useChartId( providedChartId );
|
|
201
|
+
const hiddenSeries = useDefaultHiddenSeries( chartId, defaultHiddenSeries );
|
|
202
|
+
const isSeriesVisible = useCallback(
|
|
203
|
+
( seriesLabel: string ) => ! hiddenSeries.has( seriesLabel ),
|
|
204
|
+
[ hiddenSeries ]
|
|
205
|
+
);
|
|
199
206
|
const chartRef = useRef< HTMLDivElement >( null );
|
|
200
207
|
const [ selectedIndex, setSelectedIndex ] = useState< number | undefined >( undefined );
|
|
201
208
|
const [ isNavigating, setIsNavigating ] = useState( false );
|
|
202
|
-
const internalChartRef = useRef<
|
|
209
|
+
const internalChartRef = useRef< ChartInstanceRef >( null );
|
|
203
210
|
|
|
204
211
|
const zoom = useXZoom< Date >( {
|
|
205
212
|
enabled: zoomable,
|
|
@@ -233,31 +240,30 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
|
|
|
233
240
|
);
|
|
234
241
|
|
|
235
242
|
const dataSorted = useChartDataTransform( data );
|
|
236
|
-
const { getElementStyles
|
|
243
|
+
const { getElementStyles } = useGlobalChartsContext();
|
|
237
244
|
|
|
238
|
-
//
|
|
245
|
+
// Series visibility is owned by the provider, so it applies whether it changed
|
|
246
|
+
// through the interactive legend or programmatically.
|
|
239
247
|
const seriesWithVisibility = useMemo( () => {
|
|
240
|
-
if ( ! chartId || ! legendInteractive ) {
|
|
241
|
-
return dataSorted.map( ( series, index ) => ( { series, index, isVisible: true } ) );
|
|
242
|
-
}
|
|
243
248
|
return dataSorted.map( ( series, index ) => ( {
|
|
244
249
|
series,
|
|
245
250
|
index,
|
|
246
|
-
isVisible:
|
|
251
|
+
isVisible: ! hiddenSeries.has( series.label ),
|
|
247
252
|
} ) );
|
|
248
|
-
}, [ dataSorted,
|
|
253
|
+
}, [ dataSorted, hiddenSeries ] );
|
|
249
254
|
|
|
250
255
|
// Check if all series are hidden
|
|
251
256
|
const allSeriesHidden = useMemo( () => {
|
|
252
257
|
return seriesWithVisibility.every( ( { isVisible } ) => ! isVisible );
|
|
253
258
|
}, [ seriesWithVisibility ] );
|
|
254
259
|
|
|
255
|
-
// When
|
|
256
|
-
//
|
|
257
|
-
// domain to whatever is currently visible and
|
|
258
|
-
// matching the pre-existing behaviour
|
|
260
|
+
// When series visibility changes — via the interactive legend or programmatically —
|
|
261
|
+
// and rescaling is opted out, pin the value axis to the full data range so it stays
|
|
262
|
+
// put instead of visx rescaling the domain to whatever is currently visible and
|
|
263
|
+
// making the axis jump. Default is to rescale, matching the pre-existing behaviour
|
|
264
|
+
// and AreaChart's `rescaleYOnVisibilityChange`.
|
|
259
265
|
const stableYDomain = useMemo< [ number, number ] | undefined >( () => {
|
|
260
|
-
if (
|
|
266
|
+
if ( rescaleYOnVisibilityChange ) {
|
|
261
267
|
return undefined;
|
|
262
268
|
}
|
|
263
269
|
let min = Infinity;
|
|
@@ -272,7 +278,7 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
|
|
|
272
278
|
}
|
|
273
279
|
}
|
|
274
280
|
return min < max ? [ min, max ] : undefined;
|
|
275
|
-
}, [
|
|
281
|
+
}, [ rescaleYOnVisibilityChange, dataSorted ] );
|
|
276
282
|
|
|
277
283
|
// Use the keyboard navigation hook
|
|
278
284
|
const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown } = useKeyboardNavigation( {
|
|
@@ -412,10 +418,11 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
|
|
|
412
418
|
);
|
|
413
419
|
|
|
414
420
|
return (
|
|
415
|
-
<
|
|
421
|
+
<ChartInstanceContext.Provider
|
|
416
422
|
value={ {
|
|
417
423
|
chartId,
|
|
418
424
|
chartRef: internalChartRef,
|
|
425
|
+
isSeriesVisible,
|
|
419
426
|
chartWidth: width,
|
|
420
427
|
chartHeight: measuredChartHeight || 0,
|
|
421
428
|
} }
|
|
@@ -489,10 +496,7 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
|
|
|
489
496
|
width={ width }
|
|
490
497
|
height={ chartHeight }
|
|
491
498
|
>
|
|
492
|
-
{
|
|
493
|
-
'All series are hidden. Click legend items to show data.',
|
|
494
|
-
'jetpack-charts'
|
|
495
|
-
) }
|
|
499
|
+
{ getAllHiddenMessage( legendInteractive, 'series' ) }
|
|
496
500
|
</SvgEmptyState>
|
|
497
501
|
) : null }
|
|
498
502
|
|
|
@@ -619,7 +623,7 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
|
|
|
619
623
|
);
|
|
620
624
|
} }
|
|
621
625
|
</ChartLayout>
|
|
622
|
-
</
|
|
626
|
+
</ChartInstanceContext.Provider>
|
|
623
627
|
);
|
|
624
628
|
}
|
|
625
629
|
);
|
|
@@ -634,16 +638,16 @@ type LineChartAnnotationComponents = {
|
|
|
634
638
|
type LineChartBaseProps = Optional< LineChartProps, 'width' | 'height' | 'size' >;
|
|
635
639
|
|
|
636
640
|
type LineChartComponent = React.ForwardRefExoticComponent<
|
|
637
|
-
LineChartBaseProps & React.RefAttributes<
|
|
641
|
+
LineChartBaseProps & React.RefAttributes< ChartInstanceRef >
|
|
638
642
|
> &
|
|
639
643
|
LineChartAnnotationComponents;
|
|
640
644
|
|
|
641
645
|
type LineChartResponsiveComponent = React.ForwardRefExoticComponent<
|
|
642
|
-
LineChartBaseProps & ResponsiveConfig & React.RefAttributes<
|
|
646
|
+
LineChartBaseProps & ResponsiveConfig & React.RefAttributes< ChartInstanceRef >
|
|
643
647
|
> &
|
|
644
648
|
LineChartAnnotationComponents;
|
|
645
649
|
|
|
646
|
-
const LineChartWithProvider = forwardRef<
|
|
650
|
+
const LineChartWithProvider = forwardRef< ChartInstanceRef, LineChartProps >( ( props, ref ) => {
|
|
647
651
|
const existingContext = useContext( GlobalChartsContext );
|
|
648
652
|
|
|
649
653
|
// If we're already in a GlobalChartsProvider context, render the core component directly
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DataContext } from '@visx/xychart';
|
|
2
2
|
import { useEffect, useState, useCallback } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { useChartInstanceContext } from '../../private/chart-instance-context';
|
|
4
4
|
import styles from '../line-chart.module.scss';
|
|
5
5
|
import type { AxisScale } from '@visx/axis';
|
|
6
6
|
import type { FC, ReactNode } from 'react';
|
|
@@ -15,7 +15,7 @@ interface ScaleData {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const LineChartAnnotationsOverlay: FC< LineChartAnnotationsProps > = ( { children } ) => {
|
|
18
|
-
const { chartRef, chartWidth, chartHeight } =
|
|
18
|
+
const { chartRef, chartWidth, chartHeight } = useChartInstanceContext();
|
|
19
19
|
|
|
20
20
|
const [ scales, setScales ] = useState< ScaleData | null >( null );
|
|
21
21
|
const [ scalesStable, setScalesStable ] = useState< boolean >( false );
|