@hpcc-js/layout 3.5.1 → 3.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Legend.ts CHANGED
@@ -1,516 +1,516 @@
1
- import { instanceOfIHighlight } from "@hpcc-js/api";
2
- import { Database, Palette, SVGWidget, Widget } from "@hpcc-js/common";
3
- import { format as d3Format } from "d3-format";
4
- import { scaleOrdinal as d3ScaleOrdinal } from "d3-scale";
5
- import {
6
- symbol as d3Symbol,
7
- symbolCircle as d3SymbolCircle,
8
- symbolCross as d3SymbolCross,
9
- symbolDiamond as d3SymbolDiamond,
10
- symbolSquare as d3SymbolSquare,
11
- symbolStar as d3SymbolStar,
12
- symbolTriangle as d3SymbolTriangle,
13
- symbolWye as d3SymbolWye
14
- } from "d3-shape";
15
- import { legendColor as d3LegendColor } from "d3-svg-legend";
16
- import { ChartPanel } from "./ChartPanel.ts";
17
-
18
- export class Legend extends SVGWidget {
19
- _owner: ChartPanel;
20
- _targetWidget: Widget;
21
- _targetWidgetMonitor;
22
- _legendOrdinal;
23
- _disabled: string[] = [];
24
-
25
- private _symbolTypeMap = {
26
- "circle": d3SymbolCircle,
27
- "cross": d3SymbolCross,
28
- "diamond": d3SymbolDiamond,
29
- "square": d3SymbolSquare,
30
- "star": d3SymbolStar,
31
- "triangle": d3SymbolTriangle,
32
- "wye": d3SymbolWye
33
- };
34
-
35
- constructor(owner: ChartPanel) {
36
- super();
37
- this._owner = owner;
38
- this._drawStartPos = "origin";
39
-
40
- const context = this;
41
- this._legendOrdinal = d3LegendColor()
42
- .shape("path", d3Symbol().type(d3SymbolCircle).size(150)())
43
- .shapePadding(10)
44
- .shapeRadius(10)
45
- .on("cellclick", function (d) {
46
- context.onClick(d, this);
47
- })
48
- .on("cellover", (d) => {
49
- context.onOver(d, this);
50
- })
51
- .on("cellout", (d) => {
52
- context.onOut(d, this);
53
- })
54
- ;
55
- }
56
-
57
- isDisabled(d: string | Database.Field): boolean {
58
- if (typeof d === "undefined") {
59
- return false;
60
- } else if (typeof d === "string") {
61
- return d.indexOf("__") === 0 || this._disabled.indexOf(d) >= 0;
62
- } else if (d instanceof Database.Field) {
63
- return d.id().indexOf("__") === 0 || this._disabled.indexOf(d.id()) >= 0;
64
- }
65
- return this._disabled.indexOf(d) >= 0;
66
- }
67
-
68
- filteredFields(): Database.Field[] {
69
- switch (this.dataFamily()) {
70
- case "2D":
71
- return this.fields();
72
- case "ND":
73
- return this.fields().filter(d => !this.isDisabled(d));
74
- }
75
- return this.fields();
76
- }
77
-
78
- filteredColumns(): string[] {
79
- switch (this.dataFamily()) {
80
- case "2D":
81
- return this.columns();
82
- case "ND":
83
- return this.columns().filter(d => !this.isDisabled(d));
84
- }
85
- return this.columns();
86
- }
87
-
88
- filteredData(): any[][] {
89
- switch (this.dataFamily()) {
90
- case "2D":
91
- return this.data().filter(row => !this.isDisabled(row[0]));
92
- case "ND":
93
- const disabledCols: { [key: number]: boolean } = {};
94
- let anyDisabled: boolean = false;
95
- this.columns().forEach((col, idx) => {
96
- const disabled = this.isDisabled(col);
97
- disabledCols[idx] = disabled;
98
- if (disabled) {
99
- anyDisabled = true;
100
- }
101
- });
102
- return !anyDisabled ? this.data() : this.data().map(row => {
103
- return row.filter((cell, idx) => !disabledCols[idx]);
104
- });
105
- }
106
- return this.data();
107
- }
108
-
109
- isRainbow() {
110
- const widget = this.getWidget();
111
- return widget && widget._palette && widget._palette.type() === "rainbow";
112
- }
113
-
114
- targetWidget(): Widget;
115
- targetWidget(_: Widget): this;
116
- targetWidget(_?: Widget): Widget | this {
117
- if (!arguments.length) return this._targetWidget;
118
- this._targetWidget = _;
119
- if (this._targetWidgetMonitor) {
120
- this._targetWidgetMonitor.remove();
121
- delete this._targetWidgetMonitor;
122
- }
123
- if (this._targetWidget) {
124
- const context = this;
125
- this._targetWidgetMonitor = this._targetWidget.monitor(function (key, newProp, oldProp, source) {
126
- switch (key) {
127
- case "chart":
128
- case "columns":
129
- case "data":
130
- case "paletteID":
131
- context.lazyRender();
132
- break;
133
- }
134
- });
135
- }
136
- return this;
137
- }
138
-
139
- getWidget() {
140
- if (this._targetWidget) {
141
- switch (this._targetWidget.classID()) {
142
- case "composite_MultiChart":
143
- return (this._targetWidget as any).chart();
144
- }
145
- }
146
- return this._targetWidget;
147
- }
148
-
149
- getPalette(): Palette.OrdinalPaletteFunc | Palette.RainbowPaletteFunc {
150
- const widget = this.getWidget();
151
- if (widget && widget._palette) {
152
- switch (widget._palette.type()) {
153
- case "ordinal":
154
- return Palette.ordinal(widget._palette.id());
155
- case "rainbow":
156
- return Palette.rainbow(widget._palette.id());
157
- }
158
- }
159
- return Palette.ordinal("default");
160
- }
161
-
162
- getPaletteType() {
163
- return this.getPalette().type();
164
- }
165
-
166
- fillColorFunc() {
167
- const widget = this.getWidget();
168
- if (widget && widget.fillColor) {
169
- // Legend will render before the widget, so its possible the widgets palette will not have switched yet...
170
- if (widget._palette && widget.paletteID && widget._palette.name !== widget.paletteID()) {
171
- widget._palette = widget._palette.switch(widget.paletteID());
172
- }
173
- return (row, col, sel) => {
174
- return widget.fillColor(row, col, sel);
175
- };
176
- }
177
- const palette = Palette.ordinal(widget && widget.paletteID ? widget.paletteID() || "default" : "default");
178
- return (row, col, sel) => {
179
- return palette(col);
180
- };
181
- }
182
-
183
- fillColor(row, col, sel) {
184
- return this.fillColorFunc()(row, col, sel);
185
- }
186
-
187
- protected _g;
188
- enter(domNode, element) {
189
- super.enter(domNode, element);
190
- this._g = element.append("g")
191
- .attr("class", "legendOrdinal")
192
- ;
193
- }
194
-
195
- calcMetaData() {
196
- let dataArr = [];
197
- let total = 0;
198
- let maxLabelWidth = 0;
199
- const colLength = this.columns().length;
200
-
201
- if (this._targetWidget) {
202
- const columns = this.columns();
203
- switch (this.getPaletteType()) {
204
- case "ordinal":
205
- const fillColor = this.fillColorFunc();
206
- let val = 0;
207
- switch (this.dataFamily()) {
208
- case "2D":
209
- dataArr = this.data().map(function (n, i) {
210
- val = this.data()[i].slice(1, colLength).reduce((acc, n) => acc + n, 0);
211
- const disabled = this.isDisabled(n[0]);
212
- if (!disabled) total += val;
213
- const label = n[0] + (!disabled && this.showSeriesTotal() ? ` (${val})` : "");
214
- const textSize = this.textSize(label);
215
- if (maxLabelWidth < textSize.width) maxLabelWidth = textSize.width;
216
- return [fillColor(n, n[0], false), n[0], label];
217
- }, this);
218
- break;
219
- case "ND":
220
- const widgetColumns = this.columns().filter(col => col.indexOf("__") !== 0);
221
- dataArr = widgetColumns.filter(function (n, i) { return i > 0; }).map(function (n, i) {
222
- val = this.data().reduce((acc, n) => acc + n[i + 1], 0);
223
- const disabled = this.isDisabled(columns[i + 1]);
224
- const label = n + (!disabled && this.showSeriesTotal() ? ` (${val})` : "");
225
- if (!disabled) total += val;
226
- const textSize = this.textSize(label);
227
- if (maxLabelWidth < textSize.width) maxLabelWidth = textSize.width;
228
- return [fillColor(undefined, n, false), n, label];
229
- }, this);
230
- break;
231
- default:
232
- const widgetColumns2 = this.columns();
233
- dataArr = widgetColumns2.map(function (n) {
234
- return [fillColor(undefined, n, false), n];
235
- }, this);
236
- break;
237
- }
238
- break;
239
- case "rainbow":
240
- const palette = this.getPalette() as Palette.RainbowPaletteFunc;
241
- const format = d3Format(this.rainbowFormat());
242
- const widget = this.getWidget();
243
- const steps = this.rainbowBins();
244
- const weightMin: number = widget._dataMinWeight;
245
- const weightMax: number = widget._dataMaxWeight;
246
- const stepWeightDiff = (weightMax - weightMin) / (steps - 1);
247
- dataArr.push([palette(weightMin, weightMin, weightMax), format(weightMin)]);
248
- for (let x = 1; x < steps - 1; ++x) {
249
- let mid = stepWeightDiff * x;
250
- if (Math.floor(mid) > parseInt(dataArr[0][1])) {
251
- mid = Math.floor(mid);
252
- }
253
- dataArr.push([palette(mid, weightMin, weightMax), format(mid)]);
254
- }
255
- dataArr.push([palette(weightMax, weightMin, weightMax), format(weightMax)]);
256
- break;
257
- }
258
- }
259
- return {
260
- dataArr,
261
- total,
262
- maxLabelWidth
263
- };
264
- }
265
-
266
- update(domNode, element) {
267
- super.update(domNode, element);
268
-
269
- const { dataArr, maxLabelWidth, total } = this.calcMetaData();
270
-
271
- const radius = this.shapeRadius();
272
- const size = this.radiusToSymbolSize(radius);
273
-
274
- const strokeWidth = 1;
275
-
276
- let shapePadding = this.itemPadding();// + strokeWidth;
277
- if (this.orientation() === "horizontal") {
278
- shapePadding += maxLabelWidth - (radius * 2);
279
- }
280
-
281
- const ordinal = d3ScaleOrdinal()
282
- .domain(dataArr.map(row => row[1]))
283
- .range(dataArr.map(row => row[0]));
284
- this._legendOrdinal
285
- .shape("path", d3Symbol().type(this._symbolTypeMap[this.symbolType()]).size(size)())
286
- .orient(this.orientation())
287
- .title(this.title())
288
- .labelWrap(this.labelMaxWidth())
289
- .labelAlign(this.labelAlign())
290
- .shapePadding(shapePadding)
291
- .scale(ordinal)
292
- .labels(d => dataArr[d.i][2])
293
- ;
294
-
295
- this._g.call(this._legendOrdinal);
296
-
297
- this.updateDisabled(element, dataArr);
298
-
299
- const legendCellsBbox = this._g.select(".legendCells").node().getBBox();
300
- let offsetX = Math.abs(legendCellsBbox.x);
301
- let offsetY = Math.abs(legendCellsBbox.y) + strokeWidth;
302
-
303
- if (this.orientation() === "horizontal") {
304
- if (this.labelAlign() === "start") {
305
- offsetX += strokeWidth;
306
- } else if (this.labelAlign() === "end") {
307
- offsetX -= strokeWidth;
308
- }
309
- if (this.width() > legendCellsBbox.width) {
310
- const extraWidth = this.width() - legendCellsBbox.width;
311
- offsetX += (extraWidth / 2);
312
- }
313
- } else if (this.orientation() === "vertical") {
314
- offsetX += strokeWidth;
315
- if (this._containerSize.height > legendCellsBbox.height) {
316
- const extraHeight = this.height() - legendCellsBbox.height;
317
- offsetY += (extraHeight / 2);
318
- }
319
- }
320
-
321
- this._g.attr("transform", `translate(${offsetX}, ${offsetY})`);
322
- this.pos({
323
- x: 0,
324
- y: 0
325
- });
326
- this._legendOrdinal
327
- .labelOffset(this.itemPadding())
328
- ;
329
- const legendTotal = this._g.selectAll(".legendTotal").data(dataArr.length && this.showLegendTotal() ? [total] : []);
330
- const totalText = `Total: ${total}`;
331
- const totalOffsetX = -offsetX;
332
- const totalOffsetY = legendCellsBbox.height + this.itemPadding() + strokeWidth;
333
- this.enableOverflowScroll(false);
334
- this.enableOverflow(true);
335
- legendTotal
336
- .enter()
337
- .append("text")
338
- .classed("legendTotal", true)
339
- .merge(legendTotal)
340
- .attr("transform", `translate(${totalOffsetX}, ${totalOffsetY})`)
341
- .text(totalText)
342
- ;
343
- legendTotal.exit().remove();
344
- }
345
-
346
- updateDisabled(element, dataArr) {
347
- element
348
- .style("cursor", "pointer")
349
- .selectAll("path.swatch").filter((d, i) => i < dataArr.length)
350
- .style("stroke", (d, i) => dataArr[i][0])
351
- .style("fill", (d, i) =>
352
- this._disabled.indexOf(d) < 0 ? dataArr[i][0] : "white"
353
- )
354
- ;
355
- }
356
-
357
- postUpdate(domNode, element) {
358
- let w;
359
- if (this._boundingBox) {
360
- w = this._boundingBox.width;
361
- this._boundingBox.width = this._size.width;
362
- }
363
- super.postUpdate(domNode, element);
364
- if (w !== undefined) {
365
- this._boundingBox.width = w;
366
- }
367
- this._parentRelativeDiv.style("overflow", "hidden");
368
- }
369
-
370
- exit(domNode, element) {
371
- super.exit(domNode, element);
372
- }
373
-
374
- radiusToSymbolSize(radius) {
375
- const circleSize = Math.pow(radius, 2) * Math.PI;
376
- switch (this.symbolType()) {
377
- case "star":
378
- return circleSize * 0.45;
379
- case "triangle":
380
- return circleSize * 0.65;
381
- case "cross":
382
- case "diamond":
383
- case "wye":
384
- return circleSize * 0.75;
385
- case "circle":
386
- return circleSize;
387
- case "square":
388
- return circleSize * 1.3;
389
- }
390
- }
391
-
392
- onClick(d, domNode) {
393
- switch (this.getPaletteType()) {
394
- case "ordinal":
395
- switch (this.dataFamily()) {
396
- case "2D":
397
- case "ND":
398
- const disabledIdx = this._disabled.indexOf(d);
399
- if (disabledIdx < 0) {
400
- this._disabled.push(d);
401
- } else {
402
- this._disabled.splice(disabledIdx, 1);
403
- }
404
- this._owner.refreshColumns();
405
- this._owner.refreshData();
406
- this._owner.render();
407
- break;
408
- }
409
- break;
410
- }
411
- }
412
-
413
- onOver(d, domNode) {
414
- if (instanceOfIHighlight(this._owner)) {
415
- switch (this.getPaletteType()) {
416
- case "ordinal":
417
- switch (this.dataFamily()) {
418
- case "2D":
419
- case "ND":
420
- if (this._disabled.indexOf(d) < 0) {
421
- this._owner.highlightColumn(d);
422
- }
423
- break;
424
- }
425
- break;
426
- }
427
- }
428
- }
429
-
430
- onOut(d, domNode) {
431
- if (instanceOfIHighlight(this._owner)) {
432
- switch (this.getPaletteType()) {
433
- case "ordinal":
434
- switch (this.dataFamily()) {
435
- case "2D":
436
- case "ND":
437
- this._owner.highlightColumn();
438
- break;
439
- }
440
- break;
441
- }
442
- }
443
- }
444
-
445
- onDblClick(rowData, rowIdx) {
446
- }
447
-
448
- onMouseOver(rowData, rowIdx) {
449
- }
450
- private _containerSize;
451
- resize(_size?: { width: number, height: number }) {
452
- let retVal;
453
- if (this.fitToContent()) {
454
- this._containerSize = _size;
455
- const bbox = this.getBBox();
456
- if (_size.width > bbox.width) {
457
- bbox.width = _size.width;
458
- }
459
- if (_size.height > bbox.height) {
460
- bbox.height = _size.height;
461
- }
462
- retVal = super.resize.apply(this, [{ ...bbox }]);
463
- } else {
464
- retVal = super.resize.apply(this, arguments);
465
- }
466
- return retVal;
467
- }
468
-
469
- }
470
- Legend.prototype._class += " layout_Legend";
471
-
472
- export interface Legend {
473
- title(): string;
474
- title(_: string): this;
475
- symbolType(): "circle" | "cross" | "diamond" | "square" | "star" | "triangle" | "wye";
476
- symbolType(_: "circle" | "cross" | "diamond" | "square" | "star" | "triangle" | "wye"): this;
477
- labelMaxWidth(): number;
478
- labelMaxWidth(_: number): this;
479
- orientation(): "vertical" | "horizontal";
480
- orientation(_: "vertical" | "horizontal"): this;
481
- orientation_exists: () => boolean;
482
- dataFamily(): "1D" | "2D" | "ND" | "map" | "graph" | "any";
483
- dataFamily(_: "1D" | "2D" | "ND" | "map" | "graph" | "any"): this;
484
- dataFamily_exists: () => boolean;
485
- rainbowFormat(): string;
486
- rainbowFormat(_: string): this;
487
- rainbowFormat_exists: () => boolean;
488
- rainbowBins(): number;
489
- rainbowBins(_: number): this;
490
- rainbowBins_exists: () => boolean;
491
- showSeriesTotal(): boolean;
492
- showSeriesTotal(_: boolean): this;
493
- showLegendTotal(): boolean;
494
- showLegendTotal(_: boolean): this;
495
- itemPadding(): number;
496
- itemPadding(_: number): this;
497
- shapeRadius(): number;
498
- shapeRadius(_: number): this;
499
- fitToContent(): boolean;
500
- fitToContent(_: boolean): this;
501
- labelAlign(): "start" | "middle" | "end";
502
- labelAlign(_: "start" | "middle" | "end"): this;
503
- }
504
- Legend.prototype.publish("title", "", "string", "Title");
505
- Legend.prototype.publish("symbolType", "circle", "set", "Shape of each legend item", ["circle", "cross", "diamond", "square", "star", "triangle", "wye"]);
506
- Legend.prototype.publish("labelMaxWidth", null, "number", "Max Label Width (pixels)", null, { optional: true });
507
- Legend.prototype.publish("orientation", "vertical", "set", "Orientation of Legend rows", ["vertical", "horizontal"], { tags: ["Private"] });
508
- Legend.prototype.publish("dataFamily", "ND", "set", "Type of data", ["1D", "2D", "ND", "map", "graph", "any"], { tags: ["Private"] });
509
- Legend.prototype.publish("rainbowFormat", ",", "string", "Rainbow number formatting", null, { tags: ["Private"], optional: true, disable: w => !w.isRainbow() });
510
- Legend.prototype.publish("rainbowBins", 8, "number", "Number of rainbow bins", null, { tags: ["Private"], disable: w => !w.isRainbow() });
511
- Legend.prototype.publish("showSeriesTotal", false, "boolean", "Show value next to series");
512
- Legend.prototype.publish("showLegendTotal", false, "boolean", "Show a total of the series values under the legend", null);
513
- Legend.prototype.publish("itemPadding", 8, "number", "Padding between legend items (pixels)");
514
- Legend.prototype.publish("shapeRadius", 7, "number", "Radius of legend shape (pixels)");
515
- Legend.prototype.publish("fitToContent", true, "boolean", "If true, resize will simply reapply the bounding box dimensions");
516
- Legend.prototype.publish("labelAlign", "start", "set", "Horizontal alignment of legend item label (for horizontal orientation only)", ["start", "middle", "end"], { optional: true, disable: (w: any) => w.orientation() === "vertical" });
1
+ import { instanceOfIHighlight } from "@hpcc-js/api";
2
+ import { Database, Palette, SVGWidget, Widget } from "@hpcc-js/common";
3
+ import { format as d3Format } from "d3-format";
4
+ import { scaleOrdinal as d3ScaleOrdinal } from "d3-scale";
5
+ import {
6
+ symbol as d3Symbol,
7
+ symbolCircle as d3SymbolCircle,
8
+ symbolCross as d3SymbolCross,
9
+ symbolDiamond as d3SymbolDiamond,
10
+ symbolSquare as d3SymbolSquare,
11
+ symbolStar as d3SymbolStar,
12
+ symbolTriangle as d3SymbolTriangle,
13
+ symbolWye as d3SymbolWye
14
+ } from "d3-shape";
15
+ import { legendColor as d3LegendColor } from "d3-svg-legend";
16
+ import { ChartPanel } from "./ChartPanel.ts";
17
+
18
+ export class Legend extends SVGWidget {
19
+ _owner: ChartPanel;
20
+ _targetWidget: Widget;
21
+ _targetWidgetMonitor;
22
+ _legendOrdinal;
23
+ _disabled: string[] = [];
24
+
25
+ private _symbolTypeMap = {
26
+ "circle": d3SymbolCircle,
27
+ "cross": d3SymbolCross,
28
+ "diamond": d3SymbolDiamond,
29
+ "square": d3SymbolSquare,
30
+ "star": d3SymbolStar,
31
+ "triangle": d3SymbolTriangle,
32
+ "wye": d3SymbolWye
33
+ };
34
+
35
+ constructor(owner: ChartPanel) {
36
+ super();
37
+ this._owner = owner;
38
+ this._drawStartPos = "origin";
39
+
40
+ const context = this;
41
+ this._legendOrdinal = d3LegendColor()
42
+ .shape("path", d3Symbol().type(d3SymbolCircle).size(150)())
43
+ .shapePadding(10)
44
+ .shapeRadius(10)
45
+ .on("cellclick", function (d) {
46
+ context.onClick(d, this);
47
+ })
48
+ .on("cellover", (d) => {
49
+ context.onOver(d, this);
50
+ })
51
+ .on("cellout", (d) => {
52
+ context.onOut(d, this);
53
+ })
54
+ ;
55
+ }
56
+
57
+ isDisabled(d: string | Database.Field): boolean {
58
+ if (typeof d === "undefined") {
59
+ return false;
60
+ } else if (typeof d === "string") {
61
+ return d.indexOf("__") === 0 || this._disabled.indexOf(d) >= 0;
62
+ } else if (d instanceof Database.Field) {
63
+ return d.id().indexOf("__") === 0 || this._disabled.indexOf(d.id()) >= 0;
64
+ }
65
+ return this._disabled.indexOf(d) >= 0;
66
+ }
67
+
68
+ filteredFields(): Database.Field[] {
69
+ switch (this.dataFamily()) {
70
+ case "2D":
71
+ return this.fields();
72
+ case "ND":
73
+ return this.fields().filter(d => !this.isDisabled(d));
74
+ }
75
+ return this.fields();
76
+ }
77
+
78
+ filteredColumns(): string[] {
79
+ switch (this.dataFamily()) {
80
+ case "2D":
81
+ return this.columns();
82
+ case "ND":
83
+ return this.columns().filter(d => !this.isDisabled(d));
84
+ }
85
+ return this.columns();
86
+ }
87
+
88
+ filteredData(): any[][] {
89
+ switch (this.dataFamily()) {
90
+ case "2D":
91
+ return this.data().filter(row => !this.isDisabled(row[0]));
92
+ case "ND":
93
+ const disabledCols: { [key: number]: boolean } = {};
94
+ let anyDisabled: boolean = false;
95
+ this.columns().forEach((col, idx) => {
96
+ const disabled = this.isDisabled(col);
97
+ disabledCols[idx] = disabled;
98
+ if (disabled) {
99
+ anyDisabled = true;
100
+ }
101
+ });
102
+ return !anyDisabled ? this.data() : this.data().map(row => {
103
+ return row.filter((cell, idx) => !disabledCols[idx]);
104
+ });
105
+ }
106
+ return this.data();
107
+ }
108
+
109
+ isRainbow() {
110
+ const widget = this.getWidget();
111
+ return widget && widget._palette && widget._palette.type() === "rainbow";
112
+ }
113
+
114
+ targetWidget(): Widget;
115
+ targetWidget(_: Widget): this;
116
+ targetWidget(_?: Widget): Widget | this {
117
+ if (!arguments.length) return this._targetWidget;
118
+ this._targetWidget = _;
119
+ if (this._targetWidgetMonitor) {
120
+ this._targetWidgetMonitor.remove();
121
+ delete this._targetWidgetMonitor;
122
+ }
123
+ if (this._targetWidget) {
124
+ const context = this;
125
+ this._targetWidgetMonitor = this._targetWidget.monitor(function (key, newProp, oldProp, source) {
126
+ switch (key) {
127
+ case "chart":
128
+ case "columns":
129
+ case "data":
130
+ case "paletteID":
131
+ context.lazyRender();
132
+ break;
133
+ }
134
+ });
135
+ }
136
+ return this;
137
+ }
138
+
139
+ getWidget() {
140
+ if (this._targetWidget) {
141
+ switch (this._targetWidget.classID()) {
142
+ case "composite_MultiChart":
143
+ return (this._targetWidget as any).chart();
144
+ }
145
+ }
146
+ return this._targetWidget;
147
+ }
148
+
149
+ getPalette(): Palette.OrdinalPaletteFunc | Palette.RainbowPaletteFunc {
150
+ const widget = this.getWidget();
151
+ if (widget && widget._palette) {
152
+ switch (widget._palette.type()) {
153
+ case "ordinal":
154
+ return Palette.ordinal(widget._palette.id());
155
+ case "rainbow":
156
+ return Palette.rainbow(widget._palette.id());
157
+ }
158
+ }
159
+ return Palette.ordinal("default");
160
+ }
161
+
162
+ getPaletteType() {
163
+ return this.getPalette().type();
164
+ }
165
+
166
+ fillColorFunc() {
167
+ const widget = this.getWidget();
168
+ if (widget && widget.fillColor) {
169
+ // Legend will render before the widget, so its possible the widgets palette will not have switched yet...
170
+ if (widget._palette && widget.paletteID && widget._palette.name !== widget.paletteID()) {
171
+ widget._palette = widget._palette.switch(widget.paletteID());
172
+ }
173
+ return (row, col, sel) => {
174
+ return widget.fillColor(row, col, sel);
175
+ };
176
+ }
177
+ const palette = Palette.ordinal(widget && widget.paletteID ? widget.paletteID() || "default" : "default");
178
+ return (row, col, sel) => {
179
+ return palette(col);
180
+ };
181
+ }
182
+
183
+ fillColor(row, col, sel) {
184
+ return this.fillColorFunc()(row, col, sel);
185
+ }
186
+
187
+ protected _g;
188
+ enter(domNode, element) {
189
+ super.enter(domNode, element);
190
+ this._g = element.append("g")
191
+ .attr("class", "legendOrdinal")
192
+ ;
193
+ }
194
+
195
+ calcMetaData() {
196
+ let dataArr = [];
197
+ let total = 0;
198
+ let maxLabelWidth = 0;
199
+ const colLength = this.columns().length;
200
+
201
+ if (this._targetWidget) {
202
+ const columns = this.columns();
203
+ switch (this.getPaletteType()) {
204
+ case "ordinal":
205
+ const fillColor = this.fillColorFunc();
206
+ let val = 0;
207
+ switch (this.dataFamily()) {
208
+ case "2D":
209
+ dataArr = this.data().map(function (n, i) {
210
+ val = this.data()[i].slice(1, colLength).reduce((acc, n) => acc + n, 0);
211
+ const disabled = this.isDisabled(n[0]);
212
+ if (!disabled) total += val;
213
+ const label = n[0] + (!disabled && this.showSeriesTotal() ? ` (${val})` : "");
214
+ const textSize = this.textSize(label);
215
+ if (maxLabelWidth < textSize.width) maxLabelWidth = textSize.width;
216
+ return [fillColor(n, n[0], false), n[0], label];
217
+ }, this);
218
+ break;
219
+ case "ND":
220
+ const widgetColumns = this.columns().filter(col => col.indexOf("__") !== 0);
221
+ dataArr = widgetColumns.filter(function (n, i) { return i > 0; }).map(function (n, i) {
222
+ val = this.data().reduce((acc, n) => acc + n[i + 1], 0);
223
+ const disabled = this.isDisabled(columns[i + 1]);
224
+ const label = n + (!disabled && this.showSeriesTotal() ? ` (${val})` : "");
225
+ if (!disabled) total += val;
226
+ const textSize = this.textSize(label);
227
+ if (maxLabelWidth < textSize.width) maxLabelWidth = textSize.width;
228
+ return [fillColor(undefined, n, false), n, label];
229
+ }, this);
230
+ break;
231
+ default:
232
+ const widgetColumns2 = this.columns();
233
+ dataArr = widgetColumns2.map(function (n) {
234
+ return [fillColor(undefined, n, false), n];
235
+ }, this);
236
+ break;
237
+ }
238
+ break;
239
+ case "rainbow":
240
+ const palette = this.getPalette() as Palette.RainbowPaletteFunc;
241
+ const format = d3Format(this.rainbowFormat());
242
+ const widget = this.getWidget();
243
+ const steps = this.rainbowBins();
244
+ const weightMin: number = widget._dataMinWeight;
245
+ const weightMax: number = widget._dataMaxWeight;
246
+ const stepWeightDiff = (weightMax - weightMin) / (steps - 1);
247
+ dataArr.push([palette(weightMin, weightMin, weightMax), format(weightMin)]);
248
+ for (let x = 1; x < steps - 1; ++x) {
249
+ let mid = stepWeightDiff * x;
250
+ if (Math.floor(mid) > parseInt(dataArr[0][1])) {
251
+ mid = Math.floor(mid);
252
+ }
253
+ dataArr.push([palette(mid, weightMin, weightMax), format(mid)]);
254
+ }
255
+ dataArr.push([palette(weightMax, weightMin, weightMax), format(weightMax)]);
256
+ break;
257
+ }
258
+ }
259
+ return {
260
+ dataArr,
261
+ total,
262
+ maxLabelWidth
263
+ };
264
+ }
265
+
266
+ update(domNode, element) {
267
+ super.update(domNode, element);
268
+
269
+ const { dataArr, maxLabelWidth, total } = this.calcMetaData();
270
+
271
+ const radius = this.shapeRadius();
272
+ const size = this.radiusToSymbolSize(radius);
273
+
274
+ const strokeWidth = 1;
275
+
276
+ let shapePadding = this.itemPadding();// + strokeWidth;
277
+ if (this.orientation() === "horizontal") {
278
+ shapePadding += maxLabelWidth - (radius * 2);
279
+ }
280
+
281
+ const ordinal = d3ScaleOrdinal()
282
+ .domain(dataArr.map(row => row[1]))
283
+ .range(dataArr.map(row => row[0]));
284
+ this._legendOrdinal
285
+ .shape("path", d3Symbol().type(this._symbolTypeMap[this.symbolType()]).size(size)())
286
+ .orient(this.orientation())
287
+ .title(this.title())
288
+ .labelWrap(this.labelMaxWidth())
289
+ .labelAlign(this.labelAlign())
290
+ .shapePadding(shapePadding)
291
+ .scale(ordinal)
292
+ .labels(d => dataArr[d.i][2])
293
+ ;
294
+
295
+ this._g.call(this._legendOrdinal);
296
+
297
+ this.updateDisabled(element, dataArr);
298
+
299
+ const legendCellsBbox = this._g.select(".legendCells").node().getBBox();
300
+ let offsetX = Math.abs(legendCellsBbox.x);
301
+ let offsetY = Math.abs(legendCellsBbox.y) + strokeWidth;
302
+
303
+ if (this.orientation() === "horizontal") {
304
+ if (this.labelAlign() === "start") {
305
+ offsetX += strokeWidth;
306
+ } else if (this.labelAlign() === "end") {
307
+ offsetX -= strokeWidth;
308
+ }
309
+ if (this.width() > legendCellsBbox.width) {
310
+ const extraWidth = this.width() - legendCellsBbox.width;
311
+ offsetX += (extraWidth / 2);
312
+ }
313
+ } else if (this.orientation() === "vertical") {
314
+ offsetX += strokeWidth;
315
+ if (this._containerSize.height > legendCellsBbox.height) {
316
+ const extraHeight = this.height() - legendCellsBbox.height;
317
+ offsetY += (extraHeight / 2);
318
+ }
319
+ }
320
+
321
+ this._g.attr("transform", `translate(${offsetX}, ${offsetY})`);
322
+ this.pos({
323
+ x: 0,
324
+ y: 0
325
+ });
326
+ this._legendOrdinal
327
+ .labelOffset(this.itemPadding())
328
+ ;
329
+ const legendTotal = this._g.selectAll(".legendTotal").data(dataArr.length && this.showLegendTotal() ? [total] : []);
330
+ const totalText = `Total: ${total}`;
331
+ const totalOffsetX = -offsetX;
332
+ const totalOffsetY = legendCellsBbox.height + this.itemPadding() + strokeWidth;
333
+ this.enableOverflowScroll(false);
334
+ this.enableOverflow(true);
335
+ legendTotal
336
+ .enter()
337
+ .append("text")
338
+ .classed("legendTotal", true)
339
+ .merge(legendTotal)
340
+ .attr("transform", `translate(${totalOffsetX}, ${totalOffsetY})`)
341
+ .text(totalText)
342
+ ;
343
+ legendTotal.exit().remove();
344
+ }
345
+
346
+ updateDisabled(element, dataArr) {
347
+ element
348
+ .style("cursor", "pointer")
349
+ .selectAll("path.swatch").filter((d, i) => i < dataArr.length)
350
+ .style("stroke", (d, i) => dataArr[i][0])
351
+ .style("fill", (d, i) =>
352
+ this._disabled.indexOf(d) < 0 ? dataArr[i][0] : "white"
353
+ )
354
+ ;
355
+ }
356
+
357
+ postUpdate(domNode, element) {
358
+ let w;
359
+ if (this._boundingBox) {
360
+ w = this._boundingBox.width;
361
+ this._boundingBox.width = this._size.width;
362
+ }
363
+ super.postUpdate(domNode, element);
364
+ if (w !== undefined) {
365
+ this._boundingBox.width = w;
366
+ }
367
+ this._parentRelativeDiv.style("overflow", "hidden");
368
+ }
369
+
370
+ exit(domNode, element) {
371
+ super.exit(domNode, element);
372
+ }
373
+
374
+ radiusToSymbolSize(radius) {
375
+ const circleSize = Math.pow(radius, 2) * Math.PI;
376
+ switch (this.symbolType()) {
377
+ case "star":
378
+ return circleSize * 0.45;
379
+ case "triangle":
380
+ return circleSize * 0.65;
381
+ case "cross":
382
+ case "diamond":
383
+ case "wye":
384
+ return circleSize * 0.75;
385
+ case "circle":
386
+ return circleSize;
387
+ case "square":
388
+ return circleSize * 1.3;
389
+ }
390
+ }
391
+
392
+ onClick(d, domNode) {
393
+ switch (this.getPaletteType()) {
394
+ case "ordinal":
395
+ switch (this.dataFamily()) {
396
+ case "2D":
397
+ case "ND":
398
+ const disabledIdx = this._disabled.indexOf(d);
399
+ if (disabledIdx < 0) {
400
+ this._disabled.push(d);
401
+ } else {
402
+ this._disabled.splice(disabledIdx, 1);
403
+ }
404
+ this._owner.refreshColumns();
405
+ this._owner.refreshData();
406
+ this._owner.render();
407
+ break;
408
+ }
409
+ break;
410
+ }
411
+ }
412
+
413
+ onOver(d, domNode) {
414
+ if (instanceOfIHighlight(this._owner)) {
415
+ switch (this.getPaletteType()) {
416
+ case "ordinal":
417
+ switch (this.dataFamily()) {
418
+ case "2D":
419
+ case "ND":
420
+ if (this._disabled.indexOf(d) < 0) {
421
+ this._owner.highlightColumn(d);
422
+ }
423
+ break;
424
+ }
425
+ break;
426
+ }
427
+ }
428
+ }
429
+
430
+ onOut(d, domNode) {
431
+ if (instanceOfIHighlight(this._owner)) {
432
+ switch (this.getPaletteType()) {
433
+ case "ordinal":
434
+ switch (this.dataFamily()) {
435
+ case "2D":
436
+ case "ND":
437
+ this._owner.highlightColumn();
438
+ break;
439
+ }
440
+ break;
441
+ }
442
+ }
443
+ }
444
+
445
+ onDblClick(rowData, rowIdx) {
446
+ }
447
+
448
+ onMouseOver(rowData, rowIdx) {
449
+ }
450
+ private _containerSize;
451
+ resize(_size?: { width: number, height: number }) {
452
+ let retVal;
453
+ if (this.fitToContent()) {
454
+ this._containerSize = _size;
455
+ const bbox = this.getBBox();
456
+ if (_size.width > bbox.width) {
457
+ bbox.width = _size.width;
458
+ }
459
+ if (_size.height > bbox.height) {
460
+ bbox.height = _size.height;
461
+ }
462
+ retVal = super.resize.apply(this, [{ ...bbox }]);
463
+ } else {
464
+ retVal = super.resize.apply(this, arguments);
465
+ }
466
+ return retVal;
467
+ }
468
+
469
+ }
470
+ Legend.prototype._class += " layout_Legend";
471
+
472
+ export interface Legend {
473
+ title(): string;
474
+ title(_: string): this;
475
+ symbolType(): "circle" | "cross" | "diamond" | "square" | "star" | "triangle" | "wye";
476
+ symbolType(_: "circle" | "cross" | "diamond" | "square" | "star" | "triangle" | "wye"): this;
477
+ labelMaxWidth(): number;
478
+ labelMaxWidth(_: number): this;
479
+ orientation(): "vertical" | "horizontal";
480
+ orientation(_: "vertical" | "horizontal"): this;
481
+ orientation_exists: () => boolean;
482
+ dataFamily(): "1D" | "2D" | "ND" | "map" | "graph" | "any";
483
+ dataFamily(_: "1D" | "2D" | "ND" | "map" | "graph" | "any"): this;
484
+ dataFamily_exists: () => boolean;
485
+ rainbowFormat(): string;
486
+ rainbowFormat(_: string): this;
487
+ rainbowFormat_exists: () => boolean;
488
+ rainbowBins(): number;
489
+ rainbowBins(_: number): this;
490
+ rainbowBins_exists: () => boolean;
491
+ showSeriesTotal(): boolean;
492
+ showSeriesTotal(_: boolean): this;
493
+ showLegendTotal(): boolean;
494
+ showLegendTotal(_: boolean): this;
495
+ itemPadding(): number;
496
+ itemPadding(_: number): this;
497
+ shapeRadius(): number;
498
+ shapeRadius(_: number): this;
499
+ fitToContent(): boolean;
500
+ fitToContent(_: boolean): this;
501
+ labelAlign(): "start" | "middle" | "end";
502
+ labelAlign(_: "start" | "middle" | "end"): this;
503
+ }
504
+ Legend.prototype.publish("title", "", "string", "Title");
505
+ Legend.prototype.publish("symbolType", "circle", "set", "Shape of each legend item", ["circle", "cross", "diamond", "square", "star", "triangle", "wye"]);
506
+ Legend.prototype.publish("labelMaxWidth", null, "number", "Max Label Width (pixels)", null, { optional: true });
507
+ Legend.prototype.publish("orientation", "vertical", "set", "Orientation of Legend rows", ["vertical", "horizontal"], { tags: ["Private"] });
508
+ Legend.prototype.publish("dataFamily", "ND", "set", "Type of data", ["1D", "2D", "ND", "map", "graph", "any"], { tags: ["Private"] });
509
+ Legend.prototype.publish("rainbowFormat", ",", "string", "Rainbow number formatting", null, { tags: ["Private"], optional: true, disable: w => !w.isRainbow() });
510
+ Legend.prototype.publish("rainbowBins", 8, "number", "Number of rainbow bins", null, { tags: ["Private"], disable: w => !w.isRainbow() });
511
+ Legend.prototype.publish("showSeriesTotal", false, "boolean", "Show value next to series");
512
+ Legend.prototype.publish("showLegendTotal", false, "boolean", "Show a total of the series values under the legend", null);
513
+ Legend.prototype.publish("itemPadding", 8, "number", "Padding between legend items (pixels)");
514
+ Legend.prototype.publish("shapeRadius", 7, "number", "Radius of legend shape (pixels)");
515
+ Legend.prototype.publish("fitToContent", true, "boolean", "If true, resize will simply reapply the bounding box dimensions");
516
+ Legend.prototype.publish("labelAlign", "start", "set", "Horizontal alignment of legend item label (for horizontal orientation only)", ["start", "middle", "end"], { optional: true, disable: (w: any) => w.orientation() === "vertical" });