Skip to content

Instantly share code, notes, and snippets.

@goodroot
Created January 2, 2025 16:14
Show Gist options
  • Save goodroot/923a64e5ec2c1e3587a3b2ba9261a83d to your computer and use it in GitHub Desktop.
Save goodroot/923a64e5ec2c1e3587a3b2ba9261a83d to your computer and use it in GitHub Desktop.
"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