From 5b03580b92a6af042052808ac468ec21b5561abb Mon Sep 17 00:00:00 2001
From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com>
Date: Thu, 19 Feb 2026 17:35:12 -0600
Subject: [PATCH 1/2] feat(docs): add Deps and Build size comparison graphs
with tabs
---
.../src/components/ComparisonBarChart.astro | 337 ++++++++++++++++++
.../docs/src/components/DependencyStats.astro | 315 +++++++++++++++-
.../docs/src/components/DevTimeChart.astro | 3 +-
packages/docs/src/lib/types.ts | 9 +
packages/docs/src/lib/utils.ts | 2 +-
packages/docs/src/styles/shared.css | 2 +
6 files changed, 664 insertions(+), 4 deletions(-)
create mode 100644 packages/docs/src/components/ComparisonBarChart.astro
create mode 100644 packages/docs/src/lib/types.ts
diff --git a/packages/docs/src/components/ComparisonBarChart.astro b/packages/docs/src/components/ComparisonBarChart.astro
new file mode 100644
index 00000000..08ad2299
--- /dev/null
+++ b/packages/docs/src/components/ComparisonBarChart.astro
@@ -0,0 +1,337 @@
+---
+import type { ChartDatum } from '../lib/types'
+
+interface Props {
+ title: string
+ data: ChartDatum[]
+ valueFormat: 'count' | 'mb'
+}
+
+const { title, data, valueFormat } = Astro.props
+const chartPayload = JSON.stringify({ data, valueFormat })
+---
+
+
+
+
+
+
diff --git a/packages/docs/src/components/DependencyStats.astro b/packages/docs/src/components/DependencyStats.astro
index e0b18652..5be522fa 100644
--- a/packages/docs/src/components/DependencyStats.astro
+++ b/packages/docs/src/components/DependencyStats.astro
@@ -1,13 +1,39 @@
---
import { getCollection } from 'astro:content'
-import { formatBytesToMB, formatTimeMs, getFrameworkSlug } from '../lib/utils'
+import {
+ BYTES_PER_MB,
+ formatBytesToMB,
+ formatTimeMs,
+ getFrameworkSlug,
+} from '../lib/utils'
import '../styles/shared.css'
+import ComparisonBarChart from './ComparisonBarChart.astro'
const devtimeEntries = await getCollection('devtime')
const runtimeEntries = await getCollection('runtime')
const starterStats = devtimeEntries.map((entry) => entry.data)
const ssrStats = runtimeEntries.map((entry) => entry.data)
+
+const validForCharts = starterStats.filter(
+ (f) =>
+ f?.name != null &&
+ Number.isFinite(f.devDependencies) &&
+ Number.isFinite(f.prodDependencies) &&
+ Number.isFinite(f.buildOutputSize) &&
+ Number.isFinite(f.nodeModulesSizeProdOnly),
+)
+
+const depsData = validForCharts.map((f) => ({ name: f.name, value: f.devDependencies }))
+const prodDepsData = validForCharts.map((f) => ({ name: f.name, value: f.prodDependencies }))
+const buildSizeData = validForCharts.map((f) => ({
+ name: f.name,
+ value: f.buildOutputSize / BYTES_PER_MB,
+}))
+const buildSizeProdData = validForCharts.map((f) => ({
+ name: f.name,
+ value: f.nodeModulesSizeProdOnly / BYTES_PER_MB,
+}))
---
@@ -83,6 +109,58 @@ const ssrStats = runtimeEntries.map((entry) => entry.data)
+
+
+
+
+
+
+
+
@@ -121,6 +199,62 @@ const ssrStats = runtimeEntries.map((entry) => entry.data)
+
+
+
+
+
+
+
+
Runtime Performance
SSR Performance
@@ -473,6 +607,122 @@ const ssrStats = runtimeEntries.map((entry) => entry.data)
color: var(--ft-muted);
}
+ .section-with-tabs {
+ margin-top: 16px;
+ margin-bottom: 2em;
+ }
+
+ .chart-tabs-container {
+ margin-top: 1.5em;
+ padding: 1em 1.25em;
+ border: 1px solid var(--ft-border);
+ border-radius: 12px;
+ background: var(--ft-bg-muted);
+ }
+
+ .tablist {
+ display: flex;
+ gap: 0;
+ margin-bottom: 0;
+ border-bottom: 1px solid var(--ft-border);
+ }
+
+ .chart-tablist {
+ margin: 0 0 1em 0;
+ padding: 0;
+ border-bottom: none;
+ gap: 8px;
+ }
+
+ .tab {
+ padding: 10px 20px;
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--ft-muted);
+ background: transparent;
+ border: none;
+ border-bottom: 2px solid transparent;
+ margin-bottom: -1px;
+ cursor: pointer;
+ font-family: inherit;
+ transition: color 0.2s, border-color 0.2s, background-color 0.2s;
+ }
+
+ .chart-tablist .tab {
+ padding: 8px 16px;
+ border-radius: 8px;
+ border-bottom: none;
+ margin-bottom: 0;
+ }
+
+ .tab:hover {
+ color: var(--ft-text);
+ }
+
+ .tab:focus-visible {
+ outline: 2px solid var(--ft-accent);
+ outline-offset: 2px;
+ }
+
+ .tab.active {
+ color: var(--ft-accent);
+ border-bottom-color: currentColor;
+ }
+
+ .chart-tablist .tab.active {
+ background: var(--ft-accent);
+ color: white;
+ border-bottom: none;
+ }
+
+ .tabpanel {
+ margin-top: 0;
+ }
+
+ .tabpanel[hidden] {
+ display: none;
+ }
+
+ .chart-tabpanels {
+ position: relative;
+ min-height: 320px;
+ }
+
+ .chart-tabpanel {
+ transition: opacity 0.2s ease;
+ }
+
+ .chart-tabpanel[hidden] {
+ display: none;
+ opacity: 0;
+ }
+
+ .chart-tabpanel:not([hidden]) {
+ opacity: 1;
+ }
+
+ .methodology-note {
+ margin-top: 0.5em;
+ margin-bottom: 1em;
+ }
+
+ :global(html.dark) .tab {
+ color: var(--ft-muted);
+ }
+
+ :global(html.dark) .tab:hover {
+ color: var(--ft-text);
+ }
+
+ :global(html.dark) .tab.active {
+ color: var(--ft-accent);
+ }
+
+ :global(html.dark) .chart-tablist .tab.active {
+ background: var(--ft-accent);
+ color: var(--ft-bg);
+ }
+
@media screen and (max-width: 768px) {
main {
padding: 20px 16px;
@@ -499,5 +749,68 @@ const ssrStats = runtimeEntries.map((entry) => entry.data)
th {
font-size: 12px;
}
+
+ .tab {
+ padding: 8px 14px;
+ font-size: 13px;
+ }
}
+
+
diff --git a/packages/docs/src/components/DevTimeChart.astro b/packages/docs/src/components/DevTimeChart.astro
index 481217ed..24b5988a 100644
--- a/packages/docs/src/components/DevTimeChart.astro
+++ b/packages/docs/src/components/DevTimeChart.astro
@@ -113,8 +113,7 @@ const chartData = JSON.stringify([
}
.chart-wrapper :global(.grid-line) {
- stroke: var(--ft-border);
- opacity: 0.5;
+ stroke: var(--ft-chart-grid, var(--ft-border));
}
diff --git a/packages/docs/src/lib/types.ts b/packages/docs/src/lib/types.ts
new file mode 100644
index 00000000..19e70790
--- /dev/null
+++ b/packages/docs/src/lib/types.ts
@@ -0,0 +1,9 @@
+export interface ChartDatum {
+ name: string
+ value: number
+}
+
+export interface ComparisonChartPayload {
+ data: ChartDatum[]
+ valueFormat: 'count' | 'mb'
+}
diff --git a/packages/docs/src/lib/utils.ts b/packages/docs/src/lib/utils.ts
index 43c653b4..6b1512bf 100644
--- a/packages/docs/src/lib/utils.ts
+++ b/packages/docs/src/lib/utils.ts
@@ -1,5 +1,5 @@
const BYTES_PER_KB = 1024
-const BYTES_PER_MB = BYTES_PER_KB * BYTES_PER_KB
+export const BYTES_PER_MB = BYTES_PER_KB * BYTES_PER_KB
/**
* Returns the URL slug for a framework details page.
diff --git a/packages/docs/src/styles/shared.css b/packages/docs/src/styles/shared.css
index 1990ed86..890ac6e8 100644
--- a/packages/docs/src/styles/shared.css
+++ b/packages/docs/src/styles/shared.css
@@ -11,6 +11,7 @@
--ft-border: #e5e7eb;
--ft-bg: #ffffff;
--ft-bg-muted: #f9fafb;
+ --ft-chart-grid: rgba(0, 0, 0, 0.18);
}
html.dark {
@@ -19,4 +20,5 @@ html.dark {
--ft-border: #2e2e2e;
--ft-bg: #1a1a1a;
--ft-bg-muted: #242424;
+ --ft-chart-grid: rgba(255, 255, 255, 0.22);
}
From 0d4f3f4cc7a6fecd32caff3e15d16efe00c10c27 Mon Sep 17 00:00:00 2001
From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com>
Date: Thu, 19 Feb 2026 17:41:13 -0600
Subject: [PATCH 2/2] format
---
.../src/components/ComparisonBarChart.astro | 31 ++++++++++------
.../docs/src/components/DependencyStats.astro | 35 ++++++++++++++-----
2 files changed, 46 insertions(+), 20 deletions(-)
diff --git a/packages/docs/src/components/ComparisonBarChart.astro b/packages/docs/src/components/ComparisonBarChart.astro
index 08ad2299..6f46cdb7 100644
--- a/packages/docs/src/components/ComparisonBarChart.astro
+++ b/packages/docs/src/components/ComparisonBarChart.astro
@@ -114,7 +114,10 @@ const chartPayload = JSON.stringify({ data, valueFormat })
import * as d3 from 'd3'
import type { ChartDatum, ComparisonChartPayload } from '../lib/types'
- const chartDimensionsCache = new WeakMap()
+ const chartDimensionsCache = new WeakMap<
+ HTMLElement,
+ { width: number; height: number }
+ >()
function formatValue(value: number, format: 'count' | 'mb'): string {
if (format === 'count') return String(Math.round(value))
@@ -177,10 +180,14 @@ const chartPayload = JSON.stringify({ data, valueFormat })
.closest('.comparison-chart-container')
?.querySelector('.comparison-chart-title')
const chartTitle = titleEl?.textContent ?? 'Comparison'
- d3.select(svg).append('title').text(`${chartTitle}. Bar chart comparing frameworks.`)
+ d3.select(svg)
+ .append('title')
+ .text(`${chartTitle}. Bar chart comparing frameworks.`)
d3.select(svg)
.append('desc')
- .text(`Bar chart comparing each framework by ${chartTitle}. See table for exact values.`)
+ .text(
+ `Bar chart comparing each framework by ${chartTitle}. See table for exact values.`,
+ )
const gradientId = `comparisonBarGradient-${crypto.randomUUID()}`
const g = d3
@@ -222,7 +229,9 @@ const chartPayload = JSON.stringify({ data, valueFormat })
g.append('g')
.attr('transform', `translate(0,${innerHeight})`)
.call(d3.axisBottom(xScale).tickSize(0))
- .call((sel) => sel.select('.domain').attr('class', 'comparison-axis-domain'))
+ .call((sel) =>
+ sel.select('.domain').attr('class', 'comparison-axis-domain'),
+ )
.call((sel) =>
sel
.selectAll('text')
@@ -234,11 +243,15 @@ const chartPayload = JSON.stringify({ data, valueFormat })
g.append('g')
.call(d3.axisLeft(yScale).tickSize(0))
- .call((sel) => sel.select('.domain').attr('class', 'comparison-axis-domain'))
+ .call((sel) =>
+ sel.select('.domain').attr('class', 'comparison-axis-domain'),
+ )
.call((sel) =>
sel.selectAll('.tick line').attr('class', 'comparison-axis-tick-line'),
)
- .call((sel) => sel.selectAll('text').attr('class', 'comparison-y-axis-text'))
+ .call((sel) =>
+ sel.selectAll('text').attr('class', 'comparison-y-axis-text'),
+ )
g.append('g')
.attr('class', 'comparison-grid')
@@ -275,11 +288,7 @@ const chartPayload = JSON.stringify({ data, valueFormat })
bars
.on('mouseenter', function (_event: MouseEvent, d: ChartDatum) {
d3.select(this).select('rect').attr('opacity', 1)
- setTooltipContent(
- tooltip,
- d.name,
- formatValue(d.value, valueFormat),
- )
+ setTooltipContent(tooltip, d.name, formatValue(d.value, valueFormat))
tooltip.classList.add('visible')
})
.on('mousemove', function (event: MouseEvent) {
diff --git a/packages/docs/src/components/DependencyStats.astro b/packages/docs/src/components/DependencyStats.astro
index 5be522fa..0c25ce7f 100644
--- a/packages/docs/src/components/DependencyStats.astro
+++ b/packages/docs/src/components/DependencyStats.astro
@@ -24,8 +24,14 @@ const validForCharts = starterStats.filter(
Number.isFinite(f.nodeModulesSizeProdOnly),
)
-const depsData = validForCharts.map((f) => ({ name: f.name, value: f.devDependencies }))
-const prodDepsData = validForCharts.map((f) => ({ name: f.name, value: f.prodDependencies }))
+const depsData = validForCharts.map((f) => ({
+ name: f.name,
+ value: f.devDependencies,
+}))
+const prodDepsData = validForCharts.map((f) => ({
+ name: f.name,
+ value: f.prodDependencies,
+}))
const buildSizeData = validForCharts.map((f) => ({
name: f.name,
value: f.buildOutputSize / BYTES_PER_MB,
@@ -109,7 +115,10 @@ const buildSizeProdData = validForCharts.map((f) => ({
-
+
({
aria-labelledby="deps-deps-tab"
class="tabpanel chart-tabpanel active"
>
-
+
({
-
+
({
margin-bottom: -1px;
cursor: pointer;
font-family: inherit;
- transition: color 0.2s, border-color 0.2s, background-color 0.2s;
+ transition:
+ color 0.2s,
+ border-color 0.2s,
+ background-color 0.2s;
}
.chart-tablist .tab {
@@ -763,9 +782,7 @@ const buildSizeProdData = validForCharts.map((f) => ({
'.chart-tabs-container[data-tab-section]',
)
sections.forEach((section: Element) => {
- const tabs = section.querySelectorAll(
- '[role="tab"]',
- )
+ const tabs = section.querySelectorAll('[role="tab"]')
const panels = section.querySelectorAll('[role="tabpanel"]')
if (tabs.length !== 2 || panels.length !== 2) return