summaryrefslogtreecommitdiff
path: root/assets/static/vendor/leaflet-elevation/src/components
diff options
context:
space:
mode:
authorDennis Fink2026-07-23 00:18:21 +0200
committerDennis Fink2026-07-23 00:18:21 +0200
commit1fe1d13c600b7dfe421b3b99b833b7d5d1232724 (patch)
tree2e8636160cc1719c907be56f5ef8cbc7e0f10a53 /assets/static/vendor/leaflet-elevation/src/components
parentca77de06be474dfd712402c327dc436864d233a3 (diff)
downloaddennisfink.me-1fe1d13c600b7dfe421b3b99b833b7d5d1232724.tar.gz
dennisfink.me-1fe1d13c600b7dfe421b3b99b833b7d5d1232724.zip
Add GPX trail maps to blog posts
This adds a trail flow block so posts can include attached GPX files as interactive maps with route display, elevation charts, activity summaries, and start/finish markers. The trail assets are loaded only when needed, keeping the shared templates clean while vendoring the Leaflet dependencies required for offline, self-hosted map rendering.
Diffstat (limited to '')
-rw-r--r--assets/static/vendor/leaflet-elevation/src/components/chart.js783
-rw-r--r--assets/static/vendor/leaflet-elevation/src/components/d3.js649
-rw-r--r--assets/static/vendor/leaflet-elevation/src/components/marker.js96
-rw-r--r--assets/static/vendor/leaflet-elevation/src/components/summary.js43
4 files changed, 1571 insertions, 0 deletions
diff --git a/assets/static/vendor/leaflet-elevation/src/components/chart.js b/assets/static/vendor/leaflet-elevation/src/components/chart.js
new file mode 100644
index 0000000..b0b5458
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/components/chart.js
@@ -0,0 +1,783 @@
+import * as D3 from './d3.js';
+
+const _ = L.Control.Elevation.Utils;
+
+export var Chart = L.Control.Elevation.Chart = L.Class.extend({
+
+ includes: L.Evented ? L.Evented.prototype : L.Mixin.Events,
+
+ initialize(opts, control) {
+ this.options = opts;
+ this.control = control;
+
+ this._data = control._data || [];
+
+ // cache registered components
+ this._props = {
+ scales : {},
+ paths : {},
+ areas : {},
+ grids : {},
+ axes : {},
+ legendItems : {},
+ tooltipItems: {},
+ };
+
+ this._scales = {};
+ this._domains = {};
+ this._ranges = {};
+ this._paths = {};
+
+ this._brushEnabled = opts.dragging;
+ this._zoomEnabled = opts.zooming;
+
+ opts.xTicks = this._xTicks();
+ opts.yTicks = this._yTicks();
+
+ let chart = this._chart = D3.Chart(opts);
+
+ // SVG Container
+ this._container = chart.svg;
+
+ // Panes
+ this._grid = chart.pane('grid');
+ this._area = chart.pane('area');
+ this._point = chart.pane('point');
+ this._axis = chart.pane('axis');
+ this._legend = chart.pane('legend');
+ this._tooltip = chart.pane('tooltip');
+ this._ruler = chart.pane('ruler');
+
+ // Scales
+ this._initScale();
+
+ // Helpers
+ this._mask = chart.get('mask');
+ this._context = chart.get('context');
+ this._brush = chart.get('brush');
+
+ this._zoom = d3.zoom();
+ this._drag = d3.drag();
+
+ // Interactions
+ this._initInteractions();
+
+ // svg.on('resize', (e)=>console.log(e.detail));
+
+ // Handle multi-track segments
+ this._maskGaps = [];
+ control.on('eletrack_added', ({index}) => {
+ this._maskGaps.push(index);
+ control.once('elepoint_added', ({index}) => this._maskGaps.push(index));
+ });
+
+ },
+
+ update(props) {
+ if (props) {
+ if (props.data) this._data = props.data;
+ if (props.options) this.options = props.options;
+ }
+
+ this.options.xTicks = this._xTicks();
+ this.options.yTicks = this._yTicks();
+
+ this._updateScale();
+ this._updateAxis();
+ this._updateMargins();
+ this._updateLegend();
+ this._updateClipper();
+ this._updateArea();
+
+ return this;
+ },
+
+ render() {
+ return container => container.append(() => this._container.node());
+ },
+
+ clear() {
+ this._resetDrag();
+ this._hideDiagramIndicator()
+ this._area.selectAll('path').attr("d", "M0 0");
+ this._context.clearRect(0, 0, this._width(), this._height());
+ this._mask.selectAll(".gap").remove();
+ this._maskGaps = [];
+
+ // if (this._path) {
+ // this._x.domain([0, 1]);
+ // this._y.domain([0, 1]);
+ // }
+ },
+
+ _drawPath(name) {
+ let path = this._paths[name];
+ let area = this._props.areas[name];
+
+ path.datum(this._data).attr("d",
+ D3.Area(
+ L.extend({}, area, {
+ width : this._width(),
+ height : this._height(),
+ scaleX : this._scales[area.scaleX],
+ scaleY : this._scales[area.scaleY]
+ })
+ )
+ );
+
+ if (!path.classed('leaflet-hidden')) {
+ this.options.preferCanvas
+ ? _.drawCanvas(this._context, path)
+ : _.append(this._area.node(), path.node());
+ }
+ },
+
+ _hasActiveLayers() {
+ const paths = this._paths;
+ for (var i in paths) {
+ if (!paths[i].classed('leaflet-hidden')) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Initialize "d3-brush".
+ */
+ _initBrush(e) {
+ const brush = ({selection}) => {
+ if (selection && this._data.length) {
+ let start = this._findIndexForXCoord(selection[0]);
+ let end = this._findIndexForXCoord(selection[1]);
+ this.fire('dragged', { dragstart: this._data[start], dragend: this._data[end] });
+ }
+ }
+
+ const focus = (e) => {
+ if (this._data.length && (e.type != 'brush' /*|| e.sourceEvent*/)) {
+ let rect = this._chart.panes.brush.select('.overlay').node();
+ let coords = d3.pointers(e, rect)[0];
+ let xCoord = _.clamp(coords[0], [0, this._width()]); // Clamps cursor position to chart's bounds (see issues: #270, #301)
+ let item = this._data[this._findIndexForXCoord(xCoord)];
+
+ this.fire("mouse_move", { item: item, xCoord: xCoord });
+ }
+ };
+
+ this._brush
+ .filter(({shiftKey, button}) => !shiftKey && !button && this._brushEnabled)
+ .on("end.update", brush)
+ .on("brush.update", focus);
+
+ this._chart.panes.brush
+ .on("mouseenter.focus touchstart.focus", this.fire.bind(this, "mouse_enter"))
+ .on("mouseout.focus touchend.focus", this.fire.bind(this, "mouse_out") )
+ .on("mousemove.focus touchmove.focus", focus );
+
+ },
+
+ /**
+ * Initialize "d3-zoom"
+ */
+ _initClipper() {
+ let svg = this._container;
+ let margin = this.options.margins;
+
+ const zoom = this._zoom;
+
+ const onStart = ({transform, sourceEvent}) => {
+ if (sourceEvent && sourceEvent.type == "mousedown") svg.style('cursor', 'grabbing');
+ if (transform.k == 1 && transform.x == 0) {
+ this._container.classed('zoomed', true);
+ // Apply d3-zoom and bind <mask>
+ if (this._mask) {
+ this._point.attr('mask', 'url(#' + this._mask.attr('id') + ')');
+ }
+ }
+ this.zooming = true;
+ };
+
+ const onEnd = ({transform}) => {
+ if (transform.k ==1 && transform.x == 0) {
+ this._container.classed('zoomed', false);
+ // Reset d3-zoom and remove <mask>
+ if (this._mask) {
+ this._point.attr('mask', null);
+ }
+ }
+ this.zooming = false;
+ svg.style('cursor', '');
+ };
+
+ const onZoom = ({transform, sourceEvent}) => {
+ // TODO: find a faster way to redraw the chart.
+ this.zooming = false;
+ this._updateScale(); // hacky way for restoring x scale when zooming out
+ this.zooming = true;
+ this._scales.distance = this._x = transform.rescaleX(this._x); // calculate x scale at zoom level
+ if (this._scales.time) this._scales.time = transform.rescaleX(this._scales.time); // calculate x scale at zoom level
+ this._resetDrag();
+ if (sourceEvent && sourceEvent.type == "mousemove") {
+ this._hideDiagramIndicator();
+ }
+ this.fire('zoom');
+ };
+
+ zoom
+ .scaleExtent([1, 10])
+ .extent([
+ [margin.left, 0],
+ [this._width() - margin.right, this._height()]
+ ])
+ .translateExtent([
+ [margin.left, -Infinity],
+ [this._width() - margin.right, Infinity]
+ ])
+ .filter(({shiftKey, buttons}) => (shiftKey || buttons == 4) && this._zoomEnabled)
+ .on("start", onStart)
+ .on("end", onEnd)
+ .on("zoom", onZoom);
+
+ svg.call(zoom); // add zoom functionality to "svg" group
+
+ // d3.select("body").on("keydown.grabzoom keyup.grabzoom", (e) => svg.style('cursor', e.shiftKey ? 'move' : ''));
+
+ },
+
+ _initInteractions() {
+ this._initBrush();
+ this._initRuler();
+ this._initClipper();
+ this._initLegend();
+ },
+
+ /**
+ * Toggle chart data on legend click
+ */
+ _initLegend() {
+ this._container.on('legend_clicked', ({detail}) => {
+ let { path, legend, name, enabled } = detail;
+ if (path) {
+ let label = _.select('text', legend);
+ let rect = _.select('rect', legend);
+ _.toggleStyle(label, 'text-decoration-line', 'line-through', enabled);
+ _.toggleStyle(rect, 'fill-opacity', '0', enabled);
+ _.toggleClass(path, 'leaflet-hidden', enabled);
+ this._updateArea();
+ this.fire("elepath_toggle", { path, name, legend, enabled })
+ }
+ });
+ },
+
+ /**
+ * Initialize "ruler".
+ */
+ _initRuler() {
+ if (!this.options.ruler) return;
+
+ // const yMax = this._height();
+ const formatNum = d3.format(".0f");
+ const drag = this._drag;
+
+ const label = (e, d) => {
+ let yMax = this._height();
+ let y = this._ruler.data()[0].y;
+ if (y >= yMax || y <= 0) this._ruler.select(".horizontal-drag-label").text('');
+ this._hideDiagramIndicator();
+ };
+
+ const position = (e, d) => {
+ let yCoord = d3.pointers(e, this._area.node())[0][1];
+ let yMax = this._height();
+ let y = _.clamp(yCoord, [0, yMax]);
+
+ this._ruler
+ .data([L.extend(this._ruler.data()[0], { y: y })])
+ .attr("transform", d => "translate(" + d.x + "," + d.y + ")")
+ .classed('active', y < yMax);
+
+ this._container
+ .select(".horizontal-drag-label")
+ .text(formatNum(this._y.invert(y)) + " " + (this.options.imperial ? 'ft' : 'm'));
+
+ this.fire('ruler_filter', { coords: yCoord < yMax && yCoord > 0 ? this._findCoordsForY(yCoord) : [] });
+ }
+
+ drag
+ .on("start end", label)
+ .on("drag", position);
+
+ this._ruler.call(drag);
+
+ },
+
+ /**
+ * Initialize x and y scales
+ */
+ _initScale() {
+ let opts = this.options;
+
+ this._registerAxisScale({
+ axis : 'x',
+ position: 'bottom',
+ attr : opts.xAttr,
+ min : opts.xAxisMin,
+ max : opts.xAxisMax,
+ name : 'distance'
+ });
+
+ this._registerAxisScale({
+ axis : 'y',
+ position : 'left',
+ attr : opts.yAttr,
+ min : opts.yAxisMin,
+ max : opts.yAxisMax,
+ name : 'altitude'
+ });
+
+ this._x = this._scales.distance;
+ this._y = this._scales.altitude;
+ },
+
+ _registerAreaPath(props) {
+ if (props.scale == 'y') props.scale = this._y;
+ else if (props.scale == 'x') props.scale = this._x;
+
+ let opts = this.options;
+
+ if (!props.xAttr) props.xAttr = opts.xAttr;
+ if (!props.yAttr) props.yAttr = opts.yAttr;
+ if (typeof props.preferCanvas === "undefined") props.preferCanvas = opts.preferCanvas;
+
+ // Save paths in memory for latter usage
+ this._paths[props.name] = D3.Path(props);
+ this._props.areas[props.name] = props;
+
+ if (opts.legend) {
+ this._props.legendItems[props.name] = {
+ name : props.name,
+ label : props.label,
+ color : props.color,
+ className: props.className,
+ path : this._paths[props.name]
+ };
+ }
+
+ },
+
+ _registerAxisGrid(props) {
+ if (props.scale == 'y') props.scale = this._y;
+ else if (props.scale == 'x') props.scale = this._x;
+
+ this._props.grids[props.name || props.axis] = props;
+ },
+
+ _registerAxisScale(props) {
+ if (props.scale == 'y') props.scale = this._y;
+ else if (props.scale == 'x') props.scale = this._x;
+
+ let opts = this.options;
+ let scale = props.scale;
+
+ if (typeof this._scales[props.name] === 'function') {
+ props.scale = this._scales[props.name]; // retrieve cached scale
+ } else if (typeof scale !== 'function') {
+ scale = L.extend({
+ data : this._data,
+ forceBounds: opts.forceAxisBounds
+ }, scale);
+
+ scale.attr = scale.attr || props.name;
+
+ let domain = this._domains[props.name] = D3.Domain(props);
+ let range = this._ranges[props.name] = D3.Range(props);
+ scale.range = scale.range || range(this._width(),this._height());
+ scale.domain = scale.domain || domain(this._data);
+
+ this._props.scales[props.name] = scale;
+
+ props.scale = this._scales[props.name] = D3.Scale(scale);
+ }
+
+ if (!props.ticks) {
+ if (props.axis == 'x') props.ticks = this._xTicks.bind(this);
+ else if (props.axis == 'y') props.ticks = this._yTicks.bind(this);
+ }
+
+ this._props.axes[props.name] = props;
+
+ if (props.name == this.options.yScale) this._y = scale;
+ if (props.name == this.options.xScale) this._x = scale;
+
+ return scale;
+ },
+
+ /**
+ * Add a point of interest over the chart
+ */
+ _registerCheckPoint(point) {
+ if (!this._data.length) return;
+
+ const {xAttr, yAttr} = this.options;
+ let item, x, y;
+
+ if (point.latlng) {
+ item = this._data[this._findIndexForLatLng(point.latlng)];
+ x = this._x(item[xAttr]);
+ y = this._y(item[yAttr]);
+ } else if (!isNaN(point[xAttr])) {
+ x = this._x(point[xAttr]);
+ item = this._data[this._findIndexForXCoord(x)]
+ y = this._y(item[yAttr]);
+ }
+
+ this._point.call(D3.CheckPoint({
+ point: point,
+ width: this._width(),
+ height: this._height(),
+ x: x,
+ y: y,
+ }));
+ },
+
+ _registerTooltip(props) {
+ props.order = props.order ?? 1000;
+ this._props.tooltipItems[props.name] = props;
+ },
+
+ _updateArea() {
+ // Reset and update chart profiles
+ this._context.clearRect(0, 0, this._width(), this._height());
+ _.each(this._paths, (path, i) => !path.classed('leaflet-hidden') && this._drawPath(i));
+ },
+
+ _updateAxis() {
+
+ let opts = this.options;
+
+ const gridOpts = {
+ width : this._width(),
+ height : this._height(),
+ tickFormat: "",
+ };
+
+ const axesOpts = {
+ width : this._width(),
+ height : this._height(),
+ };
+
+ // Reset grids
+ this._grid.selectAll('g').remove();
+ _.each(this._props.grids, (grid, i) => {
+ if (opts[i] !== false && opts[i] !== 'summary') {
+ this._grid.call(D3.Grid(L.extend({}, gridOpts, grid)))
+ }
+ });
+
+ // Rest axis
+ this._axis.selectAll('g').remove();
+ _.each(this._props.axes, (axis, i) => {
+ if (opts[i] !== false && opts[i] !== 'summary') {
+ this._axis.call(D3.Axis(L.extend({}, axesOpts, axis)));
+ }
+ });
+
+ // Adjust axis scale positions
+ this._axis
+ .selectAll('.y.axis.right')
+ .each((d, i, n) => {
+
+ let axis = d3.select(n[i]);
+ let transform = axis.attr('transform');
+ let translate = transform.substring(transform.indexOf("(") + 1, transform.indexOf(")")).split(",");
+
+ axis.attr('transform', 'translate(' + (+translate[0] + (i * 40)) + ',' + translate[1] + ')')
+
+ if (i > 0) {
+ axis.select(':scope > path') .attr('opacity', 0.25);
+ axis.selectAll(':scope > .tick line').attr('opacity', 0.75);
+ }
+
+ });
+
+ },
+
+ _updateClipper() {
+ const { xAttr, margins } = this.options;
+ const data = this._data;
+
+ this._zoom
+ .scaleExtent([1, 10])
+ .extent([
+ [margins.left, 0],
+ [this._width() - margins.right, this._height()]
+ ])
+ .translateExtent([
+ [margins.left, -Infinity],
+ [this._width() - margins.right, Infinity]
+ ]);
+
+ // Apply svg mask on multi-track segments
+ this._mask.selectAll(".gap").remove()
+ this._maskGaps.forEach((d, i) => {
+ if (i >= this._maskGaps.length - 2) return;
+ let x1 = this._x(data[this._findIndexForLatLng(data[this._maskGaps[i]].latlng)][xAttr]);
+ let x2 = this._x(data[this._findIndexForLatLng(data[this._maskGaps[i + 1]].latlng)][xAttr]);
+ this._mask
+ .append("rect")
+ .attr("x", x1)
+ .attr("y", 0)
+ .attr("width", x2 - x1 )
+ .attr("height", this._height())
+ .attr('class', 'gap')
+ .attr('fill-opacity', '0.8')
+ .attr("fill", 'black'); // hide = black (mask)
+ });
+ },
+
+ _updateLegend: function () {
+ let xAxesB = this._axis.selectAll('.x.axis.bottom').nodes().length;
+
+ // Get legend items
+ let items = Object.keys(this._paths);
+
+ // Calculate legend item positions
+ let n = items.length;
+ let v = Array(Math.floor(n / 2)).fill(null).map((d, i) => (i + 1) * 2 - (1 - Math.sign(n % 2)));
+ let rev = v.slice().reverse().map((d) => -(d));
+
+ // push a fake element to handle center alignment (odd numbers)
+ if (n % 2 !== 0) {
+ rev.push(0);
+ }
+ v = rev.concat(v);
+
+ // Reset legend items
+ this._legend.selectAll('g').remove();
+
+ // Render legend items
+ _.each(this._props.legendItems, (legend) => {
+ this._legend.append("g").call(
+ D3.LegendItem(L.extend({
+ width : this._width(),
+ height : this._height(),
+ margins: this.options.margins,
+ }, legend))
+ );
+ });
+
+ // Render legend item switcher
+ if (n > 1) {
+ this._legend.append("g").call(
+ D3.LegendSmall({
+ width : this._width(),
+ height : this._height(),
+ items : items,
+ onClick: (selected) => {
+ _.each(items, name => this._togglePath(name, selected == name, true));
+ this._updateArea();
+ }
+ })
+ );
+ }
+
+ _.each(items, (name, i) => {
+ // Adjust legend item positions
+ this._legend.select('[data-name=' + name + ']').attr("transform", "translate(" + (v[i] * 55) + ", " + (xAxesB * 2) + ")");
+ // Set initial state (disabled controls)
+ this._togglePath(name, !(name in this.options && this.options[name] == 'disabled'), true);
+ });
+
+ },
+
+ _updateMargins() {
+ // Get chart margins
+ let xAxesB = this._axis.selectAll('.x.axis.bottom').nodes().length;
+ let xAxesL = this._axis.selectAll('.y.axis.left').nodes().length;
+ let xAxesR = this._axis.selectAll('.y.axis.right').nodes().length;
+ let marginB = 60 + (xAxesB * 2);
+ let marginL = 10 + (xAxesL * 30);
+ let marginR = 40 + (xAxesR * 30);
+
+ let marginsUpdated = false
+
+ // Adjust right margin
+ if (xAxesR && this.options.margins.right < marginR) {
+ this.options.margins.right = marginR;
+ marginsUpdated = true;
+ }
+
+ // Adjust left margin
+ if (xAxesL && this.options.margins.left < marginL) {
+ this.options.margins.left = marginL;
+ marginsUpdated = true;
+ }
+
+ // Adjust bottom margin
+ if (xAxesB && this.options.margins.bottom < marginB) {
+ this.options.margins.bottom = marginB;
+ marginsUpdated = true;
+ }
+
+ if (marginsUpdated) {
+ this.fire('margins_updated');
+ }
+ },
+
+ _updateScale() {
+ if (this.zooming) return { x: this._x, y: this._y };
+
+ for (let i in this._scales) {
+ this._scales[i]
+ .domain(this._domains[i](this._data))
+ .range(this._ranges[i](this._width(), this._height()))
+ }
+
+ return { x: this._x, y: this._y };
+ },
+
+ /**
+ * Calculates chart width.
+ */
+ _width() {
+ if (this._chart) this._chart._width;
+ const { width, margins } = this.options;
+ return width - margins.left - margins.right;
+ },
+
+ /**
+ * Calculates chart height.
+ */
+ _height() {
+ if (this._chart) return this._chart._height;
+ const { height, margins } = this.options;
+ return height - margins.top - margins.bottom;
+ },
+
+ /*
+ * Finds data entries above a given y-elevation value and returns geo-coordinates
+ */
+ _findCoordsForY(y) {
+ let data = this._data;
+ let z = this._y.invert(y);
+
+ // save indexes of elevation values above the horizontal line
+ const list = data.reduce((array, item, index) => {
+ if (item[this.options.yAttr] >= z) array.push(index);
+ return array;
+ }, []);
+
+ let start = 0;
+ let next;
+
+ // split index list into blocks of coordinates
+ const coords = list.reduce((array, _, curr) => {
+ next = curr + 1;
+ if (list[next] !== list[curr] + 1 || next === list.length) {
+ array.push(
+ list
+ .slice(start, next)
+ .map(i => data[i].latlng)
+ );
+ start = next;
+ }
+ return array;
+ }, []);
+
+ return coords;
+ },
+
+ /*
+ * Finds a data entry for a given x-coordinate of the diagram
+ */
+ _findIndexForXCoord(x) {
+ return d3
+ .bisector(d => d[this.options.xAttr])
+ .left(this._data || [0, 1], this._x.invert(x));
+ },
+
+ /*
+ * Finds a data entry for a given latlng of the map
+ */
+ _findIndexForLatLng(latlng) {
+ let result = null;
+ let d = Infinity;
+ this._data.forEach((item, index) => {
+ let dist = latlng.distanceTo(item.latlng);
+ if (dist < d) {
+ d = dist;
+ result = index;
+ }
+ });
+ return result;
+ },
+
+ /*
+ * Removes the drag rectangle and zoms back to the total extent of the data.
+ */
+ _resetDrag() {
+ if (this._chart.panes.brush.select(".selection").attr('width')) {
+ this._chart.panes.brush.call(this._brush.clear);
+ this._hideDiagramIndicator();
+ this.fire('reset_drag');
+ }
+ },
+
+ _resetZoom() {
+ if (this._zoom) {
+ this._zoom.transform(this._chart.svg, d3.zoomIdentity);
+ }
+ },
+
+ /**
+ * Display distance and altitude level ("focus-rect").
+ */
+ _showDiagramIndicator(item, xCoordinate) {
+ this._tooltip
+ .attr("display", null)
+ .call(D3.Tooltip({
+ xCoord: xCoordinate,
+ yCoord: this._y(item[this.options.yAttr]),
+ height: this._height(),
+ width : this._width(),
+ labels: this._props.tooltipItems,
+ item: item
+ }));
+ },
+
+ _togglePath(name, enabled = true, lazy = false) {
+ let path = this._paths[name];
+ let legend = this._container.select('.legend [data-name=' + name + ']');
+ path.classed('leaflet-hidden', !enabled);
+ legend.select('text').style('text-decoration-line', enabled ? '' : 'line-through');
+ legend.select('rect').style('fill-opacity', enabled ? '': '0');
+ // Apply d3-zoom (bind <clipPath> mask)
+ if (this._mask) {
+ path.attr('mask', 'url(#' + this._mask.attr('id') + ')');
+ }
+ if (!lazy) {
+ this._updateArea();
+ }
+ },
+
+ _hideDiagramIndicator() {
+ this._tooltip.attr("display", 'none');
+ },
+
+ /**
+ * Calculate chart xTicks
+ */
+ _xTicks() {
+ if (this.__xTicks) this.__xTicks = this.options.xTicks;
+ return this.__xTicks || Math.round(this._width() / 75);
+},
+
+ /**
+ * Calculate chart yTicks
+ */
+ _yTicks() {
+ if (this.__yTicks) this.__yTicks = this.options.yTicks;
+ return this.__yTicks || Math.round(this._height() / 30);
+ }
+});
diff --git a/assets/static/vendor/leaflet-elevation/src/components/d3.js b/assets/static/vendor/leaflet-elevation/src/components/d3.js
new file mode 100644
index 0000000..2c35d57
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/components/d3.js
@@ -0,0 +1,649 @@
+export const Area = ({
+ width,
+ height,
+ xAttr,
+ yAttr,
+ scaleX,
+ scaleY,
+ interpolation = "curveLinear"
+}) => {
+ return d3.area()
+ .curve(typeof interpolation === 'string' ? d3[interpolation] : interpolation)
+ .x(d => (d.xDiagCoord = scaleX(d[xAttr])))
+ .y0(height)
+ .y1(d => scaleY(d[yAttr]));
+};
+
+export const Path = ({
+ name,
+ color,
+ strokeColor,
+ strokeOpacity,
+ fillOpacity,
+ className = ''
+}) => {
+ let path = d3.create('svg:path')
+
+ if (name) path.classed(name + ' ' + className, true);
+
+ path.style("pointer-events", "none");
+
+ path
+ .attr("fill", color || '#3366CC')
+ .attr("stroke", strokeColor || '#000')
+ .attr("stroke-opacity", strokeOpacity || '1')
+ .attr("fill-opacity", fillOpacity || '0.8');
+
+ return path;
+};
+
+export const Axis = ({
+ type = "axis",
+ tickSize = 6,
+ tickPadding = 3,
+ position,
+ height,
+ width,
+ axis,
+ scale,
+ ticks,
+ tickFormat,
+ label,
+ labelX,
+ labelY,
+ name = "",
+ onAxisMount,
+}) => {
+ return g => {
+ let [w, h] = [0, 0];
+ if (position == "bottom") h = height;
+ if (position == "right") w = width;
+
+ if (axis == "x" && type == "grid") {
+ tickSize = -height;
+ } else if (axis == "y" && type == "grid") {
+ tickSize = -width;
+ }
+
+ let axisScale = d3["axis" + position.replace(/\b\w/g, l => l.toUpperCase())]()
+ .scale(scale)
+ .ticks(typeof ticks === 'function' ? ticks() : ticks)
+ .tickPadding(tickPadding)
+ .tickSize(tickSize)
+ .tickFormat(tickFormat);
+
+ let axisGroup = g.append("g")
+ .attr("class", [axis, type, position, name].join(" "))
+ .attr("transform", "translate(" + w + "," + h + ")")
+ .call(axisScale);
+
+ if (label) {
+ axisGroup.append("svg:text")
+ .attr("x", labelX)
+ .attr("y", labelY)
+ .text(label);
+ }
+
+ if (onAxisMount) {
+ axisGroup.call(onAxisMount);
+ }
+
+ return axisGroup;
+ };
+};
+
+export const Grid = (props) => {
+ props.type = "grid";
+ return Axis(props);
+};
+
+export const PositionMarker = ({
+ theme,
+ xCoord = 0,
+ yCoord = 0,
+ labels = {},
+ item = {},
+ length = 0,
+}) => {
+ return g => {
+
+ g.attr("class", "height-focus-group");
+
+ let line = g.select('.height-focus.line');
+ let circle = g.select('.height-focus.circle-lower');
+ let text = g.select('.height-focus-label');
+
+ if (!line.node()) line = g.append('svg:line');
+ if (!circle.node()) circle = g.append('svg:circle');
+ if (!text.node()) text = g.append('svg:text');
+
+ if (isNaN(xCoord) || isNaN(yCoord)) return g;
+
+ circle
+ .attr("class", theme + " height-focus circle-lower")
+ .attr("transform", "translate(" + xCoord + "," + yCoord + ")")
+ .attr("r", 6)
+ .attr("cx", 0)
+ .attr("cy", 0);
+
+ line
+ .attr("class", theme + " height-focus line")
+ .attr("x1", xCoord)
+ .attr("x2", xCoord)
+ .attr("y1", yCoord)
+ .attr("y2", length);
+
+ text
+ .attr("class", theme + " height-focus-label")
+ .style("pointer-events", "none")
+ .attr("x", xCoord + 5)
+ .attr("y", length);
+
+ let label;
+
+ Object
+ .keys(labels)
+ .sort((a, b) => labels[a].order - labels[b].order) // TODO: any performance issues?
+ .forEach((i)=> {
+ label = text.select(".height-focus-" + labels[i].name);
+
+ if (!label.size()) {
+ label = text.append("svg:tspan")
+ .attr("class", "height-focus-" + labels[i].name /*+ " " + "order-" + labels[i].order*/)
+ .attr("dy", "1.5em");
+ }
+
+ label.text(typeof labels[i].value !== "function" ? labels[i].value : labels[i].value(item));
+
+ });
+
+ text.select('tspan').attr("dy", text.selectAll('tspan').size() > 1 ? "-1.5em" : "0em" );
+ text.selectAll('tspan').attr("x", xCoord + 5);
+
+ return g;
+ }
+};
+
+export const LegendItem = ({
+ name,
+ label,
+ width,
+ height,
+ margins = {},
+ color,
+ path,
+ className = ''
+}) => {
+ return g => {
+ g
+ .attr("class", "legend-item legend-" + name.toLowerCase())
+ .attr("data-name", name);
+
+ g.on('click.legend', () => d3.select(g.node().ownerSVGElement || g)
+ .dispatch("legend_clicked", {
+ detail: {
+ path: path.node(),
+ name: name,
+ legend: g.node(),
+ enabled: !path.classed('leaflet-hidden'),
+ }
+ })
+ );
+
+ g.append("svg:rect")
+ .attr("x", (width / 2) - 50)
+ .attr("y", height + margins.bottom / 2)
+ .attr("width", 50)
+ .attr("height", 10)
+ .attr("fill", color)
+ .attr("stroke", "#000")
+ .attr("stroke-opacity", "0.5")
+ .attr("fill-opacity", "0.25")
+ .attr("class", className);
+
+ g.append('svg:text')
+ .text(L._(label || name))
+ .attr("x", (width / 2) + 5)
+ .attr("font-size", 10)
+ .style("font-weight", "700")
+ .attr('y', height + margins.bottom / 2)
+ .attr('dy', "0.75em");
+
+ return g;
+ }
+};
+
+export const LegendSmall = ({
+ width,
+ height,
+ items,
+ onClick
+}) => {
+ return g => {
+ let idx = 0;
+
+ g.data([{
+ x: width,
+ y: height + 40
+ }]).attr("transform", d => "translate(" + d.x + "," + d.y + ")").classed('legend-switcher');
+
+ // let label = g.selectAll(".legend-switcher-label") .data([{ idx: 0 }]);
+ let label = g.append('svg:text')
+ .attr("class", "legend-switcher-label")
+ .attr("text-anchor", "end")
+ .attr("x", -25)
+ .attr("y", 5)
+ .on("mousedown", (e, d) => setIdx(L.Util.wrapNum((idx + 1), [0, items.length])));
+
+ let symbol = g.selectAll(".legend-switcher-symbol").data([
+ { type: d3.symbolTriangle, x: 0, y: 3, angle: 0, size: 50, id: "down" },
+ { type: d3.symbolTriangle, x: -13, y: 0, angle: 180, size: 50, id: "up" }
+ ]);
+
+ symbol.exit().remove();
+ label.exit().remove();
+
+ symbol.enter()
+ .append("svg:path")
+ .attr("class", "legend-switcher-symbol")
+ .attr("cursor", 'pointer')
+ .attr("fill", "#000")
+ .merge(symbol)
+ .attr("d",
+ d3.symbol()
+ .type(d => d.type)
+ .size(d => d.size)
+ )
+ .attr("transform", d => "translate(" + d.x + "," + d.y + ") rotate(" + d.angle + ")")
+ .on("mousedown", (e, d) => setIdx(L.Util.wrapNum((d.id === "up" ? idx + 1 : idx - 1), [0, items.length])));
+
+ const setIdx = (id) => {
+ idx = id;
+ // label.enter()
+ // .append('text')
+ // .attr("class", "legend-switcher-label")
+ // .attr("text-anchor", "end")
+ // .attr("x", -25)
+ // .attr("y", 5)
+ // .merge(label.data([{ idx: id }]))
+ // .text(d => items.length ? (items[idx][0].toUpperCase() + items[idx].slice(1)) : '')
+ // .on("mousedown", (e, d) => setIdx(L.Util.wrapNum((idx + 1), [0, items.length])));
+ label.text(items.length ? L._((items[idx][0].toUpperCase() + items[idx].slice(1))) : '');
+ onClick(items[idx]);
+ };
+ setIdx(0);
+
+ return g;
+ };
+};
+
+export const Tooltip = ({
+ xCoord,
+ yCoord,
+ width,
+ height,
+ labels = {},
+ item = {},
+}) => {
+ return g => {
+
+ let line = g.select('.mouse-focus-line');
+ let box = g.select('.mouse-focus-label');
+
+ if (!line.node()) line = g.append('svg:line');
+ if (!box.node()) box = g.append("g");
+
+ let rect = box.select(".mouse-focus-label-rect");
+ let text = box.select(".mouse-focus-label-text");
+
+ if (!rect.node()) rect = box.append("svg:rect");
+ if (!text.node()) text = box.append("svg:text");
+
+ // Sets focus-label-text position to the left / right of the mouse-focus-line
+ let xAlign = 0;
+ let yAlign = 0;
+ let bbox = { width: 0, height: 0 };
+ try { bbox = text.node().getBBox(); } catch (e) { return g; }
+
+ if (xCoord) xAlign = xCoord + (xCoord < width / 2 ? 10 : -bbox.width - 10);
+ if (yCoord) yAlign = Math.max(yCoord - bbox.height, L.Browser.webkit ? 0 : -Infinity);
+
+ line
+ .attr('class', 'mouse-focus-line')
+ .attr('x2', xCoord)
+ .attr('y2', 0)
+ .attr('x1', xCoord)
+ .attr('y1', height);
+
+ box
+ .attr('class', 'mouse-focus-label');
+
+ rect
+ .attr("class", "mouse-focus-label-rect")
+ .attr("x", xAlign - 5)
+ .attr("y", yAlign - 5)
+ .attr("width", bbox.width + 10)
+ .attr("height", bbox.height + 10)
+ .attr("rx", 3)
+ .attr("ry", 3);
+
+ text
+ .attr("class", "mouse-focus-label-text")
+ .style("font-weight", "700")
+ .attr("y", yAlign);
+
+ let label;
+
+ Object
+ .keys(labels)
+ .sort((a, b) => labels[a].order - labels[b].order) // TODO: any performance issues?
+ .forEach((i)=> {
+ label = text.select(".mouse-focus-label-" + labels[i].name);
+
+ if (!label.size()) {
+ label = text.append("svg:tspan", ".mouse-focus-label-x")
+ .attr("class", "mouse-focus-label-" + labels[i].name /*+ " " + "order-" + labels[i].order*/)
+ .attr("dy", "1.5em");
+ }
+
+ label.text(typeof labels[i].value !== "function" ? labels[i].value : labels[i].value(item));
+ });
+
+ text.select('tspan').attr("dy", "1em");
+ text.selectAll('tspan').attr("x", xAlign);
+
+ return g;
+ };
+};
+
+export const Ruler = ({
+ height,
+ width,
+}) => {
+ return g => {
+
+ g.data([{
+ x: 0,
+ y: height,
+ }])
+ .attr("transform", d => "translate(" + d.x + "," + d.y + ")");
+
+ let rect = g.selectAll('.horizontal-drag-rect').data([{ w: width }]);
+ let line = g.selectAll('.horizontal-drag-line').data([{ w: width }]);
+ let label = g.selectAll('.horizontal-drag-label').data([{ w: width - 8 }]);
+ let symbol = g.selectAll('.horizontal-drag-symbol')
+ .data([{
+ type: d3.symbolTriangle,
+ x: width + 7,
+ y: 0,
+ angle: -90,
+ size: 50
+ }]);
+
+ rect.exit().remove();
+ line.exit().remove();
+ label.exit().remove();
+ symbol.exit().remove();
+
+ rect.enter()
+ .append("svg:rect")
+ .attr("class", "horizontal-drag-rect")
+ .attr("x", 0)
+ .attr("y", -8)
+ .attr("height", 8)
+ .attr('fill', 'none')
+ .attr('pointer-events', 'all')
+ .merge(rect)
+ .attr("width", d => d.w);
+
+ line.enter()
+ .append("svg:line")
+ .attr("class", "horizontal-drag-line")
+ .attr("x1", 0)
+ .merge(line)
+ .attr("x2", d => d.w);
+
+ label.enter()
+ .append("svg:text")
+ .attr("class", "horizontal-drag-label")
+ .attr("text-anchor", "end")
+ .attr("y", -8)
+ .merge(label)
+ .attr("x", d => d.w)
+
+ symbol
+ .enter()
+ .append("svg:path")
+ .attr("class", "horizontal-drag-symbol")
+ .merge(symbol)
+ .attr("d",
+ d3.symbol()
+ .type(d => d.type)
+ .size(d => d.size)
+ )
+ .attr("transform", d => "translate(" + d.x + "," + d.y + ") rotate(" + d.angle + ")");
+
+ return g;
+ }
+};
+
+export const CheckPoint = ({
+ point,
+ height,
+ width,
+ x,
+ y
+}) => {
+ return g => {
+
+ if (isNaN(x) || isNaN(y)) return g;
+
+ if (!point.item || !point.item.property('isConnected')) {
+ point.position = point.position || "bottom";
+
+ point.item = g.append('g');
+
+ point.item.append("svg:line")
+ .attr("y1", 0)
+ .attr("x1", 0)
+ .attr("style","stroke: rgb(51, 51, 51); stroke-width: 0.5; stroke-dasharray: 2, 2;");
+
+ point.item
+ .append("svg:circle")
+ .attr("class", " height-focus circle-lower")
+ .attr("r", 3);
+
+ if (point.label) {
+ point.item.append("svg:text")
+ .attr("dx", "4px")
+ .attr("dy", "-4px");
+ }
+ }
+
+ point.item
+ .datum({
+ pos: point.position,
+ x: x,
+ y: y
+ })
+ .attr("class", d => "point " + d.pos)
+ .attr("transform", d => "translate(" + d.x + "," + d.y + ")");
+
+ point.item.select('line')
+ .datum({
+ y2: ({'top': -y, 'bottom': height - y})[point.position],
+ x2: ({'left': -x, 'right': width - x})[point.position] || 0
+ })
+ .attr("y2", d => d.y2)
+ .attr("x2", d => d.x2)
+
+ if (point.label) {
+ point.item.select('text')
+ .text(point.label);
+ }
+
+ return g;
+ }
+};
+
+export const Domain = ({
+ min,
+ max,
+ attr,
+ name,
+ forceBounds,
+ scale
+}) => function(data) {
+ attr = (scale && scale.attr) || attr || name;
+ let domain = data && data.length ? d3.extent(data, d => d[attr]) : [0, 1];
+ if (typeof min !== "undefined" && (min < domain[0] || forceBounds)) {
+ domain[0] = min;
+ }
+ if (typeof max !== "undefined" && (max > domain[1] || forceBounds)) {
+ domain[1] = max;
+ }
+ return domain;
+};
+
+export const Range = ({
+ axis
+}) => function(width, height) {
+ if (axis == 'x') return [0, width];
+ else if (axis == 'y') return [height, 0];
+};
+
+export const Scale = ({
+ data,
+ attr,
+ min,
+ max,
+ forceBounds,
+ range,
+}) => {
+ return d3.scaleLinear()
+ .range(range)
+ .domain(Domain({min, max, attr, forceBounds})(data));
+};
+
+export const Bisect = ({
+ data = [0, 1],
+ scale,
+ x,
+ attr
+}) => {
+ return d3
+ .bisector(d => d[attr])
+ .left(data, scale.invert(x));
+};
+
+export const Chart = ({
+ width,
+ height,
+ margins = {},
+ ruler,
+}) => {
+
+ const w = width - margins.left - margins.right;
+ const h = height - margins.top - margins.bottom;
+
+ // SVG Container
+ const svg = d3.create("svg:svg").attr("class", "background");
+
+ const defs = svg.append("svg:defs");
+
+ // SVG Groups
+ const g = svg.append("g");
+ const panes = {
+ grid : g.append("g").attr("class", "grid"),
+ area : g.append('g').attr("class", "area"),
+ axis : g.append('g').attr("class", "axis"),
+ point : g.append('g').attr("class", "point"),
+ brush : g.append("g").attr("class", "brush"),
+ tooltip: g.append("g").attr("class", "tooltip").attr('display', 'none'),
+ ruler : g.append('g').attr('class', 'ruler'),
+ legend : g.append('g').attr("class", "legend"),
+ };
+
+ // SVG Paths
+ const mask = panes.area .append("svg:mask") .attr("id", 'elevation-clipper-' + Math.random().toString(36).substr(2, 9)).attr('fill-opacity', 1);
+ const maskRect = mask .append("svg:rect") .attr('class', 'zoom') .attr('fill', 'white'); // white = transparent
+
+ // Canvas Paths
+ const foreignObject = panes.area .append('svg:foreignObject').attr('mask', 'url(#' + mask.attr('id') + ')');
+ const canvas = foreignObject.append('xhtml:canvas') .attr('class', 'canvas-plot');
+ const context = canvas.node().getContext('2d');
+
+ // Add tooltip
+ panes.tooltip.call(Tooltip({ xCoord: 0, yCoord: 0, height: h, width : w, labels: {} }));
+
+ // Add the brushing
+ let brush = d3.brushX().on('start.cursor end.cursor brush.cursor', () => panes.brush.select(".overlay").attr('cursor', null));
+
+ // Scales
+ const scale = (opts) => ({ x: Scale(opts.x), y: Scale(opts.y)});
+
+ let utils = {
+ defs,
+ mask,
+ canvas,
+ context,
+ brush,
+ };
+
+ let chart = {
+ svg,
+ g,
+ panes,
+ utils,
+ scale,
+ };
+
+ // Resize
+ chart._resize = ({
+ width,
+ height,
+ margins = {},
+ ruler,
+ }) => {
+
+ const w = width - margins.left - margins.right;
+ const h = height - margins.top - margins.bottom;
+
+ svg .attr("width", width).attr("height", height).attr("viewBox", `0 0 ${width} ${height}`);
+ g .attr("transform", "translate(" + margins.left + "," + margins.top + ")");
+
+ // Fix: https://github.com/Raruto/leaflet-elevation/issues/123
+ // Fix: https://github.com/Raruto/leaflet-elevation/issues/232
+ if (
+ /Mac|iPod|iPhone|iPad/.test(navigator.platform) &&
+ /AppleWebkit/i.test(navigator.userAgent) &&
+ !/Chrome/.test(navigator.userAgent)
+ ) {
+ canvas .style("transform", "translate(" + margins.left + "px," + margins.top + "px)");
+ }
+
+ maskRect .attr("width", w).attr("height", h).attr("x", 0).attr("y", 0);
+ foreignObject.attr('width', w).attr('height', h);
+ canvas .attr('width', w).attr('height', h);
+
+ if (ruler) {
+ panes.ruler.call(Ruler({ height: h, width: w }));
+ }
+
+ panes.brush.call(brush.extent( [ [0,0], [w, h] ] ));
+ panes.brush.select(".overlay").attr('cursor', null);
+
+ chart._width = w;
+ chart._height = h;
+
+ chart.svg.dispatch('resize', { detail: { width: w, height: h } } );
+
+ };
+
+ chart.pane = (name) => (panes[name] || (panes[name] = g.append('g').attr("class", name)));
+ chart.get = (name) => utils[name];
+
+ chart._resize({ width, height, margins});
+
+ return chart;
+};
diff --git a/assets/static/vendor/leaflet-elevation/src/components/marker.js b/assets/static/vendor/leaflet-elevation/src/components/marker.js
new file mode 100644
index 0000000..6f1f6af
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/components/marker.js
@@ -0,0 +1,96 @@
+import * as D3 from './d3.js';
+
+const _ = L.Control.Elevation.Utils;
+
+export var Marker = L.Class.extend({
+
+ initialize(options, control) {
+ this.options = options;
+ this.control = control
+
+ switch(this.options.marker) {
+ case 'elevation-line':
+ // this._container = d3.create("g").attr("class", "height-focus-group");
+ break;
+ case 'position-marker':
+ // this._marker = L.circleMarker([0, 0], { pane: 'overlayPane', radius: 6, fillColor: '#fff', fillOpacity:1, color: '#000', weight:1, interactive: false });
+ this._marker = L.marker([0, 0], { icon: this.options.markerIcon, zIndexOffset: 1000000, interactive: false });
+ break;
+ }
+
+ this._labels = {};
+
+ return this;
+ },
+
+ addTo(map) {
+ this._map = map;
+ switch(this.options.marker) {
+ case 'elevation-line': this._container = d3.select(map.getPane('elevationPane')).select("svg > g").call(D3.PositionMarker({})); break;
+ case 'position-marker': this._marker.addTo(map, { pane: 'overlayPane' }); break;
+ }
+ return this;
+ },
+
+ /**
+ * Update position marker ("leaflet-marker").
+ */
+ update(props) {
+ if (props) this._props = props;
+ else props = this._props;
+
+ if (!props) return;
+
+ if (props.options) this.options = props.options;
+ if (!this._map) this.addTo(props.map);
+
+ this._latlng = props.item.latlng;
+
+ switch(this.options.marker) {
+ case 'elevation-line':
+ if (this._container) {
+ let point = this._map.latLngToLayerPoint(this._latlng);
+ point = L.extend({}, props.item, this._map._rotate ? this._map.rotatedPointToMapPanePoint(point) : point);
+
+ let yMax = (this.control._height() / props.yCoordMax * point[this.options.yAttr]);
+
+ if (!isFinite(yMax) || isNaN(yMax)) yMax = 0;
+
+ this._container.classed("leaflet-hidden", false);
+ this._container.call(D3.PositionMarker({
+ theme : this.options.theme,
+ xCoord: point.x,
+ yCoord: point.y,
+ length: point.y - yMax, // normalized Y
+ labels: this._labels,
+ item: point,
+ }));
+ }
+ break;
+ case 'position-marker':
+ _.removeClass(this._marker.getElement(), 'leaflet-hidden');
+ this._marker.setLatLng(this._latlng);
+ break;
+ }
+ },
+
+ /*
+ * Hides the position/height indicator marker drawn onto the map
+ */
+ remove() {
+ this._props = null;
+ switch(this.options.marker) {
+ case 'elevation-line': this._container && this._container.classed("leaflet-hidden", true); break;
+ case 'position-marker': _.addClass(this._marker.getElement(), 'leaflet-hidden'); break;
+ }
+ },
+
+ getLatLng() {
+ return this._latlng;
+ },
+
+ _registerTooltip(props) {
+ this._labels[props.name] = props;
+ }
+
+});
diff --git a/assets/static/vendor/leaflet-elevation/src/components/summary.js b/assets/static/vendor/leaflet-elevation/src/components/summary.js
new file mode 100644
index 0000000..e8da57f
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/components/summary.js
@@ -0,0 +1,43 @@
+const _ = L.Control.Elevation.Utils;
+
+export var Summary = L.Class.extend({
+ initialize(opts, control) {
+ this.options = opts;
+ this.control = control;
+ this.labels = {};
+
+ let summary = this._container = _.create("div", "elevation-summary " + (opts.summary ? opts.summary + "-summary" : ''));
+ _.style(summary, 'max-width', opts.width ? opts.width + 'px' : '');
+ },
+
+ render() {
+ return container => container.append(() => this._container);
+ },
+
+ reset() {
+ this._container.innerHTML = '';
+ },
+
+ append(className, label, value) {
+ this._container.innerHTML += `<span class="${className}"><span class="summarylabel">${label}</span><span class="summaryvalue">${value}</span></span>`;
+ return this;
+ },
+
+ update() {
+ Object
+ .keys(this.labels)
+ .sort((a, b) => this.labels[a].order - this.labels[b].order) // TODO: any performance issues?
+ .forEach((i)=> {
+ let value = typeof this.labels[i].value !== "function" ? this.labels[i].value : this.labels[i].value(this.control.track_info, this.labels[i].unit || '');
+ this.append(i /*+ " order-" + this.labels[i].order*/, L._(this.labels[i].label), value, this.labels[i].order);
+ });
+ },
+
+ _registerSummary(data) {
+ for (let i in data) {
+ data[i].order = data[i].order ?? 1000;
+ this.labels[i] = data[i];
+ }
+ }
+
+});