Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 100 additions & 7 deletions dev/demo.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useState, useEffect, useRef, useCallback } from 'react'
import { createRoot } from 'react-dom/client'
import { Liveline } from 'liveline'
import type { LivelinePoint, CandlePoint } from 'liveline'
import type { LivelinePoint, CandlePoint, BarPoint } from 'liveline'

// --- Data generators ---

Expand Down Expand Up @@ -40,6 +40,42 @@ function aggregateCandles(ticks: LivelinePoint[], width: number): { candles: Can
return { candles, live: { time: slot, open: o, high: h, low: l, close: c } }
}

/** Generate initial volume bars from seed data */
function generateBars(data: LivelinePoint[], bucketSecs: number): BarPoint[] {
if (data.length === 0) return []
const bars: BarPoint[] = []
const startTime = Math.floor(data[0].time / bucketSecs) * bucketSecs
let bucketStart = startTime
let bucketVolume = 0
let prevValue = data[0].value
for (const pt of data) {
while (pt.time >= bucketStart + bucketSecs) {
if (bucketVolume > 0) bars.push({ time: bucketStart, value: bucketVolume })
bucketStart += bucketSecs
bucketVolume = 0
}
const change = Math.abs(pt.value - prevValue)
bucketVolume += 10 + change * 20 + Math.random() * 5
prevValue = pt.value
}
if (bucketVolume > 0) bars.push({ time: bucketStart, value: bucketVolume })
return bars
}

/** Incrementally update bars with a new data point — only touches the last bucket */
function addTickToBars(bars: BarPoint[], pt: LivelinePoint, prevValue: number, bucketSecs: number): BarPoint[] {
const bucketStart = Math.floor(pt.time / bucketSecs) * bucketSecs
const change = Math.abs(pt.value - prevValue)
const volume = 10 + change * 20 + Math.random() * 5
if (bars.length === 0 || bars[bars.length - 1].time < bucketStart) {
return [...bars, { time: bucketStart, value: volume }]
}
const updated = bars.slice()
const last = updated[updated.length - 1]
updated[updated.length - 1] = { time: last.time, value: last.value + volume }
return updated
}

// --- Constants ---

const TIME_WINDOWS = [
Expand Down Expand Up @@ -88,10 +124,15 @@ function Demo() {
const [windowSecs, setWindowSecs] = useState(30)
const [theme, setTheme] = useState<'dark' | 'light'>('dark')
const [grid, setGrid] = useState(true)
const [badge, setBadge] = useState(true)
const [scrub, setScrub] = useState(true)

const [volatility, setVolatility] = useState<Volatility>('normal')
const [tickRate, setTickRate] = useState(300)
const [bars, setBars] = useState<BarPoint[]>([])
const [barMode, setBarMode] = useState<'default' | 'overlay'>('default')
const [barLabels, setBarLabels] = useState(false)
const barBucketSecs = 2

const [chartType, setChartType] = useState<'line' | 'candle'>('candle')
const [candleSecs, setCandleSecs] = useState(2)
Expand Down Expand Up @@ -162,10 +203,12 @@ function Demo() {
setCandles(agg.candles)
setLiveCandle(agg.live)
liveCandleRef.current = agg.live ? { ...agg.live } : null
setBars(generateBars(seed, barBucketSecs))

intervalRef.current = window.setInterval(() => {
const now = Date.now() / 1000
const pt = generatePoint(lastValueRef.current, now, volatilityRef.current, startValueRef.current)
const prevVal = lastValueRef.current
const pt = generatePoint(prevVal, now, volatilityRef.current, startValueRef.current)
lastValueRef.current = pt.value
setValue(pt.value)
setData(prev => {
Expand All @@ -175,14 +218,15 @@ function Demo() {
return trimmed
})
tickAndAggregate(pt)
setBars(prev => addTickToBars(prev, pt, prevVal, barBucketSecs))
}, tickRate)
}, [tickRate])

useEffect(() => {
if (scenario === 'loading') {
setLoading(true)
setData([]); dataRef.current = []
setCandles([]); setLiveCandle(null); liveCandleRef.current = null
setCandles([]); setLiveCandle(null); liveCandleRef.current = null; setBars([])
clearInterval(intervalRef.current)
const timer = setTimeout(() => setScenario('live'), 3000)
return () => clearTimeout(timer)
Expand All @@ -191,15 +235,15 @@ function Demo() {
if (scenario === 'loading-hold') {
setLoading(true)
setData([]); dataRef.current = []
setCandles([]); setLiveCandle(null); liveCandleRef.current = null
setCandles([]); setLiveCandle(null); liveCandleRef.current = null; setBars([])
clearInterval(intervalRef.current)
return
}

if (scenario === 'empty') {
setLoading(false)
setData([]); dataRef.current = []
setCandles([]); setLiveCandle(null); liveCandleRef.current = null
setCandles([]); setLiveCandle(null); liveCandleRef.current = null; setBars([])
clearInterval(intervalRef.current)
return
}
Expand All @@ -213,7 +257,8 @@ function Demo() {
clearInterval(intervalRef.current)
intervalRef.current = window.setInterval(() => {
const now = Date.now() / 1000
const pt = generatePoint(lastValueRef.current, now, volatilityRef.current, startValueRef.current)
const prevVal = lastValueRef.current
const pt = generatePoint(prevVal, now, volatilityRef.current, startValueRef.current)
lastValueRef.current = pt.value
setValue(pt.value)
setData(prev => {
Expand All @@ -223,6 +268,7 @@ function Demo() {
return trimmed
})
tickAndAggregate(pt)
setBars(prev => addTickToBars(prev, pt, prevVal, barBucketSecs))
}, tickRate)
return () => clearInterval(intervalRef.current)
}, [tickRate, scenario])
Expand Down Expand Up @@ -260,7 +306,7 @@ function Demo() {
}
// Force re-seed by cycling to loading
setData([]); dataRef.current = []
setCandles([]); setLiveCandle(null); liveCandleRef.current = null
setCandles([]); setLiveCandle(null); liveCandleRef.current = null; setBars([])
lastValueRef.current = preset === 'crypto' ? 65000 : 100
clearInterval(intervalRef.current)
setLoading(true)
Expand Down Expand Up @@ -347,6 +393,7 @@ function Demo() {
<Btn active={theme === 'light'} onClick={() => setTheme('light')}>Light</Btn>
<Sep />
<Toggle on={grid} onToggle={setGrid}>Grid</Toggle>
<Toggle on={badge} onToggle={setBadge}>Badge</Toggle>
<Toggle on={scrub} onToggle={setScrub}>Scrub</Toggle>
</Section>

Expand Down Expand Up @@ -379,6 +426,7 @@ function Demo() {
formatValue={preset === 'crypto' ? formatCrypto : undefined}
onModeChange={(mode) => setChartType(mode)}
grid={grid}
badge={badge}
scrub={scrub}
/>
</div>
Expand Down Expand Up @@ -421,13 +469,58 @@ function Demo() {
window={windowSecs}
formatValue={preset === 'crypto' ? formatCrypto : undefined}
grid={grid && size.w >= 200}
badge={badge && size.w >= 200}
scrub={scrub}
/>
</div>
</div>
))}
</div>

{/* Volume bars */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 24, marginBottom: 8 }}>
<p style={{ fontSize: 12, color: 'var(--fg-30)', margin: 0 }}>Volume bars</p>
<Btn active={barMode === 'default'} onClick={() => setBarMode('default')}>Default</Btn>
<Btn active={barMode === 'overlay'} onClick={() => setBarMode('overlay')}>Overlay</Btn>
<Sep />
<Toggle on={barLabels} onToggle={setBarLabels}>Labels</Toggle>
</div>
<div style={{
height: 320,
background: 'var(--fg-02)',
borderRadius: 12,
border: '1px solid var(--fg-06)',
padding: 8,
overflow: 'hidden',
}}>
<Liveline
mode="candle"
data={data}
value={value}
candles={candles}
candleWidth={candleSecs}
liveCandle={liveCandle ?? undefined}
lineMode={chartType === 'line'}
lineData={data}
lineValue={value}
loading={loading}
paused={paused}
theme={theme}
color={preset === 'crypto' ? '#f7931a' : undefined}
window={windowSecs}
windows={preset === 'crypto' ? CRYPTO_WINDOWS : undefined}
formatValue={preset === 'crypto' ? formatCrypto : undefined}
onModeChange={(mode) => setChartType(mode)}
grid={grid}
badge={badge}
scrub={scrub}
bars={bars}
barMode={barMode}
barWidth={barBucketSecs}
barLabels={barLabels}
/>
</div>

{/* Status bar */}
<div style={{
marginTop: 10,
Expand Down
96 changes: 92 additions & 4 deletions dev/main.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useState, useEffect, useRef, useCallback } from 'react'
import { createRoot } from 'react-dom/client'
import { Liveline } from 'liveline'
import type { LivelinePoint, LivelineSeries } from 'liveline'
import type { LivelinePoint, LivelineSeries, BarPoint} from 'liveline'

// --- Data generators ---

Expand All @@ -19,6 +19,42 @@ function generatePoint(prev: number, time: number, volatility: Volatility): Live
return { time, value: prev + delta }
}

/** Generate initial volume bars from seed data */
function generateBars(data: LivelinePoint[], bucketSecs: number): BarPoint[] {
if (data.length === 0) return []
const bars: BarPoint[] = []
const startTime = Math.floor(data[0].time / bucketSecs) * bucketSecs
let bucketStart = startTime
let bucketVolume = 0
let prevValue = data[0].value
for (const pt of data) {
while (pt.time >= bucketStart + bucketSecs) {
if (bucketVolume > 0) bars.push({ time: bucketStart, value: bucketVolume })
bucketStart += bucketSecs
bucketVolume = 0
}
const change = Math.abs(pt.value - prevValue)
bucketVolume += 10 + change * 20 + Math.random() * 5
prevValue = pt.value
}
if (bucketVolume > 0) bars.push({ time: bucketStart, value: bucketVolume })
return bars
}

/** Incrementally update bars with a new data point — only touches the last bucket */
function addTickToBars(bars: BarPoint[], pt: LivelinePoint, prevValue: number, bucketSecs: number): BarPoint[] {
const bucketStart = Math.floor(pt.time / bucketSecs) * bucketSecs
const change = Math.abs(pt.value - prevValue)
const volume = 10 + change * 20 + Math.random() * 5
if (bars.length === 0 || bars[bars.length - 1].time < bucketStart) {
return [...bars, { time: bucketStart, value: volume }]
}
const updated = bars.slice()
const last = updated[updated.length - 1]
updated[updated.length - 1] = { time: last.time, value: last.value + volume }
return updated
}

// --- Constants ---

const TIME_WINDOWS = [
Expand Down Expand Up @@ -60,6 +96,10 @@ function Demo() {
const [scrub, setScrub] = useState(true)
const [exaggerate, setExaggerate] = useState(false)
const [theme, setTheme] = useState<'dark' | 'light'>('dark')
const [bars, setBars] = useState<BarPoint[]>([])
const [barMode, setBarMode] = useState<'default' | 'overlay'>('default')
const [barLabels, setBarLabels] = useState(false)
const barBucketSecs = 2
const [windowStyle, setWindowStyle] = useState<'default' | 'rounded' | 'text'>('default')
const [lineMode, setLineMode] = useState(true)

Expand All @@ -70,6 +110,7 @@ function Demo() {
const intervalRef = useRef<number>(0)
const volatilityRef = useRef(volatility)
volatilityRef.current = volatility
const lastValueRef = useRef(100)

const startLive = useCallback(() => {
clearInterval(intervalRef.current)
Expand All @@ -85,13 +126,18 @@ function Demo() {
}
setData(seed)
setValue(v)
lastValueRef.current = v
setBars(generateBars(seed, barBucketSecs))

intervalRef.current = window.setInterval(() => {
setData(prev => {
const now = Date.now() / 1000
const lastVal = prev.length > 0 ? prev[prev.length - 1].value : 100
const prevVal = lastValueRef.current
const pt = generatePoint(lastVal, now, volatilityRef.current)
lastValueRef.current = pt.value
setValue(pt.value)
setBars(b => addTickToBars(b, pt, prevVal, barBucketSecs))
const next = [...prev, pt]
return next.length > 500 ? next.slice(-500) : next
})
Expand All @@ -101,22 +147,22 @@ function Demo() {
useEffect(() => {
if (scenario === 'loading') {
setLoading(true)
setData([])
setData([]); setBars([])
clearInterval(intervalRef.current)
const timer = setTimeout(() => setScenario('live'), 3000)
return () => clearTimeout(timer)
}

if (scenario === 'loading-hold') {
setLoading(true)
setData([])
setData([]); setBars([])
clearInterval(intervalRef.current)
return
}

if (scenario === 'empty') {
setLoading(false)
setData([])
setData([]); setBars([])
clearInterval(intervalRef.current)
return
}
Expand All @@ -134,8 +180,11 @@ function Demo() {
setData(prev => {
const now = Date.now() / 1000
const lastVal = prev.length > 0 ? prev[prev.length - 1].value : 100
const prevVal = lastValueRef.current
const pt = generatePoint(lastVal, now, volatilityRef.current)
lastValueRef.current = pt.value
setValue(pt.value)
setBars(b => addTickToBars(b, pt, prevVal, barBucketSecs))
const next = [...prev, pt]
return next.length > 500 ? next.slice(-500) : next
})
Expand Down Expand Up @@ -325,6 +374,45 @@ function Demo() {
))}
</div>

{/* Volume bars */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 24, marginBottom: 8 }}>
<p style={{ fontSize: 12, color: 'var(--fg-30)', margin: 0 }}>Volume bars</p>
<Btn active={barMode === 'default'} onClick={() => setBarMode('default')}>Default</Btn>
<Btn active={barMode === 'overlay'} onClick={() => setBarMode('overlay')}>Overlay</Btn>
<Sep />
<Toggle on={barLabels} onToggle={setBarLabels}>Labels</Toggle>
</div>
<div style={{
height: 320,
background: 'var(--fg-02)',
borderRadius: 12,
border: '1px solid var(--fg-06)',
padding: 8,
overflow: 'hidden',
}}>
<Liveline
data={data}
value={value}
theme={theme}
window={windowSecs}
loading={loading}
paused={paused}
grid={grid}
scrub={scrub}
fill={fill}
badge={badge}
badgeVariant={badgeVariant}
momentum={momentum}
pulse={pulse}
windows={TIME_WINDOWS}
onWindowChange={setWindowSecs}
bars={bars}
barMode={barMode}
barWidth={barBucketSecs}
barLabels={barLabels}
/>
</div>

{/* Status bar */}
<div style={{
marginTop: 10,
Expand Down
Loading