@refinitiv-ui/elements 5.2.1 → 5.3.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 (47) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/lib/heatmap/custom-elements.json +13 -0
  3. package/lib/heatmap/index.d.ts +17 -5
  4. package/lib/heatmap/index.js +40 -20
  5. package/lib/heatmap/index.js.map +1 -1
  6. package/lib/index.js +1 -1
  7. package/lib/overlay/elements/overlay.js +1 -0
  8. package/lib/overlay/elements/overlay.js.map +1 -1
  9. package/lib/overlay/managers/backdrop-manager.d.ts +1 -1
  10. package/lib/overlay/managers/close-manager.d.ts +1 -1
  11. package/lib/overlay/managers/focus-manager.d.ts +1 -1
  12. package/lib/overlay/managers/focus-manager.js +1 -2
  13. package/lib/overlay/managers/focus-manager.js.map +1 -1
  14. package/lib/overlay/managers/viewport-manager.d.ts +1 -1
  15. package/lib/overlay/managers/zindex-manager.d.ts +1 -1
  16. package/lib/overlay/managers/zindex-manager.js +0 -2
  17. package/lib/overlay/managers/zindex-manager.js.map +1 -1
  18. package/lib/swing-gauge/const.d.ts +23 -0
  19. package/lib/swing-gauge/const.js +27 -0
  20. package/lib/swing-gauge/const.js.map +1 -0
  21. package/lib/swing-gauge/custom-elements.json +47 -20
  22. package/lib/swing-gauge/helpers.d.ts +9 -0
  23. package/lib/swing-gauge/helpers.js +106 -0
  24. package/lib/swing-gauge/helpers.js.map +1 -0
  25. package/lib/swing-gauge/index.d.ts +206 -69
  26. package/lib/swing-gauge/index.js +651 -161
  27. package/lib/swing-gauge/index.js.map +1 -1
  28. package/lib/swing-gauge/themes/halo/dark/index.js +1 -1
  29. package/lib/swing-gauge/themes/halo/light/index.js +1 -1
  30. package/lib/swing-gauge/themes/solar/charcoal/index.js +1 -1
  31. package/lib/swing-gauge/themes/solar/pearl/index.js +1 -1
  32. package/lib/swing-gauge/types.d.ts +35 -0
  33. package/lib/swing-gauge/{helpers/types.js → types.js} +0 -0
  34. package/lib/swing-gauge/{helpers/types.js.map → types.js.map} +1 -1
  35. package/lib/tooltip/elements/tooltip-element.d.ts +1 -1
  36. package/lib/tooltip/managers/tooltip-manager.d.ts +1 -1
  37. package/lib/tree/themes/halo/dark/index.js +1 -1
  38. package/lib/tree/themes/halo/light/index.js +1 -1
  39. package/lib/tree-select/themes/halo/dark/index.js +1 -0
  40. package/lib/tree-select/themes/halo/light/index.js +1 -0
  41. package/lib/tree-select/themes/solar/charcoal/index.js +1 -0
  42. package/lib/tree-select/themes/solar/pearl/index.js +1 -0
  43. package/package.json +8 -8
  44. package/lib/swing-gauge/helpers/canvas.d.ts +0 -9
  45. package/lib/swing-gauge/helpers/canvas.js +0 -115
  46. package/lib/swing-gauge/helpers/canvas.js.map +0 -1
  47. package/lib/swing-gauge/helpers/types.d.ts +0 -34
@@ -4,55 +4,116 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { css, customElement, property } from '@refinitiv-ui/core';
8
- import { helpers as canvasHelper } from './helpers/canvas';
9
- import { Canvas } from '../canvas';
7
+ import { ResponsiveElement, css, customElement, property, html, styleMap, query, state, WarningNotice } from '@refinitiv-ui/core';
10
8
  import { VERSION } from '../';
9
+ import '../canvas';
10
+ import '../label';
11
+ import { helpers as canvasHelper } from './helpers';
12
+ import { DefaultStyle, Segment, TextType } from './const.js';
13
+ /**
14
+ * Constants from swing-gauge default specs
15
+ */
16
+ const GAUGE_WIDTH_SCALE = 0.4;
17
+ const GAUGE_HEIGHT_SCALE = 0.8;
18
+ const GAUGE_UPPER_BOUND = GAUGE_HEIGHT_SCALE + GAUGE_WIDTH_SCALE / 2;
19
+ const GAUGE_LOWER_BOUND = GAUGE_HEIGHT_SCALE - GAUGE_WIDTH_SCALE / 2;
20
+ const PRIMARY_RADIAN = 1.25;
21
+ const SECONDARY_RADIAN = 1.75;
22
+ const LINE_POINTER_OFFSET = 0.3;
23
+ const POINT_POINTER_OFFSET = 0.1;
24
+ // When either value is below this threshold, the label position changes
25
+ const GAUGE_PERCENTAGE_THRESHOLD = 30;
26
+ const GAUGE_LABEL_FONT_SCALE = 0.14;
27
+ const GAUGE_VALUE_FONT_SCALE = 0.18;
28
+ const MIN_LABEL_FONT_SIZE = 12;
29
+ const MIN_VALUE_FONT_SIZE = 15;
30
+ const MAX_LABEL_LINE = 3;
31
+ const MAX_VALUE_LINE = 1;
32
+ const MAX_LEGEND_LINE = 2;
11
33
  /**
12
34
  * Data visualisation showing the percentage between two values
13
35
  */
14
- let SwingGauge = class SwingGauge extends Canvas {
36
+ let SwingGauge = class SwingGauge extends ResponsiveElement {
15
37
  constructor() {
16
- super();
38
+ super(...arguments);
39
+ this._primaryValue = 0;
17
40
  /**
18
- * Set the primary value
19
- * @default 50
41
+ * Primary label
20
42
  */
21
- this.primaryValue = 50;
43
+ this.primaryLabel = '';
44
+ this._secondaryValue = 0;
22
45
  /**
23
- * Set the primary label
46
+ * Secondary label
24
47
  */
25
- this.primaryLabel = '';
48
+ this.secondaryLabel = '';
26
49
  /**
27
- * Set the secondary value
28
- * @default 50
50
+ * Animation duration in milliseconds
29
51
  */
30
- this.secondaryValue = 50;
52
+ this.duration = 1000;
31
53
  /**
32
- * Set the secondary label
54
+ * Primary value legend
55
+ */
56
+ this.primaryLegend = '';
57
+ /**
58
+ * Secondary value legend
59
+ */
60
+ this.secondaryLegend = '';
61
+ /**
62
+ * Custome value formatter
33
63
  */
34
- this.secondaryLabel = '';
64
+ this.valueFormatter = this.defaultValueFormatter;
35
65
  /**
36
- * Sets the animation duration in milliseconds
37
- * @default 1000
66
+ * Controls swing gauge animations
38
67
  */
39
- this.duration = 1000;
40
- this.w = null;
41
- this.h = null;
42
- this.min = null;
43
- this.max = null;
44
- this.size = null;
45
- this.maxFontSize = null;
46
- this.centerlineOptions = ['solid', 'dotted', 'dashed'];
47
68
  this.onFrame = requestAnimationFrame.bind(window);
48
69
  this.cancelFrame = cancelAnimationFrame.bind(window);
49
- this.previousFillPercentage = null;
50
- this.fillPercentage = null;
51
- this.frameHandler = null;
70
+ this.requestedAnimationID = 0;
71
+ this.gaugeWidthScale = GAUGE_WIDTH_SCALE;
72
+ this.gaugeHeightScale = GAUGE_HEIGHT_SCALE;
73
+ this.gaugeUpperBound = GAUGE_UPPER_BOUND;
74
+ this.gaugeLowerBound = GAUGE_LOWER_BOUND;
75
+ this.linePointerOffset = LINE_POINTER_OFFSET;
76
+ this.pointPointerOffset = POINT_POINTER_OFFSET;
77
+ this.primaryLineRadian = PRIMARY_RADIAN;
78
+ this.secondaryLineRadian = SECONDARY_RADIAN;
79
+ this.linePointerOffsetOverflow = LINE_POINTER_OFFSET;
80
+ /**
81
+ * Data requires to draw swing gauge
82
+ */
83
+ this.data = null;
52
84
  /**
53
- * @ignore
85
+ * Internal sizes and scales
86
+ */
87
+ this.width = 0;
88
+ this.height = 0;
89
+ this.size = 0;
90
+ this.lineLength = 50;
91
+ this.scale = 1;
92
+ this.overflowScale = 0.1;
93
+ /**
94
+ * Current fill percentage
95
+ */
96
+ this.fillPercentage = 0;
97
+ /**
98
+ * Keeps previous percentage calculation to avoid re-rendering the same value
99
+ */
100
+ this.previousFillPercentage = 0;
101
+ /**
102
+ * Style for primary container
54
103
  */
55
- this.autoloop = true;
104
+ this.primaryContainerStyle = {};
105
+ /**
106
+ * Style for secondary container
107
+ */
108
+ this.secondaryContainerStyle = {};
109
+ /**
110
+ * Style for label
111
+ */
112
+ this.labelStyle = {};
113
+ /**
114
+ * Style for both values
115
+ */
116
+ this.valueStyle = {};
56
117
  }
57
118
  /**
58
119
  * Element version number
@@ -71,220 +132,649 @@ let SwingGauge = class SwingGauge extends Canvas {
71
132
  return css `
72
133
  :host {
73
134
  display: block;
74
- overflow: hidden;
135
+ height: 200px;
136
+ }
137
+ :host [part=container] {
138
+ display: flex;
139
+ flex-direction: column;
140
+ height: 100%;
141
+ }
142
+ :host [part=canvas-container] {
75
143
  position: relative;
144
+ flex: 1;
76
145
  }
77
- :host::before {
78
- content: '';
79
- display: block;
80
- min-height: 200px;
81
- box-sizing: border-box;
146
+ :host [part=canvas] {
147
+ height: 100%;
148
+ }
149
+ :host [part=primary-legend], [part=secondary-legend] {
150
+ text-align: left;
151
+ display: flex;
152
+ }
153
+ :host [part=primary-label], [part=primary-value] {
154
+ text-align: left;
155
+ }
156
+ :host [part=secondary-label], [part=secondary-value] {
157
+ text-align: right;
158
+ }
159
+ :host [part=legend-container-outer] {
160
+ width: 60%;
161
+ margin: 0 auto;
162
+ text-align: center;
163
+ }
164
+ :host [part=legend-container-inner] {
165
+ max-width: 100%;
166
+ display: inline-block;
82
167
  }
83
- canvas {
84
- top: 0;
85
- left: 0;
86
- right: 0;
87
- bottom: 0;
168
+ :host [part=primary-legend-symbol], [part=secondary-legend-symbol] {
169
+ display: inline-block;
170
+ flex-shrink: 0;
171
+ }
172
+ :host [part=primary-container] {
173
+ position: absolute;
174
+ text-align: left;
175
+ }
176
+ :host [part=secondary-container] {
88
177
  position: absolute;
178
+ text-align: right;
89
179
  }
90
180
  `;
91
181
  }
182
+ /**
183
+ * Primary value
184
+ * @param value primary value
185
+ */
186
+ set primaryValue(value) {
187
+ value = this.validateNumber(value, 'primary-value');
188
+ const oldValue = this._primaryValue;
189
+ if (oldValue !== value) {
190
+ this._primaryValue = value;
191
+ void this.requestUpdate('primaryValue', oldValue);
192
+ }
193
+ }
194
+ get primaryValue() {
195
+ return this._primaryValue;
196
+ }
197
+ /**
198
+ * Secondary value
199
+ * @param value secondary value
200
+ */
201
+ set secondaryValue(value) {
202
+ value = this.validateNumber(value, 'secondary-value');
203
+ const oldValue = this._secondaryValue;
204
+ if (oldValue !== value) {
205
+ this._secondaryValue = value;
206
+ void this.requestUpdate('secondaryValue', oldValue);
207
+ }
208
+ }
209
+ get secondaryValue() {
210
+ return this._secondaryValue;
211
+ }
212
+ /**
213
+ * Get primary percentage
214
+ */
215
+ get primaryPercentage() {
216
+ return this.getPercentage(this.primaryValue);
217
+ }
218
+ /**
219
+ * Get secondary percentage
220
+ */
221
+ get secondaryPercentage() {
222
+ return this.getPercentage(this.secondaryValue);
223
+ }
224
+ /**
225
+ * Check width and height are valid
226
+ * @returns if size is valid
227
+ */
228
+ get hasValidSize() {
229
+ return this.offsetWidth > 0 && this.offsetHeight > 0 && this.canvas.width > 0 && this.canvas.height > 0;
230
+ }
231
+ /**
232
+ * Get default value format
233
+ * @param value A value on each side of swing gauge
234
+ * @returns default value format
235
+ */
236
+ defaultValueFormatter(value) {
237
+ return `${value.toFixed(2)}%`;
238
+ }
92
239
  /**
93
240
  * Getter size of component
94
- * @returns {ElementSize} return size of component
241
+ * @returns return size of canvas
95
242
  */
96
243
  get canvasSize() {
97
244
  return {
98
- width: this.width,
99
- height: this.height
245
+ width: this.offsetWidth,
246
+ height: this.offsetHeight
100
247
  };
101
248
  }
102
249
  /**
103
- * Re-draw canvas when the size of component changed
104
- * @ignore
105
- * @param size element dimensions
250
+ * Validate number
251
+ * @protected
252
+ * @param value number that want to validate
253
+ * @param propName name of property
254
+ * @returns {number} valid number
255
+ */
256
+ validateNumber(value, propName) {
257
+ if (typeof value === 'number' && !isNaN(value) && isFinite(value) && value >= 0) {
258
+ return value;
259
+ }
260
+ new WarningNotice(`${this.localName} : The specified value "${value}" of ${propName} property is not valid. Default value will be used instead.`).show();
261
+ return 0;
262
+ }
263
+ /**
264
+ * On update lifecycle
265
+ * @param changedProperties changed properties
106
266
  * @returns {void}
107
267
  */
108
- resizedCallback(size) {
109
- super.resizedCallback(size);
110
- this.renderCanvas();
268
+ update(changedProperties) {
269
+ super.update(changedProperties);
270
+ if (changedProperties.has('primaryValue') || changedProperties.has('secondaryValue')
271
+ || (this.primaryValue === 0 && this.secondaryValue === 0)) {
272
+ this.canvas.autoloop = true;
273
+ this.renderCanvas('frame');
274
+ this.animateCanvas();
275
+ }
276
+ if (changedProperties.has('primaryLabel') || changedProperties.has('secondaryLabel')) {
277
+ this.calculateLabelFontSize();
278
+ }
279
+ if (changedProperties.has('valueFormatter')) {
280
+ this.calculateValueFontSize();
281
+ }
282
+ if (changedProperties.has('primaryValue') || changedProperties.has('secondaryValue')
283
+ || changedProperties.has('primaryLabel') || changedProperties.has('secondaryLabel')
284
+ || changedProperties.has('valueFormatter')) {
285
+ this.updateGaugePositions();
286
+ }
111
287
  }
112
288
  /**
113
- * Handles when event frame fired to re-draw canvas
114
- * @protected
115
- * @param time timestamp
289
+ * Handles when component disconnected
116
290
  * @returns {void}
117
291
  */
118
- fireFrame(time) {
119
- super.fireFrame(time);
120
- this.renderCanvas(true);
292
+ disconnectedCallback() {
293
+ super.disconnectedCallback();
294
+ if (this.requestedAnimationID) {
295
+ this.cancelFrame(this.requestedAnimationID);
296
+ }
121
297
  }
122
298
  /**
123
- * On First Updated Lifecycle
124
- * @ignore
125
- * @param changedProperties changed properties
299
+ * Calls easing based on both left and right values
300
+ * @param primaryValue primary value
301
+ * @param secondaryValue secondary value
126
302
  * @returns {void}
127
303
  */
128
- firstUpdated(changedProperties) {
129
- super.firstUpdated(changedProperties);
130
- this.reDrawCanvas();
304
+ ease(primaryValue, secondaryValue) {
305
+ let to = 0.5;
306
+ let from = this.fillPercentage;
307
+ if (primaryValue > 0 || secondaryValue > 0) {
308
+ to = primaryValue / (primaryValue + secondaryValue);
309
+ }
310
+ // this for prevent gauge not render when 'to' and 'from' are 0
311
+ if (primaryValue === 0 && from === 0) {
312
+ from = 0.1;
313
+ }
314
+ this.easeTo(to, from, performance.now() + this.duration);
131
315
  }
132
316
  /**
133
- * On Update Lifecycle
134
- * @ignore
135
- * @param changedProperties changed properties
317
+ * Eases the fill percentage
318
+ * @param to ease to value
319
+ * @param from ease from value
320
+ * @param time ease time
136
321
  * @returns {void}
137
322
  */
138
- update(changedProperties) {
139
- super.update(changedProperties);
323
+ easeTo(to, from, time) {
324
+ const diff = (this.duration - (time - performance.now())) / this.duration;
325
+ this.fillPercentage = from + (to - from) * canvasHelper.elasticOut(diff > 1 ? 1 : diff < 0 ? 0 : diff) || 0;
326
+ if (this.fillPercentage !== to) {
327
+ this.cancelFrame(this.requestedAnimationID);
328
+ this.requestedAnimationID = this.onFrame(() => this.easeTo(to, from, time));
329
+ }
330
+ else {
331
+ this.canvas.autoloop = false;
332
+ }
333
+ this.renderCanvas('frame', true);
140
334
  }
141
335
  /**
142
- * On Updated Lifecycle
143
- * @ignore
144
- * @param changedProperties changed properties
336
+ * Restart the animation, re-render the canvas
145
337
  * @returns {void}
146
338
  */
147
- updated(changedProperties) {
148
- super.updated(changedProperties);
149
- this.reDrawCanvas();
339
+ animateCanvas() {
340
+ this.ease(this.primaryValue, this.secondaryValue);
150
341
  }
151
342
  /**
152
- * Handles when component disconnected
153
- * @private
343
+ * Render chart
344
+ * @param onDraw drawing type
345
+ * @param isFrameUpdated Optional called by on frame event
154
346
  * @returns {void}
155
347
  */
156
- disconnectedCallback() {
157
- super.disconnectedCallback();
158
- if (this.frameHandler) {
159
- this.cancelFrame(this.frameHandler);
160
- this.frameHandler = null;
348
+ renderCanvas(onDraw, isFrameUpdated) {
349
+ const percentageChanged = this.previousFillPercentage !== this.fillPercentage;
350
+ const canRender = this.hasValidSize && percentageChanged;
351
+ if ((isFrameUpdated && !canRender) || !this.hasValidSize) {
352
+ return;
353
+ }
354
+ this.width = this.canvas.width;
355
+ this.height = this.canvas.height;
356
+ const min = this.width > this.height ? this.height : this.width;
357
+ const max = this.width > this.height ? this.width : this.height;
358
+ this.size = Math.floor(max / this.scale < min ? max / this.scale : min);
359
+ const canvasSize = { width: this.width, height: this.height };
360
+ // Prevent draw frame unnecessary recalculate position and draw data
361
+ if (!isFrameUpdated) {
362
+ this.data = this.getData();
363
+ }
364
+ else if (this.data !== null) {
365
+ this.data.fillPercentage = this.fillPercentage;
161
366
  }
367
+ const clear = () => {
368
+ if (!this.canvas.ctx) {
369
+ return;
370
+ }
371
+ canvasHelper.clear(canvasSize, this.canvas.ctx);
372
+ };
373
+ this.canvas.addEventListener(onDraw, clear, { once: true });
374
+ const draw = () => {
375
+ if (!this.canvas.ctx) {
376
+ return;
377
+ }
378
+ canvasHelper.draw(this.data === null ? this.getData() : this.data, this.canvas.ctx, {
379
+ strokeWidth: Math.ceil(this.scale * this.size * 0.005),
380
+ primaryColor: this.getComputedVariable('--primary-color', DefaultStyle.PRIMARY_GAUGE_COLOR),
381
+ secondaryColor: this.getComputedVariable('--secondary-color', DefaultStyle.SECONDARY_GAUGE_COLOR),
382
+ borderColor: this.getComputedVariable('--border-color', DefaultStyle.BOREDER_COLOR),
383
+ centerline: `${this.getComputedVariable('--center-line', DefaultStyle.CENTER_LINE_STYLE)}`.trim(),
384
+ centerlineOpacity: this.getComputedVariable('--center-line-opacity', DefaultStyle.CENTER_LINE_OPACITY),
385
+ centerlineColor: this.getComputedVariable('--center-line-color', DefaultStyle.CENTER_LINE_COLOR)
386
+ });
387
+ };
388
+ this.canvas.addEventListener(onDraw, draw, { once: true });
389
+ // Set this for comparison when deciding if we should paint
390
+ this.previousFillPercentage = this.fillPercentage;
162
391
  }
163
392
  /**
164
- * Calls easing based on both left and right values
165
- * @private
166
- * @param {number} v1 left value
167
- * @param {number} v2 right value
393
+ * Get computed swing-gauge data for drawing
394
+ * @returns swing-gauge data
395
+ */
396
+ getData() {
397
+ let primaryPosLine = this.getPositionStyle(Segment.PRIMARY, this.primaryLineRadian, this.linePointerOffset, 0);
398
+ const primaryPosPoint = this.getPositionStyle(Segment.PRIMARY, PRIMARY_RADIAN, this.pointPointerOffset, 0);
399
+ let secondaryPosLine = this.getPositionStyle(Segment.SECONDARY, this.secondaryLineRadian, this.linePointerOffset, 0);
400
+ const secondaryPosPoint = this.getPositionStyle(Segment.SECONDARY, SECONDARY_RADIAN, this.pointPointerOffset, 0);
401
+ // Recalculate radian to prevent container overflow
402
+ let primaryHeight = 0;
403
+ if (this.primaryContainer.children[0]) {
404
+ primaryHeight += this.primaryContainer.children[0].offsetHeight;
405
+ }
406
+ if (this.primaryContainer.children[1]) {
407
+ primaryHeight += this.primaryContainer.children[1].offsetHeight;
408
+ }
409
+ const primarySpace = this.height - this.getValueFromStyle(primaryPosLine.top);
410
+ const primaryOverflow = primarySpace < primaryHeight && this.primaryPercentage >= GAUGE_PERCENTAGE_THRESHOLD && primarySpace > 0 && primaryHeight > 0;
411
+ let secondaryHeight = 0;
412
+ if (this.secondaryContainer.children[0]) {
413
+ secondaryHeight += this.secondaryContainer.children[0].offsetHeight;
414
+ }
415
+ if (this.secondaryContainer.children[1]) {
416
+ secondaryHeight += this.secondaryContainer.children[1].offsetHeight;
417
+ }
418
+ const secondarySpace = this.height - this.getValueFromStyle(secondaryPosLine.top);
419
+ const secondaryOverflow = secondarySpace < secondaryHeight && this.secondaryPercentage >= GAUGE_PERCENTAGE_THRESHOLD && secondarySpace > 0 && secondaryHeight > 0;
420
+ if (primaryOverflow || secondaryOverflow) {
421
+ let containerHeight = primaryHeight;
422
+ let containerHeightOverflow = primarySpace;
423
+ let containerWidth = (this.width / 2) - this.getValueFromStyle(primaryPosLine.left);
424
+ if ((!primaryOverflow && secondaryOverflow)
425
+ || (primaryOverflow && secondaryOverflow && secondaryHeight > primaryHeight)) {
426
+ containerHeight = secondaryHeight;
427
+ containerHeightOverflow = secondarySpace;
428
+ containerWidth = this.getValueFromStyle(secondaryPosLine.left) - (this.width / 2);
429
+ }
430
+ // Prevents container overflow
431
+ const oldRadius = Math.sqrt(Math.pow(containerHeightOverflow, 2) + Math.pow(containerWidth, 2));
432
+ const newRadius = Math.sqrt(Math.pow(containerHeight, 2) + Math.pow(containerWidth, 2));
433
+ this.linePointerOffset *= Math.pow(2, newRadius / oldRadius);
434
+ this.linePointerOffset = this.linePointerOffset > 4 ? 4 : this.linePointerOffset;
435
+ this.linePointerOffsetOverflow = this.linePointerOffset;
436
+ this.overflowScale = this.scale;
437
+ primaryPosLine = this.getPositionStyle(Segment.PRIMARY, this.primaryLineRadian, this.linePointerOffset, 0);
438
+ secondaryPosLine = this.getPositionStyle(Segment.SECONDARY, this.secondaryLineRadian, this.linePointerOffset, 0);
439
+ }
440
+ return {
441
+ width: this.width,
442
+ height: this.height,
443
+ size: this.size,
444
+ fillPercentage: this.fillPercentage,
445
+ gaugeWidthScale: this.gaugeWidthScale,
446
+ gaugeHeightScale: this.gaugeHeightScale,
447
+ gaugeUpperBound: this.gaugeUpperBound,
448
+ gaugeLowerBound: this.gaugeLowerBound,
449
+ lineLength: this.lineLength,
450
+ offsetLeftPrimaryLine: this.getValueFromStyle(primaryPosLine.left),
451
+ offsetTopPrimaryLine: this.getValueFromStyle(primaryPosLine.top),
452
+ offsetLeftPrimaryPoint: this.getValueFromStyle(primaryPosPoint.left),
453
+ offsetTopPrimaryPoint: this.getValueFromStyle(primaryPosPoint.top),
454
+ offsetLeftSecondaryLine: this.getValueFromStyle(secondaryPosLine.left),
455
+ offsetTopSecondaryLine: this.getValueFromStyle(secondaryPosLine.top),
456
+ offsetLeftSecondaryPoint: this.getValueFromStyle(secondaryPosPoint.left),
457
+ offsetTopSecondaryPoint: this.getValueFromStyle(secondaryPosPoint.top)
458
+ };
459
+ }
460
+ /**
461
+ * Get number from CSS declaration value
462
+ * @param styleValue value of CSS declaration
463
+ * @returns number without CSS unit
464
+ */
465
+ getValueFromStyle(styleValue) {
466
+ return Number(styleValue === null || styleValue === void 0 ? void 0 : styleValue.replace('px', ''));
467
+ }
468
+ /**
469
+ * Compute and update style of containers and labels
168
470
  * @returns {void}
169
471
  */
170
- ease(v1, v2) {
171
- if (v1 || v2) {
172
- this.easeTo(v1 / (v1 + v2), this.fillPercentage, performance.now() + this.duration);
472
+ updateGaugePositions() {
473
+ if (!this.hasValidSize) {
474
+ return;
475
+ }
476
+ const primaryPosition = this.getPositionStyle(Segment.PRIMARY, this.primaryLineRadian, this.linePointerOffset, -this.lineLength);
477
+ const secondaryPosition = this.getPositionStyle(Segment.SECONDARY, this.secondaryLineRadian, this.linePointerOffset, 0);
478
+ this.primaryContainerStyle = Object.assign({ width: `${this.lineLength}px` }, primaryPosition);
479
+ this.secondaryContainerStyle = Object.assign({ width: `${this.lineLength}px` }, secondaryPosition);
480
+ // position container over line pointer
481
+ if (this.primaryPercentage < GAUGE_PERCENTAGE_THRESHOLD) {
482
+ delete this.primaryContainerStyle.top;
483
+ this.primaryContainerStyle.bottom = '5px';
173
484
  }
174
- else if (typeof this.fillPercentage === 'number') {
175
- this.easeTo(0.5, this.fillPercentage, performance.now() + this.duration);
485
+ if (this.secondaryPercentage < GAUGE_PERCENTAGE_THRESHOLD || this.secondaryValue === 0) {
486
+ delete this.secondaryContainerStyle.top;
487
+ this.secondaryContainerStyle.bottom = '5px';
488
+ }
489
+ }
490
+ /**
491
+ * Compute position style
492
+ * @param segment primary or secondary
493
+ * @param maxRadian max radian
494
+ * @param radiusOffset radius offset from gauge upper bound
495
+ * @param offset line length offset
496
+ * @returns position style
497
+ */
498
+ getPositionStyle(segment, maxRadian, radiusOffset, offset) {
499
+ let radius = this.size * (1 + radiusOffset) * this.scale;
500
+ let radianValue;
501
+ if (segment === Segment.PRIMARY) {
502
+ radianValue = this.primaryPercentage >= GAUGE_PERCENTAGE_THRESHOLD ? maxRadian : 1;
176
503
  }
177
504
  else {
178
- this.fillPercentage = 0.5;
505
+ radianValue = this.secondaryPercentage >= GAUGE_PERCENTAGE_THRESHOLD ? maxRadian : 0;
506
+ }
507
+ // reduce line length when line is on the bottom of canvas
508
+ if ((radianValue === 1 || radianValue === 0) && radiusOffset > 0.1) {
509
+ radius = this.size * (1 + this.pointPointerOffset) * this.scale;
179
510
  }
511
+ const radian = radianValue * Math.PI;
512
+ const left = `${Math.round(this.width / 2 + Math.cos(radian) * radius) + offset}px`;
513
+ const top = `${Math.round(this.height + Math.sin(radian) * radius)}px`;
514
+ return { left: left, top: top };
180
515
  }
181
516
  /**
182
- * Eases the fill percentage
183
- * @private
184
- * @param {number} to ease to value
185
- * @param {number} from ease from value
186
- * @param {number} time ease time
517
+ * Scales canvas variables making it responsive before painting
187
518
  * @returns {void}
188
519
  */
189
- easeTo(to, from, time) {
190
- let diff = this.duration - (time - performance.now());
191
- diff /= this.duration;
192
- this.fillPercentage = (from + (to - from) * canvasHelper.elasticOut(diff > 1 ? 1 : diff < 0 ? 0 : diff)) || 0;
193
- if (this.fillPercentage !== to) {
194
- if (this.frameHandler) {
195
- this.cancelFrame(this.frameHandler);
520
+ calculateCanvasSize() {
521
+ const lineLength = this.canvas.height * 0.75;
522
+ const bestWidth = 2 * (GAUGE_UPPER_BOUND + LINE_POINTER_OFFSET) * this.canvas.height + 2 * lineLength;
523
+ const ratio = bestWidth / this.canvas.height;
524
+ this.scale = 1;
525
+ if (this.canvas.width < bestWidth) {
526
+ const width = this.canvas.width;
527
+ const bestHeight = width / ratio;
528
+ this.scale = bestHeight / this.canvas.height;
529
+ }
530
+ this.scale = this.scale < 0.1 ? 0.1 : this.scale;
531
+ this.lineLength = this.scale * lineLength < 50 ? 50 : this.scale * lineLength;
532
+ this.gaugeWidthScale = this.scale * GAUGE_WIDTH_SCALE;
533
+ this.gaugeHeightScale = this.scale * GAUGE_HEIGHT_SCALE;
534
+ this.gaugeUpperBound = this.scale * GAUGE_UPPER_BOUND;
535
+ this.gaugeLowerBound = this.scale * GAUGE_LOWER_BOUND;
536
+ this.primaryLineRadian = PRIMARY_RADIAN;
537
+ this.secondaryLineRadian = SECONDARY_RADIAN;
538
+ this.linePointerOffset = LINE_POINTER_OFFSET;
539
+ this.pointPointerOffset = POINT_POINTER_OFFSET;
540
+ if (this.scale < 1) {
541
+ const offset = 0.05 * (1 - this.scale);
542
+ this.primaryLineRadian = PRIMARY_RADIAN + offset;
543
+ this.secondaryLineRadian = SECONDARY_RADIAN - offset;
544
+ this.linePointerOffset = LINE_POINTER_OFFSET + (0.4 * (1 - this.scale));
545
+ if (this.scale <= this.overflowScale) {
546
+ this.linePointerOffset = this.linePointerOffsetOverflow;
196
547
  }
197
- this.frameHandler = this.onFrame(() => this.easeTo(to, from, time));
198
548
  }
549
+ this.primaryLineRadian = this.primaryLineRadian > 1.3 ? 1.3 : this.primaryLineRadian;
550
+ this.secondaryLineRadian = this.primaryLineRadian > 1.7 ? 1.7 : this.secondaryLineRadian;
199
551
  }
200
552
  /**
201
- * Does the control has valid data?
202
- * @returns {boolean} will return true if valid data
553
+ * Calculate and update font sizes on canvas
554
+ * @returns {void}
203
555
  */
204
- dataValid() {
205
- return this.primaryValue >= 0 && this.secondaryValue >= 0;
556
+ updateFontSize() {
557
+ this.calculateLabelFontSize();
558
+ this.calculateValueFontSize();
206
559
  }
207
560
  /**
208
- * Are we able to render?
209
- * Used to prevent frame painting if data hasn't changed
210
- * @returns {boolean} will return true if canvas can render
561
+ * Calculate label or value font size
562
+ * @param ctx canvas context
563
+ * @param textType text type
564
+ * @returns {number} font size
211
565
  */
212
- canRender() {
213
- return (this.canvas.width + this.canvas.height !== 0 && this.previousFillPercentage !== this.fillPercentage);
566
+ calculateFontSize(ctx, textType) {
567
+ let maxLine;
568
+ let minFontSize;
569
+ let widthScale;
570
+ let fontSize;
571
+ let longerLabel;
572
+ if (textType === TextType.LABEL) {
573
+ maxLine = MAX_LABEL_LINE;
574
+ minFontSize = MIN_LABEL_FONT_SIZE;
575
+ // buffer for word wrap
576
+ widthScale = 1.1;
577
+ longerLabel = this.primaryLabel.length > this.secondaryLabel.length ? this.primaryLabel : this.secondaryLabel;
578
+ fontSize = Math.ceil(this.scale * this.canvas.height * GAUGE_LABEL_FONT_SCALE);
579
+ }
580
+ else {
581
+ maxLine = MAX_VALUE_LINE;
582
+ minFontSize = MIN_VALUE_FONT_SIZE;
583
+ widthScale = 1;
584
+ const primaryValue = String(this.valueFormatter(this.primaryPercentage, this.primaryValue));
585
+ const secondaryValue = String(this.valueFormatter(this.secondaryPercentage, this.secondaryValue));
586
+ longerLabel = primaryValue.length > secondaryValue.length ? primaryValue : secondaryValue;
587
+ fontSize = Math.ceil(this.scale * this.canvas.height * GAUGE_VALUE_FONT_SCALE);
588
+ }
589
+ let textWidth = canvasHelper.textWidth(ctx, longerLabel, fontSize, getComputedStyle(this).fontFamily);
590
+ let numberOfLines = Math.ceil(textWidth / this.lineLength);
591
+ if (numberOfLines > maxLine) {
592
+ numberOfLines = maxLine;
593
+ }
594
+ do {
595
+ fontSize -= 1;
596
+ if (fontSize < minFontSize) {
597
+ fontSize = minFontSize;
598
+ break;
599
+ }
600
+ textWidth = canvasHelper.textWidth(ctx, longerLabel, fontSize, getComputedStyle(this).fontFamily);
601
+ } while (textWidth * widthScale > this.lineLength * numberOfLines);
602
+ return fontSize;
214
603
  }
215
604
  /**
216
- * Calculate fill percentage and re-render chart
605
+ * Update label font size
217
606
  * @returns {void}
218
607
  */
219
- reDrawCanvas() {
220
- this.ease(this.primaryValue, this.secondaryValue);
221
- this.renderCanvas();
608
+ calculateLabelFontSize() {
609
+ if (!this.canvas.ctx) {
610
+ return;
611
+ }
612
+ this.labelStyle = { maxWidth: `${this.lineLength}px`, fontSize: `${this.calculateFontSize(this.canvas.ctx, TextType.LABEL)}px` };
222
613
  }
223
614
  /**
224
- * Render chart
225
- * @param isFrameUpdated Optional called by on frame event
615
+ * Update value font size
226
616
  * @returns {void}
227
617
  */
228
- renderCanvas(isFrameUpdated) {
229
- // Can and should we paint?
230
- if ((isFrameUpdated && !this.canRender()) || !this.dataValid()) {
618
+ calculateValueFontSize() {
619
+ if (!this.canvas.ctx) {
231
620
  return;
232
621
  }
233
- // Update the variables
234
- this.w = this.canvasSize.width;
235
- this.h = this.canvasSize.height;
236
- this.min = this.w > this.h ? this.h : this.w;
237
- this.max = this.w > this.h ? this.w : this.h;
238
- this.size = Math.floor(this.max / 2 < this.min ? this.max / 2 : this.min);
239
- this.maxFontSize = Math.round(this.size * 0.18);
240
- // Clear
241
- canvasHelper.clear(this.canvasSize, this.ctx);
242
- // Draw
243
- canvasHelper.draw({
244
- w: this.w,
245
- h: this.h,
246
- size: this.size,
247
- duration: this.duration,
248
- primaryValue: this.primaryValue,
249
- primaryLabel: this.primaryLabel,
250
- secondaryValue: this.secondaryValue,
251
- secondaryLabel: this.secondaryLabel,
252
- fillPercentage: this.fillPercentage
253
- }, this.ctx, this.canvasSize, {
254
- ctxOptions: {
255
- strokeWidth: Math.ceil(this.size * 0.005),
256
- fontSize: this.maxFontSize,
257
- fontFamily: getComputedStyle(this).fontFamily,
258
- fillStyle: this.getComputedVariable('--text-color', '#fff'),
259
- maxFontSize: Math.round(this.size * 0.18),
260
- primaryColor: this.getComputedVariable('--primary-color', '#2EB4C9'),
261
- secondaryColor: this.getComputedVariable('--secondary-color', '#C93C4B'),
262
- borderColor: this.getComputedVariable('--border-color', '#000'),
263
- centerline: `${this.getComputedVariable('--center-line', 'solid')}`.trim(),
264
- centerlineOpacity: this.getComputedVariable('--center-line-opacity', '0.6'),
265
- centerlineColor: this.getComputedVariable('--center-line-color', '#000'),
266
- centerlineOptions: this.centerlineOptions
267
- }
268
- });
269
- // Set this for comparison when deciding if we should paint
270
- this.previousFillPercentage = this.fillPercentage;
622
+ this.valueStyle = { maxWidth: `${this.lineLength}px`, fontSize: `${this.calculateFontSize(this.canvas.ctx, TextType.VALUE)}px` };
623
+ }
624
+ /**
625
+ * Compute percentage of value
626
+ * @param value value of primary or secondary
627
+ * @returns percentage of value
628
+ */
629
+ getPercentage(value) {
630
+ if (value === 0) {
631
+ return 0;
632
+ }
633
+ return 100 * value / (this.primaryValue + this.secondaryValue);
634
+ }
635
+ /**
636
+ * Handles canvas resize
637
+ * @returns {void}
638
+ */
639
+ onCanvasResize() {
640
+ this.calculateCanvasSize();
641
+ this.updateFontSize();
642
+ this.renderCanvas('resize');
643
+ this.updateGaugePositions();
644
+ }
645
+ /**
646
+ * Renders legend container
647
+ * @returns {TemplateResult} Legend template or null
648
+ */
649
+ get legendTemplate() {
650
+ return this.primaryLegend.length > 0 || this.secondaryLegend.length > 0
651
+ ? html `<div part="legend-container-outer">
652
+ <div part="legend-container-inner">
653
+ ${this.primaryLegendTemplate}
654
+ ${this.secondaryLegendTemplate}
655
+ </div>
656
+ </div>`
657
+ : null;
658
+ }
659
+ /**
660
+ * Renders primary legend if property present
661
+ * @returns {TemplateResult} Primary legend template or null
662
+ */
663
+ get primaryLegendTemplate() {
664
+ return this.primaryLegend
665
+ ? html `<div part="primary-legend">
666
+ <span part="primary-legend-symbol"></span>
667
+ <ef-label max-line="${MAX_LEGEND_LINE}" line-clamp="${MAX_LEGEND_LINE}">${this.primaryLegend}</ef-label>
668
+ </div>`
669
+ : null;
670
+ }
671
+ /**
672
+ * Renders secondary legend if property present
673
+ * @returns {TemplateResult} Secondary legend template or null
674
+ */
675
+ get secondaryLegendTemplate() {
676
+ return this.secondaryLegend
677
+ ? html `<div part="secondary-legend">
678
+ <span part="secondary-legend-symbol"></span>
679
+ <ef-label max-line="${MAX_LEGEND_LINE}" line-clamp="${MAX_LEGEND_LINE}">${this.secondaryLegend}</ef-label>
680
+ </div>`
681
+ : null;
682
+ }
683
+ render() {
684
+ return html `
685
+ <div part="container">
686
+ ${this.legendTemplate}
687
+ <div part="canvas-container">
688
+ <ef-canvas part="canvas" @resize=${this.onCanvasResize}></ef-canvas>
689
+ <div part="primary-container" style=${styleMap(this.primaryContainerStyle)}>
690
+ ${this.primaryLabel ? html `
691
+ <ef-label
692
+ part="primary-label"
693
+ max-line="${MAX_LABEL_LINE}"
694
+ line-clamp="${MAX_LABEL_LINE}"
695
+ style=${styleMap(this.labelStyle)}
696
+ >${this.primaryLabel}
697
+ </ef-label><br>`
698
+ : null}
699
+ <ef-label
700
+ part="primary-value"
701
+ truncate=""
702
+ line-clamp="1"
703
+ style=${styleMap(this.valueStyle)}
704
+ >${this.valueFormatter(this.primaryPercentage, this.primaryValue)}</ef-label
705
+ >
706
+ </div>
707
+ <div part="secondary-container" style=${styleMap(this.secondaryContainerStyle)}>
708
+ ${this.secondaryLabel ? html `
709
+ <ef-label
710
+ part="secondary-label"
711
+ max-line="${MAX_LABEL_LINE}"
712
+ line-clamp="${MAX_LABEL_LINE}"
713
+ style=${styleMap(this.labelStyle)}
714
+ >${this.secondaryLabel}
715
+ </ef-label><br>`
716
+ : null}
717
+ <ef-label
718
+ part="secondary-value"
719
+ truncate=""
720
+ line-clamp="1"
721
+ style=${styleMap(this.valueStyle)}
722
+ >${this.valueFormatter(this.secondaryPercentage, this.secondaryValue)}</ef-label
723
+ >
724
+ </div>
725
+ </div>
726
+ </div>
727
+ `;
271
728
  }
272
729
  };
273
730
  __decorate([
274
731
  property({ attribute: 'primary-value', type: Number })
275
- ], SwingGauge.prototype, "primaryValue", void 0);
732
+ ], SwingGauge.prototype, "primaryValue", null);
276
733
  __decorate([
277
734
  property({ attribute: 'primary-label', type: String })
278
735
  ], SwingGauge.prototype, "primaryLabel", void 0);
279
736
  __decorate([
280
737
  property({ attribute: 'secondary-value', type: Number })
281
- ], SwingGauge.prototype, "secondaryValue", void 0);
738
+ ], SwingGauge.prototype, "secondaryValue", null);
282
739
  __decorate([
283
740
  property({ attribute: 'secondary-label', type: String })
284
741
  ], SwingGauge.prototype, "secondaryLabel", void 0);
285
742
  __decorate([
286
743
  property({ type: Number })
287
744
  ], SwingGauge.prototype, "duration", void 0);
745
+ __decorate([
746
+ property({ type: String, reflect: true, attribute: 'primary-legend' })
747
+ ], SwingGauge.prototype, "primaryLegend", void 0);
748
+ __decorate([
749
+ property({ type: String, reflect: true, attribute: 'secondary-legend' })
750
+ ], SwingGauge.prototype, "secondaryLegend", void 0);
751
+ __decorate([
752
+ property({ type: Function, attribute: false })
753
+ ], SwingGauge.prototype, "valueFormatter", void 0);
754
+ __decorate([
755
+ state()
756
+ ], SwingGauge.prototype, "primaryContainerStyle", void 0);
757
+ __decorate([
758
+ state()
759
+ ], SwingGauge.prototype, "secondaryContainerStyle", void 0);
760
+ __decorate([
761
+ state()
762
+ ], SwingGauge.prototype, "labelStyle", void 0);
763
+ __decorate([
764
+ state()
765
+ ], SwingGauge.prototype, "valueStyle", void 0);
766
+ __decorate([
767
+ query('[part=primary-container]', true)
768
+ ], SwingGauge.prototype, "primaryContainer", void 0);
769
+ __decorate([
770
+ query('[part=secondary-container]', true)
771
+ ], SwingGauge.prototype, "secondaryContainer", void 0);
772
+ __decorate([
773
+ query('[part=legend-container-outer]')
774
+ ], SwingGauge.prototype, "legendContainer", void 0);
775
+ __decorate([
776
+ query('[part=canvas]', true)
777
+ ], SwingGauge.prototype, "canvas", void 0);
288
778
  SwingGauge = __decorate([
289
779
  customElement('ef-swing-gauge', {
290
780
  alias: 'sapphire-swing-gauge'