Skip to content

Commit 43acccc

Browse files
[charts] Add batch bar plot
Fix naming collision Fix border radius Handle border radii greater than width/height Add example and renderer prop Remove unused code WIP: Use flatbush for finding the clicked bar Add simple animation Split Render series separately Implement highlight and fading Import fixing. Remove unused prop. Update docs Slight performance improvements Share appendAtKey Fix layout-dependent animation Return layout and origin in bar plot data Fix rebase Remove unused prop Fix imports Abstract position in getClosestPoint Fix type issues Handle both layouts when creating bar flatbush Fix flatbush with nulls Fix renderer prop not being forwarded Fix animation Remove disableHover mention Try to implement onItemClick Investigate flatbush issue Remove flatbush usage Add on item click Update docs. Cleanup batch bar after on item click working. Some more progress Remove memo for now Add fading and highlighting (missing animation) Remove animation from highlight/fade Fix type imports Fix pointer leave Rename Add brightness filter to highlighted item Revert demos Fix types Fix rebase Address feedback Use ordinal time ticks Gen stuff [charts] Add batch bar plot Fix naming collision Fix border radius Handle border radii greater than width/height Add example and renderer prop Remove unused code WIP: Use flatbush for finding the clicked bar Add simple animation Split Render series separately Implement highlight and fading Import fixing. Remove unused prop. Update docs Slight performance improvements Share appendAtKey Fix layout-dependent animation Return layout and origin in bar plot data Fix rebase Remove unused prop Fix imports Abstract position in getClosestPoint Fix type issues Handle both layouts when creating bar flatbush Fix flatbush with nulls Fix renderer prop not being forwarded Fix animation Remove disableHover mention Try to implement onItemClick Investigate flatbush issue Remove flatbush usage Add on item click Update docs. Cleanup batch bar after on item click working. Some more progress Remove memo for now Add fading and highlighting (missing animation) Remove animation from highlight/fade Fix type imports Fix pointer leave Rename Add brightness filter to highlighted item Revert demos Fix types Fix rebase Test bar batch rendering
1 parent 68583b7 commit 43acccc

File tree

2 files changed

+378
-0
lines changed

2 files changed

+378
-0
lines changed
Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
import useId from '@mui/utils/useId';
2+
import * as React from 'react';
3+
import { styled } from '@mui/material/styles';
4+
import { type ProcessedBarData, type ProcessedBarSeriesData } from './types';
5+
import { ANIMATION_DURATION_MS } from '../internals/animation/animation';
6+
import { useUtilityClasses } from './barClasses';
7+
import { appendAtKey } from '../internals/appendAtKey';
8+
import { type IndividualBarPlotProps } from './IndividualBarPlot';
9+
import { useChartContext } from '../context/ChartProvider/useChartContext';
10+
import { useSelector } from '../internals/store/useSelector';
11+
import { selectorChartDrawingArea } from '../internals/plugins/corePlugins/useChartDimensions';
12+
import {
13+
selectorChartIsSeriesFaded,
14+
selectorChartIsSeriesHighlighted,
15+
selectorChartSeriesHighlightedItem,
16+
selectorChartSeriesUnfadedItem,
17+
type UseChartHighlightSignature,
18+
} from '../internals/plugins/featurePlugins/useChartHighlight';
19+
import { useStore } from '../internals/store/useStore';
20+
import { useOnItemClick } from './useOnItemClick';
21+
import { useInteractionItemProps } from './useItemInteractionProps';
22+
23+
interface BatchBarPlotProps extends IndividualBarPlotProps {}
24+
25+
const MAX_POINTS_PER_PATH = 1000;
26+
27+
function generateBarPath(
28+
x: number,
29+
y: number,
30+
width: number,
31+
height: number,
32+
topLeftBorderRadius: number,
33+
topRightBorderRadius: number,
34+
bottomRightBorderRadius: number,
35+
bottomLeftBorderRadius: number,
36+
) {
37+
const tLBR = Math.min(topLeftBorderRadius, width / 2, height / 2);
38+
const tRBR = Math.min(topRightBorderRadius, width / 2, height / 2);
39+
const bRBR = Math.min(bottomRightBorderRadius, width / 2, height / 2);
40+
const bLBR = Math.min(bottomLeftBorderRadius, width / 2, height / 2);
41+
42+
return `M${x + tLBR},${y}
43+
h${width - tLBR - tRBR}
44+
a${tRBR},${tRBR} 0 0 1 ${tRBR},${tRBR}
45+
v${height - tRBR - bRBR}
46+
a${bRBR},${bRBR} 0 0 1 -${bRBR},${bRBR}
47+
h-${width - bRBR - bLBR}
48+
a${bLBR},${bLBR} 0 0 1 -${bLBR},-${bLBR}
49+
v-${height - bLBR - tLBR}
50+
a${tLBR},${tLBR} 0 0 1 ${tLBR},-${tLBR}
51+
Z`;
52+
}
53+
54+
function createPath(barData: ProcessedBarData, borderRadius: number) {
55+
return generateBarPath(
56+
barData.x,
57+
barData.y,
58+
barData.width,
59+
barData.height,
60+
barData.borderRadiusSide === 'left' || barData.borderRadiusSide === 'top' ? borderRadius : 0,
61+
barData.borderRadiusSide === 'right' || barData.borderRadiusSide === 'top' ? borderRadius : 0,
62+
barData.borderRadiusSide === 'right' || barData.borderRadiusSide === 'bottom'
63+
? borderRadius
64+
: 0,
65+
barData.borderRadiusSide === 'left' || barData.borderRadiusSide === 'bottom' ? borderRadius : 0,
66+
);
67+
}
68+
69+
function useCreatePaths(seriesData: ProcessedBarSeriesData, borderRadius: number) {
70+
const paths = new Map<string, string[]>();
71+
const temporaryPaths = new Map<string, string[]>();
72+
73+
for (let j = 0; j < seriesData.data.length; j += 1) {
74+
const barData = seriesData.data[j];
75+
76+
const pathString = createPath(barData, borderRadius);
77+
78+
const tempPath = appendAtKey(temporaryPaths, barData.color, pathString);
79+
80+
if (tempPath.length >= MAX_POINTS_PER_PATH) {
81+
appendAtKey(paths, barData.color, tempPath.join(''));
82+
temporaryPaths.delete(barData.color);
83+
}
84+
}
85+
86+
for (const [fill, tempPath] of temporaryPaths.entries()) {
87+
if (tempPath.length > 0) {
88+
appendAtKey(paths, fill, tempPath.join(''));
89+
}
90+
}
91+
92+
return paths;
93+
}
94+
95+
export function BatchBarPlot({
96+
completedData,
97+
borderRadius = 0,
98+
onItemClick,
99+
skipAnimation = false,
100+
}: BatchBarPlotProps) {
101+
const onClick = useOnItemClick(onItemClick);
102+
const interactionItemProps = useInteractionItemProps();
103+
104+
return (
105+
<React.Fragment>
106+
{completedData.map((series) => (
107+
<SeriesBatchPlot
108+
key={series.seriesId}
109+
series={series}
110+
borderRadius={borderRadius}
111+
skipAnimation={skipAnimation}
112+
/>
113+
))}
114+
<DrawingAreaRect onClick={onClick} {...interactionItemProps} />
115+
</React.Fragment>
116+
);
117+
}
118+
119+
const MemoFadedHighlightedBars = React.memo(FadedHighlightedBars);
120+
121+
function SeriesBatchPlot({
122+
series,
123+
borderRadius,
124+
skipAnimation,
125+
}: {
126+
series: ProcessedBarSeriesData;
127+
borderRadius: number;
128+
skipAnimation: boolean;
129+
}) {
130+
const classes = useUtilityClasses();
131+
const { store } = useChartContext<[UseChartHighlightSignature]>();
132+
const isSeriesHighlighted = useSelector(store, selectorChartIsSeriesHighlighted, series.seriesId);
133+
const isSeriesFaded = useSelector(store, selectorChartIsSeriesFaded, series.seriesId);
134+
135+
return (
136+
<React.Fragment>
137+
<BarGroup
138+
className={classes.series}
139+
data-series={series.seriesId}
140+
layout={series.layout}
141+
xOrigin={series.xOrigin}
142+
yOrigin={series.yOrigin}
143+
skipAnimation={skipAnimation}
144+
data-faded={isSeriesFaded || undefined}
145+
data-highlighted={isSeriesHighlighted || undefined}
146+
>
147+
<BatchBarSeriesPlot processedSeries={series} borderRadius={borderRadius} />
148+
</BarGroup>
149+
<MemoFadedHighlightedBars processedSeries={series} borderRadius={borderRadius} />
150+
</React.Fragment>
151+
);
152+
}
153+
154+
function DrawingAreaRect(props: React.HTMLAttributes<SVGRectElement>) {
155+
const store = useStore();
156+
const drawingArea = useSelector(store, selectorChartDrawingArea);
157+
158+
return (
159+
<rect
160+
x={drawingArea.left}
161+
y={drawingArea.top}
162+
width={drawingArea.width}
163+
height={drawingArea.height}
164+
fill="transparent"
165+
{...props}
166+
/>
167+
);
168+
}
169+
170+
function FadedHighlightedBars({
171+
processedSeries,
172+
borderRadius,
173+
}: {
174+
processedSeries: ProcessedBarSeriesData;
175+
borderRadius: number;
176+
}) {
177+
const { store } = useChartContext<[UseChartHighlightSignature]>();
178+
const seriesHighlightedItem = useSelector(
179+
store,
180+
selectorChartSeriesHighlightedItem,
181+
processedSeries.seriesId,
182+
);
183+
const seriesUnfadedItem = useSelector(
184+
store,
185+
selectorChartSeriesUnfadedItem,
186+
processedSeries.seriesId,
187+
);
188+
189+
const siblings: React.ReactNode[] = [];
190+
if (seriesHighlightedItem != null) {
191+
const barData = processedSeries.data[seriesHighlightedItem];
192+
193+
siblings.push(
194+
<path
195+
key={`highlighted-${processedSeries.seriesId}`}
196+
fill={barData.color}
197+
filter="brightness(120%)"
198+
data-highlighted
199+
d={createPath(barData, borderRadius)}
200+
/>,
201+
);
202+
}
203+
204+
if (seriesUnfadedItem != null) {
205+
const barData = processedSeries.data[seriesUnfadedItem];
206+
207+
siblings.push(
208+
<path
209+
key={`unfaded-${processedSeries.seriesId}`}
210+
fill={barData.color}
211+
d={createPath(barData, borderRadius)}
212+
/>,
213+
);
214+
}
215+
216+
return <React.Fragment>{siblings}</React.Fragment>;
217+
}
218+
219+
function BatchBarSeriesPlot({
220+
processedSeries,
221+
borderRadius,
222+
}: {
223+
processedSeries: ProcessedBarSeriesData;
224+
borderRadius: number;
225+
}) {
226+
const paths = useCreatePaths(processedSeries, borderRadius);
227+
const children: React.ReactNode[] = [];
228+
229+
let i = 0;
230+
for (const [fill, dArray] of paths.entries()) {
231+
for (const d of dArray) {
232+
children.push(<path key={i} fill={fill} d={d} />);
233+
i += 1;
234+
}
235+
}
236+
237+
return <React.Fragment>{children}</React.Fragment>;
238+
}
239+
240+
const PathGroup = styled('g')({
241+
'&[data-faded="true"]': {
242+
opacity: 0.3,
243+
},
244+
'& path': {
245+
/* The browser must do hit testing to know which element a pointer is interacting with.
246+
* With many data points, we create many paths causing significant time to be spent in the hit test phase.
247+
* To fix this issue, we disable pointer events for the descendant paths.
248+
*
249+
* Ideally, users should be able to override this in case they need pointer events to be enabled,
250+
* but it can affect performance negatively, especially with many data points. */
251+
pointerEvents: 'none',
252+
},
253+
});
254+
255+
interface BarGroupProps extends React.HTMLAttributes<SVGGElement> {
256+
skipAnimation: boolean;
257+
layout: 'horizontal' | 'vertical' | undefined;
258+
xOrigin: number;
259+
yOrigin: number;
260+
}
261+
262+
function BarGroup({ skipAnimation, layout, xOrigin, yOrigin, ...props }: BarGroupProps) {
263+
if (skipAnimation) {
264+
return <PathGroup {...props} />;
265+
}
266+
267+
return <AnimatedGroup {...props} layout={layout} xOrigin={xOrigin} yOrigin={yOrigin} />;
268+
}
269+
270+
interface AnimatedGroupProps extends React.HTMLAttributes<SVGGElement> {
271+
layout: 'horizontal' | 'vertical' | undefined;
272+
xOrigin: number;
273+
yOrigin: number;
274+
}
275+
276+
function AnimatedGroup({ children, layout, xOrigin, yOrigin, ...props }: AnimatedGroupProps) {
277+
const store = useStore();
278+
const drawingArea = useSelector(store, selectorChartDrawingArea);
279+
const clipPathId = useId();
280+
281+
const animateChildren: React.ReactNode[] = [];
282+
283+
if (layout === 'horizontal') {
284+
animateChildren.push(
285+
<rect
286+
key="left"
287+
x={drawingArea.left}
288+
width={xOrigin - drawingArea.left}
289+
y={drawingArea.top}
290+
height={drawingArea.height}
291+
>
292+
<animate
293+
attributeName="x"
294+
from={xOrigin}
295+
to={drawingArea.left}
296+
dur={`${ANIMATION_DURATION_MS}ms`}
297+
fill="freeze"
298+
/>
299+
<animate
300+
attributeName="width"
301+
from={0}
302+
to={xOrigin - drawingArea.left}
303+
dur={`${ANIMATION_DURATION_MS}ms`}
304+
fill="freeze"
305+
/>
306+
</rect>,
307+
);
308+
animateChildren.push(
309+
<rect
310+
key="right"
311+
x={xOrigin}
312+
width={drawingArea.left + drawingArea.width - xOrigin}
313+
y={drawingArea.top}
314+
height={drawingArea.height}
315+
>
316+
<animate
317+
attributeName="width"
318+
from={0}
319+
to={drawingArea.left + drawingArea.width - xOrigin}
320+
dur={`${ANIMATION_DURATION_MS}ms`}
321+
fill="freeze"
322+
/>
323+
</rect>,
324+
);
325+
} else {
326+
animateChildren.push(
327+
<rect
328+
key="top"
329+
x={drawingArea.left}
330+
width={drawingArea.width}
331+
y={drawingArea.top}
332+
height={yOrigin - drawingArea.top}
333+
>
334+
<animate
335+
attributeName="y"
336+
from={yOrigin}
337+
to={drawingArea.top}
338+
dur={`${ANIMATION_DURATION_MS}ms`}
339+
fill="freeze"
340+
/>
341+
<animate
342+
attributeName="height"
343+
from={0}
344+
to={yOrigin - drawingArea.top}
345+
dur={`${ANIMATION_DURATION_MS}ms`}
346+
fill="freeze"
347+
/>
348+
</rect>,
349+
);
350+
animateChildren.push(
351+
<rect
352+
key="bottom"
353+
x={drawingArea.left}
354+
width={drawingArea.width}
355+
y={yOrigin}
356+
height={drawingArea.top + drawingArea.height - yOrigin}
357+
>
358+
<animate
359+
attributeName="height"
360+
from={0}
361+
to={drawingArea.top + drawingArea.height - yOrigin}
362+
dur={`${ANIMATION_DURATION_MS}ms`}
363+
fill="freeze"
364+
/>
365+
</rect>,
366+
);
367+
}
368+
369+
return (
370+
<React.Fragment>
371+
<clipPath id={clipPathId}>{animateChildren}</clipPath>
372+
<PathGroup clipPath={`url(#${clipPathId})`} {...props}>
373+
{children}
374+
</PathGroup>
375+
</React.Fragment>
376+
);
377+
}

packages/x-charts/src/BarChart/useBarChartProps.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ export const useBarChartProps = (props: BarChartProps) => {
150150
borderRadius,
151151
renderer,
152152
barLabel,
153+
renderer: 'svg-batch',
153154
};
154155

155156
const gridProps: ChartsGridProps = {

0 commit comments

Comments
 (0)