@ixfx/components 0.1.5 → 0.2.1

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.
@@ -6,13 +6,82 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { css, html, LitElement } from "lit";
8
8
  import { customElement, property } from "lit/decorators.js";
9
- //import { property } from "lit/decorators/property.js";
10
9
  import { createRef, ref } from "lit/directives/ref.js";
11
10
  import { CanvasHelper } from "ixfx/visual.js";
11
+ import { Rects } from "ixfx/geometry.js";
12
+ import * as Arrays from 'ixfx/arrays.js';
12
13
  import * as Numbers from 'ixfx/numbers.js';
13
14
  import { getCssVariable } from 'ixfx/dom.js';
14
15
  import { Drawing, Colour } from "ixfx/visual.js";
15
16
  import { Pathed } from 'ixfx';
17
+ const rangeIsEqual = (a, b) => {
18
+ if (a === undefined || b === undefined)
19
+ return false;
20
+ return (a.max === b.max && a.min === b.min);
21
+ };
22
+ export class PlotAxis {
23
+ // units: Readonly<{ major: number, minor: number }> = { major: Number.NaN, minor: Number.NaN }
24
+ // altAxisCrossing: AxisCrossing<T> = { at: `min` }
25
+ // ticks: TickMarks = { major: `inside`, minor: `inside` }
26
+ // labels: AxisLabels = `axis`;
27
+ #currentScalerRange;
28
+ #persistentScale;
29
+ #currentScaler;
30
+ constructor(name) {
31
+ this.name = name;
32
+ this.bounds = `auto`;
33
+ this.#persistentScale = Numbers.rangeInit();
34
+ this.#currentScaler = (v) => v;
35
+ }
36
+ humanFormatValue(value) {
37
+ if (value.length === 0)
38
+ return ``;
39
+ const v = value[0];
40
+ if (v < 2)
41
+ return v.toFixed(2);
42
+ return v.toString();
43
+ }
44
+ getRange(series) {
45
+ if (this.bounds === `auto`)
46
+ return this.computeActualRange(series);
47
+ if (this.bounds === `auto-persistent`) {
48
+ const r = this.computeActualRange(series);
49
+ this.#persistentScale = Numbers.rangeMergeRange(r, this.#persistentScale);
50
+ return this.#persistentScale;
51
+ }
52
+ return this.bounds;
53
+ }
54
+ getCurrentRange() {
55
+ return this.#currentScalerRange;
56
+ }
57
+ getValueScaler(series) {
58
+ const r = this.getRange(series);
59
+ if (!rangeIsEqual(r, this.#currentScalerRange)) {
60
+ this.#currentScalerRange = r;
61
+ this.#currentScaler = Numbers.scaler(r.min, r.max, 0, 1);
62
+ }
63
+ return this.#currentScaler;
64
+ }
65
+ computeActualRange(seriesOnAxis, column = 0) {
66
+ let min = Number.MAX_SAFE_INTEGER;
67
+ let max = Number.MIN_SAFE_INTEGER;
68
+ let count = 0;
69
+ for (const s of seriesOnAxis) {
70
+ for (const [v, _index] of s.getValuesForColumn(column)) {
71
+ if (typeof v !== `number`)
72
+ continue;
73
+ if (Number.isNaN(v))
74
+ continue;
75
+ if (v < min)
76
+ min = v;
77
+ if (v > max)
78
+ max = v;
79
+ }
80
+ count++;
81
+ }
82
+ return { min, max };
83
+ }
84
+ }
16
85
  /**
17
86
  * Attributes
18
87
  * * streaming: true/false (default: true)
@@ -22,7 +91,7 @@ import { Pathed } from 'ixfx';
22
91
  *
23
92
  * * line-width: stroke width of drawing line (default:2)
24
93
  *
25
- * * render: 'dot' or 'line' (default: 'dot')
94
+ * * render: 'dot', 'line' or 'bar'(default: 'dot')
26
95
  * * hide-legend: If added, legend is not shown
27
96
  * * manual-draw: If added, automatic drawning is disabled
28
97
  *
@@ -31,36 +100,107 @@ import { Pathed } from 'ixfx';
31
100
  */
32
101
  let PlotElement = class PlotElement extends LitElement {
33
102
  constructor() {
103
+ //@property({ attribute: `streaming`, type: Boolean })
104
+ //streaming = true;
34
105
  super(...arguments);
35
- this.streaming = true;
36
106
  this.hideLegend = false;
37
- this.maxLength = 500;
38
- this.dataWidth = 5;
39
- this.fixedMax = Number.NaN;
40
- this.fixedMin = Number.NaN;
41
- this.lineWidth = 2;
107
+ //@property({ attribute: `max-length`, type: Number })
108
+ //maxLength = 500;
109
+ //@property({ attribute: `data-width`, type: Number })
110
+ //dataWidth = 5;
111
+ //@property({ attribute: `fixed-max`, type: Number })
112
+ //fixedMax = Number.NaN;
113
+ //@property({ attribute: `fixed-min`, type: Number })
114
+ //fixedMin = Number.NaN;
115
+ //@property({ attribute: `line-width`, type: Number })
116
+ //lineWidth = 2;
42
117
  this.renderStyle = `dot`;
43
- this.manualDraw = false;
118
+ //@property({ attribute: `manual-draw`, type: Boolean })
119
+ //manualDraw = false;
44
120
  this.padding = 5;
121
+ this.primaryAxis = new PlotAxis(`primary`);
122
+ this.secondaryAxis = new PlotAxis(`secondary`);
45
123
  this.paused = false;
124
+ this.plotAreaFormat = {
125
+ fill: { type: `none` },
126
+ outline: { type: `none` }
127
+ };
128
+ /**
129
+ * Settings used for automatically generating series colour
130
+ */
131
+ this.automaticSeriesColour = {
132
+ saturation: 1,
133
+ lightness: 0.5,
134
+ alpha: 1
135
+ };
136
+ this.seriesDefault = {};
46
137
  this.#series = new Map();
47
138
  this.#legendColour = ``;
48
- this.#hue = 0;
49
- this.canvasEl = createRef();
50
- this.seriesRanges = new Map();
139
+ this.#autoSeriesColour = 0;
140
+ this.#hitboxes = [];
141
+ this.#seriesFormatting = new Map();
51
142
  this.#swatchSize = 10;
143
+ this.canvasEl = createRef();
144
+ this.tooltipEl = createRef();
145
+ this.seriesColourGenerate = (_seriesName) => {
146
+ const c = Colour.goldenAngleColour(this.#autoSeriesColour, this.automaticSeriesColour.saturation, this.automaticSeriesColour.lightness, this.automaticSeriesColour.alpha);
147
+ this.#autoSeriesColour++;
148
+ return c;
149
+ };
52
150
  }
53
151
  #series;
54
152
  #canvas;
55
153
  #drawing;
56
154
  #legendColour;
57
- #hue;
155
+ #autoSeriesColour;
156
+ #hitboxes;
157
+ #seriesFormatting;
158
+ #swatchSize;
58
159
  get series() {
59
160
  return [...this.#series.values()];
60
161
  }
61
162
  get seriesCount() {
62
163
  return this.#series.size;
63
164
  }
165
+ getSeriesFormatting(seriesName, createIfNeeded) {
166
+ let f = this.#seriesFormatting.get(seriesName);
167
+ if (!f && createIfNeeded) {
168
+ f = {
169
+ axis: `primary`,
170
+ fill: {
171
+ colour: this.seriesColourGenerate(seriesName),
172
+ type: `solid`
173
+ },
174
+ outline: {
175
+ type: `none`
176
+ },
177
+ bar: {
178
+ gapWidth: 10
179
+ },
180
+ dot: {
181
+ gapWidth: 0,
182
+ radius: `automatic`
183
+ },
184
+ line: {
185
+ width: 2,
186
+ cap: `round`,
187
+ join: `round`
188
+ }
189
+ };
190
+ this.#seriesFormatting.set(seriesName, f);
191
+ }
192
+ return f;
193
+ }
194
+ setSeriesFormatting(seriesName, formatting) {
195
+ let existing = this.getSeriesFormatting(seriesName, true);
196
+ const combined = {
197
+ ...existing,
198
+ ...formatting
199
+ };
200
+ combined.fill = resolveFillDrawStyle(combined.fill);
201
+ combined.outline = resolveOutlineDrawStyle(combined.outline);
202
+ this.#seriesFormatting.set(seriesName, combined);
203
+ }
64
204
  /**
65
205
  * Returns a `PlotElement` instance based on a query
66
206
  * ```js
@@ -111,6 +251,7 @@ let PlotElement = class PlotElement extends LitElement {
111
251
  */
112
252
  clear() {
113
253
  this.#series.clear();
254
+ this.#autoSeriesColour = 0;
114
255
  }
115
256
  /**
116
257
  * Keeps all series, but deletes their data
@@ -120,8 +261,54 @@ let PlotElement = class PlotElement extends LitElement {
120
261
  s.clear();
121
262
  }
122
263
  }
123
- render() {
124
- return html `<canvas ${ref(this.canvasEl)}></canvas>`;
264
+ #lastPointerPosition;
265
+ #onPointerLeave() {
266
+ this.#lastPointerPosition = undefined;
267
+ this.#hideTooltip();
268
+ }
269
+ #onPointerMove(event) {
270
+ this.#lastPointerPosition = {
271
+ x: event.offsetX,
272
+ screenX: event.x,
273
+ y: event.offsetY,
274
+ screenY: event.y
275
+ };
276
+ this.#updateToolTip();
277
+ }
278
+ #updateToolTip() {
279
+ if (!this.#lastPointerPosition)
280
+ return;
281
+ for (const hb of this.#hitboxes) {
282
+ if (Rects.intersectsPoint(hb, this.#lastPointerPosition)) {
283
+ if (hb.value) {
284
+ this.#setTooltip(hb.series.humanFormatValue(hb.value));
285
+ }
286
+ else {
287
+ this.#setTooltip(``);
288
+ }
289
+ return;
290
+ }
291
+ }
292
+ this.#hideTooltip();
293
+ }
294
+ #hideTooltip() {
295
+ const v = this.tooltipEl.value;
296
+ if (!v)
297
+ return;
298
+ v.classList.add(`hidden`);
299
+ }
300
+ #setTooltip(innerHtml) {
301
+ const v = this.tooltipEl.value;
302
+ if (!v)
303
+ return;
304
+ const pos = this.#lastPointerPosition;
305
+ v.classList.remove(`hidden`);
306
+ const offset = 10;
307
+ v.innerHTML = innerHtml;
308
+ if (pos) {
309
+ v.style.left = `${pos.screenX + offset}px`;
310
+ v.style.top = `${pos.screenY + offset}px`;
311
+ }
125
312
  }
126
313
  #setupCanvas() {
127
314
  if (this.#canvas !== undefined)
@@ -138,62 +325,91 @@ let PlotElement = class PlotElement extends LitElement {
138
325
  super.connectedCallback();
139
326
  }
140
327
  firstUpdated(_changedProperties) {
141
- // const canvas = this.canvasEl.value!;
142
328
  const ro = new ResizeObserver((event) => {
143
329
  const c = this.#setupCanvas();
144
330
  if (!c)
145
331
  return;
146
332
  const entry = event[0];
147
333
  c.setLogicalSize(entry.contentRect);
334
+ if (this.#hasDirty())
335
+ this.draw();
148
336
  });
149
337
  ro.observe(this);
150
338
  this.updateColours();
339
+ //this.#setupCanvas();
340
+ }
341
+ #hasDirty() {
342
+ for (const s of this.series.values()) {
343
+ if (s.isDirty)
344
+ return true;
345
+ }
346
+ return false;
151
347
  }
152
348
  updateColours() {
153
349
  this.#legendColour = getCssVariable(`legend-fg`, `black`);
154
350
  }
155
- plot(value, seriesName = ``, skipDrawing = false) {
156
- if (typeof value !== 'number')
157
- throw new TypeError(`Can only add numbers. Got: ${typeof value}`);
158
- let s = this.#series.get(seriesName.toLowerCase());
159
- if (s === undefined) {
160
- s = new PlotSeries(seriesName, this.colourGenerator(seriesName), this);
161
- this.#series.set(seriesName.toLowerCase(), s);
162
- }
163
- s.push(value);
164
- if (!this.manualDraw && !skipDrawing)
165
- this.draw();
166
- return s;
167
- }
168
- plotSeries(values, seriesName = ``, skipDrawing = false) {
351
+ //plot(value: number, seriesName = ``, skipDrawing = false) {
352
+ // plotValue(primary: number | string, secondary: number | string, seriesName: string, skipDrawing = false) {
353
+ // //if (typeof primary !== 'number' && typeof primary !== `string`) throw new TypeError(`Can only add numbers. Got: ${ typeof value }`);
354
+ // let s = this.#series.get(seriesName.toLowerCase());
355
+ // if (s === undefined) {
356
+ // s = new PlotSeries(seriesName, this);
357
+ // this.#series.set(seriesName.toLowerCase(), s);
358
+ // }
359
+ // const v: DataValue = { primary, secondary };
360
+ // //s.push(value);
361
+ // s.pushValue(v)
362
+ // if (!this.manualDraw && !skipDrawing) this.draw();
363
+ // return s;
364
+ // }
365
+ setRawValues(values, seriesName, automaticallyDraw) {
169
366
  if (!Array.isArray(values))
170
367
  throw new TypeError(`Param 'values' is not an array`);
171
- let s = this.#series.get(seriesName.toLowerCase());
172
- if (s === undefined) {
173
- s = new PlotSeries(seriesName, this.colourGenerator(seriesName), this);
174
- this.#series.set(seriesName.toLowerCase(), s);
175
- }
176
- s.setValues(values);
177
- if (!this.manualDraw && !skipDrawing)
178
- this.draw();
368
+ let s = this.getOrCreateSeries(seriesName);
369
+ s.setRawValues(values, automaticallyDraw);
179
370
  return s;
180
371
  }
181
372
  /**
182
- * Draw a set of key-value pairs as a batch.
373
+ * Treats each property of an object as the series name
183
374
  * @param value
375
+ * @param automaticallyDraw
184
376
  */
185
- plotObject(value) {
377
+ appendObjectBySeries(value, automaticallyDraw) {
186
378
  for (const p of Pathed.getPathsAndData(value, true)) {
187
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
188
- this.plot(p.value, p.path, true);
379
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
380
+ const v = p.value;
381
+ if (typeof v === `number`) {
382
+ this.appendRawValues(v, p.path, false);
383
+ }
189
384
  }
190
- this.draw();
385
+ if (automaticallyDraw)
386
+ this.draw();
191
387
  }
192
- colourGenerator(_series) {
193
- const c = Colour.HslSpace.scalar(this.#hue, 0.9, 0.4);
194
- this.#hue = Numbers.wrap(this.#hue + 0.1);
195
- return c;
388
+ appendRawValues(values, seriesName, automaticallyDraw) {
389
+ if (!Array.isArray(values))
390
+ values = [values];
391
+ let s = this.getOrCreateSeries(seriesName);
392
+ for (const v of values) {
393
+ s.pushValue(v, false);
394
+ }
395
+ if (automaticallyDraw)
396
+ this.draw();
196
397
  }
398
+ /**
399
+ * Draw a set of key-value pairs as a batch.
400
+ * @param value
401
+ */
402
+ // plotObject(value: object) {
403
+ // for (const p of Pathed.getPathsAndData(value, true)) {
404
+ // this.plot(p.value, p.path, true);
405
+ // }
406
+ // this.draw();
407
+ // }
408
+ // colourGenerator(_series: string): Colour.Colourish {
409
+ // const c = Colour.HslSpace.scalar(this.#hue, 0.9, 0.4);
410
+ // this.#hue = Numbers.wrap(this.#hue + 0.1);
411
+ // return c;
412
+ // }
197
413
  draw() {
198
414
  if (this.paused)
199
415
  return;
@@ -206,16 +422,11 @@ let PlotElement = class PlotElement extends LitElement {
206
422
  const ctx = d.ctx;
207
423
  const axisYwidth = this.computeAxisYWidth(c);
208
424
  const remainingWidth = c.width - axisYwidth;
209
- // Legend across bottom
210
425
  const clLegend = this.computeLegend(c, remainingWidth, padding);
211
426
  const plotHeight = c.height - clLegend.bounds.height - padding;
212
427
  const cy = { width: axisYwidth, height: plotHeight };
213
- const cp = this.computePlot(c, plotHeight, cy.width, padding);
214
- const cl = { ...clLegend.bounds, x: cy.width, y: cp.y + cp.height + padding };
215
- let globalScaler;
216
- if (!Number.isNaN(this.fixedMax) && !Number.isNaN(this.fixedMin)) {
217
- globalScaler = Numbers.scaler(this.fixedMin, this.fixedMax);
218
- }
428
+ const plotDataArea = this.calculatePlotDataArea(c, plotHeight, cy.width, padding);
429
+ const cl = { ...clLegend.bounds, x: cy.width, y: plotDataArea.y + plotDataArea.height + padding };
219
430
  // Draw legend
220
431
  if (!this.hideLegend) {
221
432
  ctx.save();
@@ -224,28 +435,38 @@ let PlotElement = class PlotElement extends LitElement {
224
435
  ctx.restore();
225
436
  }
226
437
  ctx.save();
227
- ctx.translate(cp.x + padding, cp.y + padding);
228
- //ctx.fillStyle = `whitesmoke`;
229
- //ctx.fillRect(0, 0, cp.width, cp.height);
438
+ ctx.translate(plotDataArea.x + padding, plotDataArea.y + padding);
439
+ if (this.plotAreaFormat.fill.type === `solid`) {
440
+ ctx.fillStyle = Colour.resolveCss(this.plotAreaFormat.fill.colour);
441
+ ctx.fillRect(0, 0, plotDataArea.width, plotDataArea.height);
442
+ }
443
+ if (this.plotAreaFormat.outline.type === `solid`) {
444
+ ctx.strokeStyle = Colour.resolveCss(this.plotAreaFormat.outline.colour);
445
+ ctx.strokeRect(0, 0, plotDataArea.width, plotDataArea.height);
446
+ }
447
+ // Compute axis scaling
448
+ const primaryAxisS = this.primaryAxis.getValueScaler(this.#series.values().filter(s => s.onPrimaryAxis));
449
+ const secondaryAxisAxisS = this.secondaryAxis.getValueScaler(this.#series.values().filter(s => !s.onPrimaryAxis));
230
450
  // Draw data
451
+ this.#hitboxes = [];
231
452
  for (const series of this.#series.values()) {
232
- const seriesScale = this.seriesRanges.get(series.name);
233
- const data = seriesScale === undefined ? (globalScaler === undefined ?
234
- series.getScaled() :
235
- series.getScaledBy(globalScaler)) :
236
- series.getScaledBy(Numbers.scaler(seriesScale[0], seriesScale[1]));
237
- const colour = Colour.toCssColour(series.colour);
238
453
  switch (this.renderStyle) {
239
454
  case `line`: {
240
- this.drawLineSeries(data, cp, d, colour);
455
+ this.#drawLineSeries(series, d, series.onPrimaryAxis ? primaryAxisS : secondaryAxisAxisS, plotDataArea, { x: padding, y: padding });
456
+ break;
457
+ }
458
+ case `bar`: {
459
+ this.#drawBarSeries(series, d, series.onPrimaryAxis ? primaryAxisS : secondaryAxisAxisS, plotDataArea, { x: padding, y: padding });
241
460
  break;
242
461
  }
243
462
  default: {
244
- this.drawDotSeries(data, cp, d, colour);
463
+ this.#drawDotSeries(series, d, series.onPrimaryAxis ? primaryAxisS : secondaryAxisAxisS, plotDataArea, { x: padding, y: padding });
245
464
  }
246
465
  }
466
+ series.isDirty = false;
247
467
  }
248
468
  ctx.restore();
469
+ this.#updateToolTip();
249
470
  }
250
471
  drawLegend(cl, d) {
251
472
  const textColour = this.#legendColour;
@@ -255,7 +476,9 @@ let PlotElement = class PlotElement extends LitElement {
255
476
  const swatchSize = 10;
256
477
  const ctx = d.ctx;
257
478
  for (const series of this.#series.values()) {
258
- ctx.fillStyle = Colour.toCssColour(series.colour);
479
+ const f = series.getFormatting();
480
+ const colour = (f.fill.type === `solid`) ? f.fill.colour : `transparent`;
481
+ ctx.fillStyle = Colour.toCssColour(colour);
259
482
  ctx.fillRect(x, y, swatchSize, swatchSize);
260
483
  ctx.fillStyle = textColour;
261
484
  x += swatchSize + padding;
@@ -269,63 +492,195 @@ let PlotElement = class PlotElement extends LitElement {
269
492
  }
270
493
  }
271
494
  }
272
- drawLineSeries(data, cp, d, colour) {
273
- const pointWidth = this.streaming ? this.dataWidth : (cp.width / data.length);
495
+ // #drawBarSeries(data: number[], cp: Rect, d: DrawingHelper, series: PlotSeries) {
496
+ // const barWidth = this.streaming ? this.dataWidth : (cp.width / data.length);
497
+ // const formatting = series.getFormatting();
498
+ // let x = 0;
499
+ // if (this.streaming) x = cp.width - (barWidth * data.length);
500
+ // const rects = data.map(d => {
501
+ // x += barWidth;
502
+ // const h = (d) * cp.height;
503
+ // return {
504
+ // x: x,
505
+ // y: cp.height - h,
506
+ // height: h,
507
+ // width: barWidth,
508
+ // value: d
509
+ // };
510
+ // })
511
+ // this.#hitboxes = rects.map(r => {
512
+ // return {
513
+ // ...r,
514
+ // series: series.name
515
+ // }
516
+ // })
517
+ // let drawOptions: { stroke: boolean, filled: boolean, strokeWidth?: number, strokeStyle?: string, fillStyle?: string } = {
518
+ // stroke: false,
519
+ // filled: false
520
+ // }
521
+ // if (formatting.outline.type === `solid`) {
522
+ // drawOptions.stroke = true;
523
+ // drawOptions.strokeStyle = formatting.outline.colour;
524
+ // drawOptions.strokeWidth = 1;
525
+ // }
526
+ // if (formatting.fill.type === `solid`) {
527
+ // drawOptions.filled = true;
528
+ // drawOptions.fillStyle = formatting.fill.colour;
529
+ // }
530
+ // d.rect(rects, drawOptions);
531
+ // }
532
+ #drawLineSeries(series, d, scaler, plotDataArea, plotDataAreaOffset) {
533
+ const formatting = series.getFormatting();
534
+ const lineFormatting = formatting.line;
535
+ const widthPerValue = plotDataArea.width / series.length;
536
+ const xOffset = widthPerValue / 2;
537
+ const lineThickness = lineFormatting.width;
538
+ const lineThicknessHalf = lineThickness / 2;
539
+ const h = plotDataArea.height;
274
540
  let x = 0;
275
- if (this.streaming)
276
- x = cp.width - (pointWidth * data.length);
277
- const pos = data.map(d => {
278
- x += pointWidth;
541
+ const dotsToDraw = [...series.getValuesForColumn(0)].map(d => {
542
+ const [value, index] = d;
543
+ x += widthPerValue;
544
+ const scaledValue = scaler(value);
545
+ if (Number.isNaN(value))
546
+ return undefined;
279
547
  return {
280
- x: x,
281
- y: (1 - d) * cp.height,
282
- radius: pointWidth
548
+ x: x - xOffset,
549
+ y: (1 - scaledValue) * h,
550
+ value, index
283
551
  };
284
552
  });
285
- // Ignore points off the screen
286
- const trimmed = pos.filter(p => {
287
- if (p.x < 0)
288
- return false;
289
- return true;
290
- });
291
- d.connectedPoints(trimmed, {
292
- strokeStyle: colour,
293
- lineWidth: this.lineWidth
294
- });
295
- }
296
- drawDotSeries(data, cp, d, colour) {
297
- const pointWidth = this.streaming ? this.dataWidth : cp.width / data.length;
553
+ const filtered = dotsToDraw.filter(f => f !== undefined);
554
+ const hitboxes = filtered.map(d => ({
555
+ height: lineThickness,
556
+ width: widthPerValue,
557
+ x: plotDataAreaOffset.x + d.x - lineThicknessHalf,
558
+ y: plotDataAreaOffset.y + d.y - lineThicknessHalf,
559
+ series,
560
+ index: d.index,
561
+ value: series.getValue(d.index),
562
+ }));
563
+ this.#hitboxes.push(...hitboxes);
564
+ const drawOpts = {
565
+ filled: false,
566
+ fillStyle: `transparent`,
567
+ stroked: true,
568
+ strokeStyle: `black`,
569
+ strokeWidth: lineFormatting.width
570
+ };
571
+ if (formatting.outline.type === `solid`) {
572
+ drawOpts.strokeStyle = formatting.outline.colour;
573
+ }
574
+ else if (formatting.fill.type === `solid`) {
575
+ drawOpts.strokeStyle = formatting.fill.colour;
576
+ }
577
+ d.ctx.lineWidth = lineFormatting.width;
578
+ d.ctx.lineCap = lineFormatting.cap;
579
+ d.ctx.lineJoin = lineFormatting.join;
580
+ d.connectedPoints(filtered, drawOpts);
581
+ //d.textBlock([ `hello` ], { fillStyle: `black`, anchor: { x: 1, y: 1 } })
582
+ }
583
+ #drawDotSeries(series, d, scaler, plotDataArea, plotDataAreaOffset) {
584
+ const formatting = series.getFormatting();
585
+ const dotFormatting = formatting.dot;
586
+ const widthPerValue = plotDataArea.width / series.length;
587
+ const radius = dotFormatting.radius === `automatic` ? widthPerValue * 0.3 : dotFormatting.radius;
588
+ const xOffset = widthPerValue / 2;
589
+ const h = plotDataArea.height;
298
590
  let x = 0;
299
- if (this.streaming)
300
- x = cp.width - (pointWidth * data.length);
301
- const pos = data.map(d => {
302
- x += pointWidth;
591
+ const dotsToDraw = [...series.getValuesForColumn(0)].map(d => {
592
+ const [value, index] = d;
593
+ x += widthPerValue;
594
+ const scaledValue = scaler(value);
595
+ if (Number.isNaN(value))
596
+ return undefined;
303
597
  return {
304
- x,
305
- y: (1 - d) * cp.height,
306
- radius: pointWidth
598
+ x: x - xOffset,
599
+ y: (1 - scaledValue) * h,
600
+ radius: radius,
601
+ value, index
307
602
  };
308
603
  });
309
- // Ignore points off the screen
310
- const trimmed = pos.filter(p => {
311
- if (p.x < 0)
312
- return false;
313
- return true;
604
+ const filtered = dotsToDraw.filter(f => f !== undefined);
605
+ const hitboxes = filtered.map(d => ({
606
+ height: d.radius * 2,
607
+ width: d.radius * 2,
608
+ x: plotDataAreaOffset.x + d.x - d.radius,
609
+ y: plotDataAreaOffset.y + d.y - d.radius,
610
+ series,
611
+ index: d.index,
612
+ value: series.getValue(d.index),
613
+ }));
614
+ this.#hitboxes.push(...hitboxes);
615
+ const drawOpts = { filled: false, fillStyle: `transparent`, stroked: false, strokeStyle: `transparent` };
616
+ if (formatting.fill.type === `solid`) {
617
+ drawOpts.filled = true;
618
+ drawOpts.fillStyle = formatting.fill.colour;
619
+ }
620
+ if (formatting.outline.type === `solid`) {
621
+ drawOpts.stroked = true;
622
+ drawOpts.strokeStyle = formatting.outline.colour;
623
+ }
624
+ d.dot(filtered, drawOpts);
625
+ //d.textBlock([ `hello` ], { fillStyle: `black`, anchor: { x: 1, y: 1 } })
626
+ }
627
+ #drawBarSeries(series, d, scaler, plotDataArea, plotDataAreaOffset) {
628
+ const formatting = series.getFormatting();
629
+ const barFormatting = formatting.bar;
630
+ const widthPerValue = plotDataArea.width / series.length;
631
+ const rectWidth = widthPerValue - (barFormatting.gapWidth / 2);
632
+ const xOffset = widthPerValue / 2;
633
+ const h = plotDataArea.height;
634
+ let x = 0;
635
+ const rectsToDraw = [...series.getValuesForColumn(0)].map(d => {
636
+ const [value, index] = d;
637
+ x += widthPerValue;
638
+ const scaledValue = scaler(value);
639
+ if (Number.isNaN(value))
640
+ return undefined;
641
+ const barHeight = scaledValue * h;
642
+ return {
643
+ x: x - xOffset,
644
+ y: h - barHeight,
645
+ width: rectWidth,
646
+ height: barHeight,
647
+ value, index
648
+ };
314
649
  });
315
- d.dot(trimmed, { filled: true, fillStyle: colour });
650
+ const filtered = rectsToDraw.filter(f => f !== undefined);
651
+ const hitboxes = filtered.map(d => ({
652
+ height: Math.max(5, d.height),
653
+ width: d.width,
654
+ x: plotDataAreaOffset.x + d.x,
655
+ y: plotDataAreaOffset.y + d.y,
656
+ series,
657
+ index: d.index,
658
+ value: series.getValue(d.index),
659
+ }));
660
+ this.#hitboxes.push(...hitboxes);
661
+ const drawOpts = { filled: false, fillStyle: `transparent`, stroked: false, strokeStyle: `transparent` };
662
+ if (formatting.fill.type === `solid`) {
663
+ drawOpts.filled = true;
664
+ drawOpts.fillStyle = formatting.fill.colour;
665
+ }
666
+ if (formatting.outline.type === `solid`) {
667
+ drawOpts.stroked = true;
668
+ drawOpts.strokeStyle = formatting.outline.colour;
669
+ }
670
+ d.rect(filtered, drawOpts);
671
+ //d.textBlock([ `hello` ], { fillStyle: `black`, anchor: { x: 1, y: 1 } })
316
672
  }
317
- computePlot(c, plotHeight, axisYwidth, padding) {
673
+ calculatePlotDataArea(c, plotHeight, axisYwidth, padding) {
318
674
  return {
319
675
  x: axisYwidth,
320
676
  y: 0,
321
- width: c.width - axisYwidth - padding,
322
- height: plotHeight - padding - padding
677
+ width: c.width - axisYwidth - padding - padding,
678
+ height: plotHeight - padding
323
679
  };
324
680
  }
325
681
  computeAxisYWidth(_c) {
326
682
  return 0;
327
683
  }
328
- #swatchSize;
329
684
  computeLegend(c, maxWidth, padding) {
330
685
  if (this.hideLegend) {
331
686
  return {
@@ -367,109 +722,207 @@ let PlotElement = class PlotElement extends LitElement {
367
722
  bounds, parts
368
723
  };
369
724
  }
725
+ getOrCreateSeries(name) {
726
+ let s = this.#series.get(name.toLowerCase());
727
+ if (s)
728
+ return s;
729
+ s = new PlotSeries(name, this);
730
+ const seriesDefault = {
731
+ limit: Number.NaN,
732
+ ...this.seriesDefault
733
+ };
734
+ if (!Number.isNaN(seriesDefault.limit)) {
735
+ s.setCapacityLimit(seriesDefault.limit);
736
+ }
737
+ this.#series.set(name.toLowerCase(), s);
738
+ return s;
739
+ }
370
740
  getSeries(name) {
371
741
  return this.#series.get(name);
372
742
  }
743
+ render() {
744
+ // eslint-disable-next-line @typescript-eslint/unbound-method
745
+ return html `<canvas @pointerleave="${this.#onPointerLeave}" @pointermove="${this.#onPointerMove}" ${ref(this.canvasEl)}></canvas><div class="hidden" ${ref(this.tooltipEl)} id="tooltip">Tooltip!</div>`;
746
+ }
373
747
  static { this.styles = css `
374
748
  :host {
375
749
  width: 100%;
376
750
  height: 100%;
377
751
  display: block;
378
752
  }
753
+
754
+ #tooltip {
755
+ border: 1px solid black;
756
+ background: white;
757
+ color: black;
758
+ padding: 0.3em;
759
+ font-size: 10px;
760
+ display: inline-block;
761
+ position: absolute;
762
+ left: 0px;
763
+ top: 0px;
764
+ opacity:1;
765
+ pointer-events: none;
766
+ }
767
+
768
+ .hidden {
769
+ opacity:0 !important;
770
+ }
379
771
  `; }
380
772
  };
381
- __decorate([
382
- property({ attribute: `streaming`, type: Boolean })
383
- ], PlotElement.prototype, "streaming", void 0);
384
773
  __decorate([
385
774
  property({ attribute: `hide-legend`, type: Boolean })
386
775
  ], PlotElement.prototype, "hideLegend", void 0);
387
- __decorate([
388
- property({ attribute: `max-length`, type: Number })
389
- ], PlotElement.prototype, "maxLength", void 0);
390
- __decorate([
391
- property({ attribute: `data-width`, type: Number })
392
- ], PlotElement.prototype, "dataWidth", void 0);
393
- __decorate([
394
- property({ attribute: `fixed-max`, type: Number })
395
- ], PlotElement.prototype, "fixedMax", void 0);
396
- __decorate([
397
- property({ attribute: `fixed-min`, type: Number })
398
- ], PlotElement.prototype, "fixedMin", void 0);
399
- __decorate([
400
- property({ attribute: `line-width`, type: Number })
401
- ], PlotElement.prototype, "lineWidth", void 0);
402
776
  __decorate([
403
777
  property({ attribute: `render`, type: String })
404
778
  ], PlotElement.prototype, "renderStyle", void 0);
405
779
  __decorate([
406
- property({ attribute: `manual-draw`, type: Boolean })
407
- ], PlotElement.prototype, "manualDraw", void 0);
780
+ property({ attribute: `padding`, type: Number })
781
+ ], PlotElement.prototype, "padding", void 0);
408
782
  PlotElement = __decorate([
409
783
  customElement(`ixfx-plot-element`)
410
784
  ], PlotElement);
411
785
  export { PlotElement };
786
+ const resolveFillDrawStyle = (s) => {
787
+ if (s.type === `none`)
788
+ return s;
789
+ const c = Colour.resolveCss(s.colour);
790
+ return {
791
+ type: `solid`,
792
+ colour: c
793
+ };
794
+ };
795
+ const resolveOutlineDrawStyle = (s) => {
796
+ if (s.type === `none`)
797
+ return s;
798
+ const c = Colour.resolveCss(s.colour);
799
+ return {
800
+ ...s,
801
+ colour: c
802
+ };
803
+ };
804
+ //export type Range = { min: number, max: number }
805
+ // const initRange = (): Range => ({ min: Number.MAX_SAFE_INTEGER, max: Number.MIN_SAFE_INTEGER });
806
+ // function computeRangeWithNewValue(v: number | undefined, prev: Range): Range {
807
+ // if (typeof v === `number`) {
808
+ // // Skip creating an object if it's in range
809
+ // if (v >= prev.min && v <= prev.max) return prev;
810
+ // return {
811
+ // min: Math.min(v, prev.min),
812
+ // max: Math.max(v, prev.max),
813
+ // }
814
+ // }
815
+ // return prev;
816
+ // }
817
+ // function computeMergedRange(newRange: Range, existingRange: Range): Range {
818
+ // if (newRange.max <= existingRange.max && newRange.min >= existingRange.min) return existingRange;
819
+ // return {
820
+ // min: Math.min(newRange.min, existingRange.min),
821
+ // max: Math.max(newRange.max, existingRange.max)
822
+ // }
823
+ // }
412
824
  export class PlotSeries {
413
- constructor(name, colour, plot) {
825
+ #data = [];
826
+ #capacityLimit = Number.NaN;
827
+ #dirty = false;
828
+ #onPrimaryAxis = true;
829
+ constructor(name, plot) {
414
830
  this.name = name;
415
- this.colour = colour;
416
831
  this.plot = plot;
417
- this.data = [];
418
- this.minSeen = Number.MAX_SAFE_INTEGER;
419
- this.maxSeen = Number.MIN_SAFE_INTEGER;
420
832
  }
421
- clear() {
422
- this.data = [];
423
- this.resetScale();
833
+ get length() {
834
+ return this.#data.length;
424
835
  }
425
- /**
426
- * Returns a copy of the data scaled by the current
427
- * range of the data
428
- * @returns
429
- */
430
- getScaled() {
431
- //const r = this.maxSeen - this.minSeen;
432
- let min = this.minSeen;
433
- let max = this.maxSeen;
434
- if (Number.isNaN(min))
435
- min = 0;
436
- if (Number.isNaN(max))
437
- max = 1;
438
- const s = Numbers.scaler(min, max);
439
- return this.getScaledBy(s);
440
- }
441
- getScaledBy(scaler) {
442
- return this.data.map(v => {
443
- if (typeof v !== `number`)
444
- throw new Error(`Data should just be numbers. Got: ${typeof v}`);
445
- if (Number.isNaN(v))
446
- throw new Error(`data contains NaN`);
447
- const scaled = scaler(v);
448
- if (Number.isNaN(scaled))
449
- throw new Error(`NaN. v: ${v} scaled: ${scaled}`);
450
- return Numbers.clamp(scaled);
451
- });
836
+ *getValuesForColumn(column) {
837
+ let index = 0;
838
+ for (const dv of this.#data) {
839
+ if (dv && column in dv) {
840
+ yield [dv[column], index];
841
+ }
842
+ else {
843
+ yield [Number.NaN, index];
844
+ }
845
+ index++;
846
+ }
452
847
  }
453
- push(value) {
454
- if (typeof value !== 'number')
455
- throw new Error(`Can only add numbers. Got: ${typeof value}`);
456
- this.data.push(value);
457
- if (this.data.length > this.plot.maxLength && this.plot.streaming) {
458
- this.data = this.data.slice(1);
848
+ getValue(index) {
849
+ return this.#data[index];
850
+ }
851
+ pushValue(value, automaticallyDraw) {
852
+ if (!Array.isArray(value)) {
853
+ value = [value];
854
+ }
855
+ if (value.length === 0)
856
+ throw new Error(`Param 'value' is empty. Expected a value or array of value components`);
857
+ this.#data.push(value);
858
+ if (!Number.isNaN(this.#capacityLimit)) {
859
+ this.#data = Arrays.ensureLength(this.#data, this.#capacityLimit, `undefined`, `from-start`);
459
860
  }
460
- this.minSeen = Math.min(this.minSeen, value);
461
- this.maxSeen = Math.max(this.maxSeen, value);
861
+ this.#dirty = true;
862
+ if (automaticallyDraw)
863
+ this.plot.draw();
462
864
  }
463
- setValues(values) {
865
+ setValues(values, automaticallyDraw) {
464
866
  if (!Array.isArray(values))
465
867
  throw new TypeError(`Param 'values' is not an array`);
466
- this.data = values;
467
- this.minSeen = Math.min(...values);
468
- this.maxSeen = Math.max(...values);
868
+ this.#data = Arrays.ensureLength(values, this.#capacityLimit, `undefined`);
869
+ //this.#updateRanges();
870
+ this.#dirty = true;
871
+ if (automaticallyDraw)
872
+ this.plot.draw();
873
+ }
874
+ humanFormatValue(value) {
875
+ let v = `${this.name} `;
876
+ if (this.onPrimaryAxis) {
877
+ v += this.plot.primaryAxis.humanFormatValue(value);
878
+ }
879
+ else {
880
+ v += this.plot.secondaryAxis.humanFormatValue(value);
881
+ }
882
+ return v;
883
+ }
884
+ setRawValues(values, automaticallyDraw) {
885
+ this.setValues(values.map(v => [v]), automaticallyDraw);
886
+ }
887
+ setCapacityLimit(v) {
888
+ if (typeof v !== `number`)
889
+ throw new TypeError(`Expect type number. Got: ${typeof v}`);
890
+ if (!Number.isFinite(v)) {
891
+ throw new TypeError(`Param 'v' is not finite`);
892
+ }
893
+ if (Number.isNaN(v)) {
894
+ throw new TypeError(`Param 'v' is NaN`);
895
+ }
896
+ this.#capacityLimit = v;
897
+ this.#data = Arrays.ensureLength(this.#data, this.#capacityLimit, `undefined`);
898
+ }
899
+ unsetCapacityLimit() {
900
+ this.#capacityLimit = Number.NaN;
901
+ }
902
+ setFormatting(formatting) {
903
+ this.plot.setSeriesFormatting(this.name, formatting);
469
904
  }
470
- resetScale() {
471
- this.minSeen = Number.MAX_SAFE_INTEGER;
472
- this.maxSeen = Number.MIN_SAFE_INTEGER;
905
+ getFormatting() {
906
+ return this.plot.getSeriesFormatting(this.name, true);
907
+ }
908
+ get isDirty() {
909
+ return this.#dirty;
910
+ }
911
+ set isDirty(value) {
912
+ this.#dirty = value;
913
+ }
914
+ get onPrimaryAxis() {
915
+ return this.#onPrimaryAxis;
916
+ }
917
+ set onPrimaryAxis(value) {
918
+ if (value === this.#onPrimaryAxis)
919
+ return;
920
+ this.#onPrimaryAxis = value;
921
+ this.#dirty = true;
922
+ }
923
+ clear() {
924
+ this.#data = [];
925
+ this.#dirty = true;
473
926
  }
474
927
  }
475
928
  //# sourceMappingURL=plot.js.map