import { useData, usePublish, type PanelRef } from '@jaspers-ai/sdk' import './styles.css' interface Bar { time: string open: number high: number low: number close: number volume: number | null [field: string]: unknown } interface State { ticker?: string multiplier?: number timespan?: string from?: string to?: string style?: 'candles' | 'line' } const W = 800 const PRICE_H = 250 const VOL_TOP = 262 const VOL_H = 60 const H = VOL_TOP + VOL_H + 18 const RIGHT = 56 const day = (n: number) => new Date(Date.now() - n * 86_400_000).toISOString().slice(0, 10) export function PriceView({ panel }: { panel: PanelRef }) { const state = useData(`workspaces/${panel.workspaceId}/panels/${panel.id}/state`) as State | undefined if (state === undefined) return

Loading…

const args = { ticker: (state.ticker ?? 'AAPL').trim().toUpperCase(), multiplier: state.multiplier ?? 1, timespan: state.timespan ?? 'day', from: state.from || day(182), to: state.to || day(0), sort: 'asc', limit: 5000, } return } function Chart({ panel, args, style }: { panel: PanelRef; args: Record; style: string }) { const { data, error, loading, refetch } = useData('massive/aggregates', args) const bars = ((data ?? []) as Bar[]).filter((b) => typeof b.close === 'number') const lastBar = bars[bars.length - 1] usePublish(panel, { ticker: String(args.ticker), bars: bars.length, first: bars[0]?.time ?? null, last: lastBar?.time ?? null, close: lastBar?.close ?? null, error: error ?? null, }) const first = bars[0] const change = first && lastBar ? lastBar.close - first.close : null return (
{args.ticker} · {args.multiplier} {args.timespan}{' '} {lastBar ? ( {lastBar.close.toFixed(2)} ({change !== null && first ? ((change / first.close) * 100).toFixed(2) : '–'}%) ) : null}
{error ?

{error}

: null} {!data && !error ?

Loading…

: null} {data && !bars.length ?

No bars in that range.

: null} {bars.length ? : null}
) } function Svg({ bars, style }: { bars: Bar[]; style: string }) { const hi = Math.max(...bars.map((b) => b.high ?? b.close)) const lo = Math.min(...bars.map((b) => b.low ?? b.close)) const span = hi - lo || 1 const maxVol = Math.max(1, ...bars.map((b) => b.volume ?? 0)) const plotW = W - RIGHT const step = plotW / bars.length const x = (i: number) => i * step + step / 2 const y = (p: number) => 6 + ((hi - p) / span) * (PRICE_H - 12) const bodyW = Math.max(1, step * 0.7) const ticks = [0, 0.25, 0.5, 0.75, 1].map((f) => lo + f * span) const labels = [0, Math.floor(bars.length / 2), bars.length - 1] const line = bars.map((b, i) => `${i ? 'L' : 'M'}${x(i).toFixed(1)},${y(b.close).toFixed(1)}`).join(' ') return ( {ticks.map((t) => ( {t.toFixed(2)} ))} {style === 'line' ? ( ) : ( bars.map((b, i) => { const up = b.close >= b.open const color = up ? 'var(--jaspers-positive, #15803d)' : 'var(--jaspers-negative, #b91c1c)' const top = y(Math.max(b.open, b.close)) const height = Math.max(1, Math.abs(y(b.open) - y(b.close))) return ( ) }) )} {bars.map((b, i) => { const h = ((b.volume ?? 0) / maxVol) * VOL_H const up = b.close >= b.open return ( ) })} {labels.map((i) => ( {bars[i]?.time.slice(0, bars[i]!.time.endsWith('T00:00:00.000Z') ? 10 : 16).replace('T', ' ')} ))} ) }