Skip to content

Commit a72e538

Browse files
committed
Add billboard chart
Signed-off-by: Sampurna Pyne <sampurnapyne1710@gmail.com>
1 parent fdd1abd commit a72e538

2 files changed

Lines changed: 238 additions & 0 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//
2+
// Copyright (c) nexB Inc. and others. All rights reserved.
3+
// VulnerableCode is a trademark of nexB Inc.
4+
// SPDX-License-Identifier: Apache-2.0
5+
// See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
// See https://github.com/aboutcode-org/vulnerablecode for support or download.
7+
// See https://aboutcode.org for more information about nexB OSS projects.
8+
//
9+
10+
import { renderers } from './renderers.js';
11+
12+
export function renderChartWithData(chartId, key, chartDataMap = {}) {
13+
// Handle dropdown charts with key
14+
const chartData = key && chartDataMap[key] ? chartDataMap[key] : chartDataMap;
15+
const chartContainer = document.getElementById(`chart-${chartId}`);
16+
if (!chartContainer) return;
17+
18+
if (!chartData?.columns?.length) {
19+
chartContainer.innerHTML = "<p class='has-text-grey has-text-centered mt-5'>No data available.</p>";
20+
return;
21+
}
22+
23+
const type = chartContainer.dataset.chartType;
24+
if (renderers[type]) renderers[type](chartId, chartData);
25+
}
26+
27+
export function initDropdownChart(chartId, chartData, defaultLabel) {
28+
const chartContainer = document.getElementById(`chart-${chartId}`);
29+
if (!chartData || !chartContainer || chartContainer.previousElementSibling?.classList.contains("chart-dropdown-wrapper")) return;
30+
31+
const options = Object.keys(chartData).filter(k => k !== "global");
32+
if (options.length) {
33+
chartContainer.insertAdjacentHTML("beforebegin", `
34+
<div class="chart-dropdown-wrapper mb-3">
35+
<div class="select is-small">
36+
<select>
37+
<option value="global">${defaultLabel}</option>
38+
${options.map(o => `<option value="${o}">${o}</option>`).join("")}
39+
</select>
40+
</div>
41+
</div>
42+
`);
43+
chartContainer.previousElementSibling.querySelector("select").addEventListener("change", ev =>
44+
renderChartWithData(chartId, ev.target.value, chartData)
45+
);
46+
}
47+
renderChartWithData(chartId, "global", chartData);
48+
}
49+
50+
export function initDashboard() {
51+
const layout = document.querySelector(".insights-layout");
52+
if (!layout) return;
53+
const data = JSON.parse(document.getElementById("insights-snapshot-data")?.textContent || "{}");
54+
document.dispatchEvent(new CustomEvent('insightsDataLoaded', { detail: { data, snapshotData: data.snapshot_data || {}, panelId: layout.dataset.panel } }));
55+
}
56+
57+
setTimeout(initDashboard, 0);
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
//
2+
// Copyright (c) nexB Inc. and others. All rights reserved.
3+
// VulnerableCode is a trademark of nexB Inc.
4+
// SPDX-License-Identifier: Apache-2.0
5+
// See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
// See https://github.com/aboutcode-org/vulnerablecode for support or download.
7+
// See https://aboutcode.org for more information about nexB OSS projects.
8+
//
9+
10+
const getCssVar = (name) => getComputedStyle(document.documentElement).getPropertyValue(name).trim();
11+
12+
const paletteVars = [
13+
"--bulma-primary",
14+
"--bulma-link",
15+
"--bulma-info",
16+
"--bulma-success",
17+
"--bulma-warning",
18+
"--bulma-danger",
19+
"--bulma-orange",
20+
"--bulma-purple",
21+
"--bulma-grey",
22+
"--bulma-primary-dark",
23+
];
24+
25+
const getPalette = () => paletteVars.map(getCssVar).filter(Boolean);
26+
27+
const getBucketColors = () => [
28+
getCssVar("--bulma-success"), getCssVar("--bulma-success"), getCssVar("--bulma-success"), getCssVar("--bulma-success"), // 0-3: Success
29+
getCssVar("--bulma-warning"), getCssVar("--bulma-warning"), getCssVar("--bulma-warning"), // 4-6: Warning
30+
getCssVar("--bulma-orange"), // 7: Orange
31+
getCssVar("--bulma-danger"), getCssVar("--bulma-danger") // 8-9: Danger
32+
];
33+
34+
const formatWholeNumbersOnly = x => Number.isInteger(x) ? x : "";
35+
36+
export const renderers = {
37+
donut(id, config) {
38+
const bbConfig = {
39+
bindto: `#chart-${id}`,
40+
data: { columns: config.columns, type: "donut", colors: { "Others": "#1b1b1b3a" } },
41+
color: { pattern: getPalette() },
42+
legend: { show: true }
43+
};
44+
45+
if (config.others_list?.length) {
46+
bbConfig.tooltip = {
47+
contents(d, defaultTitle, defaultVal, color) {
48+
if (d[0].id !== "Others") return this.internal.getTooltipContent(d, defaultTitle, defaultVal, color);
49+
50+
//Customize tooltip to show table for Others
51+
const total = config.columns.reduce((sum, col) => sum + col[1], 0);
52+
let html = "<table class='bb-tooltip'><tbody><tr><th colspan='2'>Others</th></tr>";
53+
config.others_list.forEach(([name, val]) => {
54+
html += `<tr><td class='name'>${name}</td><td class='value'>${val.toLocaleString()} (${((val / total) * 100).toFixed(1)}%)</td></tr>`;
55+
});
56+
return html + "</tbody></table>";
57+
}
58+
};
59+
}
60+
bb.generate(bbConfig);
61+
},
62+
63+
colored_bar(id, config) {
64+
const monoColor = getCssVar("--bulma-link")
65+
bb.generate({
66+
bindto: `#chart-${id}`,
67+
data: { x: "x", columns: config.columns, type: "bar", color: () => monoColor },
68+
axis: {
69+
rotated: true,
70+
x: { type: "category", label: { text: "CWE", position: "outer-middle" } },
71+
y: { label: { text: "Advisories", position: "outer-center" }, tick: { format: formatWholeNumbersOnly } }
72+
},
73+
tooltip: { format: { title: x => config.full_labels?.[x] || x, value: val => val.toLocaleString() } },
74+
legend: { show: false }
75+
});
76+
},
77+
78+
importer_bar(id, config) {
79+
bb.generate({
80+
bindto: `#chart-${id}`,
81+
data: {
82+
x: "x",
83+
columns: [["x", ...config.x_categories], ...config.columns],
84+
type: "bar",
85+
colors: {
86+
total_advisories: getCssVar("--bulma-link"),
87+
advisories_with_packages: getCssVar("--bulma-primary"),
88+
advisories_without_packages: getCssVar("--bulma-danger"),
89+
advisories_with_exploits: getCssVar("--bulma-success"),
90+
advisories_without_exploits: getCssVar("--bulma-danger"),
91+
advisories_with_kev: getCssVar("--bulma-warning"),
92+
advisories_with_metasploit: getCssVar("--bulma-purple"),
93+
advisories_with_exploitdb: getCssVar("--bulma-success"),
94+
advisories_with_ghost_packages: getCssVar("--bulma-orange"),
95+
advisories_without_ghost_packages: getCssVar("--bulma-primary-dark"),
96+
},
97+
names: {
98+
total_advisories: "All Advisories", advisories_with_packages: "Advisories with a Package",
99+
advisories_without_packages: "Advisories without a Package", advisories_with_exploits: "Advisories with an Exploit",
100+
advisories_without_exploits: "Advisories without an Exploit", advisories_with_kev: "Advisories with KEV",
101+
advisories_with_metasploit: "Advisories with Metasploit", advisories_with_exploitdb: "Advisories with Exploit-DB",
102+
advisories_with_ghost_packages: "Advisories with a Ghost Package", advisories_without_ghost_packages: "Advisories without a Ghost Package"
103+
},
104+
groups: config.groups
105+
},
106+
axis: { rotated: true, x: { type: "category" }, y: { tick: { format: formatWholeNumbersOnly } } },
107+
bar: { width: { ratio: 0.8 } }, tooltip: { grouped: true }, legend: { show: true }
108+
});
109+
},
110+
111+
scatter(id, config) {
112+
const [, ...buckets] = config.columns[0];
113+
const [, ...counts] = config.columns[1];
114+
const BUCKET_COLORS = getBucketColors();
115+
116+
const rows = document.getElementById(`chart-${id}-rows`);
117+
const total = document.getElementById(`chart-${id}-total`);
118+
const rowTemplate = document.getElementById(`chart-${id}-row-template`);
119+
120+
if (!rows || !total || !rowTemplate) return;
121+
122+
const maxCount = Math.max(1, ...counts);
123+
const frag = document.createDocumentFragment();
124+
125+
// Build the table rows for each CVSS bucket
126+
buckets.forEach((bucket, i) => {
127+
const count = counts[i];
128+
const clone = rowTemplate.content.cloneNode(true);
129+
130+
// Set bucket label (e.g. "9-10")
131+
const labelNode = clone.querySelector('.sev-bucket-label');
132+
labelNode.textContent = bucket;
133+
134+
// Calculate size and paint the colored bubble
135+
const barNode = clone.querySelector('.sev-bucket-bar');
136+
const barWidth = Math.max(2, (count / maxCount) * 140);
137+
barNode.style.width = `${barWidth}px`;
138+
barNode.style.background = BUCKET_COLORS[i];
139+
140+
// Display the vulnerability count on tooltip
141+
const countNode = clone.querySelector('.sev-td-count');
142+
countNode.title = `CVSS ${bucket}: ${count.toLocaleString()}`;
143+
countNode.innerHTML = count.toLocaleString();
144+
145+
frag.appendChild(clone);
146+
});
147+
148+
// Render the completed table to the DOM
149+
rows.innerHTML = "";
150+
rows.appendChild(frag);
151+
const sumOfCounts = counts.reduce((sum, count) => sum + count, 0);
152+
total.textContent = sumOfCounts.toLocaleString();
153+
154+
bb.generate({
155+
bindto: `#chart-${id}-bb`,
156+
data: {
157+
x: "x", columns: config.columns, type: "bubble",
158+
color: (defaultColor, dataPoint) => dataPoint.x !== undefined ? getBucketColors()[dataPoint.x] || defaultColor : defaultColor,
159+
labels: false
160+
},
161+
bubble: { maxR: 55 },
162+
axis: {
163+
x: {
164+
type: "category",
165+
tick: { multiline: false },
166+
label: { text: "CVSS Score Range", position: "outer-center" }
167+
},
168+
y: { show: false, min: 0, max: maxCount * 1.2, padding: { top: 70, bottom: 0 } }
169+
},
170+
grid: { x: { show: true } },
171+
legend: { show: false },
172+
tooltip: {
173+
format: {
174+
title: x => `CVSS ${buckets[x]}`,
175+
name: () => "Advisories",
176+
value: val => val.toLocaleString()
177+
}
178+
}
179+
});
180+
}
181+
};

0 commit comments

Comments
 (0)