@hpcc-js/chart 3.6.5 → 3.7.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 (80) hide show
  1. package/LICENSE +43 -43
  2. package/README.md +93 -93
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.umd.cjs +1 -1
  6. package/dist/index.umd.cjs.map +1 -1
  7. package/package.json +5 -5
  8. package/src/Area.md +176 -176
  9. package/src/Area.ts +12 -12
  10. package/src/Axis.css +36 -34
  11. package/src/Axis.ts +781 -781
  12. package/src/Bar.md +90 -90
  13. package/src/Bar.ts +9 -9
  14. package/src/Bubble.css +16 -16
  15. package/src/Bubble.md +69 -69
  16. package/src/Bubble.ts +196 -196
  17. package/src/BubbleXY.ts +14 -14
  18. package/src/Bullet.css +60 -60
  19. package/src/Bullet.md +104 -104
  20. package/src/Bullet.ts +176 -176
  21. package/src/Column.css +44 -44
  22. package/src/Column.md +90 -90
  23. package/src/Column.ts +684 -684
  24. package/src/Contour.md +88 -88
  25. package/src/Contour.ts +97 -97
  26. package/src/D3Cloud.ts +403 -403
  27. package/src/Gantt.md +119 -119
  28. package/src/Gantt.ts +14 -14
  29. package/src/Gauge.md +148 -148
  30. package/src/Gauge.ts +368 -368
  31. package/src/HalfPie.md +62 -62
  32. package/src/HalfPie.ts +26 -26
  33. package/src/Heat.md +42 -42
  34. package/src/Heat.ts +283 -283
  35. package/src/HexBin.css +9 -9
  36. package/src/HexBin.md +88 -88
  37. package/src/HexBin.ts +144 -144
  38. package/src/Line.css +5 -6
  39. package/src/Line.md +170 -170
  40. package/src/Line.ts +14 -14
  41. package/src/Pie.css +50 -50
  42. package/src/Pie.md +88 -88
  43. package/src/Pie.ts +546 -546
  44. package/src/QuarterPie.md +61 -61
  45. package/src/QuarterPie.ts +35 -35
  46. package/src/QuartileCandlestick.md +129 -129
  47. package/src/QuartileCandlestick.ts +349 -349
  48. package/src/Radar.css +15 -15
  49. package/src/Radar.md +104 -104
  50. package/src/Radar.ts +336 -336
  51. package/src/RadialBar.css +25 -25
  52. package/src/RadialBar.md +91 -91
  53. package/src/RadialBar.ts +217 -217
  54. package/src/Scatter.css +42 -42
  55. package/src/Scatter.md +163 -163
  56. package/src/Scatter.ts +412 -412
  57. package/src/StatChart.md +117 -117
  58. package/src/StatChart.ts +261 -261
  59. package/src/Step.md +163 -163
  60. package/src/Step.ts +12 -12
  61. package/src/Summary.css +56 -56
  62. package/src/Summary.md +219 -219
  63. package/src/Summary.ts +322 -322
  64. package/src/SummaryC.md +154 -154
  65. package/src/SummaryC.ts +240 -240
  66. package/src/WordCloud.css +3 -3
  67. package/src/WordCloud.md +144 -144
  68. package/src/WordCloud.ts +268 -268
  69. package/src/XYAxis.css +41 -41
  70. package/src/XYAxis.md +149 -149
  71. package/src/XYAxis.ts +809 -809
  72. package/src/__package__.ts +3 -3
  73. package/src/__tests__/heat.ts +71 -71
  74. package/src/__tests__/index.ts +3 -3
  75. package/src/__tests__/pie.ts +20 -20
  76. package/src/__tests__/stat.ts +16 -16
  77. package/src/__tests__/test3.ts +68 -68
  78. package/src/index.ts +28 -28
  79. package/src/test.ts +70 -70
  80. package/src/timeFormats.ts +26 -26
package/src/Column.ts CHANGED
@@ -1,684 +1,684 @@
1
- import { INDChart, ITooltip } from "@hpcc-js/api";
2
- import { d3Event, InputField, Text } from "@hpcc-js/common";
3
- import { format as d3Format } from "d3-format";
4
- import { scaleBand as d3ScaleBand } from "d3-scale";
5
- import { local as d3Local, select as d3Select } from "d3-selection";
6
- import { XYAxis } from "./XYAxis.ts";
7
-
8
- import "../src/Column.css";
9
-
10
- export class Column extends XYAxis {
11
- static __inputs: InputField[] = [{
12
- id: "label",
13
- type: "string"
14
- }, {
15
- id: "values",
16
- type: "number",
17
- multi: true
18
- }];
19
-
20
- protected _linearGap: number;
21
- private textLocal = d3Local<Text>();
22
- private stackedTextLocal = d3Local<Text>();
23
- private isHorizontal: boolean;
24
-
25
- constructor() {
26
- super();
27
- INDChart.call(this);
28
- ITooltip.call(this);
29
-
30
- this._selection.skipBringToTop(true);
31
-
32
- this._linearGap = 25.0;
33
- }
34
-
35
- layerEnter(host: XYAxis, element, duration: number = 250) {
36
- super.layerEnter(host, element, duration);
37
-
38
- const context = this;
39
- this
40
- .tooltipHTML(function (d) {
41
- switch (context.tooltipStyle()) {
42
- case "series-table":
43
- return context.tooltipFormat({
44
- label: d.row[0],
45
- arr: context.columns().slice(1).map(function (column, i) {
46
- return {
47
- label: column,
48
- color: context._palette(column),
49
- value: d.row[i + 1]
50
- };
51
- })
52
- });
53
- default:
54
- let value = d.row[d.idx];
55
- if (value instanceof Array) {
56
- value = value[1] - value[0];
57
- }
58
- return context.tooltipFormat({ label: d.row[0], series: context.layerColumns(host)[d.idx], value });
59
- }
60
- })
61
- ;
62
- }
63
-
64
- adjustedData(host: XYAxis) {
65
- const retVal = this.layerData(host).map(row => {
66
- let prevValue = 0;
67
- return row.map((cell, idx) => {
68
- if (idx === 0) {
69
- return cell;
70
- }
71
- if (idx >= this.layerColumns(host).length) {
72
- return cell;
73
- }
74
- const retVal2 = host.yAxisStacked() ? [prevValue, prevValue + cell] : cell;
75
- prevValue += cell;
76
- return retVal2;
77
- }, this);
78
- }, this);
79
- return retVal;
80
- }
81
-
82
- layerUpdate(host: XYAxis, element, duration: number = 250) {
83
- super.layerUpdate(host, element, duration);
84
- const isHorizontal = host.orientation() === "horizontal";
85
- this.isHorizontal = isHorizontal;
86
- const context = this;
87
-
88
- if (this.tabNavigation() && host.parentRelativeDiv) {
89
- host.parentRelativeDiv
90
- .attr("tabindex", "0")
91
- .attr("role", "group")
92
- .attr("aria-label", `${this.columns()[0] || "Chart"} data`);
93
- } else if (host.parentRelativeDiv) {
94
- host.parentRelativeDiv
95
- .attr("tabindex", null)
96
- .attr("role", null)
97
- .attr("aria-label", null);
98
- }
99
-
100
- this._palette = this._palette.switch(this.paletteID());
101
- if (this.useClonedPalette()) {
102
- this._palette = this._palette.cloneNotExists(this.paletteID() + "_" + this.id());
103
- }
104
- const formatPct = d3Format(context.showValueAsPercentFormat());
105
-
106
- let dataLen = 10;
107
- let offset = 0;
108
- switch (host.xAxisType()) {
109
- case "ordinal":
110
- dataLen = host.bandwidth();
111
- offset = -dataLen / 2;
112
- break;
113
- case "linear":
114
- case "time":
115
- dataLen = Math.max(Math.abs(host.dataPos(2) - host.dataPos(1)) * (100 - this._linearGap) / 100, dataLen);
116
- offset = -dataLen / 2;
117
- break;
118
- default:
119
- }
120
-
121
- this.tooltip.direction(isHorizontal ? "n" : "e");
122
-
123
- const columnScale = d3ScaleBand()
124
- .domain(context.layerColumns(host).filter(function (_d, idx) { return idx > 0; }))
125
- .rangeRound(isHorizontal ? [0, dataLen] : [dataLen, 0])
126
- .paddingInner(Math.max(this.xAxisSeriesPaddingInner(), 0.05))
127
- .paddingOuter(0)
128
- ;
129
- let domainSums = [];
130
- const seriesSums = [];
131
- const columnLength = this.columns().length;
132
- const rowData = this.data();
133
- if (this.showValue() && this.showValueAsPercent() === "series") {
134
- rowData.forEach((row) => {
135
- row.filter((_, idx) => idx > 0 && idx < columnLength).forEach((col, idx) => {
136
- if (seriesSums[idx + 1] === undefined) {
137
- seriesSums[idx + 1] = 0;
138
- }
139
- seriesSums[idx + 1] += col;
140
- });
141
- });
142
- }
143
-
144
- if (this.showDomainTotal() || (this.showValue() && this.showValueAsPercent() === "domain")) {
145
- domainSums = rowData.map(row => {
146
- return row.filter((cell, idx) => idx > 0 && idx < columnLength).reduce((sum, cell) => {
147
- return sum + cell;
148
- }, 0);
149
- });
150
- }
151
-
152
- const column = element.selectAll(".dataRow")
153
- .data(this.adjustedData(host))
154
- ;
155
- const hostData = host.data();
156
- const axisSize = this.getAxisSize(host);
157
- column.enter().append("g")
158
- .attr("class", "dataRow")
159
- .merge(column)
160
- .each(function (dataRow, dataRowIdx) {
161
- const element = d3Select(this);
162
-
163
- const columnGRect = element.selectAll(".dataCell").data(dataRow.filter(function (_d, i) { return i < context.layerColumns(host).length; }).map(function (d, i) {
164
- return {
165
- column: context.layerColumns(host)[i],
166
- row: dataRow,
167
- origRow: hostData[dataRowIdx],
168
- value: d,
169
- idx: i
170
- };
171
- }).filter(function (d) { return d.value !== null && d.idx > 0; }), (d: any) => d.column);
172
-
173
- const columnGEnter = columnGRect
174
- .enter().append("g")
175
- .attr("class", "dataCell")
176
- .on("mouseout.tooltip", function (d: any) {
177
- if (!context.tooltipInnerTextEllipsedOnly() || (d.innerTextObj && d.innerTextObj.isTruncated)) {
178
- context.tooltip.hide.apply(context, arguments);
179
- }
180
- })
181
- .on("mousemove.tooltip", function (d: any) {
182
- if (!context.tooltipInnerTextEllipsedOnly() || (d.innerTextObj && d.innerTextObj.isTruncated)) {
183
- context.tooltip.show.apply(context, arguments);
184
- }
185
- })
186
- .call(host._selection.enter.bind(host._selection))
187
- .on("click", function (d: any) {
188
- context.click(host.rowToObj(d.origRow), d.column, host._selection.selected(this));
189
- })
190
- .on("dblclick", function (d: any) {
191
- context.dblclick(host.rowToObj(d.origRow), d.column, host._selection.selected(this));
192
- })
193
- .on("keydown", function (evt, d: any) {
194
- if (context.tabNavigation()) {
195
- const event = d3Event();
196
- if (event.code === "Space" || event.key === "Enter") {
197
- event.preventDefault();
198
- host._selection.click(this);
199
- }
200
- }
201
- })
202
- .style("opacity", 0)
203
- .each(function (this: SVGElement, d: any) {
204
- const element = d3Select(this);
205
- element.append("rect")
206
- .attr("class", "columnRect series series-" + context.cssTag(d.column))
207
- ;
208
- element.append("text")
209
- .attr("class", "columnRectText")
210
- .style("stroke", "transparent")
211
- ;
212
- })
213
- ;
214
- columnGEnter.transition().duration(duration)
215
- .style("opacity", 1)
216
- ;
217
- const domainLength = host.yAxisStacked() ? dataLen : columnScale.bandwidth();
218
- columnGEnter.merge(columnGRect as any)
219
- .attr("tabindex", context.tabNavigation() ? 0 : null) // Tabster Groupper manages these inner focusables
220
- .attr("role", context.tabNavigation() ? "button" : null) // ARIA role for accessibility
221
- .attr("aria-label", context.tabNavigation() ? (d: any) => `${d.origRow[0]} - ${d.column}: ${d.value instanceof Array ? d.value[1] - d.value[0] : d.value}` : null)
222
- .each(function (this: SVGElement, d: any) {
223
- const element = d3Select(this);
224
- const domainPos = host.dataPos(dataRow[0]) + (host.yAxisStacked() ? 0 : columnScale(d.column)) + offset;
225
- const upperValue = d.value instanceof Array ? d.value[1] : d.value;
226
- let valueText = d.origRow[d.idx];
227
- if (context.showValue()) {
228
- const dm = context.dataMeta();
229
- switch (context.showValueAsPercent()) {
230
- case "series":
231
- const seriesSum = typeof dm.sum !== "undefined" ? dm.sum : seriesSums[d.idx];
232
- valueText = formatPct(valueText / seriesSum);
233
- break;
234
- case "domain":
235
- const domainSum = typeof dm.sum !== "undefined" ? dm.sum : domainSums[dataRowIdx];
236
- valueText = formatPct(valueText / domainSum);
237
- break;
238
- case null:
239
- default:
240
- valueText = d3Format(context.showValueFormat())(valueText);
241
- break;
242
- }
243
- }
244
- const upperValuePos = host.valuePos(upperValue);
245
- const lowerValuePos = host.valuePos(d.value instanceof Array ? d.value[0] : 0);
246
- const valuePos = Math.min(lowerValuePos, upperValuePos);
247
- const valueLength = Math.abs(upperValuePos - lowerValuePos);
248
-
249
- const innerTextHeight = context.innerTextFontSize();
250
- const innerTextPadding = context.innerTextPadding_exists() ? context.innerTextPadding() : innerTextHeight / 2.5;
251
-
252
- const dataRect = context.intersectRectRect(
253
- {
254
- x: isHorizontal ? domainPos : valuePos,
255
- y: isHorizontal ? valuePos : domainPos,
256
- width: isHorizontal ? domainLength : valueLength,
257
- height: isHorizontal ? valueLength : domainLength
258
- },
259
- {
260
- x: 0,
261
- y: 0,
262
- width: axisSize.width,
263
- height: axisSize.height
264
- }
265
- );
266
-
267
- const _rects = element.select("rect").transition().duration(duration)
268
- .style("fill", (d: any) => context.fillColor(d.row, d.column, d.value, d.origRow))
269
- ;
270
-
271
- if (isHorizontal) {
272
- _rects
273
- .attr("x", domainPos)
274
- .attr("y", valuePos)
275
- .attr("width", domainLength)
276
- .attr("height", valueLength)
277
- ;
278
- } else {
279
- _rects
280
- .attr("y", domainPos)
281
- .attr("x", valuePos)
282
- .attr("height", domainLength)
283
- .attr("width", valueLength)
284
- ;
285
- }
286
- const _texts = element.select("text").transition().duration(duration)
287
- .style("font-size", innerTextHeight + "px")
288
- .style("fill", (d: any) => context.textColor(d.row, d.column, d.value, d.origRow))
289
- ;
290
-
291
- _texts.style("font-family", context.innerTextFontFamily_exists() ? context.innerTextFontFamily() : null);
292
-
293
- const padding = context.innerTextPadding_exists() ? context.innerTextPadding() : 8;
294
-
295
- const textHeightOffset = innerTextHeight / 2.7;
296
-
297
- if (isHorizontal) { // Column
298
- const y = dataRect.y + dataRect.height - innerTextPadding;
299
- _texts
300
- .attr("x", domainPos + (domainLength / 2))
301
- .attr("y", y + textHeightOffset)
302
- .attr("transform", `rotate(-90, ${domainPos + (domainLength / 2)}, ${y})`)
303
- ;
304
- } else { // Bar
305
- _texts
306
- .attr("x", dataRect.x + padding)
307
- .attr("y", domainPos + (domainLength / 2) + textHeightOffset)
308
- ;
309
- }
310
- _texts
311
- .attr("height", domainLength)
312
- .attr("width", valueLength)
313
- ;
314
- if (context.showInnerText()) {
315
- _texts
316
- .text((d: any) => {
317
- const innerText = context.innerText(d.origRow, d.origRow[columnLength], d.idx);
318
- if (innerText) {
319
- const clippedValueLength = isHorizontal ? dataRect.height : dataRect.width;
320
- const innerTextObj = context.calcInnerText(clippedValueLength, innerText, valueText);
321
- d.innerTextObj = innerTextObj;
322
-
323
- return innerTextObj.text;
324
- }
325
- return "";
326
- })
327
- ;
328
- }
329
- const dataText = element.selectAll(".dataText").data(context.showValue() ? [`${upperValue}`] : []);
330
- const dataTextEnter = dataText.enter().append("g")
331
- .attr("class", "dataText")
332
- .each(function (this: SVGElement, d) {
333
- context.textLocal.set(this, new Text().target(this).colorStroke_default("transparent"));
334
- });
335
- dataTextEnter.merge(dataText as any)
336
- .each(function (this: SVGElement) {
337
- const pos = { x: 0, y: 0 };
338
- const valueFontFamily = context.valueFontFamily();
339
- const valueFontSize = context.valueFontSize();
340
- const textSize = context.textSize(valueText, valueFontFamily, valueFontSize);
341
-
342
- const isPositive = parseFloat(valueText) >= 0;
343
-
344
- let valueAnchor = context.valueAnchor() ? context.valueAnchor() : isHorizontal ? "middle" : "start";
345
-
346
- const leftSpace = dataRect.x;
347
- const rightSpace = axisSize.width - (dataRect.x + dataRect.width);
348
- const topSpace = dataRect.y;
349
- const bottomSpace = axisSize.height - (dataRect.y + dataRect.height);
350
-
351
- let noRoomInside;
352
- let isOutside;
353
- let noRoomOnExpectedSide;
354
-
355
- if (d.innerTextObj) {
356
- const { padding, valueTextWidth } = d.innerTextObj;
357
- isOutside = false;
358
- if (isHorizontal) { // Column
359
- valueAnchor = "middle";
360
- pos.x = domainPos + (domainLength / 2);
361
-
362
- if (d.innerTextObj.category === 4) {
363
- isOutside = true;
364
- pos.y = valuePos - padding - (valueFontSize / 2);
365
- } else {
366
- pos.y = valuePos + padding + (valueFontSize / 2);
367
- }
368
- } else { // Bar
369
- valueAnchor = "start";
370
- if (d.innerTextObj.category === 4) {
371
- isOutside = true;
372
- pos.x = (valueLength + valuePos) + padding;
373
- } else {
374
- pos.x = (valueLength + valuePos) - valueTextWidth - padding;
375
- }
376
- pos.y = domainPos + (domainLength / 2);
377
- }
378
- } else {
379
- /*
380
- IF this.valueCentered() and NO ROOM INSIDE
381
- ...then ASSUME THERES ROOM OUTSIDE
382
- IF NO ROOM OUTSIDE ON EXPECTED SIDE
383
- ...then ASSUME THERES ROOM ON THE OPPOSITE SIDE
384
- */
385
- if (isHorizontal) { // Column
386
- noRoomInside = dataRect.height < textSize.height;
387
- isOutside = !context.valueCentered() || noRoomInside;
388
-
389
- pos.x = dataRect.x + (dataRect.width / 2);
390
-
391
- if (isOutside) {
392
- if (isPositive) {
393
- noRoomOnExpectedSide = topSpace < textSize.height + padding;
394
- if (noRoomOnExpectedSide) {
395
- if (!noRoomInside) {
396
- isOutside = false;
397
- pos.y = dataRect.y + (dataRect.height / 2);
398
- } else {
399
- pos.y = dataRect.y + dataRect.height + textSize.height;
400
- }
401
- } else {
402
- pos.y = dataRect.y - (textSize.height / 2) - padding;
403
- }
404
- } else {
405
- noRoomOnExpectedSide = bottomSpace < textSize.height;
406
- if (noRoomOnExpectedSide) {
407
- if (!noRoomInside) {
408
- isOutside = false;
409
- pos.y = dataRect.y + (dataRect.height / 2);
410
- } else {
411
- pos.y = dataRect.y - (textSize.height / 2) - padding;
412
- }
413
- } else {
414
- pos.y = dataRect.y + textSize.height + padding;
415
- }
416
- }
417
- } else {
418
- pos.y = dataRect.y + (dataRect.height / 2);
419
- }
420
- } else { // Bar
421
- noRoomInside = dataRect.width < textSize.width;
422
- isOutside = !context.valueCentered() || noRoomInside;
423
-
424
- pos.y = dataRect.y + (dataRect.height / 2);
425
-
426
- if (isOutside) {
427
- if (isPositive) {
428
- noRoomOnExpectedSide = rightSpace < textSize.width + padding;
429
- if (noRoomOnExpectedSide) {
430
- if (context.showInnerText() || !noRoomInside) {
431
- isOutside = false;
432
- pos.x = dataRect.x + (dataRect.width / 2);
433
- } else {
434
- pos.x = dataRect.x - (textSize.width - padding);
435
- }
436
- } else {
437
- pos.x = dataRect.x + dataRect.width + (textSize.width / 2) + padding;
438
- }
439
- } else {
440
- noRoomOnExpectedSide = leftSpace < textSize.width;
441
- if (noRoomOnExpectedSide) {
442
- if (context.showInnerText() || !noRoomInside) {
443
- isOutside = false;
444
- pos.x = dataRect.x + (dataRect.width / 2);
445
- } else {
446
- pos.x = dataRect.x + dataRect.width + (textSize.width - padding);
447
- }
448
- } else {
449
- pos.x = dataRect.x - (textSize.width - padding);
450
- }
451
- }
452
- } else {
453
- pos.x = dataRect.x + (dataRect.width / 2);
454
- }
455
- }
456
- }
457
- const textColor = isOutside ? null : context.textColor(d.row, d.column, d.value, d.origRow);
458
-
459
- // Prevent overlapping labels on stacked columns
460
- const columns = context.columns();
461
- const hideValue = (context.yAxisStacked() && noRoomInside) ||
462
- (isOutside && context.yAxisStacked() && columns.indexOf(d.column) !== columns.length - 1);
463
- context.textLocal.get(this)
464
- .pos(pos)
465
- .anchor(valueAnchor)
466
- .fontFamily(valueFontFamily)
467
- .fontSize(valueFontSize)
468
- .text(`${valueText}`)
469
- .colorFill(textColor)
470
- .visible(context.showValue() && !hideValue)
471
- .render()
472
- ;
473
-
474
- });
475
- dataText.exit()
476
- .each(function (this: SVGElement, d) {
477
- context.textLocal.get(this).target(null);
478
- })
479
- .remove()
480
- ;
481
- });
482
- columnGRect.exit().transition().duration(duration)
483
- .style("opacity", 0)
484
- .remove()
485
- ;
486
-
487
- const value4pos = host.yAxisStacked() ? domainSums[dataRowIdx] : Math.max(...dataRow.filter((_, idx) => idx > 0 && idx < columnLength));
488
- const stackedTotalText = element.selectAll(".stackedTotalText").data(context.showDomainTotal() ? [domainSums[dataRowIdx]] : []);
489
- const stackedTotalTextEnter = stackedTotalText.enter().append("g")
490
- .attr("class", "stackedTotalText")
491
- .each(function (this: SVGElement, d) {
492
- context.stackedTextLocal.set(this, new Text().target(this).colorStroke_default("transparent"));
493
- });
494
- stackedTotalTextEnter.merge(stackedTotalText as any)
495
- .each(function (this: SVGElement, d: any) {
496
- const pos = { x: 0, y: 0 };
497
- const domainPos = host.dataPos(dataRow[0]);
498
- const valuePos = host.valuePos(value4pos);
499
-
500
- const valueFontFamily = context.valueFontFamily();
501
- const valueFontSize = context.valueFontSize();
502
- const textSize = context.textSize(d, valueFontFamily, valueFontSize);
503
-
504
- const isPositive = parseFloat(d) >= 0;
505
- let valueAnchor: "start" | "middle" | "end" = "middle";
506
- if (isHorizontal) {
507
- pos.x = domainPos;
508
- if (isPositive) {
509
- pos.y = valuePos - textSize.height / 2;
510
- } else {
511
- pos.y = valuePos + textSize.height / 2;
512
- }
513
- } else {
514
- valueAnchor = "start";
515
- pos.y = domainPos;
516
- if (isPositive) {
517
- pos.x = valuePos + textSize.width / 2;
518
- } else {
519
- pos.x = valuePos - textSize.width / 2;
520
- }
521
- }
522
-
523
- context.stackedTextLocal.get(this)
524
- .pos(pos)
525
- .anchor(valueAnchor)
526
- .fontFamily(valueFontFamily)
527
- .fontSize(valueFontSize)
528
- .text(d)
529
- .render()
530
- ;
531
-
532
- });
533
- stackedTotalText.exit()
534
- .each(function (this: SVGElement, d) {
535
- context.textLocal.get(this).target(null);
536
- })
537
- .remove()
538
- ;
539
- });
540
- column.exit().transition().duration(duration)
541
- .remove()
542
- ;
543
- }
544
-
545
- calcInnerText(offset, innerText, valueText) {
546
- const fontFamily = this.innerTextFontFamily_exists() ? this.innerTextFontFamily() : "Verdana";
547
- const fontSize = this.innerTextFontSize();
548
- const valueFontFamily = this.valueFontFamily_exists() ? this.valueFontFamily() : "Verdana";
549
- const valueFontSize = this.valueFontSize();
550
- const padding = this.innerTextPadding_exists() ? this.innerTextPadding() : fontSize / 2.5;
551
- const valueTextWidth = this.isHorizontal ? valueFontSize : this.textSize(valueText, valueFontFamily, valueFontSize).width;
552
- const ellipsisWidth = this.textSize("...", fontFamily, fontSize).width;
553
- const innerTextWidth = this.textSize(innerText, fontFamily, fontSize).width;
554
- const origInnerText = innerText;
555
-
556
- const fullWidth = (padding * 3) + innerTextWidth + valueTextWidth;
557
- const fullWidth2 = (padding * 3) + ellipsisWidth + valueTextWidth;
558
- const fullWidth3 = (padding * 1) + valueTextWidth;
559
- /*
560
- Categories:
561
- 1) room to display inner text (with padding) AND value text (with padding)
562
- 2) room to display ellipsis (with padding) AND value text (with padding)
563
- 3) room to display value text only (with padding)
564
- 4) no room to display any text except value on the outside
565
- */
566
- let category = 4;
567
- if (fullWidth < offset) {
568
- category = 1;
569
- } else if (fullWidth2 < offset) {
570
- const excessWidth = offset - fullWidth2;
571
- let _text = "";
572
- for (const letter of innerText) {
573
- if (this.textSize(_text + letter, fontFamily, fontSize).width > excessWidth) {
574
- innerText = _text + "...";
575
- break;
576
- } else {
577
- _text += letter;
578
- }
579
- }
580
- category = 2;
581
- } else if (fullWidth3 < offset) {
582
- innerText = "";
583
- category = 3;
584
- } else {
585
- innerText = "";
586
- }
587
-
588
- return {
589
- text: innerText,
590
- isTruncated: origInnerText !== innerText,
591
- padding,
592
- category,
593
- valueTextWidth
594
- };
595
- }
596
-
597
- innerText(origRow, lparam, idx): string {
598
- return origRow[0];
599
- }
600
- }
601
- Column.prototype._class += " chart_Column";
602
- Column.prototype.implements(INDChart.prototype);
603
- Column.prototype.implements(ITooltip.prototype);
604
-
605
- export interface Column {
606
- paletteID(): string;
607
- paletteID(_: string): this;
608
- useClonedPalette(): boolean;
609
- useClonedPalette(_: boolean): this;
610
- showValue(): boolean;
611
- showValue(_: boolean): this;
612
- showInnerText(): boolean;
613
- showInnerText(_: boolean): this;
614
- showValueFormat(): string;
615
- showValueFormat(_: string): this;
616
- showValueAsPercent(): null | "series" | "domain";
617
- showValueAsPercent(_: null | "series" | "domain"): this;
618
- showValueAsPercentFormat(): string;
619
- showValueAsPercentFormat(_: string): this;
620
- showDomainTotal(): boolean;
621
- showDomainTotal(_: boolean): this;
622
- valueCentered(): boolean;
623
- valueCentered(_: boolean): this;
624
- valueAnchor(): "start" | "middle" | "end";
625
- valueAnchor(_: "start" | "middle" | "end"): this;
626
- valueFontFamily(): string;
627
- valueFontFamily(_: string): this;
628
- valueFontFamily_exists(): boolean;
629
- valueFontSize(): number;
630
- valueFontSize(_: number): this;
631
- xAxisSeriesPaddingInner(): number;
632
- xAxisSeriesPaddingInner(_: number): this;
633
- innerTextFontFamily(): string;
634
- innerTextFontFamily(_: string): this;
635
- innerTextFontFamily_exists(): boolean;
636
- innerTextFontSize(): number;
637
- innerTextFontSize(_: number): this;
638
- innerTextPadding(): number;
639
- innerTextPadding(_: number): this;
640
- innerTextPadding_exists(): boolean;
641
- tooltipInnerTextEllipsedOnly(): boolean;
642
- tooltipInnerTextEllipsedOnly(_: boolean): this;
643
-
644
- // INDChart ---
645
- fillColor(row, column, value, origRow): string;
646
- textColor(row, column, value, origRow): string;
647
- dblclick(row, column, selected): void;
648
-
649
- // ITooltip ---
650
- tooltip;
651
- tooltipHTML(_): string;
652
- tooltipFormat(_): string;
653
- tooltipStyle(): "default" | "none" | "series-table";
654
- tooltipStyle(_: "default" | "none" | "series-table"): this;
655
- }
656
-
657
- Column.prototype.publish("valueFontFamily", null, "string", "Font family of value text", null, { optional: true });
658
- Column.prototype.publish("valueFontSize", 12, "number", "Height of value text (pixels)");
659
- Column.prototype.publish("innerTextFontFamily", null, "string", "Font family of inner text", null, { optional: true });
660
- Column.prototype.publish("innerTextPadding", 8, "number", "Offset of inner text (pixels)", null, { optional: true });
661
- Column.prototype.publish("innerTextFontSize", 12, "number", "Height of inner text (pixels)");
662
- Column.prototype.publish("paletteID", "default", "set", "Color palette for this widget", () => Column.prototype._palette.switch(), { tags: ["Basic", "Shared"] });
663
- Column.prototype.publish("useClonedPalette", false, "boolean", "Enable or disable using a cloned palette", null, { tags: ["Intermediate", "Shared"] });
664
- Column.prototype.publish("showValue", false, "boolean", "Show Value in column");
665
- Column.prototype.publish("showInnerText", false, "boolean", "Show Label in column");
666
- Column.prototype.publish("showValueFormat", ",", "string", "D3 Format for Value", null, { disable: (w: Column) => !w.showValue() || !!w.showValueAsPercent() });
667
- Column.prototype.publish("showValueAsPercent", null, "set", "If showValue is true, optionally show value as a percentage by Series or Domain", [null, "series", "domain"], { disable: w => !w.showValue(), optional: true });
668
- Column.prototype.publish("showValueAsPercentFormat", ".0%", "string", "D3 Format for %", null, { disable: (w: Column) => !w.showValue() || !w.showValueAsPercent() });
669
- Column.prototype.publish("showDomainTotal", false, "boolean", "Show Total Value for Stacked Columns", null);
670
- Column.prototype.publish("valueCentered", false, "boolean", "Show Value in center of column");
671
- Column.prototype.publish("valueAnchor", "middle", "set", "text-anchor for shown value text", ["start", "middle", "end"]);
672
- Column.prototype.publish("xAxisSeriesPaddingInner", 0.0, "number", "Determines the ratio of the range that is reserved for blank space between band (0->1)");
673
- Column.prototype.publish("tooltipInnerTextEllipsedOnly", false, "boolean", "Show tooltip only when inner text is truncated with an ellipsis");
674
-
675
- /*
676
- const origUseClonedPalette = Column.prototype.useClonedPalette;
677
- Column.prototype.useClonedPalette = function (this: Column, _?) {
678
- const retVal = origUseClonedPalette.apply(this, arguments);
679
- if (arguments.length) {
680
- this._useClonedPalette = _;
681
- }
682
- return retVal;;
683
- }
684
- */
1
+ import { INDChart, ITooltip } from "@hpcc-js/api";
2
+ import { d3Event, InputField, Text } from "@hpcc-js/common";
3
+ import { format as d3Format } from "d3-format";
4
+ import { scaleBand as d3ScaleBand } from "d3-scale";
5
+ import { local as d3Local, select as d3Select } from "d3-selection";
6
+ import { XYAxis } from "./XYAxis.ts";
7
+
8
+ import "../src/Column.css";
9
+
10
+ export class Column extends XYAxis {
11
+ static __inputs: InputField[] = [{
12
+ id: "label",
13
+ type: "string"
14
+ }, {
15
+ id: "values",
16
+ type: "number",
17
+ multi: true
18
+ }];
19
+
20
+ protected _linearGap: number;
21
+ private textLocal = d3Local<Text>();
22
+ private stackedTextLocal = d3Local<Text>();
23
+ private isHorizontal: boolean;
24
+
25
+ constructor() {
26
+ super();
27
+ INDChart.call(this);
28
+ ITooltip.call(this);
29
+
30
+ this._selection.skipBringToTop(true);
31
+
32
+ this._linearGap = 25.0;
33
+ }
34
+
35
+ layerEnter(host: XYAxis, element, duration: number = 250) {
36
+ super.layerEnter(host, element, duration);
37
+
38
+ const context = this;
39
+ this
40
+ .tooltipHTML(function (d) {
41
+ switch (context.tooltipStyle()) {
42
+ case "series-table":
43
+ return context.tooltipFormat({
44
+ label: d.row[0],
45
+ arr: context.columns().slice(1).map(function (column, i) {
46
+ return {
47
+ label: column,
48
+ color: context._palette(column),
49
+ value: d.row[i + 1]
50
+ };
51
+ })
52
+ });
53
+ default:
54
+ let value = d.row[d.idx];
55
+ if (value instanceof Array) {
56
+ value = value[1] - value[0];
57
+ }
58
+ return context.tooltipFormat({ label: d.row[0], series: context.layerColumns(host)[d.idx], value });
59
+ }
60
+ })
61
+ ;
62
+ }
63
+
64
+ adjustedData(host: XYAxis) {
65
+ const retVal = this.layerData(host).map(row => {
66
+ let prevValue = 0;
67
+ return row.map((cell, idx) => {
68
+ if (idx === 0) {
69
+ return cell;
70
+ }
71
+ if (idx >= this.layerColumns(host).length) {
72
+ return cell;
73
+ }
74
+ const retVal2 = host.yAxisStacked() ? [prevValue, prevValue + cell] : cell;
75
+ prevValue += cell;
76
+ return retVal2;
77
+ }, this);
78
+ }, this);
79
+ return retVal;
80
+ }
81
+
82
+ layerUpdate(host: XYAxis, element, duration: number = 250) {
83
+ super.layerUpdate(host, element, duration);
84
+ const isHorizontal = host.orientation() === "horizontal";
85
+ this.isHorizontal = isHorizontal;
86
+ const context = this;
87
+
88
+ if (this.tabNavigation() && host.parentRelativeDiv) {
89
+ host.parentRelativeDiv
90
+ .attr("tabindex", "0")
91
+ .attr("role", "group")
92
+ .attr("aria-label", `${this.columns()[0] || "Chart"} data`);
93
+ } else if (host.parentRelativeDiv) {
94
+ host.parentRelativeDiv
95
+ .attr("tabindex", null)
96
+ .attr("role", null)
97
+ .attr("aria-label", null);
98
+ }
99
+
100
+ this._palette = this._palette.switch(this.paletteID());
101
+ if (this.useClonedPalette()) {
102
+ this._palette = this._palette.cloneNotExists(this.paletteID() + "_" + this.id());
103
+ }
104
+ const formatPct = d3Format(context.showValueAsPercentFormat());
105
+
106
+ let dataLen = 10;
107
+ let offset = 0;
108
+ switch (host.xAxisType()) {
109
+ case "ordinal":
110
+ dataLen = host.bandwidth();
111
+ offset = -dataLen / 2;
112
+ break;
113
+ case "linear":
114
+ case "time":
115
+ dataLen = Math.max(Math.abs(host.dataPos(2) - host.dataPos(1)) * (100 - this._linearGap) / 100, dataLen);
116
+ offset = -dataLen / 2;
117
+ break;
118
+ default:
119
+ }
120
+
121
+ this.tooltip.direction(isHorizontal ? "n" : "e");
122
+
123
+ const columnScale = d3ScaleBand()
124
+ .domain(context.layerColumns(host).filter(function (_d, idx) { return idx > 0; }))
125
+ .rangeRound(isHorizontal ? [0, dataLen] : [dataLen, 0])
126
+ .paddingInner(Math.max(this.xAxisSeriesPaddingInner(), 0.05))
127
+ .paddingOuter(0)
128
+ ;
129
+ let domainSums = [];
130
+ const seriesSums = [];
131
+ const columnLength = this.columns().length;
132
+ const rowData = this.data();
133
+ if (this.showValue() && this.showValueAsPercent() === "series") {
134
+ rowData.forEach((row) => {
135
+ row.filter((_, idx) => idx > 0 && idx < columnLength).forEach((col, idx) => {
136
+ if (seriesSums[idx + 1] === undefined) {
137
+ seriesSums[idx + 1] = 0;
138
+ }
139
+ seriesSums[idx + 1] += col;
140
+ });
141
+ });
142
+ }
143
+
144
+ if (this.showDomainTotal() || (this.showValue() && this.showValueAsPercent() === "domain")) {
145
+ domainSums = rowData.map(row => {
146
+ return row.filter((cell, idx) => idx > 0 && idx < columnLength).reduce((sum, cell) => {
147
+ return sum + cell;
148
+ }, 0);
149
+ });
150
+ }
151
+
152
+ const column = element.selectAll(".dataRow")
153
+ .data(this.adjustedData(host))
154
+ ;
155
+ const hostData = host.data();
156
+ const axisSize = this.getAxisSize(host);
157
+ column.enter().append("g")
158
+ .attr("class", "dataRow")
159
+ .merge(column)
160
+ .each(function (dataRow, dataRowIdx) {
161
+ const element = d3Select(this);
162
+
163
+ const columnGRect = element.selectAll(".dataCell").data(dataRow.filter(function (_d, i) { return i < context.layerColumns(host).length; }).map(function (d, i) {
164
+ return {
165
+ column: context.layerColumns(host)[i],
166
+ row: dataRow,
167
+ origRow: hostData[dataRowIdx],
168
+ value: d,
169
+ idx: i
170
+ };
171
+ }).filter(function (d) { return d.value !== null && d.idx > 0; }), (d: any) => d.column);
172
+
173
+ const columnGEnter = columnGRect
174
+ .enter().append("g")
175
+ .attr("class", "dataCell")
176
+ .on("mouseout.tooltip", function (d: any) {
177
+ if (!context.tooltipInnerTextEllipsedOnly() || (d.innerTextObj && d.innerTextObj.isTruncated)) {
178
+ context.tooltip.hide.apply(context, arguments);
179
+ }
180
+ })
181
+ .on("mousemove.tooltip", function (d: any) {
182
+ if (!context.tooltipInnerTextEllipsedOnly() || (d.innerTextObj && d.innerTextObj.isTruncated)) {
183
+ context.tooltip.show.apply(context, arguments);
184
+ }
185
+ })
186
+ .call(host._selection.enter.bind(host._selection))
187
+ .on("click", function (d: any) {
188
+ context.click(host.rowToObj(d.origRow), d.column, host._selection.selected(this));
189
+ })
190
+ .on("dblclick", function (d: any) {
191
+ context.dblclick(host.rowToObj(d.origRow), d.column, host._selection.selected(this));
192
+ })
193
+ .on("keydown", function (evt, d: any) {
194
+ if (context.tabNavigation()) {
195
+ const event = d3Event();
196
+ if (event.code === "Space" || event.key === "Enter") {
197
+ event.preventDefault();
198
+ host._selection.click(this);
199
+ }
200
+ }
201
+ })
202
+ .style("opacity", 0)
203
+ .each(function (this: SVGElement, d: any) {
204
+ const element = d3Select(this);
205
+ element.append("rect")
206
+ .attr("class", "columnRect series series-" + context.cssTag(d.column))
207
+ ;
208
+ element.append("text")
209
+ .attr("class", "columnRectText")
210
+ .style("stroke", "transparent")
211
+ ;
212
+ })
213
+ ;
214
+ columnGEnter.transition().duration(duration)
215
+ .style("opacity", 1)
216
+ ;
217
+ const domainLength = host.yAxisStacked() ? dataLen : columnScale.bandwidth();
218
+ columnGEnter.merge(columnGRect as any)
219
+ .attr("tabindex", context.tabNavigation() ? 0 : null) // Tabster Groupper manages these inner focusables
220
+ .attr("role", context.tabNavigation() ? "button" : null) // ARIA role for accessibility
221
+ .attr("aria-label", context.tabNavigation() ? (d: any) => `${d.origRow[0]} - ${d.column}: ${d.value instanceof Array ? d.value[1] - d.value[0] : d.value}` : null)
222
+ .each(function (this: SVGElement, d: any) {
223
+ const element = d3Select(this);
224
+ const domainPos = host.dataPos(dataRow[0]) + (host.yAxisStacked() ? 0 : columnScale(d.column)) + offset;
225
+ const upperValue = d.value instanceof Array ? d.value[1] : d.value;
226
+ let valueText = d.origRow[d.idx];
227
+ if (context.showValue()) {
228
+ const dm = context.dataMeta();
229
+ switch (context.showValueAsPercent()) {
230
+ case "series":
231
+ const seriesSum = typeof dm.sum !== "undefined" ? dm.sum : seriesSums[d.idx];
232
+ valueText = formatPct(valueText / seriesSum);
233
+ break;
234
+ case "domain":
235
+ const domainSum = typeof dm.sum !== "undefined" ? dm.sum : domainSums[dataRowIdx];
236
+ valueText = formatPct(valueText / domainSum);
237
+ break;
238
+ case null:
239
+ default:
240
+ valueText = d3Format(context.showValueFormat())(valueText);
241
+ break;
242
+ }
243
+ }
244
+ const upperValuePos = host.valuePos(upperValue);
245
+ const lowerValuePos = host.valuePos(d.value instanceof Array ? d.value[0] : 0);
246
+ const valuePos = Math.min(lowerValuePos, upperValuePos);
247
+ const valueLength = Math.abs(upperValuePos - lowerValuePos);
248
+
249
+ const innerTextHeight = context.innerTextFontSize();
250
+ const innerTextPadding = context.innerTextPadding_exists() ? context.innerTextPadding() : innerTextHeight / 2.5;
251
+
252
+ const dataRect = context.intersectRectRect(
253
+ {
254
+ x: isHorizontal ? domainPos : valuePos,
255
+ y: isHorizontal ? valuePos : domainPos,
256
+ width: isHorizontal ? domainLength : valueLength,
257
+ height: isHorizontal ? valueLength : domainLength
258
+ },
259
+ {
260
+ x: 0,
261
+ y: 0,
262
+ width: axisSize.width,
263
+ height: axisSize.height
264
+ }
265
+ );
266
+
267
+ const _rects = element.select("rect").transition().duration(duration)
268
+ .style("fill", (d: any) => context.fillColor(d.row, d.column, d.value, d.origRow))
269
+ ;
270
+
271
+ if (isHorizontal) {
272
+ _rects
273
+ .attr("x", domainPos)
274
+ .attr("y", valuePos)
275
+ .attr("width", domainLength)
276
+ .attr("height", valueLength)
277
+ ;
278
+ } else {
279
+ _rects
280
+ .attr("y", domainPos)
281
+ .attr("x", valuePos)
282
+ .attr("height", domainLength)
283
+ .attr("width", valueLength)
284
+ ;
285
+ }
286
+ const _texts = element.select("text").transition().duration(duration)
287
+ .style("font-size", innerTextHeight + "px")
288
+ .style("fill", (d: any) => context.textColor(d.row, d.column, d.value, d.origRow))
289
+ ;
290
+
291
+ _texts.style("font-family", context.innerTextFontFamily_exists() ? context.innerTextFontFamily() : null);
292
+
293
+ const padding = context.innerTextPadding_exists() ? context.innerTextPadding() : 8;
294
+
295
+ const textHeightOffset = innerTextHeight / 2.7;
296
+
297
+ if (isHorizontal) { // Column
298
+ const y = dataRect.y + dataRect.height - innerTextPadding;
299
+ _texts
300
+ .attr("x", domainPos + (domainLength / 2))
301
+ .attr("y", y + textHeightOffset)
302
+ .attr("transform", `rotate(-90, ${domainPos + (domainLength / 2)}, ${y})`)
303
+ ;
304
+ } else { // Bar
305
+ _texts
306
+ .attr("x", dataRect.x + padding)
307
+ .attr("y", domainPos + (domainLength / 2) + textHeightOffset)
308
+ ;
309
+ }
310
+ _texts
311
+ .attr("height", domainLength)
312
+ .attr("width", valueLength)
313
+ ;
314
+ if (context.showInnerText()) {
315
+ _texts
316
+ .text((d: any) => {
317
+ const innerText = context.innerText(d.origRow, d.origRow[columnLength], d.idx);
318
+ if (innerText) {
319
+ const clippedValueLength = isHorizontal ? dataRect.height : dataRect.width;
320
+ const innerTextObj = context.calcInnerText(clippedValueLength, innerText, valueText);
321
+ d.innerTextObj = innerTextObj;
322
+
323
+ return innerTextObj.text;
324
+ }
325
+ return "";
326
+ })
327
+ ;
328
+ }
329
+ const dataText = element.selectAll(".dataText").data(context.showValue() ? [`${upperValue}`] : []);
330
+ const dataTextEnter = dataText.enter().append("g")
331
+ .attr("class", "dataText")
332
+ .each(function (this: SVGElement, d) {
333
+ context.textLocal.set(this, new Text().target(this).colorStroke_default("transparent"));
334
+ });
335
+ dataTextEnter.merge(dataText as any)
336
+ .each(function (this: SVGElement) {
337
+ const pos = { x: 0, y: 0 };
338
+ const valueFontFamily = context.valueFontFamily();
339
+ const valueFontSize = context.valueFontSize();
340
+ const textSize = context.textSize(valueText, valueFontFamily, valueFontSize);
341
+
342
+ const isPositive = parseFloat(valueText) >= 0;
343
+
344
+ let valueAnchor = context.valueAnchor() ? context.valueAnchor() : isHorizontal ? "middle" : "start";
345
+
346
+ const leftSpace = dataRect.x;
347
+ const rightSpace = axisSize.width - (dataRect.x + dataRect.width);
348
+ const topSpace = dataRect.y;
349
+ const bottomSpace = axisSize.height - (dataRect.y + dataRect.height);
350
+
351
+ let noRoomInside;
352
+ let isOutside;
353
+ let noRoomOnExpectedSide;
354
+
355
+ if (d.innerTextObj) {
356
+ const { padding, valueTextWidth } = d.innerTextObj;
357
+ isOutside = false;
358
+ if (isHorizontal) { // Column
359
+ valueAnchor = "middle";
360
+ pos.x = domainPos + (domainLength / 2);
361
+
362
+ if (d.innerTextObj.category === 4) {
363
+ isOutside = true;
364
+ pos.y = valuePos - padding - (valueFontSize / 2);
365
+ } else {
366
+ pos.y = valuePos + padding + (valueFontSize / 2);
367
+ }
368
+ } else { // Bar
369
+ valueAnchor = "start";
370
+ if (d.innerTextObj.category === 4) {
371
+ isOutside = true;
372
+ pos.x = (valueLength + valuePos) + padding;
373
+ } else {
374
+ pos.x = (valueLength + valuePos) - valueTextWidth - padding;
375
+ }
376
+ pos.y = domainPos + (domainLength / 2);
377
+ }
378
+ } else {
379
+ /*
380
+ IF this.valueCentered() and NO ROOM INSIDE
381
+ ...then ASSUME THERES ROOM OUTSIDE
382
+ IF NO ROOM OUTSIDE ON EXPECTED SIDE
383
+ ...then ASSUME THERES ROOM ON THE OPPOSITE SIDE
384
+ */
385
+ if (isHorizontal) { // Column
386
+ noRoomInside = dataRect.height < textSize.height;
387
+ isOutside = !context.valueCentered() || noRoomInside;
388
+
389
+ pos.x = dataRect.x + (dataRect.width / 2);
390
+
391
+ if (isOutside) {
392
+ if (isPositive) {
393
+ noRoomOnExpectedSide = topSpace < textSize.height + padding;
394
+ if (noRoomOnExpectedSide) {
395
+ if (!noRoomInside) {
396
+ isOutside = false;
397
+ pos.y = dataRect.y + (dataRect.height / 2);
398
+ } else {
399
+ pos.y = dataRect.y + dataRect.height + textSize.height;
400
+ }
401
+ } else {
402
+ pos.y = dataRect.y - (textSize.height / 2) - padding;
403
+ }
404
+ } else {
405
+ noRoomOnExpectedSide = bottomSpace < textSize.height;
406
+ if (noRoomOnExpectedSide) {
407
+ if (!noRoomInside) {
408
+ isOutside = false;
409
+ pos.y = dataRect.y + (dataRect.height / 2);
410
+ } else {
411
+ pos.y = dataRect.y - (textSize.height / 2) - padding;
412
+ }
413
+ } else {
414
+ pos.y = dataRect.y + textSize.height + padding;
415
+ }
416
+ }
417
+ } else {
418
+ pos.y = dataRect.y + (dataRect.height / 2);
419
+ }
420
+ } else { // Bar
421
+ noRoomInside = dataRect.width < textSize.width;
422
+ isOutside = !context.valueCentered() || noRoomInside;
423
+
424
+ pos.y = dataRect.y + (dataRect.height / 2);
425
+
426
+ if (isOutside) {
427
+ if (isPositive) {
428
+ noRoomOnExpectedSide = rightSpace < textSize.width + padding;
429
+ if (noRoomOnExpectedSide) {
430
+ if (context.showInnerText() || !noRoomInside) {
431
+ isOutside = false;
432
+ pos.x = dataRect.x + (dataRect.width / 2);
433
+ } else {
434
+ pos.x = dataRect.x - (textSize.width - padding);
435
+ }
436
+ } else {
437
+ pos.x = dataRect.x + dataRect.width + (textSize.width / 2) + padding;
438
+ }
439
+ } else {
440
+ noRoomOnExpectedSide = leftSpace < textSize.width;
441
+ if (noRoomOnExpectedSide) {
442
+ if (context.showInnerText() || !noRoomInside) {
443
+ isOutside = false;
444
+ pos.x = dataRect.x + (dataRect.width / 2);
445
+ } else {
446
+ pos.x = dataRect.x + dataRect.width + (textSize.width - padding);
447
+ }
448
+ } else {
449
+ pos.x = dataRect.x - (textSize.width - padding);
450
+ }
451
+ }
452
+ } else {
453
+ pos.x = dataRect.x + (dataRect.width / 2);
454
+ }
455
+ }
456
+ }
457
+ const textColor = isOutside ? null : context.textColor(d.row, d.column, d.value, d.origRow);
458
+
459
+ // Prevent overlapping labels on stacked columns
460
+ const columns = context.columns();
461
+ const hideValue = (context.yAxisStacked() && noRoomInside) ||
462
+ (isOutside && context.yAxisStacked() && columns.indexOf(d.column) !== columns.length - 1);
463
+ context.textLocal.get(this)
464
+ .pos(pos)
465
+ .anchor(valueAnchor)
466
+ .fontFamily(valueFontFamily)
467
+ .fontSize(valueFontSize)
468
+ .text(`${valueText}`)
469
+ .colorFill(textColor)
470
+ .visible(context.showValue() && !hideValue)
471
+ .render()
472
+ ;
473
+
474
+ });
475
+ dataText.exit()
476
+ .each(function (this: SVGElement, d) {
477
+ context.textLocal.get(this).target(null);
478
+ })
479
+ .remove()
480
+ ;
481
+ });
482
+ columnGRect.exit().transition().duration(duration)
483
+ .style("opacity", 0)
484
+ .remove()
485
+ ;
486
+
487
+ const value4pos = host.yAxisStacked() ? domainSums[dataRowIdx] : Math.max(...dataRow.filter((_, idx) => idx > 0 && idx < columnLength));
488
+ const stackedTotalText = element.selectAll(".stackedTotalText").data(context.showDomainTotal() ? [domainSums[dataRowIdx]] : []);
489
+ const stackedTotalTextEnter = stackedTotalText.enter().append("g")
490
+ .attr("class", "stackedTotalText")
491
+ .each(function (this: SVGElement, d) {
492
+ context.stackedTextLocal.set(this, new Text().target(this).colorStroke_default("transparent"));
493
+ });
494
+ stackedTotalTextEnter.merge(stackedTotalText as any)
495
+ .each(function (this: SVGElement, d: any) {
496
+ const pos = { x: 0, y: 0 };
497
+ const domainPos = host.dataPos(dataRow[0]);
498
+ const valuePos = host.valuePos(value4pos);
499
+
500
+ const valueFontFamily = context.valueFontFamily();
501
+ const valueFontSize = context.valueFontSize();
502
+ const textSize = context.textSize(d, valueFontFamily, valueFontSize);
503
+
504
+ const isPositive = parseFloat(d) >= 0;
505
+ let valueAnchor: "start" | "middle" | "end" = "middle";
506
+ if (isHorizontal) {
507
+ pos.x = domainPos;
508
+ if (isPositive) {
509
+ pos.y = valuePos - textSize.height / 2;
510
+ } else {
511
+ pos.y = valuePos + textSize.height / 2;
512
+ }
513
+ } else {
514
+ valueAnchor = "start";
515
+ pos.y = domainPos;
516
+ if (isPositive) {
517
+ pos.x = valuePos + textSize.width / 2;
518
+ } else {
519
+ pos.x = valuePos - textSize.width / 2;
520
+ }
521
+ }
522
+
523
+ context.stackedTextLocal.get(this)
524
+ .pos(pos)
525
+ .anchor(valueAnchor)
526
+ .fontFamily(valueFontFamily)
527
+ .fontSize(valueFontSize)
528
+ .text(d)
529
+ .render()
530
+ ;
531
+
532
+ });
533
+ stackedTotalText.exit()
534
+ .each(function (this: SVGElement, d) {
535
+ context.textLocal.get(this).target(null);
536
+ })
537
+ .remove()
538
+ ;
539
+ });
540
+ column.exit().transition().duration(duration)
541
+ .remove()
542
+ ;
543
+ }
544
+
545
+ calcInnerText(offset, innerText, valueText) {
546
+ const fontFamily = this.innerTextFontFamily_exists() ? this.innerTextFontFamily() : "Verdana";
547
+ const fontSize = this.innerTextFontSize();
548
+ const valueFontFamily = this.valueFontFamily_exists() ? this.valueFontFamily() : "Verdana";
549
+ const valueFontSize = this.valueFontSize();
550
+ const padding = this.innerTextPadding_exists() ? this.innerTextPadding() : fontSize / 2.5;
551
+ const valueTextWidth = this.isHorizontal ? valueFontSize : this.textSize(valueText, valueFontFamily, valueFontSize).width;
552
+ const ellipsisWidth = this.textSize("...", fontFamily, fontSize).width;
553
+ const innerTextWidth = this.textSize(innerText, fontFamily, fontSize).width;
554
+ const origInnerText = innerText;
555
+
556
+ const fullWidth = (padding * 3) + innerTextWidth + valueTextWidth;
557
+ const fullWidth2 = (padding * 3) + ellipsisWidth + valueTextWidth;
558
+ const fullWidth3 = (padding * 1) + valueTextWidth;
559
+ /*
560
+ Categories:
561
+ 1) room to display inner text (with padding) AND value text (with padding)
562
+ 2) room to display ellipsis (with padding) AND value text (with padding)
563
+ 3) room to display value text only (with padding)
564
+ 4) no room to display any text except value on the outside
565
+ */
566
+ let category = 4;
567
+ if (fullWidth < offset) {
568
+ category = 1;
569
+ } else if (fullWidth2 < offset) {
570
+ const excessWidth = offset - fullWidth2;
571
+ let _text = "";
572
+ for (const letter of innerText) {
573
+ if (this.textSize(_text + letter, fontFamily, fontSize).width > excessWidth) {
574
+ innerText = _text + "...";
575
+ break;
576
+ } else {
577
+ _text += letter;
578
+ }
579
+ }
580
+ category = 2;
581
+ } else if (fullWidth3 < offset) {
582
+ innerText = "";
583
+ category = 3;
584
+ } else {
585
+ innerText = "";
586
+ }
587
+
588
+ return {
589
+ text: innerText,
590
+ isTruncated: origInnerText !== innerText,
591
+ padding,
592
+ category,
593
+ valueTextWidth
594
+ };
595
+ }
596
+
597
+ innerText(origRow, lparam, idx): string {
598
+ return origRow[0];
599
+ }
600
+ }
601
+ Column.prototype._class += " chart_Column";
602
+ Column.prototype.implements(INDChart.prototype);
603
+ Column.prototype.implements(ITooltip.prototype);
604
+
605
+ export interface Column {
606
+ paletteID(): string;
607
+ paletteID(_: string): this;
608
+ useClonedPalette(): boolean;
609
+ useClonedPalette(_: boolean): this;
610
+ showValue(): boolean;
611
+ showValue(_: boolean): this;
612
+ showInnerText(): boolean;
613
+ showInnerText(_: boolean): this;
614
+ showValueFormat(): string;
615
+ showValueFormat(_: string): this;
616
+ showValueAsPercent(): null | "series" | "domain";
617
+ showValueAsPercent(_: null | "series" | "domain"): this;
618
+ showValueAsPercentFormat(): string;
619
+ showValueAsPercentFormat(_: string): this;
620
+ showDomainTotal(): boolean;
621
+ showDomainTotal(_: boolean): this;
622
+ valueCentered(): boolean;
623
+ valueCentered(_: boolean): this;
624
+ valueAnchor(): "start" | "middle" | "end";
625
+ valueAnchor(_: "start" | "middle" | "end"): this;
626
+ valueFontFamily(): string;
627
+ valueFontFamily(_: string): this;
628
+ valueFontFamily_exists(): boolean;
629
+ valueFontSize(): number;
630
+ valueFontSize(_: number): this;
631
+ xAxisSeriesPaddingInner(): number;
632
+ xAxisSeriesPaddingInner(_: number): this;
633
+ innerTextFontFamily(): string;
634
+ innerTextFontFamily(_: string): this;
635
+ innerTextFontFamily_exists(): boolean;
636
+ innerTextFontSize(): number;
637
+ innerTextFontSize(_: number): this;
638
+ innerTextPadding(): number;
639
+ innerTextPadding(_: number): this;
640
+ innerTextPadding_exists(): boolean;
641
+ tooltipInnerTextEllipsedOnly(): boolean;
642
+ tooltipInnerTextEllipsedOnly(_: boolean): this;
643
+
644
+ // INDChart ---
645
+ fillColor(row, column, value, origRow): string;
646
+ textColor(row, column, value, origRow): string;
647
+ dblclick(row, column, selected): void;
648
+
649
+ // ITooltip ---
650
+ tooltip;
651
+ tooltipHTML(_): string;
652
+ tooltipFormat(_): string;
653
+ tooltipStyle(): "default" | "none" | "series-table";
654
+ tooltipStyle(_: "default" | "none" | "series-table"): this;
655
+ }
656
+
657
+ Column.prototype.publish("valueFontFamily", null, "string", "Font family of value text", null, { optional: true });
658
+ Column.prototype.publish("valueFontSize", 12, "number", "Height of value text (pixels)");
659
+ Column.prototype.publish("innerTextFontFamily", null, "string", "Font family of inner text", null, { optional: true });
660
+ Column.prototype.publish("innerTextPadding", 8, "number", "Offset of inner text (pixels)", null, { optional: true });
661
+ Column.prototype.publish("innerTextFontSize", 12, "number", "Height of inner text (pixels)");
662
+ Column.prototype.publish("paletteID", "default", "set", "Color palette for this widget", () => Column.prototype._palette.switch(), { tags: ["Basic", "Shared"] });
663
+ Column.prototype.publish("useClonedPalette", false, "boolean", "Enable or disable using a cloned palette", null, { tags: ["Intermediate", "Shared"] });
664
+ Column.prototype.publish("showValue", false, "boolean", "Show Value in column");
665
+ Column.prototype.publish("showInnerText", false, "boolean", "Show Label in column");
666
+ Column.prototype.publish("showValueFormat", ",", "string", "D3 Format for Value", null, { disable: (w: Column) => !w.showValue() || !!w.showValueAsPercent() });
667
+ Column.prototype.publish("showValueAsPercent", null, "set", "If showValue is true, optionally show value as a percentage by Series or Domain", [null, "series", "domain"], { disable: w => !w.showValue(), optional: true });
668
+ Column.prototype.publish("showValueAsPercentFormat", ".0%", "string", "D3 Format for %", null, { disable: (w: Column) => !w.showValue() || !w.showValueAsPercent() });
669
+ Column.prototype.publish("showDomainTotal", false, "boolean", "Show Total Value for Stacked Columns", null);
670
+ Column.prototype.publish("valueCentered", false, "boolean", "Show Value in center of column");
671
+ Column.prototype.publish("valueAnchor", "middle", "set", "text-anchor for shown value text", ["start", "middle", "end"]);
672
+ Column.prototype.publish("xAxisSeriesPaddingInner", 0.0, "number", "Determines the ratio of the range that is reserved for blank space between band (0->1)");
673
+ Column.prototype.publish("tooltipInnerTextEllipsedOnly", false, "boolean", "Show tooltip only when inner text is truncated with an ellipsis");
674
+
675
+ /*
676
+ const origUseClonedPalette = Column.prototype.useClonedPalette;
677
+ Column.prototype.useClonedPalette = function (this: Column, _?) {
678
+ const retVal = origUseClonedPalette.apply(this, arguments);
679
+ if (arguments.length) {
680
+ this._useClonedPalette = _;
681
+ }
682
+ return retVal;;
683
+ }
684
+ */