CurrencyPairCard
One currency pair: the bid and ask large with the pip digits biggest, the spread, the day's change, low and high, and a sparkline of the day.
Demo
EUR/USDEuro / US dollar
up+23.5 pips (+0.22%)Sell at the bid
0.8spread
Buy at the ask
Low1.08133High1.08502
Indicative, 14:05 UTC
USD/JPYUS dollar / Yen
down-65.8 pips (-0.42%)Sell at the bid
1.3spread
Buy at the ask
Low156.020High157.115
USD/PHPUS dollar / Philippine peso
Sell at the bid
3.5spread
Buy at the ask
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/currency_pair_card.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/currency_pair_card.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/currency_pair_card.qwe
view CurrencyPairCardDemo() = <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 12px"> <CurrencyPairCard id="currency-pair-card-demo-eur" pair="EUR/USD" bid={1.08421m} ask={1.08429m} open={1.08190m} high={1.08502m} low={1.08133m} href="#eurusd" updated="Indicative, 14:05 UTC" values={[1.0819, 1.0815, 1.0824, 1.0831, 1.0827, 1.0836, 1.0849, 1.0838, 1.0842]}/> <CurrencyPairCard id="currency-pair-card-demo-jpy" pair="USD/JPY" bid={156.238m} ask={156.251m} open={156.902m} high={157.115m} low={156.020m} values={[156.90, 157.05, 156.71, 156.62, 156.40, 156.31, 156.24]}/> <CurrencyPairCard id="currency-pair-card-demo-php" pair="USD/PHP" name="US dollar / Philippine peso" bid={58.105m} ask={58.140m}/> </div>Source
ui/currency_pair_card.qwe
// CurrencyPairCard: one currency pair at a glance: the bid (sell) and the ask (buy)// written large, with the two pip digits biggest as dealers show them (1.08 42 1),// the spread between them, the day's change from the open, the day's low and high// with where the price stands between them, and a sparkline of the day.//// <CurrencyPairCard pair="EUR/USD" bid={1.08421m} ask={1.08429m} open={1.08190m} high={1.08502m} low={1.08133m}// values={[1.0819, 1.0824, 1.0831, 1.0827, 1.0838, 1.0842]} updated="14:05 UTC"/>//// pair: "EUR/USD", base over quote. bid and ask: the prices (decimals best; numbers are// read as decimals). open: the day's opening mid price, for the change (none hides it).// high and low: the day's range (none hides it). values: the day's prices for the// sparkline, oldest first (two or more; numbers). name: under the pair ("Euro / US// dollar"; the common ones are known). pip: its size (0.01 when the quote is JPY or// another priced in the hundreds, else 0.0001); decimals: digits shown (one past the// pip). locale: how numbers are written ("en-US"). updated: when the prices are from.// href: makes the pair a link (to its chart, a deal ticket). id: the element's id.import ui/theme.qwe use ui_clslet currency_pair_card_css = css`.ui-fxpair { display: grid; gap: 12px; min-width: 0; max-width: 360px; padding: 16px 18px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); font: 14px/1.35 var(--ui-font); color: var(--ui-fg); font-variant-numeric: tabular-nums; align-content: start; --k: var(--ui-muted); }.ui-fxpair-up { --k: var(--ui-success); } .ui-fxpair-down { --k: var(--ui-danger); }.ui-fxpair-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px 10px; }.ui-fxpair .ui-fxpair-title { margin: 0; display: flex; align-items: center; gap: 8px; font-size: 16px; font-weight: 650; }.ui-fxpair-title a { color: inherit; text-decoration: none; }.ui-fxpair-title a:hover { color: var(--ui-accent); }.ui-fxpair-title a:focus-visible { outline: none; box-shadow: var(--ui-focus); border-radius: 3px; }.ui-fxpair-flags { flex: none; font-size: 18px; letter-spacing: -.15em; white-space: nowrap; }.ui-fxpair-name { display: block; color: var(--ui-muted); font-size: 12px; font-weight: 400; }.ui-fxpair-chg { display: inline-flex; align-items: center; gap: 3px; padding: 2px 8px; border-radius: 999px; font-size: 13px; font-weight: 600; color: var(--k); background: color-mix(in srgb, var(--k) 13%, transparent); white-space: nowrap; }.ui-fxpair-quotes { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: stretch; gap: 8px; }.ui-fxpair-side { display: grid; gap: 2px; min-width: 0; padding: 8px 10px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-subtle); }.ui-fxpair-side:last-child { text-align: right; }.ui-fxpair-side small { color: var(--ui-muted); font-size: 11px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; }.ui-fxpair-price { font-size: 14px; font-weight: 600; white-space: nowrap; }.ui-fxpair-big { font-size: 26px; font-weight: 700; line-height: 1; letter-spacing: -.01em; }.ui-fxpair-frac { vertical-align: super; font-size: 12px; font-weight: 600; }.ui-fxpair-spread { align-self: center; display: grid; justify-items: center; color: var(--ui-muted); font-size: 12px; }.ui-fxpair-spread b { color: var(--ui-fg); font-size: 14px; font-weight: 600; }.ui-fxpair-range { display: grid; grid-template-columns: auto minmax(24px, 1fr) auto; align-items: center; gap: 10px; font-size: 12px; color: var(--ui-muted); }.ui-fxpair-range b { display: block; color: var(--ui-fg); font-size: 13px; font-weight: 600; }.ui-fxpair-range > :last-child { text-align: right; }.ui-fxpair-track { position: relative; height: 4px; border-radius: 2px; background: var(--ui-border); }.ui-fxpair-dot { position: absolute; top: 50%; width: 10px; height: 10px; margin: -5px 0 0 -5px; border: 2px solid var(--ui-surface); border-radius: 50%; background: var(--k); box-sizing: border-box; }.ui-fxpair-spark { display: block; width: 100%; height: 48px; overflow: visible; }.ui-fxpair-spark path { fill: none; stroke: var(--k); stroke-width: 1.75; stroke-linejoin: round; stroke-linecap: round; }.ui-fxpair-spark .ui-fxpair-area { fill: var(--k); opacity: .1; stroke: none; }.ui-fxpair-spark line { stroke: var(--ui-strong); stroke-dasharray: 3 3; }.ui-fxpair-foot { margin: 0; color: var(--ui-muted); font-size: 12px; }`let currency_pair_card_flags = { USD: "๐บ๐ธ", EUR: "๐ช๐บ", GBP: "๐ฌ๐ง", JPY: "๐ฏ๐ต", CHF: "๐จ๐ญ", CAD: "๐จ๐ฆ", AUD: "๐ฆ๐บ", NZD: "๐ณ๐ฟ", CNY: "๐จ๐ณ", HKD: "๐ญ๐ฐ", SGD: "๐ธ๐ฌ", INR: "๐ฎ๐ณ", PHP: "๐ต๐ญ", MXN: "๐ฒ๐ฝ", ZAR: "๐ฟ๐ฆ", SEK: "๐ธ๐ช", NOK: "๐ณ๐ด", PLN: "๐ต๐ฑ", TRY: "๐น๐ท", KRW: "๐ฐ๐ท" }let currency_pair_card_names = { USD: "US dollar", EUR: "Euro", GBP: "Pound", JPY: "Yen", CHF: "Swiss franc", CAD: "Canadian dollar", AUD: "Aussie", NZD: "Kiwi", CNY: "Yuan", INR: "Rupee", PHP: "Philippine peso", MXN: "Mexican peso" }// A pip's size for "EUR/USD": 0.01 when the quote is priced in the hundreds, else 0.0001.fn currency_pair_card_pip(pair: str) -> dec { let parts = split(upper(pair), "/") contains(["JPY", "HUF", "KRW", "IDR", "VND", "CLP", "ISK", "THB", "INR", "PHP", "RUB"], len(parts) > 1 ? parts[1] : "") ? 0.01m : 0.0001m}// A price split as dealers show it: { lead, big, frac }, the two pip digits big and the// fractional pip after them: 1.08421 is "1.08", "42", "1" (with 4 pip and 5 shown decimals).fn currency_pair_card_split(text: str, pip_places: int, places: int) { let n = len(text) let extra = places > pip_places ? places - pip_places : 0 let end = n - extra { lead: slice(text, 0, end - 2), big: slice(text, end - 2, end), frac: slice(text, end) }}// A coordinate, to two decimals.fn currency_pair_card_at(n: num) -> str { let r = round(n * 100) / 100 r == floor(r) ? str(floor(r)) : str(r)}// The sparkline in a 200 by 48 box: { d, open_y }, the path and the open's height.fn currency_pair_card_spark(pts: list, open: float?) { let all = pts + (open != null ? [open] : []) let vmin = min(all) let vmax = max(all) let y = (v) => vmax == vmin ? 24 : 4 + (vmax - v) * 40 / (vmax - vmin) let n = len(pts) let d = "M" + join(range(n) | map((i) => `${currency_pair_card_at(i * 200 / (n - 1))},${currency_pair_card_at(y(pts[i]))}`), " L") { d, open_y: open != null ? currency_pair_card_at(y(open)) : null }}view CurrencyPairCard(pair: str, bid, ask, open = null, high = null, low = null, values: list? = null, name: str? = null, pip = null, decimals: int? = null, locale: str = "en-US", updated: str? = null, href: str? = null, id: str? = null, class: str? = null) { style global currency_pair_card_css let parts = split(upper(pair), "/") let size = pip != null ? dec(pip) : currency_pair_card_pip(pair) let pip_places = size >= 0.01m ? 2 : (size >= 0.001m ? 3 : 4) let places = decimals ?? pip_places + 1 let b = dec(bid) let a = dec(ask) let mid = (b + a) / 2 let o = open != null ? dec(open) : null let pips = o != null ? round((mid - o) / size, 1, "half_up") : null let pct = o != null && o != 0 ? round((mid - o) * 100 / o, 2, "half_up") : null let mood = pips == null || pips == 0 ? "flat" : (pips > 0 ? "up" : "down") let show = (d: dec) => format_number(round(d, places, "half_up"), locale, places) let price = (d: dec) => { let s = currency_pair_card_split(show(d), pip_places, places) <span.ui-fxpair-price aria-label={show(d)}> <span aria-hidden="true">{s.lead}</span><span.ui-fxpair-big aria-hidden="true">{s.big}</span>{s.frac != "" && <span.ui-fxpair-frac aria-hidden="true">{s.frac}</span>} </span> } let title = name ?? (len(parts) > 1 && currency_pair_card_names[parts[0]] != null && currency_pair_card_names[parts[1]] != null ? `${currency_pair_card_names[parts[0]]} / ${currency_pair_card_names[parts[1]]}` : null) let hi = high != null ? dec(high) : null let lo = low != null ? dec(low) : null let where = hi != null && lo != null && hi > lo ? float((mid < lo ? lo : (mid > hi ? hi : mid)) - lo) * 100 / float(hi - lo) : 50 let pts = values != null && len(values) > 1 ? values | map((v) => float(v)) : null let spark = pts != null ? currency_pair_card_spark(pts, o != null ? float(o) : null) : null <div class={ui_cls(["ui-fxpair", mood != "flat" && `ui-fxpair-${mood}`, class])} id={id} role="group" aria-label={`${pair} quote`}> <div.ui-fxpair-head> <h3.ui-fxpair-title> <span.ui-fxpair-flags aria-hidden="true">{currency_pair_card_flags[parts[0]] ?? ""}{len(parts) > 1 ? currency_pair_card_flags[parts[1]] ?? "" : ""}</span> <span>{href != null ? <a href={href}>{pair}</a> : pair}{title != null && <span.ui-fxpair-name>{title}</span>}</span> </h3> {pips != null && <span.ui-fxpair-chg data-pips={str(pips)}> <span aria-hidden="true">{mood == "up" ? "โ" : (mood == "down" ? "โ" : "โ")}</span> <span.ui-sr>{mood == "up" ? "up" : (mood == "down" ? "down" : "unchanged")}</span> {pips > 0 ? "+" : ""}{format_number(pips, locale, 1)} pips{pct != null && ` (${pct > 0 ? "+" : ""}${format_number(pct, locale, 2)}%)`} </span>} </div> <div.ui-fxpair-quotes> <div.ui-fxpair-side data-side="bid"><small>Sell <span.ui-sr>at the bid</span></small>{price(b)}</div> <div.ui-fxpair-spread><b>{format_number(round((a - b) / size, 1, "half_up"), locale, 1)}</b>spread</div> <div.ui-fxpair-side data-side="ask"><small>Buy <span.ui-sr>at the ask</span></small>{price(a)}</div> </div> {hi != null && lo != null && <div.ui-fxpair-range> <span>Low<b>{show(lo)}</b></span> <span.ui-fxpair-track role="img" aria-label={`Now ${round(where)}% of the way from the day's low to its high`}> <span.ui-fxpair-dot style={`left: ${currency_pair_card_at(where)}%`}></span> </span> <span>High<b>{show(hi)}</b></span> </div>} {spark != null && <svg.ui-fxpair-spark viewBox="0 0 200 48" preserveAspectRatio="none" aria-hidden="true"> {spark.open_y != null && <line x1="0" x2="200" y1={spark.open_y} y2={spark.open_y} vector-effect="non-scaling-stroke"/>} <path.ui-fxpair-area d={`${spark.d} L200,48 L0,48 Z`}/> <path d={spark.d} vector-effect="non-scaling-stroke"/> </svg>} {updated != null && <p.ui-fxpair-foot>{updated}</p>} </div>}Related
CurrencyStrengthThe relative strength of the major currencies as bars, strongest first, given or worked out from pairs' changes.7 views ยท 7 downloadsCurrencyWalletBalances in several currencies with the total in a base currency, each with its rate, its worth and share, and convert and send actions.9 views ยท 6 downloadsAccountCardA bank account at a glance: type, nickname, masked number, current and available balance, what is on hold, and quick actions.8 views ยท 7 downloadsAccountDetailsThe details to share to get paid: account name and number, IBAN in groups of four (checked), BIC, sort code or routing number, each with a copy button.6 views ยท 6 downloadsCardControlsA card's controls beside its masked face: freeze it, set daily spending and cash limits, and allow online, ATM, contactless and abroad use.7 views ยท 6 downloadsCreditScoreA credit score on a gauge of its bands from poor to excellent, the band it falls in, the change since the last report and the factors that move it.7 views ยท 8 downloads
Thread
Thread 0 posts
On CurrencyPairCard: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.