Jaspers Hub

biseroff / charts

pluginReviewed★ 0↓ 0

Charts: Interactive price chart view with lines or candles and volume, backed by Yahoo, Massive, or Databento bar data.

4 KB
1import { useState } from 'react'
2import type { FeedResult } from './Feeds'
3import { INTERVALS, type Interval } from './plan'
4import { DATABENTO_INTRADAY_TIP, SOURCES, SOURCE_INTERVALS, SOURCE_NAMES, type Source } from './sources'
5
6export const COLORS = ['#3b82f6', '#f59e0b', '#10b981', '#ef4444', '#a855f7', '#06b6d4', '#ec4899', '#84cc16', '#f97316', '#94a3b8']
7
8export interface ToolbarProps {
9 symbols: string[]
10 active: string | null
11 source: Source
12 onSource: (s: Source) => void
13 interval: Interval
14 mode: 'lines' | 'candles'
15 volume: boolean
16 feeds: Record<string, FeedResult | undefined>
17 onInterval: (i: Interval) => void
18 onMode: (m: 'lines' | 'candles') => void
19 onVolume: (v: boolean) => void
20 onAdd: (symbol: string) => void
21 onRemove: (symbol: string) => void
22 onActive: (symbol: string) => void
23}
24
25/** The controls row and the symbol chips. */
26export function Toolbar(p: ToolbarProps) {
27 const [draft, setDraft] = useState('')
28 const add = () => {
29 const symbol = draft.trim().toUpperCase()
30 if (/^[A-Z0-9.^=-]{1,20}$/.test(symbol)) p.onAdd(symbol)
31 setDraft('')
32 }
33 return (
34 <div className="yc-top">
35 <div className="yc-controls">
36 <select className="yc-select" value={p.source} onChange={(e) => p.onSource(e.target.value as Source)} title="Data source">
37 {SOURCES.map((s) => (
38 <option key={s} value={s}>
39 {SOURCE_NAMES[s]}
40 </option>
41 ))}
42 </select>
43 <select
44 className="yc-select"
45 value={p.interval}
46 onChange={(e) => p.onInterval(e.target.value as Interval)}
47 title={p.source === 'databento' ? DATABENTO_INTRADAY_TIP : 'Interval'}
48 >
49 {INTERVALS.map((i) => {
50 const off = !SOURCE_INTERVALS[p.source].includes(i)
51 return (
52 <option key={i} value={i} disabled={off} title={off ? DATABENTO_INTRADAY_TIP : undefined}>
53 {i}
54 </option>
55 )
56 })}
57 </select>
58 <span className="yc-source">{SOURCE_NAMES[p.source]}</span>
59 <div className="yc-seg">
60 {(['lines', 'candles'] as const).map((m) => (
61 <button key={m} className={p.mode === m ? 'on' : ''} onClick={() => p.onMode(m)}>
62 {m === 'lines' ? 'Lines' : 'Candles'}
63 </button>
64 ))}
65 </div>
66 <button className={`yc-btn ${p.volume ? 'on' : ''}`} onClick={() => p.onVolume(!p.volume)} title="Volume pane">
67 Vol
68 </button>
69 <input
70 className="yc-input"
71 value={draft}
72 placeholder="Add symbol"
73 onChange={(e) => setDraft(e.target.value)}
74 onKeyDown={(e) => {
75 if (e.key === 'Enter') add()
76 }}
77 />
78 <button className="yc-btn" onClick={add} title="Add symbol">
79 +
80 </button>
81 </div>
82 <div className="yc-chips">
83 {p.symbols.map((s, i) => {
84 const feed = p.feeds[s]
85 const color = COLORS[i % COLORS.length]
86 return (
87 <span
88 key={s}
89 className={`yc-chip ${s === p.active ? 'active' : ''} ${feed?.loading ? 'loading' : ''}`}
90 style={{ borderColor: s === p.active ? color : undefined }}
91 onClick={() => p.onActive(s)}
92 title={[s, feed?.note, feed?.error ?? feed?.detail].filter(Boolean).join(': ')}
93 >
94 <i style={{ background: color }} />
95 {s}
96 {feed?.error ? <b className="yc-err">!</b> : null}
97 {feed?.note ? (
98 <small className={`yc-note-chip ${feed.error || /fail/.test(feed.note) ? 'bad' : /rate/.test(feed.note) ? 'warn' : ''}`}>
99 {feed.note}
100 </small>
101 ) : null}
102 <button
103 className="yc-x"
104 onClick={(e) => {
105 e.stopPropagation()
106 p.onRemove(s)
107 }}
108 title={`Remove ${s}`}
109 >
110 ×
111 </button>
112 </span>
113 )
114 })}
115 </div>
116 </div>
117 )
118}