Skip to content

Commit d0aba4a

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

2 files changed

Lines changed: 217 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: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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 getPalette = () => Array.from({ length: 10 }, (_, i) =>
11+
getComputedStyle(document.documentElement).getPropertyValue(`--insights-palette-${i}`).trim()
12+
).filter(Boolean);
13+
14+
const BUCKET_COLORS = [
15+
"#00bb00", "#33bb00", "#66bb00", "#99bb00", "#ccbb00",
16+
"#ffbb00", "#ff9900", "#ff6600", "#ff3300", "#dd0000"
17+
];
18+
19+
const formatWholeNumbersOnly = x => Number.isInteger(x) ? x : "";
20+
21+
export const renderers = {
22+
donut(id, config) {
23+
const bbConfig = {
24+
bindto: `#chart-${id}`,
25+
data: { columns: config.columns, type: "donut", colors: { "Others": "#a0a0a0" } },
26+
color: { pattern: getPalette() },
27+
legend: { show: true }
28+
};
29+
30+
if (config.others_list?.length) {
31+
bbConfig.tooltip = {
32+
contents(d, defaultTitle, defaultVal, color) {
33+
if (d[0].id !== "Others") return this.internal.getTooltipContent(d, defaultTitle, defaultVal, color);
34+
35+
//Customize tooltip to show table for Others
36+
const total = config.columns.reduce((sum, col) => sum + col[1], 0);
37+
let html = "<table class='bb-tooltip'><tbody><tr><th colspan='2'>Others</th></tr>";
38+
config.others_list.forEach(([name, val]) => {
39+
html += `<tr><td class='name'>${name}</td><td class='value'>${val.toLocaleString()} (${((val / total) * 100).toFixed(1)}%)</td></tr>`;
40+
});
41+
return html + "</tbody></table>";
42+
}
43+
};
44+
}
45+
bb.generate(bbConfig);
46+
},
47+
48+
colored_bar(id, config) {
49+
const monoColor = getPalette()[0] || "#3273dc";
50+
bb.generate({
51+
bindto: `#chart-${id}`,
52+
data: { x: "x", columns: config.columns, type: "bar", color: () => monoColor },
53+
axis: {
54+
rotated: true,
55+
x: { type: "category", label: { text: "CWE", position: "outer-middle" } },
56+
y: { label: { text: "Vulnerabilities", position: "outer-center" }, tick: { format: formatWholeNumbersOnly } }
57+
},
58+
tooltip: { format: { title: x => config.full_labels?.[x] || x, value: val => val.toLocaleString() } },
59+
legend: { show: false }
60+
});
61+
},
62+
63+
importer_bar(id, config) {
64+
bb.generate({
65+
bindto: `#chart-${id}`,
66+
data: {
67+
x: "x",
68+
columns: [["x", ...config.x_categories], ...config.columns],
69+
type: "bar",
70+
colors: {
71+
total_advisories: "#3273dc", advisories_with_packages: "#00d1b2",
72+
advisories_without_packages: "#e74c3c", advisories_with_exploits: "#55d787",
73+
advisories_without_exploits: "#e74c3c", advisories_with_kev: "#ff8c00",
74+
advisories_with_metasploit: "#870eeaff", advisories_with_exploitdb: "#00e676",
75+
advisories_with_ghost_packages: "#db4be9ff", advisories_without_ghost_packages: "#ffd700"
76+
},
77+
names: {
78+
total_advisories: "All Advisories", advisories_with_packages: "Advisories with a Package",
79+
advisories_without_packages: "Advisories without a Package", advisories_with_exploits: "Advisories with an Exploit",
80+
advisories_without_exploits: "Advisories without an Exploit", advisories_with_kev: "Advisories with KEV",
81+
advisories_with_metasploit: "Advisories with Metasploit", advisories_with_exploitdb: "Advisories with Exploit-DB",
82+
advisories_with_ghost_packages: "Advisories with a Ghost Package", advisories_without_ghost_packages: "Advisories without a Ghost Package"
83+
},
84+
groups: config.groups
85+
},
86+
axis: { rotated: true, x: { type: "category" }, y: { tick: { format: formatWholeNumbersOnly } } },
87+
bar: { width: { ratio: 0.8 } }, tooltip: { grouped: true }, legend: { show: true }
88+
});
89+
},
90+
91+
scatter(id, config) {
92+
const [, ...buckets] = config.columns[0];
93+
const [, ...counts] = config.columns[1];
94+
95+
const rows = document.getElementById(`chart-${id}-rows`);
96+
const total = document.getElementById(`chart-${id}-total`);
97+
const rowTemplate = document.getElementById(`chart-${id}-row-template`);
98+
99+
if (!rows || !total || !rowTemplate) return;
100+
101+
const maxCount = Math.max(1, ...counts);
102+
const frag = document.createDocumentFragment();
103+
104+
// Build the table rows for each CVSS bucket
105+
buckets.forEach((bucket, i) => {
106+
const count = counts[i];
107+
const clone = rowTemplate.content.cloneNode(true);
108+
109+
// Set bucket label (e.g. "9-10")
110+
const labelNode = clone.querySelector('.sev-bucket-label');
111+
labelNode.textContent = bucket;
112+
113+
// Calculate size and paint the colored bubble
114+
const barNode = clone.querySelector('.sev-bucket-bar');
115+
const barWidth = Math.max(2, (count / maxCount) * 140);
116+
barNode.style.width = `${barWidth}px`;
117+
barNode.style.background = BUCKET_COLORS[i];
118+
119+
// Display the vulnerability count on tooltip
120+
const countNode = clone.querySelector('.sev-td-count');
121+
countNode.title = `CVSS ${bucket}: ${count.toLocaleString()}`;
122+
countNode.innerHTML = count.toLocaleString();
123+
124+
frag.appendChild(clone);
125+
});
126+
127+
// Render the completed table to the DOM
128+
rows.innerHTML = "";
129+
rows.appendChild(frag);
130+
const sumOfCounts = counts.reduce((sum, count) => sum + count, 0);
131+
total.textContent = sumOfCounts.toLocaleString();
132+
133+
bb.generate({
134+
bindto: `#chart-${id}-bb`,
135+
data: {
136+
x: "x", columns: config.columns, type: "bubble",
137+
color: (defaultColor, dataPoint) => dataPoint.x !== undefined ? BUCKET_COLORS[dataPoint.x] || defaultColor : defaultColor,
138+
labels: false
139+
},
140+
bubble: { maxR: 55 },
141+
axis: {
142+
x: {
143+
type: "category",
144+
tick: { multiline: false },
145+
label: { text: "CVSS Score Range", position: "outer-center" }
146+
},
147+
y: { show: false, min: 0, max: maxCount * 1.2, padding: { top: 70, bottom: 0 } }
148+
},
149+
grid: { x: { show: true } },
150+
legend: { show: false },
151+
tooltip: {
152+
format: {
153+
title: x => `CVSS ${buckets[x]}`,
154+
name: () => "Vulnerabilities",
155+
value: val => val.toLocaleString()
156+
}
157+
}
158+
});
159+
}
160+
};

0 commit comments

Comments
 (0)