Skip to content

Commit 9e85e80

Browse files
authored
feat(docs): add Deps and Build size comparison graphs with tabs (#141)
* feat(docs): add Deps and Build size comparison graphs with tabs * format
1 parent 345ffca commit 9e85e80

6 files changed

Lines changed: 690 additions & 4 deletions

File tree

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
---
2+
import type { ChartDatum } from '../lib/types'
3+
4+
interface Props {
5+
title: string
6+
data: ChartDatum[]
7+
valueFormat: 'count' | 'mb'
8+
}
9+
10+
const { title, data, valueFormat } = Astro.props
11+
const chartPayload = JSON.stringify({ data, valueFormat })
12+
---
13+
14+
<div class="comparison-chart-container">
15+
<h3 class="comparison-chart-title">{title}</h3>
16+
<div class="comparison-chart-wrapper" data-comparison={chartPayload}>
17+
<svg class="comparison-bar-chart" role="img" aria-label={title}></svg>
18+
</div>
19+
<div class="comparison-chart-tooltip"></div>
20+
</div>
21+
22+
<style>
23+
.comparison-chart-container {
24+
position: relative;
25+
width: 100%;
26+
margin-top: 1em;
27+
margin-bottom: 2em;
28+
}
29+
30+
.comparison-chart-title {
31+
font-size: 16px;
32+
font-weight: 600;
33+
color: var(--ft-text);
34+
margin: 0 0 0.75em 0;
35+
}
36+
37+
.comparison-chart-wrapper {
38+
width: 100%;
39+
max-width: 1000px;
40+
margin: 0 auto;
41+
}
42+
43+
.comparison-bar-chart {
44+
width: 100%;
45+
height: 400px;
46+
display: block;
47+
}
48+
49+
@media (max-width: 768px) {
50+
.comparison-bar-chart {
51+
height: 280px;
52+
}
53+
}
54+
55+
.comparison-chart-tooltip {
56+
position: absolute;
57+
pointer-events: none;
58+
background: var(--ft-bg);
59+
border: 1px solid var(--ft-border);
60+
border-radius: 6px;
61+
padding: 8px 12px;
62+
font-size: 13px;
63+
line-height: 1.5;
64+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
65+
opacity: 0;
66+
transition: opacity 0.15s ease;
67+
z-index: 100;
68+
color: var(--ft-text);
69+
}
70+
71+
.comparison-chart-tooltip.visible {
72+
opacity: 1;
73+
}
74+
75+
.comparison-chart-tooltip .tooltip-title {
76+
font-weight: 600;
77+
color: var(--ft-text);
78+
margin-bottom: 4px;
79+
}
80+
81+
.comparison-chart-tooltip .tooltip-value {
82+
color: var(--ft-muted);
83+
}
84+
85+
:global(html.dark) .comparison-chart-tooltip {
86+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
87+
}
88+
89+
.comparison-chart-wrapper :global(.comparison-x-axis-text) {
90+
fill: var(--ft-text);
91+
font-size: 12px;
92+
font-weight: 500;
93+
}
94+
95+
.comparison-chart-wrapper :global(.comparison-y-axis-text) {
96+
fill: var(--ft-muted);
97+
font-size: 12px;
98+
}
99+
100+
.comparison-chart-wrapper :global(.comparison-axis-domain) {
101+
stroke: var(--ft-border);
102+
}
103+
104+
.comparison-chart-wrapper :global(.comparison-axis-tick-line) {
105+
stroke: var(--ft-border);
106+
}
107+
108+
.comparison-chart-wrapper :global(.comparison-grid-line) {
109+
stroke: var(--ft-chart-grid, var(--ft-border));
110+
}
111+
</style>
112+
113+
<script>
114+
import * as d3 from 'd3'
115+
import type { ChartDatum, ComparisonChartPayload } from '../lib/types'
116+
117+
const chartDimensionsCache = new WeakMap<
118+
HTMLElement,
119+
{ width: number; height: number }
120+
>()
121+
122+
function formatValue(value: number, format: 'count' | 'mb'): string {
123+
if (format === 'count') return String(Math.round(value))
124+
return `${value.toFixed(2)} MB`
125+
}
126+
127+
function setTooltipContent(
128+
tooltip: HTMLElement,
129+
name: string,
130+
valueText: string,
131+
) {
132+
const titleDiv = document.createElement('div')
133+
titleDiv.className = 'tooltip-title'
134+
titleDiv.textContent = name
135+
const valueDiv = document.createElement('div')
136+
valueDiv.className = 'tooltip-value'
137+
valueDiv.textContent = valueText
138+
tooltip.replaceChildren(titleDiv, valueDiv)
139+
}
140+
141+
function initComparisonChart(wrapper: HTMLElement) {
142+
const svg = wrapper.querySelector('.comparison-bar-chart') as SVGSVGElement
143+
const container = wrapper.closest('.comparison-chart-container')
144+
const tooltip = container?.querySelector(
145+
'.comparison-chart-tooltip',
146+
) as HTMLElement
147+
if (!svg || !tooltip) return
148+
149+
const dataAttr = wrapper.dataset.comparison
150+
if (!dataAttr) return
151+
152+
let payload: ComparisonChartPayload
153+
try {
154+
payload = JSON.parse(dataAttr) as ComparisonChartPayload
155+
} catch (e) {
156+
console.warn('[ComparisonBarChart] Invalid chart data:', e)
157+
return
158+
}
159+
160+
const { valueFormat } = payload
161+
const rawData = payload.data
162+
if (!rawData?.length) return
163+
164+
const data: ChartDatum[] = rawData.map((d) => ({
165+
name: String(d?.name ?? ''),
166+
value: Math.max(0, Number(d.value) || 0),
167+
}))
168+
169+
const margin = { top: 20, right: 40, bottom: 80, left: 50 }
170+
const rect = wrapper.getBoundingClientRect()
171+
const width = rect.width || 500
172+
const height = rect.height || 300
173+
const innerWidth = width - margin.left - margin.right
174+
const innerHeight = height - margin.top - margin.bottom
175+
176+
d3.select(svg).selectAll('*').remove()
177+
d3.select(svg).attr('viewBox', `0 0 ${width} ${height}`)
178+
179+
const titleEl = wrapper
180+
.closest('.comparison-chart-container')
181+
?.querySelector('.comparison-chart-title')
182+
const chartTitle = titleEl?.textContent ?? 'Comparison'
183+
d3.select(svg)
184+
.append('title')
185+
.text(`${chartTitle}. Bar chart comparing frameworks.`)
186+
d3.select(svg)
187+
.append('desc')
188+
.text(
189+
`Bar chart comparing each framework by ${chartTitle}. See table for exact values.`,
190+
)
191+
192+
const gradientId = `comparisonBarGradient-${crypto.randomUUID()}`
193+
const g = d3
194+
.select(svg)
195+
.append('g')
196+
.attr('transform', `translate(${margin.left},${margin.top})`)
197+
198+
const defs = d3.select(svg).append('defs')
199+
const gradient = defs
200+
.append('linearGradient')
201+
.attr('id', gradientId)
202+
.attr('x1', '0%')
203+
.attr('y1', '100%')
204+
.attr('x2', '0%')
205+
.attr('y2', '0%')
206+
gradient.append('stop').attr('offset', '0%').attr('stop-color', '#7cb560')
207+
gradient.append('stop').attr('offset', '100%').attr('stop-color', '#cf8c3c')
208+
209+
const xScale = d3
210+
.scaleBand()
211+
.domain(data.map((d) => d.name))
212+
.range([0, innerWidth])
213+
.padding(0.25)
214+
215+
const maxValue = d3.max(data, (d) => d.value) ?? 1
216+
const yScale = d3
217+
.scaleLinear()
218+
.domain([0, maxValue * 1.05])
219+
.range([innerHeight, 0])
220+
221+
const minBarHeightPx = 4
222+
function barY(d: ChartDatum) {
223+
return d.value === 0 ? innerHeight - minBarHeightPx : yScale(d.value)
224+
}
225+
function barHeight(d: ChartDatum) {
226+
return d.value === 0 ? minBarHeightPx : innerHeight - yScale(d.value)
227+
}
228+
229+
g.append('g')
230+
.attr('transform', `translate(0,${innerHeight})`)
231+
.call(d3.axisBottom(xScale).tickSize(0))
232+
.call((sel) =>
233+
sel.select('.domain').attr('class', 'comparison-axis-domain'),
234+
)
235+
.call((sel) =>
236+
sel
237+
.selectAll('text')
238+
.attr('class', 'comparison-x-axis-text')
239+
.attr('dy', '1em')
240+
.attr('transform', 'rotate(-45)')
241+
.attr('text-anchor', 'end'),
242+
)
243+
244+
g.append('g')
245+
.call(d3.axisLeft(yScale).tickSize(0))
246+
.call((sel) =>
247+
sel.select('.domain').attr('class', 'comparison-axis-domain'),
248+
)
249+
.call((sel) =>
250+
sel.selectAll('.tick line').attr('class', 'comparison-axis-tick-line'),
251+
)
252+
.call((sel) =>
253+
sel.selectAll('text').attr('class', 'comparison-y-axis-text'),
254+
)
255+
256+
g.append('g')
257+
.attr('class', 'comparison-grid')
258+
.call(
259+
d3
260+
.axisLeft(yScale)
261+
.tickSize(-innerWidth)
262+
.tickFormat(() => ''),
263+
)
264+
.call((sel) => sel.select('.domain').remove())
265+
.call((sel) =>
266+
sel.selectAll('.tick line').attr('class', 'comparison-grid-line'),
267+
)
268+
269+
const bars = g
270+
.selectAll('.comparison-bar')
271+
.data(data)
272+
.enter()
273+
.append('g')
274+
.attr('class', 'comparison-bar')
275+
.attr('transform', (d) => `translate(${xScale(d.name) ?? 0},0)`)
276+
277+
bars
278+
.append('rect')
279+
.attr('x', 0)
280+
.attr('y', innerHeight)
281+
.attr('width', xScale.bandwidth())
282+
.attr('height', 0)
283+
.attr('fill', `url(#${gradientId})`)
284+
.attr('rx', 4)
285+
.attr('ry', 4)
286+
.style('cursor', 'pointer')
287+
288+
bars
289+
.on('mouseenter', function (_event: MouseEvent, d: ChartDatum) {
290+
d3.select(this).select('rect').attr('opacity', 1)
291+
setTooltipContent(tooltip, d.name, formatValue(d.value, valueFormat))
292+
tooltip.classList.add('visible')
293+
})
294+
.on('mousemove', function (event: MouseEvent) {
295+
const containerRect = container?.getBoundingClientRect()
296+
if (!containerRect) return
297+
const x = event.clientX - containerRect.left + 15
298+
const y = event.clientY - containerRect.top - 10
299+
tooltip.style.left = `${x}px`
300+
tooltip.style.top = `${y}px`
301+
})
302+
.on('mouseleave', function () {
303+
d3.select(this).select('rect').attr('opacity', 0.9)
304+
tooltip.classList.remove('visible')
305+
})
306+
307+
bars
308+
.select('rect')
309+
.transition()
310+
.duration(500)
311+
.ease(d3.easeCubicOut)
312+
.attr('y', (d) => barY(d))
313+
.attr('height', (d) => barHeight(d))
314+
.attr('opacity', 0.9)
315+
316+
chartDimensionsCache.set(wrapper, { width, height })
317+
318+
if (typeof ResizeObserver !== 'undefined') {
319+
const ro = new ResizeObserver(() => {
320+
const r = wrapper.getBoundingClientRect()
321+
const w = r.width || 0
322+
const h = r.height || 0
323+
const last = chartDimensionsCache.get(wrapper)
324+
const lastW = last?.width ?? 0
325+
const lastH = last?.height ?? 0
326+
if (w > 0 && h > 0 && (w !== lastW || h !== lastH)) {
327+
ro.disconnect()
328+
initComparisonChart(wrapper)
329+
}
330+
})
331+
ro.observe(wrapper)
332+
}
333+
}
334+
335+
function init() {
336+
document.querySelectorAll('.comparison-chart-wrapper').forEach((el) => {
337+
initComparisonChart(el as HTMLElement)
338+
})
339+
}
340+
341+
if (document.readyState === 'loading') {
342+
document.addEventListener('DOMContentLoaded', init)
343+
} else {
344+
init()
345+
}
346+
</script>

0 commit comments

Comments
 (0)