Skip to content

Commit 1021514

Browse files
[charts-premium] Add candlestick chart demo
1 parent ac80343 commit 1021514

File tree

3 files changed

+364
-0
lines changed

3 files changed

+364
-0
lines changed
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import Typography from '@mui/material/Typography';
2+
import { RangeBarPlot } from '@mui/x-charts-premium/BarChartPremium';
3+
import { ChartsXAxis } from '@mui/x-charts/ChartsXAxis';
4+
import { ChartsYAxis } from '@mui/x-charts/ChartsYAxis';
5+
import { ChartContainerPremium } from '@mui/x-charts-premium/ChartContainerPremium';
6+
import { ChartsClipPath } from '@mui/x-charts/ChartsClipPath';
7+
import * as React from 'react';
8+
import {
9+
useDataset,
10+
useDrawingArea,
11+
useXScale,
12+
useYScale,
13+
} from '@mui/x-charts/hooks';
14+
import { useTheme, styled } from '@mui/system';
15+
import { ChartsTooltipContainer, useAxesTooltip } from '@mui/x-charts/ChartsTooltip';
16+
import { ChartsAxisHighlight } from '@mui/x-charts/ChartsAxisHighlight';
17+
18+
import { niceDomain } from '@mui/x-charts/utils';
19+
import originalDataset from '../dataset/sp500-intraday.json'; // Source: Yahoo Finance
20+
21+
const dataset = originalDataset.map((datum) => ({
22+
...datum,
23+
date: new Date(datum.date),
24+
}));
25+
26+
const tickLabelDateFormatter = new Intl.DateTimeFormat(undefined, {
27+
month: 'short',
28+
day: 'numeric',
29+
});
30+
const tooltipDateFormatter = new Intl.DateTimeFormat(undefined, {
31+
month: 'long',
32+
day: 'numeric',
33+
year: 'numeric',
34+
});
35+
const tickLabelDollarFormatter = new Intl.NumberFormat(undefined, {
36+
style: 'currency',
37+
currency: 'USD',
38+
maximumFractionDigits: 0,
39+
});
40+
const tooltipDollarFormatter = new Intl.NumberFormat(undefined, {
41+
style: 'currency',
42+
currency: 'USD',
43+
minimumFractionDigits: 2,
44+
maximumFractionDigits: 2,
45+
});
46+
47+
const min = dataset.reduce((acc, cur) => Math.min(acc, cur.low), Infinity);
48+
const max = dataset.reduce((acc, cur) => Math.max(acc, cur.high), -Infinity);
49+
50+
const xAxis = [
51+
{
52+
id: 'x',
53+
scaleType: 'band',
54+
dataKey: 'date',
55+
zoom: { minSpan: 10, filterMode: 'discard' },
56+
valueFormatter: (date, context) => {
57+
const formatter =
58+
context.location === 'tick' ? tickLabelDateFormatter : tooltipDateFormatter;
59+
60+
return formatter.format(Date.parse(date));
61+
},
62+
ordinalTimeTicks: ['years', 'quarterly', 'months', 'biweekly', 'weeks', 'days'],
63+
},
64+
];
65+
66+
const yAxis = [
67+
{
68+
id: 'y',
69+
scaleType: 'linear',
70+
domainLimit: () => {
71+
const domain = niceDomain('linear', [min, max]);
72+
73+
return { min: domain[0].valueOf(), max: domain[1].valueOf() };
74+
},
75+
valueFormatter: (value) => tickLabelDollarFormatter.format(value),
76+
},
77+
];
78+
79+
const series = [
80+
{
81+
type: 'rangeBar',
82+
datasetKeys: { start: 'open', end: 'close' },
83+
xAxisId: 'x',
84+
yAxisId: 'y',
85+
colorGetter: (data) => {
86+
const value = dataset[data.dataIndex];
87+
88+
return value.close > value.open ? 'green' : 'red';
89+
},
90+
},
91+
];
92+
93+
export default function RangeBarCandlestick() {
94+
const clipPathId = React.useId();
95+
96+
return (
97+
<ChartContainerPremium
98+
dataset={dataset}
99+
xAxis={xAxis}
100+
yAxis={yAxis}
101+
series={series}
102+
height={300}
103+
>
104+
<ChartsXAxis />
105+
<ChartsYAxis />
106+
<ChartsClipPath id={clipPathId} />
107+
<g clipPath={`url(#${clipPathId})`}>
108+
<HighLowPlot />
109+
<RangeBarPlot skipAnimation />
110+
<ChartsAxisHighlight x="band" />
111+
</g>
112+
<ChartsTooltipContainer>
113+
<TooltipContent />
114+
</ChartsTooltipContainer>
115+
</ChartContainerPremium>
116+
);
117+
}
118+
119+
function HighLowPlot() {
120+
const chartDataset = useDataset();
121+
const xScale = useXScale('x');
122+
const yScale = useYScale('y');
123+
const drawingArea = useDrawingArea();
124+
const theme = useTheme();
125+
126+
return (
127+
<g>
128+
{chartDataset.map((d, index) => {
129+
const x = xScale(d.date) + xScale.bandwidth() / 2;
130+
const high = yScale(d.high);
131+
const low = yScale(d.low);
132+
133+
if (x < drawingArea.left || x > drawingArea.left + drawingArea.width) {
134+
return null;
135+
}
136+
137+
return (
138+
<line
139+
key={index}
140+
x1={x}
141+
y1={high}
142+
x2={x}
143+
y2={low}
144+
stroke={theme.palette.mode === 'dark' ? 'white' : 'black'}
145+
strokeWidth={0.5}
146+
/>
147+
);
148+
})}
149+
</g>
150+
);
151+
}
152+
153+
const TooltipContainer = styled('div')(({ theme }) => ({
154+
background: theme.palette.background.paper,
155+
border: `1px solid ${theme.palette.divider}`,
156+
padding: theme.spacing(1),
157+
borderRadius: theme.shape.borderRadius,
158+
}));
159+
160+
function TooltipContent() {
161+
const chartDataset = useDataset();
162+
const tooltipData = useAxesTooltip();
163+
const dataIndex = tooltipData?.[0]?.dataIndex;
164+
165+
if (dataIndex === undefined) {
166+
return null;
167+
}
168+
169+
const { date, open, close, high, low } = chartDataset[dataIndex];
170+
171+
return (
172+
<TooltipContainer>
173+
<Typography fontWeight="bold">{tooltipDateFormatter.format(date)}</Typography>
174+
<Typography>Open: {tooltipDollarFormatter.format(open)}</Typography>
175+
<Typography>Close: {tooltipDollarFormatter.format(close)}</Typography>
176+
<Typography>Low: {tooltipDollarFormatter.format(low)}</Typography>
177+
<Typography>High: {tooltipDollarFormatter.format(high)}</Typography>
178+
</TooltipContainer>
179+
);
180+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import Typography from '@mui/material/Typography';
2+
import { RangeBarPlot, RangeBarSeries } from '@mui/x-charts-premium/BarChartPremium';
3+
import { ChartsXAxis } from '@mui/x-charts/ChartsXAxis';
4+
import { ChartsYAxis } from '@mui/x-charts/ChartsYAxis';
5+
import { ChartContainerPremium } from '@mui/x-charts-premium/ChartContainerPremium';
6+
import { ChartsClipPath } from '@mui/x-charts/ChartsClipPath';
7+
import * as React from 'react';
8+
import {
9+
useDataset,
10+
useDrawingArea,
11+
useXScale,
12+
useYScale,
13+
} from '@mui/x-charts/hooks';
14+
import { useTheme, styled } from '@mui/system';
15+
import { ChartsTooltipContainer, useAxesTooltip } from '@mui/x-charts/ChartsTooltip';
16+
import { ChartsAxisHighlight } from '@mui/x-charts/ChartsAxisHighlight';
17+
import { XAxis, YAxis } from '@mui/x-charts/models';
18+
import { niceDomain } from '@mui/x-charts/utils';
19+
import originalDataset from '../dataset/sp500-intraday.json'; // Source: Yahoo Finance
20+
21+
const dataset = originalDataset.map((datum) => ({
22+
...datum,
23+
date: new Date(datum.date),
24+
}));
25+
26+
const tickLabelDateFormatter = new Intl.DateTimeFormat(undefined, {
27+
month: 'short',
28+
day: 'numeric',
29+
});
30+
const tooltipDateFormatter = new Intl.DateTimeFormat(undefined, {
31+
month: 'long',
32+
day: 'numeric',
33+
year: 'numeric',
34+
});
35+
const tickLabelDollarFormatter = new Intl.NumberFormat(undefined, {
36+
style: 'currency',
37+
currency: 'USD',
38+
maximumFractionDigits: 0,
39+
});
40+
const tooltipDollarFormatter = new Intl.NumberFormat(undefined, {
41+
style: 'currency',
42+
currency: 'USD',
43+
minimumFractionDigits: 2,
44+
maximumFractionDigits: 2,
45+
});
46+
47+
const min = dataset.reduce((acc, cur) => Math.min(acc, cur.low), Infinity);
48+
const max = dataset.reduce((acc, cur) => Math.max(acc, cur.high), -Infinity);
49+
50+
const xAxis = [
51+
{
52+
id: 'x',
53+
scaleType: 'band',
54+
dataKey: 'date',
55+
zoom: { minSpan: 10, filterMode: 'discard' },
56+
valueFormatter: (date, context) => {
57+
const formatter =
58+
context.location === 'tick' ? tickLabelDateFormatter : tooltipDateFormatter;
59+
60+
return formatter.format(Date.parse(date));
61+
},
62+
ordinalTimeTicks: ['years', 'quarterly', 'months', 'biweekly', 'weeks', 'days'],
63+
},
64+
] satisfies XAxis[];
65+
const yAxis = [
66+
{
67+
id: 'y',
68+
scaleType: 'linear',
69+
domainLimit: () => {
70+
const domain = niceDomain('linear', [min, max]);
71+
72+
return { min: domain[0].valueOf(), max: domain[1].valueOf() };
73+
},
74+
valueFormatter: (value) => tickLabelDollarFormatter.format(value),
75+
},
76+
] satisfies YAxis[];
77+
const series = [
78+
{
79+
type: 'rangeBar',
80+
datasetKeys: { start: 'open', end: 'close' },
81+
xAxisId: 'x',
82+
yAxisId: 'y',
83+
colorGetter: (data) => {
84+
const value = dataset[data.dataIndex];
85+
86+
return value.close > value.open ? 'green' : 'red';
87+
},
88+
},
89+
] satisfies RangeBarSeries[];
90+
91+
export default function RangeBarCandlestick() {
92+
const clipPathId = React.useId();
93+
94+
return (
95+
<ChartContainerPremium
96+
dataset={dataset}
97+
xAxis={xAxis}
98+
yAxis={yAxis}
99+
series={series}
100+
height={300}
101+
>
102+
<ChartsXAxis />
103+
<ChartsYAxis />
104+
<ChartsClipPath id={clipPathId} />
105+
<g clipPath={`url(#${clipPathId})`}>
106+
<HighLowPlot />
107+
<RangeBarPlot skipAnimation />
108+
<ChartsAxisHighlight x="band" />
109+
</g>
110+
<ChartsTooltipContainer>
111+
<TooltipContent />
112+
</ChartsTooltipContainer>
113+
</ChartContainerPremium>
114+
);
115+
}
116+
117+
function HighLowPlot() {
118+
const chartDataset = useDataset<typeof dataset>()!;
119+
const xScale = useXScale<'band'>('x');
120+
const yScale = useYScale<'linear'>('y');
121+
const drawingArea = useDrawingArea();
122+
const theme = useTheme();
123+
124+
return (
125+
<g>
126+
{chartDataset.map((d, index) => {
127+
const x = xScale(d.date)! + xScale.bandwidth() / 2;
128+
const high = yScale(d.high);
129+
const low = yScale(d.low);
130+
131+
if (x < drawingArea.left || x > drawingArea.left + drawingArea.width) {
132+
return null;
133+
}
134+
135+
return (
136+
<line
137+
key={index}
138+
x1={x}
139+
y1={high}
140+
x2={x}
141+
y2={low}
142+
stroke={theme.palette.mode === 'dark' ? 'white' : 'black'}
143+
strokeWidth={0.5}
144+
/>
145+
);
146+
})}
147+
</g>
148+
);
149+
}
150+
151+
const TooltipContainer = styled('div')(({ theme }) => ({
152+
background: theme.palette.background.paper,
153+
border: `1px solid ${theme.palette.divider}`,
154+
padding: theme.spacing(1),
155+
borderRadius: theme.shape.borderRadius,
156+
}));
157+
158+
function TooltipContent() {
159+
const chartDataset = useDataset<typeof dataset>();
160+
const tooltipData = useAxesTooltip();
161+
const dataIndex = tooltipData?.[0]?.dataIndex;
162+
163+
if (dataIndex === undefined) {
164+
return null;
165+
}
166+
167+
const { date, open, close, high, low } = chartDataset![dataIndex];
168+
169+
return (
170+
<TooltipContainer>
171+
<Typography fontWeight="bold">{tooltipDateFormatter.format(date)}</Typography>
172+
<Typography>Open: {tooltipDollarFormatter.format(open)}</Typography>
173+
<Typography>Close: {tooltipDollarFormatter.format(close)}</Typography>
174+
<Typography>Low: {tooltipDollarFormatter.format(low)}</Typography>
175+
<Typography>High: {tooltipDollarFormatter.format(high)}</Typography>
176+
</TooltipContainer>
177+
);
178+
}

docs/data/charts/bar-demo/bar-demo.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,9 @@ components: BarChart, BarElement, BarPlot
4545
The following demo shows a waterfall chart built using a [range bar chart](/x/react-charts/range-bar/).
4646

4747
{{"demo": "WaterfallChart.js"}}
48+
49+
## Candlestick Chart [<span class="plan-premium"></span>](/x/introduction/licensing/#premium-plan 'Premium plan')
50+
51+
The following demo shows a simplified candlestick chart built using a [range bar chart](/x/react-charts/range-bar/).
52+
53+
{{"demo": "RangeBarCandlestick.js"}}

0 commit comments

Comments
 (0)