From aaffa73d593c46eebed27b9061b6d893c38b08bc Mon Sep 17 00:00:00 2001 From: Alexander Karan <47707063+AlexanderKaran@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:33:10 +0800 Subject: [PATCH 1/2] Made Line Charts Better --- .../src/components/VersionLineChart.astro | 255 ++++++++++++++++-- packages/docs/src/styles/starlight.css | 6 + 2 files changed, 236 insertions(+), 25 deletions(-) diff --git a/packages/docs/src/components/VersionLineChart.astro b/packages/docs/src/components/VersionLineChart.astro index 741bbb8c..6ab22aed 100644 --- a/packages/docs/src/components/VersionLineChart.astro +++ b/packages/docs/src/components/VersionLineChart.astro @@ -1,4 +1,5 @@ --- +import { formatChartValue } from '../lib/chart' import type { ChartDatum, ChartValueFormat } from '../lib/types' type ValueTransform = 'bytesToKb' | 'bytesToMb' | 'msToSeconds' @@ -15,8 +16,8 @@ interface Props { yAxisLabel: string } -const CHART_HEIGHT_PX = 400 -const MOBILE_CHART_HEIGHT_PX = 300 +const CHART_HEIGHT_PX = 340 +const MOBILE_CHART_HEIGHT_PX = 260 const { title, @@ -91,19 +92,77 @@ const chartPayload = JSON.stringify({ yAxisLabel, }) const hasChartData = chartData.length > 0 +const latestDatum = chartData.at(-1) +const previousDatum = chartData.at(-2) +const latestChange = + latestDatum != null && previousDatum != null + ? latestDatum.value - previousDatum.value + : null +const latestChangeLabel = + latestChange == null + ? null + : latestChange === 0 + ? 'No change' + : `${latestChange > 0 ? '+' : 'โˆ’'}${formatChartValue( + Math.abs(latestChange), + valueFormat, + )}` --- { hasChartData ? ( -
-

{title}

- {description ?

{description}

: null} +
+
+
+

{title}

+ {description ?

{description}

: null} +
+ {latestDatum ? ( +
+ + Latest ยท v{latestDatum.name} + + {formatChartValue(latestDatum.value, valueFormat)} + {latestChangeLabel ? ( + + {latestChangeLabel} from previous + + ) : null} +
+ ) : null} +
- + {title} chart +
+ + + + + + + + + + {chartData.map((datum) => ( + + + + + ))} + +
{title} data
Framework version{yAxisLabel}
{datum.name}{formatChartValue(datum.value, valueFormat)}
+

+ The latest release is shown as a filled point; earlier releases use + outlined points. +

+
-
+ ) : null } @@ -115,24 +174,92 @@ const hasChartData = chartData.length > 0 > .version-line-chart { width: 100%; - margin-bottom: 2em; + margin-block: 1.5rem 2.5rem; + margin-inline: 0; } .version-line-chart-title { - font-size: 16px; + margin: 0; + font-size: 1rem; + } + + .version-line-chart-heading { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-bottom: 0.75rem; + } + + .version-line-chart-heading p { + margin-block: 0.35rem 0; + } + + .version-line-chart-latest { + display: grid; + flex: 0 0 auto; + grid-template-columns: auto auto; + align-items: baseline; + column-gap: 0.75rem; + text-align: right; + } + + .version-line-chart-latest-label { + color: var(--ft-muted); + font-size: 0.72rem; + font-weight: 650; + letter-spacing: 0.06em; + text-transform: uppercase; + } + + .version-line-chart-latest strong { + color: var(--ft-text); + font-size: 1.1rem; + font-variant-numeric: tabular-nums; + letter-spacing: -0.02em; + } + + .version-line-chart-change { + grid-column: 1 / -1; + color: var(--ft-muted); + font-size: 0.75rem; + font-variant-numeric: tabular-nums; } .version-line-chart-wrapper { position: relative; + box-sizing: border-box; width: 100%; max-width: 1000px; height: var(--chartHeight); margin-inline: auto; + padding: 1rem 1rem 0.5rem 0.5rem; + border: 1px solid var(--ft-border); + border-radius: 0.75rem; + background: var(--ft-bg); + box-shadow: 0 1px 0 rgb(0 0 0 / 4%); } @media (max-width: 768px) { + .version-line-chart-heading { + display: block; + } + + .version-line-chart-latest { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + margin-top: 0.65rem; + column-gap: 0.5rem; + } + + .version-line-chart-change { + flex-basis: 100%; + } + .version-line-chart-wrapper { height: var(--mobileChartHeight); + padding: 0.75rem 0.5rem 0.25rem 0.25rem; } } @@ -141,6 +268,7 @@ const hasChartData = chartData.length > 0 import { CategoryScale, Chart, + Filler, Legend, LineController, LineElement, @@ -157,6 +285,7 @@ const hasChartData = chartData.length > 0 Chart.register( CategoryScale, + Filler, Legend, LineController, LineElement, @@ -166,11 +295,13 @@ const hasChartData = chartData.length > 0 ) const chartCache = new WeakMap() - const LINE_COLOR = '#8f7bd1' - const POINT_COLOR = '#6f5ab8' + const LINE_COLOR = '#697586' + const POINT_COLOR = '#4b5565' const TEXT_COLOR_FALLBACK = '#1f2933' const MUTED_COLOR_FALLBACK = '#697586' const BORDER_COLOR_FALLBACK = '#d9dee7' + const LINE_FILL_FALLBACK = 'rgba(105, 117, 134, 0.12)' + const LINE_FILL_TRANSPARENT_FALLBACK = 'rgba(105, 117, 134, 0)' function formatAxisTick( value: string | number, @@ -225,8 +356,25 @@ const hasChartData = chartData.length > 0 const mutedColor = getCssVar(root, '--ft-muted', MUTED_COLOR_FALLBACK) const borderColor = getCssVar(root, '--ft-border', BORDER_COLOR_FALLBACK) const gridColor = getCssVar(root, '--ft-chart-grid', borderColor) - const lineColor = getCssVar(root, '--ft-chart-bar', LINE_COLOR) - const pointColor = getCssVar(root, '--ft-chart-bar-hover', POINT_COLOR) + const lineColor = getCssVar(root, '--ft-chart-line', LINE_COLOR) + const pointColor = getCssVar(root, '--ft-chart-line-hover', POINT_COLOR) + const lineFill = getCssVar(root, '--ft-chart-line-fill', LINE_FILL_FALLBACK) + const lineFillTransparent = getCssVar( + root, + '--ft-chart-line-fill-transparent', + LINE_FILL_TRANSPARENT_FALLBACK, + ) + const canvasColor = getCssVar(root, '--ft-bg', '#ffffff') + const lastPointIndex = data.length - 1 + const pointRadii = data.map((_, index) => + index === lastPointIndex ? 6 : 4, + ) + const pointBackgroundColors = data.map((_, index) => + index === lastPointIndex ? pointColor : canvasColor, + ) + const prefersReducedMotion = window.matchMedia( + '(prefers-reduced-motion: reduce)', + ).matches chartCache.get(canvas)?.destroy() @@ -239,45 +387,101 @@ const hasChartData = chartData.length > 0 label: payload.yAxisLabel ?? chartTitle, data: values, borderColor: lineColor, - backgroundColor: lineColor, - borderWidth: 2, - pointBackgroundColor: pointColor, - pointBorderColor: pointColor, - pointHoverRadius: 6, - pointRadius: 4, + backgroundColor: (context) => { + const { chart } = context + const { chartArea, ctx } = chart + if (!chartArea) return lineFill + + const gradient = ctx.createLinearGradient( + 0, + chartArea.top, + 0, + chartArea.bottom, + ) + gradient.addColorStop(0, lineFill) + gradient.addColorStop(0.72, lineFillTransparent) + return gradient + }, + borderCapStyle: 'round', + borderJoinStyle: 'round', + borderWidth: 3, + fill: true, + pointBackgroundColor: pointBackgroundColors, + pointBorderColor: lineColor, + pointBorderWidth: 3, + pointHitRadius: 12, + pointHoverBackgroundColor: pointColor, + pointHoverBorderColor: canvasColor, + pointHoverBorderWidth: 3, + pointHoverRadius: 7, + pointRadius: pointRadii, + pointStyle: 'circle', showLine: true, - tension: 0.25, + tension: 0.32, }, ], }, options: { - animation: { duration: 500 }, + animation: { duration: prefersReducedMotion ? 0 : 500 }, + interaction: { + intersect: false, + mode: 'index', + }, + layout: { + padding: { + left: 4, + right: 8, + top: 8, + }, + }, maintainAspectRatio: false, responsive: true, plugins: { legend: { display: false }, tooltip: { + backgroundColor: textColor, + bodyColor: canvasColor, + borderColor: lineColor, + borderWidth: 1, callbacks: { label: (context) => - formatChartValue(Number(context.raw ?? 0), payload.valueFormat), + `${payload.yAxisLabel}: ${formatChartValue( + Number(context.raw ?? 0), + payload.valueFormat, + )}`, + title: (items) => `Version ${items[0]?.label ?? ''}`, }, + caretPadding: 8, + cornerRadius: 8, + displayColors: false, + padding: 10, + titleColor: canvasColor, }, }, scales: { x: { + border: { display: false }, grid: { display: false }, ticks: { - color: textColor, + autoSkip: true, + autoSkipPadding: 16, + color: mutedColor, font: { size: 12, weight: 500 }, + maxTicksLimit: 8, maxRotation: 0, minRotation: 0, + padding: 8, }, }, y: { beginAtZero: true, suggestedMax, - border: { color: gridColor }, - grid: { color: gridColor }, + border: { display: false }, + grid: { + color: gridColor, + drawTicks: false, + lineWidth: 1, + }, title: { color: mutedColor, display: Boolean(payload.yAxisLabel), @@ -287,6 +491,7 @@ const hasChartData = chartData.length > 0 ticks: { color: mutedColor, font: { size: 12 }, + padding: 10, precision: payload.valueFormat === 'count' ? 0 : undefined, callback: (value) => formatAxisTick(value, payload.valueFormat), }, diff --git a/packages/docs/src/styles/starlight.css b/packages/docs/src/styles/starlight.css index 848c4e01..ba277492 100644 --- a/packages/docs/src/styles/starlight.css +++ b/packages/docs/src/styles/starlight.css @@ -23,8 +23,14 @@ --ft-chart-bar-hover: var(--sl-color-gray-2); --ft-chart-value-on-bar: #ffffff; --ft-chart-grid: var(--sl-color-gray-5); + --ft-chart-line: var(--sl-color-gray-3); + --ft-chart-line-hover: var(--sl-color-gray-2); + --ft-chart-line-fill: rgb(148 163 184 / 13%); + --ft-chart-line-fill-transparent: rgb(148 163 184 / 0%); } :root[data-theme='light'] { + --ft-chart-line-fill: rgb(105 117 134 / 11%); + --ft-chart-line-fill-transparent: rgb(105 117 134 / 0%); } /* Make main content column wider and sidebars narrower. */ From 2c85a5f1286623734097422e15c6838143f6d0bd Mon Sep 17 00:00:00 2001 From: Alexander Karan <47707063+AlexanderKaran@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:01:58 +0800 Subject: [PATCH 2/2] Removed turnery --- .../docs/src/components/VersionLineChart.astro | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/docs/src/components/VersionLineChart.astro b/packages/docs/src/components/VersionLineChart.astro index 6ab22aed..9ce3cb51 100644 --- a/packages/docs/src/components/VersionLineChart.astro +++ b/packages/docs/src/components/VersionLineChart.astro @@ -64,6 +64,14 @@ function transformValue(value: number) { return value } +function formatLatestChange(value: number | null) { + if (value == null) return null + if (value === 0) return 'No change' + + const sign = value > 0 ? '+' : 'โˆ’' + return `${sign}${formatChartValue(Math.abs(value), valueFormat)}` +} + function getVersionChartData(): ChartDatum[] { return versions.flatMap((stats) => { const rawValue = @@ -98,15 +106,7 @@ const latestChange = latestDatum != null && previousDatum != null ? latestDatum.value - previousDatum.value : null -const latestChangeLabel = - latestChange == null - ? null - : latestChange === 0 - ? 'No change' - : `${latestChange > 0 ? '+' : 'โˆ’'}${formatChartValue( - Math.abs(latestChange), - valueFormat, - )}` +const latestChangeLabel = formatLatestChange(latestChange) --- {