Add legend component with interactive items (#3047)

This commit is contained in:
Paweł Kuna authored and GitHub committed 2026-09-13 11:59:56 +02:00
1 parent bd5c010595
commit be30919bb2
22 files changed
+727 -75

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
---
"@tabler/core": minor
"@tabler/preview": minor
"@tabler/docs": minor
---
Added `.legend` component with `.legend-dot`, `.legend-value`, `.legend-off` and `.legend-list`, plus `Legend` Astro component.
+9 -2
View File
@@ -2120,12 +2120,19 @@ $form-invalid-color: $form-feedback-invalid-color !default;
$form-invalid-border-color: $form-feedback-invalid-color !default;
// Legend
// scss-docs-start legend-variables
$legend-margin-bottom: 0.5rem !default;
$legend-font-size: 1.5rem !default;
$legend-font-weight: null !default;
$legend-bg: var(--border-color) !default;
$legend-size: 0.75em !default;
$legend-border-radius: var(--border-radius-sm) !default;
$legend-size: 0.5rem !default;
$legend-border-radius: var(--border-radius-pill) !default;
$legend-gap: 0.5rem !default;
$legend-padding-y: 0.375rem !default;
$legend-padding-x: 0.75rem !default;
$legend-list-gap: 0.25rem 1rem !default;
$legend-list-gap-interactive: 0.25rem !default;
// scss-docs-end legend-variables
// Offcanvas
// scss-docs-start offcanvas-variables
+103 -2
View File
@@ -1,14 +1,115 @@
/**
Legend
*/
@use 'sass:map';
@use '../config' as *;
.legend {
//
// Legend dot
//
.legend-dot {
--legend-size: #{$legend-size};
--legend-color: #{$legend-bg};
display: inline-block;
flex-shrink: 0;
width: var(--legend-size);
height: var(--legend-size);
background: $legend-bg;
background: var(--legend-color);
border: 1px solid var(--border-color-translucent);
@include border-radius($legend-border-radius);
}
//
// Legend
//
.legend {
--legend-color: #{$legend-bg};
--legend-gap: #{$legend-gap};
--legend-padding-y: #{$legend-padding-y};
--legend-padding-x: #{$legend-padding-x};
--legend-hover-bg: #{color-transparent(var(--legend-color), 0.1)};
display: inline-flex;
gap: var(--legend-gap);
align-items: center;
padding: var(--legend-padding-y) 0;
margin: 0;
font: inherit;
font-weight: var(--font-weight-medium);
line-height: 1;
color: var(--body-color);
text-decoration: none;
white-space: nowrap;
background: transparent;
border: 0;
transition:
background-color $transition-time,
opacity $transition-time;
@include border-radius(var(--border-radius-pill));
.legend-dot {
--legend-color: inherit;
}
&:focus:not(:focus-visible) {
outline: none;
}
&:focus-visible {
outline: 2px solid var(--primary);
outline-offset: -2px;
}
}
a.legend,
button.legend {
padding-inline: var(--legend-padding-x);
cursor: pointer;
&:hover,
&.active {
color: var(--body-color);
text-decoration: none;
background: var(--legend-hover-bg);
}
}
.legend-off {
opacity: 0.5;
.legend-dot {
--legend-color: #{$legend-bg};
}
}
.legend-value {
font-variant-numeric: tabular-nums;
color: var(--secondary);
}
@each $name, $color in map.merge($theme-colors, $social-colors) {
.legend-#{$name} {
--legend-color: var(--#{$name});
}
}
//
// Legend list
//
.legend-list {
--legend-list-gap: #{$legend-list-gap};
display: flex;
flex-wrap: wrap;
gap: var(--legend-list-gap);
align-items: center;
// Links and buttons carry their own horizontal padding, so the list can be tighter
&:has(> a.legend, > button.legend) {
--legend-list-gap: #{$legend-list-gap-interactive};
}
}
.legend-list-vertical {
flex-direction: column;
align-items: flex-start;
}
+109
View File
@@ -0,0 +1,109 @@
---
title: Legend
summary: A legend labels the series of a chart or a progress bar with a colored marker, a name and an optional value.
description: Label chart series with colored markers. Group legend items in a list, add values, and let users toggle series on click.
related: [/ui/plugins/chart, /ui/components/status, /ui/components/progress]
source: core/scss/ui/_legend.scss
classnames:
component:
- class: legend-dot
desc: Small square marker in the series color
- class: legend
desc: A marker with a label, on a span, link or button
- class: legend-list
desc: Wraps legend items in a row
part:
- class: legend-value
desc: Muted value next to the label
style:
- class: legend-list-vertical
desc: Stacks the items in a column
state:
- class: active
desc: Keeps the hover background on a link or button
- class: legend-off
desc: Dims the item and greys the marker, for a hidden series
color:
- class: legend-{color}
desc: Any base color, for example legend-blue or legend-primary, on an item or on a marker
---
import Example from '@components/Example.astro';
import CodeDocs from '@components/CodeDocs.astro';
## Legend marker
A `legend-dot` is a small square in the color of a series. Use it on its own next to a label. Set the color with a `legend-{color}` class. Full list of available colors can be found in the [Colors](/ui/base/colors) section.
<Example centered>
<span class="legend-dot legend-blue"></span>
<span class="legend-dot legend-azure"></span>
<span class="legend-dot legend-indigo"></span>
<span class="legend-dot legend-purple"></span>
<span class="legend-dot legend-pink"></span>
<span class="legend-dot legend-red"></span>
<span class="legend-dot legend-orange"></span>
<span class="legend-dot legend-yellow"></span>
<span class="legend-dot legend-lime"></span>
<span class="legend-dot legend-green"></span>
<span class="legend-dot legend-teal"></span>
<span class="legend-dot legend-cyan"></span>
</Example>
## Legend item
Wrap the marker and the label in a `legend`. Put the color class on the item, so the marker and the hover background share it. Add a `legend-value` for a number next to the label.
<Example centered>
<div class="legend-list">
<span class="legend legend-blue"><span class="legend-dot"></span> Allowed <span class="legend-value">339.0k</span></span>
<span class="legend legend-red"><span class="legend-dot"></span> Denied <span class="legend-value">1.2k</span></span>
<span class="legend legend-orange"><span class="legend-dot"></span> Challenged <span class="legend-value">815</span></span>
</div>
</Example>
## Interactive legend
Use a `button` or a link when clicking an item should toggle a series. The item gets a tinted background on hover, mixed from the series color. Add `active` to keep it, and `legend-off` to mark a hidden series.
<Example centered>
<div class="legend-list">
<button type="button" class="legend legend-blue"><span class="legend-dot"></span> Allowed <span class="legend-value">339.0k</span></button>
<button type="button" class="legend legend-purple active"><span class="legend-dot"></span> Logged <span class="legend-value">-</span></button>
<button type="button" class="legend legend-teal legend-off"><span class="legend-dot"></span> Rate limited <span class="legend-value">-</span></button>
</div>
</Example>
With ApexCharts, hide the built-in legend and call `toggleSeries` with the series name:
```html
<button type="button" class="legend legend-blue" data-series="Allowed">
<span class="legend-dot"></span> Allowed
</button>
```
```js
document.querySelectorAll('.legend').forEach((item) => {
item.addEventListener('click', () => {
chart.toggleSeries(item.dataset.series)
item.classList.toggle('legend-off')
})
})
```
## Vertical list
Add `legend-list-vertical` to stack the items, for example next to a donut chart.
<Example centered>
<div class="legend-list legend-list-vertical">
<span class="legend legend-primary"><span class="legend-dot"></span> This year <span class="legend-value">$92,400</span></span>
<span class="legend legend-secondary"><span class="legend-dot"></span> Last year <span class="legend-value">$84,300</span></span>
<span class="legend"><span class="legend-dot"></span> Forecast <span class="legend-value">$98,000</span></span>
</div>
</Example>
## Variables
Use these SCSS variables to customize legends. The default values are:
<CodeDocs name="legend-variables" file="core/scss/_variables.scss" />
+4 -10
View File
@@ -377,18 +377,12 @@ Charts that share a `chart.group` name move together. Hover one and the others s
## Legend key
ApexCharts draws its own legend, but you can also build a plain-HTML legend next to a chart's title - for example to label the series above the chart instead of below it. Add a `legend` span before each label and color it with a `bg-*` utility class.
ApexCharts draws its own legend, but you can also build a plain-HTML legend next to a chart's title - for example to label the series above the chart instead of below it. Use the [Legend](/ui/components/legend) component: a `legend-list` of `legend` elements, colored with `legend-{color}`.
<Example>
<div class="d-flex align-items-center gap-3">
<div class="d-flex align-items-center gap-2">
<span class="legend bg-primary"></span>
<span class="text-secondary">This year</span>
</div>
<div class="d-flex align-items-center gap-2">
<span class="legend bg-secondary"></span>
<span class="text-secondary">Last year</span>
</div>
<div class="legend-list">
<span class="legend legend-primary"><span class="legend-dot"></span> This year</span>
<span class="legend legend-secondary"><span class="legend-dot"></span> Last year</span>
</div>
</Example>
+58
View File
@@ -9,6 +9,9 @@ import CardBody from '@ui/CardBody.astro'
import chartsData from '@data/charts.json'
import { capitalize } from '@shared/lib/string-format'
import DocsLink from '@ui/DocsLink.astro'
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)
---
@@ -20,6 +23,61 @@ const demoCharts = Object.entries(chartsData as Record<string, { demo?: boolean;
<Activity />
</div>
<div class="col-lg-8">
<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>
<CardBody>
<Chart chartId="legend-demo" height={15} label="Site traffic" />
<CaptureScript>
<!-- BEGIN SCRIPT OF LEGEND -->
<script is:inline>
/*
Toggles a chart series when its legend item is clicked.
The chart is exposed by the demo Chart component as window.tabler_chart['chart-legend-demo'].
*/
document.querySelectorAll('#legend-demo-list .legend').forEach((item) => {
item.addEventListener('click', () => {
const chart = window.tabler_chart && window.tabler_chart['chart-legend-demo']
if (!chart) return
chart.toggleSeries(item.dataset.series)
item.classList.toggle('legend-off')
})
})
</script>
<!-- END SCRIPT OF LEGEND -->
</CaptureScript>
</CardBody>
</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>
+219
View File
@@ -0,0 +1,219 @@
---
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
import CaptureScript from '@shared/components/CaptureScript.astro'
import Card from '@ui/Card.astro'
import CardBody from '@ui/CardBody.astro'
import CardHeader from '@ui/CardHeader.astro'
import CardTitle from '@ui/CardTitle.astro'
import CardSubtitle from '@ui/CardSubtitle.astro'
import Legend from '@ui/Legend.astro'
import LegendList from '@ui/LegendList.astro'
import Subheader from '@ui/Subheader.astro'
import DocsLink from '@ui/DocsLink.astro'
import Chart from '@components/demo/Chart.astro'
import site from '@data/site.json'
import { ucFirst } from '@shared/lib/string-format'
const colors = Object.keys(site.colors)
const traffic = [
{ name: 'Organic search', color: 'azure', value: '38%' },
{ name: 'Direct', color: 'primary', value: '27%' },
{ name: 'Social media', color: 'purple', value: '18%' },
{ name: 'Referral', color: 'orange', value: '12%' },
{ name: 'Email', color: 'green', value: '5%' },
]
const requests = [
{ name: 'Allowed', color: 'blue', value: '339.0k' },
{ name: 'Denied', color: 'red', value: '1.2k' },
{ name: 'Challenged', color: 'orange', value: '815' },
{ name: 'Logged', color: 'purple', value: '-' },
{ name: 'Rate limited', color: 'teal', value: '-' },
]
const storage = [
{ name: 'Documents', color: 'primary', value: '3.5 GB', width: 44 },
{ name: 'Media', color: 'info', value: '1.5 GB', width: 19 },
{ name: 'Shared', color: 'success', value: '0.7 GB', width: 9 },
]
---
<DefaultLayout title="Legend" pageMenu="base.legend" pageLibs={['apexcharts']} description="Legends label the series of a chart or a progress bar with a colored marker, a name and an optional value.">
<DocsLink slot="page-header-actions" path="/ui/components/legend" />
<div class="row row-cards">
<div class="col-md-6 col-lg-4">
<Card>
<CardBody>
<CardTitle>Basic</CardTitle>
<CardSubtitle>A <code>Legend</code> is a marker and a label. Set the series color with <code>color</code>.</CardSubtitle>
<LegendList>
<Legend color="primary" text="This year" />
<Legend color="secondary" text="Last year" />
<Legend text="Forecast" />
</LegendList>
</CardBody>
</Card>
</div>
<div class="col-md-6 col-lg-4">
<Card>
<CardBody>
<CardTitle>With values</CardTitle>
<CardSubtitle>Add <code>value</code> to show a number next to the label.</CardSubtitle>
<LegendList>
{requests.slice(0, 3).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>
<CardTitle>States</CardTitle>
<CardSubtitle>Links and buttons get a tinted background on hover. Use <code>active</code> to keep it and <code>off</code> for a hidden series.</CardSubtitle>
<LegendList>
<Legend button color="blue" text="Default" />
<Legend button color="purple" text="Active" active />
<Legend button color="teal" text="Off" off />
</LegendList>
</CardBody>
</Card>
</div>
<div class="col-md-6 col-lg-4">
<Card>
<CardBody>
<CardTitle>Colors</CardTitle>
<CardSubtitle>Every palette color works with <code>legend-{'{color}'}</code>.</CardSubtitle>
<LegendList>
{colors.map((color) => <Legend color={color} text={ucFirst(color)} />)}
</LegendList>
</CardBody>
</Card>
</div>
<div class="col-md-6 col-lg-4">
<Card>
<CardBody>
<CardTitle>Vertical</CardTitle>
<CardSubtitle>Add <code>vertical</code> to the list to stack the items.</CardSubtitle>
<LegendList vertical>
{traffic.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>
<CardTitle>Marker only</CardTitle>
<CardSubtitle>Use a bare <code>legend-dot</code> when the label lives elsewhere.</CardSubtitle>
<div class="d-flex align-items-center gap-2">
{colors.map((color) => <span class:list={['legend-dot', `legend-${color}`]} />)}
</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" />
</LegendList>
</CardHeader>
<CardBody>
<CardSubtitle>Button items call <code>toggleSeries()</code> on the chart and toggle <code>legend-off</code>.</CardSubtitle>
<Chart chartId="legend-demo" id="legend-page-chart" height={15} label="Site traffic" />
<CaptureScript>
<!-- BEGIN SCRIPT OF LEGEND -->
<script is:inline>
/*
Toggles a chart series when its legend item is clicked.
The chart is exposed by the demo Chart component as window.tabler_chart['chart-legend-page-chart'].
*/
document.querySelectorAll('#legend-page-list .legend').forEach((item) => {
item.addEventListener('click', () => {
const chart = window.tabler_chart && window.tabler_chart['chart-legend-page-chart']
if (!chart) return
chart.toggleSeries(item.dataset.series)
item.classList.toggle('legend-off')
})
})
</script>
<!-- END SCRIPT OF LEGEND -->
</CaptureScript>
</CardBody>
</Card>
</div>
<div class="col-lg-4">
<Card>
<CardBody>
<CardTitle>Next to a donut</CardTitle>
<CardSubtitle>A vertical list beside a chart with its own legend hidden.</CardSubtitle>
<div class="d-flex align-items-center gap-4">
<div class="w-50">
<Chart chartId="legend-donut" height={10} label="Traffic sources" />
</div>
<LegendList vertical>
{traffic.map((item) => <Legend color={item.color} text={item.name} value={item.value} />)}
</LegendList>
</div>
</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-md-6 col-lg-4">
<Card>
<CardBody>
<CardTitle>With a progress bar</CardTitle>
<CardSubtitle>Label the segments of a <code>progress-separated</code> bar.</CardSubtitle>
<p class="mb-3">Using <strong>5.7 GB</strong> of 8 GB</p>
<div class="progress progress-separated mb-3">
{storage.map((item) => <div class:list={['progress-bar', `bg-${item.color}`]} role="progressbar" style={`width: ${item.width}%`} aria-label={item.name} />)}
</div>
<LegendList>
{storage.map((item) => <Legend color={item.color} text={item.name} value={item.value} />)}
<Legend text="Free" value="2.3 GB" />
</LegendList>
</CardBody>
</Card>
</div>
<div class="col-md-6 col-lg-4">
<Card>
<CardBody>
<CardTitle>Filter links</CardTitle>
<CardSubtitle>Link items with <code>href</code> work as filters. Mark the current one with <code>active</code>.</CardSubtitle>
<LegendList>
<Legend href="#" color="green" text="Passed" value={128} active />
<Legend href="#" color="red" text="Failed" value={4} />
<Legend href="#" color="yellow" text="Skipped" value={12} />
<Legend href="#" color="secondary" text="Pending" value={3} />
</LegendList>
</CardBody>
</Card>
</div>
</div>
</DefaultLayout>
+4 -4
View File
@@ -16,7 +16,7 @@ interface Props {
title: string
/** dir="rtl" + the .rtl.css variant of tabler.css */
rtl?: boolean
/** the Tabler logo above the card (+ balancing spacer below it) */
/** the Tabler logo at the bottom of the frame */
showLogo?: boolean
/** caps the [data-screenshot-target] width at columns * 320px */
columns?: number
@@ -111,11 +111,13 @@ const libJsFiles = pageLibEntries.flatMap(([, lib]) => (lib.js ?? []).map((file)
}
.screenshot-logo {
position: absolute;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
height: 8rem;
padding: 0 5rem;
flex: 1;
}
/* fake mouse pointer, to make the static screenshot read as a live interaction */
@@ -135,8 +137,6 @@ const libJsFiles = pageLibEntries.flatMap(([, lib]) => (lib.js ?? []).map((file)
<body>
<div class="page page-center align-items-center">
<div class="screenshot d-flex flex-column justify-content-center align-items-center px-12" id="screenshot">
{showLogo && <div class="screenshot-logo" />}
<div class:list={['screenshot-wrapper', className]} style={targetStyle} data-screenshot-target>
<slot />
</div>
+35
View File
@@ -0,0 +1,35 @@
---
// Legend component on its own, no card and no chart: a LegendList of button
// items — colored dot, source name, its share in tabular numerals — wrapping
// over a few rows in a narrow container. The fake cursor rests on the active
// item so the tinted hover background is part of the picture, and the last
// items show the `off` state of a hidden series.
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
import Legend from '@ui/Legend.astro'
import LegendList from '@ui/LegendList.astro'
const sources = [
{ name: 'Organic search', color: 'blue', value: '31%' },
{ name: 'Direct', color: 'azure', value: '22%' },
{ name: 'Paid search', color: 'indigo', value: '14%' },
{ name: 'Social media', color: 'purple', value: '11%', active: true },
{ name: 'Referral', color: 'pink', value: '8%' },
{ name: 'Newsletter', color: 'orange', value: '6%' },
{ name: 'Partners', color: 'teal', value: '4%' },
{ name: 'Email', color: 'green', value: '3%', off: true },
{ name: 'Other', color: 'lime', value: '1%', off: true },
]
---
<ScreenshotLayout title="Legend list" columns={2}>
<LegendList class="justify-content-center">
{
sources.map((item) => (
<Legend button color={item.color} value={item.value} active={item.active} off={item.off} class="position-relative">
{item.name}
{item.active && <span class="cursor" />}
</Legend>
))
}
</LegendList>
</ScreenshotLayout>
+45
View File
@@ -0,0 +1,45 @@
---
// Legend component: a request-volume card whose header carries an interactive
// legend. Each item is a button — colored dot, series name, total in tabular
// numerals — and the fake cursor rests on one of them so the tinted hover
// background is part of the picture. `legend-requests` is a three-series stacked bar
// chart in charts.json with the ApexCharts legend left off, since the card's
// own legend replaces it.
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
import Card from '@ui/Card.astro'
import CardHeader from '@ui/CardHeader.astro'
import CardTitle from '@ui/CardTitle.astro'
import CardBody from '@ui/CardBody.astro'
import Legend from '@ui/Legend.astro'
import LegendList from '@ui/LegendList.astro'
import Chart from '@components/demo/Chart.astro'
// Series order and colors follow the `legend-requests` entry in charts.json.
const series = [
{ name: 'Allowed', color: 'blue', value: '339.0k' },
{ name: 'Challenged', color: 'azure', value: '815' },
{ name: 'Logged', color: 'green', value: '12.4k', active: true },
{ name: 'Rate limited', color: 'red', value: '-', off: true },
]
---
<ScreenshotLayout title="Legend" columns={2.6} pageLibs={['apexcharts']}>
<Card>
<CardHeader>
<CardTitle>Requests</CardTitle>
<LegendList class="ms-auto">
{
series.map((item) => (
<Legend button color={item.color} value={item.value} active={item.active} off={item.off} class="position-relative">
{item.name}
{item.active && <span class="cursor" />}
</Legend>
))
}
</LegendList>
</CardHeader>
<CardBody>
<Chart chartId="legend-requests" height={14} label="Requests per day, split by firewall action" />
</CardBody>
</Card>
</ScreenshotLayout>
+29 -1
View File
@@ -1,7 +1,10 @@
---
// shared by index.astro and layout-vertical.astro.
import Chart from '@shared/components/demo/Chart.astro'
import CaptureScript from '@shared/components/CaptureScript.astro'
import CardTitle from '@ui/CardTitle.astro'
import Legend from '@ui/Legend.astro'
import LegendList from '@ui/LegendList.astro'
import MapVectorCard from './cards/MapVectorCard.astro'
import Welcome from './cards/Welcome.astro'
import TotalUsers from './cards/charts/TotalUsers.astro'
@@ -72,8 +75,33 @@ const { rtl = false } = Astro.props
<div class="col-lg-6">
<div class="card">
<div class="card-body">
<CardTitle>Traffic summary</CardTitle>
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
<CardTitle class="mb-0">Traffic summary</CardTitle>
<LegendList id="traffic-summary-legend">
<Legend button text="Web" data-series="Web" style="--tblr-legend-color: var(--chart-mentions-color-0)" />
<Legend button text="Social" data-series="Social" style="--tblr-legend-color: var(--chart-mentions-color-1)" />
<Legend button text="Other" data-series="Other" style="--tblr-legend-color: var(--chart-mentions-color-2)" />
</LegendList>
</div>
<Chart chartId="mentions" size="lg" label="Traffic summary" />
<CaptureScript>
<!-- BEGIN SCRIPT OF TRAFFIC SUMMARY LEGEND -->
<script is:inline>
/*
Toggles a chart series when its legend item is clicked.
The chart is exposed by the demo Chart component as window.tabler_chart['chart-mentions'].
*/
document.querySelectorAll('#traffic-summary-legend .legend').forEach((item) => {
item.addEventListener('click', () => {
const chart = window.tabler_chart && window.tabler_chart['chart-mentions']
if (!chart) return
chart.toggleSeries(item.dataset.series)
item.classList.toggle('legend-off')
})
})
</script>
<!-- END SCRIPT OF TRAFFIC SUMMARY LEGEND -->
</CaptureScript>
</div>
</div>
</div>
+8 -23
View File
@@ -1,5 +1,6 @@
---
import LegendList from '../../ui/LegendList.astro'
import Legend from '../../ui/Legend.astro'
---
<div class="card">
@@ -10,27 +11,11 @@
<div class="progress-bar bg-info" role="progressbar" style="width: 19%" aria-label="System"></div>
<div class="progress-bar bg-success" role="progressbar" style="width: 9%" aria-label="Shared"></div>
</div>
<div class="row">
<div class="col-auto d-flex align-items-center pe-2">
<span class="legend me-2 bg-primary"></span>
<span>Regular</span>
<span class="d-none d-md-inline d-lg-none d-xxl-inline ms-2 text-secondary">3520MB</span>
</div>
<div class="col-auto d-flex align-items-center px-2">
<span class="legend me-2 bg-info"></span>
<span>System</span>
<span class="d-none d-md-inline d-lg-none d-xxl-inline ms-2 text-secondary">1520MB</span>
</div>
<div class="col-auto d-flex align-items-center px-2">
<span class="legend me-2 bg-success"></span>
<span>Shared</span>
<span class="d-none d-md-inline d-lg-none d-xxl-inline ms-2 text-secondary">720MB</span>
</div>
<div class="col-auto d-flex align-items-center ps-2">
<span class="legend me-2"></span>
<span>Free</span>
<span class="d-none d-md-inline d-lg-none d-xxl-inline ms-2 text-secondary">2240MB</span>
</div>
</div>
<LegendList>
<Legend color="primary" text="Regular" value="3520MB" />
<Legend color="info" text="System" value="1520MB" />
<Legend color="success" text="Shared" value="720MB" />
<Legend text="Free" value="2240MB" />
</LegendList>
</div>
</div>
+1 -1
View File
@@ -35,7 +35,7 @@ const tasks = crmDashboard.tasks_due
<div>{item.text}</div>
</div>
<div class="col-auto d-flex align-items-center gap-2">
<span class={`legend bg-${item.color}`} />
<span class={`legend-dot bg-${item.color}`} />
<span class="small text-secondary">{item.due}</span>
</div>
</div>
@@ -1,6 +1,8 @@
---
import Subheader from '@ui/Subheader.astro'
import Chart from '@shared/components/demo/Chart.astro'
import LegendList from '@ui/LegendList.astro'
import Legend from '@ui/Legend.astro'
---
<div class="card">
@@ -14,16 +16,10 @@ import Chart from '@shared/components/demo/Chart.astro'
</div>
</div>
<div class="d-flex align-items-center gap-3">
<div class="d-flex align-items-center gap-2">
<span class="legend bg-primary"></span>
<span class="text-secondary">This year</span>
</div>
<div class="d-flex align-items-center gap-2">
<span class="legend bg-secondary"></span>
<span class="text-secondary">Last year</span>
</div>
</div>
<LegendList>
<Legend color="primary" text="This year" />
<Legend color="secondary" text="Last year" />
</LegendList>
</div>
<Chart chartId="crm-mrr-overview" height={15} label="Monthly recurring revenue" />
@@ -1,6 +1,7 @@
---
import Subheader from '@ui/Subheader.astro'
import Chart from '@shared/components/demo/Chart.astro'
import LegendList from '@ui/LegendList.astro'
import Legend from '@ui/Legend.astro'
---
@@ -15,10 +16,10 @@ import Legend from '@ui/Legend.astro'
</div>
</div>
<div class="d-flex align-items-center gap-3">
<Legend color="primary">This year</Legend>
<Legend color="secondary">Last year</Legend>
</div>
<LegendList>
<Legend color="primary" text="This year" />
<Legend color="secondary" text="Last year" />
</LegendList>
</div>
<Chart chartId="sales-overview" height={15} label="Sales overview" />
+1 -1
View File
@@ -18,7 +18,7 @@ const items = range(1, steps)
<div class:list={['d-flex align-items-center gap-2', className]}>
<div class="text-secondary">{minLabel}</div>
<div class="d-flex gap-1">
{items.map((step) => <span class="legend" style={`background: color-mix(in srgb, transparent, var(--tblr-${color}) ${(step / steps) * 100}%);`} />)}
{items.map((step) => <span class="legend-dot" style={`background: color-mix(in srgb, transparent, var(--tblr-${color}) ${(step / steps) * 100}%);`} />)}
</div>
<div class="text-secondary">{maxLabel}</div>
</div>
+29 -8
View File
@@ -117,6 +117,25 @@
}
]
},
"legend-demo": {
"extend": "demo-line",
"legend": false
},
"legend-requests": {
"type": "bar",
"stacked": true,
"datetime": true,
"series": [
{ "name": "Allowed", "color": "blue", "data": [42, 48, 45, 52, 56, 50, 58, 62, 57, 64, 68, 72] },
{ "name": "Challenged", "color": "azure", "data": [14, 18, 16, 22, 19, 25, 21, 28, 24, 31, 27, 34] },
{ "name": "Logged", "color": "green", "data": [20, 24, 22, 28, 31, 27, 34, 37, 33, 41, 44, 48] }
]
},
"legend-donut": {
"extend": "traffic-sources-donut",
"legend": false,
"sparkline": true
},
"demo-line": {
"type": "line",
"datetime": true,
@@ -959,12 +978,8 @@
"categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
"stroke-curve": "straight",
"show-markers": true,
"y-axes": [
{ "title": "Uptime (%)", "min": 99, "max": 100 }
],
"annotations-y": [
{ "value": 99.9, "label": "SLA 99.9%", "color": "red" }
],
"y-axes": [{ "title": "Uptime (%)", "min": 99, "max": 100 }],
"annotations-y": [{ "value": 99.9, "label": "SLA 99.9%", "color": "red" }],
"series": [
{
"name": "Uptime",
@@ -997,7 +1012,10 @@
{
"name": "Sessions",
"color": "primary",
"data": [1847, 1768, 1792, 2003, 2081, 2099, 2261, 2441, 2268, 2483, 2367, 2245, 2458, 2278, 2236, 2044, 1871, 1950, 2134, 2270, 2377, 2200, 2255, 2260, 2141, 2312, 2137, 2270, 2128, 1935, 2032, 2138, 2297, 2436, 2583, 2691, 2502, 2441, 2300, 2373, 2329, 2251, 2379, 2307, 2470, 2500, 2380, 2370, 2408, 2378, 2299, 2448, 2496, 2441, 2538, 2581, 2544, 2581, 2477, 2309, 2354, 2370, 2357, 2277, 2263, 2092, 2212, 2093, 1940, 2093, 2143, 2198, 2385, 2286, 2284, 2245, 2306, 2388, 2281, 2158, 2213, 2009, 1924, 2047, 2164, 2142, 2092, 2102, 2005, 1923]
"data": [
1847, 1768, 1792, 2003, 2081, 2099, 2261, 2441, 2268, 2483, 2367, 2245, 2458, 2278, 2236, 2044, 1871, 1950, 2134, 2270, 2377, 2200, 2255, 2260, 2141, 2312, 2137, 2270, 2128, 1935, 2032, 2138, 2297, 2436, 2583, 2691, 2502, 2441, 2300, 2373, 2329, 2251, 2379, 2307, 2470, 2500, 2380, 2370, 2408, 2378, 2299,
2448, 2496, 2441, 2538, 2581, 2544, 2581, 2477, 2309, 2354, 2370, 2357, 2277, 2263, 2092, 2212, 2093, 1940, 2093, 2143, 2198, 2385, 2286, 2284, 2245, 2306, 2388, 2281, 2158, 2213, 2009, 1924, 2047, 2164, 2142, 2092, 2102, 2005, 1923
]
}
]
},
@@ -1012,7 +1030,10 @@
{
"name": "Sessions",
"color": "primary",
"data": [1847, 1768, 1792, 2003, 2081, 2099, 2261, 2441, 2268, 2483, 2367, 2245, 2458, 2278, 2236, 2044, 1871, 1950, 2134, 2270, 2377, 2200, 2255, 2260, 2141, 2312, 2137, 2270, 2128, 1935, 2032, 2138, 2297, 2436, 2583, 2691, 2502, 2441, 2300, 2373, 2329, 2251, 2379, 2307, 2470, 2500, 2380, 2370, 2408, 2378, 2299, 2448, 2496, 2441, 2538, 2581, 2544, 2581, 2477, 2309, 2354, 2370, 2357, 2277, 2263, 2092, 2212, 2093, 1940, 2093, 2143, 2198, 2385, 2286, 2284, 2245, 2306, 2388, 2281, 2158, 2213, 2009, 1924, 2047, 2164, 2142, 2092, 2102, 2005, 1923]
"data": [
1847, 1768, 1792, 2003, 2081, 2099, 2261, 2441, 2268, 2483, 2367, 2245, 2458, 2278, 2236, 2044, 1871, 1950, 2134, 2270, 2377, 2200, 2255, 2260, 2141, 2312, 2137, 2270, 2128, 1935, 2032, 2138, 2297, 2436, 2583, 2691, 2502, 2441, 2300, 2373, 2329, 2251, 2379, 2307, 2470, 2500, 2380, 2370, 2408, 2378, 2299,
2448, 2496, 2441, 2538, 2581, 2544, 2581, 2477, 2309, 2354, 2370, 2357, 2277, 2263, 2092, 2212, 2093, 1940, 2093, 2143, 2198, 2385, 2286, 2284, 2245, 2306, 2388, 2281, 2158, 2213, 2009, 1924, 2047, 2164, 2142, 2092, 2102, 2005, 1923
]
}
]
},
+4
View File
@@ -212,6 +212,10 @@
"title": "Icon",
"url": "/ui/components/icon"
},
{
"title": "Legend",
"url": "/ui/components/legend"
},
{
"title": "List group",
"url": "/ui/components/list-group"
+4
View File
@@ -160,6 +160,10 @@
}
}
},
"legend": {
"url": "legend.html",
"title": "Legend"
},
"lists": {
"title": "Lists",
"url": "lists.html"
+29 -8
View File
@@ -1,15 +1,36 @@
---
// A single legend item: a colored .legend dot + label, e.g. the "This year" /
// "Last year" chart legend in SalesOverview.astro.
// Legend (.legend): marker + label, with an optional value.
// Renders a link when `href` is given, a button when `button` is set, a span otherwise.
interface Props {
color?: string
/** label text; the default slot is used instead when empty */
text?: string
/** legend-value next to the label */
value?: string | number | undefined
/** legend-{color} on the item, shared by the marker and the hover background */
color?: string | undefined
/** render a <button type="button"> (toggle control) */
button?: boolean | undefined
/** render an <a> with this target */
href?: string | undefined
/** active: keeps the hover background */
active?: boolean | undefined
/** legend-off: dims the item, for a hidden series */
off?: boolean | undefined
class?: string
/** remaining attributes (id, data-*, aria-*, …) are forwarded to the root element */
[key: string]: unknown
}
const { color = 'primary', class: className } = Astro.props
const { text, value, color, button, href, active, off, class: className, ...rest } = Astro.props
const Element = href ? 'a' : button ? 'button' : 'span'
const typeAttr = !href && button ? 'button' : undefined
const classes = ['legend', color && `legend-${color}`, active && 'active', off && 'legend-off', className]
---
<div class:list={['d-flex align-items-center gap-2', className]}>
<span class={`legend bg-${color}`}></span>
<span class="text-secondary"><slot /></span>
</div>
<Element class:list={classes} href={href} type={typeAttr} {...rest}>
<span class="legend-dot"></span>
{text ?? <slot />}
{value !== undefined && <span class="legend-value">{value}</span>}
</Element>
+17
View File
@@ -0,0 +1,17 @@
---
// Legend list container (.legend-list).
interface Props {
/** legend-list-vertical (stacked items) */
vertical?: boolean
class?: string
/** remaining attributes (id, data-*, …) are forwarded to the root element */
[key: string]: unknown
}
const { vertical, class: className, ...rest } = Astro.props
---
<div class:list={['legend-list', vertical && 'legend-list-vertical', className]} {...rest}>
<slot />
</div>
+1 -1
View File
@@ -35,7 +35,7 @@ const { text, flag, icon, person, status, legend, checkbox, checked, badge, clas
) : status ? (
<Badge color={status} class="badge-dot" text="" />
) : legend ? (
<span class={`legend bg-${legend}`} />
<span class={`legend-dot bg-${legend}`} />
) : checkbox ? (
<input type="checkbox" class="form-check-input tag-check" checked={checked} />
) : null