MarginCalc
Cost, price, margin and markup bound together: edit any and the rest follow from the two set last, with a price for a target margin and a warning under cost.
Demo
Margin and markup
- Profit on each
- $7.59
- Margin on the price
- 37.97%
- Markup on the cost
- 61.21%
Price for 45%: $22.55.
Wholesale, in euros
- Profit on each
- €26.67
- Margin on the price
- 25.00%
- Markup on the cost
- 33.34%
Give a target margin to see the price that reaches it.
Change a figure to see on_change.
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/margin_calc.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/margin_calc.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/margin_calc.qwe
component MarginCalcDemoLive() { state seen = null <div.demo-stack> <MarginCalc id="margin-calc-demo-live" title="Wholesale, in euros" currency="EUR" name="trade" cost="80" margin="25" on_change={(f) => { seen = f }}/> <p.demo-note>{seen == null ? "Change a figure to see on_change." : `on_change: price ${seen.price}, profit ${seen.profit}.`}</p> </div>}view MarginCalcDemo() = <div.demo-stack> <MarginCalc id="margin-calc-demo" cost="12.40" price="19.99" target={45}/> <MarginCalcDemoLive/> </div>Source
ui/margin_calc.qwe
// MarginCalc: cost, price, margin and markup bound together. Edit any of them and// the rest follow from the two set last, which are marked, so changing the price// with the cost set moves the margin, and changing the margin with the price set// moves the cost. Margin and markup say the same thing, so they are never the// two: setting one after the other keeps the last amount set beside it. A target// margin gives the price that reaches it, rounded up so it is not missed, and a// price under cost says what each sale loses. Everything is a decimal, the amounts// rounded half up to the currency's cents, and the percentages are worked out// from those rounded amounts, so what shows adds up.//// <MarginCalc cost={12.40} price={19.99} target={45}/>// <MarginCalc currency="EUR" cost="80" margin="25" on_change={(f) => { price = f.price }}/>//// cost, price, margin, markup: where the fields start (numbers, decimals or text,// margin and markup in percent); the first two given are the two set. target: the// target margin in percent, with its own field (left out, empty). currency: ISO// code ("USD"). locale: how to write amounts (the program's). title: the heading// ("Margin and markup"). id: a prefix for its fields' ids ("margin-calc"), unique// on the page. The fields are named cost, price, margin, markup and target_margin,// so a form around it sends them; name: a prefix for those names. on_change(figures):// inside one of your components, given { cost, price, margin, markup, profit } as// decimals (margin null at a price of 0, markup null at a cost of 0) after each// change that gives a whole answer.//// margin_calc_solve(a, b, values, minor) is the arithmetic, for a program's own use:// a and b name the two set of "cost", "price", "margin" and "markup", values holds// them as decimals and minor is the currency's decimals. It gives { cost, price,// margin, markup, profit, error }. margin_calc_target(cost, target, minor) is the// price for a target margin.import ui/theme.qwe use ui_clslet margin_calc_css = css`.ui-margincalc { display: grid; gap: 16px; min-width: 0; max-width: 560px; padding: 20px 22px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.45 var(--ui-font); }.ui-margincalc .ui-margincalc-title { display: block; margin: 0; font-size: 16px; font-weight: 650; }.ui-margincalc-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 14px; }.ui-margincalc-field { display: grid; gap: 5px; min-width: 0; }.ui-margincalc-field label { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; font-size: 13px; font-weight: 500; }.ui-margincalc-tag { color: var(--ui-muted); font-size: 11px; font-weight: 500; }.ui-margincalc-set .ui-margincalc-tag { color: var(--ui-accent); font-weight: 650; }.ui-margincalc-box { box-sizing: border-box; display: flex; align-items: center; gap: 6px; min-width: 0; height: 38px; padding: 0 10px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); transition: border-color .15s, box-shadow .15s; }.ui-margincalc-set .ui-margincalc-box { background: var(--ui-subtle); }.ui-margincalc-box:hover { border-color: var(--ui-strong); }.ui-margincalc-box:focus-within { border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-margincalc-box input { flex: 1; width: 0; min-width: 0; height: 100%; padding: 0; border: 0; outline: none; background: transparent; color: var(--ui-fg); font: 14px var(--ui-font); font-variant-numeric: tabular-nums; text-align: right; }.ui-margincalc-box span { flex: none; color: var(--ui-muted); font-size: 12px; font-weight: 600; }.ui-margincalc-bad .ui-margincalc-box { border-color: var(--ui-danger); }.ui-margincalc-out { display: grid; margin: 0; padding: 4px 16px; border-radius: var(--ui-radius-sm); background: var(--ui-subtle); }.ui-margincalc-out div { display: flex; justify-content: space-between; gap: 16px; padding: 8px 0; border-bottom: 1px solid var(--ui-border); }.ui-margincalc-out div:last-child { border-bottom: 0; }.ui-margincalc-out dt { font-weight: 500; }.ui-margincalc-out dd { margin: 0; text-align: right; font-weight: 650; font-variant-numeric: tabular-nums; }.ui-margincalc-target { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px 14px; align-items: end; }.ui-margincalc-use { min-height: 38px; padding: 0 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 500 13px var(--ui-font); cursor: pointer; }.ui-margincalc-use:hover:not(:disabled) { border-color: var(--ui-accent); color: var(--ui-accent); }.ui-margincalc-use:focus-visible { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-margincalc-use:disabled { opacity: .55; cursor: not-allowed; }.ui-margincalc-say { grid-column: 1 / -1; margin: 0; color: var(--ui-muted); font-size: 13px; }.ui-margincalc-note { margin: 0; color: var(--ui-muted); font-size: 13px; }.ui-margincalc-warn { margin: 0; padding: 8px 12px; border-radius: var(--ui-radius-sm); background: var(--ui-warning-soft); color: var(--ui-warning); font-size: 13px; font-weight: 500; }.ui-margincalc-error { margin: 0; color: var(--ui-danger); font-size: 13px; }@media (max-width: 420px) { .ui-margincalc { padding: 16px; } .ui-margincalc-grid, .ui-margincalc-target { grid-template-columns: 1fr; } }@media (prefers-reduced-motion: reduce) { .ui-margincalc-box { transition: none; } }`let margin_calc_fields = [ { id: "cost", label: "Cost", ratio: false }, { id: "price", label: "Price", ratio: false }, { id: "margin", label: "Margin", ratio: true }, { id: "markup", label: "Markup", ratio: true }]// What was typed as a decimal, null when blank, or an error value. "1,234.50" is// read; "12,50" is not guessed at, and neither is the exponent dec() would take.fn margin_calc_read(text) { let t = re.replace(trim(str(text ?? "")), "[\\s%]", "") if t == "" { return null } let plain = re.test(t, "^[+-]?\\d{1,3}(,\\d{3})+(\\.\\d*)?$") ? re.replace(t, ",", "") : t if !re.test(plain, "^[+-]?(\\d+\\.?\\d*|\\.\\d+)$") { return err("is not a number") } // Past this the arithmetic could overflow a decimal, and no price is that large. if len(plain) > 20 { return err("is too large") } let d = dec(plain) if is_err(d) || abs(d) > 1000000000000m { return err("is too large") } d}// The two fields set after `field` is edited: it and the one set before, but for// margin and markup, which only say one thing between them: then the amount stays.fn margin_calc_pair(field: str, a: str, b: str) { let ratio = (f) => f == "margin" || f == "markup" if field == a { return [a, b] } if field == b { return [b, a] } ratio(field) && ratio(a) ? [field, b] : [field, a]}// The currency's code, or USD for one money does not know, which would stop the sums.fn margin_calc_code(currency: str) -> str { let code = upper(trim(currency)) is_err(money.minor_units(code)) ? "USD" : code}// The price that gives a margin in percent, exact but for a division's last digit.fn margin_calc_price_for(cost, margin) = cost * 100 / (100 - margin)// The whole answer from two values: see the top of the file.fn margin_calc_solve(a: str, b: str, values, minor: int) { let given = (f) => a == f || b == f ? values[f] : null let c = given("cost") let p = given("price") let m = given("margin") let k = given("markup") let out = (cost, price) => { let rc = round(cost, minor, "half_up") let rp = round(price, minor, "half_up") { cost: rc, price: rp, profit: rp - rc, error: null, margin: rp == 0 ? null : (rp - rc) * 100 / rp, markup: rc == 0 ? null : (rp - rc) * 100 / rc } } let wrong = (text) => ({ error: text }) if c != null && c < 0 { return wrong("A cost cannot be below 0.") } if p != null && p < 0 { return wrong("A price cannot be below 0.") } if c != null && p != null { return out(c, p) } if c != null && m != null { if m >= 100 { return wrong(c == 0 ? "With a cost of 0 the margin is 100% at any price: set the price instead." : "A margin of 100% or more cannot be reached with a cost above 0.") } if m > 99.9999m { return wrong("That margin is too close to 100% to give a price.") } return out(c, margin_calc_price_for(c, m)) } if c != null && k != null { if k < -100 { return wrong("A markup below -100% would need a price below 0.") } return out(c, c * (100 + k) / 100) } if p != null && m != null { if m > 100 { return wrong("A margin above 100% would need a cost below 0.") } return out(p * (100 - m) / 100, p) } if p != null && k != null { if k <= -100 { return wrong(p == 0 ? "With a price of 0 and that markup any cost fits: set the cost instead." : "A markup of -100% or less cannot give a price above 0.") } return out(p * 100 / (100 + k), p) } null}// The price for a target margin, rounded up to the cent so the margin is reached.fn margin_calc_target(cost, target, minor: int) { if target >= 100 { return err("A target of 100% or more cannot be reached with a cost above 0.") } if target > 99.9999m { return err("That target is too close to 100% to give a price.") } round(margin_calc_price_for(cost, target), minor, "ceiling")}// Reads the texts and answers for them: { answer, bad: { field: message } }.fn margin_calc_work(texts, pair, minor: int) { let read = reduce(margin_calc_fields, {}, (acc, f) => set(acc, f.id, margin_calc_read(texts[f.id]))) let bad = reduce(pair, {}, (acc, f) => is_err(read[f]) ? set(acc, f, read[f].error) : acc) if len(bad) > 0 || read[pair[0]] == null || read[pair[1]] == null { return { answer: null, bad } } { answer: margin_calc_solve(pair[0], pair[1], read, minor), bad }}// The texts with the two fields not set written from the answer, blank without one.fn margin_calc_fill(texts, pair, minor: int) { let w = margin_calc_work(texts, pair, minor) let a = w.answer != null && w.answer.error == null ? w.answer : null let text = (x, digits: int) => x == null ? "" : str(round(x, digits, "half_up")) reduce(margin_calc_fields, texts, (acc, f) => { if contains(pair, f.id) { return acc } set(acc, f.id, a == null ? "" : text(a[f.id], f.ratio ? 2 : minor)) })}// Which two start set, the later first, as it would be had they been typed: the// first two given, an amount always among them.fn margin_calc_first(texts) { let given = margin_calc_fields | filter((f) => trim(texts[f.id]) != "") | map((f) => f.id) let amounts = given | filter((f) => f == "cost" || f == "price") if len(given) < 2 { return ["price", "cost"] } if len(amounts) == 0 { return [given[0], "cost"] } [first(given | filter((f) => f != amounts[0])), amounts[0]]}component MarginCalc(cost = null, price = null, margin = null, markup = null, target = null, currency: str = "USD", locale: str? = null, title: str = "Margin and markup", id: str = "margin-calc", name: str? = null, on_change = null, class: str? = null) { style global margin_calc_css let code = margin_calc_code(currency) let minor = money.minor_units(code) let start = { cost: str(cost ?? ""), price: str(price ?? ""), margin: str(margin ?? ""), markup: str(markup ?? "") } state pair = margin_calc_first(start) state texts = margin_calc_fill(start, margin_calc_first(start), minor) state goal = str(target ?? "") let named = (n: str) => name != null ? `${name}_${n}` : n let shown = (x) => { let m = money.of(x, code, "half_up") is_err(m) ? "a very large amount" : locale != null ? money.format(m, locale) : money.format(m) } let work = margin_calc_work(texts, pair, minor) let a = work.answer != null && work.answer.error == null ? work.answer : null let fault = work.answer != null ? work.answer.error : null let want = margin_calc_read(goal) let goal_bad = is_err(want) let spent = a != null ? a.cost : margin_calc_read(texts.cost) let aim = !goal_bad && want != null && spent != null && !is_err(spent) && spent >= 0 ? margin_calc_target(spent, want, minor) : null let tell = (t, p) => { let w = margin_calc_work(t, p, minor) if on_change != null && w.answer != null && w.answer.error == null { let g = w.answer on_change({ cost: g.cost, price: g.price, margin: g.margin, markup: g.markup, profit: g.profit }) } } let put = (p, field: str, text: str) => { let t = margin_calc_fill(set(texts, field, text), p, minor) pair = p texts = t tell(t, p) } let change = (field: str, text: str) => { put(margin_calc_pair(field, pair[0], pair[1]), field, text) } // What the rounding did to a margin or markup that was set. let drift = () => { if a == null { return null } let off = (f: str, x) => contains(pair, f) && x != null && round(x, 2, "half_up") != round(dec(margin_calc_read(texts[f])), 2, "half_up") if off("margin", a.margin) { return `Rounded to the cent, this price gives a margin of ${str(round(a.margin, 2, "half_up"))}%.` } if off("markup", a.markup) { return `Rounded to the cent, this price gives a markup of ${str(round(a.markup, 2, "half_up"))}%.` } null } <section class={ui_cls(["ui-margincalc", class])} aria-labelledby={`${id}-title`}> <h3.ui-margincalc-title id={`${id}-title`}>{title}</h3> <div.ui-margincalc-grid> {for f in margin_calc_fields => <div key={f.id} class={ui_cls(["ui-margincalc-field", contains(pair, f.id) && "ui-margincalc-set", has(work.bad, f.id) && "ui-margincalc-bad"])}> <label for={`${id}-${f.id}`}>{f.label}<span.ui-margincalc-tag>{contains(pair, f.id) ? "set" : "worked out"}</span></label> <div.ui-margincalc-box> <input type="text" inputmode="decimal" id={`${id}-${f.id}`} name={named(f.id)} value={texts[f.id]} placeholder="0" autocomplete="off" aria-invalid={has(work.bad, f.id) ? "true" : null} @input={(e) => { change(f.id, e.value) }}/> <span aria-hidden="true">{f.ratio ? "%" : code}</span> </div> </div>} </div> <div aria-live="polite"> {len(work.bad) > 0 && <p.ui-margincalc-error role="alert">{work.bad | keys | map((f) => `${f == "cost" ? "Cost" : f == "price" ? "Price" : f == "margin" ? "Margin" : "Markup"} ${work.bad[f]}.`) | join(" ")}</p>} {fault != null && <p.ui-margincalc-error role="alert">{fault}</p>} {len(work.bad) == 0 && work.answer == null && <p.ui-margincalc-note>Set any two of the four, one of them an amount.</p>} {a != null && a.profit < 0 && <p.ui-margincalc-warn>Under cost: each sale loses {shown(0m - a.profit)}.</p>} {a != null && a.profit == 0 && <p.ui-margincalc-warn>At cost: each sale makes nothing.</p>} {a != null && drift() != null && <p.ui-margincalc-note>{drift()}</p>} </div> {a != null && <dl.ui-margincalc-out> <div.ui-margincalc-profit><dt>Profit on each</dt><dd>{shown(a.profit)}</dd></div> <div.ui-margincalc-margin><dt>Margin on the price</dt><dd>{a.margin == null ? "No price" : `${str(round(a.margin, 2, "half_up"))}%`}</dd></div> <div.ui-margincalc-markup><dt>Markup on the cost</dt><dd>{a.markup == null ? "No cost" : `${str(round(a.markup, 2, "half_up"))}%`}</dd></div> </dl>} <div.ui-margincalc-target> <div class={ui_cls(["ui-margincalc-field", goal_bad && "ui-margincalc-bad"])}> <label for={`${id}-target`}>Target margin</label> <div.ui-margincalc-box> <input type="text" inputmode="decimal" id={`${id}-target`} name={named("target_margin")} value={goal} placeholder="0" autocomplete="off" aria-invalid={goal_bad ? "true" : null} @input={(e) => { goal = e.value }}/> <span aria-hidden="true">%</span> </div> </div> <button.ui-margincalc-use type="button" disabled={aim == null || is_err(aim)} @click={() => { put(["price", "cost"], "price", str(aim)) }}>Use this price</button> <p.ui-margincalc-say aria-live="polite"> {goal_bad ? "Target margin is not a number." : want == null ? "Give a target margin to see the price that reaches it." : spent == null || is_err(spent) ? "Set a cost to see the price for this target." : spent < 0 ? "A cost cannot be below 0." : is_err(aim) ? aim.error : `Price for ${str(want)}%: ${shown(aim)}.`} </p> </div> </section>}Related
BundleBuilderPicks a bundle from groups with minimums and maximums: tiered discount, the cheapest way to the next tier, the saving over buying alone, and totals exact to the cent.4 views · 6 downloadsPlanAdvisorUsage sliders against plans with included amounts, overage and an annual discount: the actual cost of each, the cheapest, and at what usage the answer changes.5 views · 5 downloadsProductConfiguratorBuilds a product from option groups with requires and excludes rules: clashes disabled with the reason, what a change dropped explained, a live price and what is missing.3 views · 5 downloadsProrationCalcWorks out a mid-period change of plan by the day: credit for the time left, charge for the new plan and what is due, across month lengths, leap years and downgrades.2 views · 8 downloadsBestCouponWorks out the lowest total from several coupons, trying each combination and order under their rules, and explains the winner and why each other coupon lost.3 views · 7 downloadsBoxPackerPicks the smallest box that takes items with rotation, or splits across boxes, works out dimensional and billable weight, and says when an item fits none.4 views · 7 downloads
Thread
Thread 0 posts
On MarginCalc: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.