Charts

SymbolCard

A symbol with its name, last price, change and percent, a sparkline over the previous close, the day's range and volume.

#card#data#trading#no-js

Demo

Preview
ACMEAcme Corporation
$182.66+$2.51+1.39%
$177.69$182.66Vol 4.2MAt close
BTCBitcoin
61,25000.00%
ETHEther
2,410.50-25.10-1.03%

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

Example

The code of the preview above.

demos/symbol_card.qwe
// A day of prices from a seeded random walk (Park–Miller), the same on every render.fn symbol_card_demo_day(seed: int, start: num, count: int, swing: num, drift: num = 0) -> list {    let s = seed    let p = start    for i in range(count) {        s = s * 16807 % 2147483647        p = round(p * (1 + (s / 2147483647 - 0.5 + drift) * swing) * 100) / 100        p    }}let symbol_card_demo_acme = symbol_card_demo_day(3, 180.15, 48, 0.006, 0.12)let symbol_card_demo_glbx = symbol_card_demo_day(11, 49.02, 48, 0.008)view SymbolCardDemo() =    <div.demo-stack>        <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px">            <div id="symbol_card-demo-acme">                <SymbolCard symbol="ACME" name="Acme Corporation" price={symbol_card_demo_acme[-1]} prev={180.15}                            values={symbol_card_demo_acme} prefix="$" low={min(symbol_card_demo_acme)} high={max(symbol_card_demo_acme)}                            volume={4182330} note="At close"/>            </div>            <div id="symbol_card-demo-glbx">                <SymbolCard symbol="GLBX" name="Globex" price={symbol_card_demo_glbx[-1]} prev={49.02} values={symbol_card_demo_glbx}                            prefix="$" href="#symbol_card" note="Delayed 15 min"/>            </div>        </div>        <div class="demo-stack demo-narrow" id="symbol_card-demo-small" style="gap: 8px">            <SymbolCard size="sm" symbol="BTC" name="Bitcoin" price={61250} prev={61250} decimals={0} values={[61100, 61400, 61180, 61250]}/>            <SymbolCard size="sm" symbol="ETH" name="Ether" price={2410.5} change={-25.1} values={[2440, 2431, 2425, 2410.5]}/>        </div>    </div>

Source

ui/symbol_card.qwe
// SymbolCard: one symbol at a glance: its ticker and name, the last price, the change// on the previous close in money and percent, green up or red down, and a sparkline// of the day (or any run of prices) over a dashed line at the previous close. The// day's range and the volume can go under it, and the card can be a link.////   <SymbolCard symbol="ACME" name="Acme Corp." price={182.4} prev={180.15} values={intraday} prefix="$"/>//   <SymbolCard symbol="BTC-USD" name="Bitcoin" price={61250} prev={62010} decimals={0} values={hours}//               low={60820} high={62400} volume={18234} href="/quote/BTC-USD" note="24h"/>//// symbol and name: what it is. price: the last price; prev: the previous close, from// which the change is worked out (or change: the change in money, given). values: the// prices for the sparkline, oldest first (none: no sparkline). low and high: the day's// range, drawn as a bar with the price on it. volume: the day's volume. note: a word// on when ("At close", "Delayed 15 min"). prefix and suffix: around every price ("$");// decimals: digits after the point of prices (2). href: makes the card a link.// size: "md" (default) or "sm", a compact row for lists.import ui/theme.qwe use ui_clslet symbol_card_css = css`.ui-symc { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px 16px; align-items: start;  min-width: 0; padding: 16px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface);  color: var(--ui-fg); font: 14px/1.35 var(--ui-font); text-decoration: none; }.ui-symc-up { --k: var(--ui-success); } .ui-symc-down { --k: var(--ui-danger); } .ui-symc-flat { --k: var(--ui-muted); }a.ui-symc { transition: border-color .15s, box-shadow .15s; }a.ui-symc:hover { border-color: var(--ui-strong); box-shadow: var(--ui-shadow); }a.ui-symc:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-symc-id { display: grid; min-width: 0; }.ui-symc-sym { font-size: 15px; font-weight: 700; letter-spacing: .02em; }.ui-symc-name { overflow: hidden; color: var(--ui-muted); font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }.ui-symc-quote { display: grid; justify-items: end; }.ui-symc-price { font-size: 22px; font-weight: 650; font-variant-numeric: tabular-nums; letter-spacing: -.01em; }.ui-symc-chg { display: inline-flex; align-items: center; gap: 6px; color: var(--k); font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; }.ui-symc-pill { padding: 1px 6px; border-radius: 999px; background: color-mix(in srgb, var(--k) 14%, transparent); }.ui-symc-spark { grid-column: 1 / -1; width: 100%; height: 56px; overflow: visible; }.ui-symc-line { fill: none; stroke: var(--k); stroke-width: 1.75; stroke-linejoin: round; stroke-linecap: round; }.ui-symc-fill { fill: var(--k); opacity: .1; }.ui-symc-base { stroke: var(--ui-strong); stroke-width: 1; stroke-dasharray: 3 3; }.ui-symc-foot { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; color: var(--ui-muted); font-size: 12px; }.ui-symc-foot b { color: var(--ui-fg); font-weight: 600; font-variant-numeric: tabular-nums; }.ui-symc-range { display: inline-flex; align-items: center; gap: 8px; flex: 1 1 180px; font-variant-numeric: tabular-nums; }.ui-symc-bar { position: relative; flex: 1; height: 4px; border-radius: 2px; background: var(--ui-subtle); }.ui-symc-bar span { position: absolute; top: 50%; width: 8px; height: 8px; margin-left: -4px; border-radius: 50%; background: var(--k);  box-shadow: 0 0 0 2px var(--ui-surface); transform: translateY(-50%); }.ui-symc-note { margin-left: auto; }.ui-symc-sm { grid-template-columns: minmax(0, 1fr) 96px auto; align-items: center; padding: 10px 14px; }.ui-symc-sm .ui-symc-spark { grid-column: 2; grid-row: 1; height: 32px; }.ui-symc-sm .ui-symc-price { font-size: 15px; }.ui-symc-sm .ui-symc-foot { display: none; }`// n (a whole number, 0 or more) with at least `width` digits.fn symbol_card_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 symbol_card_group(n: int) -> str =    n < 1000 ? str(n) : symbol_card_group(floor(n / 1000)) + "," + symbol_card_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 symbol_card_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)}` : `.${symbol_card_pad(frac, digits)}`)    `${v < 0 && units > 0 ? "-" : ""}${symbol_card_group(floor(units / scale))}${tail}`}// A large number, short: 1500 is "1.5k", 2000000 is "2M", 0.25 is "0.25".fn symbol_card_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 ? symbol_card_num(v)        : `${v < 0 ? "-" : ""}${symbol_card_group(floor(tenths / 10))}${tenths % 10 != 0 ? `.${tenths % 10}` : ""}${mark}`}// A change in percent, signed, to two decimals: "+1.25%", "-0.40%".fn symbol_card_signed(p: num) -> str = `${p > 0 ? "+" : (p < 0 && round(abs(p) * 100) > 0 ? "-" : "")}${symbol_card_num(abs(p), 2)}%`// A coordinate out of 100 (or a length), to two decimals.fn symbol_card_pct(n: num) -> str {    let r = round(n * 100) / 100    r == floor(r) ? str(floor(r)) : str(r)}view SymbolCard(symbol: str, name: str = "", price: num, prev: num? = null, change: num? = null, values: list = [],                low: num? = null, high: num? = null, volume: num? = null, note: str? = null, prefix: str = "",                suffix: str = "", decimals: int = 2, href: str? = null, size: str = "md", class: str? = null) {    style global symbol_card_css    let diff = change ?? (prev != null ? price - prev : 0)    let base = prev ?? price - diff    let share = base != 0 ? diff * 100 / base : 0    let side = round(diff * pow(10, decimals)) == 0 ? "ui-symc-flat" : (diff > 0 ? "ui-symc-up" : "ui-symc-down")    let money = (v) => `${v < 0 ? "-" : ""}${prefix}${symbol_card_num(abs(v), decimals)}${suffix}`    let n = len(values)    let w = 200    let h = 56    let lo = n == 0 ? 0 : min(values + [base])    let hi = n == 0 ? 1 : max(values + [base])    let sy = (v) => hi == lo ? h / 2 : 3 + (hi - v) * (h - 6) / (hi - lo)    let sx = (i) => n <= 1 ? w : i * w / (n - 1)    let line = n == 0 ? "" : "M" + join(range(n) | map((i) => `${symbol_card_pct(sx(i))},${symbol_card_pct(sy(values[i]))}`), " L")    let said = `${symbol}${name != "" ? `, ${name}` : ""}: ${money(price)}, ${diff > 0 ? "up" : (diff < 0 ? "down" : "unchanged")} ${money(abs(diff))} (${symbol_card_num(abs(share), 2)}%)`    let body = [        <div.ui-symc-id><span.ui-symc-sym>{symbol}</span>{name != "" && <span.ui-symc-name>{name}</span>}</div>,        <div.ui-symc-quote>            <span.ui-symc-price>{money(price)}</span>            <span.ui-symc-chg>{diff > 0 ? "+" : ""}{money(diff)}<span.ui-symc-pill>{symbol_card_signed(share)}</span></span>        </div>,        n > 0 && <svg.ui-symc-spark viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" aria-hidden="true">            <path.ui-symc-fill d={`${line} L${w},${h} L0,${h} Z`}/>            <line.ui-symc-base x1="0" x2={w} y1={symbol_card_pct(sy(base))} y2={symbol_card_pct(sy(base))} vector-effect="non-scaling-stroke"/>            <path.ui-symc-line d={line} vector-effect="non-scaling-stroke"/>        </svg>,        (low != null && high != null || volume != null || note != null) && <div.ui-symc-foot>            {low != null && high != null && <span.ui-symc-range>                <b>{money(low)}</b>                <span.ui-symc-bar title="Day's range"><span style={`left: ${symbol_card_pct(high > low ? (price - low) * 100 / (high - low) : 50)}%`}></span></span>                <b>{money(high)}</b>            </span>}            {volume != null && <span>Vol <b>{symbol_card_short(volume)}</b></span>}            {note != null && <span.ui-symc-note>{note}</span>}        </div>,    ]    let cls = ui_cls(["ui-symc", side, size == "sm" && "ui-symc-sm", class])    href != null ? <a class={cls} href={href} aria-label={said}>{body}</a>                 : <article class={cls} aria-label={said}>{body}</article>}
Thread
66Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.