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

Data driven properties on line-gradient colors #8977

Open
matthieugouel opened this issue Nov 14, 2019 · 28 comments
Open

Data driven properties on line-gradient colors #8977

matthieugouel opened this issue Nov 14, 2019 · 28 comments
Labels
cross-platform 📺 Requires coordination with Mapbox GL Native (style specification, rendering tests, etc.) feature 🍏

Comments

@matthieugouel
Copy link

Motivation

As far as I tested for know, it seems not possible to use data driven property to set the color associated with the percentages in the line-gradient / heatmap paint property.
For instance see this API example below :

map.addLayer({
    type: 'line',
    source: 'line',
    id: 'line',
    paint: {
         'line-width': 14,
         'line-gradient': [
             'interpolate',
             ['linear'],
             ['line-progress'],
             0, ["get", "src-color"],
             1, "["get", "dst-color"]
        ]
    },
});

Cheers,
Matthieu.

@mourner mourner added cross-platform 📺 Requires coordination with Mapbox GL Native (style specification, rendering tests, etc.) feature 🍏 labels Nov 14, 2019
@mourner
Copy link
Member

mourner commented Nov 14, 2019

For the heatmap, this is technically impossible since the heatmap layer gets colorized after the accumulation stage (when all features are rendered into one grayscale texture), using a single gradient as lookup.

For the line gradient though, this might be possible — needs investigation.

@mourner mourner changed the title Data driven properties on line-gradient / heatmap colors Data driven properties on line-gradient colors Nov 14, 2019
@matthieugouel
Copy link
Author

Ok. I thought I was the same "engine" underneath since they share common API but, yeah, I'm mostly interested in line-gradient anyway.

@matthieugouel
Copy link
Author

Is there a some kind of "standard procedure" in order to implement a data driven property ? I'm trying to investigate on my own but I'm not familiar with the code so it's not very efficient 😄

@brendan33
Copy link

I've been using something like this:

const stops = [
  0, 'green',
  0.2, 'cyan',
  0.6, 'orange',
  0.9, 'green',
  1, 'cyan',
];

map.addLayer({
    type: 'line',
    source: 'line',
    id: 'line',
    paint: {
         'line-width': 14,
         'line-gradient': [
             'interpolate',
             ['linear'],
             ['line-progress'],
             ...stops
        ]
    },
});

image

Seems to work well, but ideally I want there to be a hard line between the colour transitions. But I can't seem to figure out how to do that.

@Buzhanin
Copy link

Buzhanin commented Dec 5, 2019

Hello, I have a need to put several animated lines with different gradients on the map. Trying to read gradient values from a line's properties (in the same manner as matthieugouel did) ended up with this error:

Error: layers.line-animation.paint.line-gradient: data expressions not supported

Will it be fixed sometime? It looks like the only way to do my task is to put lines in different layers which will hit performance, and I would rather not to do that.

@cola119
Copy link

cola119 commented Mar 24, 2020

any update?

@dalbani
Copy link

dalbani commented Jun 13, 2020

I'm also very much interested in having such functionality.
Is there a way to give a hand to help implementing it?

@ohmegasquared
Copy link

Adding another request for this feature!

@ChristopherLR
Copy link

This feature would be great, thanks :)

@HarelM
Copy link

HarelM commented Nov 1, 2020

Also it would be great if the gradient values (not colors) were to be taken from a property or an array that matches the line points array.
There's a nice feature in the Oruxmaps app that shows the slope using gradient colors - i.e. when the slope is "hard" the color is red and when the slope is "easy" the color is green.
This would be a very nice addition to this framework to be able to do it.
I have looked at the code to understand where the data from line progress is coming from but couldn't fully understand, mainly I guess because it's tiled base...?
If someone can help me better understand the code I might be able to help here...
IF you want me to open a new issue since it's not exactly the same let me know...

@EarlOld
Copy link

EarlOld commented Dec 16, 2020

Hi. I found a solution for this, maybe.
Example. We need set gradient to route by speed data. 0 - green, 50 - yellow, 100 -red

  1. Create source with all points
map.addSource('route', {
          'type': 'geojson',
          'lineMetrics': true,
          'data': {
            'type': 'FeatureCollection',
            'features': [{
              "type": "Feature",
              "geometry": {
                "type": "LineString",
                "coordinates": values.map(item => item.location)
              }
            }]
          }
        });
  1. Create layer
map.addLayer({
          id: "route",
          type: "line",
          source: "route",
          paint: {
            "line-width": 5,
            'line-gradient': [
              'interpolate',
              ['linear'],
              ['line-progress'],
              ...getLineBackground()
            ]
          }
        })
  1. Create getLineBackground function.
const getLineBackground = () => {
   
    const range = chroma.scale('green', 'yellow', 'red').domain(0, 50, 100); // use chroma.js

    const colorsData = [];
    const totalDistance = values.reduce((total, currentPoint, index) => {
      if (index !== this.props.values?.length - 1) {
        return total + getDistanceFromLatLng(currentPoint.location[0], currentPoint.location[1], this.props.values[index + 1].location[0], this.props.values[index + 1].location[1]);
      }
      return total;
    }, 0); // distance between first and past point

    // next calculate percentage one line to all distance 
    const lengthBetweenPoints = values?.map((item, index) => {
      if (index === 0) {
        return {
          ...item,
          weight: 0
        };
      }
      if (index === values?.length - 1) {
        return {
          ...item,
          weight: 1
        };
      }

      return {
        ...item,
        weight: getDistanceFromLatLng(item.location[0], item.location[1], values[index + 1].location[0], values[index + 1].location[1]) / totalDistance
      };

    }); 


    let weight = 0;
    // next fill colorsData for **line-progress** property 
    lengthBetweenPoints.forEach((item, index) => {
      if (item.weight || index === 0) {
        if (index !== lengthBetweenPoints.length - 1) {
          weight += item.weight;
          colorsData.push(weight);
        } else {
          colorsData.push(item.weight);
        }
        colorsData.push(range(item.speed).hex());
      }
    });

    return colorsData;
  };

In final, we have
image

PS: Code for getDistanceFromLatLng

const getDistanceFromLatLng = (lon1,lat1,lon2,lat2) => {
  var R = 6371; // Radius of the earth in km
  var dLat = deg2rad(lat2-lat1);  // deg2rad below
  var dLon = deg2rad(lon2-lon1);
  var a =
    Math.sin(dLat/2) * Math.sin(dLat/2) +
    Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) *
    Math.sin(dLon/2) * Math.sin(dLon/2)
  ;
  var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
  var d = R * c; // Distance in km
  return d;
};

function deg2rad(deg) {
  return deg * (Math.PI/180);
}

@mourner
Copy link
Member

mourner commented Dec 16, 2020

@EarlOld this only works for a single feature. Ideally, we would be able to set data-driven line-gradients for tons of lines in the same layer (such as a road network visualizing traffic). One way we could do that is by keeping line-gradient static (so there's one texture for color lookup), but introducing a new line-gradient-progress property that would map line-progress to the final gradient key value, e.g.:

'line-gradient-progress': [
  'interpolate', 'linear', ['line-progress'], 
  0, ['get', 'start_speed'], 
  1, ['get', 'end_speed']
]

@cgibsonmm
Copy link

Any updates? Would love to have this feature to track altitude above the ground for a flight tracking app.

@eslavnov
Copy link

Also very interested in this feature. We have a lot of line segments that we want to dynamically aplly a gradient to, and currently the only way to achieve this is to split them into individual layers, which comes with a huge performance hit (we have hundreds of them). Having an ability to use data driven properties to control gradient would be extremely useful!

@nickfaughey
Copy link

nickfaughey commented Apr 27, 2021

Adding my support here - this would be perfect for visualizing a vehicle's speed along a route (collection of LineStrings, each of which has an initialVelocity and finalVelocity). In fact, the use case for line-gradient as it currently exists is extremely narrow, as you'll rarely know your stop outputs ahead of time unless you're visualizing something super simple where the outputs are completely unrelated to each feature itself. As mentioned, the only workaround is a complete anti-pattern and abuse of the library - introducing N layers for N styled features.

@eslavnov
Copy link

eslavnov commented May 6, 2021

Ultimately we've decided to implement a custom mapbox layer with three.js (via threebox) and some custom gradient logic. Now we have one layer with hundreds of lines and we can change the gradient dynamicly on a per-line basis. The performance with this approach is really nice, we notice virtually no difference compared to the vanilla implementation. Since it's a custom layer for mapbox, this means all the higher-level logic related to mapbox required very little changes - most of the effort was spent building this custom layer itself.

This is not the ideal solution since it introduces some extra dependencies and extra complexity, but if the dynamic gradients are a key feature for your particular use-case (as it is for us), I would recommend looking into supplementing mapbox with three.js.

@dalbani
Copy link

dalbani commented May 6, 2021

@eslavnov
Would you mind sharing a small working example of your solution?
I don't have much experience with three.js so that would be helpful.
Thanks!

@peterqliu
Copy link
Contributor

As mentioned, the only workaround is a complete anti-pattern and abuse of the library - introducing N layers for N styled features.

@nickfaughey @eslavnov @dalbani There's actually another way to achieve this that scales with only the numbers of colors in your ramp. Instead of adding your line geometry and varying the gradient stops, we can tweak the source geometry such that it's representable by a static gradient scheme.

Here's how it would work, with a color ramp that goes Red - Orange - Yellow - Green:

In the style, we add a line layer for every pairwise colors in the ramp sequence: RedToOrange, OrangeToYellow, YellowToGreen. This strategy requires N-1 layers, where N is the number of colors in the ramp.

In the geometry, we figure out where each stop would be, and chop up the line at those points (this is easy with a utility like turf.lineSliceAlong), so that the original line is now up to N-1 shorter segments. This can be done at runtime if you wrap it in a function.

We use layer RedToOrange to render the first segment of each original line, OrangeToYellow to render the second segments, and YellowToGreen to render the thirds (we can make it easy to filter by tagging each segment with a segmentIndex property). This works because even with variable gradient stops, the gradient between stops is always linear.

  R-O     O-Y     Y-G
|-----||-------||-------|

    R-O      O-Y    Y-G
|---------||----||---------|

The ideal solution is probably data-driven gradient stops, but this should be viable until then.

@mway77
Copy link

mway77 commented Jun 29, 2021

Here is an implementation with the Mapbox custom layer and Three.js (via ThreeBox-plugin). It only uses one layer, but you can define different lines with different start and end colors. You can also extend it easily in order to support more-points gradient.
Here is a JSFiddle where you can check the implementation:
https://jsfiddle.net/rqdz0ufL/4/
Before run you should put your Mapbox token into the code!

Hope this will help!

@ondrejrohon
Copy link

Any update on this? It would be great to have data-driven gradient stops. Thanks

@ArohanD
Copy link

ArohanD commented Feb 11, 2022

Adding my interest in this feature, especially considering threebox is now archived. Would be great to paint different gradients in the same layer as opposed to sorting features by color and then painting them to different layers.

@LunicLynx
Copy link

Has anyone tried splitting the line into features and adding a layer for each feature?
Hard, medium, easy
Red, yellow, green

Should work, no idea at which amount of layers it might break though 🙈

@GadhiyaRiddhi
Copy link

GadhiyaRiddhi commented Nov 18, 2022

@LunicLynx Can you please share your solution code as soon as possible ?

Has anyone tried splitting the line into features and adding a layer for each feature?
Hard, medium, easy
Red, yellow, green

@LunicLynx
Copy link

@GadhiyaRiddhi there is no code. I just think it should be possible.

@mholt
Copy link

mholt commented Nov 22, 2022

We would also be very interested in this.

@yerffejytnac
Copy link

Also expressing interest in this!

@JuanIrache
Copy link

Hi. I found a solution for this, maybe. Example. We need set gradient to route by speed data. 0 - green, 50 - yellow, 100 -red

  1. Create source with all points
map.addSource('route', {
          'type': 'geojson',
          'lineMetrics': true,
          'data': {
            'type': 'FeatureCollection',
            'features': [{
              "type": "Feature",
              "geometry": {
                "type": "LineString",
                "coordinates": values.map(item => item.location)
              }
            }]
          }
        });
  1. Create layer
map.addLayer({
          id: "route",
          type: "line",
          source: "route",
          paint: {
            "line-width": 5,
            'line-gradient': [
              'interpolate',
              ['linear'],
              ['line-progress'],
              ...getLineBackground()
            ]
          }
        })
  1. Create getLineBackground function.
const getLineBackground = () => {
   
    const range = chroma.scale('green', 'yellow', 'red').domain(0, 50, 100); // use chroma.js

    const colorsData = [];
    const totalDistance = values.reduce((total, currentPoint, index) => {
      if (index !== this.props.values?.length - 1) {
        return total + getDistanceFromLatLng(currentPoint.location[0], currentPoint.location[1], this.props.values[index + 1].location[0], this.props.values[index + 1].location[1]);
      }
      return total;
    }, 0); // distance between first and past point

    // next calculate percentage one line to all distance 
    const lengthBetweenPoints = values?.map((item, index) => {
      if (index === 0) {
        return {
          ...item,
          weight: 0
        };
      }
      if (index === values?.length - 1) {
        return {
          ...item,
          weight: 1
        };
      }

      return {
        ...item,
        weight: getDistanceFromLatLng(item.location[0], item.location[1], values[index + 1].location[0], values[index + 1].location[1]) / totalDistance
      };

    }); 


    let weight = 0;
    // next fill colorsData for **line-progress** property 
    lengthBetweenPoints.forEach((item, index) => {
      if (item.weight || index === 0) {
        if (index !== lengthBetweenPoints.length - 1) {
          weight += item.weight;
          colorsData.push(weight);
        } else {
          colorsData.push(item.weight);
        }
        colorsData.push(range(item.speed).hex());
      }
    });

    return colorsData;
  };

In final, we have image

PS: Code for getDistanceFromLatLng

const getDistanceFromLatLng = (lon1,lat1,lon2,lat2) => {
  var R = 6371; // Radius of the earth in km
  var dLat = deg2rad(lat2-lat1);  // deg2rad below
  var dLon = deg2rad(lon2-lon1);
  var a =
    Math.sin(dLat/2) * Math.sin(dLat/2) +
    Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) *
    Math.sin(dLon/2) * Math.sin(dLon/2)
  ;
  var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
  var d = R * c; // Distance in km
  return d;
};

function deg2rad(deg) {
  return deg * (Math.PI/180);
}

Thanks. That approach works great

@Wagnerd6
Copy link

Wagnerd6 commented May 11, 2023

I implemented it this way now:

map.on('load', function() {
    // Get the upper and lower bounds for the speed variable
    const lowerBound = lineFeature.properties.myProperty[0];
    const upperBound = lineFeature.properties.myProperty[1];
    const midpoint = (lowerBound + upperBound) / 2;

    map.addLayer({
        'id': 'line',
        'type': 'line',
        'source': {
            'type': 'geojson',
            'data': lineFeature,
            'lineMetrics': true
        },
        'layout': {
            'line-join': 'round',
            'line-cap': 'round'
        },
        'paint': {
            'line-width': 8,
            'line-gradient': [
              'interpolate',
              ['linear'],
              ['line-progress'],
              0, ['rgba', ...getRGBGradientGreenYellowRed(lowerBound, 0, 10), 1],
              0.5, ['rgba', ...getRGBGradientGreenYellowRed(midpoint, 0, 10), 1],
              1, ['rgba', ...getRGBGradientGreenYellowRed(upperBound, 0, 10), 1]
            ]
          }
    })
    });

This colors the line in a gradient green to yellow to red depending on the value of myProperty. myProperty has two values, one for the starting point and one for the end point. Everything <= 0 is green and >=10 is red. So if I have myProperty=[2.5, 7.5] the start is yellow-green, the end is orange and the colors in between a smooth gradient.

Here is the function to get the RGB tuple based on an input number (myProperty) and the upper and lower bounds (here 0 and 10):

function getRGBGradientGreenYellowRed(input: number, rangeStart: number = 0.0, rangeEnd: number = 1.0, convert255 = true): [number, number, number] {
    const distance: number = rangeEnd - rangeStart;
    const r: number = 2.0 * ((input - rangeStart) / distance);
    const g: number = 2.0 * (1.0 - ((input - rangeStart) / distance));
    const rgb: [number, number, number] = [r > 1.0 ? 1.0 : r / 1.0, g > 1.0 ? 1.0 : g / 1.0, 0.0];
    if (convert255) {
        return [rgb[0] * 255.0, rgb[1] * 255.0, rgb[2] * 255.0];
    } else {
        return rgb;
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cross-platform 📺 Requires coordination with Mapbox GL Native (style specification, rendering tests, etc.) feature 🍏
Projects
None yet
Development

No branches or pull requests