RiskMatrix
Risks on a 5 by 5 heat grid with keyboard placement: score, band, the mitigated position beside the inherent one, a sorted register, and a filter by cell.
Demo
Operational risks
| Likelihood by impact | Negligible1 | Minor2 | Moderate3 | Major4 | Severe5 |
|---|---|---|---|---|---|
| Almost certain5 | |||||
| Likely4 | |||||
| Possible3 | |||||
| Unlikely2 | |||||
| Rare1 |
- As it stands
- After mitigation
- Risks now to after:
- Low 0 to 0
- Medium 3 to 4
- High 1 to 1
- Critical 1 to 0
- Office flood: likelihood and impact must be whole numbers from 1 to 5, so it is left off the grid.
| Ref | Risk | Owner | As it stands | After mitigation | Move |
|---|---|---|---|---|---|
| R2 | Cloud region outage | Ben | 16 Critical | 9 Medium | |
| R1 | Customer data breach | Ana | 15 High | 8 Medium | |
| R4 | Lead engineer leaves | Dee | 9 Medium | 12 HighWorse than before: check this mitigation. | |
| R3 | Key vendor fails | Chi | 8 Medium | Not assessed | |
| R5 | Exchange rate swing | Eli | 5 Medium | 5 Medium |
Launch risks
| Likelihood by impact | Negligible1 | Minor2 | Moderate3 | Major4 | Severe5 |
|---|---|---|---|---|---|
| Almost certain5 | |||||
| Likely4 | |||||
| Possible3 | |||||
| Unlikely2 | |||||
| Rare1 |
- As it stands
- After mitigation
- Risks now to after:
- Accept 1 to 1
- Act 0 to 0
| Ref | Risk | Owner | As it stands | After mitigation | Move |
|---|---|---|---|---|---|
| R1 | Launch slips | 4 Accept | Not assessed |
Move a risk 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/risk_matrix.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/risk_matrix.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/risk_matrix.qwe
let risk_matrix_demo_risks = [ { id: "breach", title: "Customer data breach", owner: "Ana", likelihood: 3, impact: 5, mitigated_likelihood: 2, mitigated_impact: 4 }, { id: "outage", title: "Cloud region outage", owner: "Ben", likelihood: 4, impact: 4, mitigated_likelihood: 3, mitigated_impact: 3 }, { id: "vendor", title: "Key vendor fails", owner: "Chi", likelihood: 2, impact: 4 }, { id: "churn", title: "Lead engineer leaves", owner: "Dee", likelihood: 3, impact: 3, mitigated_likelihood: 4, mitigated_impact: 3 }, { id: "fx", title: "Exchange rate swing", owner: "Eli", likelihood: 5, impact: 1, mitigated_likelihood: 5, mitigated_impact: 1 }, { id: "flood", title: "Office flood", owner: "Ana", likelihood: 9, impact: 2 },]component RiskMatrixDemoLive() { state seen = "Move a risk to see on_change." <div.demo-stack> <RiskMatrix id="risk-matrix-demo-live" title="Launch risks" name="launch" bands={[{ name: "Accept", upto: 8 }, { name: "Act", upto: 25 }]} risks={[{ id: "late", title: "Launch slips", likelihood: 2, impact: 2 }]} on_change={(rows) => { seen = `on_change: ${rows[0].title} scores ${rows[0].score}, ${rows[0].band}.` }}/> <p.demo-note>{seen}</p> </div>}view RiskMatrixDemo() = <div.demo-stack> <RiskMatrix id="risk-matrix-demo" title="Operational risks" risks={risk_matrix_demo_risks}/> <RiskMatrixDemoLive/> </div>Source
ui/risk_matrix.qwe
// RiskMatrix: risks on a 5 by 5 grid of likelihood against impact, each with its// score and band, its mitigated position beside the inherent one, and a register// sorted worst first. Clicking a cell keeps the register to the risks in it. A risk// is moved with the keyboard as easily as with the mouse: Move picks it up, the// arrow keys carry it across the grid, Enter puts it down and Escape takes it back.//// <RiskMatrix risks={[{ title: "Data breach", likelihood: 3, impact: 5, mitigated_likelihood: 2, mitigated_impact: 4, owner: "Ana" }]}/>// <RiskMatrix risks={risks} bands={[{ name: "Accept", upto: 8 }, { name: "Act", upto: 25 }]}// on_change={(rows) => { saved = rows }}/>//// On the grid: the arrow keys go from cell to cell, Home and End to the ends of the// row, Enter or Space filters by the cell (again to stop), Escape clears the filter.//// risks: each { title, likelihood, impact }, those two whole numbers from 1 to 5, with// id, owner, mitigated_likelihood and mitigated_impact if known. A risk outside the// grid is left off it, and named in a message; a risk without both mitigated numbers// counts as not assessed. Up to 200 are taken. bands: the score bands, lowest first,// each { name, upto }, the last reaching 25 (Low to 4, Medium to 9, High to 15,// Critical to 25); the first four are coloured from calm to loud. likelihood_labels,// impact_labels: five names each for the grid's sides, lowest first. title: the// heading ("Risk matrix"). id: a prefix for its ids ("risk-matrix"), unique on the// page. Hidden fields named risk_id, likelihood, impact, mitigated_likelihood and// mitigated_impact hold each risk's place, in order, so a form around it sends them// (the mitigated two are empty when not assessed); name: a prefix for those names.// on_change(rows): inside one of your components, given every risk as { id, title,// owner, likelihood, impact, score, band, mitigated_likelihood, mitigated_impact,// mitigated_score, mitigated_band } after each move that is kept.//// risk_matrix_clean(risks) gives { rows, problems }, risk_matrix_band(score, bands)// the band, and risk_matrix_register(rows) the order, for a program's own use.import ui/theme.qwe use ui_clslet risk_matrix_css = css`.ui-risk-matrix { display: grid; gap: 14px; min-width: 0; 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-risk-matrix .ui-risk-matrix-title { display: block; margin: 0; font-size: 16px; font-weight: 650; }.ui-risk-matrix-grid { width: 100%; max-width: 640px; table-layout: fixed; border-collapse: separate; border-spacing: 3px; font-size: 12px; }.ui-risk-matrix-grid th { padding: 2px 4px; color: var(--ui-muted); font-size: 11px; font-weight: 500; overflow-wrap: anywhere; }.ui-risk-matrix-grid thead th { text-align: center; vertical-align: bottom; }.ui-risk-matrix-grid tbody th { width: 62px; text-align: right; }.ui-risk-matrix-short { display: none; }.ui-risk-matrix-cell { position: relative; height: 58px; padding: 16px 3px 3px; border-radius: var(--ui-radius-sm); vertical-align: top; cursor: pointer; }.ui-risk-matrix-cell:focus { outline: 2px solid var(--ui-accent); outline-offset: 1px; }.ui-risk-matrix-cell[aria-selected="true"] { box-shadow: inset 0 0 0 2px var(--ui-fg); }.ui-risk-matrix-b0 { background: var(--ui-success-soft); }.ui-risk-matrix-b1 { background: var(--ui-warning-soft); }.ui-risk-matrix-b2 { background: color-mix(in srgb, var(--ui-warning) 22%, var(--ui-warning-soft)); }.ui-risk-matrix-b3 { background: color-mix(in srgb, var(--ui-danger) 30%, var(--ui-danger-soft)); }.ui-risk-matrix-corner { position: absolute; top: 2px; right: 5px; color: var(--ui-fg); font-size: 10px; font-variant-numeric: tabular-nums; }.ui-risk-matrix-chips { display: flex; flex-wrap: wrap; gap: 2px; }.ui-risk-matrix-chip { padding: 0 4px; border: 1px solid var(--ui-fg); border-radius: 4px; background: var(--ui-fg); color: var(--ui-bg); font-size: 11px; font-weight: 600; line-height: 16px; }.ui-risk-matrix-after { background: transparent; color: var(--ui-fg); border-style: dashed; }.ui-risk-matrix-held { outline: 2px solid var(--ui-accent); outline-offset: 1px; }.ui-risk-matrix-key { display: flex; flex-wrap: wrap; gap: 4px 14px; margin: 0; padding: 0; list-style: none; color: var(--ui-muted); font-size: 12px; }.ui-risk-matrix-key li { display: inline-flex; align-items: center; gap: 6px; }.ui-risk-matrix-live { display: grid; gap: 6px; min-width: 0; }.ui-risk-matrix-say { margin: 0; color: var(--ui-muted); font-size: 13px; min-height: 19px; }.ui-risk-matrix-problems { display: grid; gap: 2px; margin: 0; padding: 0; list-style: none; color: var(--ui-danger); font-size: 13px; }.ui-risk-matrix-scroll { position: relative; min-width: 0; overflow-x: auto; }.ui-risk-matrix-scroll:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-risk-matrix-table { width: 100%; min-width: 620px; border-collapse: collapse; }.ui-risk-matrix-table th { padding: 6px 8px; border-bottom: 1px solid var(--ui-border); color: var(--ui-muted); font-size: 12px; font-weight: 600; text-align: left; white-space: nowrap; }.ui-risk-matrix-table td { padding: 6px 8px; border-bottom: 1px solid var(--ui-border); vertical-align: top; }.ui-risk-matrix-band { padding: 0 6px; border-radius: 4px; color: var(--ui-fg); font-size: 12px; font-weight: 600; white-space: nowrap; }.ui-risk-matrix-worse { display: block; color: var(--ui-danger); font-size: 12px; }.ui-risk-matrix-muted { color: var(--ui-muted); }.ui-risk-matrix-actions { display: flex; flex-wrap: wrap; gap: 4px; }.ui-risk-matrix-button { min-height: 28px; padding: 0 8px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 12px var(--ui-font); white-space: nowrap; cursor: pointer; }.ui-risk-matrix-button:hover { border-color: var(--ui-strong); }.ui-risk-matrix-button:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-risk-matrix-button[aria-pressed="true"] { border-color: var(--ui-accent); color: var(--ui-accent); }.ui-risk-matrix-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }@media (max-width: 480px) { .ui-risk-matrix-grid tbody th { width: 16px; } .ui-risk-matrix-cell { height: 48px; } .ui-risk-matrix-long { display: none; } .ui-risk-matrix-short { display: inline; } }`let risk_matrix_bands = [{ name: "Low", upto: 4 }, { name: "Medium", upto: 9 }, { name: "High", upto: 15 }, { name: "Critical", upto: 25 }]let risk_matrix_likelihoods = ["Rare", "Unlikely", "Possible", "Likely", "Almost certain"]let risk_matrix_impacts = ["Negligible", "Minor", "Moderate", "Major", "Severe"]let risk_matrix_cap = 200// A place on a side of the grid: a whole number from 1 to 5, as a number or its text, else null.fn risk_matrix_side(v) { let t = trim(str(v ?? "")) re.test(t, "^[1-5]$") ? int(t) : null}fn risk_matrix_band(score: int, bands: list) -> map { let at = find(range(len(bands)), (i) => score <= bands[i].upto) ?? len(bands) - 1 { name: bands[at].name, level: min([at, 3]) }}// The risks given to rows of this component's own, and what is wrong with those it left out.fn risk_matrix_clean(risks: list) -> map { let taken = slice(risks, 0, risk_matrix_cap) let read = for n in range(len(taken)) { let r = taken[n] let label = type_of(r) == "map" ? (filled(trim(str(r.title ?? ""))) ?? `Risk ${n + 1}`) : `Item ${n + 1}` if type_of(r) != "map" { { problem: `${label} is not a risk, so it is left off the grid.` } } else { let l = risk_matrix_side(r.likelihood) let i = risk_matrix_side(r.impact) let ml = risk_matrix_side(r.mitigated_likelihood) let mi = risk_matrix_side(r.mitigated_impact) let known = ml != null && mi != null if l == null || i == null { { problem: `${label}: likelihood and impact must be whole numbers from 1 to 5, so it is left off the grid.` } } else { { id: str(r.id ?? `r${n + 1}`), mark: `R${n + 1}`, title: label, owner: str(r.owner ?? ""), l, i, ml: known ? ml : null, mi: known ? mi : null } } } } let over = len(risks) > risk_matrix_cap ? [`Only the first ${risk_matrix_cap} of ${len(risks)} risks are shown.`] : [] { rows: read | filter((r) => r.problem == null), problems: (read | filter((r) => r.problem != null) | map((r) => r.problem)) + over }}fn risk_matrix_after(r: map) -> int? = r.ml == null ? null : r.ml * r.mi// Worst first: by score, then by what is left after mitigation, then by title.fn risk_matrix_register(rows: list) -> list = rows | sort_by((r) => r.title) | sort_by((r) => -(risk_matrix_after(r) ?? r.l * r.i)) | sort_by((r) => -(r.l * r.i))// The risks standing in a cell: { mark, title, after }, inherent ones first.fn risk_matrix_in_cell(rows: list, l: int, i: int) -> list { let here = rows | filter((r) => r.l == l && r.i == i) | map((r) => ({ id: r.id, mark: r.mark, title: r.title, after: false })) let later = rows | filter((r) => r.ml == l && r.mi == i) | map((r) => ({ id: r.id, mark: r.mark, title: r.title, after: true })) here + later}fn risk_matrix_cell_text(rows: list, l: int, i: int, bands: list) -> str { let found = risk_matrix_in_cell(rows, l, i) let names = found | map((f) => f.after ? `${f.title} after mitigation` : f.title) `Likelihood ${l}, impact ${i}, score ${l * i}, ${risk_matrix_band(l * i, bands).name}. ${len(found) == 0 ? "No risks." : `${len(found) == 1 ? "1 risk" : `${len(found)} risks`}: ${join(names, ", ")}.`}`}fn risk_matrix_clamp(n: int) -> int = n < 1 ? 1 : (n > 5 ? 5 : n)fn risk_matrix_public(rows: list, bands: list) -> list = rows | map((r) => { let after = risk_matrix_after(r) { id: r.id, title: r.title, owner: r.owner, likelihood: r.l, impact: r.i, score: r.l * r.i, band: risk_matrix_band(r.l * r.i, bands).name, mitigated_likelihood: r.ml, mitigated_impact: r.mi, mitigated_score: after, mitigated_band: after == null ? null : risk_matrix_band(after, bands).name } })// How many risks sit in each band, before and after mitigation; a risk not assessed stays where it is.fn risk_matrix_tally(rows: list, bands: list) -> list = for b in range(len(bands)) { let name = bands[b].name let band_of = (s: int) => risk_matrix_band(s, bands).name { name, level: min([b, 3]), before: len(rows | filter((r) => band_of(r.l * r.i) == name)), after: len(rows | filter((r) => band_of(risk_matrix_after(r) ?? r.l * r.i) == name)) } }// The matrix an event came from, and a cell in it. Top-level functions, not a component's// `let`s: a `let` that touches the page is waited for, which would move the focus only once// the handler is over, too late for a key pressed straight after.fn risk_matrix_root(e) = e.target.closest(".ui-risk-matrix")fn risk_matrix_cell(root, l: int, i: int) = root.querySelector(`[data-l="${l}"][data-i="${i}"]`)component RiskMatrix(risks: list = [], bands: list = risk_matrix_bands, likelihood_labels: list = risk_matrix_likelihoods, impact_labels: list = risk_matrix_impacts, title: str = "Risk matrix", id: str = "risk-matrix", name: str? = null, on_change = null, class: str? = null) { style global risk_matrix_css state rows = risk_matrix_clean(risks).rows state problems = risk_matrix_clean(risks).problems state at_l = 3 state at_i = 3 state only = null state held = null state said = "" let named = (n: str) => name != null ? `${name}_${n}` : n let sorted = risk_matrix_register(rows) let shown = only == null ? sorted : sorted | filter((r) => (r.l == only.l && r.i == only.i) || (r.ml == only.l && r.mi == only.i)) let tally = risk_matrix_tally(rows, bands) let move_held = (l: int, i: int) => { rows = rows | map((r) => r.id != held.id ? r : (held.which == "after" ? { ...r, ml: l, mi: i } : { ...r, l, i })) } let go = (e, l: int, i: int) => { at_l = l at_i = i if held != null { move_held(l, i) } risk_matrix_cell(risk_matrix_root(e), l, i).focus() } let pick_up = (e, r: map, which: str) => { let l = which == "after" ? (r.ml ?? r.l) : r.l let i = which == "after" ? (r.mi ?? r.i) : r.i only = null held = { id: r.id, which, title: r.title, from_l: which == "after" ? r.ml : r.l, from_i: which == "after" ? r.mi : r.i } at_l = l at_i = i move_held(l, i) said = `Moving ${r.title}. Arrow keys to move, Enter to place, Escape to cancel.` risk_matrix_cell(risk_matrix_root(e), l, i).focus() } let let_go = (e) => { let key_of = `${held.id}-${held.which}` held = null risk_matrix_root(e).querySelector(`[data-move="${key_of}"]`).focus() } let put_down = (e, l: int, i: int) => { move_held(l, i) let s = l * i said = `${held.title} placed at likelihood ${l}, impact ${i}: score ${s}, ${risk_matrix_band(s, bands).name}.` let_go(e) if on_change != null { on_change(risk_matrix_public(rows, bands)) } } let cancel = (e) => { let h = held rows = rows | map((r) => r.id != h.id ? r : (h.which == "after" ? { ...r, ml: h.from_l, mi: h.from_i } : { ...r, l: h.from_l, i: h.from_i })) said = `${h.title} left where it was.` let_go(e) } let toggle = (l: int, i: int) => { let same = only != null && only.l == l && only.i == i only = same ? null : { l, i } said = same ? "Showing every risk." : `Showing ${len(risk_matrix_in_cell(rows, l, i))} in likelihood ${l}, impact ${i}.` } let act = (e, l: int, i: int) => { at_l = l at_i = i if held != null { put_down(e, l, i) } else { toggle(l, i) } } let press = (e, l: int, i: int) => { let k = e.key let to = k == "ArrowUp" ? [l + 1, i] : (k == "ArrowDown" ? [l - 1, i] : (k == "ArrowLeft" ? [l, i - 1] : (k == "ArrowRight" ? [l, i + 1] : (k == "Home" ? [l, 1] : (k == "End" ? [l, 5] : null))))) if to != null { e.preventDefault() go(e, risk_matrix_clamp(to[0]), risk_matrix_clamp(to[1])) } else if k == "Enter" || k == " " { e.preventDefault() act(e, l, i) } else if k == "Escape" { if held != null { cancel(e) } else if only != null { only = null; said = "Showing every risk." } } } <section class={ui_cls(["ui-risk-matrix", class])} aria-labelledby={`${id}-title`}> <h3.ui-risk-matrix-title id={`${id}-title`}>{title}</h3> <table.ui-risk-matrix-grid role="grid" aria-label="Risks by likelihood and impact"> <thead> <tr><th scope="col"><span.ui-sr>Likelihood by impact</span></th>{for i in [1, 2, 3, 4, 5] => <th scope="col"><span.ui-risk-matrix-long>{impact_labels[i - 1]}</span><span.ui-risk-matrix-short>{i}</span></th>}</tr> </thead> <tbody> {for l in [5, 4, 3, 2, 1] => <tr> <th scope="row"><span.ui-risk-matrix-long>{likelihood_labels[l - 1]}</span><span.ui-risk-matrix-short>{l}</span></th> {for i in [1, 2, 3, 4, 5] { let found = risk_matrix_in_cell(rows, l, i) let tone = risk_matrix_band(l * i, bands).level <td role="gridcell" data-l={l} data-i={i} class={ui_cls(["ui-risk-matrix-cell", `ui-risk-matrix-b${tone}`])} tabindex={l == at_l && i == at_i ? "0" : "-1"} aria-label={risk_matrix_cell_text(rows, l, i, bands)} aria-selected={only != null && only.l == l && only.i == i ? "true" : "false"} @click={(e) => { act(e, l, i) }} @keydown={(e) => { press(e, l, i) }} @focus={() => { at_l = l; at_i = i }}> <span.ui-risk-matrix-corner aria-hidden="true">{l * i}</span> <span.ui-risk-matrix-chips aria-hidden="true"> {for f in found => <span key={`${f.id}-${f.after}`} title={f.after ? `${f.title}, after mitigation` : f.title} class={ui_cls(["ui-risk-matrix-chip", f.after && "ui-risk-matrix-after", held != null && held.id == f.id && (held.which == "after") == f.after && "ui-risk-matrix-held"])}>{f.mark}</span>} </span> </td> }} </tr>} </tbody> </table> <ul.ui-risk-matrix-key> <li><span.ui-risk-matrix-chip aria-hidden="true">R1</span> As it stands</li> <li><span.ui-risk-matrix-chip.ui-risk-matrix-after aria-hidden="true">R1</span> After mitigation</li> <li>Risks now to after:</li> {for t in tally => <li><span class={ui_cls(["ui-risk-matrix-band", `ui-risk-matrix-b${t.level}`])}>{t.name}</span> <span data-k={`tally-${t.level}`}>{t.before} to {t.after}</span></li>} </ul> <div.ui-risk-matrix-live> <p.ui-risk-matrix-say aria-live="polite">{said}</p> {len(problems) > 0 && <ul.ui-risk-matrix-problems role="alert">{for p in problems => <li>{p}</li>}</ul>} </div> <div hidden={true}> {for r in rows => <span key={r.id}> <input type="hidden" name={named("risk_id")} value={r.id}/> <input type="hidden" name={named("likelihood")} value={r.l}/> <input type="hidden" name={named("impact")} value={r.i}/> <input type="hidden" name={named("mitigated_likelihood")} value={r.ml ?? ""}/> <input type="hidden" name={named("mitigated_impact")} value={r.mi ?? ""}/> </span>} </div> <div.ui-risk-matrix-bar> <strong>{only == null ? `${len(rows) == 1 ? "1 risk" : `${len(rows)} risks`}, worst first` : `${len(shown)} of ${len(rows)} in likelihood ${only.l}, impact ${only.i}`}</strong> {only != null && <button.ui-risk-matrix-button type="button" @click={() => { only = null; said = "Showing every risk." }}>Show all</button>} </div> <div.ui-risk-matrix-scroll tabindex="0" role="group" aria-label="Risk register"> <table.ui-risk-matrix-table> <thead><tr><th>Ref</th><th>Risk</th><th>Owner</th><th>As it stands</th><th>After mitigation</th><th>Move</th></tr></thead> <tbody> {for r in shown { let score = r.l * r.i let after = risk_matrix_after(r) let band = risk_matrix_band(score, bands) <tr key={r.id} data-risk={r.id}> <td>{r.mark}</td> <td>{r.title}</td> <td>{r.owner}</td> <td data-k="now">{score} <span class={ui_cls(["ui-risk-matrix-band", `ui-risk-matrix-b${band.level}`])}>{band.name}</span></td> <td data-k="then"> {after == null ? <span.ui-risk-matrix-muted>Not assessed</span> : <span>{after} <span class={ui_cls(["ui-risk-matrix-band", `ui-risk-matrix-b${risk_matrix_band(after, bands).level}`])}>{risk_matrix_band(after, bands).name}</span></span>} {after != null && after > score && <span.ui-risk-matrix-worse>Worse than before: check this mitigation.</span>} </td> <td><span.ui-risk-matrix-actions> <button.ui-risk-matrix-button type="button" data-move={`${r.id}-now`} aria-pressed={held != null && held.id == r.id && held.which == "now" ? "true" : "false"} aria-label={`Move ${r.title}`} @click={(e) => { pick_up(e, r, "now") }}>Move</button> <button.ui-risk-matrix-button type="button" data-move={`${r.id}-after`} aria-pressed={held != null && held.id == r.id && held.which == "after" ? "true" : "false"} aria-label={`Move ${r.title} after mitigation`} @click={(e) => { pick_up(e, r, "after") }}>Move mitigated</button> </span></td> </tr> }} </tbody> </table> </div> {len(rows) == 0 && <p.ui-risk-matrix-say>No risks to show.</p>} </section>}Related
AccessCheckerWorks out allow or deny from roles, grants and denies given as data, names the rule that decided, and shows a matrix of everything.2 views · 6 downloadsAlertRuleBuilderBacktests an alert rule on a sample series, drawing each firing and warning of a rule that flaps or never fires.3 views · 8 downloadsAnomalyFinderFlags outliers by median absolute deviation, gaps and level shifts in a time series, plots them, and says why each was flagged.3 views · 6 downloadsAutoInsightsReads rows and says in plain sentences what stands out, ranked by strength, and says nothing rather than something weak.2 views · 5 downloadsCloudCostEstimatorPrices cloud resources through graduated tiers, compares reserved with on-demand, and says what drives the bill most.3 views · 8 downloadsCohortTableSignup and activity events to a retention triangle by week or month, heat-coloured, with a weighted average and unfinished periods left blank.2 views · 8 downloads
Thread
Thread 0 posts
On RiskMatrix: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.