Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support showing the elevation segments on the Altitude area chart directly #251

Closed
jtpio opened this issue Apr 26, 2023 · 3 comments · Fixed by #257
Closed

Support showing the elevation segments on the Altitude area chart directly #251

jtpio opened this issue Apr 26, 2023 · 3 comments · Fixed by #257

Comments

@jtpio
Copy link

jtpio commented Apr 26, 2023

Subject of the issue

This is more of a feature request than an issue.

Your environment

  • leaflet-elevation: 2.3.0
  • leaflet: 1.7.1
  • browser: Firefox 112
  • operating system: Fedora 37
  • link to your project: N/A

Steps to reproduce

Using any example: https://github.com/Raruto/leaflet-elevation/tree/master/examples

Expected behaviour

It would be nice to have something similar to what Leaflet.Heightgraph provides: https://github.com/GIScience/Leaflet.Heightgraph

image

For reference Openrunner has something similar as well and even shows the percentage as part of the tooltip:

image

This could be an opt-in option and it wouldn't be displayed by default.

Actual behaviour

This doesn't seem to be configurable currently. There is a Slope chart but it adds an additional chart on top of the Altitude.

Thanks!

@Raruto
Copy link
Owner

Raruto commented Apr 27, 2023

Hi @jtpio,

definitely it would be something nice, are you able to contribute to this project? (if so, I can give you some knowledgeable advice on where to start).

👋 Raruto

@jtpio
Copy link
Author

jtpio commented Apr 27, 2023

Thanks @Raruto.

Yes I would be happy to contribute. Not sure I'll have the bandwidth in the coming days, but some pointers would definitely help!

@Raruto
Copy link
Owner

Raruto commented Apr 27, 2023

It would be nice to have something similar to what Leaflet.Heightgraph provides: https://github.com/GIScience/Leaflet.Heightgraph

The following should be the related function that handles the d3js segments on the graph (look directly at Robin's fork which should be more up-to-date).

boldtrn/Leaflet.Heightgraphsrc/heightgraph.js#L715-L733

/**
 * Appends the areas to the graph
 */
_appendAreas(block, idx, eleIdx) {

  const c = this._categories[idx].attributes[eleIdx].color

  this._area = d3Area()
    .x(d => {
      const xDiagonalCoordinate = this._x(d.position)
      d.xDiagonalCoordinate = xDiagonalCoordinate
      return xDiagonalCoordinate
    })
   .y0(this._svgHeight)
   .y1(d => this._y(d.altitude))
   .curve(curveLinear);

 this._areapath = this._svg.append("path").attr("class", "area");

 this._areapath.datum(block)
   .attr("d", this._area)
   .attr("stroke", c)

 Object.entries(this._graphStyle).forEach(([prop, val]) => {
   this._areapath.style(prop, val)
 })

 this._areapath
   .style("fill", c)
   .style("pointer-events", "none");
}

This could be an opt-in option and it wouldn't be displayed by default.

Currently all additional chart area definitions (altitude, speed, acceleration, ...) are lazy-loaded from the src/handlers.

So I think a good way to achieve this could be to create a custom definition for this type of graph as well.

It sure won't work out of the box, but maybe with a little tweaking of the "loading" logic it might even turn out to be something more useful.

Below some notable functions:

L.Control.Elevation::options.handlers

handlers: [ // <-- A list of: Dynamic imports || "ClassName" || function Name() { return { /* a custom object definition */ } }
'Distance', // <-- same as: import("../src/handlers/distance.js")
'Time', // <-- same as: import("../src/handlers/time.js")
'Altitude', // <-- same as: import("../src/handlers/altitude.js")
'Slope', // <-- same as: import("../src/handlers/slope.js")
'Speed', // <-- same as: import("../src/handlers/speed.js")
'Acceleration', // <-- same as: import("../src/handlers/acceleration.js")
// 'Pace', // <-- same as: import("../src/handlers/pace.js")
// "Heart", // <-- same as: import("../src/handlers/heart.js")
// "Cadence", // <-- same as: import("../src/handlers/cadence.js")
// import('../src/handlers/heart.js'),
import('../src/handlers/cadence.js'),
// import('../src/handlers/pace.js'),
L.Control.Elevation.MyHeart, // <-- see custom functions declared above
// L.Control.Elevation.MyCadence, // <-- see custom functions declared above
L.Control.Elevation.MyPace, // <-- see custom functions declared above
],

L.Control.Elevation::_registerHandler

/**
* Parse a module definition and attach related function listeners
*/
_registerHandler(props) {
// eg. L.Control.Altitude
if (typeof props === "function") {
return this._registerHandler(props.call(this));
}
let {
name,
attr,
required,
deltaMax,
clampRange,
decimals,
meta,
unit,
coordinateProperties,
coordPropsToMeta,
pointToAttr,
onPointAdded,
stats,
statsName,
grid,
scale,
path,
tooltip,
summary
} = props;
// eg. "altitude" == true
if (this.options[name] || required) {
this._registerDataAttribute({
name,
attr,
meta,
deltaMax,
clampRange,
decimals,
coordPropsToMeta: _.coordPropsToMeta(coordinateProperties, meta || name, coordPropsToMeta),
pointToAttr,
onPointAdded,
stats,
statsName,
});
if (grid) {
this._registerAxisGrid(L.extend({ name }, grid));
}
if (this.options[name] !== "summary") {
if (scale) this._registerAxisScale(L.extend({ name, label: unit }, scale));
if (path) this._registerAreaPath(L.extend({ name }, path));
}
if (tooltip || props.tooltips) {
_.each([tooltip, ...(props.tooltips || [])], t => t && this._registerTooltip(L.extend({ name }, t)));
}
if (summary) {
_.each(summary, (s, k) => summary[k] = L.extend({ unit }, s));
this._registerSummary(summary);
}
}
},

L.Control.Elevation::_registerDataAttribute

/**
* Base handler for iterative track statistics (dist, time, z, slope, speed, acceleration, ...)
*/
_registerDataAttribute(props) {
// parse of "coordinateProperties" for later usage
if (props.coordPropsToMeta) {
this.on("elepoint_init", (e) => props.coordPropsToMeta.call(this, e));
}
// prevent excessive variabile instanstations
let i, curr, prev, attr = props.attr || props.name;
// save here a reference to last used point
let lastValid = {};
// iteration
this.on("elepoint_added", ({index, point}) => {
i = index;
prev = curr ?? this._data[i]; // same as: this._data[i > 0 ? i - 1 : i]
curr = this._data[i];
// retrieve point value
curr[attr] = props.pointToAttr.call(this, point, i);
// check and fix missing data on last added point
if (i > 0 && isNaN(prev[attr])) {
if (!isNaN(lastValid[attr]) && !isNaN(curr[attr])) {
prev[attr] = (lastValid[attr] + curr[attr]) / 2;
} else if (!isNaN(lastValid[attr])) {
prev[attr] = lastValid[attr];
} else if (!isNaN(curr[attr])) {
prev[attr] = curr[attr];
}
// update "yAttr" and "xAttr"
if (props.meta) {
prev[props.meta] = prev[attr];
}
}
// skip to next iteration for invalid or missing data (eg. i == 0)
if (isNaN(curr[attr])) {
return;
}
// update reference to last used point
lastValid[attr] = curr[attr];
// Limit "crazy" delta values.
if (props.deltaMax) {
curr[attr] =_.wrapDelta(curr[attr], prev[attr], props.deltaMax);
}
// Range of acceptable values.
if (props.clampRange) {
curr[attr] = _.clamp(curr[attr], props.clampRange);
}
// Limit floating point precision.
if (!isNaN(props.decimals)) {
curr[attr] = _.round(curr[attr], props.decimals);
}
// update "track_info" stats (min, max, avg, ...)
if (props.stats) {
for (const key in props.stats) {
let sname = (props.statsName || attr) + (key != '' ? '_' : '');
this.track_info[sname + key] = props.stats[key].call(this, curr[attr], this.track_info[sname + key], this._data.length);
}
}
// update here some mixins (eg. complex "track_info" stuff)
if (props.onPointAdded) props.onPointAdded.call(this, curr[attr], i, point);
});
},


Within the /test folder you can also see a proof of concept on how you can start developing any new feature (that is, before you even start attempting to modify library code).

That means, you can always monkey patch any native functionality through the L.Control.Elevation.include() and then use some d3js "selectors" to alter the chart.

L.Control.Elevation.include

<script>
L.Control.Elevation.include({
_updateSelectionBox: function() {
this.options.margins.bottom = L.Control.Elevation.Utils.clamp(this.options.margins.bottom, [60, +Infinity]);
let g = this._chart._chart.pane('legend');
const width = this._width();
const height = this._height() + 40;
let idx = 0;
let attrs = Object.keys(this._chart._paths || {});
g.data([{
x: width,
y: height
}]).attr("transform", d => "translate(" + d.x + "," + d.y + ")");
let label = g.selectAll(".select-info") .data([{ idx: 0 }]);
let symbol = g.selectAll(".select-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("path")
.attr("class", "select-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 + ")")
.attr("id", d => d.id)
.on("mousedown", (e, d) => setIdx(L.Util.wrapNum((d.id === "up" ? idx + 1 : idx - 1), [0, attrs.length])));
const setIdx = (id) => {
idx = id;
console.log(id);
label.enter()
.append('text')
.attr("class", "select-info")
.attr("id", "selectionText")
.attr("text-anchor", "end")
.attr("x", -25)
.attr("y", 5)
.merge(label.data([{ idx: id }]))
.on("mousedown", (e, d) => setIdx(L.Util.wrapNum((d.idx + 1), [0, attrs.length])))
.text(d => attrs.length ? (attrs[d.idx][0].toUpperCase() + attrs[d.idx].slice(1)) : '');
L.Control.Elevation.Utils.each(attrs, name => this._chart._togglePath(name, attrs[idx] == name, true));
this._chart._updateArea();
};
setIdx(0);
},
})
</script>

Within the examples/leaflet-elevation_dynamic-runner.html demo you can also see a more complete use case of this development pattern.

And finally, for some automated testing you can look at anyone of *.spec.js file within this repository.

Happy thoughts,
Raruto

Raruto added a commit that referenced this issue May 17, 2023
Raruto added a commit that referenced this issue Jul 5, 2023
* Add new handler: `src/handlers/lineargradient.js`

Closes: #251
Closes: #232

* Update leaflet-elevation_linear-gradient.html
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants