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

[for discussion] Draft proposal - arbitrary expressions for style functions #4715

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 183 additions & 0 deletions debug/expressions.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<!DOCTYPE html>
<html>
<head>
<title>Mapbox GL JS debug page</title>
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel='stylesheet' href='/dist/mapbox-gl.css' />
<style>
body { margin: 0; padding: 0; }
html, body, #map { height: 100%; }
#map, #chart { width: 49%; display: inline-block; }

path {
stroke: steelblue;
stroke-width: 2;
fill: none;
}

.axis path,
.axis line {
fill: none;
stroke: grey;
stroke-width: 1;
shape-rendering: crispEdges;
}
</style>
</head>

<body>
<div id='map'></div>
<div id='chart'></div>

<script src='/dist/mapbox-gl-dev.js'></script>
<script src='/debug/access_token_generated.js'></script>

<script src="http://d3js.org/d3.v3.min.js"></script>

<script>

var style = {
sprite: "mapbox://sprites/mapbox/streets-v10",
glyphs: "mapbox://fonts/mapbox/{fontstack}/{range}.pbf",
sources: {
"geojson": {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": [{"type":"Feature","properties":{"x":-10,"y":-10},"geometry":{"type":"Point","coordinates":[-10,-10]}},{"type":"Feature","properties":{"x":-10,"y":-6},"geometry":{"type":"Point","coordinates":[-10,-6]}},{"type":"Feature","properties":{"x":-10,"y":-2},"geometry":{"type":"Point","coordinates":[-10,-2]}},{"type":"Feature","properties":{"x":-10,"y":2},"geometry":{"type":"Point","coordinates":[-10,2]}},{"type":"Feature","properties":{"x":-10,"y":6},"geometry":{"type":"Point","coordinates":[-10,6]}},{"type":"Feature","properties":{"x":-10,"y":10},"geometry":{"type":"Point","coordinates":[-10,10]}},{"type":"Feature","properties":{"x":-6,"y":-10},"geometry":{"type":"Point","coordinates":[-6,-10]}},{"type":"Feature","properties":{"x":-6,"y":-6},"geometry":{"type":"Point","coordinates":[-6,-6]}},{"type":"Feature","properties":{"x":-6,"y":-2},"geometry":{"type":"Point","coordinates":[-6,-2]}},{"type":"Feature","properties":{"x":-6,"y":2},"geometry":{"type":"Point","coordinates":[-6,2]}},{"type":"Feature","properties":{"x":-6,"y":6},"geometry":{"type":"Point","coordinates":[-6,6]}},{"type":"Feature","properties":{"x":-6,"y":10},"geometry":{"type":"Point","coordinates":[-6,10]}},{"type":"Feature","properties":{"x":-2,"y":-10},"geometry":{"type":"Point","coordinates":[-2,-10]}},{"type":"Feature","properties":{"x":-2,"y":-6},"geometry":{"type":"Point","coordinates":[-2,-6]}},{"type":"Feature","properties":{"x":-2,"y":-2},"geometry":{"type":"Point","coordinates":[-2,-2]}},{"type":"Feature","properties":{"x":-2,"y":2},"geometry":{"type":"Point","coordinates":[-2,2]}},{"type":"Feature","properties":{"x":-2,"y":6},"geometry":{"type":"Point","coordinates":[-2,6]}},{"type":"Feature","properties":{"x":-2,"y":10},"geometry":{"type":"Point","coordinates":[-2,10]}},{"type":"Feature","properties":{"x":2,"y":-10},"geometry":{"type":"Point","coordinates":[2,-10]}},{"type":"Feature","properties":{"x":2,"y":-6},"geometry":{"type":"Point","coordinates":[2,-6]}},{"type":"Feature","properties":{"x":2,"y":-2},"geometry":{"type":"Point","coordinates":[2,-2]}},{"type":"Feature","properties":{"x":2,"y":2},"geometry":{"type":"Point","coordinates":[2,2]}},{"type":"Feature","properties":{"x":2,"y":6},"geometry":{"type":"Point","coordinates":[2,6]}},{"type":"Feature","properties":{"x":2,"y":10},"geometry":{"type":"Point","coordinates":[2,10]}},{"type":"Feature","properties":{"x":6,"y":-10},"geometry":{"type":"Point","coordinates":[6,-10]}},{"type":"Feature","properties":{"x":6,"y":-6},"geometry":{"type":"Point","coordinates":[6,-6]}},{"type":"Feature","properties":{"x":6,"y":-2},"geometry":{"type":"Point","coordinates":[6,-2]}},{"type":"Feature","properties":{"x":6,"y":2},"geometry":{"type":"Point","coordinates":[6,2]}},{"type":"Feature","properties":{"x":6,"y":6},"geometry":{"type":"Point","coordinates":[6,6]}},{"type":"Feature","properties":{"x":6,"y":10},"geometry":{"type":"Point","coordinates":[6,10]}},{"type":"Feature","properties":{"x":10,"y":-10},"geometry":{"type":"Point","coordinates":[10,-10]}},{"type":"Feature","properties":{"x":10,"y":-6},"geometry":{"type":"Point","coordinates":[10,-6]}},{"type":"Feature","properties":{"x":10,"y":-2},"geometry":{"type":"Point","coordinates":[10,-2]}},{"type":"Feature","properties":{"x":10,"y":2},"geometry":{"type":"Point","coordinates":[10,2]}},{"type":"Feature","properties":{"x":10,"y":6},"geometry":{"type":"Point","coordinates":[10,6]}},{"type":"Feature","properties":{"x":10,"y":10},"geometry":{"type":"Point","coordinates":[10,10]}}]
}
}
},
layers: [
{
id: 'circle',
type: 'circle',
source: 'geojson',
paint: {
'circle-radius': {
type: 'expression',
expression: [
'*',
[ '^', 2, ['zoom'] ],
0.125,
[
'+',
20,
[ 'number_data', 'x' ],
[ 'number_data', 'y' ]
]
],
zoomInterpolationBase: 1
},
'circle-opacity': {
type: 'expression',
expression: [ '/', [ '+', 10, ['number_data', 'x'] ], 30 ]
},
'circle-color': {
type: 'expression',
expression: [
'rgb',
[ '+', 128, [ '*', 10, ['number_data', 'x'] ] ],
[ '+', 128, [ '*', 10, ['number_data', 'y'] ] ],
128
]
}
}
}
],
version: 8
}

var map = window.map = new mapboxgl.Map({
container: 'map',
zoom: 0,
center: [0, 0],
style: style,
hash: true
});

// Set the dimensions of the canvas / graph
var margin = {top: 30, right: 20, bottom: 30, left: 50},
width = 600 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom;

// Set the ranges
var x = d3.scale.linear().range([0, width]);
var y = d3.scale.linear().range([height, 0]);

// Define the axes
var xAxis = d3.svg.axis().scale(x)
.orient("bottom").ticks(5);

var yAxis = d3.svg.axis().scale(y)
.orient("left").ticks(5);

// Define the line
var valueline = d3.svg.line()
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y); });

// Adds the svg canvas
var svg = d3.select("#chart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");

map.on('load', function () {
const styleFunction = map.getPaintProperty('circle', 'circle-radius')
var compiledExpression = mapboxgl.createFunction(styleFunction, {});

const data = [];
for (let z = 0; z < 5; z += 0.01) {
let output
if (!compiledExpression.isFeatureConstant && !compiledExpression.isZoomConstant) {
const zLow = Math.floor(z);
const zHigh = Math.ceil(z);
const interp = mapboxgl.createFunction({
type: 'exponential',
stops: [ [ zLow, 0 ], [ zHigh, 1 ] ],
base: compiledExpression.zoomInterpolationBase
}, { type: 'number' })
const t = interp(z);
const outLow = compiledExpression(zLow, {x: 5, y: 5});
const outHigh = compiledExpression(zHigh, {x: 5, y: 5});
output = outLow + (outHigh - outLow) * t;
} else {
output = compiledExpression(z, {x: 5, y: 5});
}
data.push({x: z, y: output});
}

initChart(data);
})

// Get the data
function initChart(data) {
// Scale the range of the data
x.domain(d3.extent(data, function(d) { return d.x; }));
y.domain([0, d3.max(data, function(d) { return d.y; })]);

// Add the valueline path.
svg.append("path")
.attr("class", "line")
.attr("d", valueline(data));

// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);

// Add the Y Axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis);

};
</script>
</body>
</html>
139 changes: 139 additions & 0 deletions docs/style-spec/expressions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
**NOTE: Consider the contents of this doc as a proposed replacement for the "Function" section of the style spec docs. Drafting it here rather than in the HTML doc so that it's easier to read/comment on.**

# Functions

The value for any layout or paint property may be specified as a function. Functions allow you to make the appearance of a map feature change with the current zoom level and/or the feature's properties.

## Property functions

<p><strong>Property functions</strong> allow the appearance of a map feature to change with its properties. Property functions can be used to visually differentate types of features within the same layer or create data visualizations. Note that support for property functions is not available across all properties and platforms at this time.</p>

`expression`
_Required [expression value](#Expressions)_
A property expression defines how one or more feature property values are combined using logical, mathematical, string, or color operations to produce the appropriate style value. See [Expressions](#Expressions) for details.

```js
{
"circle-color": {
"expression": [
'rgb',
// red is higher when feature.properties.temperature is higher
["number_data", "temperature"],
0,
// blue is higher when feature.properties.temperature is lower
["-", 100, ["number_data", "temperature"]]
]
}
}
```


## Zoom functions

**Zoom functions** allow the appearance of a map feature to change with map’s zoom level. Zoom functions can be used to create the illusion of depth and control data density.

`stops`
_Required array_
Zoom functions are defined in terms of input values and output values. A set of one input value and one output value is known as a "stop." Each stop is thus an array with two elements: the first is a zoom level; the second is either a style value or a property function. Note that support for property functions is not yet complete.

`base`
_Optional number. Default is 1._
The exponential base of the interpolation curve. It controls the rate at which the function output increases. Higher values make the output increase more towards the high end of the range. With values close to 1 the output increases linearly.

`type`
_Optional enum. One of exponential, interval._
- `exponential` functions generate an output by interpolating between stops just less than and just greater than the function input. The domain must be numeric. This is the default for properties marked with , the "exponential" symbol.
- `interval` functions return the output value of the stop just less than the function input. The domain must be numeric. This is the default for properties marked with , the "interval" symbol.


### Example: a zoom-only function.

```js
{
"circle-radius": {
"stops": [

// zoom is 5 -> circle radius will be 1px
[5, 1],

// zoom is 10 -> circle radius will be 2px
[10, 2]

]
}
}
```

### Example: a zoom-and-property function

Using property functions as the output value for one or more zoom stops allows
the appearance of a map feature to change with both the zoom level _and_ the
feature's properties.

```js
{
"circle-radius": {
"stops": [
// zoom is 0 and "rating" is 0 -> circle radius will be 0px
// zoom is 0 and "rating" is 5 -> circle radius will be 5px
[0, { "expression": [ "number_data", "rating" ] }]

// zoom is 20 and "rating" is 0 -> circle radius will be 4 * 0 = 0px
// zoom is 20 and "rating" is 5 -> circle radius will be 4 * 5 = 20px
[20, { "expression": [ "*", 4, ["number_data", "rating"] ] }]
]
}
}
```


## Property Expressions

Property expressions are represented using a Lisp-like structured syntax tree.

**Constants:**
- `[ "ln2" ]`
- `[ "pi" ]`
- `[ "e" ]`

**Literals:**
- JSON string / number / boolean literal

**Property lookup:**
- Feature property:
- `[ "number_data", key_expr ]` reads `feature.properties[key_expr]`, coercing it to a number if necessary.
- `[ "string_data", key_expr ]` reads `feature.properties[key_expr]`, coercing it to a string if necessary.
- `[ "boolean_data", key_expr ]` reads `feature.properties[key_expr]`, coercing it to a boolean if necessary, with `0`, `''`, `null`, and missing properties mapping to `false`, and all other values mapping to `true`.
- `[ "has", key_expr ]` returns `true` if the property is present, false otherwise.
- `[ "typeof", key_expr ]` yields the data type of `feature.properties[key_expr]`: one of `'string'`, `'number'`, `'boolean'`, `'object'`, `'array'`, or, in the case that the property is not present, `'none'`.
- `[ "geometry_type" ]` returns the value of `feature.geometry.type`.
- `[ "string_id" ]`, `[ "number_id" ]` returns the value of `feature.id`.

**Decision:**
- `["if", boolean_expr, expr_if_true, expr_if_false]`
- `["switch", [[bool_expr1, result_expr1], [bool_expr2, result_expr2], ...], default_result_expr]`
- `["match", input_expr, [[test_expr1, result_expr1], [test_expr2, result_expr2]], default_result_expr]`
- `["interval", numeric_expr, [lbound_expr1, result_expr1], [lbound_expr2, result_expr2], ...]`

**Comparison and boolean operations:**
- `[ "==", expr1, expr2]` (similar for `!=`)
- `[ ">", lhs_expr, rhs_expr ]` (similar for <, >=, <=)
- `[ "&&", boolean_expr1, boolean_expr2, ... ]` (similar for `||`)
- `[ "!", boolean_expr]`

**String:**
- `["concat", expr1, expr2, …]`
- `["upcase", string_expr]`, `["downcase", string_expr]`

**Numeric:**
- +, -, \*, /, %, ^ (e.g. `["+", expr1, expr2, expr3, …]`, `["-", expr1, expr2 ]`, etc.)
- log10, ln, log2
- sin, cos, tan, asin, acos, atan
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These trigonometric functions aren’t listed as being built into NSExpression, but we could define our own functions.

- ceil, floor, round, abs
- min, max
- `['linear', x, [ x1, y1 ], [ x2, y2 ] ]` - returns the output of the linear function determined by `(x1, y1)`, `(x2, y2)`, evaluated at `x`

**Color:**
- rgb, hsl, hcl, lab, hex, (others?)
- `["color", color_name_expr]`

2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ mapboxgl.supported = require('./util/browser').supported;
const config = require('./util/config');
mapboxgl.config = config;

mapboxgl.createFunction = require('./style-spec/function');

const rtlTextPlugin = require('./source/rtl_text_plugin');

mapboxgl.setRTLTextPlugin = rtlTextPlugin.setRTLTextPlugin;
Expand Down
Loading