md-area-chart spec card
Identity · when to use / when NOT · decision cues · behavioural contract · do/don't · anti-patterns · full API. Paste into your agent when you're implementing with this component.
A trend with cumulative volume. A line chart with the region under the
curve filled — stacked by default, so composition over time is the natural
reading. Same family surface as md-line-chart; the
one meaningful difference is that stack defaults to normal here.
<md-area-chart label="Traffic sources" stack="normal"></md-area-chart>
<script type="module">
const series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38, 52, 48, 56, 62, 70, 80]
},
{
label: "Organic",
data: [18, 24, 32, 40, 48, 45, 60, 65, 72, 78, 84, 92]
},
{
label: "Paid",
data: [8, 12, 14, 22, 30, 28, 36, 42, 48, 52, 58, 64]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38, 52, 48, 56, 62, 70, 80]
},
{
label: "Organic",
data: [18, 24, 32, 40, 48, 45, 60, 65, 72, 78, 84, 92]
},
{
label: "Paid",
data: [8, 12, 14, 22, 30, 28, 36, 42, 48, 52, 58, 64]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
};
export function Chart() {
return (
<MdAreaChart
series={series}
xAxis={xAxis}
label="Traffic sources"
stack="normal"
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
[xAxis]="xAxis"
label="Traffic sources"
stack="normal"
></md-area-chart>
`,
})
export class ChartComponent {
series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38, 52, 48, 56, 62, 70, 80]
},
{
label: "Organic",
data: [18, 24, 32, 40, 48, 45, 60, 65, 72, 78, 84, 92]
},
{
label: "Paid",
data: [8, 12, 14, 22, 30, 28, 36, 42, 48, 52, 58, 64]
}
];
xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
};
}<script setup lang="ts">
const series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38, 52, 48, 56, 62, 70, 80]
},
{
label: "Organic",
data: [18, 24, 32, 40, 48, 45, 60, 65, 72, 78, 84, 92]
},
{
label: "Paid",
data: [8, 12, 14, 22, 30, 28, 36, 42, 48, 52, 58, 64]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
};
</script>
<template>
<md-area-chart
:series="series"
:xAxis="xAxis"
label="Traffic sources"
stack="normal"
/>
</template><script lang="ts">
const series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38, 52, 48, 56, 62, 70, 80]
},
{
label: "Organic",
data: [18, 24, 32, 40, 48, 45, 60, 65, 72, 78, 84, 92]
},
{
label: "Paid",
data: [8, 12, 14, 22, 30, 28, 36, 42, 48, 52, 58, 64]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
};
</script>
<md-area-chart
series={series}
xAxis={xAxis}
label="Traffic sources"
stack="normal"
/>
Already installed? See the
Installation guide for one-time package setup
(core + tokens, fonts). Each tab below shows two patterns for using
md-area-chart in your project: Option A registers every AWC UI
component at once (simplest), Option B imports only this component for
tree-shake-friendly bundles.
<!-- ─── Option A: global registration (all components) ─── -->
<script type="module">
import '@awc-ui/core/define';
</script>
<!-- ─── Option B: single import (tree-shake only md-area-chart) ─── -->
<script type="module">
import '@awc-ui/core/components/md-area-chart';
</script>
<md-area-chart></md-area-chart>// ─── Option A: typed React wrapper (registers all components) ───
// Importing from '@awc-ui/react' calls defineCustomElements() as a
// side effect, so every md-* element becomes available in the browser.
import { MdAreaChart } from '@awc-ui/react';
export function Example() {
return <MdAreaChart></MdAreaChart>;
}
// ─── Option B: single import (tree-shake to only md-area-chart) ───
// Skip the wrapper and use the raw custom element. Smallest bundle,
// but you lose typed props/events on JSX.
import '@awc-ui/core/components/md-area-chart';
export function ExampleTreeShaken() {
return <md-area-chart></md-area-chart>;
}// ─── Option A: schema module (any md-* element accepted) ───
// Pair with `defineCustomElements(window)` in main.ts.
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { AwcUiModule } from '@awc-ui/angular';
@Component({
standalone: true,
imports: [AwcUiModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `<md-area-chart></md-area-chart>`,
})
export class ExampleComponent {}
// ─── Option B: typed directive (tree-shake friendly) ───
// Pair with `import '@awc-ui/core/components/md-area-chart'` in main.ts.
import { Component } from '@angular/core';
import { MdAreaChart } from '@awc-ui/angular';
@Component({
standalone: true,
imports: [MdAreaChart],
template: `<md-area-chart></md-area-chart>`,
})
export class ExampleTreeShakenComponent {}<!-- ─── Option A: typed Vue wrapper (registers all components) ─── -->
<script setup lang="ts">
import { MdAreaChart } from '@awc-ui/vue';
</script>
<template>
<MdAreaChart></MdAreaChart>
</template>
<!-- ─── Option B: single import (tree-shake to only md-area-chart) ─── -->
<script setup lang="ts">
import '@awc-ui/core/components/md-area-chart';
</script>
<template>
<md-area-chart></md-area-chart>
</template><!-- ─── Option A: global registration (done once in main entry) ─── -->
<!-- main.ts: -->
<!-- import { defineCustomElements } from '@awc-ui/svelte'; -->
<!-- defineCustomElements(window); -->
<md-area-chart></md-area-chart>
<!-- ─── Option B: single import (tree-shake to only md-area-chart) ─── -->
<script lang="ts">
import '@awc-ui/core/components/md-area-chart';
</script>
<md-area-chart></md-area-chart>stack="normal", the default) or how shares move (stack="percentage").stack="none", used
sparingly), or a shape-of-the-data streamgraph (stack="silhouette" /
stack="wiggle").zoom="inside | slider | both" to focus a range,
connect-nulls to bridge gaps, show-marks to expose the points, curve to
pick the interpolation, and loading while the data is still arriving.| Situation | Use instead |
|---|---|
| Comparing the shape of several independent series | md-line-chart |
| Discrete categories | md-bar-chart |
| Parts of a single, non-time whole | md-pie-chart |
| An inline micro-trend | md-sparkline |
| Exact values users must read | md-table |
| Series that cross each other frequently | md-line-chart — fills would obscure |
| A share-of-total series that can go negative | md-line-chart — stack="percentage" pins the y axis to 0–100 |
stack | Visual | Best for |
|---|---|---|
none | Translucent overlapping areas | 2–3 series comparison |
normal | Additive stack (default) | Composition plus total |
percentage | Each x-tick sums to 100% | Share-of-total story |
silhouette | Centred streamgraph | Shape-of-the-data exploration |
wiggle | Minimum-slope streamgraph | Many series of similar magnitude |
Mixed signs are split, not flattened. Stacking keeps a separate cursor per
sign, so positives accumulate upward from zero and negatives downward from zero;
magnitudes are never summed into one misleading total. The exception is
stack="percentage", which pins the y axis to a hard 0…100 — see
Negative values.
<md-area-chart stack="none" fill-opacity="0.35"></md-area-chart>
<script type="module">
const series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38, 52]
},
{
label: "Organic",
data: [18, 24, 32, 40, 48, 45, 60]
}
];
const xAxis = {
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38, 52]
},
{
label: "Organic",
data: [18, 24, 32, 40, 48, 45, 60]
}
];
const xAxis = {
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
};
export function Chart() {
return (
<MdAreaChart
series={series}
xAxis={xAxis}
stack="none"
fill-opacity="0.35"
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
[xAxis]="xAxis"
stack="none"
fill-opacity="0.35"
></md-area-chart>
`,
})
export class ChartComponent {
series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38, 52]
},
{
label: "Organic",
data: [18, 24, 32, 40, 48, 45, 60]
}
];
xAxis = {
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
};
}<script setup lang="ts">
const series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38, 52]
},
{
label: "Organic",
data: [18, 24, 32, 40, 48, 45, 60]
}
];
const xAxis = {
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
};
</script>
<template>
<md-area-chart
:series="series"
:xAxis="xAxis"
stack="none"
fill-opacity="0.35"
/>
</template><script lang="ts">
const series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38, 52]
},
{
label: "Organic",
data: [18, 24, 32, 40, 48, 45, 60]
}
];
const xAxis = {
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
};
</script>
<md-area-chart
series={series}
xAxis={xAxis}
stack="none"
fill-opacity="0.35"
/><md-area-chart stack="percentage"></md-area-chart>
<script type="module">
const series = [
{
label: "Mobile",
data: [40, 45, 48, 55, 62, 64]
},
{
label: "Desktop",
data: [45, 40, 35, 30, 25, 20]
},
{
label: "Tablet",
data: [15, 15, 17, 15, 13, 16]
}
];
const xAxis = {
data: ["2020", "2021", "2022", "2023", "2024", "2025"]
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [
{
label: "Mobile",
data: [40, 45, 48, 55, 62, 64]
},
{
label: "Desktop",
data: [45, 40, 35, 30, 25, 20]
},
{
label: "Tablet",
data: [15, 15, 17, 15, 13, 16]
}
];
const xAxis = {
data: ["2020", "2021", "2022", "2023", "2024", "2025"]
};
export function Chart() {
return (
<MdAreaChart
series={series}
xAxis={xAxis}
stack="percentage"
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
[xAxis]="xAxis"
stack="percentage"
></md-area-chart>
`,
})
export class ChartComponent {
series = [
{
label: "Mobile",
data: [40, 45, 48, 55, 62, 64]
},
{
label: "Desktop",
data: [45, 40, 35, 30, 25, 20]
},
{
label: "Tablet",
data: [15, 15, 17, 15, 13, 16]
}
];
xAxis = {
data: ["2020", "2021", "2022", "2023", "2024", "2025"]
};
}<script setup lang="ts">
const series = [
{
label: "Mobile",
data: [40, 45, 48, 55, 62, 64]
},
{
label: "Desktop",
data: [45, 40, 35, 30, 25, 20]
},
{
label: "Tablet",
data: [15, 15, 17, 15, 13, 16]
}
];
const xAxis = {
data: ["2020", "2021", "2022", "2023", "2024", "2025"]
};
</script>
<template>
<md-area-chart
:series="series"
:xAxis="xAxis"
stack="percentage"
/>
</template><script lang="ts">
const series = [
{
label: "Mobile",
data: [40, 45, 48, 55, 62, 64]
},
{
label: "Desktop",
data: [45, 40, 35, 30, 25, 20]
},
{
label: "Tablet",
data: [15, 15, 17, 15, 13, 16]
}
];
const xAxis = {
data: ["2020", "2021", "2022", "2023", "2024", "2025"]
};
</script>
<md-area-chart
series={series}
xAxis={xAxis}
stack="percentage"
/><md-area-chart stack="silhouette" curve="smooth" legend="none"></md-area-chart>
<script type="module">
const series = [
{
label: "A",
data: [10, 22, 28, 35, 41, 38, 52, 48, 56, 62, 70, 80],
color: "primary"
},
{
label: "B",
data: [18, 24, 32, 40, 48, 45, 60, 65, 72, 78, 84, 92],
color: "tertiary"
},
{
label: "C",
data: [8, 12, 14, 22, 30, 28, 36, 42, 48, 52, 58, 64],
color: "secondary"
},
{
label: "D",
data: [4, 7, 8, 13, 19, 18, 24, 28, 32, 35, 38, 42],
color: "error"
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
hideTicks: true
};
const yAxis = {
hidden: true
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis, yAxis });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [
{
label: "A",
data: [10, 22, 28, 35, 41, 38, 52, 48, 56, 62, 70, 80],
color: "primary"
},
{
label: "B",
data: [18, 24, 32, 40, 48, 45, 60, 65, 72, 78, 84, 92],
color: "tertiary"
},
{
label: "C",
data: [8, 12, 14, 22, 30, 28, 36, 42, 48, 52, 58, 64],
color: "secondary"
},
{
label: "D",
data: [4, 7, 8, 13, 19, 18, 24, 28, 32, 35, 38, 42],
color: "error"
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
hideTicks: true
};
const yAxis = {
hidden: true
};
export function Chart() {
return (
<MdAreaChart
series={series}
xAxis={xAxis}
yAxis={yAxis}
stack="silhouette"
curve="smooth"
legend="none"
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
[xAxis]="xAxis"
[yAxis]="yAxis"
stack="silhouette"
curve="smooth"
legend="none"
></md-area-chart>
`,
})
export class ChartComponent {
series = [
{
label: "A",
data: [10, 22, 28, 35, 41, 38, 52, 48, 56, 62, 70, 80],
color: "primary"
},
{
label: "B",
data: [18, 24, 32, 40, 48, 45, 60, 65, 72, 78, 84, 92],
color: "tertiary"
},
{
label: "C",
data: [8, 12, 14, 22, 30, 28, 36, 42, 48, 52, 58, 64],
color: "secondary"
},
{
label: "D",
data: [4, 7, 8, 13, 19, 18, 24, 28, 32, 35, 38, 42],
color: "error"
}
];
xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
hideTicks: true
};
yAxis = {
hidden: true
};
}<script setup lang="ts">
const series = [
{
label: "A",
data: [10, 22, 28, 35, 41, 38, 52, 48, 56, 62, 70, 80],
color: "primary"
},
{
label: "B",
data: [18, 24, 32, 40, 48, 45, 60, 65, 72, 78, 84, 92],
color: "tertiary"
},
{
label: "C",
data: [8, 12, 14, 22, 30, 28, 36, 42, 48, 52, 58, 64],
color: "secondary"
},
{
label: "D",
data: [4, 7, 8, 13, 19, 18, 24, 28, 32, 35, 38, 42],
color: "error"
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
hideTicks: true
};
const yAxis = {
hidden: true
};
</script>
<template>
<md-area-chart
:series="series"
:xAxis="xAxis"
:yAxis="yAxis"
stack="silhouette"
curve="smooth"
legend="none"
/>
</template><script lang="ts">
const series = [
{
label: "A",
data: [10, 22, 28, 35, 41, 38, 52, 48, 56, 62, 70, 80],
color: "primary"
},
{
label: "B",
data: [18, 24, 32, 40, 48, 45, 60, 65, 72, 78, 84, 92],
color: "tertiary"
},
{
label: "C",
data: [8, 12, 14, 22, 30, 28, 36, 42, 48, 52, 58, 64],
color: "secondary"
},
{
label: "D",
data: [4, 7, 8, 13, 19, 18, 24, 28, 32, 35, 38, 42],
color: "error"
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
hideTicks: true
};
const yAxis = {
hidden: true
};
</script>
<md-area-chart
series={series}
xAxis={xAxis}
yAxis={yAxis}
stack="silhouette"
curve="smooth"
legend="none"
/>The silhouette and wiggle modes default stroke to off per series — a band
on a floating baseline has no meaningful top edge to trace.
curve decides how the band’s top edge is drawn between points. smooth reads
as a trend; linear is honest about where the measurements actually are.
<md-area-chart label="Smooth" stack="none" curve="smooth"></md-area-chart>
<script type="module">
const series = [
{
label: "Sessions",
data: [12, 34, 22, 48, 30, 56, 41, 64]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"]
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [
{
label: "Sessions",
data: [12, 34, 22, 48, 30, 56, 41, 64]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"]
};
export function Chart() {
return (
<MdAreaChart
series={series}
xAxis={xAxis}
label="Smooth"
stack="none"
curve="smooth"
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
[xAxis]="xAxis"
label="Smooth"
stack="none"
curve="smooth"
></md-area-chart>
`,
})
export class ChartComponent {
series = [
{
label: "Sessions",
data: [12, 34, 22, 48, 30, 56, 41, 64]
}
];
xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"]
};
}<script setup lang="ts">
const series = [
{
label: "Sessions",
data: [12, 34, 22, 48, 30, 56, 41, 64]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"]
};
</script>
<template>
<md-area-chart
:series="series"
:xAxis="xAxis"
label="Smooth"
stack="none"
curve="smooth"
/>
</template><script lang="ts">
const series = [
{
label: "Sessions",
data: [12, 34, 22, 48, 30, 56, 41, 64]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"]
};
</script>
<md-area-chart
series={series}
xAxis={xAxis}
label="Smooth"
stack="none"
curve="smooth"
/><md-area-chart label="Linear" stack="none" curve="linear"></md-area-chart>
<script type="module">
const series = [
{
label: "Sessions",
data: [12, 34, 22, 48, 30, 56, 41, 64]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"]
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [
{
label: "Sessions",
data: [12, 34, 22, 48, 30, 56, 41, 64]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"]
};
export function Chart() {
return (
<MdAreaChart
series={series}
xAxis={xAxis}
label="Linear"
stack="none"
curve="linear"
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
[xAxis]="xAxis"
label="Linear"
stack="none"
curve="linear"
></md-area-chart>
`,
})
export class ChartComponent {
series = [
{
label: "Sessions",
data: [12, 34, 22, 48, 30, 56, 41, 64]
}
];
xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"]
};
}<script setup lang="ts">
const series = [
{
label: "Sessions",
data: [12, 34, 22, 48, 30, 56, 41, 64]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"]
};
</script>
<template>
<md-area-chart
:series="series"
:xAxis="xAxis"
label="Linear"
stack="none"
curve="linear"
/>
</template><script lang="ts">
const series = [
{
label: "Sessions",
data: [12, 34, 22, 48, 30, 56, 41, 64]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"]
};
</script>
<md-area-chart
series={series}
xAxis={xAxis}
label="Linear"
stack="none"
curve="linear"
/>show-labels prints each point’s value; series-labels puts the series name at
its last point, so the name follows the end of the band instead of sitting in a
legend the reader has to look back at.
<md-area-chart label="Rainfall (mm)" stack="none" curve="smooth" show-labels></md-area-chart>
<script type="module">
const series = [
{
label: "Bergen",
color: "primary",
data: [190, 152, 170, 114, 106, 132]
},
{
label: "Tromsø",
color: "tertiary",
data: [95, 87, 72, 64, 48, 59]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [
{
label: "Bergen",
color: "primary",
data: [190, 152, 170, 114, 106, 132]
},
{
label: "Tromsø",
color: "tertiary",
data: [95, 87, 72, 64, 48, 59]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
export function Chart() {
return (
<MdAreaChart
series={series}
xAxis={xAxis}
label="Rainfall (mm)"
stack="none"
curve="smooth"
show-labels
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
[xAxis]="xAxis"
label="Rainfall (mm)"
stack="none"
curve="smooth"
show-labels
></md-area-chart>
`,
})
export class ChartComponent {
series = [
{
label: "Bergen",
color: "primary",
data: [190, 152, 170, 114, 106, 132]
},
{
label: "Tromsø",
color: "tertiary",
data: [95, 87, 72, 64, 48, 59]
}
];
xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
}<script setup lang="ts">
const series = [
{
label: "Bergen",
color: "primary",
data: [190, 152, 170, 114, 106, 132]
},
{
label: "Tromsø",
color: "tertiary",
data: [95, 87, 72, 64, 48, 59]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
</script>
<template>
<md-area-chart
:series="series"
:xAxis="xAxis"
label="Rainfall (mm)"
stack="none"
curve="smooth"
show-labels
/>
</template><script lang="ts">
const series = [
{
label: "Bergen",
color: "primary",
data: [190, 152, 170, 114, 106, 132]
},
{
label: "Tromsø",
color: "tertiary",
data: [95, 87, 72, 64, 48, 59]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
</script>
<md-area-chart
series={series}
xAxis={xAxis}
label="Rainfall (mm)"
stack="none"
curve="smooth"
show-labels
/><md-area-chart label="Market share" stack="normal" curve="linear" series-labels></md-area-chart>
<script type="module">
const series = [
{
label: "Alpha",
data: [30, 34, 38, 45, 52, 61]
},
{
label: "Beta",
data: [26, 28, 30, 31, 33, 36]
},
{
label: "Gamma",
data: [14, 16, 17, 19, 20, 22]
}
];
const xAxis = {
data: ["2019", "2020", "2021", "2022", "2023", "2024"]
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [
{
label: "Alpha",
data: [30, 34, 38, 45, 52, 61]
},
{
label: "Beta",
data: [26, 28, 30, 31, 33, 36]
},
{
label: "Gamma",
data: [14, 16, 17, 19, 20, 22]
}
];
const xAxis = {
data: ["2019", "2020", "2021", "2022", "2023", "2024"]
};
export function Chart() {
return (
<MdAreaChart
series={series}
xAxis={xAxis}
label="Market share"
stack="normal"
curve="linear"
series-labels
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
[xAxis]="xAxis"
label="Market share"
stack="normal"
curve="linear"
series-labels
></md-area-chart>
`,
})
export class ChartComponent {
series = [
{
label: "Alpha",
data: [30, 34, 38, 45, 52, 61]
},
{
label: "Beta",
data: [26, 28, 30, 31, 33, 36]
},
{
label: "Gamma",
data: [14, 16, 17, 19, 20, 22]
}
];
xAxis = {
data: ["2019", "2020", "2021", "2022", "2023", "2024"]
};
}<script setup lang="ts">
const series = [
{
label: "Alpha",
data: [30, 34, 38, 45, 52, 61]
},
{
label: "Beta",
data: [26, 28, 30, 31, 33, 36]
},
{
label: "Gamma",
data: [14, 16, 17, 19, 20, 22]
}
];
const xAxis = {
data: ["2019", "2020", "2021", "2022", "2023", "2024"]
};
</script>
<template>
<md-area-chart
:series="series"
:xAxis="xAxis"
label="Market share"
stack="normal"
curve="linear"
series-labels
/>
</template><script lang="ts">
const series = [
{
label: "Alpha",
data: [30, 34, 38, 45, 52, 61]
},
{
label: "Beta",
data: [26, 28, 30, 31, 33, 36]
},
{
label: "Gamma",
data: [14, 16, 17, 19, 20, 22]
}
];
const xAxis = {
data: ["2019", "2020", "2021", "2022", "2023", "2024"]
};
</script>
<md-area-chart
series={series}
xAxis={xAxis}
label="Market share"
stack="normal"
curve="linear"
series-labels
/>color on a series takes an MD3 role name or any CSS colour. Prefer the
role: it follows the theme and stays legible in dark mode, where a hardcoded
hex will not.
<md-area-chart label="Themed roles" stack="none"></md-area-chart>
<script type="module">
const series = [
{
label: "primary",
color: "primary",
data: [10, 22, 28, 35, 41, 38]
},
{
label: "secondary",
color: "secondary",
data: [18, 24, 32, 40, 48, 45]
},
{
label: "tertiary",
color: "tertiary",
data: [8, 12, 14, 22, 30, 28]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [
{
label: "primary",
color: "primary",
data: [10, 22, 28, 35, 41, 38]
},
{
label: "secondary",
color: "secondary",
data: [18, 24, 32, 40, 48, 45]
},
{
label: "tertiary",
color: "tertiary",
data: [8, 12, 14, 22, 30, 28]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
export function Chart() {
return (
<MdAreaChart
series={series}
xAxis={xAxis}
label="Themed roles"
stack="none"
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
[xAxis]="xAxis"
label="Themed roles"
stack="none"
></md-area-chart>
`,
})
export class ChartComponent {
series = [
{
label: "primary",
color: "primary",
data: [10, 22, 28, 35, 41, 38]
},
{
label: "secondary",
color: "secondary",
data: [18, 24, 32, 40, 48, 45]
},
{
label: "tertiary",
color: "tertiary",
data: [8, 12, 14, 22, 30, 28]
}
];
xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
}<script setup lang="ts">
const series = [
{
label: "primary",
color: "primary",
data: [10, 22, 28, 35, 41, 38]
},
{
label: "secondary",
color: "secondary",
data: [18, 24, 32, 40, 48, 45]
},
{
label: "tertiary",
color: "tertiary",
data: [8, 12, 14, 22, 30, 28]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
</script>
<template>
<md-area-chart
:series="series"
:xAxis="xAxis"
label="Themed roles"
stack="none"
/>
</template><script lang="ts">
const series = [
{
label: "primary",
color: "primary",
data: [10, 22, 28, 35, 41, 38]
},
{
label: "secondary",
color: "secondary",
data: [18, 24, 32, 40, 48, 45]
},
{
label: "tertiary",
color: "tertiary",
data: [8, 12, 14, 22, 30, 28]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
</script>
<md-area-chart
series={series}
xAxis={xAxis}
label="Themed roles"
stack="none"
/><md-area-chart label="Explicit colours" stack="none"></md-area-chart>
<script type="module">
const series = [
{
label: "rgb",
color: "rgb(255, 87, 34)",
data: [10, 22, 28, 35, 41, 38]
},
{
label: "rgba",
color: "rgba(0, 150, 136, 0.9)",
data: [18, 24, 32, 40, 48, 45]
},
{
label: "named",
color: "goldenrod",
data: [8, 12, 14, 22, 30, 28]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [
{
label: "rgb",
color: "rgb(255, 87, 34)",
data: [10, 22, 28, 35, 41, 38]
},
{
label: "rgba",
color: "rgba(0, 150, 136, 0.9)",
data: [18, 24, 32, 40, 48, 45]
},
{
label: "named",
color: "goldenrod",
data: [8, 12, 14, 22, 30, 28]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
export function Chart() {
return (
<MdAreaChart
series={series}
xAxis={xAxis}
label="Explicit colours"
stack="none"
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
[xAxis]="xAxis"
label="Explicit colours"
stack="none"
></md-area-chart>
`,
})
export class ChartComponent {
series = [
{
label: "rgb",
color: "rgb(255, 87, 34)",
data: [10, 22, 28, 35, 41, 38]
},
{
label: "rgba",
color: "rgba(0, 150, 136, 0.9)",
data: [18, 24, 32, 40, 48, 45]
},
{
label: "named",
color: "goldenrod",
data: [8, 12, 14, 22, 30, 28]
}
];
xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
}<script setup lang="ts">
const series = [
{
label: "rgb",
color: "rgb(255, 87, 34)",
data: [10, 22, 28, 35, 41, 38]
},
{
label: "rgba",
color: "rgba(0, 150, 136, 0.9)",
data: [18, 24, 32, 40, 48, 45]
},
{
label: "named",
color: "goldenrod",
data: [8, 12, 14, 22, 30, 28]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
</script>
<template>
<md-area-chart
:series="series"
:xAxis="xAxis"
label="Explicit colours"
stack="none"
/>
</template><script lang="ts">
const series = [
{
label: "rgb",
color: "rgb(255, 87, 34)",
data: [10, 22, 28, 35, 41, 38]
},
{
label: "rgba",
color: "rgba(0, 150, 136, 0.9)",
data: [18, 24, 32, 40, 48, 45]
},
{
label: "named",
color: "goldenrod",
data: [8, 12, 14, 22, 30, 28]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
</script>
<md-area-chart
series={series}
xAxis={xAxis}
label="Explicit colours"
stack="none"
/>When one series dwarfs the rest, breaks: 'auto' on the axis cuts the empty
stretch so the small values stay readable instead of flattening against the
baseline.
<md-area-chart label="Monthly active users" stack="none" curve="smooth"></md-area-chart>
<script type="module">
const series = [
{
label: "MAU (k)",
color: "primary",
data: [8, 12, 18, 25, 1150, 1320, 1280, 1400]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"]
};
const yAxis = {
label: "MAU (k)",
breaks: "auto"
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis, yAxis });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [
{
label: "MAU (k)",
color: "primary",
data: [8, 12, 18, 25, 1150, 1320, 1280, 1400]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"]
};
const yAxis = {
label: "MAU (k)",
breaks: "auto"
};
export function Chart() {
return (
<MdAreaChart
series={series}
xAxis={xAxis}
yAxis={yAxis}
label="Monthly active users"
stack="none"
curve="smooth"
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
[xAxis]="xAxis"
[yAxis]="yAxis"
label="Monthly active users"
stack="none"
curve="smooth"
></md-area-chart>
`,
})
export class ChartComponent {
series = [
{
label: "MAU (k)",
color: "primary",
data: [8, 12, 18, 25, 1150, 1320, 1280, 1400]
}
];
xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"]
};
yAxis = {
label: "MAU (k)",
breaks: "auto"
};
}<script setup lang="ts">
const series = [
{
label: "MAU (k)",
color: "primary",
data: [8, 12, 18, 25, 1150, 1320, 1280, 1400]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"]
};
const yAxis = {
label: "MAU (k)",
breaks: "auto"
};
</script>
<template>
<md-area-chart
:series="series"
:xAxis="xAxis"
:yAxis="yAxis"
label="Monthly active users"
stack="none"
curve="smooth"
/>
</template><script lang="ts">
const series = [
{
label: "MAU (k)",
color: "primary",
data: [8, 12, 18, 25, 1150, 1320, 1280, 1400]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"]
};
const yAxis = {
label: "MAU (k)",
breaks: "auto"
};
</script>
<md-area-chart
series={series}
xAxis={xAxis}
yAxis={yAxis}
label="Monthly active users"
stack="none"
curve="smooth"
/>A series can carry a range of [low, high] pairs instead of data, filling
the band between them rather than down to the baseline. It answers a different
question than a line does: not how high a value was, but how wide it was.
range is its own field, not a shape data accepts, because [low, high] and
the existing [x, y] point tuple are indistinguishable at runtime — a series
says which it means by which field it fills in. Either form works per row:
const series = [ { label: 'Daily spread', range: [[-4.8, 1], [0, 6], null] }, // tuples { label: 'Daily spread', range: [{ low: -4.8, high: 1 }] }, // objects];A range never joins the stack (a band has no single value to add to a column),
takes the axis domain from both edges, and draws no stroke. Its tooltip
reads low – high and the screen-reader table gives both edges in one cell. A
null row — or a null edge — is a gap that breaks the band.
<md-area-chart curve="linear" legend="none"></md-area-chart>
<script type="module">
const series = [
{
label: "Temperature",
color: "secondary",
range: [
[-4, 2],
[-2, 5],
[1, 9],
[3, 12],
[2, 10],
[-1, 6],
[-3, 3]
]
}
];
const xAxis = {
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [
{
label: "Temperature",
color: "secondary",
range: [
[-4, 2],
[-2, 5],
[1, 9],
[3, 12],
[2, 10],
[-1, 6],
[-3, 3]
]
}
];
const xAxis = {
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
};
export function Chart() {
return (
<MdAreaChart
series={series}
xAxis={xAxis}
curve="linear"
legend="none"
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
[xAxis]="xAxis"
curve="linear"
legend="none"
></md-area-chart>
`,
})
export class ChartComponent {
series = [
{
label: "Temperature",
color: "secondary",
range: [
[-4, 2],
[-2, 5],
[1, 9],
[3, 12],
[2, 10],
[-1, 6],
[-3, 3]
]
}
];
xAxis = {
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
};
}<script setup lang="ts">
const series = [
{
label: "Temperature",
color: "secondary",
range: [
[-4, 2],
[-2, 5],
[1, 9],
[3, 12],
[2, 10],
[-1, 6],
[-3, 3]
]
}
];
const xAxis = {
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
};
</script>
<template>
<md-area-chart
:series="series"
:xAxis="xAxis"
curve="linear"
legend="none"
/>
</template><script lang="ts">
const series = [
{
label: "Temperature",
color: "secondary",
range: [
[-4, 2],
[-2, 5],
[1, 9],
[3, 12],
[2, 10],
[-1, 6],
[-3, 3]
]
}
];
const xAxis = {
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
};
</script>
<md-area-chart
series={series}
xAxis={xAxis}
curve="linear"
legend="none"
/>Layer a plain series on top and give it fill: false. Without that an area
chart fills every series down to the axis, and the line’s own fill buries
the band it is meant to sit inside. stroke: false is the mirror image — fill
only, no line.
const series = [ { label: 'Range', color: 'secondary', range: spread }, { label: 'Mean', color: 'primary', data: means, fill: false, showMarks: true },];Give the two different colour roles. Sharing one leaves the legend showing two identical swatches, and the line has to fight the band for contrast.
<md-area-chart curve="linear" legend="bottom" fill-opacity="0.45"></md-area-chart>
<script type="module">
const series = [
{
label: "Daily range",
color: "secondary",
range: [
[-6.1, 1.2],
[-4.8, 3.4],
[-1.2, 8.9],
[2.4, 14.6],
[6.8, 19.2],
[10.4, 23.8],
[12.9, 26.1],
[11.8, 24.7],
[7.2, 19.4],
[2.6, 13.1],
[-1.9, 6.3],
[-5.2, 2.1]
]
},
{
label: "Daily mean",
color: "primary",
data: [-2.4, -0.7, 3.8, 8.5, 13, 17.1, 19.5, 18.2, 13.3, 7.8, 2.2, -1.6],
fill: false,
showMarks: true
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
};
const yAxis = {
label: "°C"
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis, yAxis });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [
{
label: "Daily range",
color: "secondary",
range: [
[-6.1, 1.2],
[-4.8, 3.4],
[-1.2, 8.9],
[2.4, 14.6],
[6.8, 19.2],
[10.4, 23.8],
[12.9, 26.1],
[11.8, 24.7],
[7.2, 19.4],
[2.6, 13.1],
[-1.9, 6.3],
[-5.2, 2.1]
]
},
{
label: "Daily mean",
color: "primary",
data: [-2.4, -0.7, 3.8, 8.5, 13, 17.1, 19.5, 18.2, 13.3, 7.8, 2.2, -1.6],
fill: false,
showMarks: true
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
};
const yAxis = {
label: "°C"
};
export function Chart() {
return (
<MdAreaChart
series={series}
xAxis={xAxis}
yAxis={yAxis}
curve="linear"
legend="bottom"
fill-opacity="0.45"
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
[xAxis]="xAxis"
[yAxis]="yAxis"
curve="linear"
legend="bottom"
fill-opacity="0.45"
></md-area-chart>
`,
})
export class ChartComponent {
series = [
{
label: "Daily range",
color: "secondary",
range: [
[-6.1, 1.2],
[-4.8, 3.4],
[-1.2, 8.9],
[2.4, 14.6],
[6.8, 19.2],
[10.4, 23.8],
[12.9, 26.1],
[11.8, 24.7],
[7.2, 19.4],
[2.6, 13.1],
[-1.9, 6.3],
[-5.2, 2.1]
]
},
{
label: "Daily mean",
color: "primary",
data: [-2.4, -0.7, 3.8, 8.5, 13, 17.1, 19.5, 18.2, 13.3, 7.8, 2.2, -1.6],
fill: false,
showMarks: true
}
];
xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
};
yAxis = {
label: "°C"
};
}<script setup lang="ts">
const series = [
{
label: "Daily range",
color: "secondary",
range: [
[-6.1, 1.2],
[-4.8, 3.4],
[-1.2, 8.9],
[2.4, 14.6],
[6.8, 19.2],
[10.4, 23.8],
[12.9, 26.1],
[11.8, 24.7],
[7.2, 19.4],
[2.6, 13.1],
[-1.9, 6.3],
[-5.2, 2.1]
]
},
{
label: "Daily mean",
color: "primary",
data: [-2.4, -0.7, 3.8, 8.5, 13, 17.1, 19.5, 18.2, 13.3, 7.8, 2.2, -1.6],
fill: false,
showMarks: true
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
};
const yAxis = {
label: "°C"
};
</script>
<template>
<md-area-chart
:series="series"
:xAxis="xAxis"
:yAxis="yAxis"
curve="linear"
legend="bottom"
fill-opacity="0.45"
/>
</template><script lang="ts">
const series = [
{
label: "Daily range",
color: "secondary",
range: [
[-6.1, 1.2],
[-4.8, 3.4],
[-1.2, 8.9],
[2.4, 14.6],
[6.8, 19.2],
[10.4, 23.8],
[12.9, 26.1],
[11.8, 24.7],
[7.2, 19.4],
[2.6, 13.1],
[-1.9, 6.3],
[-5.2, 2.1]
]
},
{
label: "Daily mean",
color: "primary",
data: [-2.4, -0.7, 3.8, 8.5, 13, 17.1, 19.5, 18.2, 13.3, 7.8, 2.2, -1.6],
fill: false,
showMarks: true
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
};
const yAxis = {
label: "°C"
};
</script>
<md-area-chart
series={series}
xAxis={xAxis}
yAxis={yAxis}
curve="linear"
legend="bottom"
fill-opacity="0.45"
/>A fan chart is the range series used three times over: nested bands at 1σ, 2σ
and 3σ around a forecast, so the uncertainty is drawn as width. Everything left
of the split is measured and the bands are null there; from the last actual
onward they open outward.
Three details make it read correctly:
[m, m] pair at
the last actual, so the fan opens from the present rather than appearing
with a step.An xAxis band shades the forecast half, labelling where measurement stops and
projection starts.
<md-area-chart label="Predicted net income" curve="linear" legend="none" grid="horizontal" fill-opacity="1" title-align="center"></md-area-chart>
<script type="module">
const xAxis = {
data: ["Q1 2024", "Q2 2024", "Q3 2024", "Q4 2024", "Q1 2025", "Q2 2025", "Q3 2025", "Q4 2025", "Q1 2026", "Q2 2026", "Q3 2026", "Q4 2026", "Q1 2027", "Q2 2027", "Q3 2027", "Q4 2027"],
bands: [
{
from: 7,
to: 15,
color: "color-mix(in srgb, var(--md-sys-color-error) 6%, transparent)",
label: "Forecast"
}
]
};
const yAxis = {
min: 0
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { xAxis, yAxis });
const ACTUAL = [18.7, 19.1, 20.4, 21.6, 19.8, 19.4, 19.9, 21.7];
const MEAN = [21.6, 24.9, 23.4, 24.2, 24.5, 26.1, 27.2, 27.6];
// Random-walk uncertainty: sigma grows with the SQUARE ROOT of the horizon.
const SIGMA = MEAN.map((_, i) => 0.9 * Math.sqrt(i + 1));
const anchor = ACTUAL[ACTUAL.length - 1];
const band = (k) => [
...ACTUAL.slice(0, -1).map(() => null), // measured half: no band
[anchor, anchor], // zero width at the split
...MEAN.map((m, i) => [m - k * SIGMA[i], m + k * SIGMA[i]]),
];
// Normalise the theme's error role to #rrggbb via a throwaway 2D context,
// then tint by alpha — nested bands need distinguishable swatches.
const probe = document.createElement('canvas').getContext('2d');
probe.fillStyle = getComputedStyle(document.documentElement)
.getPropertyValue('--md-sys-color-error').trim() || '#B3261E';
const hex = probe.fillStyle;
const n = parseInt(hex.slice(1), 16);
const tint = (a) => 'rgba(' + ((n >> 16) & 255) + ',' + ((n >> 8) & 255) + ',' + (n & 255) + ',' + a + ')';
el.valueFormatter = (v) => (v == null ? '—' : v.toFixed(1) + 'M');
el.series = [
{ label: '3σ confidence', color: tint(0.18), range: band(3) }, // widest first
{ label: '2σ confidence', color: tint(0.3), range: band(2) },
{ label: '1σ confidence', color: tint(0.45), range: band(1) },
{ label: 'Net income', color: 'primary', data: [...ACTUAL, ...MEAN], fill: false },
];
</script>import { useEffect, useRef } from 'react';
import { MdAreaChart } from '@awc-ui/react';
const xAxis = {
data: ["Q1 2024", "Q2 2024", "Q3 2024", "Q4 2024", "Q1 2025", "Q2 2025", "Q3 2025", "Q4 2025", "Q1 2026", "Q2 2026", "Q3 2026", "Q4 2026", "Q1 2027", "Q2 2027", "Q3 2027", "Q4 2027"],
bands: [
{
from: 7,
to: 15,
color: "color-mix(in srgb, var(--md-sys-color-error) 6%, transparent)",
label: "Forecast"
}
]
};
const yAxis = {
min: 0
};
export function Chart() {
const ref = useRef<HTMLMdAreaChartElement>(null);
// Imperative setup runs once the element exists.
useEffect(() => {
const el = ref.current;
if (!el) return;
const ACTUAL = [18.7, 19.1, 20.4, 21.6, 19.8, 19.4, 19.9, 21.7];
const MEAN = [21.6, 24.9, 23.4, 24.2, 24.5, 26.1, 27.2, 27.6];
// Random-walk uncertainty: sigma grows with the SQUARE ROOT of the horizon.
const SIGMA = MEAN.map((_, i) => 0.9 * Math.sqrt(i + 1));
const anchor = ACTUAL[ACTUAL.length - 1];
const band = (k) => [
...ACTUAL.slice(0, -1).map(() => null), // measured half: no band
[anchor, anchor], // zero width at the split
...MEAN.map((m, i) => [m - k * SIGMA[i], m + k * SIGMA[i]]),
];
// Normalise the theme's error role to #rrggbb via a throwaway 2D context,
// then tint by alpha — nested bands need distinguishable swatches.
const probe = document.createElement('canvas').getContext('2d');
probe.fillStyle = getComputedStyle(document.documentElement)
.getPropertyValue('--md-sys-color-error').trim() || '#B3261E';
const hex = probe.fillStyle;
const n = parseInt(hex.slice(1), 16);
const tint = (a) => 'rgba(' + ((n >> 16) & 255) + ',' + ((n >> 8) & 255) + ',' + (n & 255) + ',' + a + ')';
el.valueFormatter = (v) => (v == null ? '—' : v.toFixed(1) + 'M');
el.series = [
{ label: '3σ confidence', color: tint(0.18), range: band(3) }, // widest first
{ label: '2σ confidence', color: tint(0.3), range: band(2) },
{ label: '1σ confidence', color: tint(0.45), range: band(1) },
{ label: 'Net income', color: 'primary', data: [...ACTUAL, ...MEAN], fill: false },
];
}, []);
return (
<MdAreaChart
ref={ref}
xAxis={xAxis}
yAxis={yAxis}
label="Predicted net income"
curve="linear"
legend="none"
grid="horizontal"
fill-opacity="1"
title-align="center"
/>
);
}import { Component, AfterViewInit, ElementRef, ViewChild } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart #chart
[xAxis]="xAxis"
[yAxis]="yAxis"
label="Predicted net income"
curve="linear"
legend="none"
grid="horizontal"
fill-opacity="1"
title-align="center"
></md-area-chart>
`,
})
export class ChartComponent implements AfterViewInit {
xAxis = {
data: ["Q1 2024", "Q2 2024", "Q3 2024", "Q4 2024", "Q1 2025", "Q2 2025", "Q3 2025", "Q4 2025", "Q1 2026", "Q2 2026", "Q3 2026", "Q4 2026", "Q1 2027", "Q2 2027", "Q3 2027", "Q4 2027"],
bands: [
{
from: 7,
to: 15,
color: "color-mix(in srgb, var(--md-sys-color-error) 6%, transparent)",
label: "Forecast"
}
]
};
yAxis = {
min: 0
};
@ViewChild('chart') chartRef!: ElementRef<HTMLMdAreaChartElement>;
// The view has to exist before the element can be set up.
ngAfterViewInit() {
const el = this.chartRef.nativeElement;
const ACTUAL = [18.7, 19.1, 20.4, 21.6, 19.8, 19.4, 19.9, 21.7];
const MEAN = [21.6, 24.9, 23.4, 24.2, 24.5, 26.1, 27.2, 27.6];
// Random-walk uncertainty: sigma grows with the SQUARE ROOT of the horizon.
const SIGMA = MEAN.map((_, i) => 0.9 * Math.sqrt(i + 1));
const anchor = ACTUAL[ACTUAL.length - 1];
const band = (k) => [
...ACTUAL.slice(0, -1).map(() => null), // measured half: no band
[anchor, anchor], // zero width at the split
...MEAN.map((m, i) => [m - k * SIGMA[i], m + k * SIGMA[i]]),
];
// Normalise the theme's error role to #rrggbb via a throwaway 2D context,
// then tint by alpha — nested bands need distinguishable swatches.
const probe = document.createElement('canvas').getContext('2d');
probe.fillStyle = getComputedStyle(document.documentElement)
.getPropertyValue('--md-sys-color-error').trim() || '#B3261E';
const hex = probe.fillStyle;
const n = parseInt(hex.slice(1), 16);
const tint = (a) => 'rgba(' + ((n >> 16) & 255) + ',' + ((n >> 8) & 255) + ',' + (n & 255) + ',' + a + ')';
el.valueFormatter = (v) => (v == null ? '—' : v.toFixed(1) + 'M');
el.series = [
{ label: '3σ confidence', color: tint(0.18), range: band(3) }, // widest first
{ label: '2σ confidence', color: tint(0.3), range: band(2) },
{ label: '1σ confidence', color: tint(0.45), range: band(1) },
{ label: 'Net income', color: 'primary', data: [...ACTUAL, ...MEAN], fill: false },
];
}
}<script setup lang="ts">
import { onMounted, ref } from 'vue';
const chart = ref(null);
const xAxis = {
data: ["Q1 2024", "Q2 2024", "Q3 2024", "Q4 2024", "Q1 2025", "Q2 2025", "Q3 2025", "Q4 2025", "Q1 2026", "Q2 2026", "Q3 2026", "Q4 2026", "Q1 2027", "Q2 2027", "Q3 2027", "Q4 2027"],
bands: [
{
from: 7,
to: 15,
color: "color-mix(in srgb, var(--md-sys-color-error) 6%, transparent)",
label: "Forecast"
}
]
};
const yAxis = {
min: 0
};
onMounted(() => {
const el = chart.value;
const ACTUAL = [18.7, 19.1, 20.4, 21.6, 19.8, 19.4, 19.9, 21.7];
const MEAN = [21.6, 24.9, 23.4, 24.2, 24.5, 26.1, 27.2, 27.6];
// Random-walk uncertainty: sigma grows with the SQUARE ROOT of the horizon.
const SIGMA = MEAN.map((_, i) => 0.9 * Math.sqrt(i + 1));
const anchor = ACTUAL[ACTUAL.length - 1];
const band = (k) => [
...ACTUAL.slice(0, -1).map(() => null), // measured half: no band
[anchor, anchor], // zero width at the split
...MEAN.map((m, i) => [m - k * SIGMA[i], m + k * SIGMA[i]]),
];
// Normalise the theme's error role to #rrggbb via a throwaway 2D context,
// then tint by alpha — nested bands need distinguishable swatches.
const probe = document.createElement('canvas').getContext('2d');
probe.fillStyle = getComputedStyle(document.documentElement)
.getPropertyValue('--md-sys-color-error').trim() || '#B3261E';
const hex = probe.fillStyle;
const n = parseInt(hex.slice(1), 16);
const tint = (a) => 'rgba(' + ((n >> 16) & 255) + ',' + ((n >> 8) & 255) + ',' + (n & 255) + ',' + a + ')';
el.valueFormatter = (v) => (v == null ? '—' : v.toFixed(1) + 'M');
el.series = [
{ label: '3σ confidence', color: tint(0.18), range: band(3) }, // widest first
{ label: '2σ confidence', color: tint(0.3), range: band(2) },
{ label: '1σ confidence', color: tint(0.45), range: band(1) },
{ label: 'Net income', color: 'primary', data: [...ACTUAL, ...MEAN], fill: false },
];
});
</script>
<template>
<md-area-chart
ref="chart"
:xAxis="xAxis"
:yAxis="yAxis"
label="Predicted net income"
curve="linear"
legend="none"
grid="horizontal"
fill-opacity="1"
title-align="center"
/>
</template><script lang="ts">
import { onMount } from 'svelte';
let chart;
const xAxis = {
data: ["Q1 2024", "Q2 2024", "Q3 2024", "Q4 2024", "Q1 2025", "Q2 2025", "Q3 2025", "Q4 2025", "Q1 2026", "Q2 2026", "Q3 2026", "Q4 2026", "Q1 2027", "Q2 2027", "Q3 2027", "Q4 2027"],
bands: [
{
from: 7,
to: 15,
color: "color-mix(in srgb, var(--md-sys-color-error) 6%, transparent)",
label: "Forecast"
}
]
};
const yAxis = {
min: 0
};
onMount(() => {
const el = chart;
const ACTUAL = [18.7, 19.1, 20.4, 21.6, 19.8, 19.4, 19.9, 21.7];
const MEAN = [21.6, 24.9, 23.4, 24.2, 24.5, 26.1, 27.2, 27.6];
// Random-walk uncertainty: sigma grows with the SQUARE ROOT of the horizon.
const SIGMA = MEAN.map((_, i) => 0.9 * Math.sqrt(i + 1));
const anchor = ACTUAL[ACTUAL.length - 1];
const band = (k) => [
...ACTUAL.slice(0, -1).map(() => null), // measured half: no band
[anchor, anchor], // zero width at the split
...MEAN.map((m, i) => [m - k * SIGMA[i], m + k * SIGMA[i]]),
];
// Normalise the theme's error role to #rrggbb via a throwaway 2D context,
// then tint by alpha — nested bands need distinguishable swatches.
const probe = document.createElement('canvas').getContext('2d');
probe.fillStyle = getComputedStyle(document.documentElement)
.getPropertyValue('--md-sys-color-error').trim() || '#B3261E';
const hex = probe.fillStyle;
const n = parseInt(hex.slice(1), 16);
const tint = (a) => 'rgba(' + ((n >> 16) & 255) + ',' + ((n >> 8) & 255) + ',' + (n & 255) + ',' + a + ')';
el.valueFormatter = (v) => (v == null ? '—' : v.toFixed(1) + 'M');
el.series = [
{ label: '3σ confidence', color: tint(0.18), range: band(3) }, // widest first
{ label: '2σ confidence', color: tint(0.3), range: band(2) },
{ label: '1σ confidence', color: tint(0.45), range: band(1) },
{ label: 'Net income', color: 'primary', data: [...ACTUAL, ...MEAN], fill: false },
];
});
</script>
<md-area-chart
bind:this={chart}
xAxis={xAxis}
yAxis={yAxis}
label="Predicted net income"
curve="linear"
legend="none"
grid="horizontal"
fill-opacity="1"
title-align="center"
/>inverted transposes the plot: the category / time axis runs down the side and
values run across the bottom. Reach for it when the independent variable is
naturally read as depth — altitude, a drill core, ocean depth — or when its
labels are long enough that a horizontal axis would rotate them.
Stacking still applies along the value axis, so a percentage-stacked inverted chart reads as a composition profile: at any height, what is this made of?
<md-area-chart inverted stack="percentage" curve="smooth" legend="bottom"></md-area-chart>
<script type="module">
const series = [
{
label: "N₂",
data: [78, 69, 43, 12, 2, 0]
},
{
label: "O₂",
data: [21, 10, 4, 1, 0, 0]
},
{
label: "O",
data: [0, 20, 53, 84, 88, 60]
},
{
label: "He",
data: [0, 1, 0.3, 3, 8, 32]
},
{
label: "H",
data: [0, 0, 0, 0.3, 2, 8]
}
];
const xAxis = {
data: [0, 150, 200, 300, 400, 600],
scale: "value",
label: "Height (km)"
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [
{
label: "N₂",
data: [78, 69, 43, 12, 2, 0]
},
{
label: "O₂",
data: [21, 10, 4, 1, 0, 0]
},
{
label: "O",
data: [0, 20, 53, 84, 88, 60]
},
{
label: "He",
data: [0, 1, 0.3, 3, 8, 32]
},
{
label: "H",
data: [0, 0, 0, 0.3, 2, 8]
}
];
const xAxis = {
data: [0, 150, 200, 300, 400, 600],
scale: "value",
label: "Height (km)"
};
export function Chart() {
return (
<MdAreaChart
series={series}
xAxis={xAxis}
inverted
stack="percentage"
curve="smooth"
legend="bottom"
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
[xAxis]="xAxis"
inverted
stack="percentage"
curve="smooth"
legend="bottom"
></md-area-chart>
`,
})
export class ChartComponent {
series = [
{
label: "N₂",
data: [78, 69, 43, 12, 2, 0]
},
{
label: "O₂",
data: [21, 10, 4, 1, 0, 0]
},
{
label: "O",
data: [0, 20, 53, 84, 88, 60]
},
{
label: "He",
data: [0, 1, 0.3, 3, 8, 32]
},
{
label: "H",
data: [0, 0, 0, 0.3, 2, 8]
}
];
xAxis = {
data: [0, 150, 200, 300, 400, 600],
scale: "value",
label: "Height (km)"
};
}<script setup lang="ts">
const series = [
{
label: "N₂",
data: [78, 69, 43, 12, 2, 0]
},
{
label: "O₂",
data: [21, 10, 4, 1, 0, 0]
},
{
label: "O",
data: [0, 20, 53, 84, 88, 60]
},
{
label: "He",
data: [0, 1, 0.3, 3, 8, 32]
},
{
label: "H",
data: [0, 0, 0, 0.3, 2, 8]
}
];
const xAxis = {
data: [0, 150, 200, 300, 400, 600],
scale: "value",
label: "Height (km)"
};
</script>
<template>
<md-area-chart
:series="series"
:xAxis="xAxis"
inverted
stack="percentage"
curve="smooth"
legend="bottom"
/>
</template><script lang="ts">
const series = [
{
label: "N₂",
data: [78, 69, 43, 12, 2, 0]
},
{
label: "O₂",
data: [21, 10, 4, 1, 0, 0]
},
{
label: "O",
data: [0, 20, 53, 84, 88, 60]
},
{
label: "He",
data: [0, 1, 0.3, 3, 8, 32]
},
{
label: "H",
data: [0, 0, 0, 0.3, 2, 8]
}
];
const xAxis = {
data: [0, 150, 200, 300, 400, 600],
scale: "value",
label: "Height (km)"
};
</script>
<md-area-chart
series={series}
xAxis={xAxis}
inverted
stack="percentage"
curve="smooth"
legend="bottom"
/>A null is a gap, not a zero: the fill and the line both break at it, so a hole
in the record reads as a hole. Set connect-nulls to bridge it instead — right
for a dropped sensor reading, wrong for a period a series genuinely did not
exist, where bridging invents data.
<md-area-chart stack="none" show-marks fill-opacity="0.3"></md-area-chart>
<script type="module">
const series = [
{
label: "Bridged",
data: [10, 18, null, null, 22, 28, null, 36],
connectNulls: true
},
{
label: "Left open",
data: [6, 12, null, null, 15, 19, null, 24],
connectNulls: false
}
];
const xAxis = {
data: ["A", "B", "C", "D", "E", "F", "G", "H"]
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [
{
label: "Bridged",
data: [10, 18, null, null, 22, 28, null, 36],
connectNulls: true
},
{
label: "Left open",
data: [6, 12, null, null, 15, 19, null, 24],
connectNulls: false
}
];
const xAxis = {
data: ["A", "B", "C", "D", "E", "F", "G", "H"]
};
export function Chart() {
return (
<MdAreaChart
series={series}
xAxis={xAxis}
stack="none"
show-marks
fill-opacity="0.3"
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
[xAxis]="xAxis"
stack="none"
show-marks
fill-opacity="0.3"
></md-area-chart>
`,
})
export class ChartComponent {
series = [
{
label: "Bridged",
data: [10, 18, null, null, 22, 28, null, 36],
connectNulls: true
},
{
label: "Left open",
data: [6, 12, null, null, 15, 19, null, 24],
connectNulls: false
}
];
xAxis = {
data: ["A", "B", "C", "D", "E", "F", "G", "H"]
};
}<script setup lang="ts">
const series = [
{
label: "Bridged",
data: [10, 18, null, null, 22, 28, null, 36],
connectNulls: true
},
{
label: "Left open",
data: [6, 12, null, null, 15, 19, null, 24],
connectNulls: false
}
];
const xAxis = {
data: ["A", "B", "C", "D", "E", "F", "G", "H"]
};
</script>
<template>
<md-area-chart
:series="series"
:xAxis="xAxis"
stack="none"
show-marks
fill-opacity="0.3"
/>
</template><script lang="ts">
const series = [
{
label: "Bridged",
data: [10, 18, null, null, 22, 28, null, 36],
connectNulls: true
},
{
label: "Left open",
data: [6, 12, null, null, 15, 19, null, 24],
connectNulls: false
}
];
const xAxis = {
data: ["A", "B", "C", "D", "E", "F", "G", "H"]
};
</script>
<md-area-chart
series={series}
xAxis={xAxis}
stack="none"
show-marks
fill-opacity="0.3"
/>A series that crosses zero fills to the zero line, not to the bottom of the plot — so the band above the axis and the band below it read as opposite signs rather than as one continuous shape. The domain stretches to hold both, and the zero line stays where the data puts it.
Stacking splits by sign: positives stack upward from zero and negatives stack downward, each side accumulating separately. That’s the only way a stacked column with mixed signs can stay readable — summing them into one band would cancel contributions that the reader needs to see.
<md-area-chart label="Net cash flow" stack="none" curve="linear" show-marks fill-opacity="0.35" legend="bottom"></md-area-chart>
<script type="module">
const series = [
{
label: "Operating",
color: "primary",
data: [12, 18, -6, -14, 4, 16, 22, 9]
},
{
label: "Investing",
color: "tertiary",
data: [-8, -4, -12, 6, 14, -3, -9, 11]
}
];
const xAxis = {
data: ["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"]
};
const yAxis = {
label: "€M"
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis, yAxis });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [
{
label: "Operating",
color: "primary",
data: [12, 18, -6, -14, 4, 16, 22, 9]
},
{
label: "Investing",
color: "tertiary",
data: [-8, -4, -12, 6, 14, -3, -9, 11]
}
];
const xAxis = {
data: ["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"]
};
const yAxis = {
label: "€M"
};
export function Chart() {
return (
<MdAreaChart
series={series}
xAxis={xAxis}
yAxis={yAxis}
label="Net cash flow"
stack="none"
curve="linear"
show-marks
fill-opacity="0.35"
legend="bottom"
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
[xAxis]="xAxis"
[yAxis]="yAxis"
label="Net cash flow"
stack="none"
curve="linear"
show-marks
fill-opacity="0.35"
legend="bottom"
></md-area-chart>
`,
})
export class ChartComponent {
series = [
{
label: "Operating",
color: "primary",
data: [12, 18, -6, -14, 4, 16, 22, 9]
},
{
label: "Investing",
color: "tertiary",
data: [-8, -4, -12, 6, 14, -3, -9, 11]
}
];
xAxis = {
data: ["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"]
};
yAxis = {
label: "€M"
};
}<script setup lang="ts">
const series = [
{
label: "Operating",
color: "primary",
data: [12, 18, -6, -14, 4, 16, 22, 9]
},
{
label: "Investing",
color: "tertiary",
data: [-8, -4, -12, 6, 14, -3, -9, 11]
}
];
const xAxis = {
data: ["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"]
};
const yAxis = {
label: "€M"
};
</script>
<template>
<md-area-chart
:series="series"
:xAxis="xAxis"
:yAxis="yAxis"
label="Net cash flow"
stack="none"
curve="linear"
show-marks
fill-opacity="0.35"
legend="bottom"
/>
</template><script lang="ts">
const series = [
{
label: "Operating",
color: "primary",
data: [12, 18, -6, -14, 4, 16, 22, 9]
},
{
label: "Investing",
color: "tertiary",
data: [-8, -4, -12, 6, 14, -3, -9, 11]
}
];
const xAxis = {
data: ["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"]
};
const yAxis = {
label: "€M"
};
</script>
<md-area-chart
series={series}
xAxis={xAxis}
yAxis={yAxis}
label="Net cash flow"
stack="none"
curve="linear"
show-marks
fill-opacity="0.35"
legend="bottom"
/>A “race” sweeps the plot from the first sample to the last, revealing the bands
as it goes. There is no race prop — you reassign series and xAxis on every
frame, and the chart redraws. Press play:
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">
<!-- index.html — register the AWC UI elements once -->
<script type="module">
import '@awc-ui/core/define';
</script>
<div style="inline-size:100%;display:flex;flex-direction:column;gap:12px;">
<div style="display:flex;align-items:center;gap:16px;">
<md-icon-button id="race-play" variant="filled" icon="play_arrow" aria-label="Play or pause"></md-icon-button>
<md-slider id="race-slider" min="2000" max="2024" step="0.1" value="2024" style="flex:1;" aria-label="Progress"></md-slider>
</div>
<md-area-chart id="race-chart" label="Energy mix" stack="normal" curve="linear" style="inline-size:100%;block-size:280px;"></md-area-chart>
</div>
<script type="module">
const YEARS = [2000, 2004, 2008, 2012, 2016, 2020, 2024];
const SOURCE = [
{ label: 'Coal', color: 'tertiary', data: [58, 54, 47, 40, 31, 22, 15] },
{ label: 'Gas', color: 'secondary', data: [22, 26, 30, 33, 34, 32, 28] },
{ label: 'Solar', color: 'primary', data: [1, 3, 7, 14, 24, 38, 55] },
];
const FIRST = YEARS[0], LAST = YEARS[YEARS.length - 1];
const chart = document.getElementById('race-chart');
const slider = document.getElementById('race-slider');
const play = document.getElementById('race-play');
// Start COMPLETE, not empty: a docs page that greets you with a blank plot
// reads as broken. Play resets to FIRST and sweeps (see the handler below).
let progress = LAST, raf = 0, last = 0, dragging = false;
const SPEED = (LAST - FIRST) / 8000; // full sweep ≈ 8s
function apply() {
const xs = [];
const rows = SOURCE.map(() => []);
for (let i = 0; YEARS.length > i; i++) {
const year = YEARS[i];
xs.push(year);
// Points past the leading edge are null, not dropped — the axis keeps its
// full range so it does not rescale on every frame.
SOURCE.forEach((s, k) => rows[k].push(progress >= year ? s.data[i] : null));
// An interpolated sample exactly at the edge, so the sweep glides between
// samples instead of stepping one whole year at a time.
if (YEARS.length - 1 > i && progress >= year && YEARS[i + 1] > progress) {
const span = YEARS[i + 1] - year;
const f = span > 0 ? (progress - year) / span : 0;
if (f > 1e-4) {
xs.push(progress);
SOURCE.forEach((s, k) => rows[k].push(s.data[i] + (s.data[i + 1] - s.data[i]) * f));
}
}
}
chart.xAxis = { data: xs, scale: 'value', valueFormatter: (v) => String(Math.round(v)) };
chart.series = SOURCE.map((s, k) => ({ label: s.label, color: s.color, data: rows[k] }));
if (!dragging) slider.value = progress;
}
function stop() { cancelAnimationFrame(raf); raf = 0; last = 0; play.icon = 'play_arrow'; }
function frame(now) {
if (!chart.isConnected) return stop(); // never leak a rAF into a dead node
if (!last) last = now;
progress = Math.min(LAST, progress + (now - last) * SPEED);
last = now;
apply();
if (progress >= LAST) return stop();
raf = requestAnimationFrame(frame);
}
play.addEventListener('mdClick', () => {
if (raf) return stop();
if (progress >= LAST) progress = FIRST;
play.icon = 'pause'; last = 0; raf = requestAnimationFrame(frame);
});
slider.addEventListener('mdInput', (e) => {
dragging = true; stop(); progress = e.detail.value ?? slider.value; apply();
});
slider.addEventListener('mdChange', () => { dragging = false; });
apply();
</script>// Icons need the Material Symbols stylesheet in index.html — see Installation.
import { useEffect, useRef } from 'react';
import { MdAreaChart, MdIconButton, MdSlider } from '@awc-ui/react';
export function Demo() {
const chartRef = useRef(null);
const sliderRef = useRef(null);
const playRef = useRef(null);
useEffect(() => {
const YEARS = [2000, 2004, 2008, 2012, 2016, 2020, 2024];
const SOURCE = [
{ label: 'Coal', color: 'tertiary', data: [58, 54, 47, 40, 31, 22, 15] },
{ label: 'Gas', color: 'secondary', data: [22, 26, 30, 33, 34, 32, 28] },
{ label: 'Solar', color: 'primary', data: [1, 3, 7, 14, 24, 38, 55] },
];
const FIRST = YEARS[0], LAST = YEARS[YEARS.length - 1];
const chart = chartRef.current;
const slider = sliderRef.current;
const play = playRef.current;
// Start COMPLETE, not empty: a docs page that greets you with a blank plot
// reads as broken. Play resets to FIRST and sweeps (see the handler below).
let progress = LAST, raf = 0, last = 0, dragging = false;
const SPEED = (LAST - FIRST) / 8000; // full sweep ≈ 8s
function apply() {
const xs = [];
const rows = SOURCE.map(() => []);
for (let i = 0; YEARS.length > i; i++) {
const year = YEARS[i];
xs.push(year);
// Points past the leading edge are null, not dropped — the axis keeps its
// full range so it does not rescale on every frame.
SOURCE.forEach((s, k) => rows[k].push(progress >= year ? s.data[i] : null));
// An interpolated sample exactly at the edge, so the sweep glides between
// samples instead of stepping one whole year at a time.
if (YEARS.length - 1 > i && progress >= year && YEARS[i + 1] > progress) {
const span = YEARS[i + 1] - year;
const f = span > 0 ? (progress - year) / span : 0;
if (f > 1e-4) {
xs.push(progress);
SOURCE.forEach((s, k) => rows[k].push(s.data[i] + (s.data[i + 1] - s.data[i]) * f));
}
}
}
chart.xAxis = { data: xs, scale: 'value', valueFormatter: (v) => String(Math.round(v)) };
chart.series = SOURCE.map((s, k) => ({ label: s.label, color: s.color, data: rows[k] }));
if (!dragging) slider.value = progress;
}
function stop() { cancelAnimationFrame(raf); raf = 0; last = 0; play.icon = 'play_arrow'; }
function frame(now) {
if (!chart.isConnected) return stop(); // never leak a rAF into a dead node
if (!last) last = now;
progress = Math.min(LAST, progress + (now - last) * SPEED);
last = now;
apply();
if (progress >= LAST) return stop();
raf = requestAnimationFrame(frame);
}
play.addEventListener('mdClick', () => {
if (raf) return stop();
if (progress >= LAST) progress = FIRST;
play.icon = 'pause'; last = 0; raf = requestAnimationFrame(frame);
});
slider.addEventListener('mdInput', (e) => {
dragging = true; stop(); progress = e.detail.value ?? slider.value; apply();
});
slider.addEventListener('mdChange', () => { dragging = false; });
apply();
}, []);
return (
<>
<div style={{ inlineSize: '100%', display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<MdIconButton id="race-play" ref={playRef} variant="filled" icon="play_arrow" aria-label="Play or pause"></MdIconButton>
<MdSlider id="race-slider" ref={sliderRef} min="2000" max="2024" step="0.1" value="2024" style={{ flex: '1' }} aria-label="Progress"></MdSlider>
</div>
<MdAreaChart id="race-chart" ref={chartRef} label="Energy mix" stack="normal" curve="linear" style={{ inlineSize: '100%', blockSize: '280px' }}></MdAreaChart>
</div>
</>
);
}// Icons need the Material Symbols stylesheet in index.html — see Installation.
// app.module.ts — register the AWC UI elements once
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { AwcUiModule } from '@awc-ui/angular';
@NgModule({
imports: [AwcUiModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}
// app.component.ts
import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core';
@Component({
selector: 'app-demo',
templateUrl: './app.component.html',
})
export class DemoComponent implements AfterViewInit {
@ViewChild('chart') chartRef!: ElementRef;
@ViewChild('slider') sliderRef!: ElementRef;
@ViewChild('play') playRef!: ElementRef;
ngAfterViewInit() {
const YEARS = [2000, 2004, 2008, 2012, 2016, 2020, 2024];
const SOURCE = [
{ label: 'Coal', color: 'tertiary', data: [58, 54, 47, 40, 31, 22, 15] },
{ label: 'Gas', color: 'secondary', data: [22, 26, 30, 33, 34, 32, 28] },
{ label: 'Solar', color: 'primary', data: [1, 3, 7, 14, 24, 38, 55] },
];
const FIRST = YEARS[0], LAST = YEARS[YEARS.length - 1];
const chart = this.chartRef.nativeElement;
const slider = this.sliderRef.nativeElement;
const play = this.playRef.nativeElement;
// Start COMPLETE, not empty: a docs page that greets you with a blank plot
// reads as broken. Play resets to FIRST and sweeps (see the handler below).
let progress = LAST, raf = 0, last = 0, dragging = false;
const SPEED = (LAST - FIRST) / 8000; // full sweep ≈ 8s
function apply() {
const xs = [];
const rows = SOURCE.map(() => []);
for (let i = 0; YEARS.length > i; i++) {
const year = YEARS[i];
xs.push(year);
// Points past the leading edge are null, not dropped — the axis keeps its
// full range so it does not rescale on every frame.
SOURCE.forEach((s, k) => rows[k].push(progress >= year ? s.data[i] : null));
// An interpolated sample exactly at the edge, so the sweep glides between
// samples instead of stepping one whole year at a time.
if (YEARS.length - 1 > i && progress >= year && YEARS[i + 1] > progress) {
const span = YEARS[i + 1] - year;
const f = span > 0 ? (progress - year) / span : 0;
if (f > 1e-4) {
xs.push(progress);
SOURCE.forEach((s, k) => rows[k].push(s.data[i] + (s.data[i + 1] - s.data[i]) * f));
}
}
}
chart.xAxis = { data: xs, scale: 'value', valueFormatter: (v) => String(Math.round(v)) };
chart.series = SOURCE.map((s, k) => ({ label: s.label, color: s.color, data: rows[k] }));
if (!dragging) slider.value = progress;
}
function stop() { cancelAnimationFrame(raf); raf = 0; last = 0; play.icon = 'play_arrow'; }
function frame(now) {
if (!chart.isConnected) return stop(); // never leak a rAF into a dead node
if (!last) last = now;
progress = Math.min(LAST, progress + (now - last) * SPEED);
last = now;
apply();
if (progress >= LAST) return stop();
raf = requestAnimationFrame(frame);
}
play.addEventListener('mdClick', () => {
if (raf) return stop();
if (progress >= LAST) progress = FIRST;
play.icon = 'pause'; last = 0; raf = requestAnimationFrame(frame);
});
slider.addEventListener('mdInput', (e) => {
dragging = true; stop(); progress = e.detail.value ?? slider.value; apply();
});
slider.addEventListener('mdChange', () => { dragging = false; });
apply();
}
}
<!-- app.component.html -->
<div style="inline-size:100%;display:flex;flex-direction:column;gap:12px;">
<div style="display:flex;align-items:center;gap:16px;">
<md-icon-button id="race-play" #play variant="filled" icon="play_arrow" aria-label="Play or pause"></md-icon-button>
<md-slider id="race-slider" #slider min="2000" max="2024" step="0.1" value="2024" style="flex:1;" aria-label="Progress"></md-slider>
</div>
<md-area-chart id="race-chart" #chart label="Energy mix" stack="normal" curve="linear" style="inline-size:100%;block-size:280px;"></md-area-chart>
</div><script setup>
// Icons need the Material Symbols stylesheet in index.html — see Installation.
import { onMounted, ref } from 'vue';
import '@awc-ui/core/define';
const chartRef = ref(null);
const sliderRef = ref(null);
const playRef = ref(null);
onMounted(() => {
const YEARS = [2000, 2004, 2008, 2012, 2016, 2020, 2024];
const SOURCE = [
{ label: 'Coal', color: 'tertiary', data: [58, 54, 47, 40, 31, 22, 15] },
{ label: 'Gas', color: 'secondary', data: [22, 26, 30, 33, 34, 32, 28] },
{ label: 'Solar', color: 'primary', data: [1, 3, 7, 14, 24, 38, 55] },
];
const FIRST = YEARS[0], LAST = YEARS[YEARS.length - 1];
const chart = chartRef.value;
const slider = sliderRef.value;
const play = playRef.value;
// Start COMPLETE, not empty: a docs page that greets you with a blank plot
// reads as broken. Play resets to FIRST and sweeps (see the handler below).
let progress = LAST, raf = 0, last = 0, dragging = false;
const SPEED = (LAST - FIRST) / 8000; // full sweep ≈ 8s
function apply() {
const xs = [];
const rows = SOURCE.map(() => []);
for (let i = 0; YEARS.length > i; i++) {
const year = YEARS[i];
xs.push(year);
// Points past the leading edge are null, not dropped — the axis keeps its
// full range so it does not rescale on every frame.
SOURCE.forEach((s, k) => rows[k].push(progress >= year ? s.data[i] : null));
// An interpolated sample exactly at the edge, so the sweep glides between
// samples instead of stepping one whole year at a time.
if (YEARS.length - 1 > i && progress >= year && YEARS[i + 1] > progress) {
const span = YEARS[i + 1] - year;
const f = span > 0 ? (progress - year) / span : 0;
if (f > 1e-4) {
xs.push(progress);
SOURCE.forEach((s, k) => rows[k].push(s.data[i] + (s.data[i + 1] - s.data[i]) * f));
}
}
}
chart.xAxis = { data: xs, scale: 'value', valueFormatter: (v) => String(Math.round(v)) };
chart.series = SOURCE.map((s, k) => ({ label: s.label, color: s.color, data: rows[k] }));
if (!dragging) slider.value = progress;
}
function stop() { cancelAnimationFrame(raf); raf = 0; last = 0; play.icon = 'play_arrow'; }
function frame(now) {
if (!chart.isConnected) return stop(); // never leak a rAF into a dead node
if (!last) last = now;
progress = Math.min(LAST, progress + (now - last) * SPEED);
last = now;
apply();
if (progress >= LAST) return stop();
raf = requestAnimationFrame(frame);
}
play.addEventListener('mdClick', () => {
if (raf) return stop();
if (progress >= LAST) progress = FIRST;
play.icon = 'pause'; last = 0; raf = requestAnimationFrame(frame);
});
slider.addEventListener('mdInput', (e) => {
dragging = true; stop(); progress = e.detail.value ?? slider.value; apply();
});
slider.addEventListener('mdChange', () => { dragging = false; });
apply();
});
</script>
<template>
<div style="inline-size:100%;display:flex;flex-direction:column;gap:12px;">
<div style="display:flex;align-items:center;gap:16px;">
<md-icon-button id="race-play" ref="playRef" variant="filled" icon="play_arrow" aria-label="Play or pause"></md-icon-button>
<md-slider id="race-slider" ref="sliderRef" min="2000" max="2024" step="0.1" value="2024" style="flex:1;" aria-label="Progress"></md-slider>
</div>
<md-area-chart id="race-chart" ref="chartRef" label="Energy mix" stack="normal" curve="linear" style="inline-size:100%;block-size:280px;"></md-area-chart>
</div>
</template><script>
// Icons need the Material Symbols stylesheet in index.html — see Installation.
import { onMount } from 'svelte';
import '@awc-ui/core/define';
let chartRef;
let sliderRef;
let playRef;
onMount(() => {
const YEARS = [2000, 2004, 2008, 2012, 2016, 2020, 2024];
const SOURCE = [
{ label: 'Coal', color: 'tertiary', data: [58, 54, 47, 40, 31, 22, 15] },
{ label: 'Gas', color: 'secondary', data: [22, 26, 30, 33, 34, 32, 28] },
{ label: 'Solar', color: 'primary', data: [1, 3, 7, 14, 24, 38, 55] },
];
const FIRST = YEARS[0], LAST = YEARS[YEARS.length - 1];
const chart = chartRef;
const slider = sliderRef;
const play = playRef;
// Start COMPLETE, not empty: a docs page that greets you with a blank plot
// reads as broken. Play resets to FIRST and sweeps (see the handler below).
let progress = LAST, raf = 0, last = 0, dragging = false;
const SPEED = (LAST - FIRST) / 8000; // full sweep ≈ 8s
function apply() {
const xs = [];
const rows = SOURCE.map(() => []);
for (let i = 0; YEARS.length > i; i++) {
const year = YEARS[i];
xs.push(year);
// Points past the leading edge are null, not dropped — the axis keeps its
// full range so it does not rescale on every frame.
SOURCE.forEach((s, k) => rows[k].push(progress >= year ? s.data[i] : null));
// An interpolated sample exactly at the edge, so the sweep glides between
// samples instead of stepping one whole year at a time.
if (YEARS.length - 1 > i && progress >= year && YEARS[i + 1] > progress) {
const span = YEARS[i + 1] - year;
const f = span > 0 ? (progress - year) / span : 0;
if (f > 1e-4) {
xs.push(progress);
SOURCE.forEach((s, k) => rows[k].push(s.data[i] + (s.data[i + 1] - s.data[i]) * f));
}
}
}
chart.xAxis = { data: xs, scale: 'value', valueFormatter: (v) => String(Math.round(v)) };
chart.series = SOURCE.map((s, k) => ({ label: s.label, color: s.color, data: rows[k] }));
if (!dragging) slider.value = progress;
}
function stop() { cancelAnimationFrame(raf); raf = 0; last = 0; play.icon = 'play_arrow'; }
function frame(now) {
if (!chart.isConnected) return stop(); // never leak a rAF into a dead node
if (!last) last = now;
progress = Math.min(LAST, progress + (now - last) * SPEED);
last = now;
apply();
if (progress >= LAST) return stop();
raf = requestAnimationFrame(frame);
}
play.addEventListener('mdClick', () => {
if (raf) return stop();
if (progress >= LAST) progress = FIRST;
play.icon = 'pause'; last = 0; raf = requestAnimationFrame(frame);
});
slider.addEventListener('mdInput', (e) => {
dragging = true; stop(); progress = e.detail.value ?? slider.value; apply();
});
slider.addEventListener('mdChange', () => { dragging = false; });
apply();
});
</script>
<div style="inline-size:100%;display:flex;flex-direction:column;gap:12px;">
<div style="display:flex;align-items:center;gap:16px;">
<md-icon-button id="race-play" bind:this={playRef} variant="filled" icon="play_arrow" aria-label="Play or pause"></md-icon-button>
<md-slider id="race-slider" bind:this={sliderRef} min="2000" max="2024" step="0.1" value="2024" style="flex:1;" aria-label="Progress"></md-slider>
</div>
<md-area-chart id="race-chart" bind:this={chartRef} label="Energy mix" stack="normal" curve="linear" style="inline-size:100%;block-size:280px;"></md-area-chart>
</div>Four things make the difference between a smooth race and a jerky one:
| Do | Why |
|---|---|
| Drive progress in axis value space (the year), not the array index | Real series are rarely evenly sampled. Stepping by index sweeps ten years per tick where the data is decadal and one per tick where it is annual — the edge races, then crawls, and a slider bound to the index disagrees with what you see |
Mask the future with null, do not slice the array | The axis keeps its full range, so it does not rescale on every frame |
| Insert an interpolated point at the edge | Without it the band steps one whole sample at a time instead of gliding |
Stop on !chart.isConnected | Otherwise the rAF loop outlives the element and keeps running after navigation |
zoom turns on two independent gestures:
| Value | Gesture |
|---|---|
none | Off (default) |
inside | Drag across the plot to select a range; double-click to reset |
slider | A range slider under the plot; drag its middle to pan the window |
both | Both |
Zoom is a view. The window slices what the engine draws and leaves your
series array untouched, so nothing downstream has to know a window is applied
— every event still reports absolute indices into the data you supplied.
<md-area-chart zoom="both" stack="normal" label="Sessions"></md-area-chart>
<script type="module">
const series = [
{
label: "Sessions",
data: [60, 66, 72, 78, 71, 76, 81, 86, 77, 81, 84, 86, 76, 78, 79, 80, 68, 68, 69, 69, 57, 57, 57, 58, 46, 48, 49, 51, 42, 45, 49, 53, 45, 50, 56, 61, 55, 61, 67, 73, 67, 72, 78, 83, 76, 80, 84, 87, 78, 80, 82, 83, 72, 73, 73, 74, 62, 62, 62, 62]
},
{
label: "Signups",
data: [40, 42, 44, 39, 40, 42, 37, 37, 38, 33, 33, 34, 29, 29, 30, 25, 25, 26, 22, 23, 24, 20, 22, 24, 20, 23, 26, 22, 25, 29, 26, 29, 33, 30, 33, 37, 34, 37, 41, 38, 40, 43, 40, 42, 44, 40, 42, 43, 38, 40, 41, 35, 36, 37, 31, 32, 33, 27, 28, 29]
}
];
const xAxis = {
data: ["T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "T9", "T10", "T11", "T12", "T13", "T14", "T15", "T16", "T17", "T18", "T19", "T20", "T21", "T22", "T23", "T24", "T25", "T26", "T27", "T28", "T29", "T30", "T31", "T32", "T33", "T34", "T35", "T36", "T37", "T38", "T39", "T40", "T41", "T42", "T43", "T44", "T45", "T46", "T47", "T48", "T49", "T50", "T51", "T52", "T53", "T54", "T55", "T56", "T57", "T58", "T59", "T60"],
hideTicks: true
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [
{
label: "Sessions",
data: [60, 66, 72, 78, 71, 76, 81, 86, 77, 81, 84, 86, 76, 78, 79, 80, 68, 68, 69, 69, 57, 57, 57, 58, 46, 48, 49, 51, 42, 45, 49, 53, 45, 50, 56, 61, 55, 61, 67, 73, 67, 72, 78, 83, 76, 80, 84, 87, 78, 80, 82, 83, 72, 73, 73, 74, 62, 62, 62, 62]
},
{
label: "Signups",
data: [40, 42, 44, 39, 40, 42, 37, 37, 38, 33, 33, 34, 29, 29, 30, 25, 25, 26, 22, 23, 24, 20, 22, 24, 20, 23, 26, 22, 25, 29, 26, 29, 33, 30, 33, 37, 34, 37, 41, 38, 40, 43, 40, 42, 44, 40, 42, 43, 38, 40, 41, 35, 36, 37, 31, 32, 33, 27, 28, 29]
}
];
const xAxis = {
data: ["T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "T9", "T10", "T11", "T12", "T13", "T14", "T15", "T16", "T17", "T18", "T19", "T20", "T21", "T22", "T23", "T24", "T25", "T26", "T27", "T28", "T29", "T30", "T31", "T32", "T33", "T34", "T35", "T36", "T37", "T38", "T39", "T40", "T41", "T42", "T43", "T44", "T45", "T46", "T47", "T48", "T49", "T50", "T51", "T52", "T53", "T54", "T55", "T56", "T57", "T58", "T59", "T60"],
hideTicks: true
};
export function Chart() {
return (
<MdAreaChart
series={series}
xAxis={xAxis}
zoom="both"
stack="normal"
label="Sessions"
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
[xAxis]="xAxis"
zoom="both"
stack="normal"
label="Sessions"
></md-area-chart>
`,
})
export class ChartComponent {
series = [
{
label: "Sessions",
data: [60, 66, 72, 78, 71, 76, 81, 86, 77, 81, 84, 86, 76, 78, 79, 80, 68, 68, 69, 69, 57, 57, 57, 58, 46, 48, 49, 51, 42, 45, 49, 53, 45, 50, 56, 61, 55, 61, 67, 73, 67, 72, 78, 83, 76, 80, 84, 87, 78, 80, 82, 83, 72, 73, 73, 74, 62, 62, 62, 62]
},
{
label: "Signups",
data: [40, 42, 44, 39, 40, 42, 37, 37, 38, 33, 33, 34, 29, 29, 30, 25, 25, 26, 22, 23, 24, 20, 22, 24, 20, 23, 26, 22, 25, 29, 26, 29, 33, 30, 33, 37, 34, 37, 41, 38, 40, 43, 40, 42, 44, 40, 42, 43, 38, 40, 41, 35, 36, 37, 31, 32, 33, 27, 28, 29]
}
];
xAxis = {
data: ["T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "T9", "T10", "T11", "T12", "T13", "T14", "T15", "T16", "T17", "T18", "T19", "T20", "T21", "T22", "T23", "T24", "T25", "T26", "T27", "T28", "T29", "T30", "T31", "T32", "T33", "T34", "T35", "T36", "T37", "T38", "T39", "T40", "T41", "T42", "T43", "T44", "T45", "T46", "T47", "T48", "T49", "T50", "T51", "T52", "T53", "T54", "T55", "T56", "T57", "T58", "T59", "T60"],
hideTicks: true
};
}<script setup lang="ts">
const series = [
{
label: "Sessions",
data: [60, 66, 72, 78, 71, 76, 81, 86, 77, 81, 84, 86, 76, 78, 79, 80, 68, 68, 69, 69, 57, 57, 57, 58, 46, 48, 49, 51, 42, 45, 49, 53, 45, 50, 56, 61, 55, 61, 67, 73, 67, 72, 78, 83, 76, 80, 84, 87, 78, 80, 82, 83, 72, 73, 73, 74, 62, 62, 62, 62]
},
{
label: "Signups",
data: [40, 42, 44, 39, 40, 42, 37, 37, 38, 33, 33, 34, 29, 29, 30, 25, 25, 26, 22, 23, 24, 20, 22, 24, 20, 23, 26, 22, 25, 29, 26, 29, 33, 30, 33, 37, 34, 37, 41, 38, 40, 43, 40, 42, 44, 40, 42, 43, 38, 40, 41, 35, 36, 37, 31, 32, 33, 27, 28, 29]
}
];
const xAxis = {
data: ["T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "T9", "T10", "T11", "T12", "T13", "T14", "T15", "T16", "T17", "T18", "T19", "T20", "T21", "T22", "T23", "T24", "T25", "T26", "T27", "T28", "T29", "T30", "T31", "T32", "T33", "T34", "T35", "T36", "T37", "T38", "T39", "T40", "T41", "T42", "T43", "T44", "T45", "T46", "T47", "T48", "T49", "T50", "T51", "T52", "T53", "T54", "T55", "T56", "T57", "T58", "T59", "T60"],
hideTicks: true
};
</script>
<template>
<md-area-chart
:series="series"
:xAxis="xAxis"
zoom="both"
stack="normal"
label="Sessions"
/>
</template><script lang="ts">
const series = [
{
label: "Sessions",
data: [60, 66, 72, 78, 71, 76, 81, 86, 77, 81, 84, 86, 76, 78, 79, 80, 68, 68, 69, 69, 57, 57, 57, 58, 46, 48, 49, 51, 42, 45, 49, 53, 45, 50, 56, 61, 55, 61, 67, 73, 67, 72, 78, 83, 76, 80, 84, 87, 78, 80, 82, 83, 72, 73, 73, 74, 62, 62, 62, 62]
},
{
label: "Signups",
data: [40, 42, 44, 39, 40, 42, 37, 37, 38, 33, 33, 34, 29, 29, 30, 25, 25, 26, 22, 23, 24, 20, 22, 24, 20, 23, 26, 22, 25, 29, 26, 29, 33, 30, 33, 37, 34, 37, 41, 38, 40, 43, 40, 42, 44, 40, 42, 43, 38, 40, 41, 35, 36, 37, 31, 32, 33, 27, 28, 29]
}
];
const xAxis = {
data: ["T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "T9", "T10", "T11", "T12", "T13", "T14", "T15", "T16", "T17", "T18", "T19", "T20", "T21", "T22", "T23", "T24", "T25", "T26", "T27", "T28", "T29", "T30", "T31", "T32", "T33", "T34", "T35", "T36", "T37", "T38", "T39", "T40", "T41", "T42", "T43", "T44", "T45", "T46", "T47", "T48", "T49", "T50", "T51", "T52", "T53", "T54", "T55", "T56", "T57", "T58", "T59", "T60"],
hideTicks: true
};
</script>
<md-area-chart
series={series}
xAxis={xAxis}
zoom="both"
stack="normal"
label="Sessions"
/>await chart.setZoom(10, 40); // absolute indicesawait chart.resetZoom();Label the slider’s thumbs with label-zoom-start / label-zoom-end.
loading shows an opaque overlay with a progress indicator over the plot — it
has to be opaque, because the engine still draws a placeholder axis when there
is no data. An empty series shows label-empty instead. The two never stack:
“no data” is the wrong answer to “where is my data?” mid-load. Both accept
slotted content (slot="loader", slot="empty") when text is not enough.
<md-area-chart label="Traffic sources" loading loading-label="Loading traffic…"></md-area-chart>
<script type="module">
const series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
export function Chart() {
return (
<MdAreaChart
series={series}
xAxis={xAxis}
label="Traffic sources"
loading
loading-label="Loading traffic…"
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
[xAxis]="xAxis"
label="Traffic sources"
loading
loading-label="Loading traffic…"
></md-area-chart>
`,
})
export class ChartComponent {
series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38]
}
];
xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
}<script setup lang="ts">
const series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
</script>
<template>
<md-area-chart
:series="series"
:xAxis="xAxis"
label="Traffic sources"
loading
loading-label="Loading traffic…"
/>
</template><script lang="ts">
const series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
</script>
<md-area-chart
series={series}
xAxis={xAxis}
label="Traffic sources"
loading
loading-label="Loading traffic…"
/>loading-label names the state for screen readers and prints under the
spinner. The demo above keeps loading on permanently; in practice you clear it
when the fetch resolves:
const chart = document.querySelector('md-area-chart');
chart.loading = true;const rows = await fetch('/api/traffic').then((r) => r.json());chart.series = rows;chart.loading = false; // data first, then drop the overlaySet series before clearing loading, not after: the overlay is what hides
the placeholder axis the engine draws for absent data, so clearing it first
flashes an empty grid between the spinner and the bands.
The default is a circular indicator plus the loading-label text. When that
is not the right shape — a wide dashboard tile, a skeleton mirroring the chart’s
own layout — slot your own into loader; it replaces the default entirely.
(loading is kept as the older alias for the same slot, and loader wins if
both are given.)
<md-area-chart label="Traffic sources" loading><div slot="loader" style="inline-size: min(320px, 70%); display: grid; gap: 12px; justify-items: center;">
<md-progress-indicator variant="linear" indeterminate label="Loading traffic" style="inline-size: 100%;"></md-progress-indicator>
<span>Loading traffic…</span>
</div></md-area-chart>
<script type="module">
const series = [];
const xAxis = {
data: []
};
const yAxis = {
label: "Sessions"
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis, yAxis });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [];
const xAxis = {
data: []
};
const yAxis = {
label: "Sessions"
};
export function Chart() {
return (
<MdAreaChart
series={series}
xAxis={xAxis}
yAxis={yAxis}
label="Traffic sources"
loading
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
[xAxis]="xAxis"
[yAxis]="yAxis"
label="Traffic sources"
loading
></md-area-chart>
`,
})
export class ChartComponent {
series = [];
xAxis = {
data: []
};
yAxis = {
label: "Sessions"
};
}<script setup lang="ts">
const series = [];
const xAxis = {
data: []
};
const yAxis = {
label: "Sessions"
};
</script>
<template>
<md-area-chart
:series="series"
:xAxis="xAxis"
:yAxis="yAxis"
label="Traffic sources"
loading
/>
</template><script lang="ts">
const series = [];
const xAxis = {
data: []
};
const yAxis = {
label: "Sessions"
};
</script>
<md-area-chart
series={series}
xAxis={xAxis}
yAxis={yAxis}
label="Traffic sources"
loading
/>Pick by tile shape, not by taste: circular reads as “this panel is busy” and
survives a narrow column, while a linear bar spans a wide tile and implies the
work moves left to right. Both are indeterminate here — use a determinate
value only when you genuinely know the progress.
<md-area-chart label="Traffic" loading loading-label="Fetching traffic…"><md-progress-indicator slot="loader" variant="circular" indeterminate size="40" label="Loading"></md-progress-indicator></md-area-chart>
<script type="module">
const series = [];
const xAxis = {
data: []
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [];
const xAxis = {
data: []
};
export function Chart() {
return (
<MdAreaChart
series={series}
xAxis={xAxis}
label="Traffic"
loading
loading-label="Fetching traffic…"
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
[xAxis]="xAxis"
label="Traffic"
loading
loading-label="Fetching traffic…"
></md-area-chart>
`,
})
export class ChartComponent {
series = [];
xAxis = {
data: []
};
}<script setup lang="ts">
const series = [];
const xAxis = {
data: []
};
</script>
<template>
<md-area-chart
:series="series"
:xAxis="xAxis"
label="Traffic"
loading
loading-label="Fetching traffic…"
/>
</template><script lang="ts">
const series = [];
const xAxis = {
data: []
};
</script>
<md-area-chart
series={series}
xAxis={xAxis}
label="Traffic"
loading
loading-label="Fetching traffic…"
/>An empty series array shows label-empty in the middle of the plot. Slot
empty when a sentence is not enough — an icon, a “create your first report”
action.
<md-area-chart label-empty="No traffic recorded yet"></md-area-chart>
<script type="module">
const series = [];
const el = document.querySelector("md-area-chart");
Object.assign(el, { series });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [];
export function Chart() {
return (
<MdAreaChart
series={series}
label-empty="No traffic recorded yet"
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
label-empty="No traffic recorded yet"
></md-area-chart>
`,
})
export class ChartComponent {
series = [];
}<script setup lang="ts">
const series = [];
</script>
<template>
<md-area-chart
:series="series"
label-empty="No traffic recorded yet"
/>
</template><script lang="ts">
const series = [];
</script>
<md-area-chart
series={series}
label-empty="No traffic recorded yet"
/>tooltip selects the trigger: axis (default) lists every series at the
hovered x, item reports only the series under the pointer, none disables
pointer hover — keyboard navigation and the live region keep working either way.
On a filled chart item is often the better default: a thick band means the
pointer really is on one series, and an every-series list buries it.
tooltipRenderer replaces the card’s content — the chart keeps owning the
card itself: its surface, elevation, and placement. It’s a property, not an
attribute, so it’s set in JS (or bound with .prop / :prop / [prop] in a
framework), never in markup.
const tooltipRenderer = (ctx) => `${ctx.axisLabel}: ${ctx.series.length} series`;Hover the chart below. Nothing in this card is the default card with extra columns — it’s a different object: a spelled-out day overline, the day’s total as the headline with a day-over-day delta chip, a 100% stacked share bar that gives the composition at a glance, and per-series rows that dim away from the series you’re nearest.
Each piece leans on something the context hands you. The share bar and the
percentages need every series at the hovered x — which is precisely what
ctx.series is. The dimming reads focused (equivalently
ctx.focusedSeriesIndex), so the card emphasises the same series the plot does.
The swatches and bar segments reuse each series’ resolved color, so they track
the theme with nothing hard-coded. The delta is the one thing the chart can’t
tell you — yesterday’s total isn’t at today’s x — so the renderer closes over
its own data, which is the normal way to pull in context the chart doesn’t have.
<md-area-chart label="Traffic sources" stack="normal" tooltip="axis" legend="bottom"></md-area-chart>
<script type="module">
const series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38, 52]
},
{
label: "Organic",
data: [18, 24, 32, 40, 48, 45, 60]
},
{
label: "Paid",
data: [8, 12, 14, 22, 30, 28, 36]
}
];
const xAxis = {
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
};
const tooltipRenderer = (ctx) => {
// Closed over, not passed in: the renderer is handed the hovered x only,
// so anything comparative (yesterday's total) has to come from your data.
const DATA = [
[10, 22, 28, 35, 41, 38, 52],
[18, 24, 32, 40, 48, 45, 60],
[8, 12, 14, 22, 30, 28, 36],
];
const DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const totalAt = (i) => (i < 0 ? null : DATA.reduce((sum, a) => sum + a[i], 0));
const el = (tag, css, text) => {
const n = document.createElement(tag);
if (css) n.style.cssText = css;
if (text != null) n.textContent = text;
return n;
};
const rows = ctx.series;
const total = rows.reduce((sum, r) => sum + (r.value ?? 0), 0);
const prev = totalAt(ctx.dataIndex - 1);
const delta = prev ? Math.round(((total - prev) / prev) * 100) : null;
const lit = (r) => ctx.focusedSeriesIndex < 0 || r.focused;
const card = el('div', 'min-width:212px;font-variant-numeric:tabular-nums');
card.append(el('div',
'font-size:0.68em;letter-spacing:0.1em;text-transform:uppercase;opacity:0.55',
DAYS[ctx.dataIndex] ?? ctx.axisLabel));
const head = el('div', 'display:flex;align-items:baseline;gap:5px;margin:1px 0 9px');
head.append(el('span', 'font-size:1.5em;font-weight:700;line-height:1.05', total.toLocaleString()));
head.append(el('span', 'font-size:0.76em;opacity:0.55', 'visits'));
if (delta !== null) {
const up = delta >= 0;
head.append(el('span',
'margin-inline-start:auto;font-size:0.72em;font-weight:700;padding:1px 7px;border-radius:999px;' +
'background:' + (up ? 'rgba(64,186,128,0.18)' : 'rgba(224,106,106,0.18)') + ';' +
'color:' + (up ? '#40ba80' : '#e06a6a'),
(up ? '▲ ' : '▼ ') + Math.abs(delta) + '%'));
}
card.append(head);
// Composition at a glance — the one view a row list can never give you.
const bar = el('div', 'display:flex;gap:2px;height:6px;margin-bottom:9px');
rows.forEach((r) => {
const seg = el('div',
'flex:' + ((r.value ?? 0) || 0.001) + ';border-radius:999px;background:' + r.color +
';opacity:' + (lit(r) ? '1' : '0.35'));
bar.append(seg);
});
card.append(bar);
rows.forEach((r) => {
const row = el('div',
'display:grid;grid-template-columns:9px 1fr auto 38px;gap:9px;align-items:center;' +
'padding:2.5px 0;opacity:' + (lit(r) ? '1' : '0.45'));
row.append(el('span', 'width:9px;height:9px;border-radius:3px;background:' + r.color));
row.append(el('span', lit(r) && ctx.focusedSeriesIndex >= 0 ? 'font-weight:600' : null, r.label));
row.append(el('span', 'font-weight:700', r.formattedValue));
row.append(el('span', 'opacity:0.55;text-align:end',
total ? Math.round(((r.value ?? 0) / total) * 100) + '%' : ''));
card.append(row);
});
return card;
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis, tooltipRenderer });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38, 52]
},
{
label: "Organic",
data: [18, 24, 32, 40, 48, 45, 60]
},
{
label: "Paid",
data: [8, 12, 14, 22, 30, 28, 36]
}
];
const xAxis = {
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
};
const tooltipRenderer = (ctx) => {
// Closed over, not passed in: the renderer is handed the hovered x only,
// so anything comparative (yesterday's total) has to come from your data.
const DATA = [
[10, 22, 28, 35, 41, 38, 52],
[18, 24, 32, 40, 48, 45, 60],
[8, 12, 14, 22, 30, 28, 36],
];
const DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const totalAt = (i) => (i < 0 ? null : DATA.reduce((sum, a) => sum + a[i], 0));
const el = (tag, css, text) => {
const n = document.createElement(tag);
if (css) n.style.cssText = css;
if (text != null) n.textContent = text;
return n;
};
const rows = ctx.series;
const total = rows.reduce((sum, r) => sum + (r.value ?? 0), 0);
const prev = totalAt(ctx.dataIndex - 1);
const delta = prev ? Math.round(((total - prev) / prev) * 100) : null;
const lit = (r) => ctx.focusedSeriesIndex < 0 || r.focused;
const card = el('div', 'min-width:212px;font-variant-numeric:tabular-nums');
card.append(el('div',
'font-size:0.68em;letter-spacing:0.1em;text-transform:uppercase;opacity:0.55',
DAYS[ctx.dataIndex] ?? ctx.axisLabel));
const head = el('div', 'display:flex;align-items:baseline;gap:5px;margin:1px 0 9px');
head.append(el('span', 'font-size:1.5em;font-weight:700;line-height:1.05', total.toLocaleString()));
head.append(el('span', 'font-size:0.76em;opacity:0.55', 'visits'));
if (delta !== null) {
const up = delta >= 0;
head.append(el('span',
'margin-inline-start:auto;font-size:0.72em;font-weight:700;padding:1px 7px;border-radius:999px;' +
'background:' + (up ? 'rgba(64,186,128,0.18)' : 'rgba(224,106,106,0.18)') + ';' +
'color:' + (up ? '#40ba80' : '#e06a6a'),
(up ? '▲ ' : '▼ ') + Math.abs(delta) + '%'));
}
card.append(head);
// Composition at a glance — the one view a row list can never give you.
const bar = el('div', 'display:flex;gap:2px;height:6px;margin-bottom:9px');
rows.forEach((r) => {
const seg = el('div',
'flex:' + ((r.value ?? 0) || 0.001) + ';border-radius:999px;background:' + r.color +
';opacity:' + (lit(r) ? '1' : '0.35'));
bar.append(seg);
});
card.append(bar);
rows.forEach((r) => {
const row = el('div',
'display:grid;grid-template-columns:9px 1fr auto 38px;gap:9px;align-items:center;' +
'padding:2.5px 0;opacity:' + (lit(r) ? '1' : '0.45'));
row.append(el('span', 'width:9px;height:9px;border-radius:3px;background:' + r.color));
row.append(el('span', lit(r) && ctx.focusedSeriesIndex >= 0 ? 'font-weight:600' : null, r.label));
row.append(el('span', 'font-weight:700', r.formattedValue));
row.append(el('span', 'opacity:0.55;text-align:end',
total ? Math.round(((r.value ?? 0) / total) * 100) + '%' : ''));
card.append(row);
});
return card;
};
export function Chart() {
return (
<MdAreaChart
series={series}
xAxis={xAxis}
tooltipRenderer={tooltipRenderer}
label="Traffic sources"
stack="normal"
tooltip="axis"
legend="bottom"
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
[xAxis]="xAxis"
[tooltipRenderer]="tooltipRenderer"
label="Traffic sources"
stack="normal"
tooltip="axis"
legend="bottom"
></md-area-chart>
`,
})
export class ChartComponent {
series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38, 52]
},
{
label: "Organic",
data: [18, 24, 32, 40, 48, 45, 60]
},
{
label: "Paid",
data: [8, 12, 14, 22, 30, 28, 36]
}
];
xAxis = {
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
};
tooltipRenderer = (ctx) => {
// Closed over, not passed in: the renderer is handed the hovered x only,
// so anything comparative (yesterday's total) has to come from your data.
const DATA = [
[10, 22, 28, 35, 41, 38, 52],
[18, 24, 32, 40, 48, 45, 60],
[8, 12, 14, 22, 30, 28, 36],
];
const DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const totalAt = (i) => (i < 0 ? null : DATA.reduce((sum, a) => sum + a[i], 0));
const el = (tag, css, text) => {
const n = document.createElement(tag);
if (css) n.style.cssText = css;
if (text != null) n.textContent = text;
return n;
};
const rows = ctx.series;
const total = rows.reduce((sum, r) => sum + (r.value ?? 0), 0);
const prev = totalAt(ctx.dataIndex - 1);
const delta = prev ? Math.round(((total - prev) / prev) * 100) : null;
const lit = (r) => ctx.focusedSeriesIndex < 0 || r.focused;
const card = el('div', 'min-width:212px;font-variant-numeric:tabular-nums');
card.append(el('div',
'font-size:0.68em;letter-spacing:0.1em;text-transform:uppercase;opacity:0.55',
DAYS[ctx.dataIndex] ?? ctx.axisLabel));
const head = el('div', 'display:flex;align-items:baseline;gap:5px;margin:1px 0 9px');
head.append(el('span', 'font-size:1.5em;font-weight:700;line-height:1.05', total.toLocaleString()));
head.append(el('span', 'font-size:0.76em;opacity:0.55', 'visits'));
if (delta !== null) {
const up = delta >= 0;
head.append(el('span',
'margin-inline-start:auto;font-size:0.72em;font-weight:700;padding:1px 7px;border-radius:999px;' +
'background:' + (up ? 'rgba(64,186,128,0.18)' : 'rgba(224,106,106,0.18)') + ';' +
'color:' + (up ? '#40ba80' : '#e06a6a'),
(up ? '▲ ' : '▼ ') + Math.abs(delta) + '%'));
}
card.append(head);
// Composition at a glance — the one view a row list can never give you.
const bar = el('div', 'display:flex;gap:2px;height:6px;margin-bottom:9px');
rows.forEach((r) => {
const seg = el('div',
'flex:' + ((r.value ?? 0) || 0.001) + ';border-radius:999px;background:' + r.color +
';opacity:' + (lit(r) ? '1' : '0.35'));
bar.append(seg);
});
card.append(bar);
rows.forEach((r) => {
const row = el('div',
'display:grid;grid-template-columns:9px 1fr auto 38px;gap:9px;align-items:center;' +
'padding:2.5px 0;opacity:' + (lit(r) ? '1' : '0.45'));
row.append(el('span', 'width:9px;height:9px;border-radius:3px;background:' + r.color));
row.append(el('span', lit(r) && ctx.focusedSeriesIndex >= 0 ? 'font-weight:600' : null, r.label));
row.append(el('span', 'font-weight:700', r.formattedValue));
row.append(el('span', 'opacity:0.55;text-align:end',
total ? Math.round(((r.value ?? 0) / total) * 100) + '%' : ''));
card.append(row);
});
return card;
};
}<script setup lang="ts">
const series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38, 52]
},
{
label: "Organic",
data: [18, 24, 32, 40, 48, 45, 60]
},
{
label: "Paid",
data: [8, 12, 14, 22, 30, 28, 36]
}
];
const xAxis = {
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
};
const tooltipRenderer = (ctx) => {
// Closed over, not passed in: the renderer is handed the hovered x only,
// so anything comparative (yesterday's total) has to come from your data.
const DATA = [
[10, 22, 28, 35, 41, 38, 52],
[18, 24, 32, 40, 48, 45, 60],
[8, 12, 14, 22, 30, 28, 36],
];
const DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const totalAt = (i) => (i < 0 ? null : DATA.reduce((sum, a) => sum + a[i], 0));
const el = (tag, css, text) => {
const n = document.createElement(tag);
if (css) n.style.cssText = css;
if (text != null) n.textContent = text;
return n;
};
const rows = ctx.series;
const total = rows.reduce((sum, r) => sum + (r.value ?? 0), 0);
const prev = totalAt(ctx.dataIndex - 1);
const delta = prev ? Math.round(((total - prev) / prev) * 100) : null;
const lit = (r) => ctx.focusedSeriesIndex < 0 || r.focused;
const card = el('div', 'min-width:212px;font-variant-numeric:tabular-nums');
card.append(el('div',
'font-size:0.68em;letter-spacing:0.1em;text-transform:uppercase;opacity:0.55',
DAYS[ctx.dataIndex] ?? ctx.axisLabel));
const head = el('div', 'display:flex;align-items:baseline;gap:5px;margin:1px 0 9px');
head.append(el('span', 'font-size:1.5em;font-weight:700;line-height:1.05', total.toLocaleString()));
head.append(el('span', 'font-size:0.76em;opacity:0.55', 'visits'));
if (delta !== null) {
const up = delta >= 0;
head.append(el('span',
'margin-inline-start:auto;font-size:0.72em;font-weight:700;padding:1px 7px;border-radius:999px;' +
'background:' + (up ? 'rgba(64,186,128,0.18)' : 'rgba(224,106,106,0.18)') + ';' +
'color:' + (up ? '#40ba80' : '#e06a6a'),
(up ? '▲ ' : '▼ ') + Math.abs(delta) + '%'));
}
card.append(head);
// Composition at a glance — the one view a row list can never give you.
const bar = el('div', 'display:flex;gap:2px;height:6px;margin-bottom:9px');
rows.forEach((r) => {
const seg = el('div',
'flex:' + ((r.value ?? 0) || 0.001) + ';border-radius:999px;background:' + r.color +
';opacity:' + (lit(r) ? '1' : '0.35'));
bar.append(seg);
});
card.append(bar);
rows.forEach((r) => {
const row = el('div',
'display:grid;grid-template-columns:9px 1fr auto 38px;gap:9px;align-items:center;' +
'padding:2.5px 0;opacity:' + (lit(r) ? '1' : '0.45'));
row.append(el('span', 'width:9px;height:9px;border-radius:3px;background:' + r.color));
row.append(el('span', lit(r) && ctx.focusedSeriesIndex >= 0 ? 'font-weight:600' : null, r.label));
row.append(el('span', 'font-weight:700', r.formattedValue));
row.append(el('span', 'opacity:0.55;text-align:end',
total ? Math.round(((r.value ?? 0) / total) * 100) + '%' : ''));
card.append(row);
});
return card;
};
</script>
<template>
<md-area-chart
:series="series"
:xAxis="xAxis"
:tooltipRenderer="tooltipRenderer"
label="Traffic sources"
stack="normal"
tooltip="axis"
legend="bottom"
/>
</template><script lang="ts">
const series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38, 52]
},
{
label: "Organic",
data: [18, 24, 32, 40, 48, 45, 60]
},
{
label: "Paid",
data: [8, 12, 14, 22, 30, 28, 36]
}
];
const xAxis = {
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
};
const tooltipRenderer = (ctx) => {
// Closed over, not passed in: the renderer is handed the hovered x only,
// so anything comparative (yesterday's total) has to come from your data.
const DATA = [
[10, 22, 28, 35, 41, 38, 52],
[18, 24, 32, 40, 48, 45, 60],
[8, 12, 14, 22, 30, 28, 36],
];
const DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const totalAt = (i) => (i < 0 ? null : DATA.reduce((sum, a) => sum + a[i], 0));
const el = (tag, css, text) => {
const n = document.createElement(tag);
if (css) n.style.cssText = css;
if (text != null) n.textContent = text;
return n;
};
const rows = ctx.series;
const total = rows.reduce((sum, r) => sum + (r.value ?? 0), 0);
const prev = totalAt(ctx.dataIndex - 1);
const delta = prev ? Math.round(((total - prev) / prev) * 100) : null;
const lit = (r) => ctx.focusedSeriesIndex < 0 || r.focused;
const card = el('div', 'min-width:212px;font-variant-numeric:tabular-nums');
card.append(el('div',
'font-size:0.68em;letter-spacing:0.1em;text-transform:uppercase;opacity:0.55',
DAYS[ctx.dataIndex] ?? ctx.axisLabel));
const head = el('div', 'display:flex;align-items:baseline;gap:5px;margin:1px 0 9px');
head.append(el('span', 'font-size:1.5em;font-weight:700;line-height:1.05', total.toLocaleString()));
head.append(el('span', 'font-size:0.76em;opacity:0.55', 'visits'));
if (delta !== null) {
const up = delta >= 0;
head.append(el('span',
'margin-inline-start:auto;font-size:0.72em;font-weight:700;padding:1px 7px;border-radius:999px;' +
'background:' + (up ? 'rgba(64,186,128,0.18)' : 'rgba(224,106,106,0.18)') + ';' +
'color:' + (up ? '#40ba80' : '#e06a6a'),
(up ? '▲ ' : '▼ ') + Math.abs(delta) + '%'));
}
card.append(head);
// Composition at a glance — the one view a row list can never give you.
const bar = el('div', 'display:flex;gap:2px;height:6px;margin-bottom:9px');
rows.forEach((r) => {
const seg = el('div',
'flex:' + ((r.value ?? 0) || 0.001) + ';border-radius:999px;background:' + r.color +
';opacity:' + (lit(r) ? '1' : '0.35'));
bar.append(seg);
});
card.append(bar);
rows.forEach((r) => {
const row = el('div',
'display:grid;grid-template-columns:9px 1fr auto 38px;gap:9px;align-items:center;' +
'padding:2.5px 0;opacity:' + (lit(r) ? '1' : '0.45'));
row.append(el('span', 'width:9px;height:9px;border-radius:3px;background:' + r.color));
row.append(el('span', lit(r) && ctx.focusedSeriesIndex >= 0 ? 'font-weight:600' : null, r.label));
row.append(el('span', 'font-weight:700', r.formattedValue));
row.append(el('span', 'opacity:0.55;text-align:end',
total ? Math.round(((r.value ?? 0) / total) * 100) + '%' : ''));
card.append(row);
});
return card;
};
</script>
<md-area-chart
series={series}
xAxis={xAxis}
tooltipRenderer={tooltipRenderer}
label="Traffic sources"
stack="normal"
tooltip="axis"
legend="bottom"
/>The context describes the hovered x position:
| Field | Type | Meaning |
|---|---|---|
dataIndex | number | Index of the hovered x slot |
axisValue | string | number | Date | undefined | Raw x value there |
axisLabel | string | axisValue through the x-axis formatter — the default card’s header |
series | MdChartTooltipSeries[] | Visible series that have a value here, in series order |
missing | MdChartTooltipSeries[] | Visible series with no value here — usually empty, populated for partial or irregular data |
focusedSeriesIndex | number | The emphasised series, or -1 when none is (a single-series chart never emphasises) |
Each entry in series / missing carries seriesIndex, label, color (an
already-resolved CSS colour, so it can go straight into a style), value,
formattedValue, focused, and missing.
value distinguishes two kinds of absence, which is the whole reason missing
is a separate list: null means the series has a datum here and it is null —
measured, no value — while undefined means there is no datum at all, because
the series starts later, ends earlier, or was never sampled at this x.
What you return decides what’s drawn:
| Return | Result |
|---|---|
Node | Inserted as-is — the safest option, and what framework renderers produce |
string | Set as text. Markup is shown, not parsed |
{ unsafeHtml } | Assigned to innerHTML with no sanitising — only for markup you fully control |
undefined | Fall back to the built-in tooltip for this x |
null | Render no tooltip at all for this x |
Returning undefined for the ordinary case and a Node only where you need
something special lets you override one column without reimplementing the card.
The card’s own surface stays the chart’s: restyle it with ::part(tooltip),
which wraps whatever the renderer returns. (The engine creates that part at
runtime, so it isn’t in the parts table below — the table lists only parts
declared in the component’s own markup.)
md-area-chart::part(tooltip) { background: none; box-shadow: none;}The demo’s own code panel carries that whole card, in all five technologies. The part that actually differs between them is how you bind a function prop:
| Technology | Binding | The thing to watch |
|---|---|---|
| HTML | chart.tooltipRenderer = fn | A property assignment — there is no attribute form |
| React | tooltipRenderer={fn} | Declare fn outside the component (or wrap it in useCallback) so its identity is stable between renders |
| Angular | [tooltipRenderer]="fn" | Make it an arrow property, so this stays bound when the chart calls it |
| Vue | :tooltipRenderer="fn" | A plain function, not a ref — Vue would try to unwrap a ref on access |
| Svelte | {tooltipRenderer} | Assigned as a property on the upgraded element |
For a text-only card, returning a string is enough — and it’s safe by construction, since it is set as text:
const tooltipRenderer = (ctx) => { const total = ctx.series.reduce((sum, r) => sum + (r.value ?? 0), 0); return `${ctx.axisLabel} — ${total.toLocaleString()} total`;};| Event | Cancelable | Detail | Fires |
|---|---|---|---|
mdMarkerClick | no | MdChartClickDetail<MdChartSeries> | A data-point marker is clicked |
mdLineClick | no | MdChartClickDetail<MdChartSeries> | A series line is clicked between its points |
mdAreaClick | no | MdChartClickDetail<MdChartSeries> | A filled band is clicked (its own band when stacked) |
mdAxisClick | no | MdChartAxisClickDetail | The plot background — nearest x plus every visible series’ value |
mdLegendClick | no | { seriesIndex, seriesId?, selected } | A legend chip toggles a series |
mdHover | no | MdChartHoverDetail | The hovered data index changes (rAF-throttled) |
mdZoom | no | { startIndex, endIndex, reset } | The zoom window changes (drag, slider, setZoom, resetZoom) |
mdReady | no | void | The engine has mounted and drawn |
The three click events share one payload, so a single handler can serve markers, lines and bands. Every field is present without re-mapping to chart-internal coordinates, which is what makes drill-down and selection straightforward.
type MdChartAxisValue = string | number | Date;type MdChartDataPoint = number | null;
// mdMarkerClick, mdLineClick, mdAreaClickinterface MdChartClickDetail<T = unknown> { seriesIndex: number; // 0-based index within the `series` array seriesId?: string; // the series' id, when one was given dataIndex: number; // index of the point within that series value: MdChartDataPoint; // raw value — null for an empty slot axisValue?: MdChartAxisValue; // original x-axis value at that column series: T; // the series object you passed in nativeEvent: PointerEvent | MouseEvent | KeyboardEvent;}The area chart emits it as MdChartClickDetail<MdChartSeries>, so
detail.series comes back typed as the object you handed to the series prop.
A click on the plot background instead reports the nearest x plus every visible series’ value there — one click gives you the whole column, the way the axis tooltip shows it. Series hidden by a legend toggle are left out.
// one row per visible series at the clicked xinterface MdChartAxisSeriesValue { seriesIndex: number; seriesId?: string; label: string; // as shown in the legend and tooltip dataIndex: number; // -1 when that series has no point there value: MdChartDataPoint;}
// mdAxisClickinterface MdChartAxisClickDetail { dataIndex: number; // index of the nearest x position axisValue?: MdChartAxisValue; seriesValues: MdChartAxisSeriesValue[]; nativeEvent: PointerEvent | MouseEvent | KeyboardEvent;}
// mdHover — rAF-throttledinterface MdChartHoverDetail { dataIndex: number; // the highlighted x position axisValue?: MdChartAxisValue; // one entry under tooltip="item", every series at that x under tooltip="axis" seriesIndices: number[];}This is the chart the code below wires up. Click a band to fire mdAreaClick,
click the plot background for mdAxisClick, toggle a legend chip for
mdLegendClick, and drag the slider under the plot for mdZoom — open your
console to watch the payloads described above arrive.
<md-area-chart label="Storage by type" stack="normal" zoom="slider" legend="bottom"></md-area-chart>
<script type="module">
const series = [
{
label: "Documents",
data: [12, 19, 24, 31]
},
{
label: "Media",
data: [30, 34, 41, 52]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr"]
};
const valueFormatter = (v) => new Intl.NumberFormat('en-US').format(v ?? 0) + ' GB';
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis, valueFormatter });
// One AbortController drops the whole set at once — and makes the wiring
// idempotent, which this demo needs because it is re-run when it scrolls
// back into view.
el.logAbort?.abort();
el.logAbort = new AbortController();
const signal = el.logAbort.signal;
el.addEventListener('mdAreaClick', (e) => console.log('mdAreaClick', e.detail), { signal });
el.addEventListener('mdAxisClick', (e) => console.log('mdAxisClick', e.detail), { signal });
el.addEventListener('mdLegendClick', (e) => console.log('mdLegendClick', e.detail), { signal });
el.addEventListener('mdZoom', (e) => console.log('mdZoom', e.detail.startIndex, e.detail.endIndex, e.detail.reset), { signal });
</script>import { MdAreaChart } from '@awc-ui/react';
// Hoisted out of the component: a new array identity on every render would
// re-run the chart's series watcher and restart its entrance animation.
const series = [
{ label: 'Documents', data: [12, 19, 24, 31] },
{ label: 'Media', data: [30, 34, 41, 52] },
];
const xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr'] };
const valueFormatter = (v: number | null | undefined) =>
new Intl.NumberFormat('en-US').format(v ?? 0) + ' GB';
export function StorageChart() {
// Data goes in as PROPS — the React wrapper sets object values as element
// properties for you, so no ref and no useEffect.
return (
<MdAreaChart
label="Storage by type"
stack="normal"
zoom="slider"
legend="bottom"
series={series}
xAxis={xAxis}
valueFormatter={valueFormatter}
onMdAreaClick={(e) => console.log(e.detail)}
onMdAxisClick={(e) => console.log(e.detail)}
onMdLegendClick={(e) => console.log(e.detail)}
onMdZoom={(e) => console.log(e.detail.startIndex, e.detail.endIndex)}
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-storage-chart',
// [prop] on a custom element is a PROPERTY binding, so the arrays arrive as
// objects rather than stringified attributes — no ViewChild needed.
template: `
<md-area-chart
label="Storage by type"
stack="normal"
zoom="slider"
legend="bottom"
[series]="series"
[xAxis]="xAxis"
[valueFormatter]="valueFormatter"
(mdAreaClick)="onArea($event)"
(mdAxisClick)="onAxis($event)"
(mdLegendClick)="onLegend($event)"
(mdZoom)="onZoom($event)"
></md-area-chart>
`,
})
export class StorageChartComponent {
series = [
{ label: 'Documents', data: [12, 19, 24, 31] },
{ label: 'Media', data: [30, 34, 41, 52] },
];
xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr'] };
// An arrow property, so `this` stays bound when the chart calls it.
valueFormatter = (v: number | null | undefined) =>
new Intl.NumberFormat('en-US').format(v ?? 0) + ' GB';
onArea(e: CustomEvent) { console.log(e.detail); }
onAxis(e: CustomEvent) { console.log(e.detail); }
onLegend(e: CustomEvent) { console.log(e.detail); }
onZoom(e: CustomEvent) { console.log(e.detail.startIndex, e.detail.endIndex); }
}<script setup lang="ts">
const series = [
{ label: 'Documents', data: [12, 19, 24, 31] },
{ label: 'Media', data: [30, 34, 41, 52] },
];
const xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr'] };
// Not a ref: it is a plain function, and ref() would make Vue unwrap it.
const valueFormatter = (v: number | null | undefined) =>
new Intl.NumberFormat('en-US').format(v ?? 0) + ' GB';
function onZoom(e: CustomEvent) {
console.log(e.detail.startIndex, e.detail.endIndex);
}
</script>
<template>
<!-- :prop on a custom element is a property binding, so no ref is needed. -->
<md-area-chart
label="Storage by type"
stack="normal"
zoom="slider"
legend="bottom"
:series="series"
:xAxis="xAxis"
:valueFormatter="valueFormatter"
@mdAreaClick="(e) => console.log(e.detail)"
@mdZoom="onZoom"
/>
</template><script lang="ts">
const series = [
{ label: 'Documents', data: [12, 19, 24, 31] },
{ label: 'Media', data: [30, 34, 41, 52] },
];
const xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr'] };
const valueFormatter = (v: number | null | undefined) =>
new Intl.NumberFormat('en-US').format(v ?? 0) + ' GB';
function onZoom(e: CustomEvent) {
console.log(e.detail.startIndex, e.detail.endIndex);
}
</script>
<!-- Svelte assigns object values as properties on an upgraded custom element. -->
<md-area-chart
label="Storage by type"
stack="normal"
zoom="slider"
legend="bottom"
{series}
{xAxis}
{valueFormatter}
on:mdAreaClick={(e) => console.log(e.detail)}
on:mdZoom={onZoom}
/>| Property | Attribute | Type | Default | Reflects |
|---|---|---|---|---|
label | label | string | '' | — |
subtitle | subtitle | string | undefined | — | — |
titleAlign | title-align | MdChartTitleAlign | 'start' | — |
series | JS only | MdChartSeries[] | [] | — |
xAxis | JS only | MdChartAxis | undefined | — | — |
yAxis | JS only | MdChartAxis | undefined | — | — |
curve | curve | 'linear' | 'smooth' | 'monotone' | 'step' | 'step-before' | 'step-middle' | 'smooth' | — |
stack | stack | MdChartStackMode | 'normal' | — |
connectNulls | connect-nulls | boolean | false | — |
showMarks | show-marks | boolean | false | — |
lineWidth | line-width | number | — | — |
markSize | mark-size | number | — | — |
legend | legend | MdChartLegendPosition | 'none' | 'top-end' | Yes |
tooltip | tooltip | MdChartTooltipTrigger | 'axis' | — |
zoom | zoom | 'none' | 'inside' | 'slider' | 'both' | 'none' | — |
locale | locale | string | '' | — |
valueFormatter | JS only | (value: number | null | undefined) => string | — | — |
tooltipRenderer | JS only | MdChartTooltipRenderer | — | — |
heightProp | height | string | — | — |
noAnimation | no-animation | boolean | false | — |
animation | animation | MdChartAnimation | 'expressive' | — |
animationDuration | animation-duration | number | — | — |
fillOpacity | fill-opacity | number | 0.55 | — |
showLine | show-line | boolean | true | — |
grid | grid | 'none' | 'horizontal' | 'vertical' | 'both' | 'horizontal' | — |
inverted | inverted | boolean | false | — |
seriesLabels | series-labels | boolean | false | — |
summary | summary | string | '' | — |
tableLabels | JS only | { x?: string | — | — |
labelEmpty | label-empty | string | 'No data to display' | — |
loading | loading | boolean | false | — |
loadingLabel | loading-label | string | 'Loading chart…' | — |
labelZoomStart | label-zoom-start | string | 'Zoom range start' | — |
labelZoomEnd | label-zoom-end | string | 'Zoom range end' | — |
axisTicks | axis-ticks | boolean | false | — |
showLabels | show-labels | boolean | false | — |
labelPlot | label-plot | string | 'Chart data. Use the arrow keys to move between points, Home and End for the first and last, Escape to leave.' | — |
labelPoint | label-point | string | '%x%: %values%' | — |
density | density | 0 | -1 | -2 | -3 | -4 | 0 | Yes |
| Method | Parameters |
|---|---|
refreshTheme() | none |
resize() | none |
replay() | none |
toDataURL() | none |
getInstance() | none |
setZoom() | startIndex: number, endIndex: number |
resetZoom() | none |
| Slot | Description |
|---|---|
header | Content for the header row, replacing the default title block |
empty | Replaces the empty-state message when there is no data |
loader | — |
loading | Replaces the built-in spinner while `loading` is set |
footer | Content for the footer row below the plot |
Override on the host element for per-instance theming:
| Property | Description |
|---|---|
--md-area-chart-block-size | Explicit chart height, overriding the aspect ratio (default: auto) |
--md-area-chart-min-block-size | Floor the chart height never drops below (density-aware) |
--md-area-chart-aspect-ratio | Width:height ratio used when no block-size is set (default: 16 / 9) |
--md-area-chart-background | Chart surface fill (default: surface-container-low) |
--md-area-chart-padding | Inset between the host edge and the plot canvas (density-aware) |
--md-area-chart-shape | Corner radius of the chart surface (density-aware) |
--md-area-chart-zoom-size | Height reserved for the zoom slider (default: 28px) |
--md-area-chart-zoom-track-color | Zoom slider track colour (md-slider inactive track) |
--md-area-chart-zoom-window-color | Zoom slider selected-window colour (md-slider active track) |
--md-area-chart-zoom-handle-color | Zoom slider thumb + drag-band edge colour |
--md-area-chart-zoom-band-color | Drag-to-zoom selection fill |
--md-area-chart-empty-color | Empty-state text colour (default: on-surface-variant) |
--md-area-chart-empty-background | Empty-state overlay fill (defaults to the chart background) |
--md-area-chart-empty-font | Empty-state font family (default: body-medium) |
--md-area-chart-empty-font-size | Empty-state font size (default: body-medium, 14px) |
--md-area-chart-empty-icon-size | Size of an icon slotted into the empty state (default: 40px) |
Style internal elements through shadow DOM with ::part():
| Part | Description |
|---|---|
zoom | Wrapper around the zoom slider |
zoom-slider | The zoom range slider itself |
zoom-pan | Grip that drags the whole zoom window along the track |
header | Header row above the plot (hosts the `header` slot) |
canvas | The plot surface the chart draws into |
empty | Empty-state overlay, shown when there is no data |
loading | Loading overlay, shown while `loading` is set |
zoom-band | Live selection band drawn while dragging across the plot |
footer | Footer row below the plot (hosts the `footer` slot) |
zoom-track | Zoom slider's inactive track (forwarded from md-slider) |
zoom-window | Zoom slider's selected window (forwarded from md-slider) |
zoom-handle | Zoom slider's thumbs (forwarded from md-slider) |
label, summary, label-plot
and label-point are the accessible description — they are the chart’s
accessibility, not decoration.Home / End jump to the
ends, and each move is announced through a polite live region.stack="percentage", state that values are
shares.prefers-reduced-motion with no-animation (or animation="none").md-sliders and need
label-zoom-start / label-zoom-end translated.<md-area-chart label="Weekly sessions" summary="Weekly sessions by channel, Monday to Sunday. Direct rises from 10 to 52, organic from 18 to 60." stack="normal" show-marks legend="bottom"></md-area-chart>
<script type="module">
const series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38, 52]
},
{
label: "Organic",
data: [18, 24, 32, 40, 48, 45, 60]
}
];
const xAxis = {
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38, 52]
},
{
label: "Organic",
data: [18, 24, 32, 40, 48, 45, 60]
}
];
const xAxis = {
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
};
export function Chart() {
return (
<MdAreaChart
series={series}
xAxis={xAxis}
label="Weekly sessions"
summary="Weekly sessions by channel, Monday to Sunday. Direct rises from 10 to 52, organic from 18 to 60."
stack="normal"
show-marks
legend="bottom"
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
[xAxis]="xAxis"
label="Weekly sessions"
summary="Weekly sessions by channel, Monday to Sunday. Direct rises from 10 to 52, organic from 18 to 60."
stack="normal"
show-marks
legend="bottom"
></md-area-chart>
`,
})
export class ChartComponent {
series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38, 52]
},
{
label: "Organic",
data: [18, 24, 32, 40, 48, 45, 60]
}
];
xAxis = {
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
};
}<script setup lang="ts">
const series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38, 52]
},
{
label: "Organic",
data: [18, 24, 32, 40, 48, 45, 60]
}
];
const xAxis = {
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
};
</script>
<template>
<md-area-chart
:series="series"
:xAxis="xAxis"
label="Weekly sessions"
summary="Weekly sessions by channel, Monday to Sunday. Direct rises from 10 to 52, organic from 18 to 60."
stack="normal"
show-marks
legend="bottom"
/>
</template><script lang="ts">
const series = [
{
label: "Direct",
data: [10, 22, 28, 35, 41, 38, 52]
},
{
label: "Organic",
data: [18, 24, 32, 40, 48, 45, 60]
}
];
const xAxis = {
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
};
</script>
<md-area-chart
series={series}
xAxis={xAxis}
label="Weekly sessions"
summary="Weekly sessions by channel, Monday to Sunday. Direct rises from 10 to 52, organic from 18 to 60."
stack="normal"
show-marks
legend="bottom"
/>RTL — inside a dir="rtl" parent the whole plot mirrors: the value axis and
its labels move to the right, the category axis runs right-to-left, and the
legend and zoom slider follow. Arrow keys follow the reading direction, so
← advances. dataIndex, axisValue and every event payload are
identical in both directions — only pixels move. See RTL.
<md-area-chart dir="rtl" label="مصادر الزيارات"></md-area-chart><!-- index.html — register the AWC UI elements once -->
<script type="module">
import '@awc-ui/core/define';
</script>
<md-area-chart id="traffic" dir="rtl" label="Traffic sources" stack="normal" legend="bottom"></md-area-chart>
<script type="module">
const chart = document.getElementById('traffic');
chart.xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] };
chart.series = [
{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] },
{ label: 'Organic', data: [18, 24, 32, 40, 48, 45] },
];
</script>import { MdAreaChart } from '@awc-ui/react';
const xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] };
const series = [
{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] },
{ label: 'Organic', data: [18, 24, 32, 40, 48, 45] },
];
export function Demo() {
return (
<>
<MdAreaChart id="traffic"
xAxis={xAxis}
series={series} dir="rtl" label="Traffic sources" stack="normal" legend="bottom"></MdAreaChart>
</>
);
}// app.module.ts — register the AWC UI elements once
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { AwcUiModule } from '@awc-ui/angular';
@NgModule({
imports: [AwcUiModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}
// app.component.ts — the bound values live on the class
import { Component } from '@angular/core';
@Component({
selector: 'app-demo',
templateUrl: './app.component.html',
})
export class DemoComponent {
xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] };
series = [
{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] },
{ label: 'Organic', data: [18, 24, 32, 40, 48, 45] },
];
}
<!-- app.component.html -->
<md-area-chart id="traffic"
[x-axis]="xAxis"
[series]="series" dir="rtl" label="Traffic sources" stack="normal" legend="bottom"></md-area-chart><script setup>
import '@awc-ui/core/define';
const xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] };
const series = [
{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] },
{ label: 'Organic', data: [18, 24, 32, 40, 48, 45] },
];
</script>
<template>
<md-area-chart id="traffic"
:x-axis="xAxis"
:series="series" dir="rtl" label="Traffic sources" stack="normal" legend="bottom"></md-area-chart>
</template><script>
import '@awc-ui/core/define';
const xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] };
const series = [
{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] },
{ label: 'Organic', data: [18, 24, 32, 40, 48, 45] },
];
</script>
<md-area-chart id="traffic"
{xAxis}
{series} dir="rtl" label="Traffic sources" stack="normal" legend="bottom"></md-area-chart>Direction is the engine’s job. series and xAxis stay in reading order —
oldest first — in both directions, and the chart decides which edge index 0
is drawn at. Reversing the arrays yourself to “fix” RTL mirrors twice: the
earliest sample lands back on the left while the axis still runs right-to-left,
so time now reads backwards.
<!-- index.html — register the AWC UI elements once -->
<script type="module">
import '@awc-ui/core/define';
</script>
<md-area-chart id="traffic-rtl" dir="rtl" label="Traffic sources" stack="normal"></md-area-chart>
<script type="module">
const chart = document.getElementById('traffic-rtl');
// Reading order — oldest first — exactly as it would be under dir="ltr".
// The engine mirrors the plot; reversing the arrays yourself mirrors twice.
chart.xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] };
chart.series = [{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] }];
</script>import { MdAreaChart } from '@awc-ui/react';
// Reading order — oldest first — exactly as it would be under dir="ltr".
// The engine mirrors the plot; reversing the arrays yourself mirrors twice.
const xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] };
const series = [{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] }];
export function Demo() {
return (
<>
<MdAreaChart id="traffic-rtl"
xAxis={xAxis}
series={series} dir="rtl" label="Traffic sources" stack="normal"></MdAreaChart>
</>
);
}// app.module.ts — register the AWC UI elements once
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { AwcUiModule } from '@awc-ui/angular';
@NgModule({
imports: [AwcUiModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}
// app.component.ts — the bound values live on the class
import { Component } from '@angular/core';
@Component({
selector: 'app-demo',
templateUrl: './app.component.html',
})
export class DemoComponent {
xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] };
series = [{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] }];
}
<!-- app.component.html -->
<md-area-chart id="traffic-rtl"
[x-axis]="xAxis"
[series]="series" dir="rtl" label="Traffic sources" stack="normal"></md-area-chart><script setup>
import '@awc-ui/core/define';
const xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] };
const series = [{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] }];
</script>
<template>
<md-area-chart id="traffic-rtl"
:x-axis="xAxis"
:series="series" dir="rtl" label="Traffic sources" stack="normal"></md-area-chart>
</template><script>
import '@awc-ui/core/define';
const xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] };
const series = [{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] }];
</script>
<md-area-chart id="traffic-rtl"
{xAxis}
{series} dir="rtl" label="Traffic sources" stack="normal"></md-area-chart>density="-1…-4" tightens the surface padding, the corner radius, the height
floor and the axis / title type sizes. A global data-density on an ancestor
sets the same signal for every chart beneath it; a local density wins over it.
<!-- index.html — register the AWC UI elements once -->
<script type="module">
import '@awc-ui/core/define';
</script>
<md-area-chart id="sessions" density="-2" label="Sessions" stack="normal"></md-area-chart>
<script type="module">
const chart = document.getElementById('sessions');
chart.xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] };
chart.series = [
{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] },
{ label: 'Organic', data: [18, 24, 32, 40, 48, 45] },
];
</script>import { MdAreaChart } from '@awc-ui/react';
const xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] };
const series = [
{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] },
{ label: 'Organic', data: [18, 24, 32, 40, 48, 45] },
];
export function Demo() {
return (
<>
<MdAreaChart id="sessions"
xAxis={xAxis}
series={series} density="-2" label="Sessions" stack="normal"></MdAreaChart>
</>
);
}// app.module.ts — register the AWC UI elements once
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { AwcUiModule } from '@awc-ui/angular';
@NgModule({
imports: [AwcUiModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}
// app.component.ts — the bound values live on the class
import { Component } from '@angular/core';
@Component({
selector: 'app-demo',
templateUrl: './app.component.html',
})
export class DemoComponent {
xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] };
series = [
{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] },
{ label: 'Organic', data: [18, 24, 32, 40, 48, 45] },
];
}
<!-- app.component.html -->
<md-area-chart id="sessions"
[x-axis]="xAxis"
[series]="series" density="-2" label="Sessions" stack="normal"></md-area-chart><script setup>
import '@awc-ui/core/define';
const xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] };
const series = [
{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] },
{ label: 'Organic', data: [18, 24, 32, 40, 48, 45] },
];
</script>
<template>
<md-area-chart id="sessions"
:x-axis="xAxis"
:series="series" density="-2" label="Sessions" stack="normal"></md-area-chart>
</template><script>
import '@awc-ui/core/define';
const xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] };
const series = [
{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] },
{ label: 'Organic', data: [18, 24, 32, 40, 48, 45] },
];
</script>
<md-area-chart id="sessions"
{xAxis}
{series} density="-2" label="Sessions" stack="normal"></md-area-chart>The two are independent signals: direction decides which edge index 0 sits
at, density decides how much room the surface takes around the plot.
<!-- index.html — register the AWC UI elements once -->
<script type="module">
import '@awc-ui/core/define';
</script>
<md-area-chart id="sessions-rtl" dir="rtl" density="-3" label="Sessions" stack="normal"></md-area-chart>
<script type="module">
const chart = document.getElementById('sessions-rtl');
chart.xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] };
chart.series = [
{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] },
{ label: 'Organic', data: [18, 24, 32, 40, 48, 45] },
];
</script>import { MdAreaChart } from '@awc-ui/react';
const xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] };
const series = [
{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] },
{ label: 'Organic', data: [18, 24, 32, 40, 48, 45] },
];
export function Demo() {
return (
<>
<MdAreaChart id="sessions-rtl"
xAxis={xAxis}
series={series} dir="rtl" density="-3" label="Sessions" stack="normal"></MdAreaChart>
</>
);
}// app.module.ts — register the AWC UI elements once
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { AwcUiModule } from '@awc-ui/angular';
@NgModule({
imports: [AwcUiModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}
// app.component.ts — the bound values live on the class
import { Component } from '@angular/core';
@Component({
selector: 'app-demo',
templateUrl: './app.component.html',
})
export class DemoComponent {
xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] };
series = [
{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] },
{ label: 'Organic', data: [18, 24, 32, 40, 48, 45] },
];
}
<!-- app.component.html -->
<md-area-chart id="sessions-rtl"
[x-axis]="xAxis"
[series]="series" dir="rtl" density="-3" label="Sessions" stack="normal"></md-area-chart><script setup>
import '@awc-ui/core/define';
const xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] };
const series = [
{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] },
{ label: 'Organic', data: [18, 24, 32, 40, 48, 45] },
];
</script>
<template>
<md-area-chart id="sessions-rtl"
:x-axis="xAxis"
:series="series" dir="rtl" density="-3" label="Sessions" stack="normal"></md-area-chart>
</template><script>
import '@awc-ui/core/define';
const xAxis = { data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] };
const series = [
{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] },
{ label: 'Organic', data: [18, 24, 32, 40, 48, 45] },
];
</script>
<md-area-chart id="sessions-rtl"
{xAxis}
{series} dir="rtl" density="-3" label="Sessions" stack="normal"></md-area-chart>Density — density="-1…-4" tightens padding and label sizes. See
Density.
i18n — set locale for default number and date formatting (an explicit
valueFormatter always wins over it), and translate every string the component
renders itself:
| Prop | Covers |
|---|---|
summary | Replaces the generated aria-label outright |
tableLabels | The SR data table’s column chrome (a property — it takes an object) |
label-plot, label-point | Keyboard instructions and the live region |
label-empty, loading-label | The empty and loading overlays |
label-zoom-start, label-zoom-end | The zoom slider’s thumbs |
label-point uses the %x% / %values% percent tokens — keep them when
translating; they interpolate the values.
| Custom property | Purpose | Default |
|---|---|---|
--md-area-chart-block-size | Explicit chart height, overriding the aspect ratio | auto |
--md-area-chart-min-block-size | Floor the height never drops below (density-aware) | max(120px, 160px + density × 8px) |
--md-area-chart-aspect-ratio | Width : height used when no block size is set | 16 / 9 |
--md-area-chart-background | Chart surface fill | surface-container-low |
--md-area-chart-padding | Inset between the host edge and the plot (density-aware) | max(8px, 16px + density × 2px) |
--md-area-chart-shape | Corner radius of the surface (density-aware) | max(8px, 16px + density × 2px) |
--md-area-chart-zoom-size | Height reserved for the zoom slider | 28px |
--md-area-chart-zoom-track-color | Zoom slider track | surface-container-highest |
--md-area-chart-zoom-window-color | Zoom slider selected window | secondary-container |
--md-area-chart-zoom-handle-color | Zoom thumbs and the drag-band edge | primary |
--md-area-chart-zoom-band-color | Drag-to-zoom selection fill | 16% primary |
--md-area-chart-empty-color | Empty-state text | on-surface-variant |
--md-area-chart-empty-background | Empty-state overlay fill | the chart background |
--md-area-chart-empty-font / -empty-font-size | Empty-state type | body-medium, 14px |
--md-area-chart-empty-icon-size | Icon slotted into the empty state | 40px |
md-area-chart.dashboard { --md-area-chart-background: var(--md-sys-color-surface-container); --md-area-chart-padding: 16px; --md-area-chart-shape: 16px;}Two more are not namespaced to this component. The axis line and the gridlines are drawn by the shared chart engine, so they read family-wide properties every chart type honours — set them on any ancestor and a whole dashboard retints at once.
| Custom property | Draws | Default |
|---|---|---|
--md-chart-axis-color | The axis line and its ticks | --md-sys-color-outline-variant |
--md-chart-grid-color | The gridlines across the plot | --md-sys-color-outline-variant at 50% (an override is used verbatim) |
<md-area-chart label="Traffic sources" stack="normal" grid="both" legend="bottom" style="--md-area-chart-background: var(--md-sys-color-surface-container); --md-area-chart-padding: 20px; --md-area-chart-shape: 20px;"></md-area-chart>
<script type="module">
const series = [
{
label: "Direct",
color: "#7c6fd6",
data: [10, 22, 28, 35, 41, 38]
},
{
label: "Organic",
color: "tertiary",
data: [18, 24, 32, 40, 48, 45]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
const el = document.querySelector("md-area-chart");
Object.assign(el, { series, xAxis });
</script>import { MdAreaChart } from '@awc-ui/react';
const series = [
{
label: "Direct",
color: "#7c6fd6",
data: [10, 22, 28, 35, 41, 38]
},
{
label: "Organic",
color: "tertiary",
data: [18, 24, 32, 40, 48, 45]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
export function Chart() {
return (
<MdAreaChart
series={series}
xAxis={xAxis}
label="Traffic sources"
stack="normal"
grid="both"
legend="bottom"
/>
);
}import { Component } from '@angular/core';
@Component({
selector: 'app-chart',
template: `
<md-area-chart
[series]="series"
[xAxis]="xAxis"
label="Traffic sources"
stack="normal"
grid="both"
legend="bottom"
></md-area-chart>
`,
})
export class ChartComponent {
series = [
{
label: "Direct",
color: "#7c6fd6",
data: [10, 22, 28, 35, 41, 38]
},
{
label: "Organic",
color: "tertiary",
data: [18, 24, 32, 40, 48, 45]
}
];
xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
}<script setup lang="ts">
const series = [
{
label: "Direct",
color: "#7c6fd6",
data: [10, 22, 28, 35, 41, 38]
},
{
label: "Organic",
color: "tertiary",
data: [18, 24, 32, 40, 48, 45]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
</script>
<template>
<md-area-chart
:series="series"
:xAxis="xAxis"
label="Traffic sources"
stack="normal"
grid="both"
legend="bottom"
/>
</template><script lang="ts">
const series = [
{
label: "Direct",
color: "#7c6fd6",
data: [10, 22, 28, 35, 41, 38]
},
{
label: "Organic",
color: "tertiary",
data: [18, 24, 32, 40, 48, 45]
}
];
const xAxis = {
data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
};
</script>
<md-area-chart
series={series}
xAxis={xAxis}
label="Traffic sources"
stack="normal"
grid="both"
legend="bottom"
/>Adjust gradient saturation per chart with the fill-opacity attribute (0–1). A
band fill — a stacked layer or a range — keeps an even tint, while a fill that
runs to the baseline fades toward the axis so it stops competing with it.
CSS parts — header, canvas, footer, empty, loading, zoom,
zoom-slider, zoom-pan, zoom-band, and three forwarded from the zoom
slider: zoom-track, zoom-window, zoom-handle.
<!-- index.html — register the AWC UI elements once -->
<script type="module">
import '@awc-ui/core/define';
</script>
<md-area-chart id="sessions-parts" class="parted" label="Sessions" stack="normal" zoom="slider"></md-area-chart>
<script type="module">
const chart = document.getElementById('sessions-parts');
chart.xAxis = { data: ['T1', 'T2', 'T3', 'T4', 'T5', 'T6'] };
chart.series = [
{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] },
{ label: 'Organic', data: [18, 24, 32, 40, 48, 45] },
];
</script>import { MdAreaChart } from '@awc-ui/react';
const xAxis = { data: ['T1', 'T2', 'T3', 'T4', 'T5', 'T6'] };
const series = [
{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] },
{ label: 'Organic', data: [18, 24, 32, 40, 48, 45] },
];
export function Demo() {
return (
<>
<MdAreaChart id="sessions-parts"
xAxis={xAxis}
series={series} className="parted" label="Sessions" stack="normal" zoom="slider"></MdAreaChart>
</>
);
}// app.module.ts — register the AWC UI elements once
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { AwcUiModule } from '@awc-ui/angular';
@NgModule({
imports: [AwcUiModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}
// app.component.ts — the bound values live on the class
import { Component } from '@angular/core';
@Component({
selector: 'app-demo',
templateUrl: './app.component.html',
})
export class DemoComponent {
xAxis = { data: ['T1', 'T2', 'T3', 'T4', 'T5', 'T6'] };
series = [
{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] },
{ label: 'Organic', data: [18, 24, 32, 40, 48, 45] },
];
}
<!-- app.component.html -->
<md-area-chart id="sessions-parts"
[x-axis]="xAxis"
[series]="series" class="parted" label="Sessions" stack="normal" zoom="slider"></md-area-chart><script setup>
import '@awc-ui/core/define';
const xAxis = { data: ['T1', 'T2', 'T3', 'T4', 'T5', 'T6'] };
const series = [
{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] },
{ label: 'Organic', data: [18, 24, 32, 40, 48, 45] },
];
</script>
<template>
<md-area-chart id="sessions-parts"
:x-axis="xAxis"
:series="series" class="parted" label="Sessions" stack="normal" zoom="slider"></md-area-chart>
</template><script>
import '@awc-ui/core/define';
const xAxis = { data: ['T1', 'T2', 'T3', 'T4', 'T5', 'T6'] };
const series = [
{ label: 'Direct', data: [10, 22, 28, 35, 41, 38] },
{ label: 'Organic', data: [18, 24, 32, 40, 48, 45] },
];
</script>
<md-area-chart id="sessions-parts"
{xAxis}
{series} class="parted" label="Sessions" stack="normal" zoom="slider"></md-area-chart>The stylesheet that markup pairs with — ::part() reaches into the shadow root
from ordinary CSS, so it works identically in every framework above:
md-area-chart.parted::part(canvas) { outline: 1px dashed rgba(124, 111, 214, 0.55); outline-offset: 4px; }md-area-chart.parted::part(zoom-window) { background: #7c6fd6; }md-area-chart.parted::part(zoom-handle) { background: #4b3fa8; }md-area-chart.parted::part(zoom-pan) { background: rgba(75, 63, 168, 0.18); border-radius: 999px; }md-line-chart ·
md-bar-chart ·
md-pie-chart ·
md-sparkline ·
md-slider
md-area-chartTwo artefacts to give your AI agent so it generates correct UI with this component. The per-component spec answers "how do I use this exact tag?". The main-llm spec answers "which tag should I pick in the first place?".
md-area-chart spec card
Identity · when to use / when NOT · decision cues · behavioural contract · do/don't · anti-patterns · full API. Paste into your agent when you're implementing with this component.
System-prompt preamble · decision matrix · token reference · page recipes · anti-patterns. Paste into the system prompt at the start of a piece of work.