Charts

OhlcChart

OHLC bars with an open tick on the left and a close tick on the right, a volume pane, moving averages and the details on hover.

#chart#data#trading#no-js

Demo

Preview
Northwind, daily
Northwind, daily
TimeOpenHighLowCloseVolume
May 1, 2026$73.98$74.21$73.85$73.882,098,090
May 4, 2026$73.84$74.27$73.49$73.921,309,442
May 5, 2026$73.80$74.49$73.52$74.391,909,759
May 6, 2026$74.54$74.60$74.11$74.271,990,407
May 7, 2026$74.31$74.85$74.14$74.491,380,980
May 8, 2026$74.62$74.76$72.96$73.382,311,354
May 11, 2026$73.17$73.82$72.53$72.911,126,757
May 12, 2026$72.96$74.41$72.30$74.171,373,059
May 13, 2026$74.27$75.82$74.06$75.111,430,423
May 14, 2026$75.24$77.02$74.49$76.462,418,508
May 15, 2026$76.43$76.90$75.79$75.812,404,109
May 18, 2026$75.96$76.62$75.61$75.611,923,788
May 19, 2026$75.81$76.50$75.42$75.982,112,181
May 20, 2026$75.95$77.90$75.63$77.132,414,660
May 21, 2026$77.13$77.17$76.39$76.432,261,790
May 22, 2026$76.25$76.67$75.60$75.891,665,013
May 25, 2026$75.71$75.84$75.00$75.431,743,325
May 26, 2026$75.62$76.36$73.86$74.602,309,487
May 27, 2026$74.63$75.27$73.85$74.331,686,572
May 28, 2026$74.24$74.55$73.53$74.361,712,252
May 29, 2026$74.54$74.66$72.95$73.662,151,497
Jun 1, 2026$73.57$74.72$72.92$74.451,875,387
Jun 2, 2026$74.45$74.52$73.20$73.792,318,367
Jun 3, 2026$73.68$74.63$73.05$74.381,743,359
Jun 4, 2026$74.16$74.18$72.54$72.951,739,647
Jun 5, 2026$73.08$73.55$71.93$72.651,724,329
Jun 8, 2026$72.76$73.24$71.60$71.961,526,215
Jun 9, 2026$71.86$72.34$71.53$71.891,318,070
Jun 10, 2026$71.89$72.03$71.44$71.952,024,615
Jun 11, 2026$71.95$72.29$71.39$72.141,475,741
Jun 12, 2026$71.97$72.47$71.83$72.191,673,392
Jun 15, 2026$72.37$72.49$71.54$72.142,111,443
Jun 16, 2026$72.24$72.44$71.45$71.621,553,099
Jun 17, 2026$71.80$72.73$71.50$72.321,788,692
Jun 18, 2026$72.30$72.96$71.73$72.952,198,033
Jun 19, 2026$72.74$73.86$72.29$73.501,193,414
Jun 22, 2026$73.65$75.58$73.47$74.911,588,858
Jun 23, 2026$74.85$75.60$74.27$74.831,540,983
Jun 24, 2026$74.69$75.35$73.95$74.901,400,190
Jun 25, 2026$74.71$75.43$74.04$74.661,014,826
Jun 26, 2026$74.70$75.98$74.33$75.331,619,065
Jun 29, 2026$75.19$76.81$75.09$76.312,050,504
Jun 30, 2026$76.24$77.30$76.22$76.661,807,307
Jul 1, 2026$76.83$78.30$76.71$77.701,489,262
Jul 2, 2026$77.81$78.68$77.25$78.602,318,451
Jul 3, 2026$78.54$78.93$78.00$78.151,146,085
Jul 6, 2026$78.16$79.77$77.74$78.992,474,379
Jul 7, 2026$79.17$79.89$78.48$78.851,901,825
Jul 8, 2026$78.94$80.02$78.25$79.911,254,368
Jul 9, 2026$79.83$81.80$79.08$81.242,429,691
Jul 10, 2026$81.00$81.41$79.65$79.652,039,787
Jul 13, 2026$79.70$80.15$79.49$79.761,191,161
Jul 14, 2026$79.90$79.93$78.16$78.871,793,532
Jul 15, 2026$79.07$79.35$78.00$78.701,400,369
Jul 16, 2026$78.69$78.92$77.77$78.521,787,039
Jul 17, 2026$78.34$79.00$77.83$78.511,098,606
Jul 20, 2026$78.34$78.43$76.72$77.352,165,411
Jul 21, 2026$77.13$77.46$76.66$77.172,190,676
Jul 22, 2026$76.99$78.33$76.35$77.661,903,914
Jul 23, 2026$77.48$78.24$76.78$77.291,968,542
Jul 24, 2026$77.36$77.64$76.07$76.811,930,104
Jul 27, 2026$76.85$77.27$76.31$76.881,304,479
Jul 28, 2026$76.83$77.71$76.47$77.642,030,200
Jul 29, 2026$77.75$78.60$77.49$77.991,607,696
Jul 30, 2026$78.20$78.30$76.85$77.221,898,047
Jul 31, 2026$77.37$77.73$76.65$77.191,960,905
Aug 3, 2026$77.35$77.60$75.53$76.231,608,009
Aug 4, 2026$76.12$76.78$74.97$75.022,360,919
Aug 5, 2026$74.97$75.46$74.84$74.852,196,619
Aug 6, 2026$74.80$75.06$73.31$73.711,380,897
Aug 7, 2026$73.79$74.10$72.77$73.252,247,152
Aug 10, 2026$73.32$73.89$72.33$73.032,020,487
Aug 11, 2026$72.86$72.87$71.99$72.041,213,999
Aug 12, 2026$71.87$73.49$71.36$73.001,707,551
Aug 13, 2026$73.06$73.78$71.68$71.852,384,288
Aug 14, 2026$71.97$73.31$71.68$72.731,548,014
Aug 17, 2026$72.91$73.39$72.70$73.342,251,245
Aug 18, 2026$73.14$73.78$72.48$73.062,193,555
Aug 19, 2026$73.11$73.70$72.37$72.421,738,911
Aug 20, 2026$72.51$73.87$72.30$73.151,643,326
Aug 21, 2026$73.04$74.27$72.45$74.092,537,264
Aug 24, 2026$73.90$75.24$73.50$75.171,920,014
Aug 25, 2026$74.99$75.54$73.69$74.091,801,218
Aug 26, 2026$74.14$75.47$73.43$74.901,537,552
Aug 27, 2026$74.93$74.95$73.79$74.332,346,572
Aug 28, 2026$74.40$74.53$73.04$73.521,699,328
Aug 31, 2026$73.52$74.40$72.89$73.932,355,110
Sep 1, 2026$73.72$73.96$73.44$73.551,483,590
Sep 2, 2026$73.48$73.83$71.91$72.561,766,529
Sep 3, 2026$72.49$73.10$71.86$73.052,128,344
EUR/USD, hourly
EUR/USD, hourly
TimeOpenHighLowClose
Oct 12, 10:001.08091.09151.07771.0831
Oct 12, 11:001.08051.08481.07301.0773
Oct 12, 12:001.08001.08321.07811.0815
Oct 12, 13:001.08251.08291.07091.0808
Oct 12, 14:001.07811.08831.07041.0774
Oct 12, 15:001.07831.08121.07711.0780
Oct 12, 16:001.07831.08591.07571.0825
Oct 13, 10:001.08341.09191.06891.0792
Oct 13, 11:001.07751.08961.06921.0830
Oct 13, 12:001.08211.08671.07401.0779
Oct 13, 13:001.08011.08211.07491.0789
Oct 13, 14:001.08111.08641.07451.0804
Oct 13, 15:001.08061.08741.07421.0811
Oct 13, 16:001.08091.08581.07091.0789
Oct 14, 10:001.07891.08871.07281.0845
Oct 14, 11:001.08331.08891.07461.0862
Oct 14, 12:001.08381.08651.07901.0856
Oct 14, 13:001.08331.09261.07251.0858
Oct 14, 14:001.08361.09241.07751.0842
Oct 14, 15:001.08241.08461.07321.0769
Oct 14, 16:001.07811.08281.06661.0762
Oct 15, 10:001.07851.08031.06521.0724
Oct 15, 11:001.07421.08581.06931.0775
Oct 15, 12:001.07561.08041.06941.0797
Oct 15, 13:001.07941.07961.07711.0779
Oct 15, 14:001.07821.08301.06841.0808
Oct 15, 15:001.08041.08851.07311.0775
Oct 15, 16:001.08031.08851.07021.0805
Oct 16, 10:001.08341.09571.07541.0872
Oct 16, 11:001.08421.09611.08141.0871
Oct 16, 12:001.08591.09701.07511.0901
Oct 16, 13:001.08781.09251.08661.0897
Oct 16, 14:001.08801.09831.08651.0867
Oct 16, 15:001.08681.09451.07421.0822
Oct 16, 16:001.08321.08821.07371.0825
Oct 19, 10:001.08461.09191.07181.0817
Oct 19, 11:001.08331.08381.07331.0804
Oct 19, 12:001.07771.08361.07391.0766
Oct 19, 13:001.07571.07631.07481.0763
Oct 19, 14:001.07511.08041.07301.0786
Oct 19, 15:001.07821.08861.06991.0785
Oct 19, 16:001.07821.08701.07261.0808

Hover a bar for its open (the tick on the left), high, low and close (the tick on the right).

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/ohlc_chart.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/ohlc_chart.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/ohlc_chart.qwe
// Prices from a seeded random walk (Park–Miller), the same on every render: `count`// candles from the day `from` on, skipping weekends, daily or hourly (10:00 to 16:00),// rounded to `places` decimals.fn ohlc_chart_demo_walk(seed: int, count: int, start: num, from: str, hourly: bool = false, places: int = 2) -> list {    let s = seed    let rnd = () => {        s = s * 16807 % 2147483647        s / 2147483647    }    let scale = pow(10, places)    let cut = (v) => round(v * scale) / scale    let p = start    let day = from    let hour = 10    let out = []    while len(out) < count {        if date.weekday(day) <= 5 {            let open = cut(p * (1 + (rnd() - 0.5) * 0.006))            let close = cut(open * (1 + (rnd() - 0.485) * (hourly ? 0.012 : 0.035)))            let high = cut(max([open, close]) * (1 + rnd() * 0.01))            let low = cut(min([open, close]) * (1 - rnd() * 0.01))            let volume = round((hourly ? 90000 : 900000) * (1 + rnd() * 1.5 + abs(close - open) * 30 / open))            out = out | push({ time: hourly ? `${day}T${hour < 10 ? "0" : ""}${hour}:00:00Z` : day, open, high, low, close, volume })            p = close        }        if hourly && hour < 16 && date.weekday(day) <= 5 {            hour += 1        } else {            hour = 10            day = date.add_days(day, 1)        }    }    out}let ohlc_chart_demo_daily = ohlc_chart_demo_walk(11, 90, 74.2, "2026-05-01")let ohlc_chart_demo_fx = ohlc_chart_demo_walk(5, 42, 1.0842, "2026-10-12", true, 4) | map((c) => { ...c, volume: null })view OhlcChartDemo() =    <div.demo-stack>        <div id="ohlc_chart-demo-daily">            <OhlcChart label="Northwind, daily" show_label={true} candles={ohlc_chart_demo_daily} sma={[10]} prefix="$"/>        </div>        <div id="ohlc_chart-demo-fx">            <OhlcChart label="EUR/USD, hourly" show_label={true} candles={ohlc_chart_demo_fx} volume={false}                       decimals={4} height={200}/>        </div>        <p.demo-note>Hover a bar for its open (the tick on the left), high, low and close (the tick on the right).</p>    </div>

Source

ui/ohlc_chart.qwe
// OhlcChart: a price chart of OHLC bars: a line from each period's low to its high,// a tick to the left at the open and one to the right at the close, green where the// close is up on the open and red where it is down. A volume pane, a date axis,// moving averages and the last close's line as CandlestickChart has. CSS only:// hovering a bar shows its date, open, high, low, close, change and volume, and// screen readers get the numbers as a table.////   <OhlcChart label="ACME, daily" candles={bars}/>//   <OhlcChart label="EUR/USD, 4 hours" candles={fx} decimals={4} volume={false} sma={[30]}/>//// candles: a list of { time, open, high, low, close, volume }, oldest first. time is a// date ("2026-03-02") or a date-time ("2026-03-02T14:00:00Z", shown as written);// volume may be left out. The axis below is labelled at the starts of years, months// or days as the bars cross them.// sma and ema: periods of simple and exponential moving averages of the close, each// drawn as a line of its own colour and named in a legend ([20], [50, 200]).// volume: the volume pane under the bars (true); volume_height: its height in pixels// (64). height: the price pane's height in pixels (300). last_line: a dashed line at// the last close, with its price on the axis (true). prefix and suffix: around every// price shown ("$"); decimals: digits after the point of prices (2).// label: what the chart shows; it names it for screen readers and captions their// table. show_label also writes it above the chart.import ui/theme.qwe use ui_clslet ohlc_chart_css = css`.ui-ohlc { --ui-ohlc-1: light-dark(#2a78d6, #3987e5); --ui-ohlc-2: light-dark(#eb6834, #d95926);  --ui-ohlc-3: light-dark(#1baf7a, #199e70); --ui-ohlc-4: light-dark(#eda100, #c98500);  --ui-ohlc-5: light-dark(#e87ba4, #d55181); --ui-ohlc-6: light-dark(#008300, #008300);  --ui-ohlc-7: light-dark(#6250d6, #9085e9); --ui-ohlc-8: light-dark(#e34948, #e66767);  display: grid; gap: 10px; min-width: 0; margin: 0; font: 12px/1.3 var(--ui-font); color: var(--ui-fg); }.ui-ohlc-title { margin: 0; font-size: 14px; font-weight: 600; }.ui-ohlc-s1 { --c: var(--ui-ohlc-1); } .ui-ohlc-s2 { --c: var(--ui-ohlc-2); } .ui-ohlc-s3 { --c: var(--ui-ohlc-3); }.ui-ohlc-s4 { --c: var(--ui-ohlc-4); } .ui-ohlc-s5 { --c: var(--ui-ohlc-5); } .ui-ohlc-s6 { --c: var(--ui-ohlc-6); }.ui-ohlc-s7 { --c: var(--ui-ohlc-7); } .ui-ohlc-s8 { --c: var(--ui-ohlc-8); }.ui-ohlc-up { --k: var(--ui-success); } .ui-ohlc-down { --k: var(--ui-danger); }.ui-ohlc-legend { display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 0; padding: 0; list-style: none; color: var(--ui-muted); font-size: 13px; }.ui-ohlc-legend li { display: inline-flex; align-items: center; gap: 6px; }.ui-ohlc-key { width: 14px; height: 3px; border-radius: 2px; background: var(--c); flex: none; }.ui-ohlc-frame { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 8px; padding-top: 8px; }.ui-ohlc-plot { position: relative; }.ui-ohlc-pane, .ui-ohlc-cp { position: relative; height: var(--h); }.ui-ohlc-vpane, .ui-ohlc-cv { position: relative; height: var(--vh); margin-top: 16px; }.ui-ohlc-vpane { border-top: 1px solid color-mix(in srgb, var(--ui-border) 70%, transparent); }.ui-ohlc-grid { position: absolute; left: 0; right: 0; height: 1px; background: color-mix(in srgb, var(--ui-border) 70%, transparent); }.ui-ohlc-svg { position: absolute; z-index: 2; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }.ui-ohlc-ma { fill: none; stroke: var(--c); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }.ui-ohlc-last { position: absolute; z-index: 1; left: 0; right: 0; border-top: 1px dashed var(--k); opacity: .75; pointer-events: none; }.ui-ohlc-cols { position: absolute; inset: 0; display: flex; }.ui-ohlc-col { position: relative; flex: 1; min-width: 0; }.ui-ohlc-col::before { content: ""; position: absolute; top: 0; bottom: 0; left: 50%; width: 1px; background: var(--ui-strong); opacity: 0; }.ui-ohlc-col:hover { background: color-mix(in srgb, var(--ui-subtle) 70%, transparent); }.ui-ohlc-col:hover::before { opacity: .8; }.ui-ohlc-bar { position: absolute; left: 50%; width: 2px; margin-left: -1px; background: var(--k); }.ui-ohlc-open, .ui-ohlc-close { position: absolute; height: 2px; margin-top: -1px; background: var(--k); }.ui-ohlc-open { right: 50%; left: 12%; }.ui-ohlc-close { left: 50%; right: 12%; }.ui-ohlc-vol { position: absolute; bottom: 0; left: 16%; right: 16%; min-height: 1px; border-radius: 1px 1px 0 0; background: var(--k); opacity: .35; }.ui-ohlc-col:hover .ui-ohlc-vol { opacity: .7; }.ui-ohlc-tip { position: absolute; z-index: 20; top: 0; left: calc(50% + 10px); display: none; min-width: 150px; padding: 7px 10px;  border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface);  box-shadow: var(--ui-shadow); color: var(--ui-fg); font-size: 12px; pointer-events: none; }.ui-ohlc-flip .ui-ohlc-tip { left: auto; right: calc(50% + 10px); }.ui-ohlc-col:hover .ui-ohlc-tip { display: block; }.ui-ohlc-tip strong { display: block; margin-bottom: 3px; font-weight: 600; white-space: nowrap; }.ui-ohlc-tip p { display: flex; align-items: center; gap: 6px; margin: 2px 0 0; color: var(--ui-muted); white-space: nowrap; }.ui-ohlc-tip b { margin-left: auto; padding-left: 12px; color: var(--ui-fg); font-weight: 600; font-variant-numeric: tabular-nums; }.ui-ohlc-tip .ui-ohlc-chg b { color: var(--k); }.ui-ohlc-y { position: relative; }.ui-ohlc-y > div { position: relative; }.ui-ohlc-y span { position: absolute; left: 0; transform: translateY(-50%); color: var(--ui-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-ohlc-y .ui-ohlc-tag { z-index: 1; left: -4px; padding: 1px 4px; border-radius: 3px; background: var(--k); color: var(--ui-bg); font-weight: 600; }.ui-ohlc-x { position: relative; height: 16px; margin-top: 6px; }.ui-ohlc-x span { position: absolute; top: 0; transform: translateX(-50%); color: var(--ui-muted); white-space: nowrap; }.ui-ohlc-x .ui-ohlc-first { transform: none; }.ui-ohlc-x .ui-ohlc-end { transform: translateX(-100%); }`// n (a whole number, 0 or more) with at least `width` digits.fn ohlc_chart_pad(n: int, width: int) -> str {    let s = str(n)    len(s) < width ? repeat("0", width - len(s)) + s : s}// A whole number with commas: 1234567 is "1,234,567".fn ohlc_chart_group(n: int) -> str =    n < 1000 ? str(n) : ohlc_chart_group(floor(n / 1000)) + "," + ohlc_chart_pad(n % 1000, 3)// A value as it reads, with commas and `places` digits after the point (up to two as// needed when that is null): 48210.5 is "48,210.50" with 2. Worked out in whole// numbers, so the server and the browser write the same text.fn ohlc_chart_num(v: num, places: int? = null) -> str {    let digits = places == null ? 2 : (places < 0 ? 0 : (places > 6 ? 6 : places))    let scale = [1, 10, 100, 1000, 10000, 100000, 1000000][digits]    let units = round(abs(v) * scale)    let frac = units % scale    let tail = digits == 0 || (places == null && frac == 0) ? ""        : (places == null && frac % 10 == 0 ? `.${floor(frac / 10)}` : `.${ohlc_chart_pad(frac, digits)}`)    `${v < 0 && units > 0 ? "-" : ""}${ohlc_chart_group(floor(units / scale))}${tail}`}// A large number, short: 1500 is "1.5k", 2000000 is "2M", 0.25 is "0.25".fn ohlc_chart_short(v: num) -> str {    let a = abs(v)    let unit = a >= 1000000000 ? 1000000000 : (a >= 1000000 ? 1000000 : (a >= 1000 ? 1000 : 1))    let tenths = round(a * 10 / unit)    let mark = unit == 1000000000 ? "B" : (unit == 1000000 ? "M" : "k")    unit == 1 ? ohlc_chart_num(v)        : `${v < 0 ? "-" : ""}${ohlc_chart_group(floor(tenths / 10))}${tenths % 10 != 0 ? `.${tenths % 10}` : ""}${mark}`}// A change in percent, signed, to two decimals: "+1.25%", "-0.40%".fn ohlc_chart_signed(p: num) -> str = `${p > 0 ? "+" : (p < 0 && round(abs(p) * 100) > 0 ? "-" : "")}${ohlc_chart_num(abs(p), 2)}%`// A coordinate out of 100 (or a length), to two decimals.fn ohlc_chart_pct(n: num) -> str {    let r = round(n * 100) / 100    r == floor(r) ? str(floor(r)) : str(r)}// The axis from lo to hi, widened to round ticks: about `steps` steps of 1, 2, 2.5 or 5 x 10^n.fn ohlc_chart_scale(lo: num, hi: num, steps: int = 5) -> map {    let span = hi - lo == 0 ? (lo == 0 ? 1 : abs(lo) / 10) : hi - lo    let raw = span / steps    let mag = 1    while mag * 10 <= raw { mag = mag * 10 }    while mag > raw { mag = mag / 10 }    let r = raw / mag    let step = (r <= 1 ? 1 : (r <= 2 ? 2 : (r <= 2.5 ? 2.5 : (r <= 5 ? 5 : 10)))) * mag    let a = floor(lo / step + 0.000001)    let b0 = ceil(hi / step - 0.000001)    let b = b0 == a ? a + 1 : b0    { lo: a * step, hi: b * step, ticks: for i in range(b - a + 1) => (a + i) * step }}// A candle's time as it reads: "Mar 2, 2026" for a date, "Mar 2, 14:30" for a date-time.fn ohlc_chart_when(t) -> str =    type_of(t) != "str" || !date.valid(t) ? str(t) : (len(t) > 10 ? date.format(t, "MMM D, HH:mm") : date.format(t, "MMM D, YYYY"))// Where the time axis is labelled: about `limit` of { i, text }, at the starts of years,// months or days when the times cross them ("2026", "Mar", "Mar 2"), else every few// candles ("14:30", "Mar 2").fn ohlc_chart_ticks(times: list, limit: int) -> list {    let n = len(times)    let thin = (idx: list) => {        let step = ceil(len(idx) / limit)        range(len(idx)) | filter((k) => k % step == 0) | map((k) => idx[k])    }    let spaced = (fmt_at) => {        let step = ceil(n / limit)        range(n) | filter((i) => i % step == floor(step / 2)) | map((i) => { i, text: fmt_at(i) })    }    if n == 0 { return [] }    if !all(times, (t) => type_of(t) == "str" && date.valid(t)) { return spaced((i) => str(times[i])) }    let days = times | map((t) => slice(t, 0, 10))    let changes = (keys: list) => range(1, n) | filter((i) => keys[i] != keys[i - 1])    let day_starts = changes(days)    let month_starts = changes(days | map((d) => slice(d, 0, 7)))    let year_starts = changes(days | map((d) => slice(d, 0, 4)))    let picked = if any(times, (t) => len(t) > 10) {        len(day_starts) >= 3            ? thin(day_starts) | map((i) => { i, text: date.format(days[i], "MMM D") })            : spaced((i) => index_of(day_starts, i) != null ? date.format(days[i], "MMM D") : date.format(times[i], "HH:mm"))    } else if len(year_starts) >= 3 && len(month_starts) > limit * 2 {        thin(year_starts) | map((i) => { i, text: slice(days[i], 0, 4) })    } else if len(month_starts) >= 2 {        thin(month_starts) | map((i) => { i, text: index_of(year_starts, i) != null ? slice(days[i], 0, 4) : date.format(days[i], "MMM") })    } else {        spaced((i) => date.format(days[i], "MMM D"))    }    // Starts that fall close together (a short month) keep the first.    let gap = n / (limit * 1.5)    reduce(picked, [], (kept, t) => len(kept) == 0 || t.i - kept[-1].i >= gap ? kept + [t] : kept)}// The simple moving average of `period` values, null until there are that many.fn ohlc_chart_sma(xs: list, period: int) -> list {    let out = []    let total = 0    for i in range(len(xs)) {        total += xs[i]        if i >= period { total -= xs[i - period] }        out = out | push(i + 1 >= period ? total / period : null)    }    out}// The exponential moving average of `period` values (k = 2 / (period + 1)), started// from the simple average of the first `period`; null before that.fn ohlc_chart_ema(xs: list, period: int) -> list {    let k = 2 / (period + 1)    let out = []    let prev = null    for i in range(len(xs)) {        if i + 1 == period {            prev = sum(take(xs, period)) / period        } else if i + 1 > period {            prev = xs[i] * k + prev * (1 - k)        }        out = out | push(prev)    }    out}view OhlcChart(candles: list, sma: list = [], ema: list = [], volume: bool = true, height: int = 300,                      volume_height: int = 64, last_line: bool = true, prefix: str = "", suffix: str = "",                      decimals: int = 2, label: str = "OHLC chart", show_label: bool = false,                      class: str? = null) {    style global ohlc_chart_css    let n = len(candles)    let closes = candles | map((c) => c.close)    let lines = (sma | map((p) => { name: `SMA ${p}`, values: ohlc_chart_sma(closes, p) }))        + (ema | map((p) => { name: `EMA ${p}`, values: ohlc_chart_ema(closes, p) }))    let hues = [1, 2, 7, 4, 5, 3, 6, 8]    let known = flatten(candles | map((c) => [c.low, c.high])) + flatten(lines | map((l) => l.values | filter((v) => v != null)))    let sc = ohlc_chart_scale(n == 0 ? 0 : min(known), n == 0 ? 1 : max(known), 5)    let y = (v) => (sc.hi - v) * 100 / (sc.hi - sc.lo)    let x = (i) => (i + 0.5) * 100 / n    let pct = (v) => ohlc_chart_pct(v)    let vols = candles | map((c) => c.volume ?? 0)    let vtop = max([0] + vols)    let price = (v) => v == null ? "—" : `${v < 0 ? "-" : ""}${prefix}${ohlc_chart_num(abs(v), decimals)}${suffix}`    let tick = (t) => `${t < 0 ? "-" : ""}${prefix}${ohlc_chart_num(abs(t))}`    let change = (i) => {        let before = i > 0 ? candles[i - 1].close : candles[i].open        before != 0 ? (candles[i].close - before) * 100 / before : 0    }    let side = (up) => up ? "ui-ohlc-up" : "ui-ohlc-down"    let last = n > 0 ? candles[-1] : null    let axis_width = max([4] + (sc.ticks | map((t) => len(tick(t)))) + [last != null ? len(price(last.close)) + 1 : 0])    let line_path = (vals: list) => {        let at = range(n) | filter((i) => vals[i] != null)        len(at) == 0 ? "" : "M" + join(at | map((i) => `${pct(x(i))},${pct(y(vals[i]))}`), " L")    }    <figure class={ui_cls(["ui-ohlc", class])} aria-label={label}            style={`--h: ${height}px; --vh: ${volume ? volume_height : 0}px`}>        {show_label && <figcaption.ui-ohlc-title>{label}</figcaption>}        {len(lines) > 0 && <ul.ui-ohlc-legend aria-hidden="true">            {for [j, l] in enumerate(lines) => <li><span class={`ui-ohlc-key ui-ohlc-s${hues[j % 8]}`}></span>{l.name}</li>}        </ul>}        <div.ui-ohlc-frame aria-hidden="true">            <div.ui-ohlc-plot>                <div.ui-ohlc-pane>                    {for t in sc.ticks => <span.ui-ohlc-grid style={`top: ${pct(y(t))}%`}></span>}                    {last_line && last != null && <span class={`ui-ohlc-last ${side(last.close >= last.open)}`}                                                        style={`top: ${pct(y(last.close))}%`}></span>}                    {len(lines) > 0 && <svg.ui-ohlc-svg viewBox="0 0 100 100" preserveAspectRatio="none">                        {for [j, l] in enumerate(lines) => <path class={`ui-ohlc-ma ui-ohlc-s${hues[j % 8]}`} d={line_path(l.values)}                                                                 vector-effect="non-scaling-stroke"/>}                    </svg>}                </div>                {volume && <div.ui-ohlc-vpane></div>}                <div.ui-ohlc-cols>                    {for [i, c] in enumerate(candles) => <div class={ui_cls(["ui-ohlc-col", side(c.close >= c.open), n > 1 && i >= n / 2 && "ui-ohlc-flip"])}>                        <div.ui-ohlc-cp>                            <span.ui-ohlc-bar style={`top: ${pct(y(c.high))}%; height: ${pct(y(c.low) - y(c.high))}%`}></span>                            <span.ui-ohlc-open style={`top: ${pct(y(c.open))}%`}></span>                            <span.ui-ohlc-close style={`top: ${pct(y(c.close))}%`}></span>                        </div>                        {volume && <div.ui-ohlc-cv>                            <span.ui-ohlc-vol style={`height: ${vtop > 0 ? pct(vols[i] * 100 / vtop) : 0}%`}></span>                        </div>}                        <span.ui-ohlc-tip>                            <strong>{ohlc_chart_when(c.time)}</strong>                            <p>Open<b>{price(c.open)}</b></p>                            <p>High<b>{price(c.high)}</b></p>                            <p>Low<b>{price(c.low)}</b></p>                            <p>Close<b>{price(c.close)}</b></p>                            <p class={`ui-ohlc-chg ${side(change(i) >= 0)}`}>Change<b>{ohlc_chart_signed(change(i))}</b></p>                            {volume && <p>Volume<b>{ohlc_chart_num(vols[i], 0)}</b></p>}                            {for [j, l] in enumerate(lines) => <p>                                <span class={`ui-ohlc-key ui-ohlc-s${hues[j % 8]}`}></span>{l.name}<b>{price(l.values[i])}</b>                            </p>}                        </span>                    </div>}                </div>            </div>            <div.ui-ohlc-y style={`width: ${axis_width}ch`}>                <div style="height: var(--h)">                    {for t in sc.ticks => !(last_line && last != null && abs(y(t) - y(last.close)) * height < 1400) &&                        <span style={`top: ${pct(y(t))}%`}>{tick(t)}</span>}                    {last_line && last != null && <span class={`ui-ohlc-tag ${side(last.close >= last.open)}`}                                                        style={`top: ${pct(y(last.close))}%`}>{price(last.close)}</span>}                </div>                {volume && <div style="height: var(--vh); margin-top: 16px">                    {vtop > 0 && <span style="top: 2px; transform: none">{ohlc_chart_short(vtop)}</span>}                </div>}            </div>            <div.ui-ohlc-x>                {for t in ohlc_chart_ticks(candles | map((c) => c.time), 7) => <span class={ui_cls([x(t.i) < 4 && "ui-ohlc-first", x(t.i) > 96 && "ui-ohlc-end"])}                                                                               style={`left: ${pct(x(t.i))}%`}>{t.text}</span>}            </div>            <span></span>        </div>        <table.ui-sr>            <caption>{label}</caption>            <thead><tr>                <th scope="col">Time</th><th scope="col">Open</th><th scope="col">High</th>                <th scope="col">Low</th><th scope="col">Close</th>{volume && <th scope="col">Volume</th>}            </tr></thead>            <tbody>                {for [i, c] in enumerate(candles) => <tr>                    <th scope="row">{ohlc_chart_when(c.time)}</th>                    <td>{price(c.open)}</td><td>{price(c.high)}</td><td>{price(c.low)}</td><td>{price(c.close)}</td>                    {volume && <td>{ohlc_chart_num(vols[i], 0)}</td>}                </tr>}            </tbody>        </table>    </figure>}
Thread
66Useful? Like it, or add to its thread below.

Thread 0 posts

On OhlcChart: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.