Charts

RenkoChart

Renko bricks of one box size from closing prices, a brick for each whole box the price moves, with each brick's range on hover.

#chart#data#trading#no-js

Demo

Preview
Initech, $2 bricks

Box $2.00 · 22 bricks

Initech, $2 bricks: bricks of $2.00
BrickDirectionFromToFormed
1Down$118.00$116.00Mar 6, 2026
2Down$116.00$114.00Mar 9, 2026
3Up$116.00$118.00Mar 13, 2026
4Up$118.00$120.00Mar 26, 2026
5Up$120.00$122.00Apr 7, 2026
6Up$122.00$124.00Apr 9, 2026
7Up$124.00$126.00Apr 20, 2026
8Down$124.00$122.00May 12, 2026
9Down$122.00$120.00Jun 10, 2026
10Down$120.00$118.00Jun 16, 2026
11Down$118.00$116.00Jun 18, 2026
12Down$116.00$114.00Jun 25, 2026
13Up$116.00$118.00Jul 7, 2026
14Up$118.00$120.00Jul 9, 2026
15Up$120.00$122.00Jul 15, 2026
16Down$120.00$118.00Aug 5, 2026
17Down$118.00$116.00Aug 14, 2026
18Down$116.00$114.00Aug 17, 2026
19Down$114.00$112.00Sep 17, 2026
20Down$112.00$110.00Sep 21, 2026
21Up$112.00$114.00Oct 1, 2026
22Up$114.00$116.00Oct 7, 2026
Initech, box chosen

Box $1.00 · 62 bricks

Initech, box chosen: bricks of $1.00
BrickDirectionFromToFormed
1Down$119.00$118.00Mar 5, 2026
2Down$118.00$117.00Mar 5, 2026
3Down$117.00$116.00Mar 6, 2026
4Down$116.00$115.00Mar 6, 2026
5Down$115.00$114.00Mar 9, 2026
6Up$115.00$116.00Mar 12, 2026
7Up$116.00$117.00Mar 12, 2026
8Up$117.00$118.00Mar 13, 2026
9Up$118.00$119.00Mar 19, 2026
10Up$119.00$120.00Mar 26, 2026
11Up$120.00$121.00Apr 7, 2026
12Up$121.00$122.00Apr 7, 2026
13Up$122.00$123.00Apr 9, 2026
14Up$123.00$124.00Apr 9, 2026
15Up$124.00$125.00Apr 13, 2026
16Up$125.00$126.00Apr 20, 2026
17Down$125.00$124.00Apr 22, 2026
18Down$124.00$123.00Apr 27, 2026
19Up$124.00$125.00May 8, 2026
20Down$124.00$123.00May 12, 2026
21Down$123.00$122.00May 12, 2026
22Down$122.00$121.00May 25, 2026
23Up$122.00$123.00Jun 1, 2026
24Down$122.00$121.00Jun 10, 2026
25Down$121.00$120.00Jun 10, 2026
26Down$120.00$119.00Jun 15, 2026
27Down$119.00$118.00Jun 16, 2026
28Down$118.00$117.00Jun 17, 2026
29Down$117.00$116.00Jun 18, 2026
30Down$116.00$115.00Jun 19, 2026
31Down$115.00$114.00Jun 25, 2026
32Up$115.00$116.00Jun 30, 2026
33Up$116.00$117.00Jul 7, 2026
34Up$117.00$118.00Jul 7, 2026
35Up$118.00$119.00Jul 8, 2026
36Up$119.00$120.00Jul 9, 2026
37Up$120.00$121.00Jul 10, 2026
38Up$121.00$122.00Jul 15, 2026
39Down$121.00$120.00Aug 3, 2026
40Down$120.00$119.00Aug 3, 2026
41Down$119.00$118.00Aug 5, 2026
42Down$118.00$117.00Aug 7, 2026
43Up$118.00$119.00Aug 11, 2026
44Down$118.00$117.00Aug 13, 2026
45Down$117.00$116.00Aug 14, 2026
46Down$116.00$115.00Aug 17, 2026
47Down$115.00$114.00Aug 17, 2026
48Up$115.00$116.00Aug 27, 2026
49Up$116.00$117.00Aug 27, 2026
50Down$116.00$115.00Sep 3, 2026
51Down$115.00$114.00Sep 7, 2026
52Down$114.00$113.00Sep 14, 2026
53Down$113.00$112.00Sep 17, 2026
54Down$112.00$111.00Sep 17, 2026
55Down$111.00$110.00Sep 21, 2026
56Down$110.00$109.00Sep 21, 2026
57Up$110.00$111.00Sep 23, 2026
58Up$111.00$112.00Sep 24, 2026
59Up$112.00$113.00Sep 28, 2026
60Up$113.00$114.00Oct 1, 2026
61Up$114.00$115.00Oct 2, 2026
62Up$115.00$116.00Oct 7, 2026
Closes alone

Box 1.00 · 10 bricks

Closes alone: bricks of 1.00
BrickDirectionFromTo
1Up100.00101.00
2Up101.00102.00
3Up102.00103.00
4Up103.00104.00
5Down103.00102.00
6Down102.00101.00
7Down101.00100.00
8Down100.0099.00
9Up100.00101.00
10Up101.00102.00

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/renko_chart.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/renko_chart.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/renko_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 renko_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 renko_chart_demo_daily = renko_chart_demo_walk(3, 160, 120.5, "2026-03-02")view RenkoChartDemo() =    <div.demo-stack>        <div id="renko_chart-demo-box">            <RenkoChart label="Initech, $2 bricks" show_label={true} candles={renko_chart_demo_daily} box={2} prefix="$"/>        </div>        <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 32px 40px">            <div id="renko_chart-demo-auto">                <RenkoChart label="Initech, box chosen" show_label={true} candles={renko_chart_demo_daily} prefix="$" height={200}/>            </div>            <div id="renko_chart-demo-closes">                <RenkoChart label="Closes alone" show_label={true} closes={[100, 101.2, 103.4, 104.9, 103.1, 101.8, 99.6, 98.7, 100.4, 102.2]}                            box={1} height={200}/>            </div>        </div>    </div>

Source

ui/renko_chart.qwe
// RenkoChart: Renko bricks of one box size, worked out from closing prices, so only// moves of a whole box count and time drops out: a green brick each time the close// rises a box past the last brick's top, a red one each time it falls a box below its// bottom (turning around takes two boxes). CSS only: hovering a brick shows its range// and when it formed, and screen readers get the bricks as a table.////   <RenkoChart label="ACME, $2 bricks" candles={bars} box={2} prefix="$"/>//   <RenkoChart label="BTC-USD" candles={hourly} decimals={0}/>//   <RenkoChart label="Index" closes={[4012.5, 4020.1, 4031.8, 4018.2]} box={5}/>//// candles: a list of { time, close } (OHLC candles do), oldest first; or closes: the// numbers alone, without times. box: the brick's size in price; left out, a round// size about a twenty-fourth of the range of the closes. height: the plot's height in// pixels (280). 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 renko_chart_css = css`.ui-rk { --ui-rk-up: var(--ui-success); --ui-rk-down: var(--ui-danger);  display: grid; gap: 10px; min-width: 0; margin: 0; font: 12px/1.3 var(--ui-font); color: var(--ui-fg); }.ui-rk-title { margin: 0; font-size: 14px; font-weight: 600; }.ui-rk-meta { margin: 0; color: var(--ui-muted); font-size: 13px; }.ui-rk-up { --k: var(--ui-rk-up); } .ui-rk-down { --k: var(--ui-rk-down); }.ui-rk-frame { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 8px; padding-top: 8px; }.ui-rk-plot { position: relative; height: var(--h); }.ui-rk-grid { position: absolute; left: 0; right: 0; height: 1px; background: color-mix(in srgb, var(--ui-border) 70%, transparent); }.ui-rk-last { position: absolute; z-index: 1; left: 0; right: 0; border-top: 1px dashed var(--k); opacity: .75; pointer-events: none; }.ui-rk-cols { position: absolute; inset: 0; display: flex; }.ui-rk-col { position: relative; flex: 1; min-width: 0; }.ui-rk-col:hover { background: color-mix(in srgb, var(--ui-subtle) 70%, transparent); }.ui-rk-brick { position: absolute; left: 6%; right: 6%; min-height: 2px; border: 1px solid var(--k); border-radius: 2px;  background: color-mix(in srgb, var(--k) 70%, transparent); }.ui-rk-down .ui-rk-brick { background: color-mix(in srgb, var(--k) 18%, transparent); }.ui-rk-col:hover .ui-rk-brick { background: var(--k); }.ui-rk-tip { position: absolute; z-index: 20; top: 0; left: calc(50% + 12px); 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-rk-flip .ui-rk-tip { left: auto; right: calc(50% + 12px); }.ui-rk-col:hover .ui-rk-tip { display: block; }.ui-rk-tip strong { display: block; margin-bottom: 3px; font-weight: 600; white-space: nowrap; color: var(--k); }.ui-rk-tip p { display: flex; align-items: center; gap: 6px; margin: 2px 0 0; color: var(--ui-muted); white-space: nowrap; }.ui-rk-tip b { margin-left: auto; padding-left: 12px; color: var(--ui-fg); font-weight: 600; font-variant-numeric: tabular-nums; }.ui-rk-y { position: relative; height: var(--h); }.ui-rk-y span { position: absolute; left: 0; transform: translateY(-50%); color: var(--ui-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-rk-y .ui-rk-tag { z-index: 1; left: -4px; padding: 1px 4px; border-radius: 3px; background: var(--k); color: var(--ui-bg); font-weight: 600; }.ui-rk-x { position: relative; height: 16px; margin-top: 6px; }.ui-rk-x span { position: absolute; top: 0; transform: translateX(-50%); color: var(--ui-muted); white-space: nowrap; }.ui-rk-x .ui-rk-first { transform: none; }.ui-rk-x .ui-rk-end { transform: translateX(-100%); }.ui-rk-empty { display: grid; place-items: center; height: var(--h); border: 1px dashed var(--ui-border); border-radius: var(--ui-radius-sm); color: var(--ui-muted); }`// n (a whole number, 0 or more) with at least `width` digits.fn renko_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 renko_chart_group(n: int) -> str =    n < 1000 ? str(n) : renko_chart_group(floor(n / 1000)) + "," + renko_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 renko_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)}` : `.${renko_chart_pad(frac, digits)}`)    `${v < 0 && units > 0 ? "-" : ""}${renko_chart_group(floor(units / scale))}${tail}`}// A large number, short: 1500 is "1.5k", 2000000 is "2M", 0.25 is "0.25".fn renko_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 ? renko_chart_num(v)        : `${v < 0 ? "-" : ""}${renko_chart_group(floor(tenths / 10))}${tenths % 10 != 0 ? `.${tenths % 10}` : ""}${mark}`}// A change in percent, signed, to two decimals: "+1.25%", "-0.40%".fn renko_chart_signed(p: num) -> str = `${p > 0 ? "+" : (p < 0 && round(abs(p) * 100) > 0 ? "-" : "")}${renko_chart_num(abs(p), 2)}%`// A coordinate out of 100 (or a length), to two decimals.fn renko_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 renko_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 round brick size, about a twenty-fourth of the range from lo to hi.fn renko_chart_step(lo: num, hi: num) -> num {    let sc = renko_chart_scale(lo, hi, 24)    len(sc.ticks) > 1 ? sc.ticks[1] - sc.ticks[0] : 1}// A candle's time as it reads: "Mar 2, 2026" for a date, "Mar 2, 14:30" for a date-time.fn renko_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 renko_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 bricks: a list of { up, lo, hi, time, close }, the time and close of the candle that// made each. Prices are kept as whole numbers of boxes, so they do not drift.fn renko_chart_bricks(points: list, box: num) -> list {    let out = []    if len(points) == 0 || box <= 0 { return out }    let top = floor(points[0].close / box + 0.000001)    let bottom = top    for p in points {        while p.close >= (top + 1) * box - box * 0.000001 {            out = out | push({ up: true, lo: top * box, hi: (top + 1) * box, time: p.time, close: p.close })            bottom = top            top += 1        }        while p.close <= (bottom - 1) * box + box * 0.000001 {            out = out | push({ up: false, lo: (bottom - 1) * box, hi: bottom * box, time: p.time, close: p.close })            top = bottom            bottom -= 1        }    }    out}view RenkoChart(candles: list = [], closes: list? = null, box: num? = null, height: int = 280, last_line: bool = true,                prefix: str = "", suffix: str = "", decimals: int = 2, label: str = "Renko chart",                show_label: bool = false, class: str? = null) {    style global renko_chart_css    let points = closes != null ? (closes | map((c) => { time: null, close: c })) : (candles | map((c) => { time: c.time, close: c.close }))    let vals = points | map((p) => p.close)    let size = box ?? (len(vals) < 2 ? 1 : renko_chart_step(min(vals), max(vals)))    let bricks = renko_chart_bricks(points, size)    let n = len(bricks)    let last = len(vals) > 0 ? vals[-1] : null    let known = flatten(bricks | map((b) => [b.lo, b.hi])) + (last != null ? [last] : [])    let sc = renko_chart_scale(len(known) == 0 ? 0 : min(known), len(known) == 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) => renko_chart_pct(v)    let price = (v) => `${v < 0 ? "-" : ""}${prefix}${renko_chart_num(abs(v), decimals)}${suffix}`    let tick = (t) => `${t < 0 ? "-" : ""}${prefix}${renko_chart_num(abs(t))}`    let side = (up) => up ? "ui-rk-up" : "ui-rk-down"    let last_up = n == 0 || bricks[-1].up    let timed = n > 0 && all(bricks, (b) => b.time != null)    let axis_width = max([4] + (sc.ticks | map((t) => len(tick(t)))) + [last != null ? len(price(last)) + 1 : 0])    <figure class={ui_cls(["ui-rk", class])} aria-label={label} style={`--h: ${height}px`}>        {show_label && <figcaption.ui-rk-title>{label}</figcaption>}        <p.ui-rk-meta>Box {price(size)} · {n} {n == 1 ? "brick" : "bricks"}</p>        {n == 0 ? <div.ui-rk-empty>The price has not moved a whole box.</div>            : <div.ui-rk-frame aria-hidden="true">            <div.ui-rk-plot>                {for t in sc.ticks => <span.ui-rk-grid style={`top: ${pct(y(t))}%`}></span>}                {last_line && last != null && <span class={`ui-rk-last ${side(last_up)}`} style={`top: ${pct(y(last))}%`}></span>}                <div.ui-rk-cols>                    {for [i, b] in enumerate(bricks) => <div class={ui_cls(["ui-rk-col", side(b.up), n > 1 && i >= n / 2 && "ui-rk-flip"])}>                        <span.ui-rk-brick style={`top: ${pct(y(b.hi))}%; height: ${pct(y(b.lo) - y(b.hi))}%`}></span>                        <span.ui-rk-tip>                            <strong>{b.up ? "Up" : "Down"} brick {i + 1}</strong>                            <p>From<b>{price(b.up ? b.lo : b.hi)}</b></p>                            <p>To<b>{price(b.up ? b.hi : b.lo)}</b></p>                            {b.time != null && <p>Formed<b>{renko_chart_when(b.time)}</b></p>}                            <p>Close then<b>{price(b.close)}</b></p>                        </span>                    </div>}                </div>            </div>            <div.ui-rk-y style={`width: ${axis_width}ch`}>                {for t in sc.ticks => !(last_line && last != null && abs(y(t) - y(last)) * height < 1400) &&                    <span style={`top: ${pct(y(t))}%`}>{tick(t)}</span>}                {last_line && last != null && <span class={`ui-rk-tag ${side(last_up)}`} style={`top: ${pct(y(last))}%`}>{price(last)}</span>}            </div>            <div.ui-rk-x>                {timed && (for t in renko_chart_ticks(bricks | map((b) => b.time), 7) => <span                    class={ui_cls([x(t.i) < 4 && "ui-rk-first", x(t.i) > 96 && "ui-rk-end"])} style={`left: ${pct(x(t.i))}%`}>{t.text}</span>)}            </div>            <span></span>        </div>}        <table.ui-sr>            <caption>{label}: bricks of {price(size)}</caption>            <thead><tr><th scope="col">Brick</th><th scope="col">Direction</th><th scope="col">From</th><th scope="col">To</th>                {timed && <th scope="col">Formed</th>}</tr></thead>            <tbody>                {for [i, b] in enumerate(bricks) => <tr>                    <th scope="row">{i + 1}</th><td>{b.up ? "Up" : "Down"}</td>                    <td>{price(b.up ? b.lo : b.hi)}</td><td>{price(b.up ? b.hi : b.lo)}</td>                    {timed && <td>{renko_chart_when(b.time)}</td>}                </tr>}            </tbody>        </table>    </figure>}
Thread
57Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.