@automattic/charts 4.2.0 → 4.3.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 +20 -0
- package/dist/index.cjs +470 -121
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -2
- package/dist/index.d.ts +23 -2
- package/dist/index.js +471 -122
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.tsx +42 -10
- package/src/charts/area-chart/private/validate-data.ts +2 -3
- package/src/charts/area-chart/test/area-chart.test.tsx +72 -1
- package/src/charts/bar-chart/bar-chart.tsx +100 -83
- package/src/charts/bar-chart/index.ts +1 -1
- package/src/charts/bar-chart/private/band-highlight.tsx +3 -1
- package/src/charts/bar-chart/private/band-tooltip.tsx +1 -1
- package/src/charts/bar-chart/private/index.ts +1 -0
- package/src/charts/bar-chart/private/test/band-highlight.test.tsx +28 -2
- package/src/charts/bar-chart/private/test/value-domain.test.ts +100 -0
- package/src/charts/bar-chart/private/use-bar-chart-options.ts +12 -39
- package/src/charts/bar-chart/private/value-domain.ts +90 -0
- package/src/charts/bar-chart/test/bar-chart.test.tsx +484 -9
- package/src/charts/bar-chart/types.ts +39 -0
- package/src/charts/line-chart/line-chart.tsx +148 -35
- package/src/charts/line-chart/private/index.ts +1 -0
- package/src/charts/line-chart/private/line-chart-glyph.tsx +17 -3
- package/src/charts/line-chart/private/nearest-pointer-events.tsx +155 -0
- package/src/charts/line-chart/test/line-chart.test.tsx +604 -9
- package/src/charts/line-chart/types.ts +2 -1
- package/src/charts/private/readings.ts +34 -0
- package/src/charts/private/test/readings.test.ts +29 -0
- package/src/charts/private/test/whole-number-ticks.test.tsx +57 -0
- package/src/charts/private/whole-number-ticks.tsx +79 -0
- package/src/components/legend/hooks/test/use-chart-legend-items.test.tsx +124 -0
- package/src/components/legend/hooks/use-chart-legend-items.ts +60 -2
- package/src/components/legend/private/base-legend.tsx +36 -21
- package/src/components/legend/test/legend.test.tsx +84 -0
- package/src/components/legend/types.ts +2 -0
- package/src/components/legend/utils/index.ts +0 -1
- package/src/components/tooltip/accessible-tooltip.tsx +1 -1
- package/src/hooks/test/use-chart-margin.test.tsx +35 -0
- package/src/hooks/use-chart-margin.tsx +7 -2
- package/src/providers/chart-context/global-charts-provider.tsx +1 -1
- package/src/providers/chart-context/test/chart-context.test.tsx +4 -4
- package/src/types.ts +8 -0
- package/src/components/legend/utils/label-transform-factory.ts +0 -24
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import userEvent from '@testing-library/user-event';
|
|
11
11
|
import { GlobalChartsProvider } from '../../../providers';
|
|
12
12
|
import { useGlobalChartsContext } from '../../../providers/chart-context/hooks/use-global-charts-context';
|
|
13
|
+
import { steadyTrafficData } from '../../../stories/sample-data';
|
|
13
14
|
import BarChart, { BarChartUnresponsive } from '../bar-chart';
|
|
14
15
|
import { useBarChartOptions } from '../private';
|
|
15
16
|
import type { GlobalChartsContextValue } from '../../../providers/chart-context/types';
|
|
@@ -44,6 +45,11 @@ describe( 'BarChart', () => {
|
|
|
44
45
|
return screen.getByRole( 'grid' ).querySelectorAll( '.visx-bar-group rect' );
|
|
45
46
|
};
|
|
46
47
|
|
|
48
|
+
const getGridLines = () => {
|
|
49
|
+
// eslint-disable-next-line testing-library/no-node-access -- visx grid lines cannot receive test IDs.
|
|
50
|
+
return screen.getByRole( 'grid' ).querySelectorAll( '.visx-rows line' );
|
|
51
|
+
};
|
|
52
|
+
|
|
47
53
|
const renderWithTheme = ( props = {}, children = undefined ) => {
|
|
48
54
|
return render(
|
|
49
55
|
<GlobalChartsProvider>
|
|
@@ -134,7 +140,7 @@ describe( 'BarChart', () => {
|
|
|
134
140
|
expect( screen.queryByRole( 'tooltip' ) ).not.toBeInTheDocument();
|
|
135
141
|
} );
|
|
136
142
|
|
|
137
|
-
test( '
|
|
143
|
+
test( 'keeps band correction after BarGroup subscriptions on mount and rerender', async () => {
|
|
138
144
|
const chart = ( className: string ) => (
|
|
139
145
|
<GlobalChartsProvider>
|
|
140
146
|
<BarChart { ...defaultProps } withTooltips className={ className } />
|
|
@@ -152,6 +158,63 @@ describe( 'BarChart', () => {
|
|
|
152
158
|
await waitFor( () => expect( screen.getByRole( 'tooltip' ) ).toHaveTextContent( 'Jan 1' ) );
|
|
153
159
|
} );
|
|
154
160
|
|
|
161
|
+
test( 'clears the highlight callback when the pointer leaves the plot', async () => {
|
|
162
|
+
const onBandHighlightChange = jest.fn();
|
|
163
|
+
renderWithTheme( { withTooltips: true, withBandHighlight: true, onBandHighlightChange } );
|
|
164
|
+
const [ [ x, , width ] ] = barGeometry();
|
|
165
|
+
hover( x + width / 2, 150 );
|
|
166
|
+
await expect(
|
|
167
|
+
screen.findByTestId( 'bar-chart-band-highlight' )
|
|
168
|
+
).resolves.toBeInTheDocument();
|
|
169
|
+
pointer( 'pointerout', x + width / 2, 150 );
|
|
170
|
+
await waitFor( () => expect( onBandHighlightChange ).toHaveBeenLastCalledWith( null ) );
|
|
171
|
+
expect( screen.queryByTestId( 'bar-chart-band-highlight' ) ).not.toBeInTheDocument();
|
|
172
|
+
} );
|
|
173
|
+
|
|
174
|
+
test.each( [ 'comparison', 'hidden' ] )(
|
|
175
|
+
'selects the visible primary band with a %s series',
|
|
176
|
+
async mode => {
|
|
177
|
+
const onBandHighlightChange = jest.fn();
|
|
178
|
+
const onPointerUp = jest.fn();
|
|
179
|
+
renderWithTheme( {
|
|
180
|
+
withTooltips: true,
|
|
181
|
+
withBandHighlight: true,
|
|
182
|
+
onBandHighlightChange,
|
|
183
|
+
onPointerUp,
|
|
184
|
+
defaultHiddenSeries: mode === 'hidden' ? [ 'Other' ] : [],
|
|
185
|
+
data: [
|
|
186
|
+
{ ...defaultProps.data[ 0 ], group: 'scores' },
|
|
187
|
+
{
|
|
188
|
+
label: 'Other',
|
|
189
|
+
group: 'scores',
|
|
190
|
+
options: mode === 'comparison' ? { type: 'comparison' } : {},
|
|
191
|
+
data: defaultProps.data[ 0 ].data,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
} );
|
|
195
|
+
const [ [ x, , width ] ] = barGeometry();
|
|
196
|
+
hover( x + width / 2, 150 );
|
|
197
|
+
const highlight = await screen.findByTestId( 'bar-chart-band-highlight' );
|
|
198
|
+
const otherSlot =
|
|
199
|
+
Number( highlight.getAttribute( 'x' ) ) +
|
|
200
|
+
Number( highlight.getAttribute( 'width' ) ) * 0.85;
|
|
201
|
+
hover( otherSlot, 150 );
|
|
202
|
+
pointer( 'pointerup', otherSlot, 150 );
|
|
203
|
+
for ( const callback of [ onBandHighlightChange, onPointerUp ] ) {
|
|
204
|
+
expect( callback ).not.toHaveBeenCalledWith(
|
|
205
|
+
expect.objectContaining( { key: 'Other' } )
|
|
206
|
+
);
|
|
207
|
+
expect( callback ).toHaveBeenLastCalledWith(
|
|
208
|
+
expect.objectContaining( {
|
|
209
|
+
key: 'Series A',
|
|
210
|
+
index: 0,
|
|
211
|
+
datum: defaultProps.data[ 0 ].data[ 0 ],
|
|
212
|
+
} )
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
|
|
155
218
|
test( 'reports the tooltip datum when pressing and releasing in a padded gap', async () => {
|
|
156
219
|
const onPointerDown = jest.fn();
|
|
157
220
|
const onPointerUp = jest.fn();
|
|
@@ -224,7 +287,36 @@ describe( 'BarChart', () => {
|
|
|
224
287
|
);
|
|
225
288
|
} );
|
|
226
289
|
|
|
227
|
-
test( '
|
|
290
|
+
test( 'warns once when highlight options lack tooltips across callback changes and remounts', () => {
|
|
291
|
+
const warn = jest.spyOn( console, 'warn' ).mockImplementation( () => {} );
|
|
292
|
+
try {
|
|
293
|
+
const onBandHighlightChange = jest.fn();
|
|
294
|
+
const { rerender, unmount } = renderWithTheme( {
|
|
295
|
+
withBandHighlight: true,
|
|
296
|
+
onBandHighlightChange,
|
|
297
|
+
} );
|
|
298
|
+
for ( let i = 0; i < 4; i++ ) {
|
|
299
|
+
rerender(
|
|
300
|
+
<GlobalChartsProvider>
|
|
301
|
+
<BarChart { ...defaultProps } withBandHighlight onBandHighlightChange={ jest.fn() } />
|
|
302
|
+
</GlobalChartsProvider>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
unmount();
|
|
306
|
+
renderWithTheme( { withBandHighlight: true, onBandHighlightChange } );
|
|
307
|
+
expect( warn ).toHaveBeenCalledTimes( 1 );
|
|
308
|
+
expect( screen.getByRole( 'grid' ) ).toBeInTheDocument();
|
|
309
|
+
expect( warn ).toHaveBeenCalledWith(
|
|
310
|
+
'[Charts] BarChart: withBandHighlight and onBandHighlightChange require withTooltips.'
|
|
311
|
+
);
|
|
312
|
+
expect( onBandHighlightChange ).not.toHaveBeenCalled();
|
|
313
|
+
expect( screen.queryByTestId( 'bar-chart-band-highlight' ) ).not.toBeInTheDocument();
|
|
314
|
+
} finally {
|
|
315
|
+
warn.mockRestore();
|
|
316
|
+
}
|
|
317
|
+
} );
|
|
318
|
+
|
|
319
|
+
test( 'reports band bounds and point indexes during keyboard navigation and clears them on Escape', async () => {
|
|
228
320
|
const user = userEvent.setup();
|
|
229
321
|
const onBandHighlightChange = jest.fn();
|
|
230
322
|
renderWithTheme( { withTooltips: true, onBandHighlightChange } );
|
|
@@ -233,6 +325,8 @@ describe( 'BarChart', () => {
|
|
|
233
325
|
expect( onBandHighlightChange ).toHaveBeenLastCalledWith(
|
|
234
326
|
expect.objectContaining( {
|
|
235
327
|
datum: expect.objectContaining( { value: 10 } ),
|
|
328
|
+
key: 'Series A',
|
|
329
|
+
index: 0,
|
|
236
330
|
x: expect.any( Number ),
|
|
237
331
|
y: expect.any( Number ),
|
|
238
332
|
width: expect.any( Number ),
|
|
@@ -240,6 +334,14 @@ describe( 'BarChart', () => {
|
|
|
240
334
|
} )
|
|
241
335
|
);
|
|
242
336
|
expect( screen.queryByTestId( 'bar-chart-band-highlight' ) ).not.toBeInTheDocument();
|
|
337
|
+
await user.keyboard( '{ArrowRight}' );
|
|
338
|
+
expect( onBandHighlightChange ).toHaveBeenLastCalledWith(
|
|
339
|
+
expect.objectContaining( {
|
|
340
|
+
datum: expect.objectContaining( { value: 20 } ),
|
|
341
|
+
key: 'Series A',
|
|
342
|
+
index: 1,
|
|
343
|
+
} )
|
|
344
|
+
);
|
|
243
345
|
await user.keyboard( '{Escape}' );
|
|
244
346
|
await waitFor( () => expect( onBandHighlightChange ).toHaveBeenLastCalledWith( null ) );
|
|
245
347
|
} );
|
|
@@ -282,19 +384,126 @@ describe( 'BarChart', () => {
|
|
|
282
384
|
);
|
|
283
385
|
|
|
284
386
|
test( 'aligns horizontal grid lines with explicit value ticks', () => {
|
|
285
|
-
|
|
387
|
+
renderWithTheme( {
|
|
286
388
|
gridVisibility: 'x',
|
|
287
389
|
options: {
|
|
288
390
|
axis: { y: { tickValues: [ 0, 50, 100 ] } },
|
|
289
391
|
scale: { y: { domain: [ 0, 100 ] } },
|
|
290
392
|
},
|
|
291
393
|
} );
|
|
292
|
-
|
|
293
|
-
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
|
|
294
|
-
const lines = container.querySelectorAll( '.visx-rows line' );
|
|
394
|
+
const lines = getGridLines();
|
|
295
395
|
expect( lines ).toHaveLength( 3 );
|
|
296
396
|
} );
|
|
297
397
|
|
|
398
|
+
describe( 'Whole-number value ticks', () => {
|
|
399
|
+
const wholeNumberData: SeriesData[] = [
|
|
400
|
+
{
|
|
401
|
+
label: 'Series A',
|
|
402
|
+
data: [
|
|
403
|
+
{ label: 'Mon', value: 0 },
|
|
404
|
+
{ label: 'Tue', value: 1 },
|
|
405
|
+
{ label: 'Wed', value: 1 },
|
|
406
|
+
],
|
|
407
|
+
},
|
|
408
|
+
];
|
|
409
|
+
|
|
410
|
+
test( 'labels a whole-number range smaller than the tick count once per whole number', () => {
|
|
411
|
+
renderWithTheme( { data: wholeNumberData } );
|
|
412
|
+
const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
|
|
413
|
+
const ticks = within( chart )
|
|
414
|
+
.getAllByText( /^-?[\d.,]+$/ )
|
|
415
|
+
.map( el => el.textContent );
|
|
416
|
+
expect( ticks.sort() ).toEqual( [ '0', '1' ] );
|
|
417
|
+
} );
|
|
418
|
+
|
|
419
|
+
test( 'labels a horizontal whole-number range once per whole number', () => {
|
|
420
|
+
renderWithTheme( { data: wholeNumberData, orientation: 'horizontal' } );
|
|
421
|
+
const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
|
|
422
|
+
const ticks = within( chart )
|
|
423
|
+
.getAllByText( /^-?[\d.,]+$/ )
|
|
424
|
+
.map( el => el.textContent );
|
|
425
|
+
expect( ticks.sort() ).toEqual( [ '0', '1' ] );
|
|
426
|
+
} );
|
|
427
|
+
|
|
428
|
+
test( 'draws a flat series of ones on a 0 and 1 axis', () => {
|
|
429
|
+
renderWithTheme( {
|
|
430
|
+
data: [
|
|
431
|
+
{
|
|
432
|
+
label: 'Series A',
|
|
433
|
+
data: [
|
|
434
|
+
{ label: 'Mon', value: 1 },
|
|
435
|
+
{ label: 'Tue', value: 1 },
|
|
436
|
+
],
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
} );
|
|
440
|
+
const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
|
|
441
|
+
const ticks = within( chart )
|
|
442
|
+
.getAllByText( /^-?[\d.,]+$/ )
|
|
443
|
+
.map( el => el.textContent );
|
|
444
|
+
expect( ticks.sort() ).toEqual( [ '0', '1' ] );
|
|
445
|
+
} );
|
|
446
|
+
|
|
447
|
+
test.each( [ 'vertical', 'horizontal' ] as const )(
|
|
448
|
+
'keeps every tick on a value domain the caller pinned (%s)',
|
|
449
|
+
orientation => {
|
|
450
|
+
const valueAxis = orientation === 'horizontal' ? 'x' : 'y';
|
|
451
|
+
renderWithTheme( {
|
|
452
|
+
orientation,
|
|
453
|
+
data: [
|
|
454
|
+
{
|
|
455
|
+
label: 'Series A',
|
|
456
|
+
data: [
|
|
457
|
+
{ label: 'Mon', value: 0 },
|
|
458
|
+
{ label: 'Tue', value: 0 },
|
|
459
|
+
],
|
|
460
|
+
},
|
|
461
|
+
],
|
|
462
|
+
options: {
|
|
463
|
+
[ `${ valueAxis }Scale` ]: { domain: [ 0, 1 ] },
|
|
464
|
+
axis: {
|
|
465
|
+
[ valueAxis ]: { tickFormat: ( value: number ) => `${ Math.round( value * 100 ) }%` },
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
} );
|
|
469
|
+
const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
|
|
470
|
+
expect( within( chart ).getAllByText( /^\d+%$/ ) ).toHaveLength( 6 );
|
|
471
|
+
}
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
test( "keeps a caller's value tickValues", () => {
|
|
475
|
+
renderWithTheme( {
|
|
476
|
+
data: wholeNumberData,
|
|
477
|
+
options: { axis: { y: { tickValues: [ 0, 0.5, 1 ] } } },
|
|
478
|
+
} );
|
|
479
|
+
const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
|
|
480
|
+
expect( within( chart ).getAllByText( /^-?[\d.,]+$/ ) ).toHaveLength( 3 );
|
|
481
|
+
} );
|
|
482
|
+
} );
|
|
483
|
+
|
|
484
|
+
test( 'draws category grid lines at the x axis numTicks', () => {
|
|
485
|
+
const data: SeriesData[] = [
|
|
486
|
+
{
|
|
487
|
+
label: 'Series A',
|
|
488
|
+
data: Array.from( { length: 30 }, ( _, i ) => ( { label: `Day ${ i + 1 }`, value: i } ) ),
|
|
489
|
+
},
|
|
490
|
+
];
|
|
491
|
+
const countColumns = ( numTicks: number ) => {
|
|
492
|
+
const { container, unmount } = renderWithTheme( {
|
|
493
|
+
data,
|
|
494
|
+
gridVisibility: 'y',
|
|
495
|
+
options: { axis: { x: { numTicks } } },
|
|
496
|
+
} );
|
|
497
|
+
// See the visx node constraint at getBarRects.
|
|
498
|
+
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
|
|
499
|
+
const count = container.querySelectorAll( '.visx-columns line' ).length;
|
|
500
|
+
unmount();
|
|
501
|
+
return count;
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
expect( countColumns( 15 ) ).toBeGreaterThan( countColumns( 4 ) );
|
|
505
|
+
} );
|
|
506
|
+
|
|
298
507
|
describe( 'Data Validation', () => {
|
|
299
508
|
test( 'handles empty data array', () => {
|
|
300
509
|
renderWithTheme( { data: [] } );
|
|
@@ -330,18 +539,52 @@ describe( 'BarChart', () => {
|
|
|
330
539
|
expect( screen.getByRole( 'grid', { name: /bar chart/i } ) ).toBeInTheDocument();
|
|
331
540
|
} );
|
|
332
541
|
|
|
333
|
-
test( '
|
|
542
|
+
test( 'renders a bucket with no reading instead of failing the whole chart', () => {
|
|
543
|
+
renderWithTheme( {
|
|
544
|
+
data: [
|
|
545
|
+
{
|
|
546
|
+
label: 'Series A',
|
|
547
|
+
data: [
|
|
548
|
+
{ date: new Date( '2024-01-01' ), value: null as number | null, label: 'Jan 1' },
|
|
549
|
+
{ date: new Date( '2024-01-02' ), value: 20, label: 'Jan 2' },
|
|
550
|
+
],
|
|
551
|
+
options: {},
|
|
552
|
+
},
|
|
553
|
+
],
|
|
554
|
+
} );
|
|
555
|
+
expect( screen.queryByText( /invalid data/i ) ).not.toBeInTheDocument();
|
|
556
|
+
expect( screen.getByRole( 'grid', { name: /bar chart/i } ) ).toBeInTheDocument();
|
|
557
|
+
} );
|
|
558
|
+
|
|
559
|
+
test( 'draws no bar for a bucket with no reading', () => {
|
|
334
560
|
renderWithTheme( {
|
|
335
561
|
data: [
|
|
336
562
|
{
|
|
337
563
|
label: 'Series A',
|
|
338
564
|
data: [
|
|
339
565
|
{ date: new Date( '2024-01-01' ), value: null as number | null, label: 'Jan 1' },
|
|
566
|
+
{ date: new Date( '2024-01-02' ), value: 20, label: 'Jan 2' },
|
|
567
|
+
{ date: new Date( '2024-01-03' ), value: 30, label: 'Jan 3' },
|
|
568
|
+
],
|
|
569
|
+
options: {},
|
|
570
|
+
},
|
|
571
|
+
],
|
|
572
|
+
} );
|
|
573
|
+
expect( getBarRects() ).toHaveLength( 2 );
|
|
574
|
+
} );
|
|
575
|
+
|
|
576
|
+
test( 'still rejects undefined values', () => {
|
|
577
|
+
renderWithTheme( {
|
|
578
|
+
data: [
|
|
579
|
+
{
|
|
580
|
+
label: 'Series A',
|
|
581
|
+
data: [
|
|
340
582
|
{
|
|
341
|
-
date: new Date( '2024-01-
|
|
583
|
+
date: new Date( '2024-01-01' ),
|
|
342
584
|
value: undefined as number | undefined,
|
|
343
|
-
label: 'Jan
|
|
585
|
+
label: 'Jan 1',
|
|
344
586
|
},
|
|
587
|
+
{ date: new Date( '2024-01-02' ), value: 20, label: 'Jan 2' },
|
|
345
588
|
],
|
|
346
589
|
options: {},
|
|
347
590
|
},
|
|
@@ -1655,6 +1898,90 @@ describe( 'BarChart', () => {
|
|
|
1655
1898
|
expect( tooltip ).toHaveTextContent( 'Previous period' );
|
|
1656
1899
|
expect( tooltip ).toHaveTextContent( '25' );
|
|
1657
1900
|
} );
|
|
1901
|
+
|
|
1902
|
+
test( 'reports a bucket with no reading as having no data', async () => {
|
|
1903
|
+
const user = userEvent.setup();
|
|
1904
|
+
renderWithTheme( {
|
|
1905
|
+
withTooltips: true,
|
|
1906
|
+
data: [
|
|
1907
|
+
{
|
|
1908
|
+
label: 'Series A',
|
|
1909
|
+
data: [
|
|
1910
|
+
{ date: new Date( '2024-01-01' ), value: null as number | null, label: 'Jan 1' },
|
|
1911
|
+
{ date: new Date( '2024-01-02' ), value: 20, label: 'Jan 2' },
|
|
1912
|
+
],
|
|
1913
|
+
options: {},
|
|
1914
|
+
},
|
|
1915
|
+
],
|
|
1916
|
+
} );
|
|
1917
|
+
|
|
1918
|
+
screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
|
|
1919
|
+
await user.keyboard( '{ArrowRight}' );
|
|
1920
|
+
|
|
1921
|
+
const tooltip = screen.getByTestId( 'chart-tooltip-0' );
|
|
1922
|
+
expect( tooltip ).toHaveTextContent( 'Jan 1: No data' );
|
|
1923
|
+
expect( tooltip ).not.toHaveTextContent( 'Jan 1: 0' );
|
|
1924
|
+
} );
|
|
1925
|
+
|
|
1926
|
+
test( 'keeps both periods in the tooltip when the comparison period has no reading', async () => {
|
|
1927
|
+
const user = userEvent.setup();
|
|
1928
|
+
renderWithTheme( {
|
|
1929
|
+
withTooltips: true,
|
|
1930
|
+
data: [
|
|
1931
|
+
{
|
|
1932
|
+
label: 'This period',
|
|
1933
|
+
group: 'views',
|
|
1934
|
+
data: [
|
|
1935
|
+
{ date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' },
|
|
1936
|
+
{ date: new Date( '2024-01-02' ), value: 20, label: 'Jan 2' },
|
|
1937
|
+
],
|
|
1938
|
+
},
|
|
1939
|
+
{
|
|
1940
|
+
label: 'Previous period',
|
|
1941
|
+
group: 'views',
|
|
1942
|
+
options: { type: 'comparison' as const },
|
|
1943
|
+
data: [
|
|
1944
|
+
{ date: new Date( '2024-01-01' ), value: null as number | null, label: 'Jan 1' },
|
|
1945
|
+
{ date: new Date( '2024-01-02' ), value: 25, label: 'Jan 2' },
|
|
1946
|
+
],
|
|
1947
|
+
},
|
|
1948
|
+
],
|
|
1949
|
+
} );
|
|
1950
|
+
|
|
1951
|
+
screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
|
|
1952
|
+
await user.keyboard( '{ArrowRight}' );
|
|
1953
|
+
|
|
1954
|
+
const tooltip = screen.getByTestId( 'chart-tooltip-0' );
|
|
1955
|
+
expect( tooltip ).toHaveTextContent( 'This period: 10' );
|
|
1956
|
+
expect( tooltip ).toHaveTextContent( 'Previous period: No data' );
|
|
1957
|
+
} );
|
|
1958
|
+
|
|
1959
|
+
test( 'draws no comparison shadow for a comparison bucket with no reading', () => {
|
|
1960
|
+
renderWithTheme( {
|
|
1961
|
+
data: [
|
|
1962
|
+
{
|
|
1963
|
+
label: 'This period',
|
|
1964
|
+
group: 'views',
|
|
1965
|
+
data: [
|
|
1966
|
+
{ date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' },
|
|
1967
|
+
{ date: new Date( '2024-01-02' ), value: 20, label: 'Jan 2' },
|
|
1968
|
+
],
|
|
1969
|
+
},
|
|
1970
|
+
{
|
|
1971
|
+
label: 'Previous period',
|
|
1972
|
+
group: 'views',
|
|
1973
|
+
options: { type: 'comparison' as const },
|
|
1974
|
+
data: [
|
|
1975
|
+
{ date: new Date( '2024-01-01' ), value: null as number | null, label: 'Jan 1' },
|
|
1976
|
+
{ date: new Date( '2024-01-02' ), value: 25, label: 'Jan 2' },
|
|
1977
|
+
],
|
|
1978
|
+
},
|
|
1979
|
+
],
|
|
1980
|
+
} );
|
|
1981
|
+
|
|
1982
|
+
expect( screen.queryByTestId( 'bar-chart-comparison-1-0' ) ).not.toBeInTheDocument();
|
|
1983
|
+
expect( screen.getByTestId( 'bar-chart-comparison-1-1' ) ).toBeInTheDocument();
|
|
1984
|
+
} );
|
|
1658
1985
|
} );
|
|
1659
1986
|
|
|
1660
1987
|
describe( 'Tab Key Navigation', () => {
|
|
@@ -1770,6 +2097,77 @@ describe( 'BarChart', () => {
|
|
|
1770
2097
|
expect( screen.getByTestId( 'chart-tooltip-0' ) ).toBeInTheDocument();
|
|
1771
2098
|
expect( screen.getByTestId( 'chart-tooltip-0' ) ).toHaveTextContent( 'Series A' );
|
|
1772
2099
|
} );
|
|
2100
|
+
|
|
2101
|
+
describe( 'with a leading bucket that has no reading', () => {
|
|
2102
|
+
const data = [
|
|
2103
|
+
{
|
|
2104
|
+
label: 'Series A',
|
|
2105
|
+
data: [
|
|
2106
|
+
{ date: new Date( '2024-01-01' ), value: null as number | null, label: 'Jan 1' },
|
|
2107
|
+
{ date: new Date( '2024-01-02' ), value: 20, label: 'Jan 2' },
|
|
2108
|
+
{ date: new Date( '2024-01-03' ), value: 30, label: 'Jan 3' },
|
|
2109
|
+
],
|
|
2110
|
+
options: {},
|
|
2111
|
+
},
|
|
2112
|
+
];
|
|
2113
|
+
|
|
2114
|
+
test( 'outlines nothing when the selected bucket has no bar', async () => {
|
|
2115
|
+
const user = userEvent.setup();
|
|
2116
|
+
renderWithTheme( { withTooltips: true, data } );
|
|
2117
|
+
|
|
2118
|
+
screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
|
|
2119
|
+
await user.keyboard( '{ArrowRight}' );
|
|
2120
|
+
|
|
2121
|
+
expect( screen.getByTestId( 'chart-tooltip-0' ) ).toHaveTextContent( 'No data' );
|
|
2122
|
+
expect( screen.queryByTestId( 'bar-chart-keyboard-highlight' ) ).not.toBeInTheDocument();
|
|
2123
|
+
} );
|
|
2124
|
+
|
|
2125
|
+
test( 'outlines the first rendered bar for the first bucket with a reading', async () => {
|
|
2126
|
+
const user = userEvent.setup();
|
|
2127
|
+
renderWithTheme( { withTooltips: true, data } );
|
|
2128
|
+
|
|
2129
|
+
screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
|
|
2130
|
+
await user.keyboard( '{ArrowRight}{ArrowRight}' );
|
|
2131
|
+
|
|
2132
|
+
expect( screen.getByTestId( 'chart-tooltip-1' ) ).toHaveTextContent( '20' );
|
|
2133
|
+
expect( screen.getByTestId( 'bar-chart-keyboard-highlight' ) ).toHaveTextContent(
|
|
2134
|
+
/\.visx-bar:nth-child\(1\) \{/
|
|
2135
|
+
);
|
|
2136
|
+
} );
|
|
2137
|
+
|
|
2138
|
+
test( 'skips the bars an earlier series left out when outlining a later series', async () => {
|
|
2139
|
+
const user = userEvent.setup();
|
|
2140
|
+
const twoSeries = [
|
|
2141
|
+
{
|
|
2142
|
+
label: 'Series A',
|
|
2143
|
+
data: [
|
|
2144
|
+
{ date: new Date( '2024-01-01' ), value: null as number | null, label: 'Jan 1' },
|
|
2145
|
+
{ date: new Date( '2024-01-02' ), value: 20, label: 'Jan 2' },
|
|
2146
|
+
{ date: new Date( '2024-01-03' ), value: 30, label: 'Jan 3' },
|
|
2147
|
+
],
|
|
2148
|
+
options: {},
|
|
2149
|
+
},
|
|
2150
|
+
{
|
|
2151
|
+
label: 'Series B',
|
|
2152
|
+
data: [
|
|
2153
|
+
{ date: new Date( '2024-01-01' ), value: 5, label: 'Jan 1' },
|
|
2154
|
+
{ date: new Date( '2024-01-02' ), value: 15, label: 'Jan 2' },
|
|
2155
|
+
{ date: new Date( '2024-01-03' ), value: 25, label: 'Jan 3' },
|
|
2156
|
+
],
|
|
2157
|
+
options: {},
|
|
2158
|
+
},
|
|
2159
|
+
];
|
|
2160
|
+
renderWithTheme( { withTooltips: true, data: twoSeries } );
|
|
2161
|
+
|
|
2162
|
+
screen.getByRole( 'grid', { name: /bar chart/i } ).focus();
|
|
2163
|
+
await user.keyboard( '{ArrowRight}{ArrowRight}{ArrowRight}{ArrowRight}' );
|
|
2164
|
+
|
|
2165
|
+
expect( screen.getByTestId( 'chart-tooltip-3' ) ).toHaveTextContent( '15' );
|
|
2166
|
+
expect( screen.getByTestId( 'bar-chart-keyboard-highlight' ) ).toHaveTextContent(
|
|
2167
|
+
/\.visx-bar:nth-child\(4\) \{/
|
|
2168
|
+
);
|
|
2169
|
+
} );
|
|
2170
|
+
} );
|
|
1773
2171
|
} );
|
|
1774
2172
|
|
|
1775
2173
|
test( 'keyboard navigation works with custom tooltip renderer', async () => {
|
|
@@ -2065,6 +2463,83 @@ describe( 'BarChart', () => {
|
|
|
2065
2463
|
} );
|
|
2066
2464
|
} );
|
|
2067
2465
|
|
|
2466
|
+
describe( 'Value axis baseline', () => {
|
|
2467
|
+
it( 'starts the value axis at zero in horizontal charts', () => {
|
|
2468
|
+
const { result } = renderHook( () => useBarChartOptions( steadyTrafficData, true, {} ) );
|
|
2469
|
+
const xScale = result.current.xScale as { domain?: number[] };
|
|
2470
|
+
expect( xScale.domain ).toEqual( [ 0, 989 ] );
|
|
2471
|
+
} );
|
|
2472
|
+
|
|
2473
|
+
it( 'lets a caller opt out of the zero baseline', () => {
|
|
2474
|
+
const { result } = renderHook( () =>
|
|
2475
|
+
useBarChartOptions( steadyTrafficData, false, { yScale: { zero: false } } )
|
|
2476
|
+
);
|
|
2477
|
+
const yScale = result.current.yScale as { domain?: number[] };
|
|
2478
|
+
expect( yScale.domain ).toBeUndefined();
|
|
2479
|
+
} );
|
|
2480
|
+
|
|
2481
|
+
it( 'reads the opt-out from the x scale of a horizontal chart', () => {
|
|
2482
|
+
const { result } = renderHook( () =>
|
|
2483
|
+
useBarChartOptions( steadyTrafficData, true, { xScale: { zero: false } } )
|
|
2484
|
+
);
|
|
2485
|
+
const xScale = result.current.xScale as { domain?: number[] };
|
|
2486
|
+
expect( xScale.domain ).toBeUndefined();
|
|
2487
|
+
} );
|
|
2488
|
+
|
|
2489
|
+
it( 'keeps an explicit domain as given', () => {
|
|
2490
|
+
const { result } = renderHook( () =>
|
|
2491
|
+
useBarChartOptions( steadyTrafficData, false, { yScale: { domain: [ 900, 1000 ] } } )
|
|
2492
|
+
);
|
|
2493
|
+
const yScale = result.current.yScale as { domain?: number[] };
|
|
2494
|
+
expect( yScale.domain ).toEqual( [ 900, 1000 ] );
|
|
2495
|
+
} );
|
|
2496
|
+
|
|
2497
|
+
it( 'renders an explicit domain without stretching it to zero', () => {
|
|
2498
|
+
render(
|
|
2499
|
+
<BarChart
|
|
2500
|
+
data={ steadyTrafficData }
|
|
2501
|
+
width={ 400 }
|
|
2502
|
+
height={ 300 }
|
|
2503
|
+
options={ { yScale: { domain: [ 900, 1000 ] } } }
|
|
2504
|
+
/>
|
|
2505
|
+
);
|
|
2506
|
+
const heights = Array.from( getBarRects() ).map( bar =>
|
|
2507
|
+
parseFloat( bar.getAttribute( 'height' ) || '0' )
|
|
2508
|
+
);
|
|
2509
|
+
// 921 sits at 21% of a 900–1000 axis; on a 0–1000 axis it would be 92%.
|
|
2510
|
+
expect( Math.min( ...heights ) / Math.max( ...heights ) ).toBeLessThan( 0.5 );
|
|
2511
|
+
} );
|
|
2512
|
+
|
|
2513
|
+
it( 'keeps zero in the domain when the caller opts out but a comparison series is present', () => {
|
|
2514
|
+
const data = [
|
|
2515
|
+
...steadyTrafficData,
|
|
2516
|
+
{
|
|
2517
|
+
label: 'Previous',
|
|
2518
|
+
options: { type: 'comparison' as const },
|
|
2519
|
+
data: steadyTrafficData[ 0 ].data.map( point => ( {
|
|
2520
|
+
...point,
|
|
2521
|
+
value: point.value - 10,
|
|
2522
|
+
} ) ),
|
|
2523
|
+
},
|
|
2524
|
+
];
|
|
2525
|
+
const { result } = renderHook( () =>
|
|
2526
|
+
useBarChartOptions( data, false, { yScale: { zero: false } } )
|
|
2527
|
+
);
|
|
2528
|
+
const yScale = result.current.yScale as { domain?: number[] };
|
|
2529
|
+
expect( yScale.domain ).toEqual( [ 0, 989 ] );
|
|
2530
|
+
} );
|
|
2531
|
+
|
|
2532
|
+
it( 'renders every bar of steady traffic at most a few percent shorter than the tallest', () => {
|
|
2533
|
+
render( <BarChart data={ steadyTrafficData } width={ 400 } height={ 300 } /> );
|
|
2534
|
+
const heights = Array.from( getBarRects() ).map( bar =>
|
|
2535
|
+
parseFloat( bar.getAttribute( 'height' ) || '0' )
|
|
2536
|
+
);
|
|
2537
|
+
expect( heights ).toHaveLength( 7 );
|
|
2538
|
+
const tallest = Math.max( ...heights );
|
|
2539
|
+
expect( Math.min( ...heights ) / tallest ).toBeGreaterThan( 0.9 );
|
|
2540
|
+
} );
|
|
2541
|
+
} );
|
|
2542
|
+
|
|
2068
2543
|
describe( 'Comparison Series', () => {
|
|
2069
2544
|
it( 'renders a translucent shadow behind the primary bar for a comparison series', () => {
|
|
2070
2545
|
const data = [
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BaseChartProps,
|
|
3
|
+
DataPointDate,
|
|
4
|
+
SeriesData,
|
|
5
|
+
SeriesChartLegendConfig,
|
|
6
|
+
SeriesVisibilityProps,
|
|
7
|
+
} from '../../types';
|
|
8
|
+
import type { RenderTooltipParams } from '../../visx/types';
|
|
9
|
+
import type { ReactNode } from 'react';
|
|
10
|
+
|
|
11
|
+
export type BandHighlightSelection = {
|
|
12
|
+
datum: DataPointDate;
|
|
13
|
+
key: string;
|
|
14
|
+
index: number;
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export interface BarChartProps extends BaseChartProps< SeriesData[] >, SeriesVisibilityProps {
|
|
22
|
+
/**
|
|
23
|
+
* Legend configuration. Supports `collapseGroups` on top of the shared options.
|
|
24
|
+
*/
|
|
25
|
+
legend?: SeriesChartLegendConfig;
|
|
26
|
+
renderTooltip?: ( params: RenderTooltipParams< DataPointDate > ) => ReactNode;
|
|
27
|
+
/** Place the tooltip beside its category without vertical flipping. */
|
|
28
|
+
tooltipPlacement?: 'auto' | 'beside';
|
|
29
|
+
/** Tooltip top anchor in SVG coordinates; negative offsets are supported. */
|
|
30
|
+
tooltipAnchorTop?: number;
|
|
31
|
+
orientation?: 'horizontal' | 'vertical';
|
|
32
|
+
withPatterns?: boolean;
|
|
33
|
+
showZeroValues?: boolean;
|
|
34
|
+
/** Highlight the active band across the plot when tooltips are enabled. */
|
|
35
|
+
withBandHighlight?: boolean;
|
|
36
|
+
/** Receive active band bounds in SVG coordinates, or null when dismissed. */
|
|
37
|
+
onBandHighlightChange?: ( selection: BandHighlightSelection | null ) => void;
|
|
38
|
+
children?: ReactNode;
|
|
39
|
+
}
|