diff --git a/build/vega-lite-schema.json b/build/vega-lite-schema.json index e2962a423b..88c8cc8e99 100644 --- a/build/vega-lite-schema.json +++ b/build/vega-lite-schema.json @@ -20265,105 +20265,272 @@ "additionalProperties": false, "properties": { "center": { - "$ref": "#/definitions/Vector2", - "description": "The projection's center, a two-element array of longitude and latitude in degrees.\n\n__Default value:__ `[0, 0]`" + "anyOf": [ + { + "$ref": "#/definitions/Vector2", + "description": "The projection's center, a two-element array of longitude and latitude in degrees.\n\n__Default value:__ `[0, 0]`" + }, + { + "$ref": "#/definitions/ExprRef" + } + ] }, "clipAngle": { - "description": "The projection's clipping circle radius to the specified angle in degrees. If `null`, switches to [antimeridian](http://bl.ocks.org/mbostock/3788999) cutting rather than small-circle clipping.", - "type": "number" + "anyOf": [ + { + "description": "The projection's clipping circle radius to the specified angle in degrees. If `null`, switches to [antimeridian](http://bl.ocks.org/mbostock/3788999) cutting rather than small-circle clipping.", + "type": "number" + }, + { + "$ref": "#/definitions/ExprRef" + } + ] }, "clipExtent": { - "$ref": "#/definitions/Vector2>", - "description": "The projection's viewport clip extent to the specified bounds in pixels. The extent bounds are specified as an array `[[x0, y0], [x1, y1]]`, where `x0` is the left-side of the viewport, `y0` is the top, `x1` is the right and `y1` is the bottom. If `null`, no viewport clipping is performed." + "anyOf": [ + { + "$ref": "#/definitions/Vector2>", + "description": "The projection's viewport clip extent to the specified bounds in pixels. The extent bounds are specified as an array `[[x0, y0], [x1, y1]]`, where `x0` is the left-side of the viewport, `y0` is the top, `x1` is the right and `y1` is the bottom. If `null`, no viewport clipping is performed." + }, + { + "$ref": "#/definitions/ExprRef" + } + ] }, "coefficient": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "$ref": "#/definitions/ExprRef" + } + ] }, "distance": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "$ref": "#/definitions/ExprRef" + } + ] }, "extent": { - "$ref": "#/definitions/Vector2>" + "anyOf": [ + { + "$ref": "#/definitions/Vector2>" + }, + { + "$ref": "#/definitions/ExprRef" + } + ] }, "fit": { "anyOf": [ { - "$ref": "#/definitions/Fit" + "items": { + "$ref": "#/definitions/GeoJsonFeature" + }, + "type": "array" }, { "items": { "$ref": "#/definitions/Fit" }, "type": "array" + }, + { + "$ref": "#/definitions/ExprRef" } ] }, "fraction": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "$ref": "#/definitions/ExprRef" + } + ] }, "lobes": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "$ref": "#/definitions/ExprRef" + } + ] }, "parallel": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "$ref": "#/definitions/ExprRef" + } + ] }, "parallels": { - "description": "For conic projections, the [two standard parallels](https://en.wikipedia.org/wiki/Map_projection#Conic) that define the map layout. The default depends on the specific conic projection used.", - "items": { - "type": "number" - }, - "type": "array" + "anyOf": [ + { + "description": "For conic projections, the [two standard parallels](https://en.wikipedia.org/wiki/Map_projection#Conic) that define the map layout. The default depends on the specific conic projection used.", + "items": { + "type": "number" + }, + "type": "array" + }, + { + "$ref": "#/definitions/ExprRef" + } + ] }, "pointRadius": { - "description": "The default radius (in pixels) to use when drawing GeoJSON `Point` and `MultiPoint` geometries. This parameter sets a constant default value. To modify the point radius in response to data, see the corresponding parameter of the GeoPath and GeoShape transforms.\n\n__Default value:__ `4.5`", - "type": "number" + "anyOf": [ + { + "description": "The default radius (in pixels) to use when drawing GeoJSON `Point` and `MultiPoint` geometries. This parameter sets a constant default value. To modify the point radius in response to data, see the corresponding parameter of the GeoPath and GeoShape transforms.\n\n__Default value:__ `4.5`", + "type": "number" + }, + { + "$ref": "#/definitions/ExprRef" + } + ] }, "precision": { - "description": "The threshold for the projection's [adaptive resampling](http://bl.ocks.org/mbostock/3795544) to the specified value in pixels. This value corresponds to the [Douglas–Peucker distance](http://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm). If precision is not specified, returns the projection's current resampling precision which defaults to `√0.5 ≅ 0.70710…`.", - "type": "number" + "anyOf": [ + { + "description": "The threshold for the projection's [adaptive resampling](http://bl.ocks.org/mbostock/3795544) to the specified value in pixels. This value corresponds to the [Douglas–Peucker distance](http://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm). If precision is not specified, returns the projection's current resampling precision which defaults to `√0.5 ≅ 0.70710…`.", + "type": "number" + }, + { + "$ref": "#/definitions/ExprRef" + } + ] }, "radius": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "$ref": "#/definitions/ExprRef" + } + ] }, "ratio": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "$ref": "#/definitions/ExprRef" + } + ] }, "reflectX": { - "type": "boolean" + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/definitions/ExprRef" + } + ] }, "reflectY": { - "type": "boolean" + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/definitions/ExprRef" + } + ] }, "rotate": { "anyOf": [ { - "$ref": "#/definitions/Vector2" + "anyOf": [ + { + "$ref": "#/definitions/Vector2" + }, + { + "$ref": "#/definitions/Vector3" + } + ], + "description": "The projection's three-axis rotation to the specified angles, which must be a two- or three-element array of numbers [`lambda`, `phi`, `gamma`] specifying the rotation angles in degrees about each spherical axis. (These correspond to yaw, pitch and roll.)\n\n__Default value:__ `[0, 0, 0]`" }, { - "$ref": "#/definitions/Vector3" + "$ref": "#/definitions/ExprRef" } - ], - "description": "The projection's three-axis rotation to the specified angles, which must be a two- or three-element array of numbers [`lambda`, `phi`, `gamma`] specifying the rotation angles in degrees about each spherical axis. (These correspond to yaw, pitch and roll.)\n\n__Default value:__ `[0, 0, 0]`" + ] }, "scale": { - "description": "The projection’s scale (zoom) factor, overriding automatic fitting. The default scale is projection-specific. The scale factor corresponds linearly to the distance between projected points; however, scale factor values are not equivalent across projections.", - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "$ref": "#/definitions/ExprRef" + } + ], + "description": "The projection’s scale (zoom) factor, overriding automatic fitting. The default scale is projection-specific. The scale factor corresponds linearly to the distance between projected points; however, scale factor values are not equivalent across projections." }, "size": { - "$ref": "#/definitions/Vector2" + "anyOf": [ + { + "$ref": "#/definitions/Vector2" + }, + { + "$ref": "#/definitions/ExprRef" + } + ] }, "spacing": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "$ref": "#/definitions/ExprRef" + } + ] }, "tilt": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "$ref": "#/definitions/ExprRef" + } + ] }, "translate": { - "$ref": "#/definitions/Vector2", + "anyOf": [ + { + "$ref": "#/definitions/Vector2" + }, + { + "$ref": "#/definitions/ExprRef" + } + ], "description": "The projection’s translation offset as a two-element array `[tx, ty]`." }, "type": { - "$ref": "#/definitions/ProjectionType", + "anyOf": [ + { + "$ref": "#/definitions/ProjectionType" + }, + { + "$ref": "#/definitions/ExprRef" + } + ], "description": "The cartographic projection to use. This value is case-insensitive, for example `\"albers\"` and `\"Albers\"` indicate the same projection type. You can find all valid projection types [in the documentation](https://vega.github.io/vega-lite/docs/projection.html#projection-types).\n\n__Default value:__ `equalEarth`" } }, diff --git a/examples/compiled/geo_params_projections.png b/examples/compiled/geo_params_projections.png new file mode 100644 index 0000000000..fafb76b22a Binary files /dev/null and b/examples/compiled/geo_params_projections.png differ diff --git a/examples/compiled/geo_params_projections.svg b/examples/compiled/geo_params_projections.svg new file mode 100644 index 0000000000..f281a71d7f --- /dev/null +++ b/examples/compiled/geo_params_projections.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/compiled/geo_params_projections.vg.json b/examples/compiled/geo_params_projections.vg.json new file mode 100644 index 0000000000..e876394f89 --- /dev/null +++ b/examples/compiled/geo_params_projections.vg.json @@ -0,0 +1,65 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "background": "white", + "padding": 5, + "width": 500, + "height": 300, + "style": "cell", + "data": [ + { + "name": "source_0", + "url": "data/world-110m.json", + "format": {"type": "topojson", "feature": "countries"} + } + ], + "projections": [ + { + "name": "projection", + "size": {"signal": "[width, height]"}, + "fit": {"signal": "data('source_0')"}, + "type": {"signal": "projection"} + } + ], + "signals": [ + { + "name": "projection", + "value": "equalEarth", + "bind": { + "input": "select", + "options": [ + "albers", + "albersUsa", + "azimuthalEqualArea", + "azimuthalEquidistant", + "conicConformal", + "conicEqualArea", + "conicEquidistant", + "equalEarth", + "equirectangular", + "gnomonic", + "mercator", + "naturalEarth1", + "orthographic", + "stereographic", + "transverseMercator" + ] + } + } + ], + "marks": [ + { + "name": "marks", + "type": "shape", + "style": ["geoshape"], + "from": {"data": "source_0"}, + "encode": { + "update": { + "fill": {"value": "lightgray"}, + "stroke": {"value": "gray"}, + "ariaRoleDescription": {"value": "geoshape"} + } + }, + "transform": [{"type": "geoshape", "projection": "projection"}] + } + ] +} diff --git a/examples/specs/geo_params_projections.vl.json b/examples/specs/geo_params_projections.vl.json new file mode 100644 index 0000000000..5abcba4393 --- /dev/null +++ b/examples/specs/geo_params_projections.vl.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v4.json", + "width": 500, + "height": 300, + "params": [ + { + "name": "projection", + "value": "equalEarth", + "bind": { + "input": "select", + "options": [ + "albers", + "albersUsa", + "azimuthalEqualArea", + "azimuthalEquidistant", + "conicConformal", + "conicEqualArea", + "conicEquidistant", + "equalEarth", + "equirectangular", + "gnomonic", + "mercator", + "naturalEarth1", + "orthographic", + "stereographic", + "transverseMercator" + ] + } + } + ], + "data": { + "url": "data/world-110m.json", + "format": {"type": "topojson", "feature": "countries"} + }, + "projection": {"type": {"expr": "projection"}}, + "mark": {"type": "geoshape", "fill": "lightgray", "stroke": "gray"} +} diff --git a/site/_data/examples.json b/site/_data/examples.json index 3afd0047b0..df67f83d1c 100644 --- a/site/_data/examples.json +++ b/site/_data/examples.json @@ -736,6 +736,12 @@ "title": "London Tube Lines", "description": "This example was created by @jwoLondon.", "png": true + }, + { + "name": "geo_params_projections", + "title": "Projection explorer", + "description": "Compare different projections.", + "png": true } ] }, diff --git a/src/compile/projection/component.ts b/src/compile/projection/component.ts index 7373e4626b..c6d5fd09a5 100644 --- a/src/compile/projection/component.ts +++ b/src/compile/projection/component.ts @@ -1,6 +1,5 @@ -import {SignalRef} from 'vega'; +import {Projection as VgProjection, SignalRef} from 'vega'; import {Projection} from '../../projection'; -import {Projection as VgProjection} from 'vega'; import {Split} from '../split'; export class ProjectionComponent extends Split { @@ -8,7 +7,7 @@ export class ProjectionComponent extends Split { constructor( name: string, - public specifiedProjection: Projection, + public specifiedProjection: Projection, public size: SignalRef[], public data: (string | SignalRef)[] ) { diff --git a/src/compile/projection/parse.ts b/src/compile/projection/parse.ts index 2933dee2e8..b64d4355db 100644 --- a/src/compile/projection/parse.ts +++ b/src/compile/projection/parse.ts @@ -3,6 +3,7 @@ import {hasOwnProperty} from 'vega-util'; import {LATITUDE, LATITUDE2, LONGITUDE, LONGITUDE2, SHAPE} from '../../channel'; import {getFieldOrDatumDef} from '../../channeldef'; import {DataSourceType} from '../../data'; +import {replaceExprRefInIndex} from '../../expr'; import {PROJECTION_PROPERTIES} from '../../projection'; import {GEOJSON} from '../../type'; import {deepEqual, duplicate, every} from '../../util'; @@ -16,7 +17,7 @@ export function parseProjection(model: Model) { function parseUnitProjection(model: UnitModel): ProjectionComponent { if (model.hasProjection) { - const proj = model.specifiedProjection; + const proj = replaceExprRefInIndex(model.specifiedProjection); const fit = !(proj && (proj.scale != null || proj.translate != null)); const size = fit ? [model.getSizeSignalRef('width'), model.getSizeSignalRef('height')] : undefined; const data = fit ? gatherFitData(model) : undefined; @@ -24,7 +25,7 @@ function parseUnitProjection(model: UnitModel): ProjectionComponent { const projComp = new ProjectionComponent( model.projectionName(true), { - ...(model.config.projection ?? {}), + ...(replaceExprRefInIndex(model.config.projection) ?? {}), ...(proj ?? {}) }, size, diff --git a/src/compile/unit.ts b/src/compile/unit.ts index 8f72e41f8c..87b60800f3 100644 --- a/src/compile/unit.ts +++ b/src/compile/unit.ts @@ -27,7 +27,7 @@ import {Config} from '../config'; import {isGraticuleGenerator} from '../data'; import * as vlEncoding from '../encoding'; import {Encoding, initEncoding} from '../encoding'; -import {ExprOrSignalRef, replaceExprRefInIndex} from '../expr'; +import {ExprOrSignalRef, ExprRef, replaceExprRefInIndex} from '../expr'; import {LegendInternal} from '../legend'; import {GEOSHAPE, isMarkDef, Mark, MarkDef} from '../mark'; import {Projection} from '../projection'; @@ -74,7 +74,7 @@ export class UnitModel extends ModelWithField { protected specifiedLegends: LegendInternalIndex = {}; - public specifiedProjection: Projection = {}; + public specifiedProjection: Projection = {}; public readonly selection: Dict = {}; public children: Model[] = []; diff --git a/src/log/message.ts b/src/log/message.ts index 8f9278d0fa..15d8a9468e 100644 --- a/src/log/message.ts +++ b/src/log/message.ts @@ -1,7 +1,7 @@ /** * Collection of all Vega-Lite Error Messages */ -import {AggregateOp} from 'vega'; +import {AggregateOp, SignalRef} from 'vega'; import {Aggregate} from '../aggregate'; import { Channel, @@ -17,6 +17,7 @@ import {SplitParentProperty} from '../compile/split'; import {CompositeMark} from '../compositemark'; import {ErrorBarCenter, ErrorBarExtent} from '../compositemark/errorbar'; import {DateTime, DateTimeExpr} from '../datetime'; +import {ExprRef} from '../expr'; import {Mark} from '../mark'; import {Projection} from '../projection'; import {ScaleType} from '../scale'; @@ -132,7 +133,10 @@ export function customFormatTypeNotAllowed(channel: ExtendedChannel) { return `Config.customFormatTypes is not true, thus custom format type and format for channel ${channel} are dropped.`; } -export function projectionOverridden(opt: {parentProjection: Projection; projection: Projection}) { +export function projectionOverridden(opt: { + parentProjection: Projection; + projection: Projection; +}) { const {parentProjection, projection} = opt; return `Layer's shared projection ${stringify(parentProjection)} is overridden by a child projection ${stringify( projection diff --git a/src/normalize/base.ts b/src/normalize/base.ts index 50f79d8115..2775379f17 100644 --- a/src/normalize/base.ts +++ b/src/normalize/base.ts @@ -2,6 +2,7 @@ import {SignalRef} from 'vega'; import {FieldName} from '../channeldef'; import {Config} from '../config'; import {Encoding} from '../encoding'; +import {ExprRef} from '../expr'; import {Projection} from '../projection'; import {GenericSpec, NormalizedSpec} from '../spec'; import {GenericLayerSpec, NormalizedLayerSpec} from '../spec/layer'; @@ -38,7 +39,7 @@ export type NormalizeLayerOrUnit = Normalize< export interface NormalizerParams { config: Config; parentEncoding?: Encoding; - parentProjection?: Projection; + parentProjection?: Projection; repeater?: RepeaterValue; repeaterPrefix?: string; } diff --git a/src/normalize/core.ts b/src/normalize/core.ts index b441be580c..4fbc405afc 100644 --- a/src/normalize/core.ts +++ b/src/normalize/core.ts @@ -1,3 +1,4 @@ +import {SignalRef} from 'vega'; import {isArray} from 'vega-util'; import {COLUMN, FACET, ROW} from '../channel'; import {Field, FieldName, hasConditionalFieldOrDatumDef, isFieldOrDatumDef, isValueDef} from '../channeldef'; @@ -6,6 +7,7 @@ import {boxPlotNormalizer} from '../compositemark/boxplot'; import {errorBandNormalizer} from '../compositemark/errorband'; import {errorBarNormalizer} from '../compositemark/errorbar'; import {channelHasField, Encoding} from '../encoding'; +import {ExprRef} from '../expr'; import * as log from '../log'; import {Projection} from '../projection'; import {FacetedUnitSpec, GenericSpec, LayerSpec, UnitSpec} from '../spec'; @@ -377,7 +379,10 @@ function mergeEncoding({ return !merged || isEmpty(merged) ? undefined : merged; } -function mergeProjection(opt: {parentProjection: Projection; projection: Projection}) { +function mergeProjection(opt: { + parentProjection: Projection; + projection: Projection; +}) { const {parentProjection, projection} = opt; if (parentProjection && projection) { log.warn(log.message.projectionOverridden({parentProjection, projection})); diff --git a/src/projection.ts b/src/projection.ts index 66c1adec74..1eca2fed43 100644 --- a/src/projection.ts +++ b/src/projection.ts @@ -1,31 +1,33 @@ import {BaseProjection, SignalRef, Vector2} from 'vega'; -import {ProjectionType} from './vega.schema'; +import {ExprRef} from './expr'; +import {MapExcludeValueRefAndReplaceSignalWith, ProjectionType} from './vega.schema'; -export interface Projection extends BaseProjection { +export interface Projection + extends MapExcludeValueRefAndReplaceSignalWith { /** * The cartographic projection to use. This value is case-insensitive, for example `"albers"` and `"Albers"` indicate the same projection type. You can find all valid projection types [in the documentation](https://vega.github.io/vega-lite/docs/projection.html#projection-types). * * __Default value:__ `equalEarth` */ - type?: ProjectionType | SignalRef; // Re-declare to override docs + type?: ProjectionType | ES; // Re-declare to override docs /** * The projection’s scale (zoom) factor, overriding automatic fitting. The default scale is projection-specific. The scale factor corresponds linearly to the distance between projected points; however, scale factor values are not equivalent across projections. */ - scale?: number | SignalRef; // Re-declare to override docs + scale?: number | ES; // Re-declare to override docs /** * The projection’s translation offset as a two-element array `[tx, ty]`. */ - translate?: Vector2 | SignalRef; // TODO: figure what's VL default value + translate?: Vector2 | ES; // TODO: figure what's VL default value } /** * Any property of Projection can be in config */ -export type ProjectionConfig = Projection; +export type ProjectionConfig = Projection; -export const PROJECTION_PROPERTIES: (keyof Projection)[] = [ +export const PROJECTION_PROPERTIES: (keyof Projection)[] = [ 'type', 'clipAngle', 'clipExtent', diff --git a/src/spec/layer.ts b/src/spec/layer.ts index beb903aba9..aafab34e68 100644 --- a/src/spec/layer.ts +++ b/src/spec/layer.ts @@ -1,5 +1,6 @@ import {Field} from '../channeldef'; import {SharedCompositeEncoding} from '../compositemark'; +import {ExprRef} from '../expr'; import {Projection} from '../projection'; import {BaseSpec, FrameMixins, ResolveMixins} from './base'; import {GenericUnitSpec, NormalizedUnitSpec, UnitSpec} from './unit'; @@ -35,7 +36,7 @@ export interface LayerSpec extends BaseSpec, FrameMixins, Resol /** * An object defining properties of the geographic projection shared by underlying layers. */ - projection?: Projection; + projection?: Projection; } /** diff --git a/src/spec/unit.ts b/src/spec/unit.ts index ce2b23e791..74c1124be0 100644 --- a/src/spec/unit.ts +++ b/src/spec/unit.ts @@ -1,10 +1,11 @@ -import {Field} from './../channeldef'; import {FieldName} from '../channeldef'; import {CompositeEncoding, FacetedCompositeEncoding} from '../compositemark'; import {Encoding} from '../encoding'; +import {ExprRef} from '../expr'; import {AnyMark, Mark, MarkDef} from '../mark'; import {Projection} from '../projection'; import {SelectionDef} from '../selection'; +import {Field} from './../channeldef'; import { BaseSpec, DataMixins, @@ -33,7 +34,7 @@ export interface GenericUnitSpec, M> extends BaseSpec { * An object defining properties of geographic projection, which will be applied to `shape` path for `"geoshape"` marks * and to `latitude` and `"longitude"` channels for other marks. */ - projection?: Projection; + projection?: Projection; /** * A key-value mapping between selection names and definitions. diff --git a/src/timeunit.ts b/src/timeunit.ts index 975da045fa..36a8d055ec 100644 --- a/src/timeunit.ts +++ b/src/timeunit.ts @@ -1,7 +1,6 @@ -import stringify from 'fast-json-stable-stringify'; import {isObject, isString} from 'vega-util'; import {DateTimeExpr, dateTimeExprToExpr} from './datetime'; -import {accessPathWithDatum, keys, varName} from './util'; +import {accessPathWithDatum, keys, stringify, varName} from './util'; /** Time Unit that only corresponds to only one part of Date objects. */ export const LOCAL_SINGLE_TIMEUNIT_INDEX = {