-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
Closed
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
d002cce
Implement quick draft for an expression spec
09f8a2b
Wire up draft impl of expression functions
9576516
Add expression syntax docs
f32b98d
Incorporate syntax suggestions/feedback
5d14e1d
Reflect syntax updates in implementation, prepare for type checking
fc7948f
Draft implement type checking
04167e1
Add basic color functions
7caa399
Add tests for !, &&, ||
f52532f
Remove "between", "in"
c72ed74
Add unit test for constants
439b908
Add geometry_type
f8ff159
Add {string,number}_id ops
1464a8b
Return compile errors instead of throwing
a7fc39c
compiledExpression => expressionString
2b95529
Allow zoomInterpolationBase property on expression functions
7bec2c8
Hack quick chart to visualize expression function on debug page
d9d722e
Remove [zoom] in favor of stops
291582f
Add `interval`, `linear`, and `color` expressions
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
- 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]` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.