Axes & layout
Layout depth of the cartesian chart: rotated charts (horizontal bars, top-down lines), panes stacked over one argument axis, value- and argument-axis constant lines and strips, axis breaks, fixed tick intervals with minor ticks and label templates, right-to-left layout, two-finger pinch zoom on touch screens and a first-render draw-in animation. Everything is computed by the framework-free engine both render layers share; the series geometry stays the same and only the frame turns, so every series type rotates. The basics are on the overview.
Rotated bars
[rotated]="true" swaps the axes: the argument axis runs down the left and values grow to the right. Every series type follows — these stackedBar series become horizontal stacked bars — and so do the tooltip, the crosshair, zoom and the keyboard (Up/Down walk the categories, Left/Right the series).
| Argument | Online | Retail |
|---|---|---|
| North | 420 | 310 |
| East | 380 | 260 |
| South | 290 | 340 |
| West | 510 | 220 |
| Central | 240 | 180 |
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeChart } from '@oge-ui/charts';
import type { OgeChartSeriesInput } from '@oge-ui/charts';
@Component({
selector: 'demo-root',
imports: [OgeChart],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- rotated swaps the axes: the argument axis runs down the left,
values grow to the right. Every series type follows — these
stackedBar series become horizontal stacked bars — and so do the
tooltip, crosshair, zoom and the keyboard (Up/Down walk the
categories, Left/Right the series). -->
<oge-chart
[dataSource]="data"
[series]="series"
[commonSeries]="{ argumentField: 'region' }"
[rotated]="true"
[tooltip]="{ shared: true }"
[valueAxis]="{ title: 'Units sold', abbreviate: false }"
style="height: 360px"
/>
`,
})
export class Demo {
protected readonly data = [
{ region: 'North', online: 420, retail: 310 },
{ region: 'East', online: 380, retail: 260 },
{ region: 'South', online: 290, retail: 340 },
{ region: 'West', online: 510, retail: 220 },
{ region: 'Central', online: 240, retail: 180 },
];
protected readonly series: OgeChartSeriesInput[] = [
{ type: 'stackedBar', valueField: 'online', name: 'Online' },
{ type: 'stackedBar', valueField: 'retail', name: 'Retail' },
];
}Constant lines & strips
constantLines draw a threshold or target at an axis value with a label — inside beside the line, or position: 'outside' past the plot edge — and strips shade a band. Both work on the value and on the argument axis, take any colour (theme tokens included) and keep their labels clear of the plot edges.
| Argument | p95 latency (ms) |
|---|---|
| Jan | 180 |
| Feb | 223 |
| Mar | 253 |
| Apr | 261 |
| May | 246 |
| Jun | 214 |
| Jul | 178 |
| Aug | 150 |
| Sep | 142 |
| Oct | 158 |
| Nov | 193 |
| Dec | 237 |
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeChart } from '@oge-ui/charts';
import type { OgeChartAxisOptions, OgeChartSeriesInput } from '@oge-ui/charts';
@Component({
selector: 'demo-root',
imports: [OgeChart],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- constantLines draw a threshold at an axis value with a label
(inside next to the line, or outside past the plot edge); strips shade
a value band. Both work on the value AND the argument axis; labels
never run off the plot. -->
<oge-chart
[dataSource]="data"
[series]="series"
[valueAxis]="valueAxis"
[argumentAxis]="argumentAxis"
style="height: 360px"
/>
`,
})
export class Demo {
protected readonly data = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'].map(
(month, i) => ({ month, latency: 180 + Math.round(Math.sin(i / 1.7) * 70) + i * 4 }),
);
protected readonly series: OgeChartSeriesInput[] = [
{ type: 'spline', argumentField: 'month', valueField: 'latency', name: 'p95 latency (ms)' },
];
protected readonly valueAxis: OgeChartAxisOptions = {
constantLines: [
{ value: 250, label: 'SLO 250 ms', color: 'var(--oge-danger)', width: 2 },
{ value: 150, label: 'Goal', dash: 'dot', position: 'outside' },
],
strips: [{ start: 100, end: 150, label: 'Comfort zone' }],
};
protected readonly argumentAxis: OgeChartAxisOptions = {
constantLines: [{ value: 'Jun', label: 'Release 2.0', color: 'var(--oge-accent)', dash: 'solid' }],
strips: [{ start: 'Sep', end: 'Oct', label: 'Freeze' }],
};
}Panes (price + volume)
panes stack plot areas over one shared argument axis; series and value axes pick theirs by pane name, and each pane gets its own height ratio. The crosshair runs through every pane, zoom and pan move them together, and the range selector's periods (1M / 3M / 6M / YTD / 1Y / All) set the same window.
| Argument | OGE | Volume |
|---|---|---|
| Sep 1, 2025 | O 100 H 102 L 98 C 100 | 4,200 |
| Sep 2, 2025 | O 101.405 H 106.38 L 99.405 C 104.38 | 4,068 |
| Sep 3, 2025 | O 102.803 H 104.803 L 100.036 C 102.036 | 3,686 |
| Sep 4, 2025 | O 104.188 H 106.188 L 99.41 C 101.41 | 3,097 |
| Sep 5, 2025 | O 105.553 H 109.036 L 103.553 C 107.036 | 2,365 |
| Sep 6, 2025 | O 106.893 H 111.288 L 104.893 C 109.288 | 2,030 |
| Sep 7, 2025 | O 108.2 H 110.2 L 104.101 C 106.101 | 2,799 |
| Sep 8, 2025 | O 109.47 H 111.47 L 105.615 C 107.615 | 3,458 |
| Sep 9, 2025 | O 110.695 H 115.273 L 108.695 C 113.273 | 3,934 |
| Sep 10, 2025 | O 111.871 H 115.061 L 109.871 C 113.061 | 4,176 |
| Sep 11, 2025 | O 112.991 H 114.991 L 108.107 C 110.107 | 4,156 |
| Sep 12, 2025 | O 114.052 H 116.052 L 111.605 C 113.605 | 3,877 |
| Sep 13, 2025 | O 115.048 H 120.047 L 113.048 C 118.047 | 3,369 |
| Sep 14, 2025 | O 115.974 H 117.974 L 113.648 C 115.648 | 2,688 |
| Sep 15, 2025 | O 116.826 H 118.826 L 111.911 C 113.911 | 1,910 |
| Sep 16, 2025 | O 117.602 H 120.679 L 115.602 C 118.679 | 2,481 |
| Sep 17, 2025 | O 118.297 H 122.935 L 116.297 C 120.935 | 3,196 |
| Sep 18, 2025 | O 118.908 H 120.908 L 115.151 C 117.151 | 3,758 |
| Sep 19, 2025 | O 119.433 H 121.433 L 115.248 C 117.248 | 4,104 |
| Sep 20, 2025 | O 119.871 H 124.191 L 117.871 C 122.191 | 4,197 |
| Sep 21, 2025 | O 120.218 H 123.806 L 118.218 C 121.806 | 4,026 |
| Sep 22, 2025 | O 120.475 H 122.475 L 115.746 C 117.746 | 3,609 |
| Sep 23, 2025 | O 120.64 H 122.64 L 117.756 C 119.756 | 2,994 |
| Sep 24, 2025 | O 120.713 H 125.67 L 118.713 C 123.67 | 2,247 |
| Sep 25, 2025 | O 120.695 H 122.817 L 118.695 C 120.817 | 2,149 |
| Sep 26, 2025 | O 120.586 H 122.586 L 115.598 C 117.598 | 2,907 |
| Sep 27, 2025 | O 120.387 H 123.035 L 118.387 C 121.035 | 3,543 |
| Sep 28, 2025 | O 120.1 H 124.921 L 118.1 C 122.921 | 3,987 |
| Sep 29, 2025 | O 119.727 H 121.727 L 116.352 C 118.352 | 4,190 |
| Sep 30, 2025 | O 119.271 H 121.271 L 114.804 C 116.804 | 4,130 |
| Oct 1, 2025 | O 118.734 H 122.745 L 116.734 C 120.745 | 3,814 |
| Oct 2, 2025 | O 118.12 H 122.069 L 116.12 C 120.069 | 3,276 |
| Oct 3, 2025 | O 117.433 H 119.433 L 112.92 C 114.92 | 2,575 |
| Oct 4, 2025 | O 116.676 H 118.676 L 113.375 C 115.375 | 1,811 |
| Oct 5, 2025 | O 115.854 H 120.702 L 113.854 C 118.702 | 2,595 |
| Oct 6, 2025 | O 114.972 H 117.54 L 112.972 C 115.54 | 3,292 |
| Oct 7, 2025 | O 114.036 H 116.036 L 109.041 C 111.041 | 3,825 |
| Oct 8, 2025 | O 113.05 H 115.254 L 111.05 C 113.254 | 4,135 |
| Oct 9, 2025 | O 112.02 H 116.961 L 110.02 C 114.961 | 4,188 |
| Oct 10, 2025 | O 110.951 H 112.951 L 107.989 C 109.989 | 3,978 |
| Oct 11, 2025 | O 109.851 H 111.851 L 105.158 C 107.158 | 3,528 |
| Oct 12, 2025 | O 108.725 H 112.382 L 106.725 C 110.382 | 2,888 |
| Oct 13, 2025 | O 107.58 H 111.847 L 105.58 C 109.847 | 2,128 |
| Oct 14, 2025 | O 106.422 H 108.422 L 102.181 C 104.181 | 2,268 |
| Oct 15, 2025 | O 105.257 H 107.257 L 101.568 C 103.568 | 3,012 |
| Oct 16, 2025 | O 104.093 H 108.769 L 102.093 C 106.769 | 3,623 |
| Oct 17, 2025 | O 102.935 H 105.935 L 100.935 C 103.935 | 4,034 |
| Oct 18, 2025 | O 101.79 H 103.79 L 96.856 C 98.856 | 4,198 |
| Oct 19, 2025 | O 100.665 H 102.665 L 98.421 C 100.421 | 4,098 |
| Oct 20, 2025 | O 99.566 H 104.562 L 97.566 C 102.562 | 3,746 |
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeChart, OgeRangeSelector } from '@oge-ui/charts';
import type { OgeChartAxisOptions, OgeChartPane, OgeChartPeriod, OgeChartRange, OgeChartSeriesInput } from '@oge-ui/charts';
@Component({
selector: 'demo-root',
imports: [OgeChart, OgeRangeSelector],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- panes stack plot areas over ONE shared argument axis: series and
value axes pick theirs by name. The crosshair runs through every pane
(its horizontal line stays in the hovered one) and zoom/pan move all of
them. The range selector's period buttons set the same window. -->
<oge-chart
[dataSource]="data"
[series]="series"
[panes]="panes"
[valueAxis]="valueAxes"
[crosshair]="{ horizontal: true }"
[tooltip]="{ shared: true }"
[(visualRange)]="range"
zoomEnabled="both"
[panEnabled]="true"
style="height: 420px"
/>
<oge-range-selector
[dataSource]="data"
[series]="navigator"
[periods]="periods"
[(value)]="range"
/>
`,
})
export class Demo {
protected readonly data = Array.from({ length: 260 }, (_, i) => {
const open = 100 + Math.sin(i / 14) * 18 + i * 0.12;
const close = open + Math.sin(i * 1.7) * 3;
return {
day: new Date(2025, 8, 1 + i),
open,
close,
high: Math.max(open, close) + 2,
low: Math.min(open, close) - 2,
volume: 1800 + Math.round(Math.abs(Math.cos(i / 3)) * 2400),
};
});
protected readonly panes: OgeChartPane[] = [
{ name: 'price', height: 3 },
{ name: 'volume', height: 1 },
];
protected readonly valueAxes: OgeChartAxisOptions[] = [
{ pane: 'price', title: 'Price' },
{ pane: 'volume', title: 'Volume' },
];
protected readonly series: OgeChartSeriesInput[] = [
{
type: 'candlestick',
argumentField: 'day',
openField: 'open',
highField: 'high',
lowField: 'low',
closeField: 'close',
name: 'OGE',
pane: 'price',
},
{ type: 'bar', argumentField: 'day', valueField: 'volume', name: 'Volume', pane: 'volume', opacity: 0.6 },
];
protected readonly navigator: OgeChartSeriesInput[] = [
{ type: 'area', argumentField: 'day', valueField: 'close', name: 'Close' },
];
protected readonly periods: OgeChartPeriod[] = ['1M', '3M', '6M', 'YTD', '1Y', 'All'];
protected readonly range = signal<OgeChartRange | null>(null);
}Axis breaks
One outlier would flatten every other bar. breaks skip value ranges on a linear value axis: the gap is drawn with a zig-zag marker, no tick lands inside it and bars crossing it show the cut.
| Argument | Open tickets |
|---|---|
| Search | 84 |
| Billing | 112 |
| Platform | 1,010 |
| Mobile | 96 |
| Data | 61 |
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeChart } from '@oge-ui/charts';
import type { OgeChartSeriesInput } from '@oge-ui/charts';
@Component({
selector: 'demo-root',
imports: [OgeChart],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- One outlier would flatten every other bar. breaks skip a value
range on a linear value axis — the gap is drawn with a zig-zag marker
and no tick lands inside it. -->
<oge-chart
[dataSource]="data"
[series]="series"
[valueAxis]="{ breaks: [{ start: 140, end: 940 }] }"
style="height: 340px"
/>
`,
})
export class Demo {
protected readonly data = [
{ team: 'Search', tickets: 84 },
{ team: 'Billing', tickets: 112 },
{ team: 'Platform', tickets: 1010 },
{ team: 'Mobile', tickets: 96 },
{ team: 'Data', tickets: 61 },
];
protected readonly series: OgeChartSeriesInput[] = [
{ type: 'bar', argumentField: 'team', valueField: 'tickets', name: 'Open tickets', showLabels: true },
];
}Ticks & labels
tickInterval fixes the tick distance — axis units, or a calendar interval such as { weeks: 1 } on time axes; minorTicks adds ticks and grid lines between; allowDecimals: false keeps whole numbers. label.format takes Intl options or a function, label.template wraps the text and label.overlap picks rotate / stagger / hide / skip.
| Argument | Daily high |
|---|---|
| Mar 2, 2026 | 9 |
| Mar 3, 2026 | 10 |
| Mar 4, 2026 | 10 |
| Mar 5, 2026 | 11 |
| Mar 6, 2026 | 12 |
| Mar 7, 2026 | 12 |
| Mar 8, 2026 | 13 |
| Mar 9, 2026 | 13 |
| Mar 10, 2026 | 14 |
| Mar 11, 2026 | 14 |
| Mar 12, 2026 | 15 |
| Mar 13, 2026 | 15 |
| Mar 14, 2026 | 15 |
| Mar 15, 2026 | 16 |
| Mar 16, 2026 | 16 |
| Mar 17, 2026 | 16 |
| Mar 18, 2026 | 16 |
| Mar 19, 2026 | 16 |
| Mar 20, 2026 | 16 |
| Mar 21, 2026 | 16 |
| Mar 22, 2026 | 15 |
| Mar 23, 2026 | 15 |
| Mar 24, 2026 | 15 |
| Mar 25, 2026 | 15 |
| Mar 26, 2026 | 14 |
| Mar 27, 2026 | 14 |
| Mar 28, 2026 | 14 |
| Mar 29, 2026 | 13 |
| Mar 30, 2026 | 13 |
| Mar 31, 2026 | 12 |
| Apr 1, 2026 | 12 |
| Apr 2, 2026 | 11 |
| Apr 3, 2026 | 11 |
| Apr 4, 2026 | 11 |
| Apr 5, 2026 | 10 |
| Apr 6, 2026 | 10 |
| Apr 7, 2026 | 10 |
| Apr 8, 2026 | 10 |
| Apr 9, 2026 | 9 |
| Apr 10, 2026 | 9 |
| Apr 11, 2026 | 9 |
| Apr 12, 2026 | 9 |
| Apr 13, 2026 | 9 |
| Apr 14, 2026 | 9 |
| Apr 15, 2026 | 10 |
| Apr 16, 2026 | 10 |
| Apr 17, 2026 | 10 |
| Apr 18, 2026 | 11 |
| Apr 19, 2026 | 11 |
| Apr 20, 2026 | 11 |
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeChart } from '@oge-ui/charts';
import type { OgeChartAxisOptions, OgeChartSeriesInput } from '@oge-ui/charts';
@Component({
selector: 'demo-root',
imports: [OgeChart],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- tickInterval fixes the tick distance — a number in axis units or a
calendar interval such as { weeks: 1 }; minorTicks adds the ticks (and
grid lines) in between; allowDecimals: false keeps whole numbers.
label.format takes Intl options (or a function), label.template wraps
the text and label.overlap picks rotate / stagger / hide / skip. -->
<oge-chart
[dataSource]="data"
[series]="series"
[argumentAxis]="argumentAxis"
[valueAxis]="valueAxis"
style="height: 340px"
/>
`,
})
export class Demo {
protected readonly data = Array.from({ length: 70 }, (_, i) => ({
day: new Date(2026, 2, 2 + i),
temperature: 9 + Math.round(Math.sin(i / 9) * 5 + i / 8),
}));
protected readonly series: OgeChartSeriesInput[] = [
{ type: 'stepLine', argumentField: 'day', valueField: 'temperature', name: 'Daily high' },
];
protected readonly argumentAxis: OgeChartAxisOptions = {
tickInterval: { weeks: 1 },
minorTicks: { count: 6 },
grid: true,
label: { format: { day: 'numeric', month: 'short' }, overlap: 'stagger' },
};
protected readonly valueAxis: OgeChartAxisOptions = {
allowDecimals: false,
tickInterval: 2,
label: { template: '{value} °C' },
};
}RTL & touch
rtlEnabled (unset follows the page's dir) mirrors the argument axis, moves the value axis to the right, flips the legend and the tooltip side and swaps the Left/Right arrow keys. On touch screens two fingers pinch-zoom and pan the plot; one finger pans when panEnabled is on, otherwise it drag-zooms.
| Argument | Visits | Orders |
|---|---|---|
| 1 | 400 | 80 |
| 2 | 447 | 81 |
| 3 | 490 | 82 |
| 4 | 525 | 81 |
| 5 | 549 | 79 |
| 6 | 559 | 76 |
| 7 | 557 | 73 |
| 8 | 543 | 70 |
| 9 | 519 | 68 |
| 10 | 489 | 65 |
| 11 | 457 | 64 |
| 12 | 428 | 64 |
| 13 | 405 | 64 |
| 14 | 393 | 66 |
| 15 | 392 | 69 |
| 16 | 405 | 74 |
| 17 | 430 | 79 |
| 18 | 467 | 85 |
| 19 | 510 | 92 |
| 20 | 558 | 99 |
| 21 | 605 | 106 |
| 22 | 647 | 112 |
| 23 | 680 | 118 |
| 24 | 702 | 123 |
| 25 | 711 | 127 |
| 26 | 706 | 130 |
| 27 | 691 | 132 |
| 28 | 665 | 132 |
| 29 | 635 | 131 |
| 30 | 603 | 129 |
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeChart } from '@oge-ui/charts';
import type { OgeChartSeriesInput } from '@oge-ui/charts';
@Component({
selector: 'demo-root',
imports: [OgeChart],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- rtlEnabled (unset = follow the page's dir) mirrors the argument
axis, moves the value axis to the right, flips the legend and the
tooltip side and swaps the Left/Right arrow keys. On touch screens two
fingers pinch-zoom and pan the plot; one finger pans when panEnabled is
on, otherwise it drag-zooms. -->
<oge-chart
[dataSource]="data"
[series]="series"
[rtlEnabled]="true"
zoomEnabled="both"
[panEnabled]="true"
[legend]="{ position: 'top' }"
style="height: 340px"
/>
`,
})
export class Demo {
protected readonly data = Array.from({ length: 30 }, (_, i) => ({
day: i + 1,
visits: 400 + Math.round(Math.sin(i / 3) * 120 + i * 8),
orders: 60 + Math.round(Math.cos(i / 4) * 20 + i * 2),
}));
protected readonly series: OgeChartSeriesInput[] = [
{ type: 'area', argumentField: 'day', valueField: 'visits', name: 'Visits' },
{ type: 'line', argumentField: 'day', valueField: 'orders', name: 'Orders' },
];
}Draw-in animation
The first render draws each series in from its value baseline — bars grow, lines rise. animation takes true / false or { enabled, duration, easing }; prefers-reduced-motion always switches it off.
| Argument | Revenue | Cost |
|---|---|---|
| Q1 | 120 | 80 |
| Q2 | 150 | 92 |
| Q3 | 138 | 101 |
| Q4 | 190 | 110 |
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeChart } from '@oge-ui/charts';
import type { OgeChartAnimationOptions, OgeChartSeriesInput } from '@oge-ui/charts';
@Component({
selector: 'demo-root',
imports: [OgeChart],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- The first render draws each series in from its value baseline:
bars grow, lines rise. animation takes true / false or { enabled,
duration, easing }; prefers-reduced-motion always switches it off. -->
<button type="button" (click)="replay()">Replay</button>
@if (visible()) {
<oge-chart
[dataSource]="data"
[series]="series"
[animation]="animation"
style="height: 320px"
/>
}
`,
})
export class Demo {
protected readonly data = [
{ quarter: 'Q1', revenue: 120, cost: 80 },
{ quarter: 'Q2', revenue: 150, cost: 92 },
{ quarter: 'Q3', revenue: 138, cost: 101 },
{ quarter: 'Q4', revenue: 190, cost: 110 },
];
protected readonly series: OgeChartSeriesInput[] = [
{ type: 'bar', argumentField: 'quarter', valueField: 'revenue', name: 'Revenue' },
{ type: 'spline', argumentField: 'quarter', valueField: 'cost', name: 'Cost' },
];
protected readonly animation: OgeChartAnimationOptions = { duration: 1100, easing: 'easeInOut' };
protected readonly visible = signal(true);
/** Re-mounting the chart plays the draw-in again. */
protected replay(): void {
this.visible.set(false);
setTimeout(() => this.visible.set(true));
}
}