Extend charts with new types, a palette and theme tokens (#2915)

This commit is contained in:
Paweł Kuna authored and GitHub committed 2026-08-31 18:30:20 +02:00
1 parent 135f38d4b9
commit 09d419a5b5
23 files changed
+1150 -219

No files matched your search

+1
View File
@@ -17,6 +17,7 @@ export const cssVarPrefix = 'tblr-'
// foreign name. // foreign name.
export const cssVarIgnore = [ export const cssVarIgnore = [
/^--tblr-/, // already prefixed, e.g. names that cannot round-trip postcss (see $navbar-light-icon-color) /^--tblr-/, // already prefixed, e.g. names that cannot round-trip postcss (see $navbar-light-icon-color)
/^--apx-/, // apexcharts
/^--bs-/, // bootstrap /^--bs-/, // bootstrap
/^--fc-/, // fullcalendar /^--fc-/, // fullcalendar
/^--gl-/, // star-rating.js /^--gl-/, // star-rating.js
+6
View File
@@ -0,0 +1,6 @@
---
"@tabler/preview": minor
"@tabler/docs": minor
---
Added a `charts-advanced` page and docs for two y-axes, chart annotations, a zoom brush and synced chart groups.
+6
View File
@@ -0,0 +1,6 @@
---
"@tabler/core": patch
"@tabler/docs": patch
---
Documented the ApexCharts dual license, which applies to the copy shipped in `dist/libs`, in the readme and chart docs.
+7
View File
@@ -0,0 +1,7 @@
---
"@tabler/core": minor
"@tabler/preview": patch
"@tabler/docs": patch
---
Added a `--tblr-chart-1` … `--tblr-chart-5` palette and wired ApexCharts theme tokens, so every chart follows the color mode.
+5
View File
@@ -0,0 +1,5 @@
---
"@tabler/preview": patch
---
Removed dead chart options (`groups`, `spline`, `fill`, `debug`, `remove-padding`, `show-labels`) from `charts.json`.
+5
View File
@@ -0,0 +1,5 @@
---
"@tabler/preview": patch
---
Fixed the combination chart demo so it mixes columns and lines instead of drawing plain bars.
+7
View File
@@ -0,0 +1,7 @@
---
"@tabler/core": patch
"@tabler/preview": patch
"@tabler/docs": patch
---
Fixed chart colors in dark mode: axis lines and ticks, marker and treemap outlines, and the tooltip title.
@@ -0,0 +1,5 @@
---
"@tabler/docs": patch
---
Fixed the small chart example on the chart docs page, which shared an element id with the line chart example and stayed empty.
+6
View File
@@ -0,0 +1,6 @@
---
"@tabler/preview": minor
"@tabler/docs": minor
---
Added radar, polar area, treemap, timeline, box plot, bubble and funnel chart types to the charts preview and docs.
+6
View File
@@ -0,0 +1,6 @@
---
"@tabler/preview": patch
"@tabler/docs": patch
---
Replaced the placeholder chart data on the charts page and in the chart docs with realistic series, and titled every demo card.
+5
View File
@@ -0,0 +1,5 @@
---
"@tabler/preview": minor
---
Added a straight `line` chart demo and set `stroke-curve` to straight on the line and area demos, so the spline ones differ.
+7
View File
@@ -208,3 +208,10 @@ Stay up to date by joining our community on [X](https://x.com/tabler_io) and [Fa
## 📄 License ## 📄 License
Tabler is licensed under the [MIT License](LICENSE). Tabler is licensed under the [MIT License](LICENSE).
Third-party libraries shipped in `dist/libs` keep their own licenses. One of them needs
attention: **ApexCharts is not MIT from version 5 on**. It is dual-licensed - free under
its Community license for organizations under $2M in annual revenue, and paid above that
threshold, with a separate OEM license for redistribution. Check the
[ApexCharts license options](https://apexcharts.com/license/) before you ship charts in a
commercial product.
+7
View File
@@ -94,3 +94,10 @@ We welcome contributions of all kinds! See the [contributing guide](https://gith
## 📄 License ## 📄 License
Tabler is licensed under the [MIT License](https://github.com/tabler/tabler/blob/dev/LICENSE). Tabler is licensed under the [MIT License](https://github.com/tabler/tabler/blob/dev/LICENSE).
Third-party libraries shipped in `dist/libs` keep their own licenses. One of them needs
attention: **ApexCharts is not MIT from version 5 on**. It is dual-licensed - free under
its Community license for organizations under $2M in annual revenue, and paid above that
threshold, with a separate OEM license for redistribution. Check the
[ApexCharts license options](https://apexcharts.com/license/) before you ship charts in a
commercial product.
@@ -2,6 +2,15 @@
exports[`css custom-property prefixing > leaves vendor-owned names alone and prefixes everything else 1`] = ` exports[`css custom-property prefixing > leaves vendor-owned names alone and prefixes everything else 1`] = `
[ [
"--apx-accent",
"--apx-fore",
"--apx-grid",
"--apx-series-1",
"--apx-series-2",
"--apx-series-3",
"--apx-series-4",
"--apx-series-5",
"--apx-tt-color-muted",
"--fc-border-color", "--fc-border-color",
"--fc-daygrid-event-dot-width", "--fc-daygrid-event-dot-width",
"--fc-event-bg-color", "--fc-event-bg-color",
@@ -25,6 +34,7 @@ exports[`css custom-property prefixing > leaves vendor-owned names alone and pre
"--litepicker-month-weekday-color", "--litepicker-month-weekday-color",
"--plyr-color-main", "--plyr-color-main",
"--tblr-active-bg", "--tblr-active-bg",
"--tblr-azure",
"--tblr-backdrop-bg", "--tblr-backdrop-bg",
"--tblr-bg-forms", "--tblr-bg-forms",
"--tblr-bg-surface", "--tblr-bg-surface",
@@ -38,6 +48,11 @@ exports[`css custom-property prefixing > leaves vendor-owned names alone and pre
"--tblr-border-style", "--tblr-border-style",
"--tblr-border-width", "--tblr-border-width",
"--tblr-card-bg", "--tblr-card-bg",
"--tblr-chart-1",
"--tblr-chart-2",
"--tblr-chart-3",
"--tblr-chart-4",
"--tblr-chart-5",
"--tblr-font-sans-serif", "--tblr-font-sans-serif",
"--tblr-font-size-h3", "--tblr-font-size-h3",
"--tblr-font-size-h5", "--tblr-font-size-h5",
@@ -46,12 +61,15 @@ exports[`css custom-property prefixing > leaves vendor-owned names alone and pre
"--tblr-form-select-bg-icon", "--tblr-form-select-bg-icon",
"--tblr-form-select-bg-img", "--tblr-form-select-bg-img",
"--tblr-gray-500", "--tblr-gray-500",
"--tblr-green",
"--tblr-icon-size", "--tblr-icon-size",
"--tblr-info", "--tblr-info",
"--tblr-light", "--tblr-light",
"--tblr-orange",
"--tblr-primary", "--tblr-primary",
"--tblr-primary-200", "--tblr-primary-200",
"--tblr-primary-lt", "--tblr-primary-lt",
"--tblr-purple",
"--tblr-secondary", "--tblr-secondary",
"--tblr-secondary-bg", "--tblr-secondary-bg",
"--tblr-shadow-card", "--tblr-shadow-card",
+44 -1
View File
@@ -1,10 +1,41 @@
@use '../config' as *; @use '../config' as *;
// ApexCharts reads these tokens from the chart element's computed style (theme.tokens,
// on by default since v6), so the library themes itself — including charts an app builds
// on its own, without going through Tabler's chart helpers.
//
// The palette is the public part: override --tblr-chart-1 … --tblr-chart-5 to recolor
// every chart at once, the same way --tblr-primary recolors the rest of the interface.
:root {
--chart-1: var(--primary);
--chart-2: var(--green);
--chart-3: var(--azure);
--chart-4: var(--purple);
--chart-5: var(--orange);
--apx-accent: var(--chart-1);
--apx-fore: var(--secondary);
--apx-grid: var(--border-color);
// No --apx-surface on purpose: it makes ApexCharts paint a solid background behind the
// plot, which would cover the card underneath — tinted and gradient cards included.
--apx-series-1: var(--chart-1);
--apx-series-2: var(--chart-2);
--apx-series-3: var(--chart-3);
--apx-series-4: var(--chart-4);
--apx-series-5: var(--chart-5);
}
.apexcharts-tooltip { .apexcharts-tooltip {
// ApexCharts paints the tooltip title and other muted text from this variable, and its
// default is a dark gray that disappears on the inverted Tabler tooltip surface.
// !important because ApexCharts injects its own stylesheet at runtime, after this one,
// and its default lands on the same selector.
--apx-tt-color-muted: #{$tooltip-color} !important;
padding: 0.25rem !important; padding: 0.25rem !important;
font-size: $tooltip-font-size !important; font-size: $tooltip-font-size !important;
color: $tooltip-color !important; color: $tooltip-color !important;
background: $tooltip-bg !important; background: $tooltip-bg !important;
border: 0 !important;
box-shadow: none !important; box-shadow: none !important;
} }
@@ -14,6 +45,7 @@
font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-semibold);
background: transparent !important; background: transparent !important;
border: 0 !important; border: 0 !important;
opacity: 0.7;
} }
.apexcharts-tooltip-y-group { .apexcharts-tooltip-y-group {
@@ -28,10 +60,21 @@
font-size: 16px !important; font-size: 16px !important;
} }
.apexcharts-gridline { .apexcharts-gridline,
.apexcharts-grid-borders line,
.apexcharts-xaxis-tick,
.apexcharts-yaxis-tick {
stroke: var(--border-color) !important; stroke: var(--border-color) !important;
} }
// ApexCharts outlines markers, treemap cells and data label pills in white, so they
// separate from the plot. On a dark background that outline is the thing that stands out.
.apexcharts-marker,
.apexcharts-treemap-rect,
.apexcharts-data-labels rect {
stroke: var(--bg-surface) !important;
}
.apexcharts-legend-text { .apexcharts-legend-text {
color: inherit !important; color: inherit !important;
} }
+207 -2
View File
@@ -35,6 +35,12 @@ ApexCharts draws with its own default colors, grid and tooltip. Tabler restyles
<CdnImportPlugin plugins={['vendors']} /> <CdnImportPlugin plugins={['vendors']} />
### License
Tabler is MIT licensed, but ApexCharts is not. From version 5 on it is dual-licensed: free under its Community license for organizations under $2 million in annual revenue, paid above that, with a separate OEM license for redistributing it inside a product you ship to others.
Tabler bundles the built file in `dist/libs` so the examples work out of the box, but the license stays with ApexCharts. Read the [ApexCharts license options](https://apexcharts.com/license/) before you use charts in a commercial product.
## Usage ## Usage
Add an empty element with the `chart` class, then pass it to ApexCharts. The class reserves a minimum height, so the card does not jump while the chart is loading. Add an empty element with the `chart` class, then pass it to ApexCharts. The class reserves a minimum height, so the card does not jump while the chart is loading.
@@ -75,7 +81,7 @@ The size classes below reserve space before the chart renders, so the layout doe
<Example> <Example>
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<Chart chartId="demo-line" size="sm" label="Small chart example" /> <Chart chartId="demo-line" id="demo-line-sm" size="sm" label="Small chart example" />
</div> </div>
</div> </div>
</Example> </Example>
@@ -92,6 +98,56 @@ A sparkline is a small chart with no axes, grid or tooltip, meant to sit inline
Add `chart-sparkline-sm` for a shorter sparkline, `chart-sparkline-square` for a pie or radial one, and `chart-sparkline-wide` when the default width is too narrow. Add `chart-sparkline-sm` for a shorter sparkline, `chart-sparkline-square` for a pie or radial one, and `chart-sparkline-wide` when the default width is too narrow.
### Colors
Tabler ships a five-color chart palette. Charts pick colors from it in order, so a chart with three series is colored without naming a single color.
| Variable | Default |
| --- | --- |
| `--tblr-chart-1` | `--tblr-primary` |
| `--tblr-chart-2` | `--tblr-green` |
| `--tblr-chart-3` | `--tblr-azure` |
| `--tblr-chart-4` | `--tblr-purple` |
| `--tblr-chart-5` | `--tblr-orange` |
Override them to recolor every chart at once, the same way `--tblr-primary` recolors the rest of the interface:
```css
:root {
--tblr-chart-1: #7048e8;
--tblr-chart-2: #f76707;
}
```
To color one series, pass the color in the ApexCharts config as usual:
```js
new ApexCharts(element, {
colors: ['var(--tblr-chart-2)'],
// ...
}).render();
```
### Theme tokens
ApexCharts reads its own design tokens from the chart element, and Tabler sets them to its theme colors. Text, gridlines and the chart surface follow the color mode on their own - including charts you build yourself, without any Tabler helper.
| Token | Used for | Tabler value |
| --- | --- | --- |
| `--apx-fore` | labels, axis titles, legend | `--tblr-secondary` |
| `--apx-grid` | gridlines and axis lines | `--tblr-border-color` |
| `--apx-surface` | chart background | `--tblr-bg-surface` |
| `--apx-accent` | single-series color | `--tblr-chart-1` |
| `--apx-series-1` … `--apx-series-5` | multi-series palette | `--tblr-chart-1` … `--tblr-chart-5` |
Set a token on a single card to change only that chart:
```css
.card-dark-chart {
--apx-grid: rgba(255, 255, 255, 0.1);
}
```
## Chart types ## Chart types
See the [ApexCharts documentation](https://apexcharts.com/docs/) for the full list of options behind each of these. See the [ApexCharts documentation](https://apexcharts.com/docs/) for the full list of options behind each of these.
@@ -151,7 +207,91 @@ Heatmaps provide a graphical representation of data where individual values are
<Example> <Example>
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<Chart chartId="demo-heatmap" label="Heatmap chart example" height={15} /> <Chart chartId="activity-heatmap" label="Heatmap chart example" height={15} />
</div>
</div>
</Example>
### Radar chart
Radar charts compare a few series across the same set of categories. They work best for scores and ratings, where every axis uses the same scale. Set `chart.type` to `radar` and pass the axis names in `xaxis.categories`:
<Example>
<div class="card">
<div class="card-body">
<Chart chartId="radar" label="Radar chart example" height={15} />
</div>
</div>
</Example>
### Polar area chart
A polar area chart is a pie chart where each slice keeps the same angle and grows outwards with its value. Use it when the parts of a whole differ a lot in size. Set `chart.type` to `polarArea` and pass plain numbers in `series`:
<Example>
<div class="card">
<div class="card-body">
<Chart chartId="polar-area" label="Polar area chart example" height={15} />
</div>
</div>
</Example>
### Treemap chart
Treemaps show parts of a whole as blocks. The bigger the value, the bigger the block, so they fit storage, budgets and other breakdowns with many entries. Set `chart.type` to `treemap` and pass `{ x, y }` points, where `x` is the label:
<Example>
<div class="card">
<div class="card-body">
<Chart chartId="treemap" label="Treemap chart example" height={15} />
</div>
</div>
</Example>
### Timeline chart
A timeline puts one bar per row between a start and an end date. Use it for roadmaps, releases and schedules. Set `chart.type` to `rangeBar`, turn on `plotOptions.bar.horizontal`, set `xaxis.type` to `datetime` and pass `y` as a `[start, end]` pair of timestamps:
<Example>
<div class="card">
<div class="card-body">
<Chart chartId="timeline" label="Timeline chart example" height={15} />
</div>
</div>
</Example>
### Box plot chart
Box plots show how values spread out, not just their average. Each box covers the middle half of the data, the line inside it is the median, and the whiskers reach the lowest and highest values. Set `chart.type` to `boxPlot` and pass `y` as `[min, q1, median, q3, max]`:
<Example>
<div class="card">
<div class="card-body">
<Chart chartId="box-plot" label="Box plot chart example" height={15} />
</div>
</div>
</Example>
### Bubble chart
Bubble charts add a third value to a scatter plot: the size of the point. Set `chart.type` to `bubble` and pass `{ x, y, z }` points, where `z` is the bubble size:
<Example>
<div class="card">
<div class="card-body">
<Chart chartId="bubble" label="Bubble chart example" height={15} />
</div>
</div>
</Example>
### Funnel chart
A funnel shows how many items are left at each step of a process. It is a horizontal bar chart with `plotOptions.bar.isFunnel` turned on. Turn on `distributed` as well, so every stage gets its own color, and keep the legend on, because a funnel hides the y-axis labels:
<Example>
<div class="card">
<div class="card-body">
<Chart chartId="funnel" label="Funnel chart example" height={15} />
</div> </div>
</div> </div>
</Example> </Example>
@@ -168,6 +308,71 @@ For more complex data visualizations, you can create advanced charts with multip
</div> </div>
</Example> </Example>
## Axes, annotations and zoom
### Two y-axes
Series with different units need their own scale. Pass an array to `yaxis`, bind each entry to a serie with `seriesName`, and set `opposite` on the second one to move it to the right side.
<Example>
<div class="card">
<div class="card-body">
<Chart chartId="revenue-vs-orders" label="Revenue in dollars and order count per month" height={16} />
</div>
</div>
</Example>
Mixing shapes works the same way: give a serie its own `type` (`column`, `line`, `area`) and ApexCharts draws that serie differently from the rest.
### Annotations
Annotations draw a line across the plot. Use `annotations.yaxis` for a target or a limit, and `annotations.xaxis` for a moment in time, like a release or a campaign.
<Example>
<div class="card">
<div class="card-body">
<Chart chartId="uptime-sla" label="Monthly uptime against the 99.9 percent service level target" height={16} />
</div>
</div>
</Example>
Each annotation takes a value, a `borderColor` and an optional `label`. On a category axis the value is the category name, on a datetime axis it is a timestamp.
<Example>
<div class="card">
<div class="card-body">
<Chart chartId="sales-with-markers" label="Monthly sales with campaign start markers" height={16} />
</div>
</div>
</Example>
### Brush and zoom
A brush is a small chart that picks the range of a bigger one. Give the main chart an `id`, then point the small one at it with `chart.brush`, and set the starting range in `chart.selection`. Render the main chart first - the brush looks its target up by id.
<Example>
<div class="card">
<div class="card-body">
<Chart chartId="traffic-zoom" label="Sessions over 90 days, zoomed to the selected range" height={11} />
<Chart chartId="traffic-brush" label="Range selector for the sessions chart" height={5} />
</div>
</div>
</Example>
### Synced charts
Charts that share a `chart.group` name move together. Hover one and the others show the same point, zoom one and the others follow. Every chart in the group needs its own `chart.id` and the same x values.
<Example>
<div class="card">
<div class="card-body">
<Chart chartId="synced-visits" label="Daily visits" height={8} />
<Chart chartId="synced-signups" label="Daily sign-ups" height={8} />
<Chart chartId="synced-revenue" label="Daily revenue" height={8} />
</div>
</div>
</Example>
## Legend key ## 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. Add a `legend` span before each label and color it with a `bg-*` utility class.
+68
View File
@@ -0,0 +1,68 @@
---
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
import Chart from '@components/demo/Chart.astro'
import Card from '@ui/Card.astro'
import CardBody from '@ui/CardBody.astro'
import CardTitle from '@ui/CardTitle.astro'
import CardSubtitle from '@ui/CardSubtitle.astro'
import DocsLink from '@ui/DocsLink.astro'
---
<DefaultLayout title="Charts advanced" pageMenu="plugins.charts-advanced" pageLibs={['apexcharts']} description="Charts that go past a single series: two scales in one plot, threshold and marker lines, a brush to zoom with, and a group of charts that move together.">
<DocsLink slot="page-header-actions" path="/ui/plugins/chart" />
<div class="row row-cards">
<div class="col-lg-6">
<Card>
<CardBody>
<CardTitle>Two y-axes</CardTitle>
<CardSubtitle>Plot values with different units together. Bind each axis to a serie with <code>seriesName</code> and move the second one to the right with <code>opposite</code>.</CardSubtitle>
<Chart chartId="revenue-vs-orders" height={16} label="Revenue in dollars and order count per month" />
</CardBody>
</Card>
</div>
<div class="col-lg-6">
<Card>
<CardBody>
<CardTitle>Threshold line</CardTitle>
<CardSubtitle>Draw a target or a limit across the plot with <code>annotations.yaxis</code>, so a reader sees at a glance which points miss it.</CardSubtitle>
<Chart chartId="uptime-sla" height={16} label="Monthly uptime against the 99.9 percent service level target" />
</CardBody>
</Card>
</div>
<div class="col-lg-6">
<Card>
<CardBody>
<CardTitle>Marker lines</CardTitle>
<CardSubtitle>Mark the moment something happened with <code>annotations.xaxis</code> — a release, a campaign, an incident.</CardSubtitle>
<Chart chartId="sales-with-markers" height={16} label="Monthly sales with campaign start markers" />
</CardBody>
</Card>
</div>
<div class="col-lg-6">
<Card>
<CardBody>
<CardTitle>Brush and zoom</CardTitle>
<CardSubtitle>The small chart below picks the range shown above. Point it at the main chart id with <code>chart.brush</code> and render the main chart first.</CardSubtitle>
<Chart chartId="traffic-zoom" height={11} label="Sessions over 90 days, zoomed to the selected range" />
<Chart chartId="traffic-brush" height={5} label="Range selector for the sessions chart" />
</CardBody>
</Card>
</div>
<div class="col-12">
<Card>
<CardBody>
<CardTitle>Synced charts</CardTitle>
<CardSubtitle>Charts that share a <code>chart.group</code> name follow one another — hover or zoom on one and the rest do the same.</CardSubtitle>
<Chart chartId="synced-visits" height={8} label="Daily visits" />
<Chart chartId="synced-signups" height={8} label="Daily sign-ups" />
<Chart chartId="synced-revenue" height={8} label="Daily revenue" />
</CardBody>
</Card>
</div>
</div>
</DefaultLayout>
+2 -1
View File
@@ -26,8 +26,9 @@ const demoCharts = Object.entries(chartsData as Record<string, { demo?: boolean;
<div class="col-12"> <div class="col-12">
<Card> <Card>
<CardHeader title="Sprint velocity" />
<CardBody> <CardBody>
<Chart chartId="tasks-overview" height={20} label="Tasks overview" /> <Chart chartId="tasks-overview" height={20} label="Story points completed per sprint" />
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
+2 -2
View File
@@ -51,7 +51,7 @@ const elementId = `chart-${id}`;
<script is:inline define:vars={{ chartKey, elementId, config, xFormatterExpr }}> <script is:inline define:vars={{ chartKey, elementId, config, xFormatterExpr }}>
window.tabler_chart ??= {}; window.tabler_chart ??= {};
// Heatmap shades each cell by computing off the literal base color in JS // Heatmap and treemap shade each cell by computing off the literal base color in JS
// (light -> full color-mix by value), unlike other chart types where the // (light -> full color-mix by value), unlike other chart types where the
// SVG `fill` attribute just holds `var(--chart-...)` and the browser paints // SVG `fill` attribute just holds `var(--chart-...)` and the browser paints
// it directly. A raw var()/color-mix() string can't be parsed by that color // it directly. A raw var()/color-mix() string can't be parsed by that color
@@ -83,7 +83,7 @@ const elementId = `chart-${id}`;
config.xaxis.labels.formatter = new Function('val', `return (${xFormatterExpr})`); config.xaxis.labels.formatter = new Function('val', `return (${xFormatterExpr})`);
} }
if (config.chart?.type === 'heatmap' && Array.isArray(config.colors)) { if ((config.chart?.type === 'heatmap' || config.chart?.type === 'treemap') && Array.isArray(config.colors)) {
config.colors = config.colors.map(resolveCssVar); config.colors = config.colors.map(resolveCssVar);
} }
+445 -184
View File
@@ -58,62 +58,45 @@
"sparkline": true, "sparkline": true,
"legend": true, "legend": true,
"series": [ "series": [
{ { "name": "Organic search", "color": "primary", "data": 46 },
"name": "Direct", { "name": "Direct", "color": "primary", "color-opacity": "80%", "data": 38 },
"color": "primary", { "name": "Referral", "color": "primary", "color-opacity": "60%", "data": 19 },
"data": 44 { "name": "Social", "color": "gray-300", "data": 11 }
}, ],
{ "name": "Traffic sources"
"name": "Affilliate",
"color": "primary",
"color-opacity": "80%",
"data": 55
},
{
"name": "E-mail",
"color": "primary",
"color-opacity": "60%",
"data": 12
},
{
"name": "Other",
"color": "gray-300",
"data": 2
}
]
}, },
"demo-bar": { "demo-bar": {
"type": "bar", "type": "bar",
"legend": true, "legend": true,
"horizontal": true, "horizontal": true,
"stacked": true, "stacked": true,
"categories": [2008, 2009, 2010, 2011, 2012, 2013, 2014], "categories": [2019, 2020, 2021, 2022, 2023, 2024, 2025],
"x-formatter": "val + \"K\"", "x-formatter": "val + \"K\"",
"series": [ "series": [
{ {
"name": "Container for a Fanta", "name": "North America",
"color": "purple", "color": "purple",
"data": [44, 55, 41, 37, 22, 43, 21] "data": [128, 142, 151, 168, 182, 205, 224]
}, },
{ {
"name": "Strange sunglasses", "name": "Europe",
"color": "green", "color": "green",
"data": [53, 32, 33, 52, 13, 43, 32] "data": [96, 104, 118, 127, 141, 158, 172]
}, },
{ {
"name": "Pen Pineapple Apple Pen", "name": "Asia Pacific",
"color": "yellow", "color": "yellow",
"data": [12, 17, 11, 9, 15, 11, 20] "data": [42, 51, 63, 78, 94, 112, 131]
}, },
{ {
"name": "Binoculars", "name": "Latin America",
"color": "red", "color": "red",
"data": [9, 7, 5, 8, 6, 9, 4] "data": [18, 21, 25, 29, 34, 38, 44]
}, },
{ {
"name": "Magical notebook", "name": "Middle East",
"color": "primary", "color": "primary",
"data": [25, 12, 19, 32, 25, 24, 10] "data": [9, 11, 14, 16, 19, 23, 27]
} }
] ]
}, },
@@ -123,14 +106,14 @@
"datetime": true, "datetime": true,
"series": [ "series": [
{ {
"name": "series1", "name": "New users",
"color": "primary", "color": "primary",
"data": [56, 40, 39, 47, 34, 48, 44] "data": [312, 287, 341, 368, 355, 402, 421]
}, },
{ {
"name": "series2", "name": "Returning users",
"color": "purple", "color": "purple",
"data": [45, 43, 30, 23, 38, 39, 54] "data": [180, 176, 195, 210, 204, 231, 244]
} }
] ]
}, },
@@ -141,19 +124,19 @@
"legend": true, "legend": true,
"series": [ "series": [
{ {
"name": "Session Duration", "name": "Total visits",
"color": "yellow", "color": "yellow",
"data": [117, 92, 94, 98, 75, 110, 69, 80, 109, 113, 115, 95] "data": [462, 505, 488, 531, 549, 512, 578, 601, 566, 623, 645, 672]
}, },
{ {
"name": "Page Views", "name": "Unique visitors",
"color": "green", "color": "green",
"data": [59, 80, 61, 66, 70, 84, 87, 64, 94, 56, 55, 67] "data": [318, 342, 331, 366, 379, 351, 402, 419, 394, 438, 452, 471]
}, },
{ {
"name": "Total Visits", "name": "New users",
"color": "primary", "color": "primary",
"data": [53, 51, 52, 41, 46, 60, 45, 43, 30, 50, 58, 59] "data": [96, 118, 104, 127, 133, 112, 145, 158, 141, 167, 172, 189]
} }
] ]
}, },
@@ -215,82 +198,73 @@
}, },
"completion-tasks": { "completion-tasks": {
"demo": true, "demo": true,
"datetime": true, "categories": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
"debug": true,
"series": [ "series": [
{ {
"name": "Tasks completion", "name": "Tickets solved",
"data": [155, 65, 465, 265, 225, 325, 80] "data": [38, 44, 41, 52, 47, 21, 17]
} }
] ],
}, "name": "Bar chart"
"completion-tasks-2": {
"demo": true,
"extend": "completion-tasks",
"type": "line"
},
"completion-tasks-3": {
"demo": true,
"extend": "completion-tasks",
"type": "area"
}, },
"completion-tasks-2": { "demo": true, "extend": "completion-tasks", "type": "line", "name": "Line chart" },
"completion-tasks-3": { "demo": true, "extend": "completion-tasks", "type": "area", "name": "Area chart" },
"completion-tasks-4": { "completion-tasks-4": {
"demo": true, "demo": true,
"extend": "completion-tasks", "extend": "completion-tasks",
"stroke-curve": "straight", "stroke-curve": "straight",
"type": "line" "type": "line",
"name": "Straight line"
}, },
"completion-tasks-5": { "completion-tasks-5": {
"demo": true, "demo": true,
"extend": "completion-tasks", "extend": "completion-tasks",
"stroke-curve": "stepline", "stroke-curve": "stepline",
"type": "line" "type": "line",
"name": "Stepped line"
}, },
"completion-tasks-6": { "completion-tasks-6": {
"demo": true, "demo": true,
"extend": "completion-tasks", "extend": "completion-tasks",
"type": "bar", "type": "bar",
"horizontal": true "horizontal": true,
"name": "Horizontal bars"
}, },
"completion-tasks-7": { "completion-tasks-7": {
"demo": true, "demo": true,
"extend": "completion-tasks", "extend": "completion-tasks",
"type": "line", "type": "line",
"datalabels": true "datalabels": true,
"name": "Data labels"
}, },
"completion-tasks-8": { "completion-tasks-8": {
"demo": true, "demo": true,
"datetime": true,
"type": "bar", "type": "bar",
"categories": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
"series": [ "series": [
{ {
"data": [155, 65, 465, 265, 225, 325, 80] "name": "Solved",
"data": [38, 44, 41, 52, 47, 21, 17]
}, },
{ {
"data": [113, 42, 65, 54, 76, 65, 35], "name": "Reopened",
"color": "red" "color": "red",
"data": [5, 7, 4, 9, 6, 3, 2]
} }
] ],
}, "name": "Grouped bars"
"completion-tasks-9": {
"demo": true,
"extend": "completion-tasks-8",
"stacked": true
},
"completion-tasks-10": {
"demo": true,
"extend": "completion-tasks-8",
"type": "area"
}, },
"completion-tasks-9": { "demo": true, "extend": "completion-tasks-8", "stacked": true, "name": "Stacked bars" },
"completion-tasks-10": { "demo": true, "extend": "completion-tasks-8", "type": "area", "name": "Two areas" },
"completion-tasks-11": { "completion-tasks-11": {
"demo": true, "demo": true,
"extend": "completion-tasks-8", "extend": "completion-tasks-8",
"type": "area", "type": "area",
"stacked": true "stacked": true,
"name": "Stacked areas"
}, },
"tasks-overview": { "tasks-overview": {
"type": "bar", "type": "bar",
"debug": true,
"categories": [ "categories": [
"Sprint 1", "Sprint 1",
"Sprint 2", "Sprint 2",
@@ -319,7 +293,7 @@
], ],
"series": [ "series": [
{ {
"name": "A", "name": "Story points",
"data": [44, 32, 48, 72, 60, 16, 44, 32, 78, 50, 68, 34, 26, 48, 72, 60, 84, 64, 74, 52, 62, 50, 32, 22] "data": [44, 32, 48, 72, 60, 16, 44, 32, 78, 50, 68, 34, 26, 48, 72, 60, 84, 64, 74, 52, 62, 50, 32, 22]
} }
] ]
@@ -329,57 +303,40 @@
"type": "radialBar", "type": "radialBar",
"sparkline": true, "sparkline": true,
"series": [ "series": [
{ { "name": "Delivered", "color": "primary", "data": 96 },
"name": "Total Sent", { "name": "Opened", "color": "primary", "color-opacity": "80%", "data": 48 },
"color": "primary", { "name": "Clicked", "color": "primary", "color-opacity": "60%", "data": 14 }
"data": 44 ],
}, "name": "Email campaign"
{
"name": "Reached",
"color": "primary",
"color-opacity": "80%",
"data": 36
},
{
"name": "Opened",
"color": "primary",
"color-opacity": "60%",
"data": 18
}
]
}, },
"tasks": { "tasks": {
"demo": true, "demo": true,
"type": "line", "type": "line",
"categories": ["M", "T", "W", "T", "F", "S", "S", "M", "T", "W", "T", "F", "S", "S", "M", "T", "W"], "categories": ["M", "T", "W", "T", "F", "S", "S", "M", "T", "W", "T", "F", "S", "S", "M", "T", "W"],
"groups": [1, 2, 3],
"hide-points": true, "hide-points": true,
"fill": true,
"remove-padding": true,
"hide-tooltip": true, "hide-tooltip": true,
"series": [ "series": [
{ {
"name": "New", "name": "Opened",
"color": "primary", "color": "primary",
"data": [0, 0, 1, 2, 21, 9, 12, 10, 31, 13, 65, 10, 12, 6, 4, 3, 0] "data": [12, 15, 9, 18, 24, 6, 4, 14, 31, 22, 17, 19, 8, 5, 16, 21, 13]
},
{
"name": "Completed",
"color": "lime",
"data": [0, 0, 1, 2, 7, 5, 6, 8, 24, 7, 12, 5, 6, 3, 2, 2, 0]
}, },
{ {
"name": "Closed", "name": "Closed",
"color": "lime",
"data": [8, 11, 12, 14, 19, 4, 3, 9, 26, 18, 21, 15, 6, 4, 12, 17, 15]
},
{
"name": "Reopened",
"color": "orange", "color": "orange",
"data": [0, 0, 1, 0, 2, 0, 1, 0, 2, 3, 0, 2, 3, 2, 1, 0, 0] "data": [1, 2, 0, 3, 2, 0, 0, 1, 4, 2, 1, 3, 0, 1, 2, 1, 0]
} }
] ],
"name": "Issue activity"
}, },
"development-activity": { "development-activity": {
"type": "area", "type": "area",
"groups": [1, 2, 3],
"hide-points": true, "hide-points": true,
"remove-padding": true,
"sparkline": true, "sparkline": true,
"datetime": true, "datetime": true,
"series": [ "series": [
@@ -417,48 +374,53 @@
] ]
}, },
"line": { "line": {
"demo": true,
"type": "line", "type": "line",
"categories": ["2013", "2014", "2015", "2016", "2017", "2018"], "stroke-curve": "straight",
"categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
"series": [ "series": [
{ {
"name": "Development", "name": "Starter",
"color": "orange", "color": "orange",
"data": [2, 8, 6, 7, 14, 11] "data": [12, 14, 15, 17, 18, 21]
}, },
{ {
"name": "Marketing", "name": "Pro",
"color": "primary", "color": "primary",
"data": [5, 15, 11, 15, 21, 25] "data": [31, 34, 38, 41, 45, 52]
}, },
{ {
"name": "Sales", "name": "Enterprise",
"color": "green", "color": "green",
"data": [17, 18, 21, 20, 30, 29] "data": [48, 52, 51, 58, 64, 71]
} }
] ],
"name": "Revenue by plan"
}, },
"line-stroke": { "line-stroke": {
"demo": true, "demo": true,
"type": "line", "type": "line",
"stroke-curve": "straight", "stroke-curve": "straight",
"categories": ["2013", "2014", "2015", "2016", "2017", "2018", "2019"], "categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"],
"series": [ "series": [
{ {
"name": "Development", "name": "Organic",
"color": "orange",
"data": [8, 10, 11, 12, 20, 27, 30]
},
{
"name": "Marketing",
"color": "primary", "color": "primary",
"data": [3, 16, 17, 19, 20, 30, 30] "data": [120, 138, 152, 149, 167, 181, 192]
}, },
{ {
"name": "Sales", "name": "Paid",
"color": "orange",
"data": [80, 92, 88, 101, 96, 110, 118]
},
{
"name": "Referral",
"color": "green", "color": "green",
"data": [7, 10, 11, 18, 18, 18, 24] "data": [35, 41, 38, 47, 52, 49, 58]
} }
] ],
"name": "Signups by channel",
"stroke-dash": [0, 4, 4]
}, },
"stepline": { "stepline": {
"demo": true, "demo": true,
@@ -466,16 +428,17 @@
"stroke-curve": "stepline", "stroke-curve": "stepline",
"series": [ "series": [
{ {
"name": "Development", "name": "Instances",
"color": "orange", "color": "orange",
"data": [34, 44, 54, 21, 12, 43, 33, 23, 66, 66, 58] "data": [2, 2, 3, 5, 5, 4, 6, 8, 8, 6, 5]
} }
] ],
"name": "Running instances",
"categories": ["00:00", "02:00", "04:00", "06:00", "08:00", "10:00", "12:00", "14:00", "16:00", "18:00", "20:00"]
}, },
"temperature": { "temperature": {
"demo": true, "demo": true,
"type": "line", "type": "line",
"show-labels": true,
"show-data-labels": true, "show-data-labels": true,
"show-markers": true, "show-markers": true,
"categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], "categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
@@ -483,87 +446,89 @@
{ {
"name": "Tokyo", "name": "Tokyo",
"color": "primary", "color": "primary",
"data": [7, 6.9, 9.5, 14.5, 18.4, 21.5, 25.2, 26.5, 23.3, 18.3, 13.9, 9.6] "data": [5.8, 6.5, 9.7, 14.8, 19.2, 22.4, 26.1, 27.6, 24.3, 18.7, 13.4, 8.6]
}, },
{ {
"name": "London", "name": "London",
"color": "green", "color": "green",
"data": [3.9, 4.2, 5.7, 8.5, 11.9, 15.2, 17, 16.6, 14.2, 10.3, 6.6, 4.8] "data": [5.2, 5.4, 7.3, 9.8, 13.1, 16.2, 18.7, 18.4, 15.9, 12.3, 8.4, 5.9]
} }
] ],
"name": "Average temperature"
}, },
"area": { "area": {
"demo": true, "demo": true,
"type": "area", "type": "area",
"stroke-curve": "straight",
"categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"], "categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
"series": [ "series": [
{ {
"name": "Maximum", "name": "Mobile",
"color": "primary", "color": "primary",
"data": [11, 8, 15, 18, 19, 17] "data": [5100, 5400, 5900, 6300, 6800, 7200]
}, },
{ {
"name": "Minimum", "name": "Desktop",
"color": "pink", "color": "pink",
"data": [7, 7, 5, 7, 9, 12] "data": [4200, 4100, 3900, 4000, 3800, 3700]
} }
] ],
"name": "Sessions by device"
}, },
"area-spline": { "area-spline": {
"demo": true, "demo": true,
"type": "area", "type": "area",
"spline": true,
"categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"], "categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
"series": [ "series": [
{ {
"name": "Maximum", "name": "Egress",
"color": "primary", "color": "primary",
"data": [11, 8, 15, 18, 19, 17] "data": [12, 15, 14, 18, 21, 24]
}, },
{ {
"name": "Minimum", "name": "Ingress",
"color": "pink", "color": "pink",
"data": [7, 7, 5, 7, 9, 12] "data": [7, 8, 9, 9, 11, 13]
} }
] ],
"name": "Bandwidth usage"
}, },
"area-spline-stacked": { "area-spline-stacked": {
"demo": true, "demo": true,
"type": "area", "type": "area",
"spline": true,
"stacked": true, "stacked": true,
"categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"], "categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
"series": [ "series": [
{ {
"name": "Maximum", "name": "Media",
"color": "primary", "color": "primary",
"data": [11, 8, 15, 18, 19, 17] "data": [140, 152, 161, 170, 182, 195]
}, },
{ {
"name": "Minimum", "name": "Backups",
"color": "pink", "color": "pink",
"data": [7, 7, 5, 7, 9, 12] "data": [90, 95, 101, 110, 118, 126]
} }
] ],
"name": "Storage by bucket"
}, },
"spline": { "spline": {
"demo": true, "demo": true,
"type": "line", "type": "line",
"spline": true, "categories": ["00:00", "03:00", "06:00", "09:00", "12:00", "15:00", "18:00", "21:00"],
"show-labels": true,
"categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
"series": [ "series": [
{ {
"name": "Hestavollane", "name": "Web servers",
"color": "primary", "color": "primary",
"data": [0.2, 0.8, 0.8, 0.8, 1, 1.3, 1.5, 2.9, 1.9, 2.6, 1.6, 3, 4, 3.6, 4.5, 4.2, 4.5, 4.5, 4, 3.1, 2.7, 4, 2.7, 2.3, 2.3, 4.1, 7.7, 7.1, 5.6, 6.1, 5.8, 8.6, 7.2, 9, 10.9, 11.5, 11.6, 11.1, 12, 12.3, 10.7, 9.4, 9.8, 9.6, 9.8, 9.5, 8.5, 7.4, 7.6] "data": [18, 14, 21, 46, 62, 58, 41, 27]
}, },
{ {
"name": "Vik", "name": "Workers",
"color": "green", "color": "green",
"data": [0, 0, 0.6, 0.9, 0.8, 0.2, 0, 0, 0, 0.1, 0.6, 0.7, 0.8, 0.6, 0.2, 0, 0.1, 0.3, 0.3, 0, 0.1, 0, 0, 0, 0.2, 0.1, 0, 0.3, 0, 0.1, 0.2, 0.1, 0.3, 0.3, 0, 3.1, 3.1, 2.5, 1.5, 1.9, 2.1, 1, 2.3, 1.9, 1.2, 0.7, 1.3, 0.4, 0.3] "data": [34, 29, 26, 38, 51, 47, 39, 36]
} }
] ],
"name": "CPU load"
}, },
"pie": { "pie": {
"type": "pie", "type": "pie",
@@ -741,46 +706,342 @@
"categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"], "categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
"series": [ "series": [
{ {
"name": "Maximum", "name": "Desktop",
"color": "primary", "color": "primary",
"data": [11, 8, 15, 18, 19, 17] "data": [1.8, 1.9, 1.6, 1.5, 1.4, 1.3]
}, },
{ {
"name": "Minimum", "name": "Mobile",
"color": "pink", "color": "pink",
"data": [7, 7, 5, 7, 9, 12] "data": [3.4, 3.2, 3.3, 2.9, 2.8, 2.6]
} }
] ],
"name": "Page load time"
}, },
"combination": { "combination": {
"demo": true, "demo": true,
"type": "bar", "type": "line",
"types": { "legend": true,
"2": "line", "categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
"3": "spline" "stroke-width": [0, 0, 2, 2],
},
"groups": [1, 4],
"categories": ["2013", "2014", "2015", "2016", "2017", "2018"],
"series": [ "series": [
{ {
"name": "Development", "name": "Paid spend",
"color": "green", "type": "column",
"data": [30, 20, 50, 40, 60, 50] "color": "primary",
"data": [120, 140, 130, 160, 175, 190]
}, },
{ {
"name": "Marketing", "name": "Content spend",
"type": "column",
"color": "azure",
"data": [60, 70, 65, 80, 90, 100]
},
{
"name": "Leads",
"type": "line",
"color": "green",
"data": [200, 230, 215, 260, 290, 310]
},
{
"name": "Trials",
"type": "line",
"color": "pink", "color": "pink",
"data": [200, 130, 90, 240, 130, 220] "data": [90, 105, 98, 120, 135, 150]
}
],
"name": "Spend and leads"
},
"radar": {
"demo": true,
"type": "radar",
"legend": true,
"categories": ["Support", "Security", "Speed", "Storage", "Integrations", "Price"],
"series": [
{
"name": "Pro",
"color": "primary",
"data": [70, 60, 80, 50, 65, 90]
}, },
{ {
"name": "Sales", "name": "Enterprise",
"color": "green", "color": "green",
"data": [300, 200, 160, 400, 250, 250] "data": [95, 90, 85, 100, 90, 40]
}
],
"name": "Plan comparison"
},
"polar-area": {
"demo": true,
"type": "polarArea",
"legend": true,
"series": [
{ "name": "North America", "color": "primary", "data": 42 },
{ "name": "Europe", "color": "primary", "color-opacity": "80%", "data": 33 },
{ "name": "Asia", "color": "primary", "color-opacity": "60%", "data": 21 },
{ "name": "South America", "color": "primary", "color-opacity": "40%", "data": 14 },
{ "name": "Africa", "color": "primary", "color-opacity": "20%", "data": 9 }
],
"name": "Sales by region"
},
"treemap": {
"demo": true,
"type": "treemap",
"series": [
{
"name": "Storage",
"color": "primary",
"treemap-data": [
{ "x": "Documents", "y": 218 },
{ "x": "Photos", "y": 149 },
{ "x": "Videos", "y": 184 },
{ "x": "Backups", "y": 55 },
{ "x": "Music", "y": 84 },
{ "x": "Archives", "y": 31 },
{ "x": "Other", "y": 70 }
]
}
],
"name": "Storage by folder"
},
"timeline": {
"demo": true,
"type": "rangeBar",
"horizontal": true,
"datetime": true,
"series": [
{
"name": "Roadmap",
"color": "primary",
"range-data": [
{
"x": "Research",
"y": [1767571200000, 1770681600000]
},
{
"x": "Design",
"y": [1769904000000, 1773532800000]
},
{
"x": "Development",
"y": [1772323200000, 1779235200000]
},
{
"x": "Testing",
"y": [1777593600000, 1781049600000]
},
{
"x": "Launch",
"y": [1780272000000, 1782345600000]
}
]
}
],
"name": "Release roadmap"
},
"box-plot": {
"demo": true,
"type": "boxPlot",
"series": [
{
"name": "Response time",
"color": "primary",
"box-data": [
{
"x": "Jan",
"y": [54, 66, 69, 75, 88]
},
{
"x": "Feb",
"y": [43, 65, 69, 76, 81]
},
{
"x": "Mar",
"y": [31, 39, 45, 51, 59]
},
{
"x": "Apr",
"y": [39, 46, 55, 65, 71]
},
{
"x": "May",
"y": [29, 31, 35, 39, 44]
},
{
"x": "Jun",
"y": [41, 49, 58, 61, 67]
}
]
}
],
"name": "API response times"
},
"bubble": {
"demo": true,
"type": "bubble",
"legend": true,
"series": [
{
"name": "Search",
"color": "primary",
"bubble-data": [
{ "x": 12, "y": 86, "z": 24 },
{ "x": 28, "y": 132, "z": 38 },
{ "x": 45, "y": 118, "z": 19 },
{ "x": 63, "y": 174, "z": 31 }
]
}, },
{
"name": "Social",
"color": "green",
"bubble-data": [
{ "x": 9, "y": 54, "z": 29 },
{ "x": 21, "y": 71, "z": 14 },
{ "x": 38, "y": 96, "z": 41 },
{ "x": 52, "y": 83, "z": 22 }
]
}
],
"name": "Campaign performance"
},
"funnel": {
"demo": true,
"type": "bar",
"funnel": true,
"datalabels": true,
"legend": true,
"categories": ["Visitors", "Sign-ups", "Trials", "Demos", "Customers"],
"colors": ["var(--tblr-primary)", "var(--tblr-azure)", "var(--tblr-cyan)", "var(--tblr-teal)", "var(--tblr-green)"],
"series": [
{
"name": "Pipeline",
"data": [1380, 920, 610, 380, 190]
}
],
"name": "Signup funnel"
},
"revenue-vs-orders": {
"type": "line",
"legend": true,
"categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
"stroke-width": [0, 2],
"y-axes": [
{ "series-name": "Revenue", "title": "Revenue ($)" },
{ "series-name": "Orders", "title": "Orders", "opposite": true }
],
"series": [
{
"name": "Revenue",
"type": "column",
"color": "primary",
"data": [12400, 14100, 13200, 16800, 18300, 17600, 19400, 21200, 20100, 23400, 25100, 27800]
},
{
"name": "Orders",
"type": "line",
"color": "green",
"data": [186, 204, 197, 242, 261, 253, 274, 298, 286, 321, 340, 372]
}
]
},
"uptime-sla": {
"type": "line",
"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" }
],
"series": [
{
"name": "Uptime",
"color": "primary",
"data": [99.98, 99.95, 99.87, 99.99, 100, 99.92, 99.76, 99.94, 99.97, 99.99, 99.91, 99.96]
}
]
},
"sales-with-markers": {
"type": "area",
"categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
"annotations-x": [
{ "value": "Apr", "label": "Spring campaign", "color": "azure" },
{ "value": "Sep", "label": "Relaunch", "color": "purple" }
],
"series": [
{ {
"name": "Sales", "name": "Sales",
"color": "primary", "color": "primary",
"data": [200, 130, 90, 240, 130, 220] "data": [128, 141, 137, 189, 232, 214, 198, 205, 264, 301, 288, 312]
}
]
},
"traffic-zoom": {
"type": "area",
"datetime": true,
"start-date": "2026-01-01",
"toolbar": true,
"series": [
{
"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]
}
]
},
"traffic-brush": {
"type": "line",
"datetime": true,
"start-date": "2026-01-01",
"brush-target": "traffic-zoom",
"selection-from": 1770681600000,
"selection-to": 1773273600000,
"series": [
{
"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]
}
]
},
"synced-visits": {
"type": "area",
"datetime": true,
"start-date": "2026-06-01",
"group": "traffic",
"series": [
{
"name": "Visits",
"color": "primary",
"data": [4255, 4019, 4307, 4606, 4506, 4306, 4249, 4435, 4453, 4262, 4336, 4765, 4977, 4764, 4718, 4468, 4395, 4584, 4689, 4792, 4565, 4863, 4547, 4930, 5310, 4890, 5352, 5716, 5599, 5718]
}
]
},
"synced-signups": {
"type": "line",
"datetime": true,
"start-date": "2026-06-01",
"group": "traffic",
"series": [
{
"name": "Sign-ups",
"color": "green",
"data": [191, 185, 207, 197, 192, 204, 224, 238, 214, 223, 243, 235, 255, 232, 249, 258, 231, 259, 257, 283, 302, 308, 297, 303, 312, 316, 319, 308, 292, 309]
}
]
},
"synced-revenue": {
"type": "bar",
"datetime": true,
"start-date": "2026-06-01",
"group": "traffic",
"series": [
{
"name": "Revenue",
"color": "azure",
"data": [991, 1032, 951, 841, 834, 957, 1027, 990, 944, 1016, 1023, 956, 864, 765, 686, 807, 791, 715, 720, 793, 778, 759, 827, 889, 783, 883, 915, 1028, 1164, 1090]
} }
] ]
}, },
+4
View File
@@ -447,6 +447,10 @@
"url": "charts.html", "url": "charts.html",
"title": "Charts" "title": "Charts"
}, },
"charts-advanced": {
"url": "charts-advanced.html",
"title": "Charts advanced"
},
"colorpicker": { "colorpicker": {
"url": "colorpicker.html", "url": "colorpicker.html",
"title": "Color picker" "title": "Color picker"
+106 -4
View File
@@ -16,10 +16,41 @@ describe('chartStyle', () => {
expect(css.startsWith('<style>')).toBe(true) expect(css.startsWith('<style>')).toBe(true)
}) })
it('adds fill variables for area charts', () => { it('fills an area with a wash of each serie color', () => {
const css = chartStyle({ id: 'a', data: { type: 'area', series: [] } }) const css = chartStyle({
expect(css).toContain('--chart-a-fill-0') id: 'a',
expect(css).toContain('--chart-a-fill-1') data: {
type: 'area',
series: [
{ name: 'A', data: [1], color: 'green' },
{ name: 'B', data: [2], color: 'pink' },
],
},
})
expect(css).toContain('--chart-a-fill-0: color-mix(in srgb, transparent, var(--tblr-green) 16%)')
expect(css).toContain('--chart-a-fill-1: color-mix(in srgb, transparent, var(--tblr-pink) 16%)')
})
it('falls back to the public palette when a serie names no color', () => {
const css = chartStyle({
id: 'p',
data: {
series: [
{ name: 'A', data: [1] },
{ name: 'B', data: [2] },
{ name: 'C', data: [3] },
],
},
})
expect(css).toContain('--chart-p-color-0: color-mix(in srgb, transparent, var(--tblr-chart-1) 100%)')
expect(css).toContain('--chart-p-color-1: color-mix(in srgb, transparent, var(--tblr-chart-2) 100%)')
expect(css).toContain('--chart-p-color-2: color-mix(in srgb, transparent, var(--tblr-chart-3) 100%)')
})
it('adds box variables for box plots', () => {
const css = chartStyle({ id: 'b', data: { type: 'boxPlot', series: [{ 'name': 'A', 'color': 'green', 'box-data': [{ x: 'Jan', y: [1, 2, 3, 4, 5] }] }] } })
expect(css).toContain('--chart-b-box-upper: color-mix(in srgb, transparent, var(--tblr-green) 100%)')
expect(css).toContain('--chart-b-box-lower: color-mix(in srgb, transparent, var(--tblr-green) 40%)')
}) })
}) })
@@ -29,6 +60,77 @@ describe('chartConfig', () => {
expect(config.chart).toMatchObject({ type: 'line', height: 160 }) expect(config.chart).toMatchObject({ type: 'line', height: 160 })
}) })
it('renders a polar area from plain numbers and labels, like a pie', () => {
const { config } = chartConfig({
id: 'demo',
data: {
type: 'polarArea',
series: [
{ name: 'A', data: 12 },
{ name: 'B', data: 8 },
],
},
height: 10,
})
expect(config.series).toEqual([12, 8])
expect(config.labels).toEqual(['A', 'B'])
expect(config.yaxis).toBeUndefined()
})
it('turns a bar chart into a funnel', () => {
const { config } = chartConfig({ id: 'demo', data: { type: 'bar', funnel: true, series: [{ name: 'Pipeline', data: [3, 2, 1] }] }, height: 10 })
expect(config.plotOptions).toMatchObject({ bar: { isFunnel: true, horizontal: true, distributed: true } })
})
it('passes the { x, y } points of the newer chart types straight through', () => {
const points = [{ x: 'Research', y: [1, 2] }]
const { config } = chartConfig({ id: 'demo', data: { type: 'rangeBar', series: [{ 'name': 'Roadmap', 'range-data': points }] }, height: 10 })
expect(config.series).toEqual([{ name: 'Roadmap', data: points }])
})
it('builds threshold and marker lines from the annotation shorthand', () => {
const { config } = chartConfig({
id: 'demo',
data: { 'series': [], 'annotations-y': [{ value: 99.9, label: 'SLA 99.9%' }], 'annotations-x': [{ value: 'Apr', color: 'azure' }] },
height: 10,
})
expect(config.annotations).toMatchObject({
yaxis: [{ y: 99.9, borderColor: 'var(--tblr-red)', label: { text: 'SLA 99.9%' } }],
xaxis: [{ x: 'Apr', borderColor: 'var(--tblr-azure)' }],
})
})
it('puts the second y axis on the right and binds each axis to its serie', () => {
const { config } = chartConfig({
id: 'demo',
data: {
'series': [
{ name: 'Revenue', data: [1] },
{ name: 'Orders', data: [2] },
],
'y-axes': [{ 'series-name': 'Revenue' }, { 'series-name': 'Orders', 'opposite': true }],
},
height: 10,
})
expect(config.yaxis).toMatchObject([
{ seriesName: 'Revenue', opposite: undefined },
{ seriesName: 'Orders', opposite: true },
])
})
it('points a brush chart at its target and names every chart', () => {
const { config } = chartConfig({
id: 'traffic-brush',
data: { 'series': [], 'brush-target': 'traffic-zoom', 'selection-from': 1, 'selection-to': 2 },
height: 10,
})
expect(config.chart).toMatchObject({
id: 'traffic-brush',
brush: { enabled: true, target: 'traffic-zoom' },
selection: { enabled: true, xaxis: { min: 1, max: 2 } },
})
})
it('returns the x-formatter as a separate expression instead of embedding it', () => { it('returns the x-formatter as a separate expression instead of embedding it', () => {
const { config, xFormatterExpr } = chartConfig({ id: 'demo', data: { 'series': [], 'x-formatter': 'val + "K"' }, height: 10 }) const { config, xFormatterExpr } = chartConfig({ id: 'demo', data: { 'series': [], 'x-formatter': 'val + "K"' }, height: 10 })
expect(xFormatterExpr).toBe('val + "K"') expect(xFormatterExpr).toBe('val + "K"')
+181 -25
View File
@@ -4,12 +4,22 @@
type Serie = { type Serie = {
'name'?: string 'name'?: string
/** overrides the chart type for this one serie — how a combination chart is built */
'type'?: string
'data'?: number[] | number 'data'?: number[] | number
'color'?: string 'color'?: string
'color-opacity'?: string 'color-opacity'?: string
'candlestick-data'?: { x: number; y: number[] }[] 'candlestick-data'?: { x: number; y: number[] }[]
/** one row of a heatmap: { x: category label, y: intensity value } per cell */ /** one row of a heatmap: { x: category label, y: intensity value } per cell */
'heatmap-data'?: { x: string; y: number }[] 'heatmap-data'?: { x: string; y: number }[]
/** one cell of a treemap: { x: label, y: value } — the value drives the cell size */
'treemap-data'?: { x: string; y: number }[]
/** one row of a rangeBar (timeline): { x: row label, y: [start, end] } */
'range-data'?: { x: string; y: number[] }[]
/** one box of a boxPlot: { x: category, y: [min, q1, median, q3, max] } */
'box-data'?: { x: string; y: number[] }[]
/** one point of a bubble chart — z is the bubble size */
'bubble-data'?: { x: number; y: number; z: number }[]
} }
export type ChartData = { export type ChartData = {
@@ -24,6 +34,7 @@ export type ChartData = {
'animations'?: boolean 'animations'?: boolean
'toolbar'?: boolean 'toolbar'?: boolean
'horizontal'?: boolean 'horizontal'?: boolean
'funnel'?: boolean
'legend'?: boolean 'legend'?: boolean
'title'?: string 'title'?: string
'color'?: string 'color'?: string
@@ -65,9 +76,61 @@ export type ChartData = {
'hide-points'?: boolean 'hide-points'?: boolean
'show-markers'?: boolean 'show-markers'?: boolean
'start-date'?: string 'start-date'?: string
/** horizontal threshold lines: a target, a limit, an SLA */
'annotations-y'?: Annotation[]
/** vertical marker lines: a release, a campaign start */
'annotations-x'?: Annotation[]
/** one entry per y axis — two of them put a second scale on the right */
'y-axes'?: YAxis[]
/** charts sharing a group name sync their tooltip and zoom */
'group'?: string
/** renders this chart as the range selector of the chart with that id */
'brush-target'?: string
'selection-from'?: number
'selection-to'?: number
[key: string]: unknown [key: string]: unknown
} }
type Annotation = {
value: number | string
label?: string
color?: string
dash?: number
}
type YAxis = {
'series-name'?: string
'title'?: string
'opposite'?: boolean
'min'?: number
'max'?: number
}
/** One threshold or marker line, styled the same on either axis. */
function annotationOptions(annotation: Annotation, axis: 'x' | 'y') {
const color = `var(--tblr-${annotation.color ?? 'red'})`
return {
[axis]: annotation.value,
strokeDashArray: annotation.dash ?? 4,
borderColor: color,
label: annotation.label
? {
text: annotation.label,
borderWidth: 0,
position: axis === 'y' ? 'left' : 'top',
textAnchor: axis === 'y' ? 'start' : 'middle',
offsetX: axis === 'y' ? 8 : 0,
style: { background: color, color: 'var(--tblr-white)', fontSize: '11px', fontFamily: 'inherit' },
}
: undefined,
}
}
/** The points of a serie: plain numbers, or the { x, y } shape a given chart type needs. */
function serieData(serie: Serie) {
return serie['candlestick-data'] ?? serie['heatmap-data'] ?? serie['treemap-data'] ?? serie['range-data'] ?? serie['box-data'] ?? serie['bubble-data'] ?? (serie.data as number[])
}
const escapeHtml = (value: string) => value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;') const escapeHtml = (value: string) => value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
/** Consecutive days from start-date (YYYY-MM-DD). */ /** Consecutive days from start-date (YYYY-MM-DD). */
@@ -87,13 +150,20 @@ export function chartStyle(opts: { id: string; data: ChartData }): string {
let css = '' let css = ''
for (const [i, serie] of series.entries()) { for (const [i, serie] of series.entries()) {
const color = serie.color ?? data.color ?? 'primary' const color = serie.color ?? data.color
// With no color named in the data, the serie takes the next slot of the public
// palette (--tblr-chart-1 … --tblr-chart-5), which an app can override in one place.
const base = color ? `var(--tblr-${color})` : `var(--tblr-chart-${(i % 5) + 1})`
const opacity = serie['color-opacity'] ?? '100%' const opacity = serie['color-opacity'] ?? '100%'
css += ` --chart-${id}-color-${i}: color-mix(in srgb, transparent, var(--tblr-${color}) ${opacity});\n` css += ` --chart-${id}-color-${i}: color-mix(in srgb, transparent, ${base} ${opacity});\n`
// An area is filled with a wash of its own line color.
if (type === 'area') css += ` --chart-${id}-fill-${i}: color-mix(in srgb, transparent, ${base} 16%);\n`
} }
if (type === 'area') { if (type === 'boxPlot') {
css += ` --chart-${id}-fill-0: color-mix(in srgb, transparent, var(--tblr-primary) 16%);\n` // The box is drawn in two halves — median..Q3 above, Q1..median below.
css += ` --chart-${id}-fill-1: color-mix(in srgb, transparent, var(--tblr-primary) 16%);\n` const color = series[0]?.color ?? data.color ?? 'primary'
css += ` --chart-${id}-box-upper: color-mix(in srgb, transparent, var(--tblr-${color}) 100%);\n`
css += ` --chart-${id}-box-lower: color-mix(in srgb, transparent, var(--tblr-${color}) 40%);\n`
} }
return `<style>\n :root {\n${css} }\n</style>` return `<style>\n :root {\n${css} }\n</style>`
} }
@@ -112,22 +182,41 @@ export function chartConfig(opts: { id: string; data: ChartData; height: number
const { id, data, height } = opts const { id, data, height } = opts
const type = data.type ?? 'bar' const type = data.type ?? 'bar'
const series = data.series ?? [] const series = data.series ?? []
const isRound = type === 'pie' || type === 'donut' || type === 'radialBar' const isRound = type === 'pie' || type === 'donut' || type === 'radialBar' || type === 'polarArea'
const config: Record<string, unknown> = {} const config: Record<string, unknown> = {}
config.chart = { config.chart = {
// Named so other charts can point at it — a brush needs its target's id, and
// `ApexCharts.exec(id, ...)` reaches the chart from anywhere on the page.
id,
type, type,
fontFamily: 'inherit', fontFamily: 'inherit',
height: height * 16, height: height * 16,
...(data.sparkline ? { sparkline: { enabled: true } } : { parentHeightOffset: 0, toolbar: { show: Boolean(data.toolbar) } }), ...(data.sparkline ? { sparkline: { enabled: true } } : { parentHeightOffset: 0, toolbar: { show: Boolean(data.toolbar) } }),
animations: data.animations ? undefined : { enabled: false }, animations: data.animations ? undefined : { enabled: false },
stacked: data.stacked ? true : undefined, stacked: data.stacked ? true : undefined,
group: data.group,
brush: data['brush-target'] ? { enabled: true, target: data['brush-target'] } : undefined,
selection: data['brush-target']
? {
enabled: true,
xaxis: { min: data['selection-from'], max: data['selection-to'] },
fill: { color: 'var(--tblr-primary)', opacity: 0.16 },
stroke: { color: 'var(--tblr-primary)', width: 1, opacity: 1, dashArray: 0 },
}
: undefined,
} }
if (type === 'bar') { if (type === 'bar' || type === 'rangeBar') {
config.plotOptions = { config.plotOptions = {
bar: data.horizontal ? { barHeight: '50%', horizontal: true } : { columnWidth: '50%' }, bar: data.funnel
? // A funnel is a horizontal bar with `isFunnel` on. Every stage is one point of a
// single serie, so its color comes from the per-point `colors` array in charts.json.
{ horizontal: true, barHeight: '80%', isFunnel: true, isFunnel3d: false, distributed: true }
: data.horizontal
? { barHeight: '50%', horizontal: true }
: { columnWidth: '50%' },
} }
} }
@@ -171,6 +260,49 @@ export function chartConfig(opts: { id: string; data: ChartData; height: number
config.dataLabels = { enabled: false } config.dataLabels = { enabled: false }
} }
if (type === 'treemap') {
// Same single-hue shading as the heatmap: a cell gets lighter as its value drops.
config.plotOptions = { treemap: { distributed: false, enableShades: true, shadeIntensity: 0.4 } }
config.dataLabels = { enabled: true }
}
if (type === 'radar') {
config.plotOptions = {
radar: {
polygons: {
// The rings are the one piece of chrome the --apx-grid token does not reach.
strokeColors: 'var(--apx-grid)',
connectorColors: 'var(--apx-grid)',
fill: { colors: ['transparent', 'transparent'] },
},
},
}
config.markers = { size: 3 }
}
if (type === 'polarArea') {
config.plotOptions = {
polarArea: {
rings: { strokeColor: 'var(--apx-grid)' },
spokes: { connectorColors: 'var(--apx-grid)' },
},
}
}
if (type === 'boxPlot') {
config.plotOptions = {
boxPlot: { colors: { upper: `var(--chart-${id}-box-upper)`, lower: `var(--chart-${id}-box-lower)` } },
}
// Whiskers and box outlines: the ApexCharts default (#24292e) vanishes on a dark
// background, while the border color is too faint to read on a light one.
config.stroke = { colors: ['var(--tblr-secondary)'] }
}
if (type === 'bubble') {
config.fill = { opacity: 0.8 }
config.dataLabels = { enabled: false }
}
if (data['fill-type']) { if (data['fill-type']) {
config.fill = { config.fill = {
type: data['fill-type'], type: data['fill-type'],
@@ -190,9 +322,11 @@ export function chartConfig(opts: { id: string; data: ChartData; height: number
} }
} else if (type === 'area') { } else if (type === 'area') {
config.fill = { config.fill = {
colors: [`var(--chart-${id}-fill-0)`, `var(--chart-${id}-fill-1)`], colors: series.map((_, i) => `var(--chart-${id}-fill-${i})`),
type: 'solid', type: 'solid',
} }
} else if (type === 'radar') {
config.fill = { opacity: 0.15 }
} }
if (data.title) { if (data.title) {
@@ -223,20 +357,31 @@ export function chartConfig(opts: { id: string; data: ChartData; height: number
} else { } else {
config.series = series.map((s) => ({ config.series = series.map((s) => ({
name: s.name ?? '', name: s.name ?? '',
data: s['candlestick-data'] ? s['candlestick-data'].map((c) => ({ x: c.x, y: c.y })) : s['heatmap-data'] ? s['heatmap-data'].map((c) => ({ x: c.x, y: c.y })) : (s.data as number[]), type: s.type,
data: serieData(s),
})) }))
} }
} }
config.tooltip = { theme: 'dark' } if (data['annotations-x'] || data['annotations-y']) {
config.annotations = {
xaxis: data['annotations-x']?.map((annotation) => annotationOptions(annotation, 'x')),
yaxis: data['annotations-y']?.map((annotation) => annotationOptions(annotation, 'y')),
}
}
// No `theme` here on purpose: the tooltip is styled in core/scss/vendor/_apexcharts.scss
// so it follows the color mode, while ApexCharts' own dark theme would paint its title
// row in a light color on top of that.
// The negative padding pulls a cartesian plot flush to the edges of its card. Radial
// and block layouts draw their labels outside the plot, so they keep the default room.
const looseLayout = type === 'radar' || type === 'polarArea' || type === 'treemap' || Boolean(data.funnel) || Boolean(data['y-axes'])
config.grid = { config.grid = {
padding: data.sparkline ? undefined : { top: -20, right: 0, left: -4, bottom: -4 }, padding: data.sparkline || looseLayout ? undefined : { top: -20, right: 0, left: -4, bottom: -4 },
show: data['hide-grid'] ? false : undefined, show: data['hide-grid'] ? false : undefined,
strokeDashArray: data['hide-grid'] ? undefined : 4, strokeDashArray: data['hide-grid'] ? undefined : 4,
// ApexCharts' own default (#e0e0e0) is a light-mode-only gray — near
// invisible, and wrong-toned, against a dark background.
borderColor: 'var(--tblr-border-color)',
xaxis: !data['hide-grid'] && data['show-x'] ? { lines: { show: true } } : undefined, xaxis: !data['hide-grid'] && data['show-x'] ? { lines: { show: true } } : undefined,
} }
@@ -245,11 +390,9 @@ export function chartConfig(opts: { id: string; data: ChartData; height: number
config.dataLabels = { enabled: true } config.dataLabels = { enabled: true }
} }
if (data.categories || data.datetime || type === 'heatmap') { if (data.categories || data.datetime || type === 'heatmap' || type === 'bubble' || type === 'boxPlot' || type === 'rangeBar') {
config.xaxis = { config.xaxis = {
// ApexCharts' own default label color (#373d3f) is a light-mode-only labels: { padding: 0 },
// dark gray — low-contrast (near-invisible) against a dark background.
labels: { padding: 0, style: { colors: 'var(--tblr-secondary)' } },
tooltip: { enabled: false }, tooltip: { enabled: false },
axisBorder: type === 'area' || type === 'bar' ? { show: false } : undefined, axisBorder: type === 'area' || type === 'bar' ? { show: false } : undefined,
categories: data.categories?.map(String), categories: data.categories?.map(String),
@@ -257,11 +400,27 @@ export function chartConfig(opts: { id: string; data: ChartData; height: number
} }
} }
if (!isRound) { if (type === 'radar') {
// The radar's value rings label themselves right across the plot — noise at card size.
config.yaxis = { show: false }
} else if (data['y-axes']) {
// Two scales in one plot: each axis binds to a serie by name, and the second one
// sits on the right. Without `seriesName` ApexCharts draws one axis per serie.
config.yaxis = data['y-axes'].map((axis) => ({
seriesName: axis['series-name'],
opposite: axis.opposite,
min: axis.min,
max: axis.max,
// Nudged left of the value labels, which ApexCharts lets the title overlap by a few
// pixels. Its default title color is the same light-mode-only gray as the labels.
title: axis.title ? { text: escapeHtml(axis.title), offsetX: axis.opposite ? 8 : -8, style: { fontSize: '11px', fontWeight: 500, fontFamily: 'inherit' } } : undefined,
labels: { padding: 4 },
}))
} else if (!isRound) {
config.yaxis = { config.yaxis = {
labels: { padding: 4, style: { colors: 'var(--tblr-secondary)' } }, labels: { padding: 4 },
max: data['y-max'], max: data['y-max'],
title: data['y-title'] ? { text: escapeHtml(data['y-title']) } : undefined, title: data['y-title'] ? { text: escapeHtml(data['y-title']), style: { fontSize: '11px', fontWeight: 500, fontFamily: 'inherit' } } : undefined,
tooltip: data['y-tooltip'] ? { enabled: true } : undefined, tooltip: data['y-tooltip'] ? { enabled: true } : undefined,
} }
} }
@@ -284,9 +443,6 @@ export function chartConfig(opts: { id: string; data: ChartData; height: number
offsetY: 12, offsetY: 12,
markers: { width: 10, height: 10, radius: 100 }, markers: { width: 10, height: 10, radius: 100 },
itemMargin: { horizontal: 8, vertical: 8 }, itemMargin: { horizontal: 8, vertical: 8 },
// Same fixed light-mode-only default (#373d3f) as the axis labels —
// near-invisible on a dark background.
labels: { colors: 'var(--tblr-secondary)' },
} }
: { show: false } : { show: false }