@automattic/charts 3.1.0 → 3.1.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/charts",
3
- "version": "3.1.0",
3
+ "version": "3.1.1",
4
4
  "description": "Display charts within Automattic products.",
5
5
  "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/charts/#readme",
6
6
  "bugs": {
@@ -205,8 +205,8 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
205
205
  }, [ dataSorted, stacked, stackOffset, rescaleYOnVisibility ] );
206
206
 
207
207
  const chartOptions = useMemo( () => {
208
- const { tickResolution, ...xAxisOptions } = options?.axis?.x ?? {};
209
- const formatter = xAxisOptions.tickFormat || getFormatter( dataSorted, tickResolution );
208
+ const { tickResolution, tickFormat, ...xAxisOptions } = options?.axis?.x ?? {};
209
+ const formatter = tickFormat || getFormatter( dataSorted, tickResolution );
210
210
 
211
211
  return {
212
212
  axis: {
@@ -112,6 +112,51 @@ describe( 'AreaChart', () => {
112
112
  } );
113
113
  } );
114
114
 
115
+ describe( 'X-Axis Ticks', () => {
116
+ const monthlySeries = [
117
+ {
118
+ label: 'Series A',
119
+ data: [
120
+ { date: new Date( '2024-01-01' ), value: 10 },
121
+ { date: new Date( '2024-04-01' ), value: 20 },
122
+ { date: new Date( '2024-07-01' ), value: 30 },
123
+ { date: new Date( '2024-10-01' ), value: 40 },
124
+ { date: new Date( '2025-03-01' ), value: 50 },
125
+ ],
126
+ },
127
+ ];
128
+
129
+ test( 'keeps the derived month formatter when tickFormat is passed as undefined', () => {
130
+ renderWithProvider( {
131
+ options: { axis: { x: { tickFormat: undefined } } },
132
+ data: monthlySeries,
133
+ } );
134
+
135
+ // January is absent: formatMonthOrYearTick renders it as the year instead.
136
+ const ticks = screen.getAllByText( /^(Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)$/ );
137
+ expect( ticks.length ).toBeGreaterThan( 1 );
138
+ expect(
139
+ screen.queryByText( /^(February|March|April|June|July|August|September|October)$/ )
140
+ ).not.toBeInTheDocument();
141
+ } );
142
+
143
+ test( 'honors an explicit tickFormat over the derived formatter', () => {
144
+ renderWithProvider( {
145
+ options: {
146
+ axis: {
147
+ x: {
148
+ tickFormat: ( date: Date | number ) =>
149
+ `tick-${ new Date( Number( date ) ).getUTCMonth() }`,
150
+ },
151
+ },
152
+ },
153
+ data: monthlySeries,
154
+ } );
155
+
156
+ expect( screen.getAllByText( /^tick-\d+$/ ).length ).toBeGreaterThan( 0 );
157
+ } );
158
+ } );
159
+
115
160
  describe( 'Stacking', () => {
116
161
  test( 'is stacked by default', () => {
117
162
  renderWithProvider();
@@ -154,23 +154,29 @@ export function useBarChartOptions(
154
154
  const stableOptions = useDeepMemo( options );
155
155
 
156
156
  // `labelOverflow` and `tickResolution` are consumed by this hook rather than
157
- // forwarded — visx has an axis prop for neither — so they are split off the
158
- // caller's axis options once, here, and only the rest reaches visx below.
157
+ // forwarded — visx has an axis prop for neither — and `tickFormat` is merged
158
+ // with the derived default explicitly, so an `undefined` passed by a caller
159
+ // cannot clobber it through the spread. They are split off the caller's axis
160
+ // options once, here, and only the rest reaches visx below.
159
161
  const axisConfig = useMemo( () => {
160
162
  const {
161
163
  labelOverflow: xLabelOverflow,
162
164
  tickResolution: xTickResolution,
165
+ tickFormat: xTickFormat,
163
166
  ...xAxisOptions
164
167
  } = stableOptions.axis?.x || {};
165
168
  const {
166
169
  labelOverflow: yLabelOverflow,
167
170
  tickResolution: yTickResolution,
171
+ tickFormat: yTickFormat,
168
172
  ...yAxisOptions
169
173
  } = stableOptions.axis?.y || {};
170
174
 
171
175
  return {
172
176
  xLabelOverflow,
173
177
  yLabelOverflow,
178
+ xTickFormat,
179
+ yTickFormat,
174
180
  xAxisOptions,
175
181
  yAxisOptions,
176
182
  // The dates sit on the x axis normally, and on the y axis when the
@@ -243,8 +249,8 @@ export function useBarChartOptions(
243
249
  return useMemo( () => {
244
250
  const orientationKey = horizontal ? 'horizontal' : 'vertical';
245
251
  const {
246
- xTickFormat,
247
- yTickFormat,
252
+ xTickFormat: defaultXTickFormat,
253
+ yTickFormat: defaultYTickFormat,
248
254
  tooltipLabelFormatter: defaultTooltipLabelFormatter,
249
255
  xAccessor,
250
256
  yAccessor,
@@ -294,10 +300,11 @@ export function useBarChartOptions(
294
300
  ...( stableOptions.yScale || {} ),
295
301
  ...( ! horizontal ? valueScaleDomainOverride : {} ),
296
302
  };
297
- const { xLabelOverflow, yLabelOverflow, xAxisOptions, yAxisOptions } = axisConfig;
298
- const providedToolTipLabelFormatter = horizontal
299
- ? yAxisOptions.tickFormat
300
- : xAxisOptions.tickFormat;
303
+ const { xLabelOverflow, yLabelOverflow, xTickFormat, yTickFormat, xAxisOptions, yAxisOptions } =
304
+ axisConfig;
305
+ // The dates sit on the y axis of a horizontal chart, so the caller's format
306
+ // for them moves with the orientation. It also labels the tooltip.
307
+ const dateAxisTickFormat = horizontal ? yTickFormat : xTickFormat;
301
308
 
302
309
  // A band scale has no ticks of its own for visx to ask for, so it samples
303
310
  // the domain by index and can miss the tick that dates the day or names the
@@ -306,7 +313,7 @@ export function useBarChartOptions(
306
313
  const { timeAxis } = defaultOptions;
307
314
  const dateAxisOptions = horizontal ? yAxisOptions : xAxisOptions;
308
315
  const bandTickValues =
309
- timeAxis && ! dateAxisOptions.tickFormat
316
+ timeAxis && ! dateAxisTickFormat
310
317
  ? getBandTickValues(
311
318
  timeAxis.domain,
312
319
  timeAxis.tickFormatter,
@@ -327,7 +334,7 @@ export function useBarChartOptions(
327
334
  x: {
328
335
  orientation: 'bottom' as const,
329
336
  numTicks: DEFAULT_NUM_TICKS,
330
- tickFormat: xTickFormat,
337
+ tickFormat: xTickFormat || defaultXTickFormat,
331
338
  ...( horizontal ? {} : dateAxisTickValues ),
332
339
  ...( xLabelOverflow === 'ellipsis' ? { tickComponent: TruncatedXTickComponent } : {} ),
333
340
  ...xAxisOptions,
@@ -335,7 +342,7 @@ export function useBarChartOptions(
335
342
  y: {
336
343
  orientation: 'left' as const,
337
344
  numTicks: DEFAULT_NUM_TICKS,
338
- tickFormat: yTickFormat,
345
+ tickFormat: yTickFormat || defaultYTickFormat,
339
346
  ...( horizontal ? dateAxisTickValues : {} ),
340
347
  ...( yLabelOverflow === 'ellipsis' ? { tickComponent: TruncatedYTickComponent } : {} ),
341
348
  ...yAxisOptions,
@@ -345,7 +352,7 @@ export function useBarChartOptions(
345
352
  padding: getGroupPadding( horizontal ? yScale : xScale ),
346
353
  },
347
354
  tooltip: {
348
- labelFormatter: providedToolTipLabelFormatter || defaultTooltipLabelFormatter,
355
+ labelFormatter: dateAxisTickFormat || defaultTooltipLabelFormatter,
349
356
  },
350
357
  };
351
358
  }, [ defaultOptions, axisConfig, stableOptions, horizontal, data ] );
@@ -505,6 +505,28 @@ describe( 'BarChart', () => {
505
505
  expect( distinctTexts( ticks ).size ).toBeGreaterThan( 1 );
506
506
  } );
507
507
 
508
+ test( 'keeps the derived date formatter when tickFormat is passed as undefined', () => {
509
+ renderWithTheme( {
510
+ width: 800,
511
+ options: { axis: { x: { tickFormat: undefined } } },
512
+ data: [
513
+ {
514
+ label: 'Series A',
515
+ data: Array.from( { length: 14 }, ( _, i ) => ( {
516
+ date: new Date( 2024, 0, 1 + i ),
517
+ value: 10 + i,
518
+ } ) ),
519
+ options: {},
520
+ },
521
+ ],
522
+ } );
523
+
524
+ const ticks = screen.getAllByText( /^Jan \d+$/ );
525
+ expect( distinctTexts( ticks ).size ).toBeGreaterThan( 1 );
526
+ // A clobbered formatter falls back to raw `Date.toString()` ticks.
527
+ expect( screen.queryByText( /GMT/ ) ).not.toBeInTheDocument();
528
+ } );
529
+
508
530
  test( 'renders distinct hour ticks for sub-daily buckets in a single day', () => {
509
531
  renderWithTheme( {
510
532
  width: 800,
@@ -291,8 +291,8 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
291
291
  } );
292
292
 
293
293
  const chartOptions = useMemo( () => {
294
- const { tickResolution, ...xAxisOptions } = options?.axis?.x ?? {};
295
- const formatter = xAxisOptions.tickFormat || getFormatter( dataSorted, tickResolution );
294
+ const { tickResolution, tickFormat, ...xAxisOptions } = options?.axis?.x ?? {};
295
+ const formatter = tickFormat || getFormatter( dataSorted, tickResolution );
296
296
 
297
297
  return {
298
298
  axis: {
@@ -500,6 +500,57 @@ describe( 'LineChart', () => {
500
500
  expect( ticks.length ).toBeGreaterThan( 1 );
501
501
  } );
502
502
 
503
+ test( 'keeps the derived month formatter when tickFormat is passed as undefined.', () => {
504
+ renderWithTheme( {
505
+ width: 800,
506
+ options: { axis: { x: { tickFormat: undefined } } },
507
+ data: [
508
+ {
509
+ label: 'Series A',
510
+ data: [
511
+ { date: new Date( '2024-01-01' ), value: 10 },
512
+ { date: new Date( '2024-04-01' ), value: 20 },
513
+ { date: new Date( '2024-07-01' ), value: 30 },
514
+ { date: new Date( '2024-10-01' ), value: 40 },
515
+ { date: new Date( '2025-03-01' ), value: 50 },
516
+ ],
517
+ },
518
+ ],
519
+ } );
520
+
521
+ // January is absent: formatMonthOrYearTick renders it as the year instead.
522
+ const ticks = screen.getAllByText( /^(Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)$/ );
523
+ expect( ticks.length ).toBeGreaterThan( 1 );
524
+ expect(
525
+ screen.queryByText( /^(February|March|April|June|July|August|September|October)$/ )
526
+ ).not.toBeInTheDocument();
527
+ } );
528
+
529
+ test( 'honors an explicit tickFormat over the derived formatter.', () => {
530
+ renderWithTheme( {
531
+ width: 800,
532
+ options: {
533
+ axis: {
534
+ x: {
535
+ tickFormat: ( date: Date | number ) =>
536
+ `tick-${ new Date( Number( date ) ).getUTCMonth() }`,
537
+ },
538
+ },
539
+ },
540
+ data: [
541
+ {
542
+ label: 'Series A',
543
+ data: [
544
+ { date: new Date( '2024-01-01' ), value: 10 },
545
+ { date: new Date( '2024-07-01' ), value: 30 },
546
+ ],
547
+ },
548
+ ],
549
+ } );
550
+
551
+ expect( screen.getAllByText( /^tick-\d+$/ ).length ).toBeGreaterThan( 0 );
552
+ } );
553
+
503
554
  test( 'renders ticks in year format.', () => {
504
555
  renderWithTheme( {
505
556
  width: 800,