Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/docs/content/docs/api/exports.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The package ships ESM and TypeScript declarations. There is no CommonJS export.
| `@bloxwap/chart/indicators` | Indicator registry, built-ins, and calculation helpers |
| `@bloxwap/chart/drawings` | Drawing registry, catalog, built-ins, and geometry types |
| `@bloxwap/chart/wasm` | WASM initialization and kernel interface |
| `@bloxwap/chart/ui` | Drawing toolbar, settings, header, scale buttons, indicator dialog, context menu, touch gestures, countdown ticker, controller, history, UI primitives, and frame scheduler |
| `@bloxwap/chart/ui` | Drawing toolbar, settings, header, control rail, scale buttons, indicator dialog, context menu, touch gestures, countdown ticker, controller, history, UI primitives, and frame scheduler |
| `@bloxwap/chart/icons` | `ICONS` and `icon(name, size?)` |
| `@bloxwap/chart/react` | `<Chart>` React component (`'use client'`), `ChartProps`, `ChartInstance` |
| `@bloxwap/chart/datafeed` | `createDatafeedChart`, `attachDatafeed`, `loadHistory`, `createLiveBarFolder`, paging and bucket helpers |
Expand All @@ -40,6 +40,7 @@ Beyond `createChart`, the root entry exports:
| Area | Exports |
| --- | --- |
| Header | `createChartHeader`, `DEFAULT_TIMEFRAMES`, `DEFAULT_HEADER_INTERVAL_MS`, `DEFAULT_CHART_TYPES`, `CHART_TYPE_LABELS`, `HEADER_CSS`, `HEADER_STYLE_MARKER`, `injectHeaderStyles`, `BLOXWAP_HEADER_TOKENS`, `BLOXWAP_HEADER_THEME`; types `HeaderTokens`, `ThemedHeaderTokens` |
| Control rail | `createControlRail`; types `ControlRail`, `ControlRailOptions` |
| Scale buttons | `createScaleButtons`, `toggleScale`, `scaleToggleState`, `syncScaleToggles`, `SCALE_TOGGLES` |
| Indicator dialog | `createIndicatorsDialog`, `indicatorInputs`, `indicatorStyleGroups`, `INDICATORS_DIALOG_CSS`, `INDICATORS_DIALOG_STYLE_MARKER` |
| Context menu | `createChartContextMenu`; types `ChartContextMenuHooks`, `ContextMenuTarget`, `ContextMenuAction`, `ContextMenuEntry` |
Expand Down
42 changes: 40 additions & 2 deletions apps/docs/content/docs/guides/header.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Header and scale buttons
description: Add a TradingView-style top bar with timeframes, chart types, indicators, and price-scale toggles.
title: Header, control rail and scale buttons
description: Add a TradingView-style top bar or a vertical control rail with timeframes, chart types, indicators, and price-scale toggles.
---

The chart header is an optional top bar from `@bloxwap/chart/ui`. It holds the symbol, a timeframe switcher that drives your datafeed, a chart-type menu, an Indicators button, Auto / % / Log price-scale toggles, and slots for your own controls. It is a normal-flow element: put its container above the chart. Nothing else depends on it, so skip it if you keep your own header.
Expand Down Expand Up @@ -89,6 +89,44 @@ The chart-type button and scale toggles follow `series` and `priceAxis` changes

Every control is its own tab stop, and dropdowns work from the keyboard: arrows, Home, and End move through entries, Escape closes the menu (from anywhere on the page, so a menu opened with a click or tap closes too) and returns focus to its button, and Tab closes it and continues from its button. Menus stack above the settings card and the indicators dialog.

## Control rail

`createControlRail` puts the same controls in a vertical rail beside the drawing toolbar, so the plot starts at the top of the card. This is the layout bloxwap.pro and the playground use on wide screens. It holds a timeframe menu, the chart-type menu, the Indicators button, and the Auto / % / Log toggles, then your own controls.

```html
<div style="display: flex; height: 480px">
<div id="controls" style="display: flex"></div>
<div id="tools" style="width: 52px"></div>
<div id="stage" style="position: relative; flex: 1"><canvas id="chart"></canvas></div>
</div>
```

```ts
import { createControlRail } from '@bloxwap/chart/ui';

const rail = createControlRail({
chart,
document,
container: document.querySelector<HTMLElement>('#controls')!,
datafeed, // picking 1h reloads the loaded symbol at 3_600_000
flyouts: toolbar.flyouts, // one menu open at a time with the drawing rail
onIndicators: () => indicators.openPicker(),
slots: { top: [undoButton, redoButton], bottom: [settingsButton] },
});

rail.slots.bottom.append(snapshotButton); // slots are live containers
rail.setTheme('light');
rail.destroy();
```

The layout, top to bottom, is `[timeframe] [chart type] [Indicators] — [Auto % Log] — [top slot] … [bottom slot]`. Dividers only appear between groups that are shown. The top slot scrolls with the built-in controls, and the rail adds paging arrows when they overflow. The bottom slot stays pinned at the foot.

It takes the header's `symbol`, `timeframes`, `intervalMs`, `datafeed`, `onTimeframeChange`, `chartTypes`, `onChartTypeChange`, `onIndicators`, `scaleButtons`, `flyouts`, and `theme` options, with the same defaults and the same [datafeed rules](#working-with-a-datafeed). `slots` is `{ top?, bottom? }`. The handle has `element`, `slots` (the `top` and `bottom` containers), `symbol`, `intervalMs`, `setSymbol`, `setInterval`, `setChartType`, `setTheme`, `refresh`, and `destroy`, which behave like the header's.

Menus open to the right of their button, on click or mouse hover like the drawing rail's, and work from the keyboard like the header's dropdowns. The rail uses the shared toolbar tokens (`--cts-accent`, `--cts-accent-soft`, and so on) rather than `--cts-header-*`: set them on `.cts-theme` to restyle it along with the drawing rail. A pressed scale toggle takes the accent color on the soft accent fill.

To switch layouts by width, as the playground does, create both. Hide the rail's container below a breakpoint and the header's above it, then move your own buttons between `rail.slots` and the header's `slots.right` container. Both follow the datafeed, so they stay in step.

## Scale buttons on the chart

TradingView shows `A`, `%`, and `L` toggles at the foot of the price axis. `createScaleButtons` adds the same strip above the time axis:
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Add the toolbar with `createDrawingToolbar({ chart, document, canvas, rail, over
- [Indicators](https://bloxwap.github.io/chart/docs/guides/indicators/): 20 built-in studies, per-plot styling, the indicator dialog, and custom calculations.
- [Drawings](https://bloxwap.github.io/chart/docs/guides/drawings/): Place, edit, and manage annotations in data coordinates.
- [Toolbar and settings](https://bloxwap.github.io/chart/docs/guides/toolbar-settings/): Drawing rail, navigation, the settings card, and context menus.
- [Header and scale buttons](https://bloxwap.github.io/chart/docs/guides/header/): Top bar with timeframes, chart types, indicators, and Auto / % / Log toggles.
- [Header, control rail and scale buttons](https://bloxwap.github.io/chart/docs/guides/header/): Top bar or vertical control rail with timeframes, chart types, indicators, and Auto / % / Log toggles.
- [Touch gestures](https://bloxwap.github.io/chart/docs/guides/touch/): Pan, pinch, fling, and long-press crosshair.
- [Time scale](https://bloxwap.github.io/chart/docs/guides/time-scale/): Bar-indexed or time-continuous axis, and what stays bar-indexed.
- [Chart events](https://bloxwap.github.io/chart/docs/guides/events/): Visible range, crosshair, data load, and layout events and their delivery rules.
Expand Down
9 changes: 7 additions & 2 deletions apps/playground/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ bloxwap.pro uses the chart:
section of the settings card switches between BTC, the random walk (1m or 1d)
and **Gaps (1h)**. Gaps (1h) turns on `timeScale.continuous`, so missing
buckets show as gaps.
- **Control rail:** on wide screens, `createControlRail` stacks the timeframe
menu, chart-type menu, **Indicators** button and Auto / % / Log toggles in a
rail left of the drawing toolbar. The page adds the right-edge panel picker,
undo/redo and the theme control to its top slot, and the snapshot, settings
and drawing-rail toggle buttons to its bottom slot.
- **Header:** `createChartHeader` shows the symbol and the 1m–1M timeframes.
Picking a timeframe calls `datafeed.setSymbol`. The header also has the
chart-type menu (including Heikin Ashi and hollow candles), an **Indicators**
Expand Down Expand Up @@ -71,8 +76,8 @@ bloxwap.pro uses the chart:
`#00ff3f` accent. The header and the on-chart scale toggles get it from
`BLOXWAP_HEADER_THEME`, which their `setTheme` clears on light. With no saved
choice, the page starts on dark.
- **Narrow screens (under 600px):** the control rail hides and its buttons move
into the header. When the header is too wide, it collapses the timeframes into
- **Narrow screens (under 600px):** the control rail hides, the header shows,
and the page's own buttons move from the rail's slots into the header. When the header is too wide, it collapses the timeframes into
a dropdown and wraps onto a second row. The drawing rail starts collapsed. The
status line wraps onto more rows instead of running under the price axis. On
touch devices, a one-finger drag pans even across the demo drawings: tap one
Expand Down
91 changes: 16 additions & 75 deletions apps/playground/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@
#header { flex: none; min-width: 0; }
/* Wide: the control rail carries the header's controls, so the plot starts at the top of the card. */
@media (min-width: 600px) { #header { display: none; } }
/* Rail text toggles (timeframe, Auto / % / Log) fit the 36px buttons; a pressed toggle takes the accent. */
#ctrl-rail .cts-btn-label { font-size: 11px; }
#ctrl-rail .rail-scale.cts-on { color: var(--cts-accent); background: var(--cts-accent-soft); }
#ctrl-rail { flex: none; display: flex; }
#stage { position: relative; flex: 1; min-height: 0; }
#tools { flex: none; width: 52px; overflow: hidden; transition: width 240ms cubic-bezier(0.2, 0.8, 0.2, 1); }
#tools.collapsed { width: 0; }
Expand Down Expand Up @@ -63,10 +61,7 @@
</head>
<body>
<div id="app">
<nav id="ctrl-rail" class="cts-theme cts-rail" aria-label="Chart controls">
<div class="cts-rail-scroll" id="ctrl-scroll"></div>
<div id="ctrl-bottom" class="cts-rail-footer" style="display: flex; flex-direction: column; gap: 4px;"></div>
</nav>
<div id="ctrl-rail"></div>
<div id="tools"></div>
<main id="main">
<div id="header"></div>
Expand All @@ -81,9 +76,8 @@
import { createDatafeedChart, datafeedFootprintSource, bucketStartMs } from '../dist/datafeed/index.js';
import {
createDrawingToolbar, createChartSettings, createIndicatorsDialog, createThemeControl, createFrameScheduler,
createChartHeader, DEFAULT_TIMEFRAMES, attachScrollableRail, el, iconButton, setButtonIcon,
menuItem, menuLabel, menuSeparator, startCountdownTicker, labelButton, setButtonLabel, ICONS,
DEFAULT_CHART_TYPES, CHART_TYPE_LABELS, SCALE_TOGGLES, toggleScale, scaleToggleState,
createChartHeader, createControlRail, DEFAULT_TIMEFRAMES, el, iconButton, setButtonIcon,
menuItem, menuLabel, menuSeparator, startCountdownTicker,
} from '../dist/ui/index.js';
import { BLOXWAP_HEADER_THEME } from '../dist/ui/index.js';

Expand Down Expand Up @@ -588,8 +582,8 @@
onChange: () => { toolbar.cancelNavigation(); toolbar.refreshViewport(); },
});

// ---------------- rail: timeframe, chart type, indicators, price scale (the wide layout's header) ----------------
// Flyouts open to the right of the rail; every control follows the datafeed and config like the header does.
// ---------------- rail: the right-edge panel disclosure (createControlRail below holds the header's controls) ----------------
// Flyouts open to the right of the rail.
const railMenu = (label) => {
const menu = flyouts.create();
menu.append(menuLabel(document, label));
Expand All @@ -606,39 +600,6 @@
item.setAttribute('aria-checked', String(on));
};

const tfBtn = labelButton(document, '15m', 'Timeframe');
const tfMenu = railMenu('Timeframe');
const tfItems = DEFAULT_TIMEFRAMES.map((tf) => radio(tfMenu, { label: tf.label,
onClick: () => { if (tf.intervalMs !== datafeed.intervalMs) void load(datafeed.symbol ?? COIN, tf.intervalMs); } }));
flyouts.attach(tfBtn, tfMenu);
function syncRailTimeframe() {
const intervalMs = datafeed.intervalMs;
const label = DEFAULT_TIMEFRAMES.find((tf) => tf.intervalMs === intervalMs)?.label ?? '';
setButtonLabel(tfBtn, label);
tfBtn.title = `Timeframe: ${label}`;
tfBtn.setAttribute('aria-label', tfBtn.title);
DEFAULT_TIMEFRAMES.forEach((tf, i) => check(tfItems[i], tf.intervalMs === intervalMs));
}
datafeed.subscribeState(syncRailTimeframe);

const typeBtn = iconButton(document, 'chart-candlestick', 'Chart type', 20);
const typeMenu = railMenu('Chart type');
const typeItems = DEFAULT_CHART_TYPES.map((type) => radio(typeMenu, { icon: `chart-${type}`, label: CHART_TYPE_LABELS[type],
onClick: () => chart.updateConfig({ series: { type } }) }));
flyouts.attach(typeBtn, typeMenu);
function syncRailType() {
const type = chart.getConfig().series.type;
// A custom series (the footprint) keeps the candlestick glyph.
setButtonIcon(typeBtn, ICONS[`chart-${type}`] !== undefined ? `chart-${type}` : 'chart-candlestick', 20);
typeBtn.title = `Chart type: ${CHART_TYPE_LABELS[type] ?? type}`;
typeBtn.setAttribute('aria-label', typeBtn.title);
DEFAULT_CHART_TYPES.forEach((t, i) => check(typeItems[i], t === type));
}

const indicatorsBtn = iconButton(document, 'function-square', 'Indicators', 20);
indicatorsBtn.setAttribute('aria-haspopup', 'dialog');
indicatorsBtn.addEventListener('click', () => indicators.openPicker());

// Right-edge panel disclosure: the volume profile (both ways from a center line), the order book (out of the
// right edge), or none. The button shows the chosen panel's glyph and takes the accent while one shows.
const SIDE_MODES = [
Expand All @@ -663,28 +624,6 @@
}
syncSideMode();

const scaleBtns = SCALE_TOGGLES.map(({ key, label, title }) => {
const b = labelButton(document, label, title);
b.classList.add('rail-scale');
b.setAttribute('aria-label', title);
b.addEventListener('click', () => toggleScale(chart, key));
return b;
});
function syncRailScale() {
const state = scaleToggleState(chart.getConfig().priceAxis);
SCALE_TOGGLES.forEach(({ key }, i) => {
scaleBtns[i].classList.toggle('cts-on', state[key]);
scaleBtns[i].setAttribute('aria-pressed', String(state[key]));
});
}
chart.subscribeConfigChange(({ keys }) => {
if (keys.includes('series')) syncRailType();
if (keys.includes('priceAxis')) syncRailScale();
});
syncRailTimeframe();
syncRailType();
syncRailScale();

// ---------------- header (narrow screens): symbol, timeframes → datafeed, chart type, indicators, scale toggles ----------------
const shotBtn = iconButton(document, 'image', 'Download snapshot', 18);
shotBtn.innerHTML = CAMERA;
Expand All @@ -703,6 +642,11 @@
onIndicators: () => indicators.openPicker(),
slots: { right: [headerControls] },
});
// Wide: the control rail replaces the header, driving the same source. Host controls join its slots below.
const controlRail = createControlRail({ chart, document, container: $('ctrl-rail'), datafeed: source, flyouts,
onIndicators: () => indicators.openPicker(),
slots: { top: [sideBtn, el(document, 'div', 'cts-rail-divider')] },
});

// Embeds can pin a host theme with ?lockedTheme=dark (or light). Without a saved choice, start on dark (bloxwapDark).
const themeKey = 'chart-ts:theme';
Expand All @@ -714,7 +658,7 @@
onSelect: mode => safeSet(themeKey, mode),
onChange: theme => {
document.body.classList.toggle('light', theme === 'light');
$('ctrl-rail').classList.toggle('cts-light', theme === 'light');
controlRail.setTheme(theme);
toolbar.setTheme(theme);
settings.setTheme(theme);
header.setTheme(theme);
Expand Down Expand Up @@ -742,21 +686,18 @@

// Wide: the control rail replaces the header. Narrow (< 600px): the rail hides, the header shows, and the
// shared buttons join it, compacting and wrapping. The rail-only controls stay put in the hidden rail.
const divider = () => el(document, 'div', 'cts-rail-divider');
const narrow = matchMedia('(max-width: 599px)');
$('ctrl-scroll').append(tfBtn, typeBtn, indicatorsBtn, sideBtn, divider(), ...scaleBtns, divider());
const themeDivider = divider();
const themeDivider = el(document, 'div', 'cts-rail-divider');
const placeControls = () => {
const theme = themeControl.element ? [themeControl.element] : [];
flyouts.close();
if (narrow.matches) headerControls.append(shotBtn, undoBtn, redoBtn, ...theme, settingsBtn, railToggle);
else {
$('ctrl-scroll').append(undoBtn, redoBtn, ...(theme.length > 0 ? [themeDivider, ...theme] : []));
$('ctrl-bottom').append(shotBtn, settingsBtn, railToggle);
controlRail.slots.top.append(undoBtn, redoBtn, ...(theme.length > 0 ? [themeDivider, ...theme] : []));
controlRail.slots.bottom.append(shotBtn, settingsBtn, railToggle);
}
};
placeControls();
attachScrollableRail(document, $('ctrl-scroll'));
narrow.addEventListener('change', placeControls);
// Keep the chart usable in a narrow embed; drawing tools are one click away.
if (narrow.matches) railToggle.click();
Expand Down Expand Up @@ -968,7 +909,7 @@
chart.addDrawing({ name: 'long-position', points: [{ index: n - 30, price: data[n - 30].close }, { index: n + 10, price: data[n - 30].close * 1.012 }] });
}
}
Object.assign(window, { chart, datafeed, toolbar, header, indicators, settings });
Object.assign(window, { chart, datafeed, toolbar, header, controlRail, indicators, settings });
</script>
</body>
</html>
Loading
Loading