Skip to content

Commit b117833

Browse files
committed
feat: improve chart performance and styling
1 parent 768c2bb commit b117833

File tree

2 files changed

+169
-155
lines changed

2 files changed

+169
-155
lines changed

dashboard/src/components/charts/AllNodesStackedBarChart.tsx

Lines changed: 156 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { useEffect, useState, useMemo } from 'react'
1+
import { useEffect, useState, useMemo, useCallback } from 'react'
22
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from 'recharts'
33
import { DateRange } from 'react-day-picker'
44
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
5-
import { type ChartConfig, ChartContainer, ChartTooltip, ChartLegend, ChartLegendContent } from '@/components/ui/chart'
5+
import { type ChartConfig, ChartContainer, ChartTooltip } from '@/components/ui/chart'
66
import { useTranslation } from 'react-i18next'
77
import useDirDetection from '@/hooks/use-dir-detection'
88
import { getUsage, Period, type NodeUsageStat } from '@/service/api'
@@ -159,57 +159,44 @@ export function AllNodesStackedBarChart() {
159159
// Build color palette for nodes
160160
const nodeList: NodeResponse[] = useMemo(() => (Array.isArray(nodesData) ? nodesData : []), [nodesData])
161161

162-
// Helper function to extract hue from HSL string
163-
const extractHue = (hslString: string): number => {
164-
const match = hslString.match(/hsl\(([^,]+)/)
165-
if (match) {
166-
const hue = parseFloat(match[1])
167-
return isNaN(hue) ? 0 : hue
168-
}
169-
return 0
170-
}
171162

172163
// Function to generate distinct colors based on theme
173-
const generateDistinctColor = (index: number, _totalNodes: number, isDark: boolean): string => {
174-
// Get current theme colors for reference
175-
const root = document.documentElement
176-
const computedStyle = getComputedStyle(root)
177-
178-
// Extract base colors from current theme
179-
const primaryHue = extractHue(computedStyle.getPropertyValue('--primary'))
180-
const chart1Hue = extractHue(computedStyle.getPropertyValue('--chart-1'))
181-
const chart2Hue = extractHue(computedStyle.getPropertyValue('--chart-2'))
182-
183-
// Create a palette of distinct hues
184-
const baseHues = [primaryHue, chart1Hue, chart2Hue]
185-
const additionalHues = [
186-
(primaryHue + 60) % 360, // Complementary
187-
(primaryHue + 120) % 360, // Triadic
188-
(primaryHue + 180) % 360, // Opposite
189-
(primaryHue + 240) % 360, // Triadic
190-
(primaryHue + 300) % 360, // Triadic
191-
(chart1Hue + 60) % 360,
192-
(chart1Hue + 120) % 360,
193-
(chart2Hue + 60) % 360,
194-
(chart2Hue + 120) % 360,
195-
(primaryHue + 30) % 360, // Analogous
196-
(primaryHue + 90) % 360, // Split complementary
197-
(primaryHue + 150) % 360,
198-
(primaryHue + 210) % 360,
199-
(primaryHue + 270) % 360,
200-
(primaryHue + 330) % 360,
164+
const generateDistinctColor = useCallback((index: number, _totalNodes: number, isDark: boolean): string => {
165+
// Define a more distinct color palette with better contrast
166+
const distinctHues = [
167+
0, // Red
168+
30, // Orange
169+
60, // Yellow
170+
120, // Green
171+
180, // Cyan
172+
210, // Blue
173+
240, // Indigo
174+
270, // Purple
175+
300, // Magenta
176+
330, // Pink
177+
15, // Red-orange
178+
45, // Yellow-orange
179+
75, // Yellow-green
180+
150, // Green-cyan
181+
200, // Cyan-blue
182+
225, // Blue-indigo
183+
255, // Indigo-purple
184+
285, // Purple-magenta
185+
315, // Magenta-pink
186+
345, // Pink-red
201187
]
202188

203-
const allHues = [...baseHues, ...additionalHues]
204-
const hue = allHues[index % allHues.length]
189+
const hue = distinctHues[index % distinctHues.length]
190+
191+
// Create more distinct saturation and lightness values
192+
const saturationVariations = [65, 75, 85, 70, 80, 60, 90, 55, 95, 50]
193+
const lightnessVariations = isDark ? [45, 55, 35, 50, 40, 60, 30, 65, 25, 70] : [40, 50, 30, 45, 35, 55, 25, 60, 20, 65]
205194

206-
// For dark theme: higher saturation, moderate lightness
207-
// For light theme: moderate saturation, lower lightness
208-
const saturation = isDark ? 70 + (index % 20) : 60 + (index % 25)
209-
const lightness = isDark ? 50 + (index % 15) : 45 + (index % 20)
195+
const saturation = saturationVariations[index % saturationVariations.length]
196+
const lightness = lightnessVariations[index % lightnessVariations.length]
210197

211198
return `hsl(${hue}, ${saturation}%, ${lightness}%)`
212-
}
199+
}, [])
213200

214201
// Build chart config dynamically based on nodes
215202
const chartConfig = useMemo(() => {
@@ -233,33 +220,12 @@ export function AllNodesStackedBarChart() {
233220
}
234221
})
235222
return config
236-
}, [nodeList, resolvedTheme])
237-
238-
// Chart configuration for the container
239-
const chartContainerConfig = useMemo(() => {
240-
const config: ChartConfig = {}
241-
const isDark = resolvedTheme === 'dark'
242-
nodeList.forEach((node, idx) => {
243-
let color
244-
if (idx === 0) {
245-
// First node uses primary color like CostumeBarChart
246-
color = 'hsl(var(--primary))'
247-
} else if (idx < 5) {
248-
// Use palette colors for nodes 2-5: --chart-2, --chart-3, ...
249-
color = `hsl(var(--chart-${idx + 1}))`
250-
} else {
251-
// Generate distinct colors for nodes beyond palette
252-
color = generateDistinctColor(idx, nodeList.length, isDark)
253-
}
254-
config[node.name] = {
255-
label: node.name,
256-
color: color,
257-
}
258-
})
259-
return config
260-
}, [nodeList, resolvedTheme])
223+
}, [nodeList, resolvedTheme, generateDistinctColor])
261224

262225
useEffect(() => {
226+
let isCancelled = false
227+
let timeoutId: NodeJS.Timeout
228+
263229
const fetchUsageData = async () => {
264230
if (!dateRange?.from || !dateRange?.to) {
265231
setChartData(null)
@@ -273,12 +239,8 @@ export function AllNodesStackedBarChart() {
273239
const endDate = dateRange.to
274240
const period = getPeriodFromDateRange(dateRange)
275241

276-
// Check if end date is today
277-
const today = dateUtils.toDayjs(new Date())
278-
const isEndDateToday = dateUtils.toDayjs(endDate).isSame(today, 'day')
279-
280-
// Use current time if end date is today, otherwise use end of day
281-
const endTime = isEndDateToday ? new Date().toISOString() : dateUtils.toDayjs(endDate).endOf('day').toISOString()
242+
// Always use end of day for daily period to avoid extra bars
243+
const endTime = period === Period.day ? dateUtils.toDayjs(endDate).endOf('day').toISOString() : new Date().toISOString()
282244

283245
const params: Parameters<typeof getUsage>[0] = {
284246
period: period,
@@ -287,6 +249,9 @@ export function AllNodesStackedBarChart() {
287249
group_by_node: true,
288250
}
289251
const response = await getUsage(params)
252+
253+
// Check if component is still mounted
254+
if (isCancelled) return
290255

291256
// API response and nodes list logged
292257

@@ -348,12 +313,14 @@ export function AllNodesStackedBarChart() {
348313
})
349314

350315
// Final chart data (aggregated) processed
351-
setChartData(data)
352-
353-
// Calculate total usage
354-
const total = aggregatedStats.reduce((sum: number, point: NodeUsageStat) => sum + point.uplink + point.downlink, 0)
355-
const formattedTotal = formatBytes(total, 2)
356-
if (typeof formattedTotal === 'string') setTotalUsage(formattedTotal)
316+
if (!isCancelled) {
317+
setChartData(data)
318+
319+
// Calculate total usage
320+
const total = aggregatedStats.reduce((sum: number, point: NodeUsageStat) => sum + point.uplink + point.downlink, 0)
321+
const formattedTotal = formatBytes(total, 2)
322+
if (typeof formattedTotal === 'string') setTotalUsage(formattedTotal)
323+
}
357324
} else {
358325
setChartData(null)
359326
setTotalUsage('0')
@@ -408,13 +375,15 @@ export function AllNodesStackedBarChart() {
408375
})
409376

410377
// Final chart data processed
411-
setChartData(data)
412-
413-
// Calculate total usage
414-
let total = 0
415-
Object.values(statsByNode).forEach((arr) => arr.forEach((stat) => { total += stat.uplink + stat.downlink }))
416-
const formattedTotal = formatBytes(total, 2)
417-
if (typeof formattedTotal === 'string') setTotalUsage(formattedTotal)
378+
if (!isCancelled) {
379+
setChartData(data)
380+
381+
// Calculate total usage
382+
let total = 0
383+
Object.values(statsByNode).forEach((arr) => arr.forEach((stat) => { total += stat.uplink + stat.downlink }))
384+
const formattedTotal = formatBytes(total, 2)
385+
if (typeof formattedTotal === 'string') setTotalUsage(formattedTotal)
386+
}
418387
} else {
419388
// No periods found, setting empty data
420389
setChartData(null)
@@ -427,10 +396,23 @@ export function AllNodesStackedBarChart() {
427396
setTotalUsage('0')
428397
console.error('Error fetching usage data:', err)
429398
} finally {
430-
setIsLoading(false)
399+
if (!isCancelled) {
400+
setIsLoading(false)
401+
}
402+
}
403+
}
404+
405+
// Debounce the API call to prevent excessive requests during zoom
406+
timeoutId = setTimeout(() => {
407+
fetchUsageData()
408+
}, 300)
409+
410+
return () => {
411+
isCancelled = true
412+
if (timeoutId) {
413+
clearTimeout(timeoutId)
431414
}
432415
}
433-
fetchUsageData()
434416
}, [dateRange, nodeList])
435417

436418
// Add effect to update dateRange when selectedTime changes
@@ -443,11 +425,15 @@ export function AllNodesStackedBarChart() {
443425
} else if (selectedTime === '24h') {
444426
from = new Date(now.getTime() - 24 * 60 * 60 * 1000)
445427
} else if (selectedTime === '3d') {
446-
from = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000)
428+
from = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000)
447429
} else if (selectedTime === '1w') {
448-
from = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
430+
from = new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000)
431+
}
432+
if (from) {
433+
// For 1w and 3d, set to end of current day to avoid extra bar
434+
const to = (selectedTime === '1w' || selectedTime === '3d') ? dateUtils.toDayjs(now).endOf('day').toDate() : now
435+
setDateRange({ from, to })
449436
}
450-
if (from) setDateRange({ from, to: now })
451437
}
452438
}, [selectedTime, showCustomRange])
453439

@@ -493,61 +479,83 @@ export function AllNodesStackedBarChart() {
493479
className="max-h-[400px] min-h-[200px]"
494480
/>
495481
) : (
496-
<ChartContainer
497-
dir={'ltr'}
498-
config={chartContainerConfig}
499-
className="max-h-[400px] min-h-[200px] w-full overflow-x-auto"
500-
>
501-
{chartData && chartData.length > 0 ? (
502-
<BarChart
503-
accessibilityLayer
504-
data={chartData}
505-
margin={{ top: 5, right: 10, left: 10, bottom: 5 }}
506-
>
507-
<CartesianGrid direction={'ltr'} vertical={false} />
508-
<XAxis
509-
direction={'ltr'}
510-
dataKey="time"
511-
tickLine={false}
512-
tickMargin={10}
513-
axisLine={false}
514-
minTickGap={5}
515-
/>
516-
<YAxis
517-
direction={'ltr'}
518-
tickLine={false}
519-
axisLine={false}
520-
tickFormatter={value => `${value.toFixed(2)} GB`}
521-
tick={{
522-
fill: 'hsl(var(--muted-foreground))',
523-
fontSize: 9,
524-
fontWeight: 500,
525-
}}
526-
width={32}
527-
tickMargin={2}
528-
/>
529-
{/* When using ChartTooltip, pass period as a prop */}
530-
<ChartTooltip cursor={false} content={<CustomTooltip chartConfig={chartConfig} dir={dir} period={getPeriodFromDateRange(dateRange)} />} />
531-
<ChartLegend className={"overflow-x-auto justify-evenly"} content={<ChartLegendContent />} />
532-
{nodeList.map((node, idx) => (
533-
<Bar
534-
key={node.id}
535-
dataKey={node.name}
536-
stackId="a"
537-
fill={chartConfig[node.name]?.color || `hsl(var(--chart-${(idx % 5) + 1}))`}
538-
radius={idx === 0 ? [0, 0, 4, 4] : idx === nodeList.length - 1 ? [4, 4, 0, 0] : [0, 0, 0, 0]}
482+
<div className="w-full max-w-7xl mx-auto">
483+
<ChartContainer
484+
dir={'ltr'}
485+
config={chartConfig}
486+
className="max-h-[400px] min-h-[200px] w-full"
487+
>
488+
{chartData && chartData.length > 0 ? (
489+
<BarChart
490+
accessibilityLayer
491+
data={chartData}
492+
margin={{ top: 5, right: 10, left: 10, bottom: 5 }}
493+
>
494+
<CartesianGrid direction={'ltr'} vertical={false} />
495+
<XAxis
496+
direction={'ltr'}
497+
dataKey="time"
498+
tickLine={false}
499+
tickMargin={10}
500+
axisLine={false}
501+
minTickGap={5}
539502
/>
540-
))}
541-
</BarChart>
542-
) : (
543-
<EmptyState
544-
type="no-data"
545-
title={t('statistics.noDataInRange')}
546-
description={t('statistics.noDataInRangeDescription')}
547-
className="max-h-[400px] min-h-[200px]"
548-
/>
503+
<YAxis
504+
direction={'ltr'}
505+
tickLine={false}
506+
axisLine={false}
507+
tickFormatter={value => `${value.toFixed(2)} GB`}
508+
tick={{
509+
fill: 'hsl(var(--muted-foreground))',
510+
fontSize: 9,
511+
fontWeight: 500,
512+
}}
513+
width={32}
514+
tickMargin={2}
515+
/>
516+
{/* When using ChartTooltip, pass period as a prop */}
517+
<ChartTooltip cursor={false} content={<CustomTooltip chartConfig={chartConfig} dir={dir} period={getPeriodFromDateRange(dateRange)} />} />
518+
{nodeList.map((node, idx) => (
519+
<Bar
520+
key={node.id}
521+
dataKey={node.name}
522+
stackId="a"
523+
fill={chartConfig[node.name]?.color || `hsl(var(--chart-${(idx % 5) + 1}))`}
524+
radius={nodeList.length === 1 ? [4, 4, 4, 4] : idx === 0 ? [0, 0, 4, 4] : idx === nodeList.length - 1 ? [4, 4, 0, 0] : [0, 0, 0, 0]}
525+
/>
526+
))}
527+
</BarChart>
528+
) : (
529+
<EmptyState
530+
type="no-data"
531+
title={t('statistics.noDataInRange')}
532+
description={t('statistics.noDataInRangeDescription')}
533+
className="max-h-[400px] min-h-[200px]"
534+
/>
535+
)}
536+
</ChartContainer>
537+
{/* Separate scrollable legend */}
538+
{chartData && chartData.length > 0 && (
539+
<div className="overflow-x-auto pt-3">
540+
<div className="flex items-center justify-center gap-4 min-w-max">
541+
{nodeList.map((node) => {
542+
const itemConfig = chartConfig[node.name]
543+
return (
544+
<div key={node.id} className="flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground">
545+
<div
546+
className="h-2 w-2 shrink-0 rounded-[2px]"
547+
style={{
548+
backgroundColor: itemConfig?.color || 'hsl(var(--chart-1))',
549+
}}
550+
/>
551+
<span className="whitespace-nowrap text-xs">{node.name}</span>
552+
</div>
553+
)
554+
})}
555+
</div>
556+
</div>
549557
)}
550-
</ChartContainer>
558+
</div>
551559
)}
552560
</CardContent>
553561
</Card>

0 commit comments

Comments
 (0)