mapbox / mapbox-gl-style-spec

76 stars 38 forks source link

Rationalize and simplify the taxonomy of functions #613

Closed jfirebaugh closed 7 years ago

jfirebaugh commented 7 years ago

The space of function types is currently difficult to grok because there are effectively two axes of function types:

It's not well documented or easy to grasp how these two axes combine, or what combinations are valid. I've tried to do so in #604, but this doesn't remove the underlying complexity from the domain model.

I believe that in order for users to use functions effectively, we need to rationalize and simplify our taxonomy.

Here's what I propose:

  1. A single type axis and property for functions, with the following values:
    • continuous -- a function in which stop domain values represent sample points in a continuous domain
    • discrete -- a function in which stop domain values enumerate a discrete domain
    • identity -- the identity function
    • composite -- the type formerly known as "zoom-and-property"
  2. The type property is always required and never deduced from other property values. Implementations and user interfaces can always look at this value and then decide what to do; they never have to implement a type-deduction algorithm, which would need to be synchronized between implementations and updated as new rules are introduced.
  3. A property property is optional for continuous functions, and required for discrete and identity functions (see below for composite functions). If present, the function is said to be a "data-driven function". If absent, the function is said to be a "zoom-driven function".
  4. A continuous or composite function supports the following properties:
    • interpolation -- possible values are "exponential" (default for "interpolated" properties; allowed only for interpolated properties) and "step-after" (default for "piecewise-constant" properties; replaces the previous interval function type).
    • interpolation-base -- renamed from base. Permitted only with "interpolation": "exponential".
    • interpolation-color-space -- renamed from colorSpace. Permitted only only with "interpolation": "exponential" and when the range type is color.
  5. Stop domain values for composite functions are floating point zoom level values. Stop range values for composite functions are functions, with types limited to continuous and discrete.
    • However all such "inner" functions must share the same type, property, and interpolation-* properties. The property value is always required.
    • The "outer" composite function may have interpolation-* properties that differ from the inner functions.
    • Ideally we would enforce these requirements via object structure somehow, but I'm not sure of the best way to do so.

Because this proposal involves numerous breaking changes, it would most likely have to involve introducing a new version of the style specification. I think that reducing the complexity of the function taxonomy would be worth a style version bump, even if we made no other breaking changes.

cc @mapbox/gl @mapbox/appbox

davidtheclark commented 7 years ago

One point here worth reconsidering (or maybe you'd already reconsidered it enough) is the term property. Not only is it an overloaded term, but, as @tmcw pointed out in chat, it seems inconsistent with the vector tile spec's use of attribute: https://www.mapbox.com/vector-tiles/specification/#encoding-attr, https://github.com/mapbox/vector-tile-spec/tree/master/2.1#44-feature-attributes. The more we can reduce ambiguity between property, attribute, field, etc., the better.

jfirebaugh commented 7 years ago

I'd like to stick with "property". https://github.com/mapbox/vector-tile-spec/issues/63

1ec5 commented 7 years ago

One point here worth reconsidering (or maybe you'd already reconsidered it enough) is the term property. Not only is it an overloaded term, but, as @tmcw pointed out in chat, it seems inconsistent with the vector tile spec's use of attribute

FYI, the iOS and macOS SDKs do standardize on “attribute” because “property” is already a language feature. I just couldn’t fathom MGLFeature storing properties inside a properties property (but not properties like identifier and coordinates). Fortunately, that kind of confusion seems to be less of a problem for the style and vector tile specifications than for the SDKs.

kkaefer commented 7 years ago

It seems like identity is a special case of a continuous function?

samanpwbb commented 7 years ago

I think this proposal simplifies and clarifies the function spec. The main UI challenge here will be communicating the four function types: continuous, discrete, identity, composite to users. Onces a user understands them, everything else will be much more intuitive.

Some thoughts:

jfirebaugh commented 7 years ago

An identity function returns the value of the selected property. In some cases (when the property values form a continuous domain/range), it can be viewed as a special case of continuous, but I think it's better served with a distinct type:

jfirebaugh commented 7 years ago

We could split out another type: zoom. This type would work like continuous, but it would not allow a property property, and continuous could then require property and always be "data-driven". This partitions the types:

zoom-driven: zoom data-driven: continuous, discrete, identity zoom-and-data-driven: composite

We've talked about allowing e.g. pitch-driven functions, so to generalize this further, we could name this type camera, give it a camera-property property defaulting to zoom, and call it "camera-driven" rather than "zoom-driven". (To take this approach further, I'd advocate for calling the second category "source-driven" rather than "data-driven", renaming the property property to source-property, and so on.)

1ec5 commented 7 years ago

We've talked about allowing e.g. pitch-driven functions, so to generalize this further, we could name this type camera, give it a camera-property property defaulting to zoom, and call it "camera-driven" rather than "zoom-driven".

Not to derail this discussion, and I’m not sure how practical it would be, but I wonder if it’d be more useful to vary paint and layout properties by viewing distance (as a more generalized version of zoom level) than by pitch. That is, when the pitch is 60°, a marker close to the viewer would use a more detailed icon while a marker far from the viewer would abbreviate its label more, and this would hold true as the center coordinate changes but the zoom level and pitch stay constant. I suppose a viewing distance–driven function could coexist with a camera-driven function.

lucaswoj commented 7 years ago

I'm excited about this effort to simplify the function syntax! We have learned so much about the UX of functions since the initial syntax was designed.

The taxonomy of functions proposed in https://github.com/mapbox/mapbox-gl-style-spec/issues/613#issuecomment-264524327 is great. By eliminating invalid combinations of functions we make the spec more robust, understandable, and pave the way for UI design in Studio.

I have some thoughts about the naming of these new types. Some of the names refer to the nature of the data (continuous, discrete) while others refer to the source of the data (i.e. zoom, camera, composite). Including the nature of the data and the source of the data in each name makes the taxonomy clear from the names alone:

anandthakker commented 7 years ago

This definitely clarifies and simplifies the function landscape. How does this interact with the idea of bringing in arbitrary function expressions (https://github.com/mapbox/mapbox-gl-function/issues/28)? Would that provide an alternative to some of the concepts in this proposal? For instance:

lucaswoj commented 7 years ago

It is possible to simplify the taxonomy further by making the domain values the source of truth for the "nature" of the data (continuous vs discrete vs identity)

I recognize that this taxonomy does not lend itself well to implementation in a static type system. We may need to explicitly differentiate between continuous, discrete and identity functions in our GL Native type system.

jfirebaugh commented 7 years ago

a numeric or numeric array domain indicates a continuous function

A numeric does not necessarily indicate a continuous function. Numeric values are sometimes used as enumerated constants.

jfirebaugh commented 7 years ago

I'm purposefully leaving arbitrary function expressions out of this proposal. We have implementation experience (and are developing UI experience) for the function types in this proposal, and the proposal is guided by that experience. We don't have equivalent experience with function expressions.

lucaswoj commented 7 years ago

A numeric does not necessarily indicate a continuous function. Numeric values are sometimes used as enumerated constants.

This is a moot point in practice. If the domain is a set of enumerated constants, there's no* input value for which the output value of a numeric "source-continuous" function is different than the output value of a "source-discrete" function.

{
    type: "source-continuous",
    stops: [[0, 0], [2, 4]]
}

{
    type: "source-discrete",
    stops: [[0, 0], [2, 4]]
}

* Depending our implementation, invalid domain values may result in a different output values. The behaviour in this case could be controlled by the user via the interpolation parameter.

jfirebaugh commented 7 years ago

Earlier I said I'd prefer not to merge identity with continuous, but I can imagine merging all three of continuous, discrete and identity into a single source type. identity would be replaced with a source type function without stops or interpolation-*, while discrete would be replaced with a source type function with "interpolation": "none".

This produces three types: camera, source, and composite, and suggests a UI that first asks what the input to the function should be, rather than asking for a function type. For example, this could be a dropdown with options:

If "zoom" or "pitch" is chosen, it's a camera function with the selected camera-property. If a source property is chosen, it's source, and then metadata about the property values can guide further choices (e.g. if the values are strings, it's got to be "interpolation": "none"; if they're numeric, provide an "interpolation" UI option defaulted to "exponential").

jfirebaugh commented 7 years ago

This is a moot point in practice.

It isn't moot once we introduce default values for discrete functions. https://github.com/mapbox/mapbox-gl-style-spec/issues/480

lucaswoj commented 7 years ago

It isn't moot once we introduce default values for discrete functions. #480

Good point. Could it make sense to have a "interpolation": "default-value" mode?

* Depending our implementation, invalid domain values may result in a different output values. The behaviour in this case could be controlled by the user via the interpolation parameter.

mourner commented 7 years ago

Love the proposal! This will certainly make things much easier to reason about. The only point I'm not sure about is the composite functions proposal (item 5 in the post) — turning them into nested functions where outer/inner functions can have different interpolations sounds pretty complicated (in addition to making the definitions huge). As far as I recall, @ansis considered this at some point but rejected it because there are technical difficulties in implementation. Is this right?

samanpwbb commented 7 years ago

This produces three types: camera, source, and composite, and suggests a UI that first asks what the input to the function should be, rather than asking for a function type.

We would likely design a UI like this either way so I am in favor writing it into the spec so our ui models the spec better.

jfirebaugh commented 7 years ago

This produces three types: camera, source, and composite...

These three types correspond to three fundamental strategies in the rendering implementation:

So camera/source/composite forms a taxonomy that is a good model for both the implementation and the UI. This is a strong indication that it's a good taxonomy.

lucaswoj commented 7 years ago

This issue was moved to mapbox/mapbox-gl-js#4154