Raruto / leaflet-elevation

Leaflet plugin that allows to add elevation profiles using d3js
GNU General Public License v3.0
255 stars 85 forks source link

New feature: Scaling #82

Closed velocat closed 3 years ago

velocat commented 4 years ago

Is it possible to implement scaling of charts, for example, setting cursors for a selected area on the chart?

That is, we select an area on the X scale (2 vertical rulers) and the graph is automatically expanded for this area. This would be very useful for viewing finer details in graphs.

I found something similar here (for D3): https://jsfiddle.net/cyril123/aebnL4sL/ Only scaling along one axis is desirable, and it would be more beautiful so that it does not come from the mouse wheel, but with the help of vertical rulers :)

PS I find this useful, as it will allow, for example, in the future to create a script with the following content: We find an area with erroneous heights (it often happens that some points fall out), zoom in to select a small area with problem points, select it with the mouse, and then send a request to the elevation server. As a result, you can correct the track with problem areas. This is all, of course, another script, not within the framework of this, but the presence of this feature will allow you to create what I described :))

Raruto commented 4 years ago

Hi velocat, If I remember correctly wp-gpx-maps had a similar function (ie. fit chart to dragged track selection).

Let me know if you achieve to develop a working example, Raruto

velocat commented 4 years ago

Unfortunately, they don't use d3, but use chart.js I've spent a lot of time trying to figure out d3, but I haven't made any significant progress yet. At the moment, I only managed to trigger the event leading to zooming, but the wisdom of transformations is confusing me more and more....

so Far like this: https://github.com/velocat/leaflet-elevation/tree/Zoom

I hope You can tell me what to add, because I'm stuck and can't move on ))

Raruto commented 4 years ago

Hi velocat,

here it is a proof of concept (tested with version 1.4.0):

L.Control.Elevation.Chart.addInitHook(function(){
  let svg = this._container;
  let g = this._container.select('g');
  // let path = this._path;
  // let gx = this._axis.select('.x.axis.bottom');

  let zoom = d3.zoom()
    .scaleExtent([1, 10])
    .on("zoom", (e) => {
      this._updateScale();                            // hacky way for restoring x scale when zooming out
      this._x = d3.event.transform.rescaleX(this._x); // calculate x scale at zoom level
      this._updateAxis();
      this._updateArea();
  });

  g.call(zoom);                                       // add zoom functionality to "svg" group
});

// and after that continue as usual

let controlElevation = L.control.elevation(...);
controlElevation.load(...)

To learn more, take a look at the following resources:

Hoping this can help you, Raruto

velocat commented 4 years ago

This is great! And I kept trying to rewrite all this in the code itself ))) It turned out through the hook everything is simple))) I've been studying the D3 documentation for a few days now, but I still don't understand it, although I already understand how it works better )))

But the truth is, you still need to register something... Since Ticks disappear when zooming and there are no restrictions, the chart spreads to the entire width of the page:

before zoom: 01

after zoom: 02

And after the reverse zoom of such an expanded chart, it turns out like this: 03

And after that, it will not be able to expand it normally.

PS Also: scaling is only applied to the elevation graph, and slopes, speed, and so on are not scaled

PPS It seems to me that for the sake of beauty, it would not be bad to add a timeout fade for FocusLabel if the cursor left the chart area, for example, after 10 seconds, and then it remains always hanging over the chart after hovering

Raruto commented 4 years ago

I've been studying the D3 documentation for a few days now, but I still don't understand it, although I already understand how it works better )))

Yep, working with "svg" can be a pain 😄

you still need to register something...

I don't have so much free time to devote to it...

the chart spreads to the entire width of the page

Take a look at: https://observablehq.com/@d3/zoomable-area-chart?collection=@d3/d3-zoom

In particular, start playing with these portions of code:

  .extent([[margin.left, 0], [width - margin.right, height]])
  .translateExtent([[margin.left, -Infinity], [width - margin.right, Infinity]])
  svg.append("clipPath")
      .attr("id", clip.id)
    .append("rect")
      .attr("x", margin.left)
      .attr("y", margin.top)
      .attr("width", width - margin.left - margin.right)
      .attr("height", height - margin.top - margin.bottom);
  const path = svg.append("path")
      .attr("clip-path", clip)

NB https://observablehq.com examples can be directly edited (in browser) to understand how they works.

scaling is only applied to the elevation graph, and slopes, speed, and so on are not scaled

Probably this happens because the following events are not being fired:

https://github.com/Raruto/leaflet-elevation/blob/936486b897a7f5e5827c0a06266ece3933b14711/src/control.js#L734 https://github.com/Raruto/leaflet-elevation/blob/936486b897a7f5e5827c0a06266ece3933b14711/src/control.js#L735

It seems to me that for the sake of beauty, it would not be bad to add a timeout fade for FocusLabel if the cursor left the chart area, for example, after 10 seconds, and then it remains always hanging over the chart after hovering

The following hook might help you:

https://github.com/Raruto/leaflet-elevation/blob/936486b897a7f5e5827c0a06266ece3933b14711/src/control.js#L637

Raruto commented 4 years ago

Here it is a more complete example:

L.Control.Elevation.Chart.addInitHook(function(){
  let svg = this._container;
  let g = this._container.select('g');
  let path = this._path;
  let area = this._area;

  let margin = this.options.margins;
  let clipper_id = 'clipper-altitude';                 // TODO: generate unique ids

  const clip = area.append("clipPath")                 // generate and append <clipPath> element
      .attr("id", clipper_id)
    .append("rect")
      .attr("x", 0)
      .attr("y", 0)
      .attr("width", this._width())
      .attr("height", this._height());

  path.attr("clip-path", 'url(#' + clipper_id + ')');  // bind <clipPath> mask ("Altitude")

  let zoom = d3.zoom()
    .scaleExtent([1, 10])
    .extent([[margin.left, 0], [this._width() - margin.right, this._height()]])
    .translateExtent([[margin.left, -Infinity], [this._width() - margin.right, Infinity]])
    .on("zoom", (e) => {
      this._updateScale();                            // hacky way for restoring x scale when zooming out
      this._x = d3.event.transform.rescaleX(this._x); // calculate x scale at zoom level
      this._updateAxis();
      this._updateArea();
  });

  g.call(zoom);                                       // add zoom functionality to "svg" group
});

// and after that continue as usual

let controlElevation = L.control.elevation(...);
controlElevation.load(...)
velocat commented 4 years ago

That's great! :) Many thanks! Now it does not move out))

It remains only to solve 2 more questions, and everything will be fine in general:

PS Although I'm afraid I won't stop there.... XD the idea is maturing in my head that it would be nice to add an x-axis switch between distance and time ))))))

Raruto commented 4 years ago

It remains only to solve 2 more questions, and everything will be fine in general:

  • Missing ticks on the right when zooming
  • No scaling on other charts (on reception. Speed, etc.)

Take a look at the d3-zoom branch

zoom = ctrl + wheel pan = ctrl + drag

velocat commented 4 years ago

I tried to apply it to myself. Dragging and zooming works, but the result is not what is expected: When zooming in, the graphs also expand beyond the block, to the entire page.

I decided that the problem is how the coordinates are calculated and substituted them with constant values, but the result is still the same ((

let zoom = d3.zoom()
  .scaleExtent([1, 10])
  .extent([[ 50, 0 ], [ 614, 160 ]])
  .translateExtent([[ 50, -Infinity ], [ 750, Infinity ]])
  .filter(() => d3.event.ctrlKey);

PS I have achieved the desired result, but this only happens in the following sequence of actions:

zoom1

zoom2

zoom3

PS it seems to me that using Ctrl+wheel is not a good idea, since in the browser this combination is used to scale all the content. This way, if the cursor suddenly appears outside of this block, the entire page will be scaled.

Raruto commented 4 years ago

it seems to me that using Ctrl+wheel is not a good idea, since in the browser this combination is used to scale all the content. This way, if the cursor suddenly appears outside of this block, the entire page will be scaled.

This is also a quite common behavior (I moved the zoom listener to the whole <svg> element, for more info take a look at my latest commits: https://github.com/Raruto/leaflet-elevation/commit/421286216030ae3dd8ad706522de76a60c62283d )

velocat commented 4 years ago

But this does not cancel the zoom change of the entire page if suddenly the cursor went out of the svg area, and Ctrl is still pressed and the wheel turns?

In General, I meant that in this case you need to either completely cancel the Ctrl+mousewheel event on the entire page, or replace it with Shift+mousewheel, for example To avoid accidental user behavior.

Raruto commented 4 years ago

This problem arises when the elevation div is detached, to "partially" solve it you can do as follows:

d3.select('body')
  .on("wheel", function() {
    if (d3.event.ctrlKey) d3.event.preventDefault();
  });

In order to avoid unwanted effects, I wouldn't disable it within the library.

Raruto commented 3 years ago

Hi velocat, for the time being I have released the d3-zoom branch as v1.5.0

Happy testing, Raruto