Created
January 2, 2025 16:14
-
-
Save goodroot/923a64e5ec2c1e3587a3b2ba9261a83d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use client" | |
import React, { useEffect, useRef } from 'react' | |
import * as echarts from 'echarts' | |
import type { EChartsOption } from 'echarts' | |
interface QuestDBResponse { | |
query: string | |
columns: Array<{ | |
name: string | |
type: string | |
}> | |
dataset: Array<[string, string, number, number, number, number]> | |
count: number | |
} | |
export const EChartsDemo: React.FC = () => { | |
const chartRef = useRef<HTMLDivElement>(null) | |
const chartInstanceRef = useRef<echarts.ECharts | null>(null) | |
const fetchAndUpdateData = async () => { | |
try { | |
const response = await fetch( | |
'https://demo.questdb.io/exec?' + | |
new URLSearchParams({ | |
// Our QuestDB query! Note that the timestamp values | |
// will coincide with our axis values and time-frames | |
// which inform the creation of our candlesticks. | |
query: ` | |
WITH intervals AS ( | |
SELECT | |
timestamp_floor('5s', timestamp) AS interval_start, | |
symbol, | |
first(price) as open_price, | |
max(price) as high_price, | |
min(price) as low_price, | |
last(price) as close_price | |
FROM trades | |
WHERE symbol = 'BTC-USD' | |
AND timestamp > dateadd('m', -45, now()) | |
GROUP BY timestamp_floor('5s', timestamp), symbol | |
) | |
SELECT * FROM intervals | |
ORDER BY interval_start ASC; | |
` | |
}) | |
) | |
const responseData: QuestDBResponse = await response.json() | |
if (!responseData?.dataset?.length || !chartInstanceRef.current) { | |
return | |
} | |
// Transform data for candlestick [time, open, close, low, high] | |
const candlestickData = responseData.dataset.map(row => { | |
const [intervalStart, _symbol, open, high, low, close] = row | |
return [ | |
new Date(intervalStart).getTime(), | |
open, | |
close, | |
low, | |
high | |
] | |
}) | |
chartInstanceRef.current.setOption({ | |
series: [ | |
{ | |
data: candlestickData | |
} | |
] | |
}) | |
} catch (error) { | |
console.error('Error fetching data: is QuestDB running?', error) | |
} | |
} | |
useEffect(() => { | |
if (!chartRef.current) return | |
const chart = echarts.init(chartRef.current) | |
chartInstanceRef.current = chart | |
// Set up initial chart options | |
// This is a fairly generic config. | |
// It keeps the chart smooth and responsive. | |
// While not being _too_ data heavy on our poor demo. | |
const option: EChartsOption = { | |
backgroundColor: 'transparent', | |
animation: true, | |
animationDuration: 300, | |
animationEasing: 'linear', | |
tooltip: { | |
trigger: 'axis', | |
axisPointer: { | |
type: 'cross', | |
crossStyle: { | |
color: '#cccccc' | |
} | |
}, | |
formatter: (params: any) => { | |
const date = new Date(params[0].axisValue) | |
const timeStr = date.toLocaleTimeString([], { | |
hour: '2-digit', | |
minute: '2-digit', | |
second: '2-digit', | |
hour12: false, | |
fractionalSecondDigits: 3 | |
}) | |
const [open, high, low, close] = params[0].data.slice(1) | |
return ` | |
Time: ${timeStr}<br/> | |
Open: $${open.toLocaleString()}<br/> | |
High: $${high.toLocaleString()}<br/> | |
Low: $${low.toLocaleString()}<br/> | |
Close: $${close.toLocaleString()} | |
` | |
}, | |
backgroundColor: 'rgba(0, 0, 0, 0.8)', | |
borderColor: '#333', | |
textStyle: { | |
color: '#fff' | |
} | |
}, | |
grid: { | |
left: '5%', | |
right: '5%', | |
top: '5%', | |
bottom: '15%', | |
containLabel: true | |
}, | |
// The default view of the slider. | |
// We wanted it close, so the candles dance. | |
dataZoom: [ | |
{ | |
type: 'slider', | |
show: true, | |
xAxisIndex: [0], | |
start: 95, | |
end: 100, | |
top: '90%', | |
borderColor: '#333333', | |
textStyle: { | |
color: '#e1e1e1' | |
}, | |
backgroundColor: 'rgba(47, 69, 84, 0.3)', | |
fillerColor: 'rgba(167, 183, 204, 0.2)', | |
handleStyle: { | |
color: '#cccccc' | |
} | |
}, | |
{ | |
type: 'inside', | |
xAxisIndex: [0], | |
start: 95, | |
end: 100 | |
} | |
], | |
xAxis: [{ | |
type: 'time', | |
splitLine: { show: false }, | |
axisLabel: { | |
color: '#e1e1e1', | |
formatter: (value: number) => { | |
return new Date(value).toLocaleTimeString([], { | |
hour: '2-digit', | |
minute: '2-digit', | |
second: '2-digit', | |
hour12: false | |
}) | |
} | |
}, | |
axisLine: { | |
onZero: false, | |
lineStyle: { color: '#333' } | |
}, | |
min: 'dataMin', | |
max: 'dataMax' | |
}], | |
yAxis: [{ | |
type: 'value', | |
scale: true, | |
splitLine: { | |
show: true, | |
lineStyle: { | |
color: '#333', | |
type: 'dashed' | |
} | |
}, | |
axisLabel: { | |
color: '#e1e1e1', | |
formatter: (value: number) => { | |
return `$${value.toLocaleString()}` | |
} | |
}, | |
axisLine: { | |
lineStyle: { color: '#333' } | |
} | |
}], | |
// We're using BTC-USD as our demo symbol. | |
// You can change it to whatever you want. | |
// The colours are typical - green good, red bad. | |
// ... Unless you're shorting! | |
series: [ | |
{ | |
name: 'BTC-USD', | |
type: 'candlestick', | |
data: [], | |
itemStyle: { | |
color0: '#ef5350', // Bearish candle color (close < open) | |
color: '#00b07c', // Bullish candle color (close >= open) | |
borderColor0: '#ef5350', | |
borderColor: '#00b07c' | |
} | |
} | |
] | |
} | |
chart.setOption(option) | |
// On window resize, re-size the chart | |
let resizeTimeout: NodeJS.Timeout | |
const handleResize = () => { | |
if (resizeTimeout) clearTimeout(resizeTimeout) | |
resizeTimeout = setTimeout(() => { | |
chart.resize() | |
}, 100) | |
} | |
window.addEventListener('resize', handleResize) | |
// Initial data fetch | |
fetchAndUpdateData() | |
// Re-fetch data periodically (500 ms) | |
const intervalId = setInterval(fetchAndUpdateData, 500) | |
return () => { | |
clearInterval(intervalId) | |
window.removeEventListener('resize', handleResize) | |
if (resizeTimeout) clearTimeout(resizeTimeout) | |
chart.dispose() | |
} | |
}, []) | |
return ( | |
<div | |
ref={chartRef} | |
className="w-full" | |
style={{ | |
height: 'min(400px, 60vw)', | |
minHeight: '250px', | |
willChange: 'transform' // Tells the browser to optimize GPU usage | |
}} | |
/> | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment