awesome-risk-radar-kpi
v0.1.0
Published
Enterprise-grade interactive Risk Radar KPI component for React — spider/radar chart with drilldown, anomaly detection, trend analysis, multi-filter, and export.
Maintainers
Readme
awesome-risk-radar-kpi
Enterprise-grade interactive Risk Radar KPI component for React. A spider/radar chart that visualises multi-dimensional risk exposure with drilldown panels, anomaly detection, trend arrows, multi-filter controls, comparison mode, and one-click export to PNG, SVG, JSON, or CSV — all styles injected at runtime so no CSS import is needed.
Features
- Spider / radar chart — SVG-based 8-axis risk polygon with configurable grid levels and axis labels
- 3 risk score models —
matrix(probability × impact bands),weighted(configurable weights),exposure(probability × impact × exposure) - Trend detection — per-node trend arrows (▲/▼/●) from
trendDatatime series using a 2% threshold - Anomaly detection — statistical outlier detection (2σ above mean) highlights sudden risk spikes with a
!badge and "Sudden spike" tag - Drilldown panel — click any node to open a detailed panel with owner, probability, impact, last updated, and drivers
- Hover tooltip — floating panel with full risk detail on mouse-over
- Multi-filter controls — filter by Department, Region, Risk Owner, and Time Period simultaneously
- Top Ranked Risks sidebar — automatically sorted top-5 by risk score with colour-coded values
- Risk Highlights sidebar — shows all risks with trend labels and anomaly tags
- Comparison mode — overlay a second dataset on the radar for period-over-period or scenario comparison
- 4 built-in themes —
light,dark,purple,corporate; all colours driven by a theme object - Export — one-click PNG, SVG, JSON, and CSV export of the current filtered dataset
- High contrast mode —
highContrastprop applies CSS contrast filter - Self-contained — CSS Module replaced with runtime
<style>injection; no CSS import required - ESM + CJS dual build — works in Vite, Next.js, CRA, and any modern bundler
- Zero runtime dependencies beyond React
Installation
npm install awesome-risk-radar-kpi
# or
yarn add awesome-risk-radar-kpi
# or
pnpm add awesome-risk-radar-kpiPeer dependencies — React ≥ 17 and ReactDOM ≥ 17 must already be installed.
Quick Start
No CSS import needed — styles are injected automatically on first render.
import AwesomeRiskRadarKpi from "awesome-risk-radar-kpi";
var data = [
{
id: "RISK001",
category: "Supply Chain",
probability: 0.7,
impact: 85,
trendData: [65, 70, 72, 74, 78],
owner: "Operations",
department: "Logistics",
region: "Global",
period: "Q1 2026",
lastUpdated: "2026-02-10",
drivers: ["Supplier delay"],
},
{
id: "RISK002",
category: "Cybersecurity",
probability: 0.9,
impact: 95,
trendData: [85, 88, 90, 91, 92],
owner: "Security",
department: "IT",
region: "Global",
period: "Q1 2026",
lastUpdated: "2026-02-14",
drivers: ["Ransomware"],
},
{
id: "RISK003",
category: "Financial Risk",
probability: 0.8,
impact: 88,
trendData: [70, 74, 76, 80, 82],
owner: "Finance",
department: "Treasury",
region: "North America",
period: "Q1 2026",
lastUpdated: "2026-02-12",
},
{
id: "RISK004",
category: "Market Risk",
probability: 0.75,
impact: 76,
trendData: [68, 70, 71, 72, 73],
owner: "Strategy",
department: "Market Intelligence",
region: "Global",
period: "Q1 2026",
lastUpdated: "2026-02-11",
},
];
export default function App() {
return (
<div style={{ padding: 32 }}>
<AwesomeRiskRadarKpi data={data} />
</div>
);
}Props
| Prop | Type | Default | Description |
| ----------------- | ---------------------------------------------------------------- | ---------------------------- | ----------------------------------------------------------------------------------------- |
| data | RiskItem[] | — | Static array of risk objects. Takes precedence over dataUrl and apiEndpoint. |
| dataUrl | string | '/data/riskRadarData.json' | Path to a local JSON file. Used when data is not provided. |
| apiEndpoint | string | — | REST API URL. Used when neither data nor dataUrl resolves. |
| size | number | 520 | SVG canvas size in px (width and height of the radar chart). |
| thresholdRules | { low: number, moderate: number } | { low: 40, moderate: 70 } | Risk colour thresholds. Scores below low → green, below moderate → amber, else → red. |
| riskScoreModel | 'matrix' \| 'weighted' \| 'exposure' | 'matrix' | Scoring model used when riskScore is not pre-calculated on the item. |
| matrixConfig | { likelihoodThresholds: number[], impactThresholds: number[] } | [20,40,60,80] each | Band thresholds for the matrix model. |
| weights | { probability: number, impact: number, exposure: number } | 0.45 / 0.45 / 0.1 | Weights for the weighted model. |
| trendAdjustment | { multiplier: number, mode: 'additive' \| 'multiplicative' } | — | Optional trend-based score adjustment applied after model scoring. |
| showTrend | boolean | true | Show trend arrows (▲/▼/●) on nodes and the trend legend. |
| showLabels | boolean | true | Show axis labels on the radar chart. |
| animation | boolean | true | Fade-in animation on mount. |
| compareMode | string | — | Label for comparison mode (e.g. 'Q4 2025'). Enables overlay of compareData. |
| compareData | RiskItem[] | — | Second dataset to overlay on the radar in grey. |
| theme | 'light' \| 'dark' \| 'purple' \| 'corporate' | 'light' | Built-in colour theme. |
| colorScheme | Partial<ThemeObject> | — | Override individual theme tokens. Merged on top of the resolved theme. |
| showFilters | boolean | true | Show the Department / Region / Owner / Period filter dropdowns. |
| highContrast | boolean | false | Apply CSS contrast(1.1) filter for accessibility. |
| onDrilldown | (category: string) => void | — | Fired when a risk node is clicked. Receives the category name. |
RiskItem object
| Field | Type | Required | Description |
| ------------- | ---------- | -------- | -------------------------------------------------------------------------- |
| id | string | ✅ | Unique identifier. Used as React key. |
| category | string | ✅ | Must match one of the 8 radar axes (see below). |
| probability | number | ✅ | 0–1 (fraction) or 0–100 (percentage). Auto-normalised. |
| impact | number | ✅ | 0–1 or 0–100. Auto-normalised. |
| riskScore | number | ➖ | Pre-calculated 0–100 score. If supplied, skips model calculation. |
| exposure | number | ➖ | 0–1 or 0–100. Used by exposure and weighted models. |
| threshold | number | ➖ | Per-item colour threshold override (0–100). |
| trendData | number[] | ➖ | Historical score series (oldest → newest) for trend and anomaly detection. |
| owner | string | ➖ | Risk owner name shown in tooltip and drilldown. |
| department | string | ➖ | Department used as filter option. |
| region | string | ➖ | Region used as filter option. |
| period | string | ➖ | Time period used as filter option. |
| lastUpdated | string | ➖ | Date string shown in tooltip and drilldown. |
| drivers | string[] | ➖ | List of risk drivers shown in the drilldown panel. |
Radar axes (fixed)
The 8 axes are: Supply Chain, Financial Risk, Operational Risk, Cybersecurity, Regulatory Compliance, Market Risk, Customer Risk, Technology Risk. Each RiskItem.category must exactly match one of these to appear on the chart.
Examples
Example 1 — Full 8-axis dataset with dark theme
Uses all 8 risk categories from the sample data, dark theme, and an onDrilldown callback.
import AwesomeRiskRadarKpi from "awesome-risk-radar-kpi";
var riskData = [
{
id: "RISK001",
category: "Supply Chain",
probability: 0.7,
impact: 85,
exposure: 0.9,
threshold: 60,
trendData: [65, 70, 72, 74, 78],
owner: "Operations",
department: "Logistics",
region: "Global",
period: "Q1 2026",
lastUpdated: "2026-02-10",
drivers: ["Supplier delay", "Shipping cost spike", "Port congestion"],
},
{
id: "RISK002",
category: "Financial Risk",
probability: 0.8,
impact: 88,
exposure: 0.85,
threshold: 65,
trendData: [70, 74, 76, 80, 82],
owner: "Finance",
department: "Treasury",
region: "North America",
period: "Q1 2026",
lastUpdated: "2026-02-12",
drivers: ["Interest rate volatility", "Liquidity tightening"],
},
{
id: "RISK003",
category: "Operational Risk",
probability: 0.6,
impact: 70,
exposure: 0.7,
threshold: 55,
trendData: [60, 61, 63, 64, 64],
owner: "Operations",
department: "Manufacturing",
region: "Europe",
period: "Q1 2026",
lastUpdated: "2026-02-08",
drivers: ["Workforce shortage", "Equipment downtime"],
},
{
id: "RISK004",
category: "Cybersecurity",
probability: 0.9,
impact: 95,
exposure: 0.95,
threshold: 70,
trendData: [85, 88, 90, 91, 92],
owner: "Security",
department: "IT",
region: "Global",
period: "Q1 2026",
lastUpdated: "2026-02-14",
drivers: ["Ransomware attempts", "Third-party exposure"],
},
{
id: "RISK005",
category: "Regulatory Compliance",
probability: 0.55,
impact: 60,
exposure: 0.6,
threshold: 60,
trendData: [62, 60, 59, 58, 58],
owner: "Legal",
department: "Compliance",
region: "APAC",
period: "Q1 2026",
lastUpdated: "2026-02-05",
drivers: ["New reporting mandates", "Audit backlog"],
},
{
id: "RISK006",
category: "Market Risk",
probability: 0.75,
impact: 76,
exposure: 0.8,
threshold: 62,
trendData: [68, 70, 71, 72, 73],
owner: "Strategy",
department: "Market Intelligence",
region: "Global",
period: "Q1 2026",
lastUpdated: "2026-02-11",
drivers: ["Competitor pricing", "Demand volatility"],
},
{
id: "RISK007",
category: "Customer Risk",
probability: 0.6,
impact: 65,
exposure: 0.7,
threshold: 55,
trendData: [58, 59, 60, 61, 61],
owner: "Customer Success",
department: "Sales",
region: "Latin America",
period: "Q1 2026",
lastUpdated: "2026-02-09",
drivers: ["Churn risk", "Service backlog"],
},
{
id: "RISK008",
category: "Technology Risk",
probability: 0.65,
impact: 72,
exposure: 0.75,
threshold: 58,
trendData: [62, 64, 66, 68, 69],
owner: "Technology",
department: "Engineering",
region: "North America",
period: "Q1 2026",
lastUpdated: "2026-02-13",
drivers: ["Legacy system aging", "Scalability constraints"],
},
];
export default function RiskDashboard() {
return (
<div style={{ padding: 32, background: "#111827", minHeight: "100vh" }}>
<AwesomeRiskRadarKpi
data={riskData}
theme="dark"
size={540}
riskScoreModel="matrix"
thresholdRules={{ low: 40, moderate: 70 }}
showTrend={true}
showLabels={true}
animation={true}
onDrilldown={function (category) {
console.log("Drilldown on:", category);
}}
/>
</div>
);
}Example 2 — Corporate theme with weighted model, filters hidden, onDrilldown routing
Uses the weighted scoring model, disables the filter bar, and routes drilldown clicks to a custom panel.
import { useState } from "react";
import AwesomeRiskRadarKpi from "awesome-risk-radar-kpi";
var riskData = [
{
id: "RISK001",
category: "Supply Chain",
probability: 0.7,
impact: 85,
exposure: 0.9,
trendData: [65, 70, 72, 74, 78],
owner: "Operations",
department: "Logistics",
region: "Global",
period: "Q1 2026",
lastUpdated: "2026-02-10",
drivers: ["Supplier delay", "Shipping cost spike", "Port congestion"],
},
{
id: "RISK002",
category: "Financial Risk",
probability: 0.8,
impact: 88,
exposure: 0.85,
trendData: [70, 74, 76, 80, 82],
owner: "Finance",
department: "Treasury",
region: "North America",
period: "Q1 2026",
lastUpdated: "2026-02-12",
drivers: ["Interest rate volatility", "Liquidity tightening"],
},
{
id: "RISK003",
category: "Operational Risk",
probability: 0.6,
impact: 70,
exposure: 0.7,
trendData: [60, 61, 63, 64, 64],
owner: "Operations",
department: "Manufacturing",
region: "Europe",
period: "Q1 2026",
lastUpdated: "2026-02-08",
drivers: ["Workforce shortage", "Equipment downtime"],
},
{
id: "RISK004",
category: "Cybersecurity",
probability: 0.9,
impact: 95,
exposure: 0.95,
trendData: [85, 88, 90, 91, 92],
owner: "Security",
department: "IT",
region: "Global",
period: "Q1 2026",
lastUpdated: "2026-02-14",
drivers: ["Ransomware attempts", "Third-party exposure"],
},
{
id: "RISK005",
category: "Regulatory Compliance",
probability: 0.55,
impact: 60,
exposure: 0.6,
trendData: [62, 60, 59, 58, 58],
owner: "Legal",
department: "Compliance",
region: "APAC",
period: "Q1 2026",
lastUpdated: "2026-02-05",
drivers: ["New reporting mandates", "Audit backlog"],
},
{
id: "RISK006",
category: "Market Risk",
probability: 0.75,
impact: 76,
exposure: 0.8,
trendData: [68, 70, 71, 72, 73],
owner: "Strategy",
department: "Market Intelligence",
region: "Global",
period: "Q1 2026",
lastUpdated: "2026-02-11",
drivers: ["Competitor pricing", "Demand volatility"],
},
{
id: "RISK007",
category: "Customer Risk",
probability: 0.6,
impact: 65,
exposure: 0.7,
trendData: [58, 59, 60, 61, 61],
owner: "Customer Success",
department: "Sales",
region: "Latin America",
period: "Q1 2026",
lastUpdated: "2026-02-09",
drivers: ["Churn risk", "Service backlog"],
},
{
id: "RISK008",
category: "Technology Risk",
probability: 0.65,
impact: 72,
exposure: 0.75,
trendData: [62, 64, 66, 68, 69],
owner: "Technology",
department: "Engineering",
region: "North America",
period: "Q1 2026",
lastUpdated: "2026-02-13",
drivers: ["Legacy system aging", "Scalability constraints"],
},
];
var drawerStyle = {
position: "fixed",
top: 0,
right: 0,
width: 320,
height: "100vh",
background: "#ffffff",
borderLeft: "1px solid #e5e7eb",
padding: 28,
zIndex: 200,
fontFamily: "sans-serif",
overflowY: "auto",
};
export default function CorporateRiskDashboard() {
var [selected, setSelected] = useState(null);
return (
<div style={{ padding: 32, background: "#f3f4f6", minHeight: "100vh" }}>
<AwesomeRiskRadarKpi
data={riskData}
theme="corporate"
size={500}
riskScoreModel="weighted"
weights={{ probability: 0.5, impact: 0.4, exposure: 0.1 }}
thresholdRules={{ low: 35, moderate: 65 }}
showFilters={false}
showTrend={true}
animation={true}
onDrilldown={function (category) {
var item = riskData.find(function (r) {
return r.category === category;
});
setSelected(item || null);
}}
/>
{selected && (
<div style={drawerStyle}>
<button
onClick={function () {
setSelected(null);
}}
style={{
float: "right",
background: "none",
border: "1px solid #d1d5db",
borderRadius: 4,
padding: "4px 10px",
cursor: "pointer",
}}
>
✕
</button>
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 4 }}>
{selected.category}
</h2>
<p style={{ fontSize: 12, color: "#6b7280", marginBottom: 16 }}>
Owner: {selected.owner}
</p>
<hr style={{ marginBottom: 16 }} />
<p style={{ fontSize: 13 }}>
Probability: {(selected.probability * 100).toFixed(0)}%
</p>
<p style={{ fontSize: 13 }}>Impact: {selected.impact}</p>
<p style={{ fontSize: 13 }}>Last Updated: {selected.lastUpdated}</p>
{selected.drivers && (
<>
<p style={{ fontSize: 13, fontWeight: 600, marginTop: 12 }}>
Drivers:
</p>
<ul style={{ fontSize: 13, paddingLeft: 18 }}>
{selected.drivers.map(function (d) {
return <li key={d}>{d}</li>;
})}
</ul>
</>
)}
</div>
)}
</div>
);
}Example 3 — Comparison mode (Q1 vs Q4 prior year) with purple theme and REST fallback
Overlays a prior-period dataset on the radar for visual comparison, with a live API endpoint and a static fallback.
import AwesomeRiskRadarKpi from "awesome-risk-radar-kpi";
// Current period — Q1 2026
var currentData = [
{
id: "RISK001",
category: "Supply Chain",
probability: 0.7,
impact: 85,
trendData: [65, 70, 72, 74, 78],
owner: "Operations",
department: "Logistics",
region: "Global",
period: "Q1 2026",
lastUpdated: "2026-02-10",
drivers: ["Supplier delay", "Port congestion"],
},
{
id: "RISK002",
category: "Financial Risk",
probability: 0.8,
impact: 88,
trendData: [70, 74, 76, 80, 82],
owner: "Finance",
department: "Treasury",
region: "North America",
period: "Q1 2026",
lastUpdated: "2026-02-12",
drivers: ["Interest rate volatility"],
},
{
id: "RISK003",
category: "Operational Risk",
probability: 0.6,
impact: 70,
trendData: [60, 61, 63, 64, 64],
owner: "Operations",
department: "Manufacturing",
region: "Europe",
period: "Q1 2026",
lastUpdated: "2026-02-08",
drivers: ["Workforce shortage"],
},
{
id: "RISK004",
category: "Cybersecurity",
probability: 0.9,
impact: 95,
trendData: [85, 88, 90, 91, 92],
owner: "Security",
department: "IT",
region: "Global",
period: "Q1 2026",
lastUpdated: "2026-02-14",
drivers: ["Ransomware attempts"],
},
{
id: "RISK005",
category: "Regulatory Compliance",
probability: 0.55,
impact: 60,
trendData: [62, 60, 59, 58, 58],
owner: "Legal",
department: "Compliance",
region: "APAC",
period: "Q1 2026",
lastUpdated: "2026-02-05",
drivers: ["New reporting mandates"],
},
{
id: "RISK006",
category: "Market Risk",
probability: 0.75,
impact: 76,
trendData: [68, 70, 71, 72, 73],
owner: "Strategy",
department: "Market Intelligence",
region: "Global",
period: "Q1 2026",
lastUpdated: "2026-02-11",
drivers: ["Competitor pricing"],
},
{
id: "RISK007",
category: "Customer Risk",
probability: 0.6,
impact: 65,
trendData: [58, 59, 60, 61, 61],
owner: "Customer Success",
department: "Sales",
region: "Latin America",
period: "Q1 2026",
lastUpdated: "2026-02-09",
drivers: ["Churn risk"],
},
{
id: "RISK008",
category: "Technology Risk",
probability: 0.65,
impact: 72,
trendData: [62, 64, 66, 68, 69],
owner: "Technology",
department: "Engineering",
region: "North America",
period: "Q1 2026",
lastUpdated: "2026-02-13",
drivers: ["Legacy system aging"],
},
];
// Prior period — Q4 2025 (lower scores for comparison)
var priorData = [
{ id: "P001", category: "Supply Chain", riskScore: 55 },
{ id: "P002", category: "Financial Risk", riskScore: 61 },
{ id: "P003", category: "Operational Risk", riskScore: 48 },
{ id: "P004", category: "Cybersecurity", riskScore: 72 },
{ id: "P005", category: "Regulatory Compliance", riskScore: 44 },
{ id: "P006", category: "Market Risk", riskScore: 58 },
{ id: "P007", category: "Customer Risk", riskScore: 42 },
{ id: "P008", category: "Technology Risk", riskScore: 50 },
];
export default function ComparisonDashboard() {
return (
<div style={{ padding: 32, background: "#f6f0ff", minHeight: "100vh" }}>
<AwesomeRiskRadarKpi
data={currentData}
theme="purple"
size={520}
riskScoreModel="matrix"
compareMode="Q4 2025"
compareData={priorData}
showTrend={true}
showLabels={true}
highContrast={false}
animation={true}
onDrilldown={function (category) {
console.log("Drilldown:", category);
}}
/>
<p
style={{
textAlign: "center",
marginTop: 12,
fontSize: 13,
color: "#6f5c8f",
}}
>
Solid polygon = Q1 2026 · Outline polygon = Q4 2025 (comparison)
</p>
</div>
);
}Themes
Pass the theme prop with one of the built-in names, or pass colorScheme to override individual tokens:
// Built-in themes
<AwesomeRiskRadarKpi theme="dark" ... />
<AwesomeRiskRadarKpi theme="light" ... />
<AwesomeRiskRadarKpi theme="purple" ... />
<AwesomeRiskRadarKpi theme="corporate" ... />
// Custom token overrides (merged on top of light theme)
<AwesomeRiskRadarKpi
colorScheme={{
accent: '#e11d48',
low: '#059669',
moderate: '#d97706',
high: '#dc2626',
}}
/>Theme token reference
| Token | Description |
| ------------ | -------------------------------------------------------- |
| background | Outer container background |
| panel | Card / tooltip / drilldown background |
| text | Primary text colour |
| mutedText | Secondary / muted text |
| grid | Radar grid polygon stroke |
| axis | Axis line stroke |
| low | Colour for low-risk nodes (score < thresholdRules.low) |
| moderate | Colour for moderate-risk nodes |
| high | Colour for high-risk nodes |
| accent | Primary radar polygon fill / stroke |
| compare | Comparison dataset polygon fill / stroke |
Risk Score Models
matrix (default)
Bands probability and impact into N levels (default 5 each using [20,40,60,80] thresholds), multiplies the band indices, and normalises to 0–100.
<AwesomeRiskRadarKpi
riskScoreModel="matrix"
matrixConfig={{
likelihoodThresholds: [25, 50, 75],
impactThresholds: [25, 50, 75],
}}
/>weighted
Weighted average of probability, impact, and exposure.
<AwesomeRiskRadarKpi
riskScoreModel="weighted"
weights={{ probability: 0.5, impact: 0.4, exposure: 0.1 }}
/>exposure
score = (probability × impact × exposure) / 10000
<AwesomeRiskRadarKpi riskScoreModel="exposure" />📄 License
MIT
