@stocksharp/trading-controls 1.3.0 → 1.4.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.
Files changed (62) hide show
  1. package/README.md +113 -5
  2. package/dist/esm/chart-engine.js +15 -0
  3. package/dist/esm/chart-engine.js.map +1 -0
  4. package/dist/esm/control-types.js +4 -0
  5. package/dist/esm/control-types.js.map +1 -1
  6. package/dist/esm/equity-widget.js +261 -0
  7. package/dist/esm/equity-widget.js.map +1 -0
  8. package/dist/esm/heatmap-grid.js +271 -0
  9. package/dist/esm/heatmap-grid.js.map +1 -0
  10. package/dist/esm/index.js +19 -0
  11. package/dist/esm/index.js.map +1 -1
  12. package/dist/esm/optimization-heatmap-widget.js +303 -0
  13. package/dist/esm/optimization-heatmap-widget.js.map +1 -0
  14. package/dist/esm/option-smile-widget.js +252 -0
  15. package/dist/esm/option-smile-widget.js.map +1 -0
  16. package/dist/esm/pnl-curve.js +5 -1
  17. package/dist/esm/pnl-curve.js.map +1 -1
  18. package/dist/esm/surface-grid.js +282 -0
  19. package/dist/esm/surface-grid.js.map +1 -0
  20. package/dist/esm/surface-widget.js +379 -0
  21. package/dist/esm/surface-widget.js.map +1 -0
  22. package/dist/sstradingcontrols.js +1595 -168
  23. package/dist/sstradingcontrols.js.map +4 -4
  24. package/dist/types/chart-engine.d.ts +3 -0
  25. package/dist/types/chart-engine.d.ts.map +1 -0
  26. package/dist/types/control-types.d.ts +4 -0
  27. package/dist/types/control-types.d.ts.map +1 -1
  28. package/dist/types/equity-widget.d.ts +34 -0
  29. package/dist/types/equity-widget.d.ts.map +1 -0
  30. package/dist/types/heatmap-grid.d.ts +88 -0
  31. package/dist/types/heatmap-grid.d.ts.map +1 -0
  32. package/dist/types/index.d.ts +15 -0
  33. package/dist/types/index.d.ts.map +1 -1
  34. package/dist/types/optimization-heatmap-widget.d.ts +45 -0
  35. package/dist/types/optimization-heatmap-widget.d.ts.map +1 -0
  36. package/dist/types/option-smile-widget.d.ts +39 -0
  37. package/dist/types/option-smile-widget.d.ts.map +1 -0
  38. package/dist/types/pnl-curve.d.ts +3 -0
  39. package/dist/types/pnl-curve.d.ts.map +1 -1
  40. package/dist/types/surface-grid.d.ts +64 -0
  41. package/dist/types/surface-grid.d.ts.map +1 -0
  42. package/dist/types/surface-widget.d.ts +64 -0
  43. package/dist/types/surface-widget.d.ts.map +1 -0
  44. package/package.json +28 -2
  45. package/screenshots/equity.png +0 -0
  46. package/screenshots/optimization-surface.png +0 -0
  47. package/screenshots/optimization.png +0 -0
  48. package/screenshots/option-smile.png +0 -0
  49. package/screenshots/panels.jpg +0 -0
  50. package/src/chart-engine.global.ts +64 -0
  51. package/src/chart-engine.ts +29 -0
  52. package/src/control-types.ts +4 -0
  53. package/src/equity-widget.ts +289 -0
  54. package/src/heatmap-grid.ts +405 -0
  55. package/src/index.ts +46 -0
  56. package/src/optimization-heatmap-widget.ts +347 -0
  57. package/src/option-smile-widget.ts +281 -0
  58. package/src/pnl-curve.ts +13 -1
  59. package/src/surface-grid.ts +410 -0
  60. package/src/surface-widget.ts +457 -0
  61. package/styles/trading-controls.css +281 -0
  62. package/translation-keys.json +14 -1
@@ -0,0 +1,271 @@
1
+ // The geometry of a heatmap - one metric over two discrete axes - as pure functions over numbers.
2
+ //
3
+ // A heatmap is a lattice: every distinct value of one axis crossed with every distinct value of
4
+ // the other, one cell per pair, coloured by what that pair measured. What produced the pairs is
5
+ // not here and does not need to be - an optimisation sweep is one source of them, and the object
6
+ // is the same shape whatever the source.
7
+ //
8
+ // None of this needs a canvas, which is what makes it checkable: a fake 2D context can only
9
+ // record the calls a control made, so the numbers those calls carry are checked here, against
10
+ // arithmetic rather than against pixels.
11
+ //
12
+ // The colour comes out as a signed number in -1..1 rather than as a colour. The sign says which
13
+ // side of the anchor a cell fell on and the magnitude how far, and the control turns that into
14
+ // one of the two direction colours its host answered with. Nothing in this file knows a colour.
15
+ /// Which way is better. Stated wherever a scale is built, never inferred: a metric does not
16
+ /// carry its own direction, and a map of drawdowns would otherwise paint its worst corner in
17
+ /// the winning colour.
18
+ export const HeatDirections = {
19
+ Higher: 'higher',
20
+ Lower: 'lower',
21
+ };
22
+ // Proportions, not looks: a canvas has no cascade to read them from, and moving them into CSS
23
+ // would only mean reading them back out again.
24
+ const LEFT_GUTTER = 54;
25
+ const RIGHT_PAD = 6;
26
+ const TOP_BAND = 26;
27
+ const BOTTOM_BAND = 30;
28
+ const TICK_GAP = 4;
29
+ const LABEL_HEIGHT = 13;
30
+ /// Half a pixel off each side, so abutting cells show a hairline of the panel between them and
31
+ /// the lattice reads as cells rather than as one continuous wash.
32
+ const CELL_INSET = 0.5;
33
+ const LEGEND_WIDTH = 140;
34
+ const LEGEND_HEIGHT = 8;
35
+ const LEGEND_Y = 14;
36
+ const LEGEND_STEPS = 28;
37
+ /// Room one label wants. Below this the labels touch, and an axis whose values overprint each
38
+ /// other is worse than an axis showing every fourth value.
39
+ const MIN_TICK_WIDTH = 34;
40
+ const MIN_TICK_HEIGHT = 13;
41
+ /// A value that is a number written as text, trailing unit included: "12", "-0.5", "7.5%".
42
+ const NUMERIC = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?\s*%?$/;
43
+ /// Every run at a pair, as one cell.
44
+ ///
45
+ /// A run whose metric is not a number is dropped rather than folded in: a report that carries a
46
+ /// missing figure as an empty string would otherwise turn the whole cell into a NaN, and a NaN
47
+ /// cell paints as nothing while claiming the pair was measured.
48
+ export function foldCells(cells) {
49
+ const folded = new Map();
50
+ const order = [];
51
+ for (const cell of cells) {
52
+ if (cell === null || cell === undefined || !Number.isFinite(cell.value))
53
+ continue;
54
+ const id = pairKey(cell.x, cell.y);
55
+ const found = folded.get(id);
56
+ if (found === undefined) {
57
+ folded.set(id, { x: cell.x, y: cell.y, sum: cell.value, count: 1 });
58
+ order.push(id);
59
+ }
60
+ else {
61
+ found.sum += cell.value;
62
+ found.count += 1;
63
+ }
64
+ }
65
+ return order.map(id => {
66
+ const fold = folded.get(id);
67
+ return { x: fold.x, y: fold.y, value: fold.sum / fold.count, count: fold.count };
68
+ });
69
+ }
70
+ /// One axis, in the order it is read.
71
+ ///
72
+ /// Numerically when every value on it is a number, which is the whole point: a text sort puts
73
+ /// "10" before "2" and the axis stops being monotonic, so the map's shape becomes an artefact of
74
+ /// how the values happened to be spelled. Text order otherwise - and it is the right answer for
75
+ /// the shapes that actually arrive, because .NET writes a time span at a fixed width, so
76
+ /// "00:05:00" < "00:15:00" < "01:00:00" is also chronological.
77
+ export function axisValues(values) {
78
+ const unique = [];
79
+ const seen = new Set();
80
+ for (const value of values) {
81
+ if (seen.has(value))
82
+ continue;
83
+ seen.add(value);
84
+ unique.push(value);
85
+ }
86
+ if (unique.every(value => NUMERIC.test(value)))
87
+ return unique.sort((left, right) => numberOf(left) - numberOf(right));
88
+ return unique.sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
89
+ }
90
+ /// What the colours are measured against.
91
+ ///
92
+ /// Two anchors, and which one applies is decided by the data rather than by the caller. When the
93
+ /// values straddle zero, zero is the anchor: profit and loss are different in kind, not merely
94
+ /// in degree, and a map that shaded a loss green because it was less bad than the others would
95
+ /// be lying about the only thing a reader checks first. When zero is nowhere in the range - a
96
+ /// Sharpe of 1.8 to 2.2, a profit of 100 to 5000 - the anchor is the middle of the range,
97
+ /// because anchoring at a zero the sweep never came near produces one flat block with no
98
+ /// contrast, and comparing the cells with each other is what the reader came for.
99
+ ///
100
+ /// The reach is the same on both sides of the anchor, so one unit of colour means one amount of
101
+ /// metric everywhere on the map. Scaling each side to its own extreme instead would make a five
102
+ /// dollar loss look exactly as red as a five hundred dollar one.
103
+ export function heatScale(buckets) {
104
+ let min = Infinity;
105
+ let max = -Infinity;
106
+ for (const bucket of buckets) {
107
+ if (bucket.value < min)
108
+ min = bucket.value;
109
+ if (bucket.value > max)
110
+ max = bucket.value;
111
+ }
112
+ if (!isFinite(min) || !isFinite(max)) {
113
+ min = 0;
114
+ max = 0;
115
+ }
116
+ const anchor = min <= 0 && max >= 0 ? 0 : (min + max) / 2;
117
+ const reach = Math.max(Math.abs(max - anchor), Math.abs(anchor - min));
118
+ return { anchor, reach: reach > 0 ? reach : 1, min, max };
119
+ }
120
+ /// How good a value is, as -1..1. Positive is better whichever way better runs.
121
+ export function tintOf(value, scale, betterWhen) {
122
+ const distance = (value - scale.anchor) / scale.reach;
123
+ const signed = betterWhen === HeatDirections.Lower ? -distance : distance;
124
+ return Math.max(-1, Math.min(1, signed));
125
+ }
126
+ /// The inverse, for labelling a legend: what value a tint stands for.
127
+ export function valueAt(tint, scale, betterWhen) {
128
+ const distance = betterWhen === HeatDirections.Lower ? -tint : tint;
129
+ return scale.anchor + distance * scale.reach;
130
+ }
131
+ /// Lay one map out, or null when nothing was measured.
132
+ export function layoutHeatmap(input) {
133
+ const buckets = foldCells(input.cells);
134
+ if (buckets.length === 0)
135
+ return null;
136
+ const columns = axisValues(buckets.map(bucket => bucket.x));
137
+ const rows = axisValues(buckets.map(bucket => bucket.y));
138
+ const scale = heatScale(buckets);
139
+ const plot = {
140
+ x: LEFT_GUTTER,
141
+ y: TOP_BAND,
142
+ width: Math.max(1, input.width - LEFT_GUTTER - RIGHT_PAD),
143
+ height: Math.max(1, input.height - TOP_BAND - BOTTOM_BAND),
144
+ };
145
+ const cellWidth = plot.width / columns.length;
146
+ const cellHeight = plot.height / rows.length;
147
+ const rectOf = (column, row) => ({
148
+ x: plot.x + column * cellWidth + CELL_INSET,
149
+ // Row 0 sits at the FOOT of the plot: this is a chart, and a chart's Y grows upward. A
150
+ // table would put it at the top, and then the map and its axis would disagree about
151
+ // which corner is which.
152
+ y: plot.y + plot.height - (row + 1) * cellHeight + CELL_INSET,
153
+ width: Math.max(1, cellWidth - CELL_INSET * 2),
154
+ height: Math.max(1, cellHeight - CELL_INSET * 2),
155
+ });
156
+ const measured = new Map(buckets.map(bucket => [pairKey(bucket.x, bucket.y), bucket]));
157
+ const cells = [];
158
+ const gaps = [];
159
+ for (let row = 0; row < rows.length; row++) {
160
+ for (let column = 0; column < columns.length; column++) {
161
+ const bucket = measured.get(pairKey(columns[column], rows[row]));
162
+ if (bucket === undefined) {
163
+ gaps.push({ x: columns[column], y: rows[row], rect: rectOf(column, row) });
164
+ continue;
165
+ }
166
+ cells.push({
167
+ bucket,
168
+ rect: rectOf(column, row),
169
+ tint: tintOf(bucket.value, scale, input.betterWhen),
170
+ best: false,
171
+ });
172
+ }
173
+ }
174
+ // The highest tint is the best cell whichever way better runs, which is the reason the tint
175
+ // is signed by direction rather than by the metric. A tie goes to the first in scan order -
176
+ // the cell nearest the origin - rather than to whichever run happened to arrive first.
177
+ let best = cells[0];
178
+ for (const shape of cells)
179
+ if (shape.tint > best.tint)
180
+ best = shape;
181
+ best.best = true;
182
+ const xTicks = tickIndices(columns.length, tickStep(columns.length, plot.width, MIN_TICK_WIDTH))
183
+ .map(index => ({
184
+ text: columns[index],
185
+ x: plot.x + (index + 0.5) * cellWidth,
186
+ y: plot.y + plot.height + TICK_GAP,
187
+ }));
188
+ const yTicks = tickIndices(rows.length, tickStep(rows.length, plot.height, MIN_TICK_HEIGHT))
189
+ .map(index => ({
190
+ text: rows[index],
191
+ x: plot.x - TICK_GAP,
192
+ y: plot.y + plot.height - (index + 0.5) * cellHeight,
193
+ }));
194
+ return {
195
+ plot,
196
+ columns,
197
+ rows,
198
+ cells,
199
+ gaps,
200
+ xTicks,
201
+ yTicks,
202
+ xTitle: { text: input.xLabel, x: plot.x + plot.width / 2, y: plot.y + plot.height + TICK_GAP + LABEL_HEIGHT },
203
+ // Above the plot rather than beside it: a title down the left edge would have to be
204
+ // rotated, and a rotation is a transform the control would then have to unwind around
205
+ // everything else it draws.
206
+ yTitle: { text: input.yLabel, x: 0, y: 0 },
207
+ legend: legendOf(plot, scale, input.betterWhen),
208
+ scale,
209
+ };
210
+ }
211
+ /// The cell under a point, or null. Cells never overlap, so the first match is the only one -
212
+ /// unlike a scatter, where the hit test has to walk the draw order backwards.
213
+ export function hitHeatmap(layout, x, y) {
214
+ for (const shape of layout.cells) {
215
+ const rect = shape.rect;
216
+ if (x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height)
217
+ return shape;
218
+ }
219
+ return null;
220
+ }
221
+ function legendOf(plot, scale, betterWhen) {
222
+ const right = plot.x + plot.width;
223
+ const left = Math.max(plot.x, right - LEGEND_WIDTH);
224
+ const stepWidth = (right - left) / LEGEND_STEPS;
225
+ const steps = [];
226
+ for (let step = 0; step < LEGEND_STEPS; step++) {
227
+ steps.push({
228
+ // Half a pixel of overlap: abutting fills leave a seam wherever the boundary is
229
+ // fractional, and a key with a comb of background through it reads as a scale with
230
+ // holes in it.
231
+ rect: { x: left + step * stepWidth, y: LEGEND_Y, width: stepWidth + 0.5, height: LEGEND_HEIGHT },
232
+ tint: -1 + (2 * step) / (LEGEND_STEPS - 1),
233
+ });
234
+ }
235
+ return {
236
+ steps,
237
+ low: { value: valueAt(-1, scale, betterWhen), x: left, y: 0 },
238
+ anchor: { value: scale.anchor, x: (left + right) / 2, y: 0 },
239
+ high: { value: valueAt(1, scale, betterWhen), x: right, y: 0 },
240
+ };
241
+ }
242
+ /// How many values to skip between labels so they do not touch.
243
+ function tickStep(count, span, room) {
244
+ const slots = Math.max(1, Math.floor(span / room));
245
+ return Math.max(1, Math.ceil(count / slots));
246
+ }
247
+ /// Which values get a label. The last one always does - the end of an axis is what a reader
248
+ /// checks first - and the label before it gives way when the step would land them on top of
249
+ /// each other.
250
+ function tickIndices(count, step) {
251
+ const indices = [];
252
+ for (let index = 0; index < count; index += step)
253
+ indices.push(index);
254
+ const last = count - 1;
255
+ if (indices[indices.length - 1] !== last) {
256
+ if (last - indices[indices.length - 1] < step)
257
+ indices.pop();
258
+ indices.push(last);
259
+ }
260
+ return indices;
261
+ }
262
+ /// A pair as one map key. NUL rather than a printable separator: an axis value may contain any
263
+ /// character a parameter's own notation uses, and "a|b" crossed with "c" must not collide with
264
+ /// "a" crossed with "b|c".
265
+ function pairKey(x, y) {
266
+ return `${x}\u0000${y}`;
267
+ }
268
+ function numberOf(value) {
269
+ return Number(value.trim().replace(/%$/, ''));
270
+ }
271
+ //# sourceMappingURL=heatmap-grid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heatmap-grid.js","sourceRoot":"","sources":["../../src/heatmap-grid.ts"],"names":[],"mappings":"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"}
package/dist/esm/index.js CHANGED
@@ -15,7 +15,26 @@ export { OrderBookWidget } from './orderbook-widget.js';
15
15
  // sparkline and a statistics card are the same curve at two sizes. A backtest's own
16
16
  // equity is not here - that one shares the price axis and belongs to the chart engine.
17
17
  export { compressPnl, drawPnlCurve, pnlCurve } from './pnl-curve.js';
18
+ // The same curve at the size of a chart: one control for an equity panel, so a host does not
19
+ // hand-roll the arithmetic a sparkline already does.
20
+ export { EquityWidget } from './equity-widget.js';
21
+ // The chart engine the two chart panels are built on, re-exported so a host can reach the same
22
+ // createChart these panels use rather than resolving a second copy of it.
23
+ export { AreaSeries, CrosshairMode, LineSeries, createChart } from './chart-engine.js';
24
+ // One metric over two parameters. It knows nothing about optimisation - a grid of {x, y, value}
25
+ // is the same object whatever produced it - which is what lets a backtest sweep and anything
26
+ // else that varies two things share a control.
27
+ export { OptimizationHeatmapWidget } from './optimization-heatmap-widget.js';
28
+ export { HeatDirections, heatScale, hitHeatmap, layoutHeatmap, tintOf, valueAt } from './heatmap-grid.js';
29
+ // The same measurements as a landscape, turned by hand. No library under it: the projection is
30
+ // arithmetic and the gestures are pointer events, so a mouse and a thumb take the same path.
31
+ export { SurfaceWidget } from './surface-widget.js';
32
+ export { DEFAULT_VIEW, MAX_PITCH, MIN_PITCH, clampView, dragView, project, surfaceLayout, zoomView, } from './surface-grid.js';
18
33
  export { OptionDeskWidget, greekPlaces, greekScales, scaleChain, sideGreeks } from './option-desk-widget.js';
34
+ // The smile: the same chain the desk reads, drawn as the shape a trader looks for rather than as
35
+ // a column of percentages.
36
+ export { OptionSmileWidget } from './option-smile-widget.js';
37
+ export { sideVolatility, sortedChain, toSmileSeries } from './option-smile-widget.js';
19
38
  // Option pricing, for a host that has quotes and an expiry but no pricing service. A host
20
39
  // that computes its own greeks sends them instead and none of this is reached.
21
40
  export { OptionTypes, d1, d2, greeks, impliedVolatility, normalCdf, normalPdf, premium } from './black-scholes.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AA2BvF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAIlD,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEnG,8EAA8E;AAC9E,8EAA8E;AAC9E,eAAe;AACf,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,cAAc,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE7F,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAI5E,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAIxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAI/D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAIxD,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAM7F,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAUxD,kFAAkF;AAClF,oFAAoF;AACpF,uFAAuF;AACvF,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAIrE,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAI7G,0FAA0F;AAC1F,+EAA+E;AAC/E,OAAO,EAAE,WAAW,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,iBAAiB,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAInH,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAI1E,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAI3D,0FAA0F;AAC1F,0FAA0F;AAC1F,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAI5E,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAI3E,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAIxD,2EAA2E;AAC3E,8EAA8E;AAC9E,oEAAoE;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAI7D,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAIvD,8FAA8F;AAC9F,8FAA8F;AAC9F,mFAAmF;AACnF,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,303 @@
1
+ // Optimisation heatmap - multi-instance.
2
+ //
3
+ // One metric over two parameters: an axis of discrete values across, another up, a cell per pair
4
+ // of them, coloured by what that pair measured. The panel does not know what an optimisation is -
5
+ // a heatmap of a metric over two parameters is the same object whatever produced it - so the
6
+ // pairs are handed in and the two axis names come with them.
7
+ //
8
+ // The map is a <canvas>, the one surface a class name cannot reach: its geometry comes out of
9
+ // `heatmap-grid.ts` as numbers and its colours out of the host's `canvasPalette()`, so nothing
10
+ // here is painted from a palette this package chose. The tooltip is DOM, because it is text.
11
+ //
12
+ // Two things are drawn rather than tabulated and are worth stating. A cell carries no printed
13
+ // number: at the size a forty by forty sweep leaves a cell, digits are unreadable, and reading
14
+ // colour at that size is the entire reason to draw the map. And a pair nobody ran is struck
15
+ // through rather than left blank, because an unrun pair and a pair that measured zero are
16
+ // different facts that a scale with zero at its neutral point would otherwise paint the same.
17
+ import { formatStatistic } from './statistics-widget.js';
18
+ import { makeElement, makeIconButton, makePanelId, makePanelRoot } from './dom.js';
19
+ import { ControlTypes } from './control-types.js';
20
+ import { assertHost } from './trading-host.js';
21
+ import { hitHeatmap, layoutHeatmap } from './heatmap-grid.js';
22
+ // Opacity, not colour. Every hue below is the host's `canvasPalette`; how solid a thing is drawn
23
+ // is the map's own business, and keeping the two apart is what lets one palette serve a light
24
+ // theme and a dark one without the package knowing which it is in.
25
+ //
26
+ // The ground is drawn under every measured cell so that a cell sitting exactly on the anchor -
27
+ // which takes no tint at all - is still visibly a cell. That is also what lets the tint itself
28
+ // go all the way down to nothing without a floor: neutral reads as the ground, and a pair that
29
+ // was never run has no ground under it.
30
+ const GROUND_ALPHA = 0.1;
31
+ const GAP_ALPHA = 0.25;
32
+ const LABEL_ALPHA = 0.65;
33
+ const BEST_WIDTH = 2;
34
+ // Gap between the cursor and the tooltip, and between the tooltip and the edge it is clamped
35
+ // against.
36
+ const TOOLTIP_OFFSET = 12;
37
+ const TOOLTIP_MARGIN = 4;
38
+ export class OptimizationHeatmapWidget {
39
+ static create(hostEl, state, deps) {
40
+ // Assert before building: the markup below is localized through the host, so a missing
41
+ // host has to fail here rather than render a panel captioned with raw English keys.
42
+ const host = assertHost(deps?.host, 'OptimizationHeatmapWidget');
43
+ const root = OptimizationHeatmapWidget._buildRoot(host);
44
+ root.id = makePanelId(OptimizationHeatmapWidget.TYPE);
45
+ hostEl.appendChild(root);
46
+ return new OptimizationHeatmapWidget(root, state || {}, deps);
47
+ }
48
+ // The panel's markup. The metric's name sits in the header rather than on the canvas: it is
49
+ // the caption the drawn legend is read against, and putting it in the chrome costs the map
50
+ // no pixels and leaves the text selectable and sized by the stylesheet.
51
+ static _buildRoot(host) {
52
+ const title = host.t('OptimizationHeatmap');
53
+ return makePanelRoot('optimization-heatmap-panel', title, [
54
+ makeElement('div', 'panel-header', {}, [
55
+ makeElement('span', '', {}, [title]),
56
+ makeElement('span', 'heatmap-metric', {}, []),
57
+ makeIconButton('bt-icon-btn bt-icon-cancel panel-close-btn', host.t('ClosePanel'), 'bi-x', { type: 'button' }),
58
+ ]),
59
+ makeElement('div', 'panel-body', {}, [
60
+ makeElement('canvas', 'heatmap-canvas', { role: 'img', 'aria-label': host.t('OptimizationHeatmapChart') }, []),
61
+ makeElement('div', 'heatmap-empty', { hidden: '' }, [host.t('NoOptimizationResults')]),
62
+ makeElement('div', 'heatmap-tooltip', { role: 'tooltip', hidden: '' }, []),
63
+ ]),
64
+ ]);
65
+ }
66
+ constructor(rootEl, _state, deps) {
67
+ this._host = assertHost(deps?.host, 'OptimizationHeatmapWidget');
68
+ this.rootEl = rootEl;
69
+ this.canvasEl = this.rootEl.querySelector('.heatmap-canvas');
70
+ this._metricEl = this.rootEl.querySelector('.heatmap-metric');
71
+ this._emptyEl = this.rootEl.querySelector('.heatmap-empty');
72
+ this._tooltipEl = this.rootEl.querySelector('.heatmap-tooltip');
73
+ this._closeBtn = this.rootEl.querySelector('.panel-close-btn');
74
+ this._ctx = this.canvasEl?.getContext('2d') ?? null;
75
+ this._data = null;
76
+ this._layout = null;
77
+ this._size = null;
78
+ this._resizeObserver = null;
79
+ this._closeBtn?.addEventListener('click', (e) => { e.preventDefault(); this._host.close(); });
80
+ this._bindHover();
81
+ this._setEmpty(true);
82
+ if (this.canvasEl && typeof ResizeObserver === 'function') {
83
+ this._resizeObserver = new ResizeObserver(() => this._render());
84
+ this._resizeObserver.observe(this.canvasEl);
85
+ }
86
+ this._host.register(this);
87
+ }
88
+ dispose() {
89
+ try {
90
+ this._resizeObserver?.disconnect();
91
+ }
92
+ catch { /* already torn down */ }
93
+ this._host.unregister(this);
94
+ try {
95
+ this.rootEl.remove();
96
+ }
97
+ catch { /* already detached */ }
98
+ }
99
+ /// Show this map. The whole of it at once: a pair that has dropped out of the set has stopped
100
+ /// existing, and leaving its cell behind would report a run that is no longer in the report.
101
+ update(data) {
102
+ this._data = data;
103
+ if (this._metricEl !== null)
104
+ this._metricEl.textContent = data.metricLabel;
105
+ this._hideTooltip();
106
+ this._render();
107
+ }
108
+ // The canvas in CSS pixels, and its backing store in device pixels. Without the second the
109
+ // map is drawn at a third of the resolution the screen has, and a lattice of hairlines is
110
+ // exactly the drawing that shows it.
111
+ _sizeCanvas() {
112
+ const canvas = this.canvasEl;
113
+ if (!canvas)
114
+ return;
115
+ const rect = canvas.getBoundingClientRect();
116
+ const ratio = typeof devicePixelRatio === 'number' && devicePixelRatio > 0 ? devicePixelRatio : 1;
117
+ const width = Math.max(1, Math.floor(rect.width));
118
+ const height = Math.max(1, Math.floor(rect.height));
119
+ if (canvas.width !== width * ratio || canvas.height !== height * ratio) {
120
+ canvas.width = width * ratio;
121
+ canvas.height = height * ratio;
122
+ // Everything below draws in CSS pixels, which is also the space a mouse event
123
+ // arrives in - so the hit test needs no conversion of its own.
124
+ this._ctx?.setTransform(ratio, 0, 0, ratio, 0, 0);
125
+ }
126
+ this._size = { width, height };
127
+ }
128
+ _render() {
129
+ const ctx = this._ctx;
130
+ if (ctx === null)
131
+ return;
132
+ this._sizeCanvas();
133
+ const size = this._size;
134
+ if (size === null)
135
+ return;
136
+ ctx.clearRect(0, 0, size.width, size.height);
137
+ this._layout = null;
138
+ const data = this._data;
139
+ if (data === null) {
140
+ this._setEmpty(true);
141
+ return;
142
+ }
143
+ const layout = layoutHeatmap({
144
+ width: size.width,
145
+ height: size.height,
146
+ cells: data.cells,
147
+ betterWhen: data.betterWhen,
148
+ xLabel: data.xLabel,
149
+ yLabel: data.yLabel,
150
+ });
151
+ this._setEmpty(layout === null);
152
+ if (layout === null)
153
+ return;
154
+ this._layout = layout;
155
+ const palette = this._host.presentation.canvasPalette();
156
+ ctx.font = palette.font;
157
+ ctx.lineWidth = 1;
158
+ for (const shape of layout.cells)
159
+ this._paintTint(ctx, shape.rect, shape.tint, palette.grid, palette.up, palette.down);
160
+ // A pair nobody ran, struck through. One diagonal rather than a fill of any kind: no
161
+ // amount of colour can say "not measured" on a map whose colours are all measurements.
162
+ ctx.globalAlpha = GAP_ALPHA;
163
+ ctx.strokeStyle = palette.grid;
164
+ for (const gap of layout.gaps) {
165
+ ctx.beginPath();
166
+ ctx.moveTo(gap.rect.x, gap.rect.y + gap.rect.height);
167
+ ctx.lineTo(gap.rect.x + gap.rect.width, gap.rect.y);
168
+ ctx.stroke();
169
+ }
170
+ // Last, so it sits over its own fill. In the grid colour because that is the only
171
+ // non-directional colour the palette carries, and an outline in either direction colour
172
+ // would vanish on half the cells it might land on.
173
+ const best = layout.cells.find(shape => shape.best);
174
+ if (best !== undefined) {
175
+ ctx.globalAlpha = 1;
176
+ ctx.strokeStyle = palette.grid;
177
+ ctx.lineWidth = BEST_WIDTH;
178
+ OptimizationHeatmapWidget._outline(ctx, best.rect, BEST_WIDTH / 2);
179
+ ctx.lineWidth = 1;
180
+ }
181
+ // The key, drawn out of the same two colours the map is: a legend in CSS would read the
182
+ // page's own tokens, and a host whose canvas palette comes from anywhere else would then
183
+ // explain its map in colours the map does not use.
184
+ for (const step of layout.legend.steps)
185
+ this._paintTint(ctx, step.rect, step.tint, palette.grid, palette.up, palette.down);
186
+ ctx.globalAlpha = LABEL_ALPHA;
187
+ ctx.fillStyle = palette.grid;
188
+ ctx.textBaseline = 'top';
189
+ ctx.textAlign = 'center';
190
+ for (const tick of layout.xTicks)
191
+ ctx.fillText(tick.text, tick.x, tick.y);
192
+ ctx.fillText(layout.xTitle.text, layout.xTitle.x, layout.xTitle.y);
193
+ ctx.fillText(formatStatistic(layout.legend.anchor.value), layout.legend.anchor.x, layout.legend.anchor.y);
194
+ ctx.textAlign = 'left';
195
+ ctx.fillText(layout.yTitle.text, layout.yTitle.x, layout.yTitle.y);
196
+ ctx.fillText(formatStatistic(layout.legend.low.value), layout.legend.low.x, layout.legend.low.y);
197
+ ctx.textAlign = 'right';
198
+ ctx.fillText(formatStatistic(layout.legend.high.value), layout.legend.high.x, layout.legend.high.y);
199
+ ctx.textBaseline = 'middle';
200
+ for (const tick of layout.yTicks)
201
+ ctx.fillText(tick.text, tick.x, tick.y);
202
+ ctx.globalAlpha = 1;
203
+ }
204
+ // The ground, then as much of the direction colour as the cell earned. Two fills rather than
205
+ // one blended colour: blending would need a background this control is not allowed to know.
206
+ _paintTint(ctx, rect, tint, ground, up, down) {
207
+ ctx.globalAlpha = GROUND_ALPHA;
208
+ ctx.fillStyle = ground;
209
+ ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
210
+ if (tint === 0)
211
+ return;
212
+ ctx.globalAlpha = Math.abs(tint);
213
+ ctx.fillStyle = tint > 0 ? up : down;
214
+ ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
215
+ }
216
+ static _outline(ctx, rect, inset) {
217
+ const left = rect.x + inset;
218
+ const top = rect.y + inset;
219
+ const right = rect.x + rect.width - inset;
220
+ const bottom = rect.y + rect.height - inset;
221
+ ctx.beginPath();
222
+ ctx.moveTo(left, top);
223
+ ctx.lineTo(right, top);
224
+ ctx.lineTo(right, bottom);
225
+ ctx.lineTo(left, bottom);
226
+ ctx.closePath();
227
+ ctx.stroke();
228
+ }
229
+ _setEmpty(empty) {
230
+ if (this._emptyEl === null)
231
+ return;
232
+ if (empty)
233
+ this._emptyEl.removeAttribute('hidden');
234
+ else
235
+ this._emptyEl.setAttribute('hidden', '');
236
+ }
237
+ _bindHover() {
238
+ const canvas = this.canvasEl;
239
+ if (!canvas)
240
+ return;
241
+ canvas.addEventListener('mousemove', (e) => {
242
+ const layout = this._layout;
243
+ if (layout === null) {
244
+ this._hideTooltip();
245
+ return;
246
+ }
247
+ const rect = canvas.getBoundingClientRect();
248
+ const x = e.clientX - rect.left;
249
+ const y = e.clientY - rect.top;
250
+ const hit = hitHeatmap(layout, x, y);
251
+ // A pair nobody ran says nothing, so it says nothing: a tooltip reading "no data"
252
+ // over a cell that is visibly struck through is the same statement twice.
253
+ if (hit !== null)
254
+ this._showTooltip(hit, x, y);
255
+ else
256
+ this._hideTooltip();
257
+ });
258
+ canvas.addEventListener('mouseleave', () => this._hideTooltip());
259
+ }
260
+ _showTooltip(shape, x, y) {
261
+ const tooltip = this._tooltipEl;
262
+ const canvas = this.canvasEl;
263
+ const data = this._data;
264
+ if (!tooltip || !canvas || data === null)
265
+ return;
266
+ const lines = [
267
+ this._tooltipLine(data.xLabel, shape.bucket.x),
268
+ this._tooltipLine(data.yLabel, shape.bucket.y),
269
+ this._tooltipLine(data.metricLabel, formatStatistic(shape.bucket.value)),
270
+ ];
271
+ // How many runs the figure is the mean of, and only when it is a mean of more than one.
272
+ // A count of 1 on every cell is a column of noise; a count of 5 on one of them is the
273
+ // difference between a result and a coincidence.
274
+ if (shape.bucket.count > 1)
275
+ lines.push(this._tooltipLine(this._host.t('Runs'), String(shape.bucket.count)));
276
+ if (shape.best)
277
+ lines.push(makeElement('div', 'heatmap-tt-row heatmap-tt-best', {}, [this._host.t('Best')]));
278
+ tooltip.replaceChildren(...lines);
279
+ // Measured, then placed: the size is only known once it is showing, and it is clamped
280
+ // inside the canvas so a cell near the right or bottom edge does not push its own
281
+ // tooltip out of view.
282
+ tooltip.removeAttribute('hidden');
283
+ const canvasRect = canvas.getBoundingClientRect();
284
+ const parentRect = tooltip.offsetParent?.getBoundingClientRect() ?? canvasRect;
285
+ const originX = canvasRect.left - parentRect.left;
286
+ const originY = canvasRect.top - parentRect.top;
287
+ const maxX = originX + canvasRect.width - tooltip.offsetWidth - TOOLTIP_MARGIN;
288
+ const maxY = originY + canvasRect.height - tooltip.offsetHeight - TOOLTIP_MARGIN;
289
+ tooltip.style.left = `${Math.max(0, Math.min(originX + x + TOOLTIP_OFFSET, maxX))}px`;
290
+ tooltip.style.top = `${Math.max(0, Math.min(originY + y + TOOLTIP_OFFSET, maxY))}px`;
291
+ }
292
+ _tooltipLine(label, value) {
293
+ return makeElement('div', 'heatmap-tt-row', {}, [
294
+ makeElement('span', 'heatmap-tt-label', {}, [label]),
295
+ makeElement('span', 'heatmap-tt-value', {}, [value]),
296
+ ]);
297
+ }
298
+ _hideTooltip() {
299
+ this._tooltipEl?.setAttribute('hidden', '');
300
+ }
301
+ }
302
+ OptimizationHeatmapWidget.TYPE = ControlTypes.OptimizationHeatmap;
303
+ //# sourceMappingURL=optimization-heatmap-widget.js.map