Funnel, heatmap & flows
The non-cartesian charts, each a model and a layout in the framework-free engine both render layers run: funnel and pyramid with conversion rates, the heatmap with colour scales and grid keyboard navigation, the squarified treemap and the sunburst with drill-down and breadcrumbs, the Sankey flow diagram with hover highlighting, and a GeoJSON vector map with choropleth fills, zoom and pan. Every chart is keyboard-operable with live announcements and has a screen-reader table. Pie, polar and the cartesian series are on the overview.
Funnel & pyramid
Stages sized by value: dynamicSlope (widths follow the values) or dynamicHeight (heights follow them, one fixed outline narrowing to a neckWidth). type: 'pyramid' puts the apex on top. Labels go inside or outside with connectors; the tooltip, the announcement and the screen-reader table carry the conversion rates.
| Argument | Value | Share |
|---|---|---|
| Visits | 12,400 | 100% of first stage |
| Sign-ups | 5,120 | 41.3% of first stage |
| Trials | 2,380 | 19.2% of first stage |
| Quotes | 1,060 | 8.5% of first stage |
| Orders | 512 | 4.1% of first stage |
| Argument | Value | Share |
|---|---|---|
| 0–14 | 1,650 | 100% of first stage |
| 15–24 | 1,210 | 73.3% of first stage |
| 25–44 | 2,480 | 150.3% of first stage |
| 45–64 | 2,110 | 127.9% of first stage |
| 65+ | 1,380 | 83.6% of first stage |
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeFunnelChart } from '@oge-ui/charts';
@Component({
selector: 'demo-root',
imports: [OgeFunnelChart],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Stages sized by value. The tooltip, the announcement and the sr
table carry the conversion rates (share of the first and of the
previous stage). Arrow keys walk the stages, Enter selects. -->
<oge-funnel-chart
[dataSource]="pipeline"
argumentField="stage"
valueField="count"
[neckWidth]="0.2"
[label]="{ position: 'outside' }"
title="Sales pipeline"
style="height: 320px"
/>
<!-- a pyramid: apex on top, stage heights follow the values -->
<oge-funnel-chart
[dataSource]="ages"
argumentField="group"
valueField="people"
type="pyramid"
algorithm="dynamicHeight"
[sortData]="false"
title="Population (thousands)"
style="height: 320px"
/>
`,
})
export class Demo {
protected readonly pipeline = [
{ stage: 'Visits', count: 12400 },
{ stage: 'Sign-ups', count: 5120 },
{ stage: 'Trials', count: 2380 },
{ stage: 'Quotes', count: 1060 },
{ stage: 'Orders', count: 512 },
];
protected readonly ages = [
{ group: '0–14', people: 1650 },
{ group: '15–24', people: 1210 },
{ group: '25–44', people: 2480 },
{ group: '45–64', people: 2110 },
{ group: '65+', people: 1380 },
];
}Heatmap
A category × category grid coloured through a colorScale — two or more stops (theme tokens work, blended with color-mix()) or segmented bands — with the colour-scale legend, cell labels where they fit and tooltips. Focus the grid: arrows move the active cell, Home/End, Ctrl+Home/End and PageUp/PageDown jump, Enter selects.
| Argument | 08 | 10 | 12 | 14 | 16 | 18 | 20 | 22 |
|---|---|---|---|---|---|---|---|---|
| Mon | 24 | 35 | 44 | 40 | 41 | 38 | 24 | 17 |
| Tue | 31 | 33 | 42 | 47 | 39 | 36 | 31 | 15 |
| Wed | 29 | 40 | 40 | 45 | 46 | 34 | 29 | 22 |
| Thu | 27 | 38 | 38 | 43 | 44 | 32 | 27 | 20 |
| Fri | 25 | 36 | 45 | 41 | 42 | 39 | 25 | 18 |
| Sat | 26 | 24 | 30 | 34 | 27 | 27 | 26 | 16 |
| Sun | 24 | 22 | 28 | 32 | 25 | 25 | 24 | 14 |
| Argument | Q1 | Q2 | Q3 | Q4 |
|---|---|---|---|---|
| North | -6 | +1 | -5 | +2 |
| East | -1 | +6 | 0 | -6 |
| South | +4 | -2 | +5 | -1 |
| West | -4 | +3 | -3 | +4 |
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeHeatmap } from '@oge-ui/charts';
import type { OgeChartColorScale } from '@oge-ui/charts';
@Component({
selector: 'demo-root',
imports: [OgeHeatmap],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Category × category cells coloured through a colour scale — theme
tokens or any CSS colours, blended with color-mix(). Arrow keys move
the active cell (Home/End, Ctrl+Home/End, PageUp/PageDown too); screen
readers get a real two-dimensional table. -->
<oge-heatmap
[dataSource]="tickets"
xField="hour"
yField="day"
valueField="tickets"
title="Support tickets by hour"
style="height: 340px"
/>
<!-- three stops make a diverging scale -->
<oge-heatmap
[dataSource]="change"
xField="quarter"
yField="region"
valueField="change"
[colorScale]="diverging"
[valueFormat]="signed"
title="Change vs last year (%)"
style="height: 260px"
/>
`,
})
export class Demo {
protected readonly tickets = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].flatMap((day, d) =>
['08', '10', '12', '14', '16', '18', '20', '22'].map((hour, h) => ({
day,
hour,
tickets: Math.round(14 + 26 * Math.sin(((h + 1) / 8) * Math.PI) * (d < 5 ? 1 : 0.45) + ((d * 7 + h * 3) % 9)),
})),
);
protected readonly change = ['North', 'East', 'South', 'West'].flatMap((region, r) =>
['Q1', 'Q2', 'Q3', 'Q4'].map((quarter, q) => ({ region, quarter, change: ((r * 5 + q * 7) % 13) - 6 })),
);
protected readonly diverging: OgeChartColorScale = {
min: -6,
max: 6,
colors: ['var(--oge-danger)', 'var(--oge-bg)', 'var(--oge-success)'],
};
protected readonly signed = (value: number) => (value > 0 ? `+${value}` : String(value));
}Treemap
Nested rectangles sized by value: squarified tiling (or sliceAndDice), group headers, fitted labels and palette colours per top-level group, lightened by depth — or colorScale by value. Click a group or press Enter to drill in; the breadcrumb or Escape goes back. [(rootKey)] is the drill state.
| Argument | Value | Share |
|---|---|---|
| Americas | 657 | 40.4% |
| Americas / United States | 412 | 62.7% |
| Americas / Canada | 96 | 14.6% |
| Americas / Brazil | 88 | 13.4% |
| Americas / Mexico | 61 | 9.3% |
| Europe | 577 | 35.5% |
| Europe / Germany | 184 | 31.9% |
| Europe / United Kingdom | 151 | 26.2% |
| Europe / France | 120 | 20.8% |
| Europe / Spain | 64 | 11.1% |
| Europe / Türkiye | 58 | 10.1% |
| Asia Pacific | 344 | 21.2% |
| Asia Pacific / Japan | 163 | 47.4% |
| Asia Pacific / Australia | 77 | 22.4% |
| Asia Pacific / India | 70 | 20.3% |
| Asia Pacific / Singapore | 34 | 9.9% |
| Africa | 47 | 2.9% |
| Africa / South Africa | 29 | 61.7% |
| Africa / Egypt | 18 | 38.3% |
Current root: all markets
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeTreemap } from '@oge-ui/charts';
@Component({
selector: 'demo-root',
imports: [OgeTreemap],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Squarified tiles under group headers. Click a group (or press Enter)
to drill into it; the breadcrumb or Escape goes back. [(rootKey)] is
the drill state. -->
<oge-treemap
[dataSource]="markets"
valueField="sales"
[(rootKey)]="root"
title="Sales by market (M$)"
style="height: 380px"
/>
<p>Current root: {{ root() || 'all markets' }}</p>
`,
})
export class Demo {
protected readonly root = signal('');
protected readonly markets = [
{
name: 'Americas',
items: [
{ name: 'United States', sales: 412 },
{ name: 'Canada', sales: 96 },
{ name: 'Brazil', sales: 88 },
{ name: 'Mexico', sales: 61 },
],
},
{
name: 'Europe',
items: [
{ name: 'Germany', sales: 184 },
{ name: 'United Kingdom', sales: 151 },
{ name: 'France', sales: 120 },
{ name: 'Spain', sales: 64 },
{ name: 'Türkiye', sales: 58 },
],
},
{
name: 'Asia Pacific',
items: [
{ name: 'Japan', sales: 163 },
{ name: 'Australia', sales: 77 },
{ name: 'India', sales: 70 },
{ name: 'Singapore', sales: 34 },
],
},
{ name: 'Africa', items: [{ name: 'South Africa', sales: 29 }, { name: 'Egypt', sales: 18 }] },
];
}Sunburst
The same hierarchy model as rings around a centre — here from flat data (idField + parentField). Segments are sized by value inside their parent's angle; click a branch to re-root on it and the centre (or Escape) to go back up. Arrow keys walk siblings, children and parents.
| Argument | Value | Share |
|---|---|---|
| Engineering | 45 | 52.3% |
| Engineering / Web | 18 | 40% |
| Engineering / Mobile | 11 | 24.4% |
| Engineering / Platform | 16 | 35.6% |
| Engineering / Platform / Infrastructure | 9 | 56.3% |
| Engineering / Platform / Data | 7 | 43.8% |
| Sales | 26 | 30.2% |
| Sales / EMEA | 12 | 46.2% |
| Sales / Americas | 14 | 53.8% |
| Operations | 15 | 17.4% |
| Operations / Support | 10 | 66.7% |
| Operations / Finance | 5 | 33.3% |
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeSunburstChart } from '@oge-ui/charts';
@Component({
selector: 'demo-root',
imports: [OgeSunburstChart],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Flat data works too: idField + parentField build the hierarchy.
One ring per level; click a branch to re-root on it, click the centre
to go back up. -->
<oge-sunburst-chart
[dataSource]="org"
parentField="parent"
valueField="people"
title="Headcount"
style="height: 380px"
/>
`,
})
export class Demo {
protected readonly org = [
{ id: 'eng', parent: null, name: 'Engineering' },
{ id: 'web', parent: 'eng', name: 'Web', people: 18 },
{ id: 'mobile', parent: 'eng', name: 'Mobile', people: 11 },
{ id: 'platform', parent: 'eng', name: 'Platform' },
{ id: 'infra', parent: 'platform', name: 'Infrastructure', people: 9 },
{ id: 'data', parent: 'platform', name: 'Data', people: 7 },
{ id: 'sales', parent: null, name: 'Sales' },
{ id: 'emea', parent: 'sales', name: 'EMEA', people: 12 },
{ id: 'amer', parent: 'sales', name: 'Americas', people: 14 },
{ id: 'ops', parent: null, name: 'Operations' },
{ id: 'support', parent: 'ops', name: 'Support', people: 10 },
{ id: 'finance', parent: 'ops', name: 'Finance', people: 5 },
];
}Sankey diagram
Flows between nodes, one item per link: columns from the longest path, node heights from the throughput, positions relaxed so bands cross little. Hover a node to light up its links (or a link to light its two ends); the keyboard walks nodes Up/Down within a column and Left/Right across. Cycles are tolerated and the flow mirrors in RTL.
| Source | Target | Value |
|---|---|---|
| Solar | Electricity | 42 |
| Wind | Electricity | 58 |
| Gas | Electricity | 64 |
| Gas | Heat | 38 |
| Coal | Electricity | 27 |
| Oil | Transport | 92 |
| Electricity | Homes | 71 |
| Electricity | Industry | 84 |
| Electricity | Transport | 18 |
| Electricity | Losses | 18 |
| Heat | Homes | 26 |
| Heat | Industry | 12 |
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeSankeyChart } from '@oge-ui/charts';
import type { OgeSankeyNode } from '@oge-ui/charts';
@Component({
selector: 'demo-root',
imports: [OgeSankeyChart],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- One item per flow. Columns come from the longest path, node heights
from the throughput; hovering a node lights up its links. Keyboard:
Up/Down within a column, Left/Right across columns. -->
<oge-sankey-chart
[dataSource]="energy"
[nodes]="nodes"
title="Energy flows (TWh)"
style="height: 380px"
/>
`,
})
export class Demo {
protected readonly energy = [
{ source: 'Solar', target: 'Electricity', value: 42 },
{ source: 'Wind', target: 'Electricity', value: 58 },
{ source: 'Gas', target: 'Electricity', value: 64 },
{ source: 'Gas', target: 'Heat', value: 38 },
{ source: 'Coal', target: 'Electricity', value: 27 },
{ source: 'Oil', target: 'Transport', value: 92 },
{ source: 'Electricity', target: 'Homes', value: 71 },
{ source: 'Electricity', target: 'Industry', value: 84 },
{ source: 'Electricity', target: 'Transport', value: 18 },
{ source: 'Electricity', target: 'Losses', value: 18 },
{ source: 'Heat', target: 'Homes', value: 26 },
{ source: 'Heat', target: 'Industry', value: 12 },
];
// optional per-node settings: a label or a colour
protected readonly nodes: OgeSankeyNode[] = [{ id: 'Losses', color: 'var(--oge-muted-color)' }];
}Vector map
A choropleth over any GeoJSON FeatureCollection: equirectangular or Mercator projection fitted to the box, fills from a colour scale joined by key, labels where they fit. Wheel, pinch or the buttons zoom and dragging pans; focused, the arrow keys move to the nearest region in that direction, + and - zoom, Shift+arrows pan and Enter selects. This fictional country's borders are generated, so neighbours share their edges.
| Argument | Value |
|---|---|
| Northmark | 20 |
| Fjordale | 57 |
| Highcrest | 94 |
| Eastwatch | 51 |
| Westmoor | 88 |
| Midvale | 45 |
| Riverbend | no data |
| Stonegate | 39 |
| Southreach | 76 |
| Lakeshire | 33 |
| Ambergrove | 70 |
| Sunholm | 27 |
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeVectorMap } from '@oge-ui/charts';
import type { OgeGeoJsonFeatureCollection } from '@oge-ui/charts';
@Component({
selector: 'demo-root',
imports: [OgeVectorMap],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Any GeoJSON FeatureCollection of polygons — no tiles, no projection
library. Values join the regions by key (feature.id, then
properties.name). Wheel / pinch / the buttons zoom, drag pans; arrows
move to the nearest region, + and - zoom, Shift+arrows pan. -->
<oge-vector-map
[geoJson]="provinces"
[dataSource]="sales"
keyField="province"
valueField="revenue"
projection="mercator"
title="Revenue by province"
style="height: 420px"
/>
`,
})
export class Demo {
// load your own: this.http.get<OgeGeoJsonFeatureCollection>('/assets/regions.geo.json')
protected readonly provinces: OgeGeoJsonFeatureCollection = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
id: 'Northmark',
properties: { name: 'Northmark' },
geometry: { type: 'Polygon', coordinates: [[[6, 56], [9.2, 56], [9.6, 53.4], [6, 53.4], [6, 56]]] },
},
{
type: 'Feature',
id: 'Fjordale',
properties: { name: 'Fjordale' },
geometry: { type: 'Polygon', coordinates: [[[9.2, 56], [12.4, 56], [12.4, 53.4], [9.6, 53.4], [9.2, 56]]] },
},
{
type: 'Feature',
id: 'Midvale',
properties: { name: 'Midvale' },
geometry: { type: 'Polygon', coordinates: [[[6, 53.4], [12.4, 53.4], [12.4, 50.8], [6, 50.8], [6, 53.4]]] },
},
],
};
protected readonly sales = [
{ province: 'Northmark', revenue: 48 },
{ province: 'Fjordale', revenue: 85 },
{ province: 'Midvale', revenue: 22 },
];
}