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