From b1e50d8930a52ba55ca95efab36df8dd319d48a1 Mon Sep 17 00:00:00 2001 From: Henry Haverinen Date: Wed, 23 Sep 2026 15:20:27 +0300 Subject: [PATCH] Add custom risk levels to the risk matrix widget A risk method can now give its own risk levels and a level grid, so the matrix colours follow the method instead of the position of each cell. Levels pick steps of a fixed five-step colour scale (green, light green, amber, red, dark red) and are listed in a legend. Cells without a valid level are drawn in grey with a warning. Without the new parameters the matrix looks as before. Co-Authored-By: Claude Opus 5.5 --- .cards/local/reports/riskMatrixWidget.json | 2 +- .../reports/riskMatrixWidget/index.adoc.hbs | 69 +++++++++++++- .../riskMatrixWidget/parameterSchema.json | 39 ++++++++ .../base_4c0lf8u0/c/base_kxwagxhg/index.adoc | 95 ++++++++++++++++++- 4 files changed, 197 insertions(+), 8 deletions(-) diff --git a/.cards/local/reports/riskMatrixWidget.json b/.cards/local/reports/riskMatrixWidget.json index 0cc636f..1d1a6cc 100644 --- a/.cards/local/reports/riskMatrixWidget.json +++ b/.cards/local/reports/riskMatrixWidget.json @@ -1,6 +1,6 @@ { "displayName": "Risk matrix widget", - "description": "A likelihood by impact risk matrix, banded green, amber and red", + "description": "A likelihood by impact risk matrix, banded green, amber and red, or coloured by the risk levels of your own method", "category": "General", "name": "base/reports/riskMatrixWidget" } diff --git a/.cards/local/reports/riskMatrixWidget/index.adoc.hbs b/.cards/local/reports/riskMatrixWidget/index.adoc.hbs index bbfbff6..dd78b48 100644 --- a/.cards/local/reports/riskMatrixWidget/index.adoc.hbs +++ b/.cards/local/reports/riskMatrixWidget/index.adoc.hbs @@ -1,7 +1,16 @@ {{#each cells}}{{#if (ne (add (max this.likelihood ../likelihood.length) (max this.impact ../impact.length)) (add ../likelihood.length ../impact.length))}} WARNING: A cell at likelihood {{this.likelihood}}, impact {{this.impact}} falls outside this matrix, which has {{../likelihood.length}} likelihood levels and {{../impact.length}} impact levels. Its count of {{this.count}} is not shown. -{{/if}}{{/each}}{{#vegaLite}} +{{/if}}{{/each}}{{#if levelGrid}}{{#if (ne levelGrid.length likelihood.length)}} +WARNING: The level grid has {{levelGrid.length}} rows, but this matrix has {{likelihood.length}} likelihood levels. Cells without a level are drawn in grey. + +{{/if}}{{#each levelGrid}}{{#if (ne this.length @root.impact.length)}} +WARNING: Row {{add @index 1}} of the level grid has {{this.length}} entries, but this matrix has {{@root.impact.length}} impact levels. Cells without a level are drawn in grey. + +{{/if}}{{#each this}}{{#if (ne (max this @root.levels.length) @root.levels.length)}} +WARNING: Row {{add @../index 1}} of the level grid refers to level {{this}}, but only {{@root.levels.length}} levels are defined. That cell is drawn in grey. + +{{/if}}{{/each}}{{/each}}{{/if}}{{#vegaLite}} "spec": { "$schema": "https://vega.github.io/schema/vega-lite/v5.json", "width": {"step": 58}, @@ -18,11 +27,31 @@ WARNING: A cell at likelihood {{this.likelihood}}, impact {{this.impact}} falls "data": { "values": [ {{#each likelihood as |lLabel lIdx|}}{{#each ../impact as |iLabel iIdx|}} - {"l": {{lIdx}}, "i": {{iIdx}}, "lLabel": "{{{jsonEscape lLabel}}}", "iLabel": "{{{jsonEscape iLabel}}}", "key": "{{lIdx}}-{{iIdx}}"}{{#unless @last}},{{/unless}} + {"l": {{lIdx}}, "i": {{iIdx}}, "lLabel": "{{{jsonEscape lLabel}}}", "iLabel": "{{{jsonEscape iLabel}}}", "key": "{{lIdx}}-{{iIdx}}"{{#with (lookup (lookup @root.levelGrid lIdx) iIdx)}}, "level": {{this}}{{/with}} }{{#unless @last}},{{/unless}} {{/each}}{{#unless @last}},{{/unless}}{{/each}} ] }, "transform": [ + {{#if levels}} + { + "lookup": "level", + "from": { + "data": { + "values": [ + {{#each levels}} + {"level": {{add @index 1}}, "levelName": "{{{jsonEscape this.name}}}", "colour": {{this.colour}} }{{#unless @last}},{{/unless}} + {{/each}} + ] + }, + "key": "level", + "fields": ["levelName", "colour"] + } + }, + { + "calculate": "datum.colour === 5 ? '#ffffff' : '#1a1a1a'", + "as": "ink" + } + {{else}} { "joinaggregate": [ {"op": "max", "field": "l", "as": "lMax"}, @@ -32,7 +61,12 @@ WARNING: A cell at likelihood {{this.likelihood}}, impact {{this.impact}} falls { "calculate": "(datum.l + datum.i) / (datum.lMax + datum.iMax) <= 1 / 3 ? '#0ca30c' : (datum.l + datum.i) / (datum.lMax + datum.iMax) <= 2 / 3 ? '#fab219' : '#d03b3b'", "as": "band" + }, + { + "calculate": "'#1a1a1a'", + "as": "ink" } + {{/if}} ], "layer": [ { @@ -80,7 +114,32 @@ WARNING: A cell at likelihood {{this.likelihood}}, impact {{this.impact}} falls "titleColor": "currentColor" } }, + {{#if levels}} + "color": { + "condition": { + "test": "datum.levelName != null", + "field": "levelName", + "type": "nominal", + "scale": { + "domain": [{{#each levels}}{{#with (lookup @root.levels (subtract (subtract @root.levels.length 1) @index))}}"{{{jsonEscape this.name}}}"{{/with}}{{#unless @last}}, {{/unless}}{{/each}}], + "range": [{{#each levels}}{{#with (lookup (lookup @root.levels (subtract (subtract @root.levels.length 1) @index)) "colour")}}"{{#if (eq this 1)}}#0ca30c{{/if}}{{#if (eq this 2)}}#b5e68a{{/if}}{{#if (eq this 3)}}#fab219{{/if}}{{#if (eq this 4)}}#d03b3b{{/if}}{{#if (eq this 5)}}#7a1414{{/if}}"{{/with}}{{#unless @last}}, {{/unless}}{{/each}}] + }, + "legend": { + "orient": "right", + "title": null, + "symbolType": "square", + "symbolSize": 180, + "symbolStrokeWidth": 0, + "labelFontSize": 12, + "labelColor": "currentColor", + "labelLimit": 0 + } + }, + "value": "#9a9a9a" + } + {{else}} "color": {"field": "band", "type": "nominal", "scale": null, "legend": null} + {{/if}} } }, { @@ -104,8 +163,7 @@ WARNING: A cell at likelihood {{this.likelihood}}, impact {{this.impact}} falls "mark": { "type": "text", "fontSize": 15, - "fontWeight": "bold", - "color": "#1a1a1a" + "fontWeight": "bold" }, "encoding": { "y": { @@ -145,7 +203,8 @@ WARNING: A cell at likelihood {{this.likelihood}}, impact {{this.impact}} falls "titleColor": "currentColor" } }, - "text": {"field": "count", "type": "quantitative"} + "text": {"field": "count", "type": "quantitative"}, + "fill": {"field": "ink", "type": "nominal", "scale": null, "legend": null} } }{{#if legend}}, { diff --git a/.cards/local/reports/riskMatrixWidget/parameterSchema.json b/.cards/local/reports/riskMatrixWidget/parameterSchema.json index ed8bbbf..482ffc0 100644 --- a/.cards/local/reports/riskMatrixWidget/parameterSchema.json +++ b/.cards/local/reports/riskMatrixWidget/parameterSchema.json @@ -37,6 +37,41 @@ "description": "Axis title for the impact scale (optional, defaults to 'Impact')", "type": "string" }, + "levels": { + "description": "Risk levels of your risk method, lowest first (optional, give together with 'levelGrid'). Each level names a step of a fixed five-step colour scale, so the matrix colours follow your method instead of the position of each cell. The levels are listed in a legend beside the matrix.", + "type": "array", + "minItems": 2, + "maxItems": 5, + "items": { + "additionalProperties": false, + "type": "object", + "properties": { + "name": { + "description": "Name of this risk level, shown in the legend", + "type": "string" + }, + "colour": { + "description": "Step of the colour scale: 1 green, 2 light green, 3 amber, 4 red, 5 dark red", + "type": "integer", + "minimum": 1, + "maximum": 5 + } + }, + "required": ["name", "colour"] + } + }, + "levelGrid": { + "description": "The risk level of every cell (optional, give together with 'levels'). One row per likelihood level, lowest first; each row has one entry per impact level, lowest first. Each entry is a 1-based position in 'levels'.", + "type": "array", + "minItems": 3, + "maxItems": 5, + "items": { + "type": "array", + "minItems": 3, + "maxItems": 5, + "items": {"type": "integer", "minimum": 1, "maximum": 5} + } + }, "cells": { "description": "How many risks sit in each cell. Likelihood and impact are 1-based positions in the scales above. Cells you leave out are drawn in their risk colour with no count.", "type": "array", @@ -67,6 +102,10 @@ } }, "required": ["name", "title", "likelihood", "impact"], + "dependencies": { + "levels": ["levelGrid"], + "levelGrid": ["levels"] + }, "title": "Report", "type": "object" } diff --git a/cardRoot/base_h8a3eqto/c/base_4c0lf8u0/c/base_kxwagxhg/index.adoc b/cardRoot/base_h8a3eqto/c/base_4c0lf8u0/c/base_kxwagxhg/index.adoc index 85e65d8..d4ccd53 100644 --- a/cardRoot/base_h8a3eqto/c/base_4c0lf8u0/c/base_kxwagxhg/index.adoc +++ b/cardRoot/base_h8a3eqto/c/base_4c0lf8u0/c/base_kxwagxhg/index.adoc @@ -1,7 +1,7 @@ = Risk matrix widget -Cell colour is the risk level, derived from the position in the two scales, using the -fixed status palette. The number in a cell is how many risks sit there. Cells with no +By default, cell colour is the risk level, derived from the position in the two scales, +using the fixed status palette. Your own risk levels can replace it; see the last sections. The number in a cell is how many risks sit there. Cells with no risks still show their risk colour. == Five by five @@ -116,3 +116,94 @@ count would otherwise be dropped without a word. The widget says so instead. "likelihood": ["Low", "Medium", "High"], "impact": ["Low", "Medium", "High"] {{/report}} + +== Custom risk levels + +When your risk method defines its own risk level for every cell, give the levels and a +level grid. The cells are then coloured by your method instead of their position, and +the levels are listed in a legend. Each level picks one of five colour steps: 1 green, +2 light green, 3 amber, 4 red, 5 dark red. + +{{#report}} + "name": "base/reports/riskMatrixWidget", + "title": "Risk matrix", + "legend": "open risks by likelihood and impact", + "likelihood": ["Very Low", "Low", "Moderate", "High", "Very High"], + "impact": ["Very Low", "Low", "Moderate", "High", "Very High"], + "levels": [ + {"name": "Very Low", "colour": 1}, + {"name": "Low", "colour": 2}, + {"name": "Moderate", "colour": 3}, + {"name": "High", "colour": 4}, + {"name": "Very High", "colour": 5} + ], + "levelGrid": [ + [1, 1, 2, 2, 3], + [1, 2, 3, 3, 3], + [2, 3, 3, 4, 4], + [2, 3, 4, 4, 5], + [3, 3, 4, 5, 5] + ], + "cells": [ + {"likelihood": 5, "impact": 5, "count": 1}, + {"likelihood": 4, "impact": 5, "count": 2}, + {"likelihood": 3, "impact": 4, "count": 4}, + {"likelihood": 3, "impact": 3, "count": 6}, + {"likelihood": 2, "impact": 3, "count": 11}, + {"likelihood": 2, "impact": 2, "count": 5}, + {"likelihood": 1, "impact": 1, "count": 3} + ] +{{/report}} + +== Custom risk levels, three levels + +{{#report}} + "name": "base/reports/riskMatrixWidget", + "title": "Risk matrix", + "likelihood": ["Rare", "Unlikely", "Possible", "Likely"], + "impact": ["Minor", "Moderate", "Major", "Severe"], + "levels": [ + {"name": "Acceptable", "colour": 1}, + {"name": "Tolerable", "colour": 3}, + {"name": "Unacceptable", "colour": 4} + ], + "levelGrid": [ + [1, 1, 1, 2], + [1, 1, 2, 3], + [1, 2, 3, 3], + [2, 3, 3, 3] + ], + "cells": [ + {"likelihood": 4, "impact": 4, "count": 1}, + {"likelihood": 2, "impact": 3, "count": 3}, + {"likelihood": 1, "impact": 1, "count": 5} + ] +{{/report}} + +== A level grid that does not fit + +The schema cannot tie the size of the level grid to the two scales, or its entries to the +number of levels. Cells without a valid level are drawn in grey, and the widget says why. + +{{#report}} + "name": "base/reports/riskMatrixWidget", + "title": "Risk matrix", + "legend": "one row is short and one cell names a missing level", + "likelihood": ["Rare", "Unlikely", "Possible", "Likely"], + "impact": ["Minor", "Moderate", "Major", "Severe"], + "levels": [ + {"name": "Low", "colour": 1}, + {"name": "Medium", "colour": 3}, + {"name": "High", "colour": 4} + ], + "levelGrid": [ + [1, 1, 2, 2], + [1, 2, 2], + [2, 2, 3, 3], + [2, 3, 3, 4] + ], + "cells": [ + {"likelihood": 2, "impact": 4, "count": 2}, + {"likelihood": 4, "impact": 4, "count": 1} + ] +{{/report}}