@hpcc-js/timeline 3.3.2 → 3.3.5

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/ReactGantt.ts CHANGED
@@ -1,773 +1,773 @@
1
- import { d3Event, select as d3Select, SVGZoomWidget, Utility } from "@hpcc-js/common";
2
- import { HTMLTooltip } from "@hpcc-js/html";
3
- import { scaleLinear as d3ScaleLinear } from "d3-scale";
4
- import { React, render, LabelledRect } from "@hpcc-js/react";
5
-
6
- export type IGanttData = [string, number, number, any?];
7
-
8
- export interface IRangeOptions {
9
- rangePadding: number;
10
- fontFamily: string;
11
- fontSize: number;
12
- strokeWidth?: number;
13
- fill: string;
14
- stroke: string;
15
- textFill: string;
16
- cornerRadius: number;
17
- }
18
-
19
- export class ReactGantt extends SVGZoomWidget {
20
-
21
- protected _selection = new Utility.Selection(this);
22
-
23
- protected _buckets;
24
- protected _interpolateX;
25
- protected _interpolateY;
26
-
27
- protected _bucketsBySeries;
28
- protected _dataBySeries;
29
- protected _origIdxMap;
30
- private _seriesBackgrounds;
31
-
32
- protected _maxFontSize;
33
-
34
- public _tooltip;
35
-
36
- public _minStart: number;
37
- public _maxEnd: number;
38
-
39
- protected _prevZoomState: { visibleStart: number; visibleEnd: number } | null = null;
40
-
41
- protected _title_idx = 0;
42
- protected _startDate_idx = 1;
43
- protected _endDate_idx = 2;
44
- protected _icon_idx = -1;
45
- protected _color_idx = -1;
46
- protected _series_idx = -1;
47
- protected _bucket_idx = -1;
48
- protected _yoffset_idx = -1;
49
-
50
- protected _maxX: number;
51
- protected _maxY: number;
52
-
53
- private _rangeOptions: IRangeOptions = {
54
- rangePadding: 2,
55
- fontFamily: "Verdana",
56
- fontSize: 12,
57
- fill: "white",
58
- stroke: "black",
59
- textFill: "black",
60
- cornerRadius: 3,
61
- strokeWidth: 0
62
- };
63
-
64
- constructor(drawStartPosition: "origin" | "center" = "origin") {
65
- super();
66
- this._drawStartPos = drawStartPosition;
67
-
68
- this.showToolbar_default(false);
69
-
70
- this._tooltip = new HTMLTooltip();
71
- this._tooltip
72
- .tooltipHTML(d => {
73
- return `<div style="text-align:center;">${d[0]}<br/><br/>${d[1]} -&gt; ${d[2]}</div>`;
74
- });
75
- this._tooltip
76
- .followCursor(true)
77
- ;
78
- }
79
-
80
- selection(_: any[]): this;
81
- selection(): any[];
82
- selection(_?: any[]): any[] | this {
83
- if (!arguments.length) return this._selection.get();
84
- this._selection.set(_);
85
- return this;
86
- }
87
-
88
- private _rangeRenderer: React.FunctionComponent = LabelledRect;
89
- rangeRenderer(): React.FunctionComponent;
90
- rangeRenderer(_: React.FunctionComponent): this;
91
- rangeRenderer(_?: React.FunctionComponent): this | React.FunctionComponent {
92
- if (!arguments.length) return this._rangeRenderer;
93
- this._rangeRenderer = _;
94
- return this._rangeRenderer;
95
- }
96
-
97
- enter(domNode, element) {
98
- super.enter(domNode, element);
99
-
100
- const context = this;
101
- element
102
- .on("click", function (this: SVGElement, d) {
103
- context._selection.clear();
104
- });
105
-
106
- this._tooltip.target(domNode);
107
- }
108
- update(domNode, element) {
109
- super.update(domNode, element);
110
-
111
- this.zoomExtent([0.05, this.maxZoom()]);
112
-
113
- this._title_idx = this.titleColumn() !== null ? this.columns().indexOf(this.titleColumn()) : this._title_idx;
114
- this._startDate_idx = this.startDateColumn() !== null ? this.columns().indexOf(this.startDateColumn()) : this._startDate_idx;
115
- this._endDate_idx = this.endDateColumn() !== null ? this.columns().indexOf(this.endDateColumn()) : this._endDate_idx;
116
- this._icon_idx = this.iconColumn() !== null ? this.columns().indexOf(this.iconColumn()) : this._icon_idx;
117
- this._color_idx = this.colorColumn() !== null ? this.columns().indexOf(this.colorColumn()) : this._color_idx;
118
- this._series_idx = this.seriesColumn() !== null ? this.columns().indexOf(this.seriesColumn()) : this._series_idx;
119
- this._bucket_idx = this.bucketColumn() !== null ? this.columns().indexOf(this.bucketColumn()) : -1;
120
-
121
- const context = this;
122
- const w = this.width();
123
-
124
- const x0 = 0;
125
- const x1 = w;
126
-
127
- this._interpolateX = d3ScaleLinear()
128
- .domain([this._minStart, this._maxEnd])
129
- .range([x0, x1])
130
- ;
131
-
132
- this.data().sort((a, b) => a[1] - b[1]);
133
-
134
- if (this._series_idx > -1) {
135
- this._origIdxMap = {};
136
- this._dataBySeries = {};
137
- this._bucketsBySeries = {};
138
- this.data().forEach((dataRow, origIdx) => {
139
- const seriesKey = dataRow[this._series_idx];
140
- if (!this._dataBySeries[seriesKey]) {
141
- this._origIdxMap[seriesKey] = {};
142
- this._dataBySeries[seriesKey] = [];
143
- }
144
- this._dataBySeries[seriesKey].push({
145
- dataRow,
146
- origIdx
147
- });
148
- });
149
- const gutter = this.gutter();
150
- let bucketOffset = 0;
151
- const seriesKeys = Object.keys(this._dataBySeries);
152
- seriesKeys.forEach(seriesKey => {
153
- this._dataBySeries[seriesKey].sort((a, b) => a.dataRow[1] - b.dataRow[1]);
154
- this._bucketsBySeries[seriesKey] = this.calcBuckets(this._dataBySeries[seriesKey].map(n => n.dataRow), 1, 2);
155
- this._bucketsBySeries[seriesKey].bucketHeight = this.bucketHeight();
156
- this._bucketsBySeries[seriesKey].bucketOffset = bucketOffset;
157
- bucketOffset += (this._bucketsBySeries[seriesKey].bucketHeight + this.strokeWidth() + this.gutter()) * (this._bucketsBySeries[seriesKey].maxBucket + 1);
158
- this._dataBySeries[seriesKey].forEach((n, i) => {
159
- this._origIdxMap[seriesKey][n.origIdx] = i;
160
- });
161
- });
162
- this._seriesBackgrounds = this._renderElement.selectAll(".series-background")
163
- .data(seriesKeys.map(key => {
164
- return this._bucketsBySeries[key];
165
- }))
166
- ;
167
- this._seriesBackgrounds
168
- .join(
169
- enter => enter.append("rect")
170
- .attr("class", "series-background"),
171
- update => update,
172
- exit => exit
173
- .each(function (d) {
174
- delete d.element;
175
- })
176
- .remove()
177
- )
178
- .attr("opacity", d => d.props && d.props.hidden ? 0 : 1)
179
- .each(function (this: SVGGElement, d, i) {
180
- d3Select(this)
181
- .attr("x", 0)
182
- .attr("y", d.bucketOffset - (gutter / 2))
183
- .attr("width", w)
184
- .attr("height", ((d.bucketHeight + gutter) * (d.maxBucket + 1)) + gutter)
185
- .attr("fill", i % 2 ? context.oddSeriesBackground() : context.evenSeriesBackground())
186
- ;
187
- });
188
- } else {
189
- if (this._bucket_idx !== -1) {
190
- this._buckets = this.calcBuckets(this.data(), this._startDate_idx, this._endDate_idx, this._bucket_idx);
191
- } else {
192
- this._buckets = this.calcBuckets(this.data(), this._startDate_idx, this._endDate_idx);
193
- }
194
- }
195
-
196
- if (!this.preserveZoom() || !this._prevZoomState) {
197
- const interpedStart = this._interpolateX(this._minStart);
198
- this.zoomTo(
199
- [interpedStart, 0],
200
- 1
201
- );
202
- }
203
-
204
- const bucketHeight = this.bucketHeight();
205
-
206
- this.setRangeOptions();
207
-
208
- this._maxFontScale = (bucketHeight - (this.rangePadding() * 2));
209
- this.measureDataText();
210
-
211
- const itemSelection = this._renderElement.selectAll(".item")
212
- .data(this.data())
213
- ;
214
- const borderOffset1 = this.strokeWidth();
215
- const borderOffset2 = borderOffset1 * 2;
216
- itemSelection
217
- .join(
218
- enter => enter.append("g")
219
- .attr("class", "item")
220
- .on("click.selectionBag", function (d, i) {
221
- const _id = d.id === undefined ? i : d.id;
222
- if (context._selection.isSelected({ _id, element: d.element })) {
223
- context._selection.clear();
224
- } else {
225
- context._selection.click(
226
- {
227
- _id,
228
- element: () => d.element
229
- },
230
- d3Event
231
- );
232
- }
233
- context.selectionChanged();
234
- d3Event().stopPropagation();
235
- })
236
- .on("click", function (this: SVGElement, d) {
237
- const selected = d.element.classed("selected");
238
- if (d[context.columns().length]) {
239
- d.__lparam = d[context.columns().length];
240
- }
241
- context.click(d, "", selected);
242
- })
243
- .on("dblclick", function (this: SVGElement, d) {
244
- const selected = d.element.classed("selected");
245
- if (d[context.columns().length]) {
246
- d.__lparam = d[context.columns().length];
247
- }
248
- context.click(d, "", selected);
249
- })
250
- .on("mousein", function (d) {
251
- context.highlightItem(d3Select(this), d);
252
- const selected = d.element.classed("selected");
253
- context.mousein(d, "", selected);
254
- })
255
- .on("mouseover", function (d) {
256
- const d3evt = d3Event();
257
- context._tooltip._triggerElement = d.element;
258
- context._tooltip._cursorLoc = [
259
- d3evt.clientX,
260
- d3evt.clientY
261
- ];
262
- context._tooltip
263
- .data(d)
264
- .visible(true)
265
- .fitContent(true)
266
- .render()
267
- ;
268
- context.highlightItem(d3Select(this), d);
269
- const selected = d.element.classed("selected");
270
- context.mouseover(d, "", selected);
271
- })
272
- .on("mouseout", function (d) {
273
- context._tooltip
274
- .visible(false)
275
- .render()
276
- ;
277
- context.highlightItem(null, null);
278
- const selected = d.element.classed("selected");
279
- context.mouseout(d, "", selected);
280
- })
281
- .each(function (d, i) {
282
- d.that = this;
283
- d.element = d3Select(this);
284
- d.x = context._interpolateX(d[1]);
285
- const endX = context._interpolateX(d[2]);
286
- if (context._series_idx > -1) {
287
- const seriesKey = d[context._series_idx];
288
- const bucket = context._bucketsBySeries[seriesKey].bucketMap[context._origIdxMap[seriesKey][i]];
289
- d.y = context._bucketsBySeries[seriesKey].interpolateY(bucket) + context._bucketsBySeries[seriesKey].bucketOffset;
290
- } else {
291
- const _i = context._bucket_idx === -1 ? i : d[context._bucket_idx];
292
- d.y = context._buckets.interpolateY(context._buckets.bucketMap[_i]);
293
- }
294
- d.props = {
295
- ...d[3],
296
- text: d[0]
297
- };
298
- d.props.width = endX - d.x;
299
- d.props.height = bucketHeight;
300
- d.x += borderOffset1;
301
- d.y += borderOffset1;
302
- d.props.width -= borderOffset2;
303
- d.props.height -= borderOffset2;
304
- d.element.attr("transform", `translate(${d.x + (d.props.width / 2)} ${d.y + (d.props.height / 2)})`);
305
- }),
306
- update => update,
307
- exit => exit
308
- .each(function (d) {
309
- delete d.element;
310
- })
311
- .remove()
312
- )
313
- .attr("opacity", d => d.props && d.props.hidden ? 0 : 1)
314
- .each(function (this: SVGGElement, d, i) {
315
- d.that = this;
316
- if (context._series_idx > -1) {
317
- const seriesKey = d[context._series_idx];
318
- d.x = context.renderRangeElement(d, i, false, context._rangeOptions, seriesKey);
319
- } else {
320
- d.x = context.renderRangeElement(d, i, false, context._rangeOptions);
321
- }
322
- })
323
- .on("dblclick.zoom", d => {
324
- const x1 = this._interpolateX(d[1]);
325
- const x2 = this._interpolateX(d[2]);
326
- const xRange = x2 - x1;
327
- const xScale = w / xRange;
328
- this.zoomTo(
329
- [
330
- -x1 * xScale,
331
- 0
332
- ],
333
- xScale
334
- );
335
- })
336
- ;
337
- element.on("dblclick.zoom", null);
338
-
339
- // restore zoom state after all rendering is set up
340
- if (this.preserveZoom() && this._prevZoomState && this._interpolateX) {
341
- const width = this.width();
342
- if (width > 0) {
343
- const visibleStart = this._minStart;
344
- const visibleEnd = this._maxEnd;
345
- const clampedStart = Math.max(visibleStart, Math.min(visibleEnd, this._prevZoomState.visibleStart));
346
- let clampedEnd = Math.max(visibleStart, Math.min(visibleEnd, this._prevZoomState.visibleEnd));
347
- if (clampedEnd <= clampedStart) {
348
- const visibleWidth = visibleEnd - visibleStart;
349
- const epsilon = visibleWidth * 1e-6 || 1e-6;
350
- clampedEnd = Math.min(visibleEnd, clampedStart + epsilon);
351
- }
352
- const startPixel = this._interpolateX(clampedStart);
353
- const endPixel = this._interpolateX(clampedEnd);
354
- const span = endPixel - startPixel;
355
- if (isFinite(span) && Math.abs(span) > 1e-9) {
356
- const rawScale = width / span;
357
- const minScale = 0.05; // must match zoomExtent minimum set at start of update()
358
- const maxScale = this.maxZoom();
359
- const targetScale = Math.max(minScale, Math.min(maxScale, rawScale));
360
-
361
- if (targetScale > 0 && isFinite(targetScale)) {
362
- const centerPixel = (startPixel + endPixel) / 2;
363
- const halfViewport = width / (2 * targetScale);
364
- const x0 = this._interpolateX(visibleStart);
365
- const x1 = this._interpolateX(visibleEnd);
366
-
367
- let clampedCenter = centerPixel;
368
- if (clampedCenter - halfViewport < x0) {
369
- clampedCenter = x0 + halfViewport;
370
- }
371
- if (clampedCenter + halfViewport > x1) {
372
- clampedCenter = x1 - halfViewport;
373
- }
374
-
375
- const translateX = (width / 2) - (targetScale * clampedCenter);
376
- if (isFinite(translateX)) {
377
- this.zoomTo([translateX, 0], targetScale);
378
- }
379
- }
380
- }
381
- }
382
- }
383
- }
384
- exit(domNode, element) {
385
- this._tooltip.target(null);
386
- super.exit(domNode, element);
387
- }
388
- renderRangeElement(d, i, transformEach = false, options: any = {}, seriesKey?: string) {
389
- const borderOffset1 = options.strokeWidth;
390
- const borderOffset2 = borderOffset1 * 2;
391
- const padding = options.rangePadding;
392
- let endX;
393
- const x = isNaN(this._transform.x) ? 0 : this._transform.x;
394
- const k = isNaN(this._transform.k) ? 1 : this._transform.k;
395
- let b;
396
- const bucketHeight = this.bucketHeight();
397
- d.that.setAttribute("data-series", seriesKey);
398
-
399
- if (this._color_idx > -1) {
400
- d.that.setAttribute("data-color", d[this._color_idx]);
401
- }
402
-
403
- if (seriesKey !== undefined) {
404
- b = this._bucketsBySeries[seriesKey].bucketMap[this._origIdxMap[seriesKey][i]];
405
- d.that.setAttribute("data-b", b);
406
- d.that.setAttribute("data-bucketOffset", this._bucketsBySeries[seriesKey].bucketOffset);
407
- d.y = this._bucketsBySeries[seriesKey].interpolateY(b) + this._bucketsBySeries[seriesKey].bucketOffset;
408
- d.that.setAttribute("data-dy", d.y);
409
- } else {
410
- b = this._buckets.bucketMap[i];
411
- d.y = this._buckets.interpolateY(b);
412
- }
413
- if (this._color_idx > -1) {
414
- options.fill = d[this._color_idx];
415
- }
416
- if (!transformEach) {
417
- d.x = this._interpolateX(d[1]);
418
- endX = this._interpolateX(d[2]);
419
- d.props = {
420
- ...d[3],
421
- text: d[0]
422
- };
423
- d.props.width = (endX - d.x) / k;
424
- } else {
425
- d.x = this._interpolateX(d[1]) * k;
426
- endX = this._interpolateX(d[2]) * k;
427
- d.props = {
428
- ...d[3],
429
- text: d[0]
430
- };
431
- d.props.width = (endX - d.x) / k;
432
- d.x += x;
433
- d.props.width *= k;
434
- }
435
- d.props.height = bucketHeight;
436
- if (seriesKey === undefined && this._buckets.bucketScale < 1) {
437
- d.props.height = this._buckets.bucketScale * bucketHeight;
438
- }
439
- if (d.element === undefined && d.that) {
440
- d.element = d3Select(d.that);
441
- }
442
- d.element.attr("transform", `translate(${d.x + (d.props.width / 2)} ${d.y + (d.props.height / 2)})`);
443
-
444
- d.x += borderOffset1;
445
- d.y += borderOffset1;
446
- d.props.width -= borderOffset2;
447
- d.props.height -= borderOffset2;
448
- d.props.width = d.props.width < 1 ? 1 : d.props.width;
449
- d.props.height = d.props.height < 1 ? 1 : d.props.height;
450
-
451
- let text = this.truncateText(d.props.text, d.props.width - padding, this._maxFontScale);
452
-
453
- if (text !== d.props.text) {
454
- text = this.truncateText(d.props.text, d.props.width - padding);
455
- } else {
456
- d.props.fontSize = this._maxFontScale * options.fontSize;
457
- }
458
- if (seriesKey === undefined && this._buckets.bucketScale < 1) {
459
- d.props.fontSize = Math.min(this._maxFontScale, this._buckets.bucketScale) * options.fontSize;
460
- }
461
- if (!this._maxY || this._maxY < d.y + d.props.height) {
462
- this._maxY = d.y + d.props.height;
463
- }
464
- if (!this._maxX || this._maxX < d.x + d.props.width) {
465
- this._maxX = d.x + d.props.width;
466
- }
467
- render(
468
- this._rangeRenderer,
469
- {
470
- ...options,
471
- ...d.props,
472
- text,
473
- },
474
- d.that
475
- );
476
- }
477
-
478
- setRangeOptions() {
479
- this._rangeOptions = {
480
- rangePadding: this.rangePadding(),
481
- fontFamily: this.fontFamily(),
482
- fontSize: this.fontSize(),
483
- strokeWidth: this.strokeWidth(),
484
- fill: this.fill(),
485
- stroke: this.stroke(),
486
- textFill: this.rangeFontColor(),
487
- cornerRadius: this.cornerRadius(),
488
- };
489
- }
490
-
491
- public _transform = { k: 1, x: 0, y: 0 };
492
- zoomed(transform) {
493
- this._transform = transform;
494
- // store current visible range for zoom preservation
495
- if (this._interpolateX && typeof this._interpolateX.invert === "function") {
496
- const width = this.width();
497
- if (width > 0 && isFinite(transform.k) && transform.k !== 0) {
498
- const startPixel = (0 - transform.x) / transform.k;
499
- const endPixel = (width - transform.x) / transform.k;
500
- let visibleStart = this._interpolateX.invert(startPixel);
501
- let visibleEnd = this._interpolateX.invert(endPixel);
502
- if (isFinite(visibleStart) && isFinite(visibleEnd)) {
503
- if (visibleStart > visibleEnd) {
504
- const tmp = visibleStart;
505
- visibleStart = visibleEnd;
506
- visibleEnd = tmp;
507
- }
508
- this._prevZoomState = {
509
- visibleStart,
510
- visibleEnd
511
- };
512
- }
513
- }
514
- }
515
- switch (this.renderMode()) {
516
- case "scale-all":
517
- this._zoomScale = transform.k;
518
- this._zoomTranslate = [transform.x, 0];
519
- this._zoomG.attr("transform", `translate(${transform.x} ${0})scale(${transform.k} 1)`);
520
- break;
521
- default:
522
- const options = this._rangeOptions;
523
- this.data().forEach((d, i) => {
524
- if (this._color_idx > -1) {
525
- options.fill = d[this._color_idx];
526
- }
527
- if (this._series_idx > -1) {
528
- const seriesKey = d[this._series_idx];
529
- this.renderRangeElement(d, i, true, options, seriesKey);
530
- } else {
531
- this.renderRangeElement(d, i, true, options);
532
- }
533
- });
534
- }
535
-
536
- this.zoomedHook(transform);
537
- }
538
-
539
- zoomedHook(transform) {
540
-
541
- }
542
-
543
- private calcBuckets(data, startKey: string | number, endKey: string | number, bucketKey?: string | number) {
544
- const bucketMap = {};
545
- const bucketKeyMap = {};
546
- const tol = this.overlapTolerence();
547
- const buckets = [{ end: -Infinity }];
548
- let maxBucket = 0;
549
- if (bucketKey !== undefined) {
550
- data.forEach((d, i) => {
551
- bucketMap[i] = d[bucketKey];
552
- bucketKeyMap[d[bucketKey]] = true;
553
- });
554
- maxBucket = Object.keys(bucketKeyMap).length;
555
- } else {
556
- data.forEach((d, i) => {
557
- for (let i2 = 0; i2 < buckets.length; ++i2) {
558
- if (i === 0 || buckets[i2][endKey] + tol <= d[startKey]) {
559
- bucketMap[i] = i2;
560
- if (maxBucket < i2) maxBucket = i2;
561
- buckets[i2][endKey] = d[endKey];
562
- break;
563
- }
564
- }
565
- if (bucketMap[i] === undefined) {
566
- bucketMap[i] = buckets.length;
567
- const b = {};
568
- b[endKey] = d[endKey];
569
- buckets.push(b as any);
570
- }
571
-
572
- if (maxBucket < bucketMap[i]) maxBucket = bucketMap[i];
573
- });
574
- }
575
- const height = (maxBucket + 1) * (this.bucketHeight() + this.gutter());
576
- return {
577
- bucketMap,
578
- maxBucket,
579
- bucketScale: this.height() / height,
580
- interpolateY: d3ScaleLinear()
581
- .domain([0, maxBucket + 1])
582
- .range([0, Math.min(this.height(), height)])
583
- };
584
- }
585
-
586
- data(): IGanttData[];
587
- data(_: IGanttData[]): this;
588
- data(_?: IGanttData[]): this | IGanttData[] {
589
- const retVal = super.data.apply(this, arguments);
590
- if (arguments.length > 0) {
591
- this._minStart = Math.min(...this.data().map(n => n[1])) ?? 0;
592
- this._maxEnd = Math.max(...this.data().map(n => n[2])) ?? 1;
593
- this.measureDataText(true);
594
- }
595
- return retVal;
596
- }
597
-
598
- protected _textWidths;
599
- protected _maxFontScale;
600
- protected _characterWidths;
601
- protected _prevFontFamily;
602
- protected _prevFontSize;
603
- measureDataText(forceMeasure = false) {
604
- const textWidths = {};
605
- const characterWidths = {};
606
- const fontFamily = this.fontFamily();
607
- const fontSize = this.fontSize();
608
- const bucketHeight = this.bucketHeight();
609
-
610
- if (bucketHeight) {
611
- this._maxFontScale = (bucketHeight - (this.rangePadding() * 2)) / fontSize;
612
- }
613
-
614
- if (forceMeasure || this._prevFontFamily !== fontFamily || this._prevFontSize !== fontSize) {
615
- characterWidths["."] = Utility.textSize(".", fontFamily, fontSize).width;
616
- this.data().forEach(d => {
617
- if (!textWidths[d[0]]) {
618
- textWidths[d[0]] = Utility.textSize(d[0], fontFamily, fontSize).width;
619
- }
620
- d[0].split("").forEach(char => {
621
- if (!characterWidths[char]) {
622
- characterWidths[char] = Utility.textSize(char, fontFamily, fontSize).width;
623
- }
624
- });
625
- });
626
- this._textWidths = textWidths;
627
- this._characterWidths = characterWidths;
628
- }
629
- this._prevFontFamily = fontFamily;
630
- this._prevFontSize = fontSize;
631
- }
632
-
633
- truncateText(text, width, scale = 1) {
634
- const textFits = this._textWidths[text] * scale < width;
635
- if (textFits) {
636
- return text;
637
- }
638
- let ret = "";
639
- let sum = 0;
640
- const _width = width - (this._characterWidths["."] * 3);
641
- for (const char of text) {
642
- sum += this._characterWidths[char];
643
- if (sum < _width) {
644
- ret += char;
645
- } else {
646
- break;
647
- }
648
- }
649
- return _width < 0 ? "" : ret + "...";
650
- }
651
-
652
- resize(_size?: { width: number, height: number }) {
653
- let retVal;
654
- if (this.fitWidthToContent() || this.fitHeightToContent()) {
655
- retVal = super.resize.call(this, {
656
- width: _size.width,
657
- height: this._maxY
658
- });
659
- } else {
660
- retVal = super.resize.apply(this, arguments);
661
- }
662
- return retVal;
663
- }
664
-
665
- selectionChanged() {
666
-
667
- }
668
-
669
- highlightItem(_element, d) {
670
-
671
- }
672
-
673
- click(row, _col, sel) {
674
-
675
- }
676
-
677
- dblclick(row, _col, sel) {
678
-
679
- }
680
-
681
- mousein(row, _col, sel) {
682
- }
683
-
684
- mouseover(row, _col, sel) {
685
- }
686
-
687
- mouseout(row, _col, sel) {
688
- }
689
- }
690
- ReactGantt.prototype._class += " timeline_ReactGantt";
691
-
692
- export interface ReactGantt {
693
- titleColumn(): string;
694
- titleColumn(_: string): this;
695
- startDateColumn(): string;
696
- startDateColumn(_: string): this;
697
- endDateColumn(): string;
698
- endDateColumn(_: string): this;
699
- iconColumn(): string;
700
- iconColumn(_: string): this;
701
- colorColumn(): string;
702
- colorColumn(_: string): this;
703
- seriesColumn(): string;
704
- seriesColumn(_: string): this;
705
- bucketColumn(): string;
706
- bucketColumn(_: string): this;
707
- overlapTolerence(): number;
708
- overlapTolerence(_: number): this;
709
- smallestRangeWidth(): number;
710
- smallestRangeWidth(_: number): this;
711
- bucketHeight(): number;
712
- bucketHeight(_: number): this;
713
- gutter(): number;
714
- gutter(_: number): this;
715
- showToolbar_default(_: boolean): this;
716
- fontSize(): number;
717
- fontSize(_: number): this;
718
- fontFamily(): string;
719
- fontFamily(_: string): this;
720
- strokeWidth(): number;
721
- strokeWidth(_: number): this;
722
- stroke(): string;
723
- stroke(_: string): this;
724
- cornerRadius(): number;
725
- cornerRadius(_: number): this;
726
- fill(): string;
727
- fill(_: string): this;
728
- rangeFontColor(): string;
729
- rangeFontColor(_: string): this;
730
- rangePadding(): number;
731
- rangePadding(_: number): this;
732
- renderMode(): "default" | "scale-all";
733
- renderMode(_: "default" | "scale-all"): this;
734
- maxZoom(): number;
735
- maxZoom(_: number): this;
736
- fitWidthToContent(): boolean;
737
- fitWidthToContent(_: boolean): this;
738
- fitHeightToContent(): boolean;
739
- fitHeightToContent(_: boolean): this;
740
- preserveZoom(): boolean;
741
- preserveZoom(_: boolean): this;
742
- evenSeriesBackground(): string;
743
- evenSeriesBackground(_: string): this;
744
- oddSeriesBackground(): string;
745
- oddSeriesBackground(_: string): this;
746
- }
747
-
748
- ReactGantt.prototype.publish("fitWidthToContent", false, "boolean", "If true, resize will simply reapply the bounding box width");
749
- ReactGantt.prototype.publish("fitHeightToContent", false, "boolean", "If true, resize will simply reapply the bounding box height");
750
- ReactGantt.prototype.publish("preserveZoom", false, "boolean", "If true, maintain zoom level when data is updated");
751
- ReactGantt.prototype.publish("titleColumn", null, "string", "Column name to for the title");
752
- ReactGantt.prototype.publish("startDateColumn", null, "string", "Column name to for the start date");
753
- ReactGantt.prototype.publish("endDateColumn", null, "string", "Column name to for the end date");
754
- ReactGantt.prototype.publish("iconColumn", null, "string", "Column name to for the icon");
755
- ReactGantt.prototype.publish("colorColumn", null, "string", "Column name to for the color");
756
- ReactGantt.prototype.publish("seriesColumn", null, "string", "Column name to for the series identifier");
757
- ReactGantt.prototype.publish("bucketColumn", null, "string", "Column name to for the bucket identifier");
758
- ReactGantt.prototype.publish("renderMode", "default", "set", "Render modes vary in features and performance", ["default", "scale-all"]);
759
- ReactGantt.prototype.publish("rangePadding", 3, "number", "Padding within each range rectangle (pixels)");
760
- ReactGantt.prototype.publish("fill", "#1f77b4", "string", "Background color of range rectangle");
761
- ReactGantt.prototype.publish("stroke", null, "string", "Color of range rectangle border");
762
- ReactGantt.prototype.publish("strokeWidth", null, "number", "Width of range rectangle border (pixels)");
763
- ReactGantt.prototype.publish("cornerRadius", 3, "number", "Space between range buckets (pixels)");
764
- ReactGantt.prototype.publish("fontFamily", null, "string", "Font family within range rectangle", null, { optional: true });
765
- ReactGantt.prototype.publish("fontSize", 10, "number", "Size of font within range rectangle (pixels)");
766
- ReactGantt.prototype.publish("rangeFontColor", "#ecf0f1", "html-color", "rangeFontColor");
767
- ReactGantt.prototype.publish("overlapTolerence", 2, "number", "overlapTolerence");
768
- ReactGantt.prototype.publish("smallestRangeWidth", 10, "number", "Width of the shortest range (pixels)");
769
- ReactGantt.prototype.publish("bucketHeight", 100, "number", "Max height of range element (pixels)");
770
- ReactGantt.prototype.publish("gutter", 2, "number", "Space between range buckets (pixels)");
771
- ReactGantt.prototype.publish("maxZoom", 16, "number", "Maximum zoom");
772
- ReactGantt.prototype.publish("evenSeriesBackground", "#FFFFFF", "html-color", "Background color of even series rows");
773
- ReactGantt.prototype.publish("oddSeriesBackground", "#DDDDDD", "html-color", "Background color of odd series rows");
1
+ import { d3Event, select as d3Select, SVGZoomWidget, Utility } from "@hpcc-js/common";
2
+ import { HTMLTooltip } from "@hpcc-js/html";
3
+ import { scaleLinear as d3ScaleLinear } from "d3-scale";
4
+ import { React, render, LabelledRect } from "@hpcc-js/react";
5
+
6
+ export type IGanttData = [string, number, number, any?];
7
+
8
+ export interface IRangeOptions {
9
+ rangePadding: number;
10
+ fontFamily: string;
11
+ fontSize: number;
12
+ strokeWidth?: number;
13
+ fill: string;
14
+ stroke: string;
15
+ textFill: string;
16
+ cornerRadius: number;
17
+ }
18
+
19
+ export class ReactGantt extends SVGZoomWidget {
20
+
21
+ protected _selection = new Utility.Selection(this);
22
+
23
+ protected _buckets;
24
+ protected _interpolateX;
25
+ protected _interpolateY;
26
+
27
+ protected _bucketsBySeries;
28
+ protected _dataBySeries;
29
+ protected _origIdxMap;
30
+ private _seriesBackgrounds;
31
+
32
+ protected _maxFontSize;
33
+
34
+ public _tooltip;
35
+
36
+ public _minStart: number;
37
+ public _maxEnd: number;
38
+
39
+ protected _prevZoomState: { visibleStart: number; visibleEnd: number } | null = null;
40
+
41
+ protected _title_idx = 0;
42
+ protected _startDate_idx = 1;
43
+ protected _endDate_idx = 2;
44
+ protected _icon_idx = -1;
45
+ protected _color_idx = -1;
46
+ protected _series_idx = -1;
47
+ protected _bucket_idx = -1;
48
+ protected _yoffset_idx = -1;
49
+
50
+ protected _maxX: number;
51
+ protected _maxY: number;
52
+
53
+ private _rangeOptions: IRangeOptions = {
54
+ rangePadding: 2,
55
+ fontFamily: "Verdana",
56
+ fontSize: 12,
57
+ fill: "white",
58
+ stroke: "black",
59
+ textFill: "black",
60
+ cornerRadius: 3,
61
+ strokeWidth: 0
62
+ };
63
+
64
+ constructor(drawStartPosition: "origin" | "center" = "origin") {
65
+ super();
66
+ this._drawStartPos = drawStartPosition;
67
+
68
+ this.showToolbar_default(false);
69
+
70
+ this._tooltip = new HTMLTooltip();
71
+ this._tooltip
72
+ .tooltipHTML(d => {
73
+ return `<div style="text-align:center;">${d[0]}<br/><br/>${d[1]} -&gt; ${d[2]}</div>`;
74
+ });
75
+ this._tooltip
76
+ .followCursor(true)
77
+ ;
78
+ }
79
+
80
+ selection(_: any[]): this;
81
+ selection(): any[];
82
+ selection(_?: any[]): any[] | this {
83
+ if (!arguments.length) return this._selection.get();
84
+ this._selection.set(_);
85
+ return this;
86
+ }
87
+
88
+ private _rangeRenderer: React.FunctionComponent = LabelledRect;
89
+ rangeRenderer(): React.FunctionComponent;
90
+ rangeRenderer(_: React.FunctionComponent): this;
91
+ rangeRenderer(_?: React.FunctionComponent): this | React.FunctionComponent {
92
+ if (!arguments.length) return this._rangeRenderer;
93
+ this._rangeRenderer = _;
94
+ return this._rangeRenderer;
95
+ }
96
+
97
+ enter(domNode, element) {
98
+ super.enter(domNode, element);
99
+
100
+ const context = this;
101
+ element
102
+ .on("click", function (this: SVGElement, d) {
103
+ context._selection.clear();
104
+ });
105
+
106
+ this._tooltip.target(domNode);
107
+ }
108
+ update(domNode, element) {
109
+ super.update(domNode, element);
110
+
111
+ this.zoomExtent([0.05, this.maxZoom()]);
112
+
113
+ this._title_idx = this.titleColumn() !== null ? this.columns().indexOf(this.titleColumn()) : this._title_idx;
114
+ this._startDate_idx = this.startDateColumn() !== null ? this.columns().indexOf(this.startDateColumn()) : this._startDate_idx;
115
+ this._endDate_idx = this.endDateColumn() !== null ? this.columns().indexOf(this.endDateColumn()) : this._endDate_idx;
116
+ this._icon_idx = this.iconColumn() !== null ? this.columns().indexOf(this.iconColumn()) : this._icon_idx;
117
+ this._color_idx = this.colorColumn() !== null ? this.columns().indexOf(this.colorColumn()) : this._color_idx;
118
+ this._series_idx = this.seriesColumn() !== null ? this.columns().indexOf(this.seriesColumn()) : this._series_idx;
119
+ this._bucket_idx = this.bucketColumn() !== null ? this.columns().indexOf(this.bucketColumn()) : -1;
120
+
121
+ const context = this;
122
+ const w = this.width();
123
+
124
+ const x0 = 0;
125
+ const x1 = w;
126
+
127
+ this._interpolateX = d3ScaleLinear()
128
+ .domain([this._minStart, this._maxEnd])
129
+ .range([x0, x1])
130
+ ;
131
+
132
+ this.data().sort((a, b) => a[1] - b[1]);
133
+
134
+ if (this._series_idx > -1) {
135
+ this._origIdxMap = {};
136
+ this._dataBySeries = {};
137
+ this._bucketsBySeries = {};
138
+ this.data().forEach((dataRow, origIdx) => {
139
+ const seriesKey = dataRow[this._series_idx];
140
+ if (!this._dataBySeries[seriesKey]) {
141
+ this._origIdxMap[seriesKey] = {};
142
+ this._dataBySeries[seriesKey] = [];
143
+ }
144
+ this._dataBySeries[seriesKey].push({
145
+ dataRow,
146
+ origIdx
147
+ });
148
+ });
149
+ const gutter = this.gutter();
150
+ let bucketOffset = 0;
151
+ const seriesKeys = Object.keys(this._dataBySeries);
152
+ seriesKeys.forEach(seriesKey => {
153
+ this._dataBySeries[seriesKey].sort((a, b) => a.dataRow[1] - b.dataRow[1]);
154
+ this._bucketsBySeries[seriesKey] = this.calcBuckets(this._dataBySeries[seriesKey].map(n => n.dataRow), 1, 2);
155
+ this._bucketsBySeries[seriesKey].bucketHeight = this.bucketHeight();
156
+ this._bucketsBySeries[seriesKey].bucketOffset = bucketOffset;
157
+ bucketOffset += (this._bucketsBySeries[seriesKey].bucketHeight + this.strokeWidth() + this.gutter()) * (this._bucketsBySeries[seriesKey].maxBucket + 1);
158
+ this._dataBySeries[seriesKey].forEach((n, i) => {
159
+ this._origIdxMap[seriesKey][n.origIdx] = i;
160
+ });
161
+ });
162
+ this._seriesBackgrounds = this._renderElement.selectAll(".series-background")
163
+ .data(seriesKeys.map(key => {
164
+ return this._bucketsBySeries[key];
165
+ }))
166
+ ;
167
+ this._seriesBackgrounds
168
+ .join(
169
+ enter => enter.append("rect")
170
+ .attr("class", "series-background"),
171
+ update => update,
172
+ exit => exit
173
+ .each(function (d) {
174
+ delete d.element;
175
+ })
176
+ .remove()
177
+ )
178
+ .attr("opacity", d => d.props && d.props.hidden ? 0 : 1)
179
+ .each(function (this: SVGGElement, d, i) {
180
+ d3Select(this)
181
+ .attr("x", 0)
182
+ .attr("y", d.bucketOffset - (gutter / 2))
183
+ .attr("width", w)
184
+ .attr("height", ((d.bucketHeight + gutter) * (d.maxBucket + 1)) + gutter)
185
+ .attr("fill", i % 2 ? context.oddSeriesBackground() : context.evenSeriesBackground())
186
+ ;
187
+ });
188
+ } else {
189
+ if (this._bucket_idx !== -1) {
190
+ this._buckets = this.calcBuckets(this.data(), this._startDate_idx, this._endDate_idx, this._bucket_idx);
191
+ } else {
192
+ this._buckets = this.calcBuckets(this.data(), this._startDate_idx, this._endDate_idx);
193
+ }
194
+ }
195
+
196
+ if (!this.preserveZoom() || !this._prevZoomState) {
197
+ const interpedStart = this._interpolateX(this._minStart);
198
+ this.zoomTo(
199
+ [interpedStart, 0],
200
+ 1
201
+ );
202
+ }
203
+
204
+ const bucketHeight = this.bucketHeight();
205
+
206
+ this.setRangeOptions();
207
+
208
+ this._maxFontScale = (bucketHeight - (this.rangePadding() * 2));
209
+ this.measureDataText();
210
+
211
+ const itemSelection = this._renderElement.selectAll(".item")
212
+ .data(this.data())
213
+ ;
214
+ const borderOffset1 = this.strokeWidth();
215
+ const borderOffset2 = borderOffset1 * 2;
216
+ itemSelection
217
+ .join(
218
+ enter => enter.append("g")
219
+ .attr("class", "item")
220
+ .on("click.selectionBag", function (d, i) {
221
+ const _id = d.id === undefined ? i : d.id;
222
+ if (context._selection.isSelected({ _id, element: d.element })) {
223
+ context._selection.clear();
224
+ } else {
225
+ context._selection.click(
226
+ {
227
+ _id,
228
+ element: () => d.element
229
+ },
230
+ d3Event
231
+ );
232
+ }
233
+ context.selectionChanged();
234
+ d3Event().stopPropagation();
235
+ })
236
+ .on("click", function (this: SVGElement, d) {
237
+ const selected = d.element.classed("selected");
238
+ if (d[context.columns().length]) {
239
+ d.__lparam = d[context.columns().length];
240
+ }
241
+ context.click(d, "", selected);
242
+ })
243
+ .on("dblclick", function (this: SVGElement, d) {
244
+ const selected = d.element.classed("selected");
245
+ if (d[context.columns().length]) {
246
+ d.__lparam = d[context.columns().length];
247
+ }
248
+ context.click(d, "", selected);
249
+ })
250
+ .on("mousein", function (d) {
251
+ context.highlightItem(d3Select(this), d);
252
+ const selected = d.element.classed("selected");
253
+ context.mousein(d, "", selected);
254
+ })
255
+ .on("mouseover", function (d) {
256
+ const d3evt = d3Event();
257
+ context._tooltip._triggerElement = d.element;
258
+ context._tooltip._cursorLoc = [
259
+ d3evt.clientX,
260
+ d3evt.clientY
261
+ ];
262
+ context._tooltip
263
+ .data(d)
264
+ .visible(true)
265
+ .fitContent(true)
266
+ .render()
267
+ ;
268
+ context.highlightItem(d3Select(this), d);
269
+ const selected = d.element.classed("selected");
270
+ context.mouseover(d, "", selected);
271
+ })
272
+ .on("mouseout", function (d) {
273
+ context._tooltip
274
+ .visible(false)
275
+ .render()
276
+ ;
277
+ context.highlightItem(null, null);
278
+ const selected = d.element.classed("selected");
279
+ context.mouseout(d, "", selected);
280
+ })
281
+ .each(function (d, i) {
282
+ d.that = this;
283
+ d.element = d3Select(this);
284
+ d.x = context._interpolateX(d[1]);
285
+ const endX = context._interpolateX(d[2]);
286
+ if (context._series_idx > -1) {
287
+ const seriesKey = d[context._series_idx];
288
+ const bucket = context._bucketsBySeries[seriesKey].bucketMap[context._origIdxMap[seriesKey][i]];
289
+ d.y = context._bucketsBySeries[seriesKey].interpolateY(bucket) + context._bucketsBySeries[seriesKey].bucketOffset;
290
+ } else {
291
+ const _i = context._bucket_idx === -1 ? i : d[context._bucket_idx];
292
+ d.y = context._buckets.interpolateY(context._buckets.bucketMap[_i]);
293
+ }
294
+ d.props = {
295
+ ...d[3],
296
+ text: d[0]
297
+ };
298
+ d.props.width = endX - d.x;
299
+ d.props.height = bucketHeight;
300
+ d.x += borderOffset1;
301
+ d.y += borderOffset1;
302
+ d.props.width -= borderOffset2;
303
+ d.props.height -= borderOffset2;
304
+ d.element.attr("transform", `translate(${d.x + (d.props.width / 2)} ${d.y + (d.props.height / 2)})`);
305
+ }),
306
+ update => update,
307
+ exit => exit
308
+ .each(function (d) {
309
+ delete d.element;
310
+ })
311
+ .remove()
312
+ )
313
+ .attr("opacity", d => d.props && d.props.hidden ? 0 : 1)
314
+ .each(function (this: SVGGElement, d, i) {
315
+ d.that = this;
316
+ if (context._series_idx > -1) {
317
+ const seriesKey = d[context._series_idx];
318
+ d.x = context.renderRangeElement(d, i, false, context._rangeOptions, seriesKey);
319
+ } else {
320
+ d.x = context.renderRangeElement(d, i, false, context._rangeOptions);
321
+ }
322
+ })
323
+ .on("dblclick.zoom", d => {
324
+ const x1 = this._interpolateX(d[1]);
325
+ const x2 = this._interpolateX(d[2]);
326
+ const xRange = x2 - x1;
327
+ const xScale = w / xRange;
328
+ this.zoomTo(
329
+ [
330
+ -x1 * xScale,
331
+ 0
332
+ ],
333
+ xScale
334
+ );
335
+ })
336
+ ;
337
+ element.on("dblclick.zoom", null);
338
+
339
+ // restore zoom state after all rendering is set up
340
+ if (this.preserveZoom() && this._prevZoomState && this._interpolateX) {
341
+ const width = this.width();
342
+ if (width > 0) {
343
+ const visibleStart = this._minStart;
344
+ const visibleEnd = this._maxEnd;
345
+ const clampedStart = Math.max(visibleStart, Math.min(visibleEnd, this._prevZoomState.visibleStart));
346
+ let clampedEnd = Math.max(visibleStart, Math.min(visibleEnd, this._prevZoomState.visibleEnd));
347
+ if (clampedEnd <= clampedStart) {
348
+ const visibleWidth = visibleEnd - visibleStart;
349
+ const epsilon = visibleWidth * 1e-6 || 1e-6;
350
+ clampedEnd = Math.min(visibleEnd, clampedStart + epsilon);
351
+ }
352
+ const startPixel = this._interpolateX(clampedStart);
353
+ const endPixel = this._interpolateX(clampedEnd);
354
+ const span = endPixel - startPixel;
355
+ if (isFinite(span) && Math.abs(span) > 1e-9) {
356
+ const rawScale = width / span;
357
+ const minScale = 0.05; // must match zoomExtent minimum set at start of update()
358
+ const maxScale = this.maxZoom();
359
+ const targetScale = Math.max(minScale, Math.min(maxScale, rawScale));
360
+
361
+ if (targetScale > 0 && isFinite(targetScale)) {
362
+ const centerPixel = (startPixel + endPixel) / 2;
363
+ const halfViewport = width / (2 * targetScale);
364
+ const x0 = this._interpolateX(visibleStart);
365
+ const x1 = this._interpolateX(visibleEnd);
366
+
367
+ let clampedCenter = centerPixel;
368
+ if (clampedCenter - halfViewport < x0) {
369
+ clampedCenter = x0 + halfViewport;
370
+ }
371
+ if (clampedCenter + halfViewport > x1) {
372
+ clampedCenter = x1 - halfViewport;
373
+ }
374
+
375
+ const translateX = (width / 2) - (targetScale * clampedCenter);
376
+ if (isFinite(translateX)) {
377
+ this.zoomTo([translateX, 0], targetScale);
378
+ }
379
+ }
380
+ }
381
+ }
382
+ }
383
+ }
384
+ exit(domNode, element) {
385
+ this._tooltip.target(null);
386
+ super.exit(domNode, element);
387
+ }
388
+ renderRangeElement(d, i, transformEach = false, options: any = {}, seriesKey?: string) {
389
+ const borderOffset1 = options.strokeWidth;
390
+ const borderOffset2 = borderOffset1 * 2;
391
+ const padding = options.rangePadding;
392
+ let endX;
393
+ const x = isNaN(this._transform.x) ? 0 : this._transform.x;
394
+ const k = isNaN(this._transform.k) ? 1 : this._transform.k;
395
+ let b;
396
+ const bucketHeight = this.bucketHeight();
397
+ d.that.setAttribute("data-series", seriesKey);
398
+
399
+ if (this._color_idx > -1) {
400
+ d.that.setAttribute("data-color", d[this._color_idx]);
401
+ }
402
+
403
+ if (seriesKey !== undefined) {
404
+ b = this._bucketsBySeries[seriesKey].bucketMap[this._origIdxMap[seriesKey][i]];
405
+ d.that.setAttribute("data-b", b);
406
+ d.that.setAttribute("data-bucketOffset", this._bucketsBySeries[seriesKey].bucketOffset);
407
+ d.y = this._bucketsBySeries[seriesKey].interpolateY(b) + this._bucketsBySeries[seriesKey].bucketOffset;
408
+ d.that.setAttribute("data-dy", d.y);
409
+ } else {
410
+ b = this._buckets.bucketMap[i];
411
+ d.y = this._buckets.interpolateY(b);
412
+ }
413
+ if (this._color_idx > -1) {
414
+ options.fill = d[this._color_idx];
415
+ }
416
+ if (!transformEach) {
417
+ d.x = this._interpolateX(d[1]);
418
+ endX = this._interpolateX(d[2]);
419
+ d.props = {
420
+ ...d[3],
421
+ text: d[0]
422
+ };
423
+ d.props.width = (endX - d.x) / k;
424
+ } else {
425
+ d.x = this._interpolateX(d[1]) * k;
426
+ endX = this._interpolateX(d[2]) * k;
427
+ d.props = {
428
+ ...d[3],
429
+ text: d[0]
430
+ };
431
+ d.props.width = (endX - d.x) / k;
432
+ d.x += x;
433
+ d.props.width *= k;
434
+ }
435
+ d.props.height = bucketHeight;
436
+ if (seriesKey === undefined && this._buckets.bucketScale < 1) {
437
+ d.props.height = this._buckets.bucketScale * bucketHeight;
438
+ }
439
+ if (d.element === undefined && d.that) {
440
+ d.element = d3Select(d.that);
441
+ }
442
+ d.element.attr("transform", `translate(${d.x + (d.props.width / 2)} ${d.y + (d.props.height / 2)})`);
443
+
444
+ d.x += borderOffset1;
445
+ d.y += borderOffset1;
446
+ d.props.width -= borderOffset2;
447
+ d.props.height -= borderOffset2;
448
+ d.props.width = d.props.width < 1 ? 1 : d.props.width;
449
+ d.props.height = d.props.height < 1 ? 1 : d.props.height;
450
+
451
+ let text = this.truncateText(d.props.text, d.props.width - padding, this._maxFontScale);
452
+
453
+ if (text !== d.props.text) {
454
+ text = this.truncateText(d.props.text, d.props.width - padding);
455
+ } else {
456
+ d.props.fontSize = this._maxFontScale * options.fontSize;
457
+ }
458
+ if (seriesKey === undefined && this._buckets.bucketScale < 1) {
459
+ d.props.fontSize = Math.min(this._maxFontScale, this._buckets.bucketScale) * options.fontSize;
460
+ }
461
+ if (!this._maxY || this._maxY < d.y + d.props.height) {
462
+ this._maxY = d.y + d.props.height;
463
+ }
464
+ if (!this._maxX || this._maxX < d.x + d.props.width) {
465
+ this._maxX = d.x + d.props.width;
466
+ }
467
+ render(
468
+ this._rangeRenderer,
469
+ {
470
+ ...options,
471
+ ...d.props,
472
+ text,
473
+ },
474
+ d.that
475
+ );
476
+ }
477
+
478
+ setRangeOptions() {
479
+ this._rangeOptions = {
480
+ rangePadding: this.rangePadding(),
481
+ fontFamily: this.fontFamily(),
482
+ fontSize: this.fontSize(),
483
+ strokeWidth: this.strokeWidth(),
484
+ fill: this.fill(),
485
+ stroke: this.stroke(),
486
+ textFill: this.rangeFontColor(),
487
+ cornerRadius: this.cornerRadius(),
488
+ };
489
+ }
490
+
491
+ public _transform = { k: 1, x: 0, y: 0 };
492
+ zoomed(transform) {
493
+ this._transform = transform;
494
+ // store current visible range for zoom preservation
495
+ if (this._interpolateX && typeof this._interpolateX.invert === "function") {
496
+ const width = this.width();
497
+ if (width > 0 && isFinite(transform.k) && transform.k !== 0) {
498
+ const startPixel = (0 - transform.x) / transform.k;
499
+ const endPixel = (width - transform.x) / transform.k;
500
+ let visibleStart = this._interpolateX.invert(startPixel);
501
+ let visibleEnd = this._interpolateX.invert(endPixel);
502
+ if (isFinite(visibleStart) && isFinite(visibleEnd)) {
503
+ if (visibleStart > visibleEnd) {
504
+ const tmp = visibleStart;
505
+ visibleStart = visibleEnd;
506
+ visibleEnd = tmp;
507
+ }
508
+ this._prevZoomState = {
509
+ visibleStart,
510
+ visibleEnd
511
+ };
512
+ }
513
+ }
514
+ }
515
+ switch (this.renderMode()) {
516
+ case "scale-all":
517
+ this._zoomScale = transform.k;
518
+ this._zoomTranslate = [transform.x, 0];
519
+ this._zoomG.attr("transform", `translate(${transform.x} ${0})scale(${transform.k} 1)`);
520
+ break;
521
+ default:
522
+ const options = this._rangeOptions;
523
+ this.data().forEach((d, i) => {
524
+ if (this._color_idx > -1) {
525
+ options.fill = d[this._color_idx];
526
+ }
527
+ if (this._series_idx > -1) {
528
+ const seriesKey = d[this._series_idx];
529
+ this.renderRangeElement(d, i, true, options, seriesKey);
530
+ } else {
531
+ this.renderRangeElement(d, i, true, options);
532
+ }
533
+ });
534
+ }
535
+
536
+ this.zoomedHook(transform);
537
+ }
538
+
539
+ zoomedHook(transform) {
540
+
541
+ }
542
+
543
+ private calcBuckets(data, startKey: string | number, endKey: string | number, bucketKey?: string | number) {
544
+ const bucketMap = {};
545
+ const bucketKeyMap = {};
546
+ const tol = this.overlapTolerence();
547
+ const buckets = [{ end: -Infinity }];
548
+ let maxBucket = 0;
549
+ if (bucketKey !== undefined) {
550
+ data.forEach((d, i) => {
551
+ bucketMap[i] = d[bucketKey];
552
+ bucketKeyMap[d[bucketKey]] = true;
553
+ });
554
+ maxBucket = Object.keys(bucketKeyMap).length;
555
+ } else {
556
+ data.forEach((d, i) => {
557
+ for (let i2 = 0; i2 < buckets.length; ++i2) {
558
+ if (i === 0 || buckets[i2][endKey] + tol <= d[startKey]) {
559
+ bucketMap[i] = i2;
560
+ if (maxBucket < i2) maxBucket = i2;
561
+ buckets[i2][endKey] = d[endKey];
562
+ break;
563
+ }
564
+ }
565
+ if (bucketMap[i] === undefined) {
566
+ bucketMap[i] = buckets.length;
567
+ const b = {};
568
+ b[endKey] = d[endKey];
569
+ buckets.push(b as any);
570
+ }
571
+
572
+ if (maxBucket < bucketMap[i]) maxBucket = bucketMap[i];
573
+ });
574
+ }
575
+ const height = (maxBucket + 1) * (this.bucketHeight() + this.gutter());
576
+ return {
577
+ bucketMap,
578
+ maxBucket,
579
+ bucketScale: this.height() / height,
580
+ interpolateY: d3ScaleLinear()
581
+ .domain([0, maxBucket + 1])
582
+ .range([0, Math.min(this.height(), height)])
583
+ };
584
+ }
585
+
586
+ data(): IGanttData[];
587
+ data(_: IGanttData[]): this;
588
+ data(_?: IGanttData[]): this | IGanttData[] {
589
+ const retVal = super.data.apply(this, arguments);
590
+ if (arguments.length > 0) {
591
+ this._minStart = Math.min(...this.data().map(n => n[1])) ?? 0;
592
+ this._maxEnd = Math.max(...this.data().map(n => n[2])) ?? 1;
593
+ this.measureDataText(true);
594
+ }
595
+ return retVal;
596
+ }
597
+
598
+ protected _textWidths;
599
+ protected _maxFontScale;
600
+ protected _characterWidths;
601
+ protected _prevFontFamily;
602
+ protected _prevFontSize;
603
+ measureDataText(forceMeasure = false) {
604
+ const textWidths = {};
605
+ const characterWidths = {};
606
+ const fontFamily = this.fontFamily();
607
+ const fontSize = this.fontSize();
608
+ const bucketHeight = this.bucketHeight();
609
+
610
+ if (bucketHeight) {
611
+ this._maxFontScale = (bucketHeight - (this.rangePadding() * 2)) / fontSize;
612
+ }
613
+
614
+ if (forceMeasure || this._prevFontFamily !== fontFamily || this._prevFontSize !== fontSize) {
615
+ characterWidths["."] = Utility.textSize(".", fontFamily, fontSize).width;
616
+ this.data().forEach(d => {
617
+ if (!textWidths[d[0]]) {
618
+ textWidths[d[0]] = Utility.textSize(d[0], fontFamily, fontSize).width;
619
+ }
620
+ d[0].split("").forEach(char => {
621
+ if (!characterWidths[char]) {
622
+ characterWidths[char] = Utility.textSize(char, fontFamily, fontSize).width;
623
+ }
624
+ });
625
+ });
626
+ this._textWidths = textWidths;
627
+ this._characterWidths = characterWidths;
628
+ }
629
+ this._prevFontFamily = fontFamily;
630
+ this._prevFontSize = fontSize;
631
+ }
632
+
633
+ truncateText(text, width, scale = 1) {
634
+ const textFits = this._textWidths[text] * scale < width;
635
+ if (textFits) {
636
+ return text;
637
+ }
638
+ let ret = "";
639
+ let sum = 0;
640
+ const _width = width - (this._characterWidths["."] * 3);
641
+ for (const char of text) {
642
+ sum += this._characterWidths[char];
643
+ if (sum < _width) {
644
+ ret += char;
645
+ } else {
646
+ break;
647
+ }
648
+ }
649
+ return _width < 0 ? "" : ret + "...";
650
+ }
651
+
652
+ resize(_size?: { width: number, height: number }) {
653
+ let retVal;
654
+ if (this.fitWidthToContent() || this.fitHeightToContent()) {
655
+ retVal = super.resize.call(this, {
656
+ width: _size.width,
657
+ height: this._maxY
658
+ });
659
+ } else {
660
+ retVal = super.resize.apply(this, arguments);
661
+ }
662
+ return retVal;
663
+ }
664
+
665
+ selectionChanged() {
666
+
667
+ }
668
+
669
+ highlightItem(_element, d) {
670
+
671
+ }
672
+
673
+ click(row, _col, sel) {
674
+
675
+ }
676
+
677
+ dblclick(row, _col, sel) {
678
+
679
+ }
680
+
681
+ mousein(row, _col, sel) {
682
+ }
683
+
684
+ mouseover(row, _col, sel) {
685
+ }
686
+
687
+ mouseout(row, _col, sel) {
688
+ }
689
+ }
690
+ ReactGantt.prototype._class += " timeline_ReactGantt";
691
+
692
+ export interface ReactGantt {
693
+ titleColumn(): string;
694
+ titleColumn(_: string): this;
695
+ startDateColumn(): string;
696
+ startDateColumn(_: string): this;
697
+ endDateColumn(): string;
698
+ endDateColumn(_: string): this;
699
+ iconColumn(): string;
700
+ iconColumn(_: string): this;
701
+ colorColumn(): string;
702
+ colorColumn(_: string): this;
703
+ seriesColumn(): string;
704
+ seriesColumn(_: string): this;
705
+ bucketColumn(): string;
706
+ bucketColumn(_: string): this;
707
+ overlapTolerence(): number;
708
+ overlapTolerence(_: number): this;
709
+ smallestRangeWidth(): number;
710
+ smallestRangeWidth(_: number): this;
711
+ bucketHeight(): number;
712
+ bucketHeight(_: number): this;
713
+ gutter(): number;
714
+ gutter(_: number): this;
715
+ showToolbar_default(_: boolean): this;
716
+ fontSize(): number;
717
+ fontSize(_: number): this;
718
+ fontFamily(): string;
719
+ fontFamily(_: string): this;
720
+ strokeWidth(): number;
721
+ strokeWidth(_: number): this;
722
+ stroke(): string;
723
+ stroke(_: string): this;
724
+ cornerRadius(): number;
725
+ cornerRadius(_: number): this;
726
+ fill(): string;
727
+ fill(_: string): this;
728
+ rangeFontColor(): string;
729
+ rangeFontColor(_: string): this;
730
+ rangePadding(): number;
731
+ rangePadding(_: number): this;
732
+ renderMode(): "default" | "scale-all";
733
+ renderMode(_: "default" | "scale-all"): this;
734
+ maxZoom(): number;
735
+ maxZoom(_: number): this;
736
+ fitWidthToContent(): boolean;
737
+ fitWidthToContent(_: boolean): this;
738
+ fitHeightToContent(): boolean;
739
+ fitHeightToContent(_: boolean): this;
740
+ preserveZoom(): boolean;
741
+ preserveZoom(_: boolean): this;
742
+ evenSeriesBackground(): string;
743
+ evenSeriesBackground(_: string): this;
744
+ oddSeriesBackground(): string;
745
+ oddSeriesBackground(_: string): this;
746
+ }
747
+
748
+ ReactGantt.prototype.publish("fitWidthToContent", false, "boolean", "If true, resize will simply reapply the bounding box width");
749
+ ReactGantt.prototype.publish("fitHeightToContent", false, "boolean", "If true, resize will simply reapply the bounding box height");
750
+ ReactGantt.prototype.publish("preserveZoom", false, "boolean", "If true, maintain zoom level when data is updated");
751
+ ReactGantt.prototype.publish("titleColumn", null, "string", "Column name to for the title");
752
+ ReactGantt.prototype.publish("startDateColumn", null, "string", "Column name to for the start date");
753
+ ReactGantt.prototype.publish("endDateColumn", null, "string", "Column name to for the end date");
754
+ ReactGantt.prototype.publish("iconColumn", null, "string", "Column name to for the icon");
755
+ ReactGantt.prototype.publish("colorColumn", null, "string", "Column name to for the color");
756
+ ReactGantt.prototype.publish("seriesColumn", null, "string", "Column name to for the series identifier");
757
+ ReactGantt.prototype.publish("bucketColumn", null, "string", "Column name to for the bucket identifier");
758
+ ReactGantt.prototype.publish("renderMode", "default", "set", "Render modes vary in features and performance", ["default", "scale-all"]);
759
+ ReactGantt.prototype.publish("rangePadding", 3, "number", "Padding within each range rectangle (pixels)");
760
+ ReactGantt.prototype.publish("fill", "#1f77b4", "string", "Background color of range rectangle");
761
+ ReactGantt.prototype.publish("stroke", null, "string", "Color of range rectangle border");
762
+ ReactGantt.prototype.publish("strokeWidth", null, "number", "Width of range rectangle border (pixels)");
763
+ ReactGantt.prototype.publish("cornerRadius", 3, "number", "Space between range buckets (pixels)");
764
+ ReactGantt.prototype.publish("fontFamily", null, "string", "Font family within range rectangle", null, { optional: true });
765
+ ReactGantt.prototype.publish("fontSize", 10, "number", "Size of font within range rectangle (pixels)");
766
+ ReactGantt.prototype.publish("rangeFontColor", "#ecf0f1", "html-color", "rangeFontColor");
767
+ ReactGantt.prototype.publish("overlapTolerence", 2, "number", "overlapTolerence");
768
+ ReactGantt.prototype.publish("smallestRangeWidth", 10, "number", "Width of the shortest range (pixels)");
769
+ ReactGantt.prototype.publish("bucketHeight", 100, "number", "Max height of range element (pixels)");
770
+ ReactGantt.prototype.publish("gutter", 2, "number", "Space between range buckets (pixels)");
771
+ ReactGantt.prototype.publish("maxZoom", 16, "number", "Maximum zoom");
772
+ ReactGantt.prototype.publish("evenSeriesBackground", "#FFFFFF", "html-color", "Background color of even series rows");
773
+ ReactGantt.prototype.publish("oddSeriesBackground", "#DDDDDD", "html-color", "Background color of odd series rows");