Rework the charts page into sections; add .page-section-title (#3101)

This commit is contained in:
Paweł Kuna authored and GitHub committed 2026-09-20 22:08:56 +02:00
1 parent 88811afaff
commit a984752c84
20 files changed
+632 -112

No files matched your search

+131 -40
View File
@@ -6,6 +6,8 @@ import Chart from '@components/demo/Chart.astro'
import Card from '@ui/Card.astro'
import CardHeader from '@ui/CardHeader.astro'
import CardBody from '@ui/CardBody.astro'
import CardTitle from '@ui/CardTitle.astro'
import CardSubtitle from '@ui/CardSubtitle.astro'
import chartsData from '@data/charts.json'
import { capitalize } from '@shared/lib/string-format'
import DocsLink from '@ui/DocsLink.astro'
@@ -13,7 +15,99 @@ import CaptureScript from '@shared/components/CaptureScript.astro'
import LegendList from '@ui/LegendList.astro'
import Legend from '@ui/Legend.astro'
const demoCharts = Object.entries(chartsData as Record<string, { demo?: boolean; name?: string }>).filter(([, chart]) => chart && chart.demo)
type DemoChart = { name?: string; extend?: string; type?: string; series?: { name?: string; color?: string }[] }
const charts = chartsData as Record<string, DemoChart>
// Legend rules: one serie needs no legend, two to four series get a <Legend> list above the
// chart, and only circular charts or charts with five and more series keep the legend below.
const circular = ['pie', 'donut', 'radialBar', 'polarArea', 'radar']
function topLegend(chartId: string) {
const own = charts[chartId]
const chart = own?.extend ? { ...charts[own.extend], ...own } : own
const series = chart?.series ?? []
if (circular.includes(chart?.type ?? 'bar') || series.length < 2 || series.length > 4) return undefined
return series.map((serie) => ({ text: serie.name ?? '', color: serie.color ?? 'primary' }))
}
const third = 'col-md-6 col-lg-4'
const half = 'col-lg-6'
const wide = 'col-lg-8'
// Every chart is placed by hand, section by section and row by row (each row adds up to
// 12 columns). Charts with many points, several series or long labels get a wider column.
const sections: { title: string; description: string; charts: [chartId: string, col: string, subtitle: string][] }[] = [
{
title: 'Line charts',
description: 'Trends over time, where the change between points matters more than single values.',
charts: [
['completion-tasks-2', third, 'Tickets solved per day with a smooth curve, the default.'],
['completion-tasks-4', third, 'The same tickets drawn with straight segments.'],
['completion-tasks-5', third, 'A stepped line for values that change in jumps.'],
['completion-tasks-7', third, 'Tickets solved per day, labeled with values.'],
['tasks', wide, 'Issues opened, closed and reopened per day over the last 17 days.'],
['line', third, 'Monthly revenue of each plan, January to June.'],
['line-stroke', third, 'Monthly signups, dashed for paid and referral.'],
['spline', third, 'CPU load of web servers and workers in one day.'],
['temperature', half, 'Monthly average temperature in Tokyo and London.'],
['stepline', half, 'Number of running instances during the day.'],
],
},
{
title: 'Area charts',
description: 'Lines with a filled area, to stress volume or to show how parts add up to a total.',
charts: [
['completion-tasks-3', third, 'Tickets solved per day as a filled area.'],
['completion-tasks-10', third, 'Solved and reopened tickets as two overlapping areas.'],
['completion-tasks-11', third, 'Solved and reopened tickets stacked on top of each other.'],
['area', third, 'Monthly sessions on mobile and desktop.'],
['area-spline', third, 'Monthly egress and ingress traffic.'],
['area-spline-stacked', third, 'Storage used by media and backups, stacked.'],
],
},
{
title: 'Bar charts',
description: 'Comparison of separate categories, side by side or stacked.',
charts: [
['completion-tasks', third, 'Tickets solved per day, last week.'],
['completion-tasks-6', third, 'The same tickets as horizontal bars.'],
['completion-tasks-8', third, 'Solved and reopened tickets side by side.'],
['completion-tasks-9', third, 'Solved and reopened tickets in one stacked bar per day.'],
['cache-status', third, 'Cache status of requests per day, colored by meaning.'],
['funnel', third, 'Visitors left at each step from first visit to customer.'],
],
},
{
title: 'Circular charts',
description: 'Parts of a whole and progress toward a goal, best with a few values.',
charts: [
['demo-pie', third, 'Share of visits by source.'],
['campaigns', third, 'Delivery, open and click rates of one campaign.'],
['polar-area', third, 'Sales volume of each region.'],
],
},
{
title: 'Other charts',
description: 'Mixed series, distributions, schedules and hierarchies.',
charts: [
['combination', wide, 'Monthly spend as columns with leads and trials as lines.'],
['radar', third, 'Pro and Enterprise plans rated in six areas.'],
['scatter', third, 'Page load time samples on desktop and mobile.'],
['timeline', wide, 'Planned duration of each release phase.'],
['bubble', third, 'Search and social campaigns on three metrics.'],
['treemap', third, 'Disk space taken by each folder.'],
['box-plot', third, 'Spread of API response times in each month.'],
],
},
]
---
<DefaultLayout title="Charts" pageMenu="plugins.charts" pageLibs={['apexcharts']}>
@@ -23,16 +117,15 @@ const demoCharts = Object.entries(chartsData as Record<string, { demo?: boolean;
<Activity />
</div>
<div class="col-lg-8">
<div class="col-12">
<Card>
<CardHeader title="Chart with legend">
<LegendList class="ms-auto" id="legend-demo-list">
<Legend button color="yellow" text="Total visits" value="6.7k" data-series="Total visits" />
<Legend button color="green" text="Unique visitors" value="4.7k" data-series="Unique visitors" />
<Legend button color="primary" text="New users" value="1.6k" data-series="New users" />
</LegendList>
</CardHeader>
<CardHeader title="Chart with legend" />
<CardBody>
<LegendList divided class="mb-3" id="legend-demo-list">
<Legend button size="lg" color="yellow" text="Total visits" value="63.0k" data-series="Total visits" />
<Legend button size="lg" color="green" text="Unique visitors" value="43.0k" data-series="Unique visitors" />
<Legend button size="lg" color="primary" text="New users" value="13.5k" data-series="New users" />
</LegendList>
<Chart chartId="legend-demo" height={15} label="Site traffic" />
<CaptureScript>
<!-- BEGIN SCRIPT OF LEGEND -->
@@ -56,28 +149,6 @@ const demoCharts = Object.entries(chartsData as Record<string, { demo?: boolean;
</Card>
</div>
<div class="col-lg-4">
<Card>
<CardHeader title="Legend variants" />
<CardBody>
<LegendList class="mb-3">
<Legend color="blue" text="Allowed" value="339.0k" />
<Legend color="red" text="Denied" value="1.2k" />
<Legend color="orange" text="Challenged" value={815} />
</LegendList>
<LegendList class="mb-3">
<Legend href="#" color="purple" text="Logged" value="-" active />
<Legend href="#" color="teal" text="Rate limited" value="-" off />
</LegendList>
<LegendList vertical>
<Legend color="primary" text="This year" />
<Legend color="secondary" text="Last year" />
<Legend text="Free" />
</LegendList>
</CardBody>
</Card>
</div>
<div class="col-12">
<SocialReferrals />
</div>
@@ -92,15 +163,35 @@ const demoCharts = Object.entries(chartsData as Record<string, { demo?: boolean;
</div>
{
demoCharts.map(([chartId, chart]) => (
<div class="col-lg-6 col-xl-4">
<Card>
{chart.name && <CardHeader title={chart.name} />}
<CardBody>
<Chart chartId={chartId} height={15} label={chart.name ?? capitalize(chartId.replace(/-/g, ' '))} />
</CardBody>
</Card>
</div>
sections.map((section) => (
<Fragment>
<div class="col-12">
<h2 class="page-section-title">{section.title}</h2>
<p class="page-section-description">{section.description}</p>
</div>
{section.charts.map(([chartId, col, subtitle]) => (
<div class={col}>
<Card class="h-100">
<CardHeader>
<div>
<CardTitle as="h3">{charts[chartId]?.name ?? capitalize(chartId.replace(/-/g, ' '))}</CardTitle>
<CardSubtitle>{subtitle}</CardSubtitle>
</div>
</CardHeader>
<CardBody>
{topLegend(chartId) && (
<LegendList class="justify-content-end mb-3">
{topLegend(chartId)?.map((item) => (
<Legend color={item.color} text={item.text} />
))}
</LegendList>
)}
<Chart chartId={chartId} height={15} label={charts[chartId]?.name ?? capitalize(chartId.replace(/-/g, ' '))} legend={topLegend(chartId) ? false : undefined} />
</CardBody>
</Card>
</div>
))}
</Fragment>
))
}
</div>
+125 -17
View File
@@ -32,6 +32,13 @@ const requests = [
{ name: 'Rate limited', color: 'teal', value: '-' },
]
const latency = [
{ name: 'P99', color: 'primary', value: 124 },
{ name: 'P95', color: 'azure', value: 76 },
{ name: 'P75', color: 'green', value: 32 },
{ name: 'P50', color: 'yellow', value: 10 },
]
const storage = [
{ name: 'Documents', color: 'primary', value: '3.5 GB', width: 44 },
{ name: 'Media', color: 'info', value: '1.5 GB', width: 19 },
@@ -119,13 +126,128 @@ const storage = [
</Card>
</div>
<div class="col-lg-8">
<Card>
<CardBody>
<CardTitle>Large</CardTitle>
<CardSubtitle>Set <code>size="lg"</code> to put a big value under the label. Add <code>unit</code> for the unit and <code>divided</code> on the list for the lines.</CardSubtitle>
<LegendList divided>
<Legend size="lg" color="primary" text="P99" value={124} unit="ms" />
<Legend size="lg" color="azure" text="P95" value={76} unit="ms" />
<Legend size="lg" color="green" text="P75" value={32} unit="ms" />
<Legend size="lg" color="yellow" text="Requests" value="1,234" off />
</LegendList>
</CardBody>
</Card>
</div>
<div class="col-md-6 col-lg-4">
<Card>
<CardBody>
<CardTitle>Under a stat</CardTitle>
<CardSubtitle>A legend row explains the split behind a single number.</CardSubtitle>
<Subheader>Requests</Subheader>
<div class="h1 mb-3">341.0k</div>
<LegendList class="border-top pt-3">
{requests.map((item) => <Legend color={item.color} text={item.name} value={item.value} />)}
</LegendList>
</CardBody>
</Card>
</div>
<div class="col-lg-8">
<Card>
<CardBody>
<CardTitle>Above a chart</CardTitle>
<CardSubtitle>Large items show the latest value of each series, so the chart needs no legend of its own.</CardSubtitle>
<LegendList divided class="mb-3">
{latency.map((item) => <Legend size="lg" color={item.color} text={item.name} value={item.value} unit="ms" />)}
</LegendList>
<Chart chartId="legend-latency" height={12} label="Read latency by percentile" />
</CardBody>
</Card>
</div>
<div class="col-md-6 col-lg-4">
<Card>
<CardBody>
<CardTitle>Large vertical</CardTitle>
<CardSubtitle>Combine <code>size="lg"</code> with a <code>vertical</code> list for a side panel.</CardSubtitle>
<LegendList vertical>
{storage.map((item) => <Legend size="lg" color={item.color} text={item.name} value={item.value.replace(' GB', '')} unit="GB" />)}
</LegendList>
</CardBody>
</Card>
</div>
<div class="col-lg-8">
<Card>
<CardBody>
<CardTitle>Large links</CardTitle>
<CardSubtitle>Large items work as links and buttons too. Mark the current one with <code>active</code> and a hidden series with <code>off</code>.</CardSubtitle>
<LegendList divided>
<Legend href="#" size="lg" color="green" text="Passed" value={128} active />
<Legend href="#" size="lg" color="red" text="Failed" value={4} />
<Legend href="#" size="lg" color="yellow" text="Skipped" value={12} />
<Legend href="#" size="lg" color="secondary" text="Pending" value={3} off />
</LegendList>
</CardBody>
</Card>
</div>
<div class="col-md-6 col-lg-4">
<Card>
<CardBody>
<CardTitle>With units</CardTitle>
<CardSubtitle>Add <code>unit</code> to a regular item to show the unit after the value.</CardSubtitle>
<LegendList>
{latency.slice(0, 3).map((item) => <Legend color={item.color} text={item.name} value={item.value} unit="ms" />)}
</LegendList>
</CardBody>
</Card>
</div>
<div class="col-lg-8">
<Card>
<CardBody>
<CardTitle>Without lines</CardTitle>
<CardSubtitle>Leave out <code>divided</code> when the items sit in a tight space or wrap to more rows.</CardSubtitle>
<LegendList>
{traffic.map((item) => <Legend size="lg" color={item.color} text={item.name} value={item.value} />)}
</LegendList>
</CardBody>
</Card>
</div>
<div class="col-md-6 col-lg-4">
<Card>
<CardBody>
<CardTitle>Loading</CardTitle>
<CardSubtitle>Use <code>placeholder</code> blocks for the label and the value while the data loads.</CardSubtitle>
<div class="legend-list legend-list-divided placeholder-glow" aria-hidden="true">
{
[1, 2].map(() => (
<span class="legend legend-lg">
<span class="legend-dot" />
<span class="placeholder col-9" />
<span class="legend-value">
<span class="placeholder col-6" />
</span>
</span>
))
}
</div>
</CardBody>
</Card>
</div>
<div class="col-lg-8">
<Card>
<CardHeader title="Interactive legend">
<LegendList class="ms-auto" id="legend-page-list">
<Legend button color="yellow" text="Total visits" value="6.7k" data-series="Total visits" />
<Legend button color="green" text="Unique visitors" value="4.7k" data-series="Unique visitors" />
<Legend button color="primary" text="New users" value="1.6k" data-series="New users" />
<Legend button color="yellow" text="Total visits" value="63.0k" data-series="Total visits" />
<Legend button color="green" text="Unique visitors" value="43.0k" data-series="Unique visitors" />
<Legend button color="primary" text="New users" value="13.5k" data-series="New users" />
</LegendList>
</CardHeader>
<CardBody>
@@ -170,20 +292,6 @@ const storage = [
</Card>
</div>
<div class="col-md-6 col-lg-4">
<Card>
<CardBody>
<CardTitle>Under a stat</CardTitle>
<CardSubtitle>A legend row explains the split behind a single number.</CardSubtitle>
<Subheader>Requests</Subheader>
<div class="h1 mb-3">341.0k</div>
<LegendList class="border-top pt-3">
{requests.map((item) => <Legend color={item.color} text={item.name} value={item.value} />)}
</LegendList>
</CardBody>
</Card>
</div>
<div class="col-md-6 col-lg-4">
<Card>
<CardBody>