Skip to content

Commit f6ea007

Browse files
Nikschavanmikestottuk
authored andcommitted
CHARTS-33: Allow glyphs to be defined from theme (#43875)
1 parent 9341e4b commit f6ea007

File tree

6 files changed

+262
-25
lines changed

6 files changed

+262
-25
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: minor
2+
Type: added
3+
4+
Allow setting the glyphs array in the theme to have these rendered in the line chart

projects/js-packages/charts/src/components/line-chart/line-chart.tsx

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useId, useMemo, useContext } from 'react';
77
import { useXYChartTheme, useChartTheme } from '../../providers/theme/theme-provider';
88
import { Legend } from '../legend';
99
import { parseAsLocalDate } from '../shared/date-parsing';
10+
import { DefaultGlyph } from '../shared/default-glyph';
1011
import { useChartMargin } from '../shared/use-chart-margin';
1112
import { useElementHeight } from '../shared/use-element-height';
1213
import { withResponsive } from '../shared/with-responsive';
@@ -25,24 +26,6 @@ export type RenderLineStartGlyphProps< Datum extends object > = GlyphProps< Datu
2526
glyphStyle?: React.SVGProps< SVGCircleElement >;
2627
};
2728

28-
const DefaultGlyph = < Datum extends object >( props: RenderLineStartGlyphProps< Datum > ) => {
29-
const { theme } = useContext( DataContext ) || {};
30-
31-
return (
32-
<circle
33-
cx={ props.x }
34-
cy={ props.y }
35-
r={ props.size }
36-
fill={ props.color }
37-
stroke={ theme?.backgroundColor }
38-
strokeWidth={ 1.5 }
39-
paintOrder="fill"
40-
data-testid={ `start-glyph-${ props.index }` }
41-
{ ...props.glyphStyle }
42-
/>
43-
);
44-
};
45-
4629
const defaultRenderGlyph = < Datum extends object >(
4730
props: RenderLineStartGlyphProps< Datum >
4831
) => {
@@ -258,6 +241,16 @@ const LineChart: FC< LineChartProps > = ( {
258241
};
259242
}, [ options, dataSorted, width ] );
260243

244+
const tooltipRenderGlyph = useMemo( () => {
245+
return ( props: GlyphProps< DataPointDate > ) => {
246+
const seriesIndex = dataSorted.findIndex(
247+
series => series.label === props.key || series.data.includes( props.datum as DataPointDate )
248+
);
249+
const themeGlyph = providerTheme.glyphs?.[ seriesIndex ];
250+
return themeGlyph ? themeGlyph( props ) : renderGlyph( props );
251+
};
252+
}, [ dataSorted, providerTheme.glyphs, renderGlyph ] );
253+
261254
const defaultMargin = useChartMargin( height, chartOptions, dataSorted, theme );
262255

263256
const error = validateData( dataSorted );
@@ -271,7 +264,7 @@ const LineChart: FC< LineChartProps > = ( {
271264
value: '', // Empty string since we don't want to show a specific value
272265
color: group?.options?.stroke ?? providerTheme.colors[ index % providerTheme.colors.length ],
273266
shapeStyle: group?.options?.legendShapeStyle,
274-
renderGlyph: withLegendGlyph ? renderGlyph : undefined,
267+
renderGlyph: withLegendGlyph ? providerTheme.glyphs?.[ index ] ?? renderGlyph : undefined,
275268
glyphSize: Number( glyphStyle?.radius ),
276269
} ) );
277270

@@ -322,7 +315,7 @@ const LineChart: FC< LineChartProps > = ( {
322315
index={ index }
323316
data={ seriesData }
324317
color={ stroke }
325-
renderGlyph={ renderGlyph }
318+
renderGlyph={ providerTheme.glyphs?.[ index ] ?? renderGlyph }
326319
accessors={ accessors }
327320
glyphStyle={ glyphStyle }
328321
/>
@@ -364,7 +357,7 @@ const LineChart: FC< LineChartProps > = ( {
364357
snapTooltipToDatumY
365358
showSeriesGlyphs
366359
renderTooltip={ renderTooltip }
367-
renderGlyph={ renderGlyph }
360+
renderGlyph={ tooltipRenderGlyph }
368361
glyphStyle={ glyphStyle }
369362
showVerticalCrosshair={ withTooltipCrosshairs?.showVertical }
370363
showHorizontalCrosshair={ withTooltipCrosshairs?.showHorizontal }

projects/js-packages/charts/src/components/line-chart/stories/index.stories.tsx

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,102 @@
1+
import { GlyphDiamond, GlyphStar } from '@visx/glyph';
12
import React from 'react';
3+
import { ThemeProvider, jetpackTheme, wooTheme } from '../../../providers/theme';
4+
import { DefaultGlyph } from '../../shared/default-glyph';
25
import LineChart from '../line-chart';
36
import { lineChartStoryArgs, lineChartMetaArgs } from './config';
47
import largeValuesData from './large-values-sample';
58
import sampleData from './sample-data';
69
import webTrafficData from './site-traffic-sample';
710
import type { Meta, StoryFn, StoryObj } from '@storybook/react';
811

12+
const customStorybookTheme = {
13+
...jetpackTheme,
14+
glyphs: [
15+
props => React.createElement( DefaultGlyph, { ...props, key: props.key } ),
16+
props =>
17+
React.createElement( GlyphStar, {
18+
key: props.key,
19+
top: props.y,
20+
left: props.x,
21+
size: props.size * props.size,
22+
fill: props.color,
23+
} ),
24+
props =>
25+
React.createElement( GlyphDiamond, {
26+
key: props.key,
27+
top: props.y,
28+
left: props.x,
29+
size: props.size * props.size,
30+
fill: props.color,
31+
} ),
32+
],
33+
};
34+
35+
const THEME_MAP = {
36+
default: undefined,
37+
jetpack: jetpackTheme,
38+
woo: wooTheme,
39+
customStorybook: customStorybookTheme,
40+
};
41+
942
const meta: Meta< typeof LineChart > = {
1043
...lineChartMetaArgs,
1144
title: 'JS Packages/Charts/Types/Line Chart',
45+
component: LineChart,
46+
parameters: {
47+
layout: 'centered',
48+
},
49+
decorators: [
50+
( Story, { args } ) => {
51+
const theme = THEME_MAP[ args.themeName ];
52+
53+
return (
54+
<ThemeProvider theme={ theme }>
55+
<div
56+
style={ {
57+
resize: 'both',
58+
overflow: 'auto',
59+
padding: '2rem',
60+
width: '800px',
61+
maxWidth: '1200px',
62+
border: '1px dashed #ccc',
63+
display: 'inline-block',
64+
} }
65+
>
66+
<Story />
67+
</div>
68+
</ThemeProvider>
69+
);
70+
},
71+
],
72+
argTypes: {
73+
themeName: {
74+
control: 'select',
75+
options: [ 'default', 'jetpack', 'woo', 'customStorybook' ],
76+
defaultValue: 'default',
77+
},
78+
maxWidth: {
79+
control: {
80+
type: 'number',
81+
min: 100,
82+
max: 1200,
83+
},
84+
},
85+
aspectRatio: {
86+
control: {
87+
type: 'number',
88+
min: 0,
89+
max: 1,
90+
},
91+
},
92+
resizeDebounceTime: {
93+
control: {
94+
type: 'number',
95+
min: 0,
96+
max: 10000,
97+
},
98+
},
99+
},
12100
} satisfies Meta< typeof LineChart >;
13101

14102
export default meta;
@@ -271,6 +359,78 @@ BrokenLine.parameters = {
271359
},
272360
};
273361

362+
export const WithStartGlyphs: StoryObj< typeof LineChart > = Template.bind( {} );
363+
WithStartGlyphs.args = {
364+
...Default.args,
365+
withStartGlyphs: true,
366+
};
367+
368+
export const WithCustomGlyph: StoryObj< typeof LineChart > = Template.bind( {} );
369+
WithCustomGlyph.args = {
370+
...Default.args,
371+
showLegend: true,
372+
withStartGlyphs: true,
373+
withLegendGlyph: true,
374+
renderGlyph: ( { color, size, x, y } ) => {
375+
return <GlyphStar top={ y } left={ x } size={ size * size } fill={ color } />;
376+
},
377+
glyphStyle: {
378+
radius: 10,
379+
},
380+
};
381+
382+
const CustomStarGlyph = ( { color, size, x, y } ) => {
383+
const hasXY = typeof x === 'number' && typeof y === 'number' && ( x !== 0 || y !== 0 );
384+
const groupProps = hasXY ? { transform: `translate(${ x }, ${ y })` } : {};
385+
return (
386+
<g { ...groupProps }>
387+
<svg
388+
xmlns="http://www.w3.org/2000/svg"
389+
width={ size * 2 }
390+
height={ size * 2 }
391+
viewBox="0 0 24 24"
392+
style={ { overflow: 'visible', pointerEvents: 'none' } }
393+
>
394+
<path
395+
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"
396+
fill={ color }
397+
stroke={ color }
398+
strokeWidth="2"
399+
strokeLinecap="round"
400+
strokeLinejoin="round"
401+
transform="translate(-12, -12)"
402+
/>
403+
</svg>
404+
</g>
405+
);
406+
};
407+
408+
export const WithCustomSvgGlyph: StoryObj< typeof LineChart > = Template.bind( {} );
409+
WithCustomSvgGlyph.args = {
410+
...Default.args,
411+
showLegend: true,
412+
withStartGlyphs: true,
413+
withLegendGlyph: true,
414+
renderGlyph: ( { color, size, x, y } ) => (
415+
<CustomStarGlyph color={ color } size={ size } x={ x } y={ y } />
416+
),
417+
glyphStyle: {
418+
radius: 8,
419+
},
420+
};
421+
422+
export const WithCustomGlyphsPerDataPoint: StoryObj< typeof LineChart > = Template.bind( {} );
423+
WithCustomGlyphsPerDataPoint.args = {
424+
...Default.args,
425+
showLegend: true,
426+
withStartGlyphs: true,
427+
withLegendGlyph: true,
428+
themeName: 'customStorybook', // Mock prop used to switch the rendered theme in the storybook.
429+
glyphStyle: {
430+
radius: 8,
431+
},
432+
};
433+
274434
export const DateStringFormats: StoryObj< typeof LineChart > = {
275435
render: () => {
276436
return (

projects/js-packages/charts/src/components/line-chart/test/line-chart.test.tsx

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,36 @@
33
*/
44

55
import { render, screen } from '@testing-library/react';
6-
import { ThemeProvider } from '../../../providers/theme';
6+
import { GlyphDiamond } from '@visx/glyph';
7+
import React from 'react';
8+
import { jetpackTheme, ThemeProvider, wooTheme } from '../../../providers/theme';
79
import LineChart from '../line-chart';
810

11+
const customTheme = {
12+
...jetpackTheme,
13+
glyphs: [
14+
props =>
15+
React.createElement(
16+
'g',
17+
{ 'data-testid': 'custom-glyph-diamond' },
18+
React.createElement( GlyphDiamond, {
19+
key: props.key,
20+
top: props.y,
21+
left: props.x,
22+
size: props.size * props.size,
23+
fill: props.color,
24+
} )
25+
),
26+
],
27+
};
28+
29+
const THEME_MAP = {
30+
default: undefined,
31+
jetpack: jetpackTheme,
32+
woo: wooTheme,
33+
custom: customTheme,
34+
};
35+
936
describe( 'LineChart', () => {
1037
const defaultProps = {
1138
width: 500,
@@ -22,9 +49,11 @@ describe( 'LineChart', () => {
2249
],
2350
};
2451

25-
const renderWithTheme = ( props = {} ) => {
52+
const renderWithTheme = ( props = {}, themeName = 'jetpack' ) => {
53+
const theme = THEME_MAP[ themeName ];
54+
2655
return render(
27-
<ThemeProvider>
56+
<ThemeProvider theme={ theme }>
2857
{ /* @ts-expect-error TODO Fix the missing props */ }
2958
<LineChart { ...defaultProps } { ...props } />
3059
</ThemeProvider>
@@ -233,6 +262,32 @@ describe( 'LineChart', () => {
233262
const startGlyphs = screen.getAllByTestId( /start-glyph/i );
234263
expect( startGlyphs ).toHaveLength( 1 );
235264
} );
265+
266+
test( 'Renders custom glyph from theme', () => {
267+
renderWithTheme(
268+
{
269+
withStartGlyphs: true,
270+
data: [
271+
{
272+
label: 'Series A',
273+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
274+
},
275+
{
276+
label: 'Series B',
277+
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
278+
},
279+
],
280+
},
281+
'custom'
282+
);
283+
284+
// We are rendering one custom glyph from theme and the second dataset will be using default glyph.
285+
const defaultGlyphs = screen.getAllByTestId( /start-glyph/i );
286+
expect( defaultGlyphs ).toHaveLength( 1 );
287+
288+
const customGlyphs = screen.getAllByTestId( /custom-glyph/i );
289+
expect( customGlyphs ).toHaveLength( 1 );
290+
} );
236291
} );
237292

238293
describe( 'Legend Glyphs', () => {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { DataContext } from '@visx/xychart';
2+
import { useContext } from 'react';
3+
import type { RenderLineStartGlyphProps } from '../line-chart/line-chart';
4+
5+
export const DefaultGlyph = < Datum extends object >(
6+
props: RenderLineStartGlyphProps< Datum >
7+
) => {
8+
const { theme } = useContext( DataContext ) || {};
9+
10+
return (
11+
<circle
12+
cx={ props.x }
13+
cy={ props.y }
14+
r={ props.size }
15+
fill={ props.color }
16+
stroke={ theme?.backgroundColor }
17+
strokeWidth={ 1.5 }
18+
paintOrder="fill"
19+
data-testid={ `start-glyph-${ props.index }` }
20+
{ ...props.glyphStyle }
21+
/>
22+
);
23+
};

projects/js-packages/charts/src/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { AxisScale, Orientation, TickFormatter, AxisRendererProps } from '@visx/axis';
22
import type { LegendShape } from '@visx/legend/lib/types';
33
import type { ScaleInput, ScaleType } from '@visx/scale';
4-
import type { EventHandlerParams, GridStyles, LineStyles } from '@visx/xychart';
4+
import type { EventHandlerParams, GlyphProps, GridStyles, LineStyles } from '@visx/xychart';
55
import type { CSSProperties, PointerEvent, ReactNode } from 'react';
66

77
type ValueOf< T > = T[ keyof T ];
@@ -99,6 +99,8 @@ export type ChartTheme = {
9999
seriesLineStyles?: LineStyles[];
100100
/** Styles for legend shapes */
101101
legendShapeStyles?: CSSProperties[];
102+
/** Array of render functions for glyphs */
103+
glyphs?: Array< < Datum extends object >( props: GlyphProps< Datum > ) => ReactNode >;
102104
/** Styles for legend labels */
103105
legendLabelStyles?: CSSProperties;
104106
/** Styles for legend container */

0 commit comments

Comments
 (0)