@trdrs/quickcharts 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @trdrs/quickcharts
2
2
 
3
+ ## 2.2.0
4
+
5
+ The way back to the live edge and the plot a host control floats over, touch that follows the hand, and the formatter, the glyph lists and the icon artwork for hosts that draw their own. No public name changes.
6
+
7
+ The return to the live edge glides: `goLive`, its command and `Alt+L` ease the view home sideways over about half a second, keeping the zoom and the price scale as they are. From far back the view steps in to a width and a half first, and a touch, a drag, a wheel or any other navigation stops it where it stands. Under a reduced-motion preference it goes in one step.
8
+
9
+ A chart reads whether its view sits back from the live edge: `awayFromLiveEdge()` and the `liveEdge` event, reported once per change, with a glide under way and bar replay reading as returned. `plotArea()` and the `plotArea` event give the main pane's plot in the pixels of the host's element, so a control floating over the bars stays beside the price scale as it widens.
10
+
11
+ Two fingers zoom proportionally: the bars spread exactly as far as the fingers do, and the point under the fingers stays under them as they travel. A pan one finger began before the second landed stays a pan, and a drawing that holds the pointer holds the pinch off.
12
+
13
+ `@trdrs/quickcharts/format` carries the price formatter and its types alone, with nothing that names a window, a document or a DOM type, so a native view or a server writes a price exactly as the chart's axis does.
14
+
15
+ A finger held on the plot scrubs the crosshair for as long as it stays down: the crosshair follows the finger, and lifting it takes the crosshair away, so the next one-finger drag pans the chart.
16
+
17
+ A flick of one finger coasts the chart calmly: the throw leaves the finger at no more than two pixels a millisecond and slows smoothly to rest within about a second, and a touch or any other navigation stops it where it stands.
18
+
19
+ `touch.freePan` makes the chart a canvas under the finger: a one-finger drag on the plot releases the main price scale's framing, as a drag on the price scale does, so the price follows the finger as freely as the time and nothing re-frames while the viewer pans, flicks or pinches, and a pinch zooms the time and leaves the price where it stands. A double-tap on the price scale, a new symbol or timeframe, and `goLive` frame the bars again. It is off unless the host turns it on.
20
+
21
+ `chartIconArtwork(id)` answers the chart's own drawings of an icon as standalone svg markup on `currentColor`, each on its own grid, so a host draws the same icons the chart does in a toolbar of its own or a native view. It answers the chart's artwork whatever `icons` draws instead; an illustration drawn in the theme's roles answers none.
22
+
23
+ `@trdrs/quickcharts/glyphs` carries the glyph picker's lists on their own, free of any DOM name: `EMOJI_CATEGORIES` and `ICON_CATEGORIES`, each category with the catalog key of its `heading`, and `isEmojiGlyph`, so a picker of the host's own offers the same glyphs and arms a glyph tool through `chart.drawings.arm` with `{ tool, props: { glyph } }`.
24
+
3
25
  ## 2.1.0
4
26
 
5
27
  Long press for the crosshair, and emoji artwork that loads the first time the chart draws an emoji. No public name changes.
package/README.md CHANGED
@@ -256,6 +256,18 @@ formatter.parse('110.5') // null, because that symbol does not write decimals
256
256
  formatter.precision() // 0, because a fractional format writes no decimal digits
257
257
  ```
258
258
 
259
+ A surface with no DOM at all, a native view or a server, imports the same formatter from
260
+ `@trdrs/quickcharts/format`. The entry carries the formatter and its types and nothing that names a
261
+ window, a document or a DOM type, so it loads anywhere JavaScript runs and writes a price exactly as
262
+ the chart's axis does:
263
+
264
+ ```ts
265
+ import { createPriceFormatter, type PriceFormat } from '@trdrs/quickcharts/format'
266
+
267
+ const bonds: PriceFormat = { pricescale: 128, minmov: 1, minmove2: 4, fractional: true }
268
+ createPriceFormatter(bonds).format(110.515625) // "110'16'2"
269
+ ```
270
+
259
271
  A ladder changes width by band, because the symbol declared those bands:
260
272
 
261
273
  ```ts
@@ -920,6 +932,57 @@ function drive(widget: ChartWidget): void {
920
932
  }
921
933
  ```
922
934
 
935
+ ### The live edge and the plot
936
+
937
+ `goLive()` glides the view back to the live edge on an easing curve, moving it sideways only: the
938
+ zoom and the price scale stay as they are. From far back it steps in to a width and a half first, so
939
+ the return reads the same from any distance, and a touch, a drag, a wheel or any other navigation
940
+ stops it where it stands. Under a reduced-motion preference it goes in one step. `Alt+L` and the
941
+ `chart.view.goLive` command run the same glide.
942
+
943
+ `awayFromLiveEdge()` says whether the view sits back from the live edge, and the `liveEdge` event
944
+ reports each change, so a control offering the way back shows exactly while there is somewhere to go
945
+ back from. A glide under way reads as returned from its first frame, and bar replay, which has its
946
+ own way back, reads as returned throughout. `plotArea()` and the `plotArea` event give the main
947
+ pane's plot in the pixels of the element you handed the widget, as the four distances an absolutely
948
+ positioned element takes, so a control floating over the bars stays beside the price scale as it
949
+ widens:
950
+
951
+ ```ts
952
+ import { type ChartHandle } from '@trdrs/quickcharts'
953
+
954
+ function backToLive(chart: ChartHandle, button: HTMLButtonElement): () => void {
955
+ const place = (): void => {
956
+ const plot = chart.plotArea()
957
+ if (!plot) return
958
+ button.style.right = `${plot.right + 8}px`
959
+ button.style.bottom = `${plot.bottom + 8}px`
960
+ }
961
+ button.onclick = () => chart.goLive()
962
+ button.hidden = !chart.awayFromLiveEdge()
963
+ place()
964
+ const offEdge = chart.on('liveEdge', (away) => {
965
+ button.hidden = !away
966
+ })
967
+ const offPlot = chart.on('plotArea', place)
968
+ return () => {
969
+ offEdge()
970
+ offPlot()
971
+ }
972
+ }
973
+ ```
974
+
975
+ On a touch screen two fingers zoom proportionally: the bars spread exactly as far as the fingers
976
+ do, and the point under the fingers stays under them as they travel. A finger held on the plot
977
+ scrubs the crosshair while it stays down, and a flick coasts the chart calmly to rest.
978
+
979
+ With `touch: { freePan: true }` the chart is a canvas under the finger: a one-finger drag on the plot
980
+ releases the main price scale's framing, as a drag on the price scale does, so the price follows the
981
+ finger as freely as the time and nothing re-frames while the viewer pans, flicks or pinches. A pinch
982
+ zooms the time and leaves the price where it stands. A double-tap on the price scale, a new symbol or
983
+ timeframe, and `goLive()` frame the bars again; `goLive()` frames them from the tap, and the price
984
+ follows the window home as the time glides. It is off unless you turn it on.
985
+
923
986
  ### Chart styles
924
987
 
925
988
  `candles`, `hollow`, `bars`, `line`, `area`, `baseline`, `stepline`, listed as `CHART_STYLES`. A
@@ -1215,8 +1278,8 @@ function watch(widget: ChartWidget): () => void {
1215
1278
 
1216
1279
  Widget events: `ready`, `activeChart`, `theme`, `locale`, `saveNeeded`, `saveConflict`,
1217
1280
  `fullscreen`, `dispose`. Chart events: `symbol`, `timeframe`, `style`, `visibleRange`,
1218
- `logicalRange`, `dataLoaded`, `feedStatus`, `scaleMode`, `timezone`, `indicator`, `drawing`,
1219
- `replay`, `compare`, `history`.
1281
+ `logicalRange`, `liveEdge`, `plotArea`, `dataLoaded`, `feedStatus`, `scaleMode`, `timezone`, `indicator`,
1282
+ `drawing`, `replay`, `compare`, `history`.
1220
1283
 
1221
1284
  ### Commands
1222
1285
 
@@ -1736,6 +1799,21 @@ chart asks each factory again for every glyph it drew, a `toolbarButton` glyph i
1736
1799
  the new drawing in place of the old one. A factory that refuses the new direction leaves the
1737
1800
  previous drawing standing and records the failure.
1738
1801
 
1802
+ `chartIconArtwork(id)` answers the chart's own drawings of an icon as markup, for a host that draws
1803
+ the chart's icons outside it: a toolbar of its own, or a native view. Each drawing carries its grid,
1804
+ its body with every ink on `currentColor`, and the whole drawing as one standalone `<svg>`. An icon
1805
+ the chart draws at more than one size answers each, on its own grid, so you pick the one that suits
1806
+ your box; the drawing toolbar's are on a 28 grid. It answers the chart's own artwork whatever `icons`
1807
+ draws instead. An illustration drawn in the theme's roles answers none.
1808
+
1809
+ ```ts
1810
+ import { chartIconArtwork } from '@trdrs/quickcharts'
1811
+
1812
+ const [trendLine] = chartIconArtwork('tool.trend_line')
1813
+ const toolButton = document.createElement('button')
1814
+ toolButton.innerHTML = trendLine!.svg // the chart's own trend line, in the button's ink
1815
+ ```
1816
+
1739
1817
  The chart mirrors the glyphs in `MIRRORED_ICONS` for a right-to-left language whatever artwork they
1740
1818
  wear, so draw their left-to-right form. The line-end icons are drawn for a line's left end, and the
1741
1819
  right end's picker wears the same drawing mirrored.
@@ -2573,6 +2651,24 @@ const saved = drawings?.export() // the persistence wire format (SerializedDrawi
2573
2651
  note(`${selected?.type ?? 'nothing'} selected, ${saved?.length ?? 0} drawings`)
2574
2652
  ```
2575
2653
 
2654
+ `@trdrs/quickcharts/glyphs` carries the glyph picker's lists on their own, with nothing that names a
2655
+ window, a document or a DOM type, so a picker of your own, a native view among them, offers the same
2656
+ glyphs the chart's picker does: `EMOJI_CATEGORIES` and `ICON_CATEGORIES`, each category with its
2657
+ `id`, the catalog key of its `heading` and its `glyphs`, and `isEmojiGlyph`. The command arms a glyph
2658
+ tool with the glyph its next placement draws:
2659
+
2660
+ ```ts
2661
+ import { createChartI18n, type ChartWidget } from '@trdrs/quickcharts'
2662
+ import { EMOJI_CATEGORIES } from '@trdrs/quickcharts/glyphs'
2663
+
2664
+ const { t } = createChartI18n()
2665
+ for (const category of EMOJI_CATEGORIES) note(`${t(category.heading)}: ${category.glyphs.length} emoji`)
2666
+
2667
+ function armEmoji(widget: ChartWidget, glyph: string): void {
2668
+ widget.commands.execute('chart.drawings.arm', { tool: 'emoji', props: { glyph } })
2669
+ }
2670
+ ```
2671
+
2576
2672
  ### The drawing toolbar
2577
2673
 
2578
2674
  The drawing toolbar runs down the chart's leading edge. Its buttons are the cursor with its three
@@ -1,4 +1,4 @@
1
- import { C as ChartSaveLoadAdapter } from '../resources-Le6flcEn.js';
1
+ import { C as ChartSaveLoadAdapter } from '../resources-C0cYryO9.js';
2
2
 
3
3
  /** The three template collections. */
4
4
  type RestTemplateKind = 'indicator' | 'drawing' | 'palette';
@@ -1,5 +1,5 @@
1
1
  import { Time, IChartApi, ISeriesApi, SeriesType } from 'lightweight-charts';
2
- import { i as ResourceStore, j as TemplateMeta, T as TemplateBody, R as ResourceRef, W as WriteOutcome } from './resources-Le6flcEn.js';
2
+ import { R as ResourceStore, T as TemplateMeta, a as TemplateBody, b as ResourceRef, W as WriteOutcome } from './resources-C0cYryO9.js';
3
3
 
4
4
  /** One timeframe bucket's visibility rule: enabled + the inclusive value range it covers. */
5
5
  interface VisibilityRange {
@@ -0,0 +1,145 @@
1
+ // src/symbology.ts
2
+ function parseTickBands(variableTickSize) {
3
+ const parts = variableTickSize.trim().split(/\s+/).filter(Boolean);
4
+ if (parts.length === 0 || parts.length % 2 === 0) return [];
5
+ const bands = [];
6
+ for (let i = 0; i < parts.length; i += 2) {
7
+ const size = Number(parts[i]);
8
+ if (!Number.isFinite(size) || size <= 0) return [];
9
+ const raw = parts[i + 1];
10
+ if (raw === void 0) {
11
+ bands.push({ size, below: Number.POSITIVE_INFINITY });
12
+ break;
13
+ }
14
+ const below = Number(raw);
15
+ if (!Number.isFinite(below)) return [];
16
+ if (bands.length > 0 && below <= bands[bands.length - 1].below) return [];
17
+ bands.push({ size, below });
18
+ }
19
+ return bands;
20
+ }
21
+ function tickBandFor(bands, price) {
22
+ const at = Math.abs(price);
23
+ for (const band of bands) if (at < band.below) return band;
24
+ return bands.length > 0 ? bands[bands.length - 1] : null;
25
+ }
26
+
27
+ // src/priceFormatter.ts
28
+ var FRACTION_SIGN = "'";
29
+ function decimalsOfPriceScale(pricescale) {
30
+ if (!Number.isFinite(pricescale) || pricescale <= 1) return 0;
31
+ return Math.min(16, Math.ceil(Math.log10(pricescale) - 1e-9));
32
+ }
33
+ function localeDecimalSign(locale) {
34
+ try {
35
+ const part = new Intl.NumberFormat(locale).formatToParts(1.1).find((p) => p.type === "decimal");
36
+ return part?.value ?? ".";
37
+ } catch {
38
+ return ".";
39
+ }
40
+ }
41
+ function groupDigits(digits, groupSign) {
42
+ if (!groupSign) return digits;
43
+ let out = "";
44
+ for (let i = 0; i < digits.length; i++) {
45
+ if (i > 0 && (digits.length - i) % 3 === 0) out += groupSign;
46
+ out += digits[i];
47
+ }
48
+ return out;
49
+ }
50
+ function fixedText(value, decimals) {
51
+ return Math.abs(value).toFixed(Math.min(20, decimals));
52
+ }
53
+ function writeDecimal(price, decimals, punctuation) {
54
+ const fixed = fixedText(price, decimals);
55
+ const dot = fixed.indexOf(".");
56
+ const whole = dot < 0 ? fixed : fixed.slice(0, dot);
57
+ const frac = dot < 0 ? "" : fixed.slice(dot + 1);
58
+ const sign = price < 0 && Number(fixed) !== 0 ? "-" : "";
59
+ const body = groupDigits(whole, punctuation.groupSign);
60
+ return frac ? `${sign}${body}${punctuation.decimalSign}${frac}` : `${sign}${body}`;
61
+ }
62
+ function writeFractional(price, format, punctuation) {
63
+ const scale = format.pricescale;
64
+ const second = format.minmove2 && format.minmove2 > 1 ? format.minmove2 : 1;
65
+ const at = Math.abs(price);
66
+ const total = Math.round(at * scale);
67
+ const whole = Math.floor(total / scale);
68
+ const units = total - whole * scale;
69
+ const outer = Math.floor(units / second);
70
+ const inner = units - outer * second;
71
+ const outerWidth = String(Math.ceil(scale / second) - 1).length;
72
+ const sign = price < 0 && total !== 0 ? "-" : "";
73
+ const body = groupDigits(String(whole), punctuation.groupSign);
74
+ const outerText = String(outer).padStart(outerWidth, "0");
75
+ return second > 1 ? `${sign}${body}${FRACTION_SIGN}${outerText}${FRACTION_SIGN}${inner}` : `${sign}${body}${FRACTION_SIGN}${outerText}`;
76
+ }
77
+ function parseFractional(text, format) {
78
+ const scale = format.pricescale;
79
+ const second = format.minmove2 && format.minmove2 > 1 ? format.minmove2 : 1;
80
+ const parts = text.split(FRACTION_SIGN);
81
+ if (parts.length < 2 || parts.length > 3) return null;
82
+ const negative = parts[0].trim().startsWith("-");
83
+ const whole = Number(parts[0].replace(/[^0-9]/g, ""));
84
+ const outer = Number(parts[1].trim());
85
+ const inner = parts.length === 3 ? Number(parts[2].trim()) : 0;
86
+ if (!Number.isInteger(whole) || !Number.isInteger(outer) || !Number.isInteger(inner)) return null;
87
+ if (outer < 0 || inner < 0 || inner >= second) return null;
88
+ if (outer * second + inner >= scale) return null;
89
+ const value = whole + (outer * second + inner) / scale;
90
+ return negative ? -value : value;
91
+ }
92
+ function createPriceFormatter(format, options) {
93
+ const explicit = options?.numericPunctuation;
94
+ const punctuation = {
95
+ decimalSign: explicit?.decimalSign ?? (options?.locale ? localeDecimalSign(options.locale) : "."),
96
+ groupSign: explicit?.groupSign ?? ""
97
+ };
98
+ const fractional = format.fractional === true;
99
+ const bands = format.variableTickSize ? parseTickBands(format.variableTickSize) : [];
100
+ const bandDecimals = (band) => {
101
+ const text = decimalText(band.size);
102
+ const dot = text.indexOf(".");
103
+ return dot < 0 ? 0 : text.length - dot - 1;
104
+ };
105
+ const fixedDecimals = decimalsOfPriceScale(format.pricescale);
106
+ const widest = bands.length > 0 ? Math.max(...bands.map(bandDecimals)) : fixedDecimals;
107
+ const decimalsFor = (price) => {
108
+ if (bands.length === 0) return fixedDecimals;
109
+ const band = tickBandFor(bands, price);
110
+ return band ? bandDecimals(band) : fixedDecimals;
111
+ };
112
+ return {
113
+ format(price) {
114
+ if (!Number.isFinite(price)) return "";
115
+ return fractional ? writeFractional(price, format, punctuation) : writeDecimal(price, decimalsFor(price), punctuation);
116
+ },
117
+ parse(text) {
118
+ const trimmed = text.trim();
119
+ if (trimmed === "") return null;
120
+ if (fractional) return parseFractional(trimmed, format);
121
+ let plain = trimmed;
122
+ if (punctuation.groupSign) plain = plain.split(punctuation.groupSign).join("");
123
+ if (punctuation.decimalSign !== ".") plain = plain.split(punctuation.decimalSign).join(".");
124
+ if (!/^[+-]?(\d+(\.\d*)?|\.\d+)$/.test(plain)) return null;
125
+ const value = Number(plain);
126
+ return Number.isFinite(value) ? value : null;
127
+ },
128
+ precision: () => fractional ? 0 : widest
129
+ };
130
+ }
131
+ function decimalText(value) {
132
+ const text = String(value);
133
+ const exp = /^(-?)(\d)(?:\.(\d+))?e-(\d+)$/.exec(text);
134
+ if (!exp) return text;
135
+ const [, sign, lead, rest = "", power] = exp;
136
+ const zeros = Number(power) - 1;
137
+ return `${sign}0.${"0".repeat(zeros)}${lead}${rest}`;
138
+ }
139
+
140
+ export {
141
+ parseTickBands,
142
+ tickBandFor,
143
+ createPriceFormatter
144
+ };
145
+ //# sourceMappingURL=chunk-6OLK55Q2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/symbology.ts","../src/priceFormatter.ts"],"sourcesContent":["// Quick Charts SYMBOLOGY — the display facts `ChartDatafeed.resolve` serves for one symbol, and the\n// only place a chart price display gets its precision from. The datafeed says how a market's prices\n// are WRITTEN on the chart; a host that trades keeps its own execution facts, and the display and\n// execution grids may differ.\n//\n// What lives here: identity, venue and type, supported resolutions, exchange timezone and session,\n// data status, currency and unit, volume precision, and the price-format facts. Every one is a\n// display fact. A host keeps its execution facts and its quote values (last, change, volume) on its\n// own side of the chart.\n//\n// This module is self-contained by the same rule as `datafeed.ts` — the contract must never drag a\n// backend SDK into the chart's dependency surface. It imports nothing.\n\n/** How one symbol's prices are WRITTEN, in five facts. Together they express\n * every supported form: decimal, pip, fractional, fraction of a fraction, and variable tick.\n *\n * - `pricescale` — price units per whole unit. 100 writes cents, 100000 writes FX pipettes,\n * 100000000 writes satoshis, 32 writes thirty-seconds. It alone sets the decimal column width.\n * - `minmov` — the smallest move, in those units. It declares the grid a price moves on; it never\n * widens or narrows the decimal column (ES at `{100, 25}` writes 4500.25, the same two decimals\n * as `{100, 1}`).\n * - `minmove2` — the further division of one `minmov` step for a fraction-of-a-fraction market:\n * 4 means quarters of a thirty-second.\n * - `fractional` — write the sub-unit part as a counted fraction (110'16) instead of decimals.\n * - `variableTickSize` — an ordered ladder of tick sizes by price band, written as a\n * space-separated string: alternating tick and upper bound, ending with the tick that applies\n * above the last bound. \"0.01 10 0.02 100 0.05\" means 0.01 below 10, 0.02 below 100, 0.05 at or\n * above 100. The band a price falls in is a DECLARED fact of the symbol, not a guess from its\n * magnitude. */\nexport interface PriceFormat {\n pricescale: number\n minmov: number\n minmove2?: number\n fractional?: boolean\n variableTickSize?: string\n}\n\n/** How live the served data is, in three states. `delayed_streaming` is a real\n * stream behind a delay, not a degraded `streaming`. */\nexport type DataStatus = 'streaming' | 'endofday' | 'delayed_streaming'\n\n/** The subsession ids a symbol may name. `regular` is the weekly `session`; `premarket` and\n * `postmarket` are the extended spans before and after it; `extended` is the whole span from\n * pre-market open through post-market close. */\nexport type SubsessionId = 'regular' | 'extended' | 'premarket' | 'postmarket'\n\n/** One named session of a market with extended hours. `session`\n * uses the grammar of `SymbolInfo.session`; `sessionCorrections` uses the grammar of\n * `SymbolInfo.corrections` and lists only the days that shorten THIS subsession. `description`\n * is a label a feed may state; the chart labels a subsession by its `id` from its own catalog. */\nexport interface Subsession {\n id: SubsessionId\n session: string\n description?: string\n sessionCorrections?: string\n}\n\n/** Resolved metadata for ONE symbol: everything the chart needs to title it, page it, session it,\n * and write its prices.\n *\n * `timezone`, `session`, `sessionHolidays`, `corrections` and `subsessions` are the\n * exchange-hours facts and travel with the symbol. The chart's session model (`sessionModel.ts`)\n * is built from them alone: market state, session shading, the regular-hours filter and the\n * status popup all read what the feed said, and a feed that states no extended hours gets none. */\nexport interface SymbolInfo {\n /** The symbol the datafeed answers to on every later call. */\n ticker: string\n /** The short display identity, e.g. 'ESZ2026'. */\n name: string\n /** The long human description, e.g. 'E-mini S&P 500 Dec 2026'. */\n description: string\n /** The venue a UI attributes the symbol to. */\n exchange: string\n /** The venue that actually lists it, when it differs from `exchange`. */\n listedExchange: string\n /** The venue's own type token, e.g. 'futures', 'stock', 'crypto', 'forex'. */\n type: string\n /** The resolutions this symbol serves, as chart timeframe tokens ('1m', '4h', '1d', '1mo'). An\n * EMPTY list declares no restriction: the feed serves any token the grammar admits (a futures\n * or spread feed says exactly this), and the chart never reads it as \"serves nothing\". A finite\n * list is a promise the chart may enforce. */\n supportedResolutions: readonly string[]\n /** The exchange's IANA zone, e.g. 'America/New_York'. */\n timezone: string\n /** The session string, e.g. '1700-1600' or '0930-1600'. */\n session: string\n /** The session-holidays string: comma-separated 'YYYYMMDD' full closures. */\n sessionHolidays?: string\n /** The corrections string: `;`-separated `<session>:<dates>` entries, each a session in\n * the grammar of `session` that holds on the `,`-separated 'YYYYMMDD' trading days named. A\n * correction outranks a holiday on the same date. */\n corrections?: string\n /** The symbol's named sessions, for a market whose exchange keeps extended hours. Absent for a\n * market with one continuous session. */\n subsessions?: readonly Subsession[]\n dataStatus: DataStatus\n /** ISO 4217 for a money-quoted symbol; absent when the quote unit is not a currency. */\n currencyCode?: string\n /** The unit a non-currency symbol is quoted in, e.g. an energy or weight unit id. */\n unitId?: string\n /** Decimal places for a volume value. 0 for a whole-contract market. */\n volumePrecision: number\n format: PriceFormat\n}\n\n\n/** One band of a `variableTickSize` ladder: `size` is the tick that applies to prices strictly below\n * `below`, and the last band's `below` is `Infinity`. */\nexport interface TickBand {\n /** The tick size in this band. */\n size: number\n /** The exclusive upper bound of the band. */\n below: number\n}\n\n/** Parse the space-separated variable-tick ladder into ordered bands. Returns an\n * empty array for a string the grammar cannot read, which is how a caller tells \"no ladder\" from\n * \"a ladder I must honour\" without guessing at a half-parsed one. */\nexport function parseTickBands(variableTickSize: string): TickBand[] {\n const parts = variableTickSize.trim().split(/\\s+/).filter(Boolean)\n if (parts.length === 0 || parts.length % 2 === 0) return []\n const bands: TickBand[] = []\n for (let i = 0; i < parts.length; i += 2) {\n const size = Number(parts[i])\n if (!Number.isFinite(size) || size <= 0) return []\n const raw = parts[i + 1]\n if (raw === undefined) {\n bands.push({ size, below: Number.POSITIVE_INFINITY })\n break\n }\n const below = Number(raw)\n if (!Number.isFinite(below)) return []\n if (bands.length > 0 && below <= bands[bands.length - 1]!.below) return []\n bands.push({ size, below })\n }\n return bands\n}\n\n/** The band a price falls in, by the ladder's declared bounds (magnitude of the SYMBOL's own\n * declared ladder, never a generic magnitude rule). Negative prices read on their absolute value,\n * so a spread writes both sides in one column. */\nexport function tickBandFor(bands: readonly TickBand[], price: number): TickBand | null {\n const at = Math.abs(price)\n for (const band of bands) if (at < band.below) return band\n return bands.length > 0 ? bands[bands.length - 1]! : null\n}\n","// The one Quick Charts PRICE FORMATTER. Every price a chart writes comes through here: the price\n// scale, the OHLC legend, the crosshair, drawing labels, marks, indicator price plots and exported\n// image text, plus a host's own surfaces through the public factory. One resolved formatter per\n// symbol means a layout cannot draw one market's prices at another's precision.\n//\n// The rule that makes it trustworthy: PRECISION COMES FROM THE SYMBOL'S DECLARED FACTS, NEVER FROM\n// THE PRICE. There is no \"small numbers get more decimals\" branch anywhere below. A variable-tick\n// symbol does change width by price band, but the bands are a fact the datafeed declared, not a\n// guess the formatter made.\n//\n// Deterministic in the strict sense: the same `PriceFormat` and options always produce the same\n// string for the same number, on every machine and in every locale, because the only locale input\n// is the punctuation the caller passes (or asks to be derived once, at construction).\nimport { parseTickBands, tickBandFor, type PriceFormat, type TickBand } from './symbology'\n\n/** How a formatted price is punctuated. A price localizes the decimal sign only, so\n * `groupSign` defaults to '' (no thousands separator) and a host that wants grouping asks for it\n * explicitly. */\nexport interface NumericPunctuation {\n /** What separates the whole part from the decimals. Default '.', or the locale's sign when a\n * `locale` is given. */\n decimalSign?: string\n /** What separates thousands in the whole part. Default '': a price is written ungrouped, because\n * a grouped price is harder to compare against the axis beside it. */\n groupSign?: string\n}\n\nexport interface PriceFormatterOptions {\n /** A BCP 47 tag used ONCE, at construction, to derive the decimal sign. Anything explicit in\n * `numericPunctuation` wins over it. */\n locale?: string\n numericPunctuation?: NumericPunctuation\n}\n\n/** A resolved formatter for one symbol. `format` and `parse` are exact inverses over every price\n * that lands on the symbol's own grid. */\nexport interface PriceFormatter {\n /** The price as the symbol writes it. */\n format(price: number): string\n /** The number a viewer typed, or null when the text is not a price in this symbol's format. */\n parse(text: string): number | null\n /** How many digits `format` writes after the decimal sign. 0 for a fractional format, which\n * writes none; the widest band's width for a variable-tick format, so a column sized from it\n * never has to reflow. */\n precision(): number\n}\n\n/** The apostrophe written between a whole price and its counted fraction, and again\n * between that fraction and its sub-fraction: 110'16 and 110'16'2. */\nconst FRACTION_SIGN = \"'\"\n\n/** Decimal digits implied by a price scale: the digits it takes to write `pricescale - 1`. A power\n * of ten resolves exactly (100 → 2, 100000000 → 8); 32 resolves to 2, the width of '31'. */\nfunction decimalsOfPriceScale(pricescale: number): number {\n if (!Number.isFinite(pricescale) || pricescale <= 1) return 0\n return Math.min(16, Math.ceil(Math.log10(pricescale) - 1e-9))\n}\n\n/** The decimal sign a locale writes, resolved once. Falls back to '.' where `Intl` cannot answer. */\nfunction localeDecimalSign(locale: string): string {\n try {\n const part = new Intl.NumberFormat(locale).formatToParts(1.1).find((p) => p.type === 'decimal')\n return part?.value ?? '.'\n } catch {\n return '.'\n }\n}\n\n/** A non-negative integer written with `groupSign` every three digits from the right. */\nfunction groupDigits(digits: string, groupSign: string): string {\n if (!groupSign) return digits\n let out = ''\n for (let i = 0; i < digits.length; i++) {\n if (i > 0 && (digits.length - i) % 3 === 0) out += groupSign\n out += digits[i]\n }\n return out\n}\n\n/** The magnitude of `value` written at exactly `decimals` places. Rounding is the double's own: a\n * price arrives as a binary float, and `toFixed` rounds the value that is actually stored, so the\n * same input yields the same digits on every engine. The sign is applied by the caller, which is\n * what keeps a price that rounds to zero from writing '-0.00'. */\nfunction fixedText(value: number, decimals: number): string {\n return Math.abs(value).toFixed(Math.min(20, decimals))\n}\n\ninterface Punctuation {\n decimalSign: string\n groupSign: string\n}\n\nfunction writeDecimal(price: number, decimals: number, punctuation: Punctuation): string {\n const fixed = fixedText(price, decimals)\n const dot = fixed.indexOf('.')\n const whole = dot < 0 ? fixed : fixed.slice(0, dot)\n const frac = dot < 0 ? '' : fixed.slice(dot + 1)\n const sign = price < 0 && Number(fixed) !== 0 ? '-' : ''\n const body = groupDigits(whole, punctuation.groupSign)\n return frac ? `${sign}${body}${punctuation.decimalSign}${frac}` : `${sign}${body}`\n}\n\n/** The fractional form. `units` is the count of `1/pricescale` steps in the sub-unit part, split\n * into whole `minmove2`-sized groups (the thirty-seconds) and a remainder (the quarters of one\n * thirty-second). With no `minmove2` the sub-unit part is the count itself: 110'16. */\nfunction writeFractional(price: number, format: PriceFormat, punctuation: Punctuation): string {\n const scale = format.pricescale\n const second = format.minmove2 && format.minmove2 > 1 ? format.minmove2 : 1\n const at = Math.abs(price)\n const total = Math.round(at * scale)\n const whole = Math.floor(total / scale)\n const units = total - whole * scale\n const outer = Math.floor(units / second)\n const inner = units - outer * second\n const outerWidth = String(Math.ceil(scale / second) - 1).length\n const sign = price < 0 && total !== 0 ? '-' : ''\n const body = groupDigits(String(whole), punctuation.groupSign)\n const outerText = String(outer).padStart(outerWidth, '0')\n return second > 1\n ? `${sign}${body}${FRACTION_SIGN}${outerText}${FRACTION_SIGN}${inner}`\n : `${sign}${body}${FRACTION_SIGN}${outerText}`\n}\n\nfunction parseFractional(text: string, format: PriceFormat): number | null {\n const scale = format.pricescale\n const second = format.minmove2 && format.minmove2 > 1 ? format.minmove2 : 1\n const parts = text.split(FRACTION_SIGN)\n if (parts.length < 2 || parts.length > 3) return null\n const negative = parts[0]!.trim().startsWith('-')\n const whole = Number(parts[0]!.replace(/[^0-9]/g, ''))\n const outer = Number(parts[1]!.trim())\n const inner = parts.length === 3 ? Number(parts[2]!.trim()) : 0\n if (!Number.isInteger(whole) || !Number.isInteger(outer) || !Number.isInteger(inner)) return null\n if (outer < 0 || inner < 0 || inner >= second) return null\n if (outer * second + inner >= scale) return null\n const value = whole + (outer * second + inner) / scale\n return negative ? -value : value\n}\n\n/** Build the formatter for one symbol's price format. The result is stateless and reusable; a host\n * resolves a symbol once and keeps its formatter for as long as the symbol is on screen. */\nexport function createPriceFormatter(format: PriceFormat, options?: PriceFormatterOptions): PriceFormatter {\n const explicit = options?.numericPunctuation\n const punctuation: Punctuation = {\n decimalSign: explicit?.decimalSign ?? (options?.locale ? localeDecimalSign(options.locale) : '.'),\n groupSign: explicit?.groupSign ?? '',\n }\n const fractional = format.fractional === true\n const bands: readonly TickBand[] = format.variableTickSize ? parseTickBands(format.variableTickSize) : []\n const bandDecimals = (band: TickBand): number => {\n const text = decimalText(band.size)\n const dot = text.indexOf('.')\n return dot < 0 ? 0 : text.length - dot - 1\n }\n const fixedDecimals = decimalsOfPriceScale(format.pricescale)\n const widest = bands.length > 0 ? Math.max(...bands.map(bandDecimals)) : fixedDecimals\n const decimalsFor = (price: number): number => {\n if (bands.length === 0) return fixedDecimals\n const band = tickBandFor(bands, price)\n return band ? bandDecimals(band) : fixedDecimals\n }\n\n return {\n format(price) {\n if (!Number.isFinite(price)) return ''\n return fractional ? writeFractional(price, format, punctuation) : writeDecimal(price, decimalsFor(price), punctuation)\n },\n parse(text) {\n const trimmed = text.trim()\n if (trimmed === '') return null\n if (fractional) return parseFractional(trimmed, format)\n let plain = trimmed\n if (punctuation.groupSign) plain = plain.split(punctuation.groupSign).join('')\n if (punctuation.decimalSign !== '.') plain = plain.split(punctuation.decimalSign).join('.')\n if (!/^[+-]?(\\d+(\\.\\d*)?|\\.\\d+)$/.test(plain)) return null\n const value = Number(plain)\n return Number.isFinite(value) ? value : null\n },\n precision: () => (fractional ? 0 : widest),\n }\n}\n\n/** A number's shortest decimal text, with exponent notation expanded, so a tick of 1e-8 reads\n * '0.00000001' and its decimal width can be counted. */\nfunction decimalText(value: number): string {\n const text = String(value)\n const exp = /^(-?)(\\d)(?:\\.(\\d+))?e-(\\d+)$/.exec(text)\n if (!exp) return text\n const [, sign, lead, rest = '', power] = exp\n const zeros = Number(power) - 1\n return `${sign}0.${'0'.repeat(zeros)}${lead}${rest}`\n}\n"],"mappings":";AAsHO,SAAS,eAAe,kBAAsC;AACnE,QAAM,QAAQ,iBAAiB,KAAK,EAAE,MAAM,KAAK,EAAE,OAAO,OAAO;AACjE,MAAI,MAAM,WAAW,KAAK,MAAM,SAAS,MAAM,EAAG,QAAO,CAAC;AAC1D,QAAM,QAAoB,CAAC;AAC3B,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,GAAG;AACxC,UAAM,OAAO,OAAO,MAAM,CAAC,CAAC;AAC5B,QAAI,CAAC,OAAO,SAAS,IAAI,KAAK,QAAQ,EAAG,QAAO,CAAC;AACjD,UAAM,MAAM,MAAM,IAAI,CAAC;AACvB,QAAI,QAAQ,QAAW;AACrB,YAAM,KAAK,EAAE,MAAM,OAAO,OAAO,kBAAkB,CAAC;AACpD;AAAA,IACF;AACA,UAAM,QAAQ,OAAO,GAAG;AACxB,QAAI,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO,CAAC;AACrC,QAAI,MAAM,SAAS,KAAK,SAAS,MAAM,MAAM,SAAS,CAAC,EAAG,MAAO,QAAO,CAAC;AACzE,UAAM,KAAK,EAAE,MAAM,MAAM,CAAC;AAAA,EAC5B;AACA,SAAO;AACT;AAKO,SAAS,YAAY,OAA4B,OAAgC;AACtF,QAAM,KAAK,KAAK,IAAI,KAAK;AACzB,aAAW,QAAQ,MAAO,KAAI,KAAK,KAAK,MAAO,QAAO;AACtD,SAAO,MAAM,SAAS,IAAI,MAAM,MAAM,SAAS,CAAC,IAAK;AACvD;;;AChGA,IAAM,gBAAgB;AAItB,SAAS,qBAAqB,YAA4B;AACxD,MAAI,CAAC,OAAO,SAAS,UAAU,KAAK,cAAc,EAAG,QAAO;AAC5D,SAAO,KAAK,IAAI,IAAI,KAAK,KAAK,KAAK,MAAM,UAAU,IAAI,IAAI,CAAC;AAC9D;AAGA,SAAS,kBAAkB,QAAwB;AACjD,MAAI;AACF,UAAM,OAAO,IAAI,KAAK,aAAa,MAAM,EAAE,cAAc,GAAG,EAAE,KAAK,CAAC,MAAM,EAAE,SAAS,SAAS;AAC9F,WAAO,MAAM,SAAS;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,SAAS,YAAY,QAAgB,WAA2B;AAC9D,MAAI,CAAC,UAAW,QAAO;AACvB,MAAI,MAAM;AACV,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,QAAI,IAAI,MAAM,OAAO,SAAS,KAAK,MAAM,EAAG,QAAO;AACnD,WAAO,OAAO,CAAC;AAAA,EACjB;AACA,SAAO;AACT;AAMA,SAAS,UAAU,OAAe,UAA0B;AAC1D,SAAO,KAAK,IAAI,KAAK,EAAE,QAAQ,KAAK,IAAI,IAAI,QAAQ,CAAC;AACvD;AAOA,SAAS,aAAa,OAAe,UAAkB,aAAkC;AACvF,QAAM,QAAQ,UAAU,OAAO,QAAQ;AACvC,QAAM,MAAM,MAAM,QAAQ,GAAG;AAC7B,QAAM,QAAQ,MAAM,IAAI,QAAQ,MAAM,MAAM,GAAG,GAAG;AAClD,QAAM,OAAO,MAAM,IAAI,KAAK,MAAM,MAAM,MAAM,CAAC;AAC/C,QAAM,OAAO,QAAQ,KAAK,OAAO,KAAK,MAAM,IAAI,MAAM;AACtD,QAAM,OAAO,YAAY,OAAO,YAAY,SAAS;AACrD,SAAO,OAAO,GAAG,IAAI,GAAG,IAAI,GAAG,YAAY,WAAW,GAAG,IAAI,KAAK,GAAG,IAAI,GAAG,IAAI;AAClF;AAKA,SAAS,gBAAgB,OAAe,QAAqB,aAAkC;AAC7F,QAAM,QAAQ,OAAO;AACrB,QAAM,SAAS,OAAO,YAAY,OAAO,WAAW,IAAI,OAAO,WAAW;AAC1E,QAAM,KAAK,KAAK,IAAI,KAAK;AACzB,QAAM,QAAQ,KAAK,MAAM,KAAK,KAAK;AACnC,QAAM,QAAQ,KAAK,MAAM,QAAQ,KAAK;AACtC,QAAM,QAAQ,QAAQ,QAAQ;AAC9B,QAAM,QAAQ,KAAK,MAAM,QAAQ,MAAM;AACvC,QAAM,QAAQ,QAAQ,QAAQ;AAC9B,QAAM,aAAa,OAAO,KAAK,KAAK,QAAQ,MAAM,IAAI,CAAC,EAAE;AACzD,QAAM,OAAO,QAAQ,KAAK,UAAU,IAAI,MAAM;AAC9C,QAAM,OAAO,YAAY,OAAO,KAAK,GAAG,YAAY,SAAS;AAC7D,QAAM,YAAY,OAAO,KAAK,EAAE,SAAS,YAAY,GAAG;AACxD,SAAO,SAAS,IACZ,GAAG,IAAI,GAAG,IAAI,GAAG,aAAa,GAAG,SAAS,GAAG,aAAa,GAAG,KAAK,KAClE,GAAG,IAAI,GAAG,IAAI,GAAG,aAAa,GAAG,SAAS;AAChD;AAEA,SAAS,gBAAgB,MAAc,QAAoC;AACzE,QAAM,QAAQ,OAAO;AACrB,QAAM,SAAS,OAAO,YAAY,OAAO,WAAW,IAAI,OAAO,WAAW;AAC1E,QAAM,QAAQ,KAAK,MAAM,aAAa;AACtC,MAAI,MAAM,SAAS,KAAK,MAAM,SAAS,EAAG,QAAO;AACjD,QAAM,WAAW,MAAM,CAAC,EAAG,KAAK,EAAE,WAAW,GAAG;AAChD,QAAM,QAAQ,OAAO,MAAM,CAAC,EAAG,QAAQ,WAAW,EAAE,CAAC;AACrD,QAAM,QAAQ,OAAO,MAAM,CAAC,EAAG,KAAK,CAAC;AACrC,QAAM,QAAQ,MAAM,WAAW,IAAI,OAAO,MAAM,CAAC,EAAG,KAAK,CAAC,IAAI;AAC9D,MAAI,CAAC,OAAO,UAAU,KAAK,KAAK,CAAC,OAAO,UAAU,KAAK,KAAK,CAAC,OAAO,UAAU,KAAK,EAAG,QAAO;AAC7F,MAAI,QAAQ,KAAK,QAAQ,KAAK,SAAS,OAAQ,QAAO;AACtD,MAAI,QAAQ,SAAS,SAAS,MAAO,QAAO;AAC5C,QAAM,QAAQ,SAAS,QAAQ,SAAS,SAAS;AACjD,SAAO,WAAW,CAAC,QAAQ;AAC7B;AAIO,SAAS,qBAAqB,QAAqB,SAAiD;AACzG,QAAM,WAAW,SAAS;AAC1B,QAAM,cAA2B;AAAA,IAC/B,aAAa,UAAU,gBAAgB,SAAS,SAAS,kBAAkB,QAAQ,MAAM,IAAI;AAAA,IAC7F,WAAW,UAAU,aAAa;AAAA,EACpC;AACA,QAAM,aAAa,OAAO,eAAe;AACzC,QAAM,QAA6B,OAAO,mBAAmB,eAAe,OAAO,gBAAgB,IAAI,CAAC;AACxG,QAAM,eAAe,CAAC,SAA2B;AAC/C,UAAM,OAAO,YAAY,KAAK,IAAI;AAClC,UAAM,MAAM,KAAK,QAAQ,GAAG;AAC5B,WAAO,MAAM,IAAI,IAAI,KAAK,SAAS,MAAM;AAAA,EAC3C;AACA,QAAM,gBAAgB,qBAAqB,OAAO,UAAU;AAC5D,QAAM,SAAS,MAAM,SAAS,IAAI,KAAK,IAAI,GAAG,MAAM,IAAI,YAAY,CAAC,IAAI;AACzE,QAAM,cAAc,CAAC,UAA0B;AAC7C,QAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,UAAM,OAAO,YAAY,OAAO,KAAK;AACrC,WAAO,OAAO,aAAa,IAAI,IAAI;AAAA,EACrC;AAEA,SAAO;AAAA,IACL,OAAO,OAAO;AACZ,UAAI,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACpC,aAAO,aAAa,gBAAgB,OAAO,QAAQ,WAAW,IAAI,aAAa,OAAO,YAAY,KAAK,GAAG,WAAW;AAAA,IACvH;AAAA,IACA,MAAM,MAAM;AACV,YAAM,UAAU,KAAK,KAAK;AAC1B,UAAI,YAAY,GAAI,QAAO;AAC3B,UAAI,WAAY,QAAO,gBAAgB,SAAS,MAAM;AACtD,UAAI,QAAQ;AACZ,UAAI,YAAY,UAAW,SAAQ,MAAM,MAAM,YAAY,SAAS,EAAE,KAAK,EAAE;AAC7E,UAAI,YAAY,gBAAgB,IAAK,SAAQ,MAAM,MAAM,YAAY,WAAW,EAAE,KAAK,GAAG;AAC1F,UAAI,CAAC,6BAA6B,KAAK,KAAK,EAAG,QAAO;AACtD,YAAM,QAAQ,OAAO,KAAK;AAC1B,aAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;AAAA,IAC1C;AAAA,IACA,WAAW,MAAO,aAAa,IAAI;AAAA,EACrC;AACF;AAIA,SAAS,YAAY,OAAuB;AAC1C,QAAM,OAAO,OAAO,KAAK;AACzB,QAAM,MAAM,gCAAgC,KAAK,IAAI;AACrD,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,CAAC,EAAE,MAAM,MAAM,OAAO,IAAI,KAAK,IAAI;AACzC,QAAM,QAAQ,OAAO,KAAK,IAAI;AAC9B,SAAO,GAAG,IAAI,KAAK,IAAI,OAAO,KAAK,CAAC,GAAG,IAAI,GAAG,IAAI;AACpD;","names":[]}