@raruto/leaflet-elevation 2.3.4 → 2.5.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.
@@ -1,783 +1,783 @@
1
- import * as D3 from './d3.js';
2
-
3
- const _ = L.Control.Elevation.Utils;
4
-
5
- export var Chart = L.Control.Elevation.Chart = L.Class.extend({
6
-
7
- includes: L.Evented ? L.Evented.prototype : L.Mixin.Events,
8
-
9
- initialize(opts, control) {
10
- this.options = opts;
11
- this.control = control;
12
-
13
- this._data = control._data || [];
14
-
15
- // cache registered components
16
- this._props = {
17
- scales : {},
18
- paths : {},
19
- areas : {},
20
- grids : {},
21
- axes : {},
22
- legendItems : {},
23
- tooltipItems: {},
24
- };
25
-
26
- this._scales = {};
27
- this._domains = {};
28
- this._ranges = {};
29
- this._paths = {};
30
-
31
- this._brushEnabled = opts.dragging;
32
- this._zoomEnabled = opts.zooming;
33
-
34
- opts.xTicks = this._xTicks();
35
- opts.yTicks = this._yTicks();
36
-
37
- let chart = this._chart = D3.Chart(opts);
38
-
39
- // SVG Container
40
- this._container = chart.svg;
41
-
42
- // Panes
43
- this._grid = chart.pane('grid');
44
- this._area = chart.pane('area');
45
- this._point = chart.pane('point');
46
- this._axis = chart.pane('axis');
47
- this._legend = chart.pane('legend');
48
- this._tooltip = chart.pane('tooltip');
49
- this._ruler = chart.pane('ruler');
50
-
51
- // Scales
52
- this._initScale();
53
-
54
- // Helpers
55
- this._mask = chart.get('mask');
56
- this._context = chart.get('context');
57
- this._brush = chart.get('brush');
58
-
59
- this._zoom = d3.zoom();
60
- this._drag = d3.drag();
61
-
62
- // Interactions
63
- this._initInteractions();
64
-
65
- // svg.on('resize', (e)=>console.log(e.detail));
66
-
67
- // Handle multi-track segments
68
- this._maskGaps = [];
69
- control.on('eletrack_added', ({index}) => {
70
- this._maskGaps.push(index);
71
- control.once('elepoint_added', ({index}) => this._maskGaps.push(index));
72
- });
73
-
74
- },
75
-
76
- update(props) {
77
- if (props) {
78
- if (props.data) this._data = props.data;
79
- if (props.options) this.options = props.options;
80
- }
81
-
82
- this.options.xTicks = this._xTicks();
83
- this.options.yTicks = this._yTicks();
84
-
85
- this._updateScale();
86
- this._updateAxis();
87
- this._updateMargins();
88
- this._updateLegend();
89
- this._updateClipper();
90
- this._updateArea();
91
-
92
- return this;
93
- },
94
-
95
- render() {
96
- return container => container.append(() => this._container.node());
97
- },
98
-
99
- clear() {
100
- this._resetDrag();
101
- this._hideDiagramIndicator()
102
- this._area.selectAll('path').attr("d", "M0 0");
103
- this._context.clearRect(0, 0, this._width(), this._height());
104
- this._mask.selectAll(".gap").remove();
105
- this._maskGaps = [];
106
-
107
- // if (this._path) {
108
- // this._x.domain([0, 1]);
109
- // this._y.domain([0, 1]);
110
- // }
111
- },
112
-
113
- _drawPath(name) {
114
- let path = this._paths[name];
115
- let area = this._props.areas[name];
116
-
117
- path.datum(this._data).attr("d",
118
- D3.Area(
119
- L.extend({}, area, {
120
- width : this._width(),
121
- height : this._height(),
122
- scaleX : this._scales[area.scaleX],
123
- scaleY : this._scales[area.scaleY]
124
- })
125
- )
126
- );
127
-
128
- if (!path.classed('leaflet-hidden')) {
129
- this.options.preferCanvas
130
- ? _.drawCanvas(this._context, path)
131
- : _.append(this._area.node(), path.node());
132
- }
133
- },
134
-
135
- _hasActiveLayers() {
136
- const paths = this._paths;
137
- for (var i in paths) {
138
- if (!paths[i].classed('leaflet-hidden')) {
139
- return true;
140
- }
141
- }
142
- return false;
143
- },
144
-
145
- /**
146
- * Initialize "d3-brush".
147
- */
148
- _initBrush(e) {
149
- const brush = ({selection}) => {
150
- if (selection && this._data.length) {
151
- let start = this._findIndexForXCoord(selection[0]);
152
- let end = this._findIndexForXCoord(selection[1]);
153
- this.fire('dragged', { dragstart: this._data[start], dragend: this._data[end] });
154
- }
155
- }
156
-
157
- const focus = (e) => {
158
- if (this._data.length && (e.type != 'brush' /*|| e.sourceEvent*/)) {
159
- let rect = this._chart.panes.brush.select('.overlay').node();
160
- let coords = d3.pointers(e, rect)[0];
161
- let xCoord = coords[0];
162
- let item = this._data[this._findIndexForXCoord(xCoord)];
163
-
164
- this.fire("mouse_move", { item: item, xCoord: xCoord });
165
- }
166
- };
167
-
168
- this._brush
169
- .filter(({shiftKey, button}) => !shiftKey && !button && this._brushEnabled)
170
- .on("end.update", brush)
171
- .on("brush.update", focus);
172
-
173
- this._chart.panes.brush
174
- .on("mouseenter.focus touchstart.focus", this.fire.bind(this, "mouse_enter"))
175
- .on("mouseout.focus touchend.focus", this.fire.bind(this, "mouse_out") )
176
- .on("mousemove.focus touchmove.focus", focus );
177
-
178
- },
179
-
180
- /**
181
- * Initialize "d3-zoom"
182
- */
183
- _initClipper() {
184
- let svg = this._container;
185
- let margin = this.options.margins;
186
-
187
- const zoom = this._zoom;
188
-
189
- const onStart = ({transform, sourceEvent}) => {
190
- if (sourceEvent && sourceEvent.type == "mousedown") svg.style('cursor', 'grabbing');
191
- if (transform.k == 1 && transform.x == 0) {
192
- this._container.classed('zoomed', true);
193
- // Apply d3-zoom and bind <mask>
194
- if (this._mask) {
195
- this._point.attr('mask', 'url(#' + this._mask.attr('id') + ')');
196
- }
197
- }
198
- this.zooming = true;
199
- };
200
-
201
- const onEnd = ({transform}) => {
202
- if (transform.k ==1 && transform.x == 0) {
203
- this._container.classed('zoomed', false);
204
- // Reset d3-zoom and remove <mask>
205
- if (this._mask) {
206
- this._point.attr('mask', null);
207
- }
208
- }
209
- this.zooming = false;
210
- svg.style('cursor', '');
211
- };
212
-
213
- const onZoom = ({transform, sourceEvent}) => {
214
- // TODO: find a faster way to redraw the chart.
215
- this.zooming = false;
216
- this._updateScale(); // hacky way for restoring x scale when zooming out
217
- this.zooming = true;
218
- this._scales.distance = this._x = transform.rescaleX(this._x); // calculate x scale at zoom level
219
- if (this._scales.time) this._scales.time = transform.rescaleX(this._scales.time); // calculate x scale at zoom level
220
- this._resetDrag();
221
- if (sourceEvent && sourceEvent.type == "mousemove") {
222
- this._hideDiagramIndicator();
223
- }
224
- this.fire('zoom');
225
- };
226
-
227
- zoom
228
- .scaleExtent([1, 10])
229
- .extent([
230
- [margin.left, 0],
231
- [this._width() - margin.right, this._height()]
232
- ])
233
- .translateExtent([
234
- [margin.left, -Infinity],
235
- [this._width() - margin.right, Infinity]
236
- ])
237
- .filter(({shiftKey, buttons}) => (shiftKey || buttons == 4) && this._zoomEnabled)
238
- .on("start", onStart)
239
- .on("end", onEnd)
240
- .on("zoom", onZoom);
241
-
242
- svg.call(zoom); // add zoom functionality to "svg" group
243
-
244
- // d3.select("body").on("keydown.grabzoom keyup.grabzoom", (e) => svg.style('cursor', e.shiftKey ? 'move' : ''));
245
-
246
- },
247
-
248
- _initInteractions() {
249
- this._initBrush();
250
- this._initRuler();
251
- this._initClipper();
252
- this._initLegend();
253
- },
254
-
255
- /**
256
- * Toggle chart data on legend click
257
- */
258
- _initLegend() {
259
- this._container.on('legend_clicked', ({detail}) => {
260
- let { path, legend, name, enabled } = detail;
261
- if (path) {
262
- let label = _.select('text', legend);
263
- let rect = _.select('rect', legend);
264
- _.toggleStyle(label, 'text-decoration-line', 'line-through', enabled);
265
- _.toggleStyle(rect, 'fill-opacity', '0', enabled);
266
- _.toggleClass(path, 'leaflet-hidden', enabled);
267
- this._updateArea();
268
- this.fire("elepath_toggle", { path, name, legend, enabled })
269
- }
270
- });
271
- },
272
-
273
- /**
274
- * Initialize "ruler".
275
- */
276
- _initRuler() {
277
- if (!this.options.ruler) return;
278
-
279
- // const yMax = this._height();
280
- const formatNum = d3.format(".0f");
281
- const drag = this._drag;
282
-
283
- const label = (e, d) => {
284
- let yMax = this._height();
285
- let y = this._ruler.data()[0].y;
286
- if (y >= yMax || y <= 0) this._ruler.select(".horizontal-drag-label").text('');
287
- this._hideDiagramIndicator();
288
- };
289
-
290
- const position = (e, d) => {
291
- let yCoord = d3.pointers(e, this._area.node())[0][1];
292
- let yMax = this._height();
293
- let y = _.clamp(yCoord, [0, yMax]);
294
-
295
- this._ruler
296
- .data([L.extend(this._ruler.data()[0], { y: y })])
297
- .attr("transform", d => "translate(" + d.x + "," + d.y + ")")
298
- .classed('active', y < yMax);
299
-
300
- this._container
301
- .select(".horizontal-drag-label")
302
- .text(formatNum(this._y.invert(y)) + " " + (this.options.imperial ? 'ft' : 'm'));
303
-
304
- this.fire('ruler_filter', { coords: yCoord < yMax && yCoord > 0 ? this._findCoordsForY(yCoord) : [] });
305
- }
306
-
307
- drag
308
- .on("start end", label)
309
- .on("drag", position);
310
-
311
- this._ruler.call(drag);
312
-
313
- },
314
-
315
- /**
316
- * Initialize x and y scales
317
- */
318
- _initScale() {
319
- let opts = this.options;
320
-
321
- this._registerAxisScale({
322
- axis : 'x',
323
- position: 'bottom',
324
- attr : opts.xAttr,
325
- min : opts.xAxisMin,
326
- max : opts.xAxisMax,
327
- name : 'distance'
328
- });
329
-
330
- this._registerAxisScale({
331
- axis : 'y',
332
- position : 'left',
333
- attr : opts.yAttr,
334
- min : opts.yAxisMin,
335
- max : opts.yAxisMax,
336
- name : 'altitude'
337
- });
338
-
339
- this._x = this._scales.distance;
340
- this._y = this._scales.altitude;
341
- },
342
-
343
- _registerAreaPath(props) {
344
- if (props.scale == 'y') props.scale = this._y;
345
- else if (props.scale == 'x') props.scale = this._x;
346
-
347
- let opts = this.options;
348
-
349
- if (!props.xAttr) props.xAttr = opts.xAttr;
350
- if (!props.yAttr) props.yAttr = opts.yAttr;
351
- if (typeof props.preferCanvas === "undefined") props.preferCanvas = opts.preferCanvas;
352
-
353
- // Save paths in memory for latter usage
354
- this._paths[props.name] = D3.Path(props);
355
- this._props.areas[props.name] = props;
356
-
357
- if (opts.legend) {
358
- this._props.legendItems[props.name] = {
359
- name : props.name,
360
- label : props.label,
361
- color : props.color,
362
- className: props.className,
363
- path : this._paths[props.name]
364
- };
365
- }
366
-
367
- },
368
-
369
- _registerAxisGrid(props) {
370
- if (props.scale == 'y') props.scale = this._y;
371
- else if(props.scale == 'x') props.scale = this._x;
372
-
373
- this._props.grids[props.name || props.axis] = props;
374
- },
375
-
376
- _registerAxisScale(props) {
377
- if (props.scale == 'y') props.scale = this._y;
378
- else if(props.scale == 'x') props.scale = this._x;
379
-
380
- let opts = this.options;
381
- let scale = props.scale;
382
-
383
- if (typeof this._scales[props.name] === 'function') {
384
- props.scale = this._scales[props.name]; // retrieve cached scale
385
- } else if (typeof scale !== 'function') {
386
- scale = L.extend({
387
- data : this._data,
388
- forceBounds: opts.forceAxisBounds
389
- }, scale);
390
-
391
- scale.attr = scale.attr || props.name;
392
-
393
- let domain = this._domains[props.name] = D3.Domain(props);
394
- let range = this._ranges[props.name] = D3.Range(props);
395
- scale.range = scale.range || range(this._width(),this._height());
396
- scale.domain = scale.domain || domain(this._data);
397
-
398
- this._props.scales[props.name] = scale;
399
-
400
- props.scale = this._scales[props.name] = D3.Scale(scale);
401
- }
402
-
403
- if (!props.ticks) {
404
- if (props.axis == 'x') props.ticks = this._xTicks.bind(this);
405
- else if (props.axis == 'y') props.ticks = this._yTicks.bind(this);
406
- }
407
-
408
- this._props.axes[props.name] = props;
409
-
410
- if (props.name == this.options.yScale) this._y = scale;
411
- if (props.name == this.options.xScale) this._x = scale;
412
-
413
- return scale;
414
- },
415
-
416
- /**
417
- * Add a point of interest over the chart
418
- */
419
- _registerCheckPoint(point) {
420
- if (!this._data.length) return;
421
-
422
- const {xAttr, yAttr} = this.options;
423
- let item, x, y;
424
-
425
- if (point.latlng) {
426
- item = this._data[this._findIndexForLatLng(point.latlng)];
427
- x = this._x(item[xAttr]);
428
- y = this._y(item[yAttr]);
429
- } else if (!isNaN(point[xAttr])) {
430
- x = this._x(point[xAttr]);
431
- item = this._data[this._findIndexForXCoord(x)]
432
- y = this._y(item[yAttr]);
433
- }
434
-
435
- this._point.call(D3.CheckPoint({
436
- point: point,
437
- width: this._width(),
438
- height: this._height(),
439
- x: x,
440
- y: y,
441
- }));
442
- },
443
-
444
- _registerTooltip(props) {
445
- props.order = props.order ?? 1000;
446
- this._props.tooltipItems[props.name] = props;
447
- },
448
-
449
- _updateArea() {
450
- // Reset and update chart profiles
451
- this._context.clearRect(0, 0, this._width(), this._height());
452
- _.each(this._paths, (path, i) => !path.classed('leaflet-hidden') && this._drawPath(i));
453
- },
454
-
455
- _updateAxis() {
456
-
457
- let opts = this.options;
458
-
459
- const gridOpts = {
460
- width : this._width(),
461
- height : this._height(),
462
- tickFormat: "",
463
- };
464
-
465
- const axesOpts = {
466
- width : this._width(),
467
- height : this._height(),
468
- };
469
-
470
- // Reset grids
471
- this._grid.selectAll('g').remove();
472
- _.each(this._props.grids, (grid, i) => {
473
- if (opts[i] !== false && opts[i] !== 'summary') {
474
- this._grid.call(D3.Grid(L.extend({}, gridOpts, grid)))
475
- }
476
- });
477
-
478
- // Rest axis
479
- this._axis.selectAll('g').remove();
480
- _.each(this._props.axes, (axis, i) => {
481
- if (opts[i] !== false && opts[i] !== 'summary') {
482
- this._axis.call(D3.Axis(L.extend({}, axesOpts, axis)));
483
- }
484
- });
485
-
486
- // Adjust axis scale positions
487
- this._axis
488
- .selectAll('.y.axis.right')
489
- .each((d, i, n) => {
490
-
491
- let axis = d3.select(n[i]);
492
- let transform = axis.attr('transform');
493
- let translate = transform.substring(transform.indexOf("(") + 1, transform.indexOf(")")).split(",");
494
-
495
- axis.attr('transform', 'translate(' + (+translate[0] + (i * 40)) + ',' + translate[1] + ')')
496
-
497
- if (i > 0) {
498
- axis.select(':scope > path') .attr('opacity', 0.25);
499
- axis.selectAll(':scope > .tick line').attr('opacity', 0.75);
500
- }
501
-
502
- });
503
-
504
- },
505
-
506
- _updateClipper() {
507
- const { xAttr, margins } = this.options;
508
- const data = this._data;
509
-
510
- this._zoom
511
- .scaleExtent([1, 10])
512
- .extent([
513
- [margins.left, 0],
514
- [this._width() - margins.right, this._height()]
515
- ])
516
- .translateExtent([
517
- [margins.left, -Infinity],
518
- [this._width() - margins.right, Infinity]
519
- ]);
520
-
521
- // Apply svg mask on multi-track segments
522
- this._mask.selectAll(".gap").remove()
523
- this._maskGaps.forEach((d, i) => {
524
- if(i >= this._maskGaps.length - 2) return;
525
- let x1 = this._x(data[this._findIndexForLatLng(data[this._maskGaps[i]].latlng)][xAttr]);
526
- let x2 = this._x(data[this._findIndexForLatLng(data[this._maskGaps[i + 1]].latlng)][xAttr]);
527
- this._mask
528
- .append("rect")
529
- .attr("x", x1)
530
- .attr("y", 0)
531
- .attr("width", x2 - x1 )
532
- .attr("height", this._height())
533
- .attr('class', 'gap')
534
- .attr('fill-opacity', '0.8')
535
- .attr("fill", 'black'); // hide = black (mask)
536
- });
537
- },
538
-
539
- _updateLegend: function () {
540
- let xAxesB = this._axis.selectAll('.x.axis.bottom').nodes().length;
541
-
542
- // Get legend items
543
- let items = Object.keys(this._paths);
544
-
545
- // Calculate legend item positions
546
- let n = items.length;
547
- let v = Array(Math.floor(n / 2)).fill(null).map((d, i) => (i + 1) * 2 - (1 - Math.sign(n % 2)));
548
- let rev = v.slice().reverse().map((d) => -(d));
549
-
550
- // push a fake element to handle center alignment (odd numbers)
551
- if (n % 2 !== 0) {
552
- rev.push(0);
553
- }
554
- v = rev.concat(v);
555
-
556
- // Reset legend items
557
- this._legend.selectAll('g').remove();
558
-
559
- // Render legend items
560
- _.each(this._props.legendItems, (legend) => {
561
- this._legend.append("g").call(
562
- D3.LegendItem(L.extend({
563
- width : this._width(),
564
- height : this._height(),
565
- margins: this.options.margins,
566
- }, legend))
567
- );
568
- });
569
-
570
- // Render legend item switcher
571
- if (n > 1) {
572
- this._legend.append("g").call(
573
- D3.LegendSmall({
574
- width : this._width(),
575
- height : this._height(),
576
- items : items,
577
- onClick: (selected) => {
578
- _.each(items, name => this._togglePath(name, selected == name, true));
579
- this._updateArea();
580
- }
581
- })
582
- );
583
- }
584
-
585
- _.each(items, (name, i) => {
586
- // Adjust legend item positions
587
- this._legend.select('[data-name=' + name + ']').attr("transform", "translate(" + (v[i] * 55) + ", " + (xAxesB * 2) + ")");
588
- // Set initial state (disabled controls)
589
- this._togglePath(name, !(name in this.options && this.options[name] == 'disabled'), true);
590
- });
591
-
592
- },
593
-
594
- _updateMargins() {
595
- // Get chart margins
596
- let xAxesB = this._axis.selectAll('.x.axis.bottom').nodes().length;
597
- let xAxesL = this._axis.selectAll('.y.axis.left').nodes().length;
598
- let xAxesR = this._axis.selectAll('.y.axis.right').nodes().length;
599
- let marginB = 60 + (xAxesB * 2);
600
- let marginL = 10 + (xAxesL * 30);
601
- let marginR = 40 + (xAxesR * 30);
602
-
603
- let marginsUpdated = false
604
-
605
- // Adjust right margin
606
- if (xAxesR && this.options.margins.right < marginR) {
607
- this.options.margins.right = marginR;
608
- marginsUpdated = true;
609
- }
610
-
611
- // Adjust left margin
612
- if(xAxesL && this.options.margins.left < marginL) {
613
- this.options.margins.left = marginL;
614
- marginsUpdated = true;
615
- }
616
-
617
- // Adjust bottom margin
618
- if(xAxesB && this.options.margins.bottom < marginB) {
619
- this.options.margins.bottom = marginB;
620
- marginsUpdated = true;
621
- }
622
-
623
- if (marginsUpdated) {
624
- this.fire('margins_updated');
625
- }
626
- },
627
-
628
- _updateScale() {
629
- if (this.zooming) return { x: this._x, y: this._y };
630
-
631
- for (let i in this._scales) {
632
- this._scales[i]
633
- .domain(this._domains[i](this._data))
634
- .range(this._ranges[i](this._width(), this._height()))
635
- }
636
-
637
- return { x: this._x, y: this._y };
638
- },
639
-
640
- /**
641
- * Calculates chart width.
642
- */
643
- _width() {
644
- if (this._chart) this._chart._width;
645
- const { width, margins } = this.options;
646
- return width - margins.left - margins.right;
647
- },
648
-
649
- /**
650
- * Calculates chart height.
651
- */
652
- _height() {
653
- if (this._chart) return this._chart._height;
654
- const { height, margins } = this.options;
655
- return height - margins.top - margins.bottom;
656
- },
657
-
658
- /*
659
- * Finds data entries above a given y-elevation value and returns geo-coordinates
660
- */
661
- _findCoordsForY(y) {
662
- let data = this._data;
663
- let z = this._y.invert(y);
664
-
665
- // save indexes of elevation values above the horizontal line
666
- const list = data.reduce((array, item, index) => {
667
- if (item[this.options.yAttr] >= z) array.push(index);
668
- return array;
669
- }, []);
670
-
671
- let start = 0;
672
- let next;
673
-
674
- // split index list into blocks of coordinates
675
- const coords = list.reduce((array, _, curr) => {
676
- next = curr + 1;
677
- if (list[next] !== list[curr] + 1 || next === list.length) {
678
- array.push(
679
- list
680
- .slice(start, next)
681
- .map(i => data[i].latlng)
682
- );
683
- start = next;
684
- }
685
- return array;
686
- }, []);
687
-
688
- return coords;
689
- },
690
-
691
- /*
692
- * Finds a data entry for a given x-coordinate of the diagram
693
- */
694
- _findIndexForXCoord(x) {
695
- return d3
696
- .bisector(d => d[this.options.xAttr])
697
- .left(this._data || [0, 1], this._x.invert(x));
698
- },
699
-
700
- /*
701
- * Finds a data entry for a given latlng of the map
702
- */
703
- _findIndexForLatLng(latlng) {
704
- let result = null;
705
- let d = Infinity;
706
- this._data.forEach((item, index) => {
707
- let dist = latlng.distanceTo(item.latlng);
708
- if (dist < d) {
709
- d = dist;
710
- result = index;
711
- }
712
- });
713
- return result;
714
- },
715
-
716
- /*
717
- * Removes the drag rectangle and zoms back to the total extent of the data.
718
- */
719
- _resetDrag() {
720
- if (this._chart.panes.brush.select(".selection").attr('width')) {
721
- this._chart.panes.brush.call(this._brush.clear);
722
- this._hideDiagramIndicator();
723
- this.fire('reset_drag');
724
- }
725
- },
726
-
727
- _resetZoom() {
728
- if (this._zoom) {
729
- this._zoom.transform(this._chart.svg, d3.zoomIdentity);
730
- }
731
- },
732
-
733
- /**
734
- * Display distance and altitude level ("focus-rect").
735
- */
736
- _showDiagramIndicator(item, xCoordinate) {
737
- this._tooltip
738
- .attr("display", null)
739
- .call(D3.Tooltip({
740
- xCoord: xCoordinate,
741
- yCoord: this._y(item[this.options.yAttr]),
742
- height: this._height(),
743
- width : this._width(),
744
- labels: this._props.tooltipItems,
745
- item: item
746
- }));
747
- },
748
-
749
- _togglePath(name, enabled = true, lazy = false) {
750
- let path = this._paths[name];
751
- let legend = this._container.select('.legend [data-name=' + name + ']');
752
- path.classed('leaflet-hidden', !enabled);
753
- legend.select('text').style('text-decoration-line', enabled ? '' : 'line-through');
754
- legend.select('rect').style('fill-opacity', enabled ? '': '0');
755
- // Apply d3-zoom (bind <clipPath> mask)
756
- if (this._mask) {
757
- path.attr('mask', 'url(#' + this._mask.attr('id') + ')');
758
- }
759
- if (!lazy) {
760
- this._updateArea();
761
- }
762
- },
763
-
764
- _hideDiagramIndicator() {
765
- this._tooltip.attr("display", 'none');
766
- },
767
-
768
- /**
769
- * Calculate chart xTicks
770
- */
771
- _xTicks() {
772
- if (this.__xTicks) this.__xTicks = this.options.xTicks;
773
- return this.__xTicks || Math.round(this._width() / 75);
774
- },
775
-
776
- /**
777
- * Calculate chart yTicks
778
- */
779
- _yTicks() {
780
- if (this.__yTicks) this.__yTicks = this.options.yTicks;
781
- return this.__yTicks || Math.round(this._height() / 30);
782
- }
783
- });
1
+ import * as D3 from './d3.js';
2
+
3
+ const _ = L.Control.Elevation.Utils;
4
+
5
+ export var Chart = L.Control.Elevation.Chart = L.Class.extend({
6
+
7
+ includes: L.Evented ? L.Evented.prototype : L.Mixin.Events,
8
+
9
+ initialize(opts, control) {
10
+ this.options = opts;
11
+ this.control = control;
12
+
13
+ this._data = control._data || [];
14
+
15
+ // cache registered components
16
+ this._props = {
17
+ scales : {},
18
+ paths : {},
19
+ areas : {},
20
+ grids : {},
21
+ axes : {},
22
+ legendItems : {},
23
+ tooltipItems: {},
24
+ };
25
+
26
+ this._scales = {};
27
+ this._domains = {};
28
+ this._ranges = {};
29
+ this._paths = {};
30
+
31
+ this._brushEnabled = opts.dragging;
32
+ this._zoomEnabled = opts.zooming;
33
+
34
+ opts.xTicks = this._xTicks();
35
+ opts.yTicks = this._yTicks();
36
+
37
+ let chart = this._chart = D3.Chart(opts);
38
+
39
+ // SVG Container
40
+ this._container = chart.svg;
41
+
42
+ // Panes
43
+ this._grid = chart.pane('grid');
44
+ this._area = chart.pane('area');
45
+ this._point = chart.pane('point');
46
+ this._axis = chart.pane('axis');
47
+ this._legend = chart.pane('legend');
48
+ this._tooltip = chart.pane('tooltip');
49
+ this._ruler = chart.pane('ruler');
50
+
51
+ // Scales
52
+ this._initScale();
53
+
54
+ // Helpers
55
+ this._mask = chart.get('mask');
56
+ this._context = chart.get('context');
57
+ this._brush = chart.get('brush');
58
+
59
+ this._zoom = d3.zoom();
60
+ this._drag = d3.drag();
61
+
62
+ // Interactions
63
+ this._initInteractions();
64
+
65
+ // svg.on('resize', (e)=>console.log(e.detail));
66
+
67
+ // Handle multi-track segments
68
+ this._maskGaps = [];
69
+ control.on('eletrack_added', ({index}) => {
70
+ this._maskGaps.push(index);
71
+ control.once('elepoint_added', ({index}) => this._maskGaps.push(index));
72
+ });
73
+
74
+ },
75
+
76
+ update(props) {
77
+ if (props) {
78
+ if (props.data) this._data = props.data;
79
+ if (props.options) this.options = props.options;
80
+ }
81
+
82
+ this.options.xTicks = this._xTicks();
83
+ this.options.yTicks = this._yTicks();
84
+
85
+ this._updateScale();
86
+ this._updateAxis();
87
+ this._updateMargins();
88
+ this._updateLegend();
89
+ this._updateClipper();
90
+ this._updateArea();
91
+
92
+ return this;
93
+ },
94
+
95
+ render() {
96
+ return container => container.append(() => this._container.node());
97
+ },
98
+
99
+ clear() {
100
+ this._resetDrag();
101
+ this._hideDiagramIndicator()
102
+ this._area.selectAll('path').attr("d", "M0 0");
103
+ this._context.clearRect(0, 0, this._width(), this._height());
104
+ this._mask.selectAll(".gap").remove();
105
+ this._maskGaps = [];
106
+
107
+ // if (this._path) {
108
+ // this._x.domain([0, 1]);
109
+ // this._y.domain([0, 1]);
110
+ // }
111
+ },
112
+
113
+ _drawPath(name) {
114
+ let path = this._paths[name];
115
+ let area = this._props.areas[name];
116
+
117
+ path.datum(this._data).attr("d",
118
+ D3.Area(
119
+ L.extend({}, area, {
120
+ width : this._width(),
121
+ height : this._height(),
122
+ scaleX : this._scales[area.scaleX],
123
+ scaleY : this._scales[area.scaleY]
124
+ })
125
+ )
126
+ );
127
+
128
+ if (!path.classed('leaflet-hidden')) {
129
+ this.options.preferCanvas
130
+ ? _.drawCanvas(this._context, path)
131
+ : _.append(this._area.node(), path.node());
132
+ }
133
+ },
134
+
135
+ _hasActiveLayers() {
136
+ const paths = this._paths;
137
+ for (var i in paths) {
138
+ if (!paths[i].classed('leaflet-hidden')) {
139
+ return true;
140
+ }
141
+ }
142
+ return false;
143
+ },
144
+
145
+ /**
146
+ * Initialize "d3-brush".
147
+ */
148
+ _initBrush(e) {
149
+ const brush = ({selection}) => {
150
+ if (selection && this._data.length) {
151
+ let start = this._findIndexForXCoord(selection[0]);
152
+ let end = this._findIndexForXCoord(selection[1]);
153
+ this.fire('dragged', { dragstart: this._data[start], dragend: this._data[end] });
154
+ }
155
+ }
156
+
157
+ const focus = (e) => {
158
+ if (this._data.length && (e.type != 'brush' /*|| e.sourceEvent*/)) {
159
+ let rect = this._chart.panes.brush.select('.overlay').node();
160
+ let coords = d3.pointers(e, rect)[0];
161
+ let xCoord = coords[0];
162
+ let item = this._data[this._findIndexForXCoord(xCoord)];
163
+
164
+ this.fire("mouse_move", { item: item, xCoord: xCoord });
165
+ }
166
+ };
167
+
168
+ this._brush
169
+ .filter(({shiftKey, button}) => !shiftKey && !button && this._brushEnabled)
170
+ .on("end.update", brush)
171
+ .on("brush.update", focus);
172
+
173
+ this._chart.panes.brush
174
+ .on("mouseenter.focus touchstart.focus", this.fire.bind(this, "mouse_enter"))
175
+ .on("mouseout.focus touchend.focus", this.fire.bind(this, "mouse_out") )
176
+ .on("mousemove.focus touchmove.focus", focus );
177
+
178
+ },
179
+
180
+ /**
181
+ * Initialize "d3-zoom"
182
+ */
183
+ _initClipper() {
184
+ let svg = this._container;
185
+ let margin = this.options.margins;
186
+
187
+ const zoom = this._zoom;
188
+
189
+ const onStart = ({transform, sourceEvent}) => {
190
+ if (sourceEvent && sourceEvent.type == "mousedown") svg.style('cursor', 'grabbing');
191
+ if (transform.k == 1 && transform.x == 0) {
192
+ this._container.classed('zoomed', true);
193
+ // Apply d3-zoom and bind <mask>
194
+ if (this._mask) {
195
+ this._point.attr('mask', 'url(#' + this._mask.attr('id') + ')');
196
+ }
197
+ }
198
+ this.zooming = true;
199
+ };
200
+
201
+ const onEnd = ({transform}) => {
202
+ if (transform.k ==1 && transform.x == 0) {
203
+ this._container.classed('zoomed', false);
204
+ // Reset d3-zoom and remove <mask>
205
+ if (this._mask) {
206
+ this._point.attr('mask', null);
207
+ }
208
+ }
209
+ this.zooming = false;
210
+ svg.style('cursor', '');
211
+ };
212
+
213
+ const onZoom = ({transform, sourceEvent}) => {
214
+ // TODO: find a faster way to redraw the chart.
215
+ this.zooming = false;
216
+ this._updateScale(); // hacky way for restoring x scale when zooming out
217
+ this.zooming = true;
218
+ this._scales.distance = this._x = transform.rescaleX(this._x); // calculate x scale at zoom level
219
+ if (this._scales.time) this._scales.time = transform.rescaleX(this._scales.time); // calculate x scale at zoom level
220
+ this._resetDrag();
221
+ if (sourceEvent && sourceEvent.type == "mousemove") {
222
+ this._hideDiagramIndicator();
223
+ }
224
+ this.fire('zoom');
225
+ };
226
+
227
+ zoom
228
+ .scaleExtent([1, 10])
229
+ .extent([
230
+ [margin.left, 0],
231
+ [this._width() - margin.right, this._height()]
232
+ ])
233
+ .translateExtent([
234
+ [margin.left, -Infinity],
235
+ [this._width() - margin.right, Infinity]
236
+ ])
237
+ .filter(({shiftKey, buttons}) => (shiftKey || buttons == 4) && this._zoomEnabled)
238
+ .on("start", onStart)
239
+ .on("end", onEnd)
240
+ .on("zoom", onZoom);
241
+
242
+ svg.call(zoom); // add zoom functionality to "svg" group
243
+
244
+ // d3.select("body").on("keydown.grabzoom keyup.grabzoom", (e) => svg.style('cursor', e.shiftKey ? 'move' : ''));
245
+
246
+ },
247
+
248
+ _initInteractions() {
249
+ this._initBrush();
250
+ this._initRuler();
251
+ this._initClipper();
252
+ this._initLegend();
253
+ },
254
+
255
+ /**
256
+ * Toggle chart data on legend click
257
+ */
258
+ _initLegend() {
259
+ this._container.on('legend_clicked', ({detail}) => {
260
+ let { path, legend, name, enabled } = detail;
261
+ if (path) {
262
+ let label = _.select('text', legend);
263
+ let rect = _.select('rect', legend);
264
+ _.toggleStyle(label, 'text-decoration-line', 'line-through', enabled);
265
+ _.toggleStyle(rect, 'fill-opacity', '0', enabled);
266
+ _.toggleClass(path, 'leaflet-hidden', enabled);
267
+ this._updateArea();
268
+ this.fire("elepath_toggle", { path, name, legend, enabled })
269
+ }
270
+ });
271
+ },
272
+
273
+ /**
274
+ * Initialize "ruler".
275
+ */
276
+ _initRuler() {
277
+ if (!this.options.ruler) return;
278
+
279
+ // const yMax = this._height();
280
+ const formatNum = d3.format(".0f");
281
+ const drag = this._drag;
282
+
283
+ const label = (e, d) => {
284
+ let yMax = this._height();
285
+ let y = this._ruler.data()[0].y;
286
+ if (y >= yMax || y <= 0) this._ruler.select(".horizontal-drag-label").text('');
287
+ this._hideDiagramIndicator();
288
+ };
289
+
290
+ const position = (e, d) => {
291
+ let yCoord = d3.pointers(e, this._area.node())[0][1];
292
+ let yMax = this._height();
293
+ let y = _.clamp(yCoord, [0, yMax]);
294
+
295
+ this._ruler
296
+ .data([L.extend(this._ruler.data()[0], { y: y })])
297
+ .attr("transform", d => "translate(" + d.x + "," + d.y + ")")
298
+ .classed('active', y < yMax);
299
+
300
+ this._container
301
+ .select(".horizontal-drag-label")
302
+ .text(formatNum(this._y.invert(y)) + " " + (this.options.imperial ? 'ft' : 'm'));
303
+
304
+ this.fire('ruler_filter', { coords: yCoord < yMax && yCoord > 0 ? this._findCoordsForY(yCoord) : [] });
305
+ }
306
+
307
+ drag
308
+ .on("start end", label)
309
+ .on("drag", position);
310
+
311
+ this._ruler.call(drag);
312
+
313
+ },
314
+
315
+ /**
316
+ * Initialize x and y scales
317
+ */
318
+ _initScale() {
319
+ let opts = this.options;
320
+
321
+ this._registerAxisScale({
322
+ axis : 'x',
323
+ position: 'bottom',
324
+ attr : opts.xAttr,
325
+ min : opts.xAxisMin,
326
+ max : opts.xAxisMax,
327
+ name : 'distance'
328
+ });
329
+
330
+ this._registerAxisScale({
331
+ axis : 'y',
332
+ position : 'left',
333
+ attr : opts.yAttr,
334
+ min : opts.yAxisMin,
335
+ max : opts.yAxisMax,
336
+ name : 'altitude'
337
+ });
338
+
339
+ this._x = this._scales.distance;
340
+ this._y = this._scales.altitude;
341
+ },
342
+
343
+ _registerAreaPath(props) {
344
+ if (props.scale == 'y') props.scale = this._y;
345
+ else if (props.scale == 'x') props.scale = this._x;
346
+
347
+ let opts = this.options;
348
+
349
+ if (!props.xAttr) props.xAttr = opts.xAttr;
350
+ if (!props.yAttr) props.yAttr = opts.yAttr;
351
+ if (typeof props.preferCanvas === "undefined") props.preferCanvas = opts.preferCanvas;
352
+
353
+ // Save paths in memory for latter usage
354
+ this._paths[props.name] = D3.Path(props);
355
+ this._props.areas[props.name] = props;
356
+
357
+ if (opts.legend) {
358
+ this._props.legendItems[props.name] = {
359
+ name : props.name,
360
+ label : props.label,
361
+ color : props.color,
362
+ className: props.className,
363
+ path : this._paths[props.name]
364
+ };
365
+ }
366
+
367
+ },
368
+
369
+ _registerAxisGrid(props) {
370
+ if (props.scale == 'y') props.scale = this._y;
371
+ else if (props.scale == 'x') props.scale = this._x;
372
+
373
+ this._props.grids[props.name || props.axis] = props;
374
+ },
375
+
376
+ _registerAxisScale(props) {
377
+ if (props.scale == 'y') props.scale = this._y;
378
+ else if (props.scale == 'x') props.scale = this._x;
379
+
380
+ let opts = this.options;
381
+ let scale = props.scale;
382
+
383
+ if (typeof this._scales[props.name] === 'function') {
384
+ props.scale = this._scales[props.name]; // retrieve cached scale
385
+ } else if (typeof scale !== 'function') {
386
+ scale = L.extend({
387
+ data : this._data,
388
+ forceBounds: opts.forceAxisBounds
389
+ }, scale);
390
+
391
+ scale.attr = scale.attr || props.name;
392
+
393
+ let domain = this._domains[props.name] = D3.Domain(props);
394
+ let range = this._ranges[props.name] = D3.Range(props);
395
+ scale.range = scale.range || range(this._width(),this._height());
396
+ scale.domain = scale.domain || domain(this._data);
397
+
398
+ this._props.scales[props.name] = scale;
399
+
400
+ props.scale = this._scales[props.name] = D3.Scale(scale);
401
+ }
402
+
403
+ if (!props.ticks) {
404
+ if (props.axis == 'x') props.ticks = this._xTicks.bind(this);
405
+ else if (props.axis == 'y') props.ticks = this._yTicks.bind(this);
406
+ }
407
+
408
+ this._props.axes[props.name] = props;
409
+
410
+ if (props.name == this.options.yScale) this._y = scale;
411
+ if (props.name == this.options.xScale) this._x = scale;
412
+
413
+ return scale;
414
+ },
415
+
416
+ /**
417
+ * Add a point of interest over the chart
418
+ */
419
+ _registerCheckPoint(point) {
420
+ if (!this._data.length) return;
421
+
422
+ const {xAttr, yAttr} = this.options;
423
+ let item, x, y;
424
+
425
+ if (point.latlng) {
426
+ item = this._data[this._findIndexForLatLng(point.latlng)];
427
+ x = this._x(item[xAttr]);
428
+ y = this._y(item[yAttr]);
429
+ } else if (!isNaN(point[xAttr])) {
430
+ x = this._x(point[xAttr]);
431
+ item = this._data[this._findIndexForXCoord(x)]
432
+ y = this._y(item[yAttr]);
433
+ }
434
+
435
+ this._point.call(D3.CheckPoint({
436
+ point: point,
437
+ width: this._width(),
438
+ height: this._height(),
439
+ x: x,
440
+ y: y,
441
+ }));
442
+ },
443
+
444
+ _registerTooltip(props) {
445
+ props.order = props.order ?? 1000;
446
+ this._props.tooltipItems[props.name] = props;
447
+ },
448
+
449
+ _updateArea() {
450
+ // Reset and update chart profiles
451
+ this._context.clearRect(0, 0, this._width(), this._height());
452
+ _.each(this._paths, (path, i) => !path.classed('leaflet-hidden') && this._drawPath(i));
453
+ },
454
+
455
+ _updateAxis() {
456
+
457
+ let opts = this.options;
458
+
459
+ const gridOpts = {
460
+ width : this._width(),
461
+ height : this._height(),
462
+ tickFormat: "",
463
+ };
464
+
465
+ const axesOpts = {
466
+ width : this._width(),
467
+ height : this._height(),
468
+ };
469
+
470
+ // Reset grids
471
+ this._grid.selectAll('g').remove();
472
+ _.each(this._props.grids, (grid, i) => {
473
+ if (opts[i] !== false && opts[i] !== 'summary') {
474
+ this._grid.call(D3.Grid(L.extend({}, gridOpts, grid)))
475
+ }
476
+ });
477
+
478
+ // Rest axis
479
+ this._axis.selectAll('g').remove();
480
+ _.each(this._props.axes, (axis, i) => {
481
+ if (opts[i] !== false && opts[i] !== 'summary') {
482
+ this._axis.call(D3.Axis(L.extend({}, axesOpts, axis)));
483
+ }
484
+ });
485
+
486
+ // Adjust axis scale positions
487
+ this._axis
488
+ .selectAll('.y.axis.right')
489
+ .each((d, i, n) => {
490
+
491
+ let axis = d3.select(n[i]);
492
+ let transform = axis.attr('transform');
493
+ let translate = transform.substring(transform.indexOf("(") + 1, transform.indexOf(")")).split(",");
494
+
495
+ axis.attr('transform', 'translate(' + (+translate[0] + (i * 40)) + ',' + translate[1] + ')')
496
+
497
+ if (i > 0) {
498
+ axis.select(':scope > path') .attr('opacity', 0.25);
499
+ axis.selectAll(':scope > .tick line').attr('opacity', 0.75);
500
+ }
501
+
502
+ });
503
+
504
+ },
505
+
506
+ _updateClipper() {
507
+ const { xAttr, margins } = this.options;
508
+ const data = this._data;
509
+
510
+ this._zoom
511
+ .scaleExtent([1, 10])
512
+ .extent([
513
+ [margins.left, 0],
514
+ [this._width() - margins.right, this._height()]
515
+ ])
516
+ .translateExtent([
517
+ [margins.left, -Infinity],
518
+ [this._width() - margins.right, Infinity]
519
+ ]);
520
+
521
+ // Apply svg mask on multi-track segments
522
+ this._mask.selectAll(".gap").remove()
523
+ this._maskGaps.forEach((d, i) => {
524
+ if (i >= this._maskGaps.length - 2) return;
525
+ let x1 = this._x(data[this._findIndexForLatLng(data[this._maskGaps[i]].latlng)][xAttr]);
526
+ let x2 = this._x(data[this._findIndexForLatLng(data[this._maskGaps[i + 1]].latlng)][xAttr]);
527
+ this._mask
528
+ .append("rect")
529
+ .attr("x", x1)
530
+ .attr("y", 0)
531
+ .attr("width", x2 - x1 )
532
+ .attr("height", this._height())
533
+ .attr('class', 'gap')
534
+ .attr('fill-opacity', '0.8')
535
+ .attr("fill", 'black'); // hide = black (mask)
536
+ });
537
+ },
538
+
539
+ _updateLegend: function () {
540
+ let xAxesB = this._axis.selectAll('.x.axis.bottom').nodes().length;
541
+
542
+ // Get legend items
543
+ let items = Object.keys(this._paths);
544
+
545
+ // Calculate legend item positions
546
+ let n = items.length;
547
+ let v = Array(Math.floor(n / 2)).fill(null).map((d, i) => (i + 1) * 2 - (1 - Math.sign(n % 2)));
548
+ let rev = v.slice().reverse().map((d) => -(d));
549
+
550
+ // push a fake element to handle center alignment (odd numbers)
551
+ if (n % 2 !== 0) {
552
+ rev.push(0);
553
+ }
554
+ v = rev.concat(v);
555
+
556
+ // Reset legend items
557
+ this._legend.selectAll('g').remove();
558
+
559
+ // Render legend items
560
+ _.each(this._props.legendItems, (legend) => {
561
+ this._legend.append("g").call(
562
+ D3.LegendItem(L.extend({
563
+ width : this._width(),
564
+ height : this._height(),
565
+ margins: this.options.margins,
566
+ }, legend))
567
+ );
568
+ });
569
+
570
+ // Render legend item switcher
571
+ if (n > 1) {
572
+ this._legend.append("g").call(
573
+ D3.LegendSmall({
574
+ width : this._width(),
575
+ height : this._height(),
576
+ items : items,
577
+ onClick: (selected) => {
578
+ _.each(items, name => this._togglePath(name, selected == name, true));
579
+ this._updateArea();
580
+ }
581
+ })
582
+ );
583
+ }
584
+
585
+ _.each(items, (name, i) => {
586
+ // Adjust legend item positions
587
+ this._legend.select('[data-name=' + name + ']').attr("transform", "translate(" + (v[i] * 55) + ", " + (xAxesB * 2) + ")");
588
+ // Set initial state (disabled controls)
589
+ this._togglePath(name, !(name in this.options && this.options[name] == 'disabled'), true);
590
+ });
591
+
592
+ },
593
+
594
+ _updateMargins() {
595
+ // Get chart margins
596
+ let xAxesB = this._axis.selectAll('.x.axis.bottom').nodes().length;
597
+ let xAxesL = this._axis.selectAll('.y.axis.left').nodes().length;
598
+ let xAxesR = this._axis.selectAll('.y.axis.right').nodes().length;
599
+ let marginB = 60 + (xAxesB * 2);
600
+ let marginL = 10 + (xAxesL * 30);
601
+ let marginR = 40 + (xAxesR * 30);
602
+
603
+ let marginsUpdated = false
604
+
605
+ // Adjust right margin
606
+ if (xAxesR && this.options.margins.right < marginR) {
607
+ this.options.margins.right = marginR;
608
+ marginsUpdated = true;
609
+ }
610
+
611
+ // Adjust left margin
612
+ if (xAxesL && this.options.margins.left < marginL) {
613
+ this.options.margins.left = marginL;
614
+ marginsUpdated = true;
615
+ }
616
+
617
+ // Adjust bottom margin
618
+ if (xAxesB && this.options.margins.bottom < marginB) {
619
+ this.options.margins.bottom = marginB;
620
+ marginsUpdated = true;
621
+ }
622
+
623
+ if (marginsUpdated) {
624
+ this.fire('margins_updated');
625
+ }
626
+ },
627
+
628
+ _updateScale() {
629
+ if (this.zooming) return { x: this._x, y: this._y };
630
+
631
+ for (let i in this._scales) {
632
+ this._scales[i]
633
+ .domain(this._domains[i](this._data))
634
+ .range(this._ranges[i](this._width(), this._height()))
635
+ }
636
+
637
+ return { x: this._x, y: this._y };
638
+ },
639
+
640
+ /**
641
+ * Calculates chart width.
642
+ */
643
+ _width() {
644
+ if (this._chart) this._chart._width;
645
+ const { width, margins } = this.options;
646
+ return width - margins.left - margins.right;
647
+ },
648
+
649
+ /**
650
+ * Calculates chart height.
651
+ */
652
+ _height() {
653
+ if (this._chart) return this._chart._height;
654
+ const { height, margins } = this.options;
655
+ return height - margins.top - margins.bottom;
656
+ },
657
+
658
+ /*
659
+ * Finds data entries above a given y-elevation value and returns geo-coordinates
660
+ */
661
+ _findCoordsForY(y) {
662
+ let data = this._data;
663
+ let z = this._y.invert(y);
664
+
665
+ // save indexes of elevation values above the horizontal line
666
+ const list = data.reduce((array, item, index) => {
667
+ if (item[this.options.yAttr] >= z) array.push(index);
668
+ return array;
669
+ }, []);
670
+
671
+ let start = 0;
672
+ let next;
673
+
674
+ // split index list into blocks of coordinates
675
+ const coords = list.reduce((array, _, curr) => {
676
+ next = curr + 1;
677
+ if (list[next] !== list[curr] + 1 || next === list.length) {
678
+ array.push(
679
+ list
680
+ .slice(start, next)
681
+ .map(i => data[i].latlng)
682
+ );
683
+ start = next;
684
+ }
685
+ return array;
686
+ }, []);
687
+
688
+ return coords;
689
+ },
690
+
691
+ /*
692
+ * Finds a data entry for a given x-coordinate of the diagram
693
+ */
694
+ _findIndexForXCoord(x) {
695
+ return d3
696
+ .bisector(d => d[this.options.xAttr])
697
+ .left(this._data || [0, 1], this._x.invert(x));
698
+ },
699
+
700
+ /*
701
+ * Finds a data entry for a given latlng of the map
702
+ */
703
+ _findIndexForLatLng(latlng) {
704
+ let result = null;
705
+ let d = Infinity;
706
+ this._data.forEach((item, index) => {
707
+ let dist = latlng.distanceTo(item.latlng);
708
+ if (dist < d) {
709
+ d = dist;
710
+ result = index;
711
+ }
712
+ });
713
+ return result;
714
+ },
715
+
716
+ /*
717
+ * Removes the drag rectangle and zoms back to the total extent of the data.
718
+ */
719
+ _resetDrag() {
720
+ if (this._chart.panes.brush.select(".selection").attr('width')) {
721
+ this._chart.panes.brush.call(this._brush.clear);
722
+ this._hideDiagramIndicator();
723
+ this.fire('reset_drag');
724
+ }
725
+ },
726
+
727
+ _resetZoom() {
728
+ if (this._zoom) {
729
+ this._zoom.transform(this._chart.svg, d3.zoomIdentity);
730
+ }
731
+ },
732
+
733
+ /**
734
+ * Display distance and altitude level ("focus-rect").
735
+ */
736
+ _showDiagramIndicator(item, xCoordinate) {
737
+ this._tooltip
738
+ .attr("display", null)
739
+ .call(D3.Tooltip({
740
+ xCoord: xCoordinate,
741
+ yCoord: this._y(item[this.options.yAttr]),
742
+ height: this._height(),
743
+ width : this._width(),
744
+ labels: this._props.tooltipItems,
745
+ item: item
746
+ }));
747
+ },
748
+
749
+ _togglePath(name, enabled = true, lazy = false) {
750
+ let path = this._paths[name];
751
+ let legend = this._container.select('.legend [data-name=' + name + ']');
752
+ path.classed('leaflet-hidden', !enabled);
753
+ legend.select('text').style('text-decoration-line', enabled ? '' : 'line-through');
754
+ legend.select('rect').style('fill-opacity', enabled ? '': '0');
755
+ // Apply d3-zoom (bind <clipPath> mask)
756
+ if (this._mask) {
757
+ path.attr('mask', 'url(#' + this._mask.attr('id') + ')');
758
+ }
759
+ if (!lazy) {
760
+ this._updateArea();
761
+ }
762
+ },
763
+
764
+ _hideDiagramIndicator() {
765
+ this._tooltip.attr("display", 'none');
766
+ },
767
+
768
+ /**
769
+ * Calculate chart xTicks
770
+ */
771
+ _xTicks() {
772
+ if (this.__xTicks) this.__xTicks = this.options.xTicks;
773
+ return this.__xTicks || Math.round(this._width() / 75);
774
+ },
775
+
776
+ /**
777
+ * Calculate chart yTicks
778
+ */
779
+ _yTicks() {
780
+ if (this.__yTicks) this.__yTicks = this.options.yTicks;
781
+ return this.__yTicks || Math.round(this._height() / 30);
782
+ }
783
+ });