Skip to content

Add data driven styling capabilities #2

Merged
lucaswoj merged 22 commits intomasterfrom
scales
Jun 18, 2015
Merged

Add data driven styling capabilities #2
lucaswoj merged 22 commits intomasterfrom
scales

Conversation

@lucaswoj
Copy link

This pull request adds support for the new scale syntax including

  • added domain and range properties in addition to steps
  • added the ability for scales to accept more attributes (i.e. not just "zoom") and calculate outputs based on them via the property parameter
  • added rounding parameter for more flexible and explicit discrete scales
  • maintained backwards compatibility with functions

cc @ansis @jfirebaugh

@lucaswoj
Copy link
Author

@jfirebaugh Discrete scales should be explicitly denoted by their rounding parameter

@lucaswoj
Copy link
Author

Ok, now I pinky swear to not make any more commits to this PR ☺️

test/test.js Outdated
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the thinking behind supporting things like scale({mapbox: 1}, {mapbox: 3}), scale(3, {mapbox: 1}), and scale({mapbox: 1}, 3)?

Copy link
Author

Choose a reason for hiding this comment

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

In the short term we're going to call a scale as scale(5, {mapbox: 1}) (where 5 is the zoom) but in the long term, we might want additional attributes like $hover or $time. This argument syntax gives us flexibility to pass in a bunch of different attributes without having to do too much object construction.

@jfirebaugh
Copy link
Contributor

Nice work. There are a few stylistic nits that I'll give line-by-line comments for, and a few architectural issues.

I still think we will want scales to be explicitly typed. I'll write up why in detail later.

The API here has some features that I'm not sure are justified in terms of their cost-benefit:

  • Continued support of legacy format. Let's pull the support for this into a separate function that can migrate from old to new. That will be useful in the style migration script, and allow the core code to process one consistent format only.
  • Multiple ways to pass parameters, i.e. scale(0) versus scale({$zoom: 0}) and scale({mapbox: 1}, {mapbox: 3}), scale(3, {mapbox: 1}), and scale({mapbox: 1}, 3). I think we should break backward compatibility in order to standardize on a single convention. See below for thoughts on an optimal convention.
  • Rounding modes. As far as I know, the single rounding behavior that piecewise-constant functions provides has been entirely sufficient. Consider what value none, floor, and ceiling provide, and weigh that against the costs of added code (in both js and native), tests, documentation, and editor UI.

Parameter passing and partial evaluation

See the discussion thread on mapbox/mapbox-gl-js#1267. I think we want this module to support partial evaluation of functions over what you elsewhere called "Global Attributes", which is a term I like.

Example of proposed API use:

var scale = MapboxGLScale({
  domain: [1, 3],
  range: [2, 6],
  property: '$zoom'
});

var partial1 = scale.globalEval({zoom: 1});
partial1({mapbox: 2}); // 2
partial1({mapbox: 3}); // 2

var partial2 = scale.globalEval({zoom: 2});
partial2({mapbox: 2}); // 4
partial2({mapbox: 3}); // 4

// Potential shorthand notation - we may not need this
scale({zoom: 1}, {mapbox: 2}); // 2
scale({zoom: 2}, {mapbox: 2}); // 4

This is nice because we can optimize the result of scale.globalEval to be a function that ignores its argument and returns a constant value when the scale does not depend on feature properties.

index.js Outdated
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't mutate input parameters.

@lucaswoj
Copy link
Author

I still think we will want scales to be explicitly typed. I'll write up why in detail later.

I have definitely come to agree with you here. I think specifying a rounding parameter is equivalent to and more desirable than a type parameter, in the world of discrete and continuous scales. My intentions here are to

  • create conceptually continuous parameterized design space for users of the editor GUI

  • avoid separate code paths for different types until we absolutely need to

  • avoid supporting different features for different types until we absolutely need to

  • allow continuous inputs to be mapped to discrete outputs without the complexity of d3 scales

    Continued support of legacy format. Let's pull the support for this into a separate function that can migrate from old to new. That will be useful in the style migration script, and allow the core code to process one consistent format only.

Ok! I'll do that.

Multiple ways to pass parameters, i.e. scale(0) versus scale({$zoom: 0}) and scale({mapbox: 1}, {mapbox: 3}), scale(3, {mapbox: 1}), and scale({mapbox: 1}, 3). I think we should break backward compatibility in order to standardize on a single convention. See below for thoughts on an optimal convention.

I'm on board with dropping support for numeric arguments, automatically converted to {$zoom: ...} objects.

The main goal with the scale(object, object, ...) syntax was avoiding unnecessary object construction and cloning. If we move to partial evaluation, that problem goes away in the short term (no need to add $zoom to feature attributes).

Rounding modes. As far as I know, the single rounding behavior that piecewise-constant functions provides has been entirely sufficient. Consider what value none, floor, and ceiling provide, and weigh that against the costs of added code (in both js and native), tests, documentation, and editor UI.

Dropping the ceiling mode is fine by me.

The floor vs normal modes are intended to be the way we differentiate between continuous and discrete function types, so I'd like to keep those or replace them with a type parameter.

See the discussion thread on mapbox/mapbox-gl-js#1267. I think we want this module to support partial evaluation of functions over what you elsewhere called "Global Attributes", which is a term I like.

Yup, this seems pretty obvious in retrospect.

Next Steps

  • Move legacy support into a separate function
  • Drop support for numerical $zoom arguments
  • Refactor to use partial evaluation for global attributes / feature attributes
  • Remove the ceiling rounding mode.
  • Add support + tests for string and boolean valued domains
  • Move validation into a separate function

test/test.js Outdated
Copy link
Contributor

Choose a reason for hiding this comment

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

Add tests for:

  • string-valued domains
  • boolean-valued domains
  • Scale({domain: [1], range: [2], property: 'p'})({p: 'omg'})
  • Scale({domain: ['A', 'B', 'C'], range: [1, 2, 3], property: 'p'})({p: 42})
  • Scale({domain: ['A', 'B', 'C'], range: [1, 2, 3], property: 'p'})({p: 'D'})

What other corner cases are there?

@jfirebaugh
Copy link
Contributor

API proposal:

module.exports = create;
module.exports.migrate = function() { ... }; // migration function, to be used by gl-style-migrate
module.exports.validate = function() { ... }; // validation function, to be used by gl-style-validate and internal validation

@jfirebaugh
Copy link
Contributor

Next Steps

👍 -- perfect

@lucaswoj
Copy link
Author

@jfirebaugh 🎁

@lucaswoj
Copy link
Author

@jfirebaugh Is this ready to go?

index.js Outdated
Copy link
Contributor

Choose a reason for hiding this comment

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

Pull the inner function out so it's not creating a new one every time:

var constant = function() { return parameters; };
return function() { return constant; }

index.js Outdated
Copy link
Contributor

Choose a reason for hiding this comment

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

This is where partial evaluation is implemented:

var value = evaluate(parameters, attribute);
return function() { return value; }

@lucaswoj
Copy link
Author

Ok! I made the changes you requested as well as

package.json Outdated
Copy link
Author

Choose a reason for hiding this comment

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

I'd like to merge mapbox/mapbox-gl-style-spec#290 before we merge this PR so that we can use the slightly more stable v8 branch as a dependency instead.

index.js Outdated
Copy link
Contributor

Choose a reason for hiding this comment

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

it would be useful to have isConstant on the outer function here

Copy link
Author

Choose a reason for hiding this comment

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

Ok. I was just going with undefined == false but its probably good to be explicit here.

Copy link
Contributor

Choose a reason for hiding this comment

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

oh, I didn't realize isConstant applies to the partial function so I was thinking it would be true.

When creating the buffers we don't need to evaluate the scale at all if it's zoom based. Right now there is no way to tell unless you evaluate the first half. It's probably fine performance-wise to always evaluate it, but it might be nice to not need not.

I was trying https://github.com/mapbox/mapbox-gl-js/blob/83ca3f19cf8fd428f3fa1338b0209292eb9a97a3/js/data/line_bucket.js#L19 but I guess I need to evaluate the first half always

Copy link
Author

Choose a reason for hiding this comment

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

Makes sense. We could have isGlobalConstant / isFeatureConstant properties.

Copy link
Author

Choose a reason for hiding this comment

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

Or would you rather have an isConstantWithRespectToZoom property?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think isFeatureConstant would be good for this case

Copy link
Author

Choose a reason for hiding this comment

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

Cool! Added in the latest diff.

@lucaswoj lucaswoj changed the title Add new scale syntax Add data driven styling capabilities Jun 18, 2015
@lucaswoj
Copy link
Author

@jfirebaugh this is ready for 👀

@jfirebaugh
Copy link
Contributor

:shipit:!

@lucaswoj
Copy link
Author

@jfirebaugh Are we going to break anything by merging this into master right now? Maybe we should keep this in a separate branch for now or pin upstream dependencies to a specific tag of this repository

@jfirebaugh
Copy link
Contributor

Merging is safe. Dependent modules are using a versioned release from npm, so they will be fine. If integration in gl-js goes well, we'll publish the changes as v2.0.0.

@lucaswoj
Copy link
Author

Ok, just bumped the version. Will merge as soon as CI passes.

lucaswoj added a commit that referenced this pull request Jun 18, 2015
Add data driven styling capabilities
@lucaswoj lucaswoj merged commit 3e296af into master Jun 18, 2015
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 this pull request may close these issues.

3 participants