feat: add comprehensive light/dark theme support for all graph components

- Add complete CSS variables for both light and dark themes in globals.css
- Light theme: dark text on light backgrounds (primary: #1f2937, bg: #f9fafb)
- Dark theme: white text on dark backgrounds (primary: #ffffff, bg: #1e293b)
- Update all chart components to use CSS variables instead of hardcoded colors
- Tooltips, legends, titles, and text automatically adapt to theme changes
- Fix SankeyChart text to properly display in light mode (was hardcoded white)
- Update DonutChart center label to use theme-aware text colors
- All components now dynamically respond to theme toggle via next-themes
This commit is contained in:
Alan Buscaglia
2025-10-21 17:07:23 +02:00
parent f223b24060
commit 17a841a86d
12 changed files with 291 additions and 69 deletions
+29 -15
View File
@@ -545,28 +545,42 @@ const sampleThreatMapData = {
],
};
// Sample data for DonutChart
// Sample data for DonutChart (Findings by Severity)
const sampleDonutChartData = [
{
name: "AWS",
value: 1840,
percentage: 45,
color: "var(--chart-provider-aws)",
name: "Critical",
value: 432,
percentage: 22,
color: "var(--chart-danger-emphasis)",
change: 5,
},
{
name: "Azure",
value: 1280,
name: "High",
value: 1232,
percentage: 32,
color: "var(--chart-provider-azure)",
color: "var(--chart-danger)",
change: 8,
},
{
name: "Medium",
value: 925,
percentage: 28,
color: "var(--chart-warning-emphasis)",
change: -3,
},
{
name: "Low",
value: 543,
percentage: 15,
color: "var(--chart-warning)",
change: -2,
},
{
name: "Google",
value: 920,
percentage: 23,
color: "var(--chart-provider-google)",
change: 8,
name: "Info",
value: 108,
percentage: 3,
color: "var(--chart-info)",
change: 0,
},
];
@@ -753,14 +767,14 @@ export default async function Home({
<div className="col-span-12 lg:col-span-6">
<Spacer y={16} />
<h3 className="mb-4 text-sm font-bold uppercase">
Provider Distribution
Severity Distribution
</h3>
<DonutChart
data={sampleDonutChartData}
height={350}
showLegend
centerLabel={{
value: "4,040",
value: "3,240",
label: "Total Findings",
}}
/>
+19 -5
View File
@@ -24,18 +24,27 @@ const CustomTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
return (
<div className="rounded-lg border border-slate-700 bg-slate-800 p-3 shadow-lg">
<div
className="rounded-lg border p-3 shadow-lg"
style={{
backgroundColor: "var(--chart-background)",
borderColor: "var(--chart-border-emphasis)",
}}
>
<div className="flex items-center gap-2">
<div
className="h-3 w-3 rounded-sm"
style={{ backgroundColor: data.color }}
/>
<span className="text-sm font-semibold text-white">
<span
className="text-sm font-semibold"
style={{ color: "var(--chart-text-primary)" }}
>
{data.percentage}% {data.name}
</span>
</div>
{data.change !== undefined && (
<p className="mt-2 text-xs text-slate-400">
<p className="mt-2 text-xs" style={{ color: "var(--chart-text-secondary)" }}>
<span className="font-bold">
{data.change > 0 ? "+" : ""}
{data.change}%
@@ -145,14 +154,19 @@ export function DonutChart({
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-white text-3xl font-bold"
className="text-3xl font-bold"
style={{
fill: "var(--chart-text-primary)",
}}
>
{formattedValue}
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) + 24}
className="fill-slate-400"
style={{
fill: "var(--chart-text-secondary)",
}}
>
{centerLabel.label}
</tspan>
+34 -9
View File
@@ -26,7 +26,12 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
<div className="w-full">
{title && (
<div className="mb-4">
<h3 className="text-lg font-semibold text-white">{title}</h3>
<h3
className="text-lg font-semibold"
style={{ color: "var(--chart-text-primary)" }}
>
{title}
</h3>
</div>
)}
@@ -48,8 +53,9 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
>
<div className="w-24 text-right">
<span
className="text-sm text-white"
className="text-sm"
style={{
color: "var(--chart-text-primary)",
opacity: isFaded ? 0.5 : 1,
transition: "opacity 0.2s",
}}
@@ -70,26 +76,44 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
/>
{isHovered && (
<div className="absolute top-10 left-0 z-10 min-w-[200px] rounded-lg border border-slate-700 bg-slate-800 p-3 shadow-lg">
<div
className="absolute top-10 left-0 z-10 min-w-[200px] rounded-lg border p-3 shadow-lg"
style={{
backgroundColor: "var(--chart-background)",
borderColor: "var(--chart-border-emphasis)",
}}
>
<div className="flex items-center gap-2">
<div
className="h-3 w-3 rounded-sm"
style={{ backgroundColor: barColor }}
/>
<span className="font-semibold text-white">
<span
className="font-semibold"
style={{ color: "var(--chart-text-primary)" }}
>
{item.value.toLocaleString()} {item.name} Risk
</span>
</div>
{item.newFindings !== undefined && (
<div className="mt-2 flex items-center gap-2">
<Bell size={14} className="text-slate-400" />
<span className="text-sm text-slate-400">
<Bell
size={14}
style={{ color: "var(--chart-text-secondary)" }}
/>
<span
className="text-sm"
style={{ color: "var(--chart-text-secondary)" }}
>
{item.newFindings} New Findings
</span>
</div>
)}
{item.change !== undefined && (
<p className="mt-1 text-sm text-slate-400">
<p
className="mt-1 text-sm"
style={{ color: "var(--chart-text-secondary)" }}
>
<span className="font-bold">
{item.change > 0 ? "+" : ""}
{item.change}%
@@ -102,14 +126,15 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
</div>
<div
className="flex w-40 items-center gap-2 text-sm text-white"
className="flex w-40 items-center gap-2 text-sm"
style={{
color: "var(--chart-text-primary)",
opacity: isFaded ? 0.5 : 1,
transition: "opacity 0.2s",
}}
>
<span className="font-semibold">{item.percentage}%</span>
<span className="text-slate-400"></span>
<span style={{ color: "var(--chart-text-secondary)" }}></span>
<span className="font-bold">{item.value.toLocaleString()}</span>
</div>
</div>
+19 -6
View File
@@ -48,8 +48,16 @@ const CustomLineTooltip = ({
const totalValue = typedPayload.reduce((sum, item) => sum + item.value, 0);
return (
<div className="rounded-lg border border-slate-700 bg-slate-800 p-3 shadow-lg">
<p className="mb-3 text-xs text-slate-400">{label}</p>
<div
className="rounded-lg border p-3 shadow-lg"
style={{
backgroundColor: "var(--chart-background)",
borderColor: "var(--chart-border-emphasis)",
}}
>
<p className="mb-3 text-xs" style={{ color: "var(--chart-text-secondary)" }}>
{label}
</p>
<div className="mb-3">
<AlertPill value={totalValue} textSize="sm" />
@@ -67,18 +75,23 @@ const CustomLineTooltip = ({
className="h-2 w-2 rounded-full"
style={{ backgroundColor: item.stroke }}
/>
<span className="text-sm text-white">{item.value}</span>
<span className="text-sm" style={{ color: "var(--chart-text-primary)" }}>
{item.value}
</span>
</div>
{newFindings !== undefined && (
<div className="flex items-center gap-2">
<Bell size={14} className="text-slate-400" />
<span className="text-xs text-slate-400">
<Bell size={14} style={{ color: "var(--chart-text-secondary)" }} />
<span
className="text-xs"
style={{ color: "var(--chart-text-secondary)" }}
>
{newFindings} New Findings
</span>
</div>
)}
{change !== undefined && typeof change === "number" && (
<p className="text-xs text-slate-400">
<p className="text-xs" style={{ color: "var(--chart-text-secondary)" }}>
<span className="font-bold">
{change > 0 ? "+" : ""}
{change}%
+15 -3
View File
@@ -36,15 +36,27 @@ const CustomTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
const data = payload[0];
return (
<div className="rounded-lg border border-slate-700 bg-slate-800 p-3 shadow-lg">
<p className="text-sm font-semibold text-white">
<div
className="rounded-lg border p-3 shadow-lg"
style={{
borderColor: CHART_COLORS.tooltipBorder,
backgroundColor: CHART_COLORS.tooltipBackground,
}}
>
<p
className="text-sm font-semibold"
style={{ color: CHART_COLORS.textPrimary }}
>
{data.payload.category}
</p>
<div className="mt-1">
<AlertPill value={data.value} />
</div>
{data.payload.change !== undefined && (
<p className="mt-1 text-xs text-slate-400">
<p
className="mt-1 text-xs"
style={{ color: CHART_COLORS.textSecondary }}
>
<span className="font-bold">
{data.payload.change > 0 ? "+" : ""}
{data.payload.change}%
+18 -6
View File
@@ -66,10 +66,23 @@ const CustomTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
return (
<div className="rounded-lg border border-slate-700 bg-slate-800 p-3 shadow-lg">
<p className="text-sm font-semibold text-white">{data.name}</p>
<div
className="rounded-lg border p-3 shadow-lg"
style={{
borderColor: CHART_COLORS.tooltipBorder,
backgroundColor: CHART_COLORS.tooltipBackground,
}}
>
<p
className="text-sm font-semibold"
style={{ color: CHART_COLORS.textPrimary }}
>
{data.name}
</p>
{data.value && (
<p className="text-xs text-slate-400">Value: {data.value}</p>
<p className="text-xs" style={{ color: CHART_COLORS.textSecondary }}>
Value: {data.value}
</p>
)}
</div>
);
@@ -143,7 +156,7 @@ const CustomNode = (props: any) => {
x={isOut ? x - 6 : x + width + 6}
y={y + height / 2}
fontSize="14"
className="fill-white stroke-white"
fill={CHART_COLORS.textPrimary}
>
{nodeName}
</text>
@@ -152,8 +165,7 @@ const CustomNode = (props: any) => {
x={isOut ? x - 6 : x + width + 6}
y={y + height / 2 + 13}
fontSize="12"
className="fill-slate-400 stroke-slate-400"
strokeOpacity="0.5"
fill={CHART_COLORS.textSecondary}
>
{payload.value}
</text>
+17 -3
View File
@@ -45,9 +45,23 @@ const CustomTooltip = ({ active, payload }: any) => {
const severityColor = getSeverityColorByRiskScore(data.x);
return (
<div className="rounded-lg border border-slate-700 bg-slate-800 p-3 shadow-lg">
<p className="text-sm font-semibold text-white">{data.name}</p>
<p className="mt-1 text-xs text-slate-400">
<div
className="rounded-lg border p-3 shadow-lg"
style={{
borderColor: CHART_COLORS.tooltipBorder,
backgroundColor: CHART_COLORS.tooltipBackground,
}}
>
<p
className="text-sm font-semibold"
style={{ color: CHART_COLORS.textPrimary }}
>
{data.name}
</p>
<p
className="mt-1 text-xs"
style={{ color: CHART_COLORS.textSecondary }}
>
<span style={{ color: severityColor }}>{data.x}</span> Risk Score
</p>
<div className="mt-2">
+10 -2
View File
@@ -9,14 +9,22 @@ interface ChartLegendProps {
export function ChartLegend({ items }: ChartLegendProps) {
return (
<div className="bg-card-border mt-4 inline-flex gap-[46px] rounded-full border-2 px-[19px] py-[9px]">
<div
className="mt-4 inline-flex gap-[46px] rounded-full border-2 px-[19px] py-[9px]"
style={{ borderColor: "var(--chart-border)" }}
>
{items.map((item, index) => (
<div key={`legend-${index}`} className="flex items-center gap-1">
<div
className="h-3 w-3 rounded"
style={{ backgroundColor: item.color }}
/>
<span className="text-xs text-gray-300">{item.label}</span>
<span
className="text-xs"
style={{ color: "var(--chart-text-secondary)" }}
>
{item.label}
</span>
</div>
))}
</div>
+1 -1
View File
@@ -21,7 +21,7 @@ export const STATUS_COLORS = {
export const CHART_COLORS = {
tooltipBorder: "var(--chart-border-emphasis)",
tooltipBackground: "var(--chart-background)",
textPrimary: "#ffffff",
textPrimary: "var(--chart-text-primary)",
textSecondary: "var(--chart-text-secondary)",
gridLine: "var(--chart-border-emphasis)",
backgroundTrack: "rgba(51, 65, 85, 0.5)", // slate-700 with 50% opacity
+84 -17
View File
@@ -118,29 +118,41 @@ function MapTooltip({
location: LocationPoint;
position: { x: number; y: number };
}) {
const CHART_COLORS = {
tooltipBorder: "var(--chart-border-emphasis)",
tooltipBackground: "var(--chart-background)",
textPrimary: "var(--chart-text-primary)",
textSecondary: "var(--chart-text-secondary)",
};
return (
<div
className="pointer-events-none absolute z-50 min-w-[200px] rounded-lg border border-slate-700 bg-slate-800 p-3 shadow-lg"
className="pointer-events-none absolute z-50 min-w-[200px] rounded-lg border p-3 shadow-lg"
style={{
left: `${position.x + 15}px`,
top: `${position.y + 15}px`,
transform: "translate(0, -50%)",
borderColor: CHART_COLORS.tooltipBorder,
backgroundColor: CHART_COLORS.tooltipBackground,
}}
>
<div className="flex items-center gap-2">
<MapPin size={14} className="text-slate-400" />
<span className="text-sm font-semibold text-white">
<MapPin size={14} style={{ color: CHART_COLORS.textSecondary }} />
<span
className="text-sm font-semibold"
style={{ color: CHART_COLORS.textPrimary }}
>
{location.name}
</span>
</div>
<div className="mt-1 flex items-center gap-2">
<AlertTriangle size={14} className="text-[#DB2B49]" />
<span className="text-sm text-white">
<span className="text-sm" style={{ color: CHART_COLORS.textPrimary }}>
{location.totalFindings.toLocaleString()} Fail Findings
</span>
</div>
{location.change !== undefined && (
<p className="mt-1 text-xs text-slate-400">
<p className="mt-1 text-xs" style={{ color: CHART_COLORS.textSecondary }}>
<span className="font-bold">
{location.change > 0 ? "+" : ""}
{location.change}%
@@ -153,11 +165,23 @@ function MapTooltip({
}
function EmptyState() {
const CHART_COLORS = {
tooltipBorder: "var(--chart-border-emphasis)",
tooltipBackground: "var(--chart-background)",
textSecondary: "var(--chart-text-secondary)",
};
return (
<div className="flex h-full min-h-[400px] items-center justify-center rounded-lg border border-slate-700 bg-slate-800 p-6">
<div
className="flex h-full min-h-[400px] items-center justify-center rounded-lg border p-6"
style={{
borderColor: CHART_COLORS.tooltipBorder,
backgroundColor: CHART_COLORS.tooltipBackground,
}}
>
<div className="text-center">
<Info size={48} className="mx-auto mb-2 text-slate-500" />
<p className="text-sm text-slate-400">
<Info size={48} className="mx-auto mb-2" style={{ color: CHART_COLORS.textSecondary }} />
<p className="text-sm" style={{ color: CHART_COLORS.textSecondary }}>
Select a location on the map to view details
</p>
</div>
@@ -166,10 +190,16 @@ function EmptyState() {
}
function LoadingState({ height }: { height: number }) {
const CHART_COLORS = {
textSecondary: "var(--chart-text-secondary)",
};
return (
<div className="flex items-center justify-center" style={{ height }}>
<div className="text-center">
<div className="mb-2 text-slate-400">Loading map...</div>
<div className="mb-2" style={{ color: CHART_COLORS.textSecondary }}>
Loading map...
</div>
</div>
</div>
);
@@ -350,17 +380,34 @@ export function ThreatMap({
isLoadingMap,
]);
const CHART_COLORS = {
tooltipBorder: "var(--chart-border-emphasis)",
tooltipBackground: "var(--chart-background)",
textPrimary: "var(--chart-text-primary)",
textSecondary: "var(--chart-text-secondary)",
};
return (
<div className="flex w-full flex-col gap-6 lg:flex-row lg:items-start">
{/* Map Section */}
<div className="flex-1">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold text-white">Threat Map</h3>
<h3
className="text-lg font-semibold"
style={{ color: CHART_COLORS.textPrimary }}
>
Threat Map
</h3>
<div className="relative">
<select
value={selectedRegion}
onChange={(e) => setSelectedRegion(e.target.value)}
className="appearance-none rounded-lg border border-slate-700 bg-slate-800 px-4 py-2 pr-10 text-sm text-white focus:border-slate-600 focus:outline-none"
className="appearance-none rounded-lg border px-4 py-2 pr-10 text-sm focus:outline-none"
style={{
borderColor: CHART_COLORS.tooltipBorder,
backgroundColor: CHART_COLORS.tooltipBackground,
color: CHART_COLORS.textPrimary,
}}
>
<option value="All Regions">All Regions</option>
{data.regions.map((region) => (
@@ -371,14 +418,19 @@ export function ThreatMap({
</select>
<ChevronDown
size={16}
className="pointer-events-none absolute top-1/2 right-3 -translate-y-1/2 text-slate-400"
className="pointer-events-none absolute top-1/2 right-3 -translate-y-1/2"
style={{ color: CHART_COLORS.textSecondary }}
/>
</div>
</div>
<div
ref={containerRef}
className="rounded-lg border border-slate-700 bg-slate-800/50 p-4"
className="rounded-lg border p-4"
style={{
borderColor: CHART_COLORS.tooltipBorder,
backgroundColor: CHART_COLORS.tooltipBackground,
}}
>
{isLoadingMap ? (
<LoadingState height={dimensions.height} />
@@ -401,7 +453,10 @@ export function ThreatMap({
</div>
<div className="mt-4 flex items-center gap-2">
<div className="h-3 w-3 rounded-full bg-[#DB2B49]" />
<span className="text-sm text-slate-400">
<span
className="text-sm"
style={{ color: CHART_COLORS.textSecondary }}
>
{filteredLocations.length} Locations
</span>
</div>
@@ -414,15 +469,27 @@ export function ThreatMap({
<div className="w-full lg:w-[400px]">
<div className="mb-4 h-10" />
{selectedLocation ? (
<div className="rounded-lg border border-slate-700 bg-slate-800 p-6">
<div
className="rounded-lg border p-6"
style={{
borderColor: CHART_COLORS.tooltipBorder,
backgroundColor: CHART_COLORS.tooltipBackground,
}}
>
<div className="mb-6">
<div className="mb-1 flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-[#86DA26]" />
<h4 className="text-base font-semibold text-white">
<h4
className="text-base font-semibold"
style={{ color: CHART_COLORS.textPrimary }}
>
{selectedLocation.name}
</h4>
</div>
<p className="text-sm text-slate-400">
<p
className="text-sm"
style={{ color: CHART_COLORS.textSecondary }}
>
{selectedLocation.totalFindings.toLocaleString()} Total Findings
</p>
</div>
@@ -1,5 +1,7 @@
"use client";
import { useState } from "react";
import { ScatterPlot } from "@/components/graphs";
interface DataPoint {
@@ -23,12 +25,16 @@ export function ScatterPlotWithSelection({
xLabel = "Risk Score",
yLabel = "Compliance %",
}: ScatterPlotWithSelectionProps) {
const [selectedPoint, setSelectedPoint] = useState<DataPoint | null>(null);
return (
<ScatterPlot
data={data}
height={height}
xLabel={xLabel}
yLabel={yLabel}
selectedPoint={selectedPoint}
onSelectPoint={setSelectedPoint}
/>
);
}
+39 -2
View File
@@ -2,7 +2,7 @@
@config "../tailwind.config.js";
@theme {
/* Chart Severity Colors */
/* Chart Severity Colors - Dark Theme */
--chart-info: #2e51b2;
--chart-warning: #fdd34f;
--chart-warning-emphasis: #ff7d19;
@@ -22,7 +22,8 @@
--chart-provider-azure: #00bcd4;
--chart-provider-google: #EA4335;
/* Chart UI Colors */
/* Chart UI Colors - Dark Theme (defaults) */
--chart-text-primary: #ffffff;
--chart-text-secondary: #94a3b8;
--chart-border: #475569;
--chart-border-emphasis: #334155;
@@ -35,6 +36,24 @@
@layer base {
:root {
/* Light Theme Chart Colors */
--chart-info: #1e40af;
--chart-warning: #d97706;
--chart-warning-emphasis: #dc2626;
--chart-danger: #dc2626;
--chart-danger-emphasis: #991b1b;
--chart-success-color: #16a34a;
--chart-fail: #dc2626;
--chart-radar-primary: #9d174d;
--chart-text-primary: #1f2937;
--chart-text-secondary: #6b7280;
--chart-border: #d1d5db;
--chart-border-emphasis: #9ca3af;
--chart-background: #f9fafb;
--chart-alert-bg: #fecdd3;
--chart-alert-text: #be123c;
/* Chart HSL values */
--chart-success: 146 80% 35%;
--chart-fail: 339 90% 51%;
--chart-muted: 45 93% 47%;
@@ -49,6 +68,24 @@
}
.dark {
/* Dark Theme Chart Colors */
--chart-info: #2e51b2;
--chart-warning: #fdd34f;
--chart-warning-emphasis: #ff7d19;
--chart-danger: #ff3077;
--chart-danger-emphasis: #971348;
--chart-success-color: #86da26;
--chart-fail: #db2b49;
--chart-radar-primary: #b51c80;
--chart-text-primary: #ffffff;
--chart-text-secondary: #94a3b8;
--chart-border: #475569;
--chart-border-emphasis: #334155;
--chart-background: #1e293b;
--chart-alert-bg: #432232;
--chart-alert-text: #f54280;
/* Chart HSL values */
--chart-success: 146 80% 35%;
--chart-fail: 339 90% 51%;
--chart-muted: 45 93% 47%;