From d028c2b7f8e33355ce10ec73efe2b9d98f09c421 Mon Sep 17 00:00:00 2001 From: Bhuvanesh S Date: Fri, 29 May 2026 19:13:33 +0530 Subject: [PATCH 1/2] feat: add Realtime Networking Intelligence dashboard Signed-off-by: Bhuvanesh S --- .../components/AIEngagementInsights.svelte | 92 +++++ .../src/lib/components/ActivityHeatmap.svelte | 189 ++++++++++ .../src/lib/components/AnalyticsWidget.svelte | 126 +++++++ .../ContributionForecastChart.svelte | 208 +++++++++++ .../lib/components/ContributionRadar.svelte | 193 ++++++++++ .../components/EventInteractionHeatmap.svelte | 124 +++++++ .../web/src/lib/components/GrowthRadar.svelte | 158 ++++++++ .../lib/components/MomentumInsights.svelte | 93 +++++ .../NetworkingGrowthForecast.svelte | 144 ++++++++ .../lib/components/NetworkingPulseCard.svelte | 198 ++++++++++ .../src/lib/components/OSSImpactGraph.svelte | 149 ++++++++ .../RealtimeConnectionStream.svelte | 129 +++++++ .../lib/components/ReputationScoreCard.svelte | 204 +++++++++++ .../lib/components/VelocityScoreCard.svelte | 173 +++++++++ apps/web/src/routes/dashboard/+page.svelte | 338 ++++++++++++++++++ 15 files changed, 2518 insertions(+) create mode 100644 apps/web/src/lib/components/AIEngagementInsights.svelte create mode 100644 apps/web/src/lib/components/ActivityHeatmap.svelte create mode 100644 apps/web/src/lib/components/AnalyticsWidget.svelte create mode 100644 apps/web/src/lib/components/ContributionForecastChart.svelte create mode 100644 apps/web/src/lib/components/ContributionRadar.svelte create mode 100644 apps/web/src/lib/components/EventInteractionHeatmap.svelte create mode 100644 apps/web/src/lib/components/GrowthRadar.svelte create mode 100644 apps/web/src/lib/components/MomentumInsights.svelte create mode 100644 apps/web/src/lib/components/NetworkingGrowthForecast.svelte create mode 100644 apps/web/src/lib/components/NetworkingPulseCard.svelte create mode 100644 apps/web/src/lib/components/OSSImpactGraph.svelte create mode 100644 apps/web/src/lib/components/RealtimeConnectionStream.svelte create mode 100644 apps/web/src/lib/components/ReputationScoreCard.svelte create mode 100644 apps/web/src/lib/components/VelocityScoreCard.svelte create mode 100644 apps/web/src/routes/dashboard/+page.svelte diff --git a/apps/web/src/lib/components/AIEngagementInsights.svelte b/apps/web/src/lib/components/AIEngagementInsights.svelte new file mode 100644 index 00000000..dc5dec13 --- /dev/null +++ b/apps/web/src/lib/components/AIEngagementInsights.svelte @@ -0,0 +1,92 @@ + + + + + diff --git a/apps/web/src/lib/components/ActivityHeatmap.svelte b/apps/web/src/lib/components/ActivityHeatmap.svelte new file mode 100644 index 00000000..acb0577a --- /dev/null +++ b/apps/web/src/lib/components/ActivityHeatmap.svelte @@ -0,0 +1,189 @@ + + +
+
+

Realtime Contributor Activity

+ + Live + +
+ +
+
+ {#each heatmapData as week, wIndex} +
+ {#each week as day, dIndex} +
+ {/each} +
+ {/each} +
+
+ +
+ Less +
+
+
+
+
+ More +
+
+ + diff --git a/apps/web/src/lib/components/AnalyticsWidget.svelte b/apps/web/src/lib/components/AnalyticsWidget.svelte new file mode 100644 index 00000000..17d25bfe --- /dev/null +++ b/apps/web/src/lib/components/AnalyticsWidget.svelte @@ -0,0 +1,126 @@ + + +
+
+
+ {icon} +
+ + {isPositive ? '↑' : '↓'} {trend} + +
+ +
+

{title}

+
{value}
+
+
+ + diff --git a/apps/web/src/lib/components/ContributionForecastChart.svelte b/apps/web/src/lib/components/ContributionForecastChart.svelte new file mode 100644 index 00000000..a8f1b5cc --- /dev/null +++ b/apps/web/src/lib/components/ContributionForecastChart.svelte @@ -0,0 +1,208 @@ + + +
+
+

{title}

+
+ Historical + Predicted +
+
+ + +
+ + diff --git a/apps/web/src/lib/components/ContributionRadar.svelte b/apps/web/src/lib/components/ContributionRadar.svelte new file mode 100644 index 00000000..dc6951d6 --- /dev/null +++ b/apps/web/src/lib/components/ContributionRadar.svelte @@ -0,0 +1,193 @@ + + +
+

Contribution Radar

+ +
+ + + {#each gridLevels as level} + + {/each} + + + {#each data as _, i} + {@const { x, y } = getCoordinates(100, i)} + + {/each} + + + + + + {#each data as d, i} + {@const { x, y } = getCoordinates(d.value, i)} + + {d.label}: {d.value}% + + {/each} + + + {#each labels as label} + + {label.text} + + {/each} + +
+
+ + diff --git a/apps/web/src/lib/components/EventInteractionHeatmap.svelte b/apps/web/src/lib/components/EventInteractionHeatmap.svelte new file mode 100644 index 00000000..9a322895 --- /dev/null +++ b/apps/web/src/lib/components/EventInteractionHeatmap.svelte @@ -0,0 +1,124 @@ + + +
+
+

Event Interaction Heatmap

+
+ +
+ {#each heatmapData as intensity, i} +
+ {/each} +
+ +
+ Quiet +
+
+
+
+
+
+
+ Peak +
+
+ + diff --git a/apps/web/src/lib/components/GrowthRadar.svelte b/apps/web/src/lib/components/GrowthRadar.svelte new file mode 100644 index 00000000..f778d526 --- /dev/null +++ b/apps/web/src/lib/components/GrowthRadar.svelte @@ -0,0 +1,158 @@ + + +
+

Growth Analytics Radar

+ +
+ + + {#each gridLevels as level} + + {/each} + + + {#each data as _, i} + {@const { x, y } = getCoordinates(100, i)} + + {/each} + + + + + + {#each data as d, i} + {@const { x, y } = getCoordinates(d.value, i)} + + {d.label}: {d.value}% + + {/each} + + + {#each data as d, i} + {@const { x, y } = getCoordinates(125, i)} + + {d.label} + + {/each} + +
+
+ + diff --git a/apps/web/src/lib/components/MomentumInsights.svelte b/apps/web/src/lib/components/MomentumInsights.svelte new file mode 100644 index 00000000..6cc2d944 --- /dev/null +++ b/apps/web/src/lib/components/MomentumInsights.svelte @@ -0,0 +1,93 @@ + + +
+
+

⚡ Momentum Insights

+
+ +
    + {#each insights as insight} +
  • + +

    {@html insight.text}

    +
  • + {/each} +
+
+ + diff --git a/apps/web/src/lib/components/NetworkingGrowthForecast.svelte b/apps/web/src/lib/components/NetworkingGrowthForecast.svelte new file mode 100644 index 00000000..651930bb --- /dev/null +++ b/apps/web/src/lib/components/NetworkingGrowthForecast.svelte @@ -0,0 +1,144 @@ + + +
+
+

{title}

+
+ + +
+ + diff --git a/apps/web/src/lib/components/NetworkingPulseCard.svelte b/apps/web/src/lib/components/NetworkingPulseCard.svelte new file mode 100644 index 00000000..864676a5 --- /dev/null +++ b/apps/web/src/lib/components/NetworkingPulseCard.svelte @@ -0,0 +1,198 @@ + + +
+
+
+

Networking Pulse

+ + Live + +
+
+ {status} +
+
+ +
+
+
+ {scansPerHour} + /hr +
+ QR Scan Velocity +
+ +
+ +
+
+ {totalConnections} +
+ Total Connections +
+
+ + +
+ + diff --git a/apps/web/src/lib/components/OSSImpactGraph.svelte b/apps/web/src/lib/components/OSSImpactGraph.svelte new file mode 100644 index 00000000..3fe7518b --- /dev/null +++ b/apps/web/src/lib/components/OSSImpactGraph.svelte @@ -0,0 +1,149 @@ + + +
+
+

{title}

+
+ +
+
+ {maxVal} + {Math.round(maxVal / 2)} + 0 +
+ +
+ {#each data as item} +
+
+
+
+ {item.label} +
+ {/each} +
+
+
+ + diff --git a/apps/web/src/lib/components/RealtimeConnectionStream.svelte b/apps/web/src/lib/components/RealtimeConnectionStream.svelte new file mode 100644 index 00000000..d2874d42 --- /dev/null +++ b/apps/web/src/lib/components/RealtimeConnectionStream.svelte @@ -0,0 +1,129 @@ + + +
+
+

Live Activity Stream

+
+ +
+ {#each stream as item (item.id)} +
+
+ {#if item.type === 'scan'} + + + + {:else if item.type === 'view'} + + + + + {:else} + + + + + + + {/if} +
+
+

{item.user} {item.action}

+ {item.time} +
+
+ {/each} +
+
+ + diff --git a/apps/web/src/lib/components/ReputationScoreCard.svelte b/apps/web/src/lib/components/ReputationScoreCard.svelte new file mode 100644 index 00000000..f59f4133 --- /dev/null +++ b/apps/web/src/lib/components/ReputationScoreCard.svelte @@ -0,0 +1,204 @@ + + +
+
+
+

AI Reputation Score

+ {tier} Tier +
+
+ ↑ {trend} pts +
+
+ +
+ + + + +
+ {score} + / 1000 +
+
+ + +
+ + diff --git a/apps/web/src/lib/components/VelocityScoreCard.svelte b/apps/web/src/lib/components/VelocityScoreCard.svelte new file mode 100644 index 00000000..cc8971cd --- /dev/null +++ b/apps/web/src/lib/components/VelocityScoreCard.svelte @@ -0,0 +1,173 @@ + + +
+
+
+

Contribution Velocity

+ {status} +
+
+ {growthPercentage} +
+
+ +
+
+ {velocityScore} + Current Momentum +
+ +
+ +
+ {predictedScore} + Predicted (30d) +
+
+ + +
+ + diff --git a/apps/web/src/routes/dashboard/+page.svelte b/apps/web/src/routes/dashboard/+page.svelte new file mode 100644 index 00000000..1ace78cd --- /dev/null +++ b/apps/web/src/routes/dashboard/+page.svelte @@ -0,0 +1,338 @@ + + + + Analytics Command Center | DevCard + + +
+
+
+

GitHub Analytics Command Center

+

Realtime insights into contributor intelligence and repository health.

+
+
+ + +
+
+ +
+
+ {#each stats as stat} + + {/each} +
+ +
+
+ +
+ +
+
+

🤖 AI Contribution Insights

+
+
    +
  • + +

    High merge probability detected for your recent frontend PRs based on historical maintainer behavior.

    +
  • +
  • + +

    Your PR review time is slightly above average today. Consider smaller, isolated commits.

    +
  • +
  • + +

    Found 3 open issues matching your React/Next.js skillset with "critical" priority.

    +
  • +
+ +
+
+ +
+

AI Reputation & Impact

+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+

Contribution Velocity Predictor

+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+

Networking & Event Intelligence

+
+ +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + From d2e7bffbbf6f7ccfdf2ec40d989c013fd297e6b9 Mon Sep 17 00:00:00 2001 From: Bhuvanesh S Date: Fri, 29 May 2026 19:19:38 +0530 Subject: [PATCH 2/2] refactor: extract reusable analytics visualization infrastructure Signed-off-by: Bhuvanesh S --- .../components/AIEngagementInsights.svelte | 83 +---------------- .../ContributionForecastChart.svelte | 41 +++----- .../lib/components/ContributionRadar.svelte | 57 +++--------- .../web/src/lib/components/GrowthRadar.svelte | 44 ++++----- .../web/src/lib/components/InsightFeed.svelte | 93 +++++++++++++++++++ .../lib/components/MomentumInsights.svelte | 86 +---------------- .../NetworkingGrowthForecast.svelte | 29 ++---- apps/web/src/lib/utils/chartMath.ts | 84 +++++++++++++++++ apps/web/src/lib/utils/visualizationEngine.ts | 58 ++++++++++++ 9 files changed, 292 insertions(+), 283 deletions(-) create mode 100644 apps/web/src/lib/components/InsightFeed.svelte create mode 100644 apps/web/src/lib/utils/chartMath.ts create mode 100644 apps/web/src/lib/utils/visualizationEngine.ts diff --git a/apps/web/src/lib/components/AIEngagementInsights.svelte b/apps/web/src/lib/components/AIEngagementInsights.svelte index dc5dec13..0f41e1f6 100644 --- a/apps/web/src/lib/components/AIEngagementInsights.svelte +++ b/apps/web/src/lib/components/AIEngagementInsights.svelte @@ -1,4 +1,6 @@ - - - + diff --git a/apps/web/src/lib/components/ContributionForecastChart.svelte b/apps/web/src/lib/components/ContributionForecastChart.svelte index a8f1b5cc..11a66058 100644 --- a/apps/web/src/lib/components/ContributionForecastChart.svelte +++ b/apps/web/src/lib/components/ContributionForecastChart.svelte @@ -11,25 +11,10 @@ title?: string; }>(); + import { getPathData, getAreaPathData, generatePointCoordinates } from '$lib/utils/visualizationEngine'; + let maxVal = $derived(Math.max(...historicalData, ...predictedData) * 1.1); - - // Calculate coordinates for SVG paths - function getPathData(data: number[], width: number, height: number): string { - if (!data.length) return ''; - const stepX = width / (data.length - 1); - - return data.map((val, i) => { - const x = i * stepX; - const y = height - (val / maxVal) * height; - return `${i === 0 ? 'M' : 'L'} ${x} ${y}`; - }).join(' '); - } - - function getAreaPathData(data: number[], width: number, height: number): string { - if (!data.length) return ''; - const linePath = getPathData(data, width, height); - return `${linePath} L ${width} ${height} L 0 ${height} Z`; - } + let predictedCombined = $derived([historicalData[historicalData.length-1], ...predictedData.slice(1)]);
@@ -51,24 +36,24 @@ - - + + - - + + - {#each historicalData as val, i} - - {labels[i]}: {val} contributions + {#each generatePointCoordinates(historicalData, maxVal, 400, 200) as { cx, cy }, i} + + {labels[i]}: {historicalData[i]} contributions {/each} - {#each predictedData.slice(1) as val, i} - - {labels[i+1]} (Predicted): {val} contributions + {#each generatePointCoordinates(predictedCombined, maxVal, 400, 200).slice(1) as { cx, cy }, i} + + {labels[i+1]} (Predicted): {predictedData[i+1]} contributions {/each} diff --git a/apps/web/src/lib/components/ContributionRadar.svelte b/apps/web/src/lib/components/ContributionRadar.svelte index dc6951d6..06f4d5fc 100644 --- a/apps/web/src/lib/components/ContributionRadar.svelte +++ b/apps/web/src/lib/components/ContributionRadar.svelte @@ -13,51 +13,22 @@ size?: number; }>(); - // Radar chart logic + import { + getRadarCoordinates, + generateRadarPoints, + getGridPolygon, + generateLabelCoordinates + } from '$lib/utils/chartMath'; + let center = $derived(size / 2); let radius = $derived(center * 0.7); // Leave room for labels let sides = $derived(data.length); - let angle = $derived((Math.PI * 2) / sides); - - // Calculate coordinates for a given value (0-100) at a specific index - function getCoordinates(value: number, index: number) { - const r = (value / 100) * radius; - // -Math.PI/2 to start at the top - const theta = index * angle - Math.PI / 2; - return { - x: center + r * Math.cos(theta), - y: center + r * Math.sin(theta) - }; - } - // Generate the polygon points string for the data - let dataPoints = $derived( - data.map((d, i) => { - const { x, y } = getCoordinates(d.value, i); - return `${x},${y}`; - }).join(' ') - ); - - // Generate grid levels (20, 40, 60, 80, 100) - let gridLevels = [20, 40, 60, 80, 100]; + let dataValues = $derived(data.map(d => d.value)); + let dataPoints = $derived(generateRadarPoints(dataValues, center, radius)); - function getGridPolygon(level: number) { - let points = []; - for (let i = 0; i < sides; i++) { - const { x, y } = getCoordinates(level, i); - points.push(`${x},${y}`); - } - return points.join(' '); - } - - // Generate label coordinates - let labels = $derived( - data.map((d, i) => { - // Push labels out slightly further than 100% - const { x, y } = getCoordinates(120, i); - return { x, y, text: d.label }; - }) - ); + let gridLevels = [20, 40, 60, 80, 100]; + let labels = $derived(generateLabelCoordinates(data, center, radius, 120));
@@ -68,14 +39,14 @@ {#each gridLevels as level} {/each} {#each data as _, i} - {@const { x, y } = getCoordinates(100, i)} + {@const { x, y } = getRadarCoordinates(100, i, sides, center, radius)} {#each data as d, i} - {@const { x, y } = getCoordinates(d.value, i)} + {@const { x, y } = getRadarCoordinates(d.value, i, sides, center, radius)} (); - // Radar chart math identical to ContributionRadar but heavily optimized + import { + getRadarCoordinates, + generateRadarPoints, + getGridPolygon, + generateLabelCoordinates + } from '$lib/utils/chartMath'; + let center = $derived(size / 2); let radius = $derived(center * 0.65); // Give labels a bit more breathing room let sides = $derived(data.length); - let angle = $derived((Math.PI * 2) / sides); - - function getCoordinates(value: number, index: number) { - const r = (value / 100) * radius; - const theta = index * angle - Math.PI / 2; - return { - x: center + r * Math.cos(theta), - y: center + r * Math.sin(theta) - }; - } - let dataPoints = $derived( - data.map((d, i) => `${getCoordinates(d.value, i).x},${getCoordinates(d.value, i).y}`).join(' ') - ); + let dataValues = $derived(data.map(d => d.value)); + let dataPoints = $derived(generateRadarPoints(dataValues, center, radius)); let gridLevels = [25, 50, 75, 100]; - - function getGridPolygon(level: number) { - return Array.from({ length: sides }).map((_, i) => { - const { x, y } = getCoordinates(level, i); - return `${x},${y}`; - }).join(' '); - } + let labels = $derived(generateLabelCoordinates(data, center, radius, 125));
@@ -49,12 +38,12 @@ {#each gridLevels as level} - + {/each} {#each data as _, i} - {@const { x, y } = getCoordinates(100, i)} + {@const { x, y } = getRadarCoordinates(100, i, sides, center, radius)} {/each} @@ -63,22 +52,21 @@ {#each data as d, i} - {@const { x, y } = getCoordinates(d.value, i)} + {@const { x, y } = getRadarCoordinates(d.value, i, sides, center, radius)} {d.label}: {d.value}% {/each} - {#each data as d, i} - {@const { x, y } = getCoordinates(125, i)} + {#each labels as label} - {d.label} + {label.text} {/each} diff --git a/apps/web/src/lib/components/InsightFeed.svelte b/apps/web/src/lib/components/InsightFeed.svelte new file mode 100644 index 00000000..513c5a5c --- /dev/null +++ b/apps/web/src/lib/components/InsightFeed.svelte @@ -0,0 +1,93 @@ + + +
+
+

+ {#if variant === 'ai'}🤖 {/if} + {title} +

+
+ +
    + {#each items as item} +
  • + + +

    {item.text}

    +
  • + {/each} +
+
+ + diff --git a/apps/web/src/lib/components/MomentumInsights.svelte b/apps/web/src/lib/components/MomentumInsights.svelte index 6cc2d944..4e603a3a 100644 --- a/apps/web/src/lib/components/MomentumInsights.svelte +++ b/apps/web/src/lib/components/MomentumInsights.svelte @@ -1,7 +1,9 @@ -
-
-

⚡ Momentum Insights

-
- -
    - {#each insights as insight} -
  • - -

    {@html insight.text}

    -
  • - {/each} -
-
- - + diff --git a/apps/web/src/lib/components/NetworkingGrowthForecast.svelte b/apps/web/src/lib/components/NetworkingGrowthForecast.svelte index 651930bb..04991966 100644 --- a/apps/web/src/lib/components/NetworkingGrowthForecast.svelte +++ b/apps/web/src/lib/components/NetworkingGrowthForecast.svelte @@ -9,24 +9,9 @@ title?: string; }>(); - let maxVal = $derived(Math.max(...historicalData) * 1.2); - - function getPathData(data: number[], width: number, height: number): string { - if (!data.length) return ''; - const stepX = width / (data.length - 1); - - return data.map((val, i) => { - const x = i * stepX; - const y = height - (val / maxVal) * height; - return `${i === 0 ? 'M' : 'L'} ${x} ${y}`; - }).join(' '); - } + import { getPathData, getAreaPathData, generatePointCoordinates } from '$lib/utils/visualizationEngine'; - function getAreaPathData(data: number[], width: number, height: number): string { - if (!data.length) return ''; - const linePath = getPathData(data, width, height); - return `${linePath} L ${width} ${height} L 0 ${height} Z`; - } + let maxVal = $derived(Math.max(...historicalData) * 1.2);
@@ -43,13 +28,13 @@ - - + + - {#each historicalData as val, i} - - {labels[i]}: {val} connections + {#each generatePointCoordinates(historicalData, maxVal, 400, 200) as { cx, cy }, i} + + {labels[i]}: {historicalData[i]} connections {/each} diff --git a/apps/web/src/lib/utils/chartMath.ts b/apps/web/src/lib/utils/chartMath.ts new file mode 100644 index 00000000..270eadab --- /dev/null +++ b/apps/web/src/lib/utils/chartMath.ts @@ -0,0 +1,84 @@ +export interface Coordinate { + x: number; + y: number; +} + +/** + * Calculates X/Y coordinates for a point on a radar chart + */ +export function getRadarCoordinates( + value: number, + index: number, + sides: number, + center: number, + radius: number +): Coordinate { + // Handle edge case of 0 sides + if (sides === 0) return { x: center, y: center }; + + const angle = (Math.PI * 2) / sides; + const r = (value / 100) * radius; + // -Math.PI/2 to start drawing from the top + const theta = index * angle - Math.PI / 2; + + return { + x: center + r * Math.cos(theta), + y: center + r * Math.sin(theta) + }; +} + +/** + * Generates an SVG polygon points string for a data series + */ +export function generateRadarPoints( + dataValues: number[], + center: number, + radius: number +): string { + const sides = dataValues.length; + if (sides === 0) return ''; + + return dataValues + .map((val, i) => { + const { x, y } = getRadarCoordinates(val, i, sides, center, radius); + return `${x},${y}`; + }) + .join(' '); +} + +/** + * Generates an SVG polygon points string for a background grid level + */ +export function getGridPolygon( + level: number, + sides: number, + center: number, + radius: number +): string { + if (sides === 0) return ''; + + return Array.from({ length: sides }) + .map((_, i) => { + const { x, y } = getRadarCoordinates(level, i, sides, center, radius); + return `${x},${y}`; + }) + .join(' '); +} + +/** + * Generates label coordinates pushed further out from the radar edges + */ +export function generateLabelCoordinates( + data: { label: string; value: number }[], + center: number, + radius: number, + pushFactor: number = 120 +): (Coordinate & { text: string })[] { + const sides = data.length; + if (sides === 0) return []; + + return data.map((d, i) => { + const { x, y } = getRadarCoordinates(pushFactor, i, sides, center, radius); + return { x, y, text: d.label }; + }); +} diff --git a/apps/web/src/lib/utils/visualizationEngine.ts b/apps/web/src/lib/utils/visualizationEngine.ts new file mode 100644 index 00000000..720bc1bb --- /dev/null +++ b/apps/web/src/lib/utils/visualizationEngine.ts @@ -0,0 +1,58 @@ +/** + * Generates an SVG line path string for sequential data + */ +export function getPathData( + data: number[], + maxVal: number, + width: number, + height: number +): string { + if (!data || data.length === 0) return ''; + // Avoid division by zero + if (data.length === 1) return `M 0 ${height - (data[0] / maxVal) * height}`; + + const stepX = width / (data.length - 1); + const safeMax = maxVal > 0 ? maxVal : 1; // Prevent division by zero + + return data.map((val, i) => { + const x = i * stepX; + const y = height - (val / safeMax) * height; + return `${i === 0 ? 'M' : 'L'} ${x} ${y}`; + }).join(' '); +} + +/** + * Generates an SVG area path string (filled down to baseline) for sequential data + */ +export function getAreaPathData( + data: number[], + maxVal: number, + width: number, + height: number +): string { + if (!data || data.length === 0) return ''; + const linePath = getPathData(data, maxVal, width, height); + // Complete the path by drawing to bottom right, then bottom left, then close + return `${linePath} L ${width} ${height} L 0 ${height} Z`; +} + +/** + * Generates an array of coordinates for interactive points + */ +export function generatePointCoordinates( + data: number[], + maxVal: number, + width: number, + height: number +): { cx: number; cy: number }[] { + if (!data || data.length === 0) return []; + if (data.length === 1) return [{ cx: 0, cy: height - (data[0] / maxVal) * height }]; + + const stepX = width / (data.length - 1); + const safeMax = maxVal > 0 ? maxVal : 1; + + return data.map((val, i) => ({ + cx: i * stepX, + cy: height - (val / safeMax) * height + })); +}