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

Geo #2734

Merged
merged 10 commits into from
Jan 25, 2018
Merged

Geo #2734

merged 10 commits into from
Jan 25, 2018

Conversation

willium
Copy link
Member

@willium willium commented Aug 1, 2017

Add Geo to Vega-Lite

  • Add examples
  • Implement
  • Add tests
  • Add docs
  • Add interaction support (@arvind)

choropleth
repeat
trellis
geo_layer
geo_point
geo_text
geo_line
image

if (model instanceof UnitModel) {
model.component.projection = parseUnitProjection(model, config);
} else {
const mergedProjection = parseNonUnitProjections(model);
Copy link
Member

Choose a reason for hiding this comment

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

model.component.projection =

Copy link
Member Author

Choose a reason for hiding this comment

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

ah right, I was going to check if undefined

// TODO: make sure this actually works for both kinds of lookups, with GeoJSON transform, and with basic projection
const data = model.requestDataName(MAIN);

const width = model.getSizeSignalRef('width');
Copy link
Member

Choose a reason for hiding this comment

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

I suggest using {signal: model.getName('width')} as I am about to restrict getSizeSignalRef() for using in assemble only.

If you haven't, make sure that in assemble, you replace size signals with updated names.


const isGeoshapeMark = markDef && markDef.type === GEOSHAPE;
const hasEncodedProjection = encoding && [X, Y, SHAPE].some((channel) => isProjectionFieldDef(encoding[channel]));
const inheritedProjection = config && config.projection;
Copy link
Member

Choose a reason for hiding this comment

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

config is never null -- you can just read config.projection when needed.

}
}

function parseUnitProjection(model: UnitModel, config: Config): ProjectionComponent {
Copy link
Member

Choose a reason for hiding this comment

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

No need to take config as parameter. You can access it from model.config.
The init part already passed config to all models in the model tree.

const merged = model.children.reduce((projections, child) => {
const projection = child.component.projection;
if (projection) {
projections.forEach((other) => {
Copy link
Member

Choose a reason for hiding this comment

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

Here you seems to always merge -- see parseNonUnitScaleCore how I make two passes over the children. The first pass checks whether I can merge all of them. If can merge, then make second pass to really merge.

return SCALE_CHANNELS.reduce((scaleComponents: ScaleComponentIndex, channel: ScaleChannel) => {
const scaleComponents: ScaleComponentIndex = {};
SCALE_CHANNELS.forEach((channel: ScaleChannel) => {
if (mark === GEOSHAPE && channel === SHAPE) {
Copy link
Member

Choose a reason for hiding this comment

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

Add comment why geoshape's shape is always totally skipped?

@willium willium force-pushed the geo-lite branch 10 times, most recently from 86c9f35 to 71b8d5d Compare August 5, 2017 02:44
let fieldDef: FieldDef<string>;
let specifiedScale: Scale = {};

const channelDef = encoding[channel];

if (isFieldDef(channelDef)) {
if (isFieldDef(channelDef) && isScaleFieldDef(channelDef)) {
Copy link
Member Author

Choose a reason for hiding this comment

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

This was giving me errors in my editor (the linter), but now I wonder if this is what is causing my issues @kanitw

Copy link
Member

Choose a reason for hiding this comment

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

What happen if scale is undefined?

Copy link
Member Author

Choose a reason for hiding this comment

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

I dont think its causing the issues. I slightly rewrote it and the issues are the same

@willium willium force-pushed the geo-lite branch 6 times, most recently from cdd833f to df923c8 Compare August 6, 2017 00:23
src/encoding.ts Outdated
*/
shape?: Conditional<LegendFieldDef<F>, ValueDef<string>>; // TODO: maybe distinguish ordinal-only
shape?: Conditional<FieldDef<F>, ValueDef<string>>; // TODO: maybe distinguish ordinal-only
Copy link
Member

Choose a reason for hiding this comment

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

This is the problematic line as shape can still be LegendFieldDef for point.

@willium willium force-pushed the geo-lite branch 11 times, most recently from 4828f6c to 5abd47e Compare August 10, 2017 03:44
Copy link
Member

@kanitw kanitw left a comment

Choose a reason for hiding this comment

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

Quick pass over the documentation.

I will spend some more time to thoroughly review the code later this week.

src/fielddef.ts Outdated
@@ -143,6 +143,7 @@ export interface FieldDefBase<F> {
export interface FieldDef<F> extends FieldDefBase<F> {
/**
* The encoded field's type of measurement (`"quantitative"`, `"temporal"`, `"ordinal"`, or `"nominal"`).
* It can also be a geo type (`"latitude"`, `"longitude"`, and `"geojson"`) when applicable.
Copy link
Member

Choose a reason for hiding this comment

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

Can you describe specifically what "when applicable" mean? (So that users understand right away and do not have to try to find other docs to read.)

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think this is a good idea. Adding anything more is just weirdly specific. Here's what I came up with, but it's honestly more confusing.

   * It can also be a geo type (`"latitude"`, `"longitude"`, and `"geojson"`) when projecting geographical coordinates using a `point` mark (on `x`, and `y` channels), a `rule` mark (on `x`, `x2`, `y`, and `y2` channels) or projecting a GeoJSON shapefile on a `geoshape` mark (on the `shape` channel).

Copy link
Member

Choose a reason for hiding this comment

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

Cut this part -- it's too much

using a point mark (on x, and y channels), a rule mark (on x, x2, y, and y2 channels) or projecting a GeoJSON shapefile on a geoshape mark (on the shape channel)."

and then paraphrase this part to be slightly human-friendly and link to related concept

when projecting geographical coordinates

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't really understand what you're saying

Copy link
Member

Choose a reason for hiding this comment

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

Sorry, I mean something like this:

It can also be a geographic type ("latitude", "longitude", and "geojson") for using with geographic projection

and link projection to projection docs

("when applicable" is vague and doesn't convey much information :D)

src/encoding.ts Outdated
* `"circle"` (default), `"square"`, `"cross"`, `"diamond"`, `"triangle-up"`,
* or `"triangle-down"`, or else a custom SVG path string.
* __Default value:__ If undefined, the default shape depends on [mark config](config.html#point-config)'s `shape` property.
* For `geoshape` marks it should be a fielddef of the geojson data
Copy link
Member

Choose a reason for hiding this comment

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

No description should be added below "Default value:" line

Could you please move it above and add a line break between the main description and the "Default value:" (Yeah we should have added it prior to this PR.)

fielddef => field definition

src/fielddef.ts Outdated
return true;
case 'quantitative':
return !!fieldDef.bin;
case 'temporal':
case 'latitude':
Copy link
Member

Choose a reason for hiding this comment

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

inconsistent tabbing

* The type of the projection. The default is "mercator".
*/
type?: ProjectionType;
/**
Copy link
Member

Choose a reason for hiding this comment

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

Please add line breaks between properties and JS Docs of the next properties

src/spec.ts Outdated
@@ -103,6 +104,11 @@ export interface GenericUnitSpec<E extends Encoding<any>, M> extends BaseSpec, L
*/
encoding: E;

/**
* A geo projection
Copy link
Member

Choose a reason for hiding this comment

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

An object defining properties of geographic projection.

Please also describe more if this can be null or something?
Does it work only for certain type of marks? (If all marks, ignore my comment.)

What's the __Default value:__, is there any case it is generated by default?

```

The `geoshape` mark represents an arbitrary shapes whose geometry is determined by specified GeoJSON shape data.

Copy link
Member

Choose a reason for hiding this comment

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

Need one or a few examples. (All other mark pages have at least one example.)


{% include table.html props="type" source="FieldDef" %}

**Note**:
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for adding this note. This is definitely useful to add.

A few thoughts:

  • We should add it to type.md too

  • For this page, I think maybe it's better to add this description to the JSDoc in the FieldDef interface so the explanation appears in the table. Plus, Altair users can directly benefit from this explanation.

Copy link
Member Author

@willium willium Jan 21, 2018

Choose a reason for hiding this comment

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

Can you clarify what you mean? (also do you mean types.md?)

Copy link
Member

Choose a reason for hiding this comment

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

  • There is type.md

  • You can put the note in FieldDef interface in fielddef.ts instead of here.

permalink: /docs/projection.html
---
## Projection Overview
Projection's are specified at the mark specification level, alongside encoding.
Copy link
Member

Choose a reason for hiding this comment

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

I think we usually begin a docs by explaining the concept. You can copy some of the text from https://vega.github.io/vega/docs/projections

specified at the mark specification level, alongside encoding

Also, there is no concept of "mark specification". I think you only mean specification. Please also make it clear whether all types of specification support projection. (Is it only unit and layer or all types of specification?)

## Projection Properties
{% include table.html props="type,clipAngle,clipExtent,center,rotate,precision" source="Projection" %}


Copy link
Member

Choose a reason for hiding this comment

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

Missing example


{:#properties}
## Projection Properties
{% include table.html props="type,clipAngle,clipExtent,center,rotate,precision" source="Projection" %}
Copy link
Member

Choose a reason for hiding this comment

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

From Vega docs,

In addition to the shared properties above, the following properties are supported for specific projection types in the d3-geo-projection library: coefficient, distance, fraction, lobes, parallel, radius, ratio, spacing, tilt.

Do we support these properties too?

Copy link
Member Author

Choose a reason for hiding this comment

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

We don't currently. Do you want to? I can add them in projection.ts

Copy link
Member

Choose a reason for hiding this comment

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

I think so.


export interface Projection {
/**
* The type of the projection. The default is "mercator".
Copy link
Member

@kanitw kanitw Jan 14, 2018

Choose a reason for hiding this comment

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

Add link to the section that describe all supported projection type?

Also use __Default value:__ to describe default value. -- same for other properties in this interface.

@vega vega deleted a comment from willium Jan 14, 2018
@kanitw
Copy link
Member

kanitw commented Jan 15, 2018

  • Related to Geo #2734 (comment), I think you have to update table in spec.md and perhaps layer.md to include the new projection property. (Maybe there are other properties not added yet as well.)

Copy link
Member

@kanitw kanitw left a comment

Choose a reason for hiding this comment

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

Ok. I actually have sometimes so here are some more comments.

I still need to look at src/compile/projection/parse.ts and examples

const geo = model.reduceFieldDef((rel, fieldDef, channel) => {
if ((contains([X, Y, X2, Y2], channel) && contains([LATITUDE, LONGITUDE], fieldDef.type))
|| (channel === SHAPE && fieldDef.type === GEOJSON)) {
rel[fieldDef.type] = fieldDef.field;
Copy link
Member

@kanitw kanitw Jan 15, 2018

Choose a reason for hiding this comment

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

I wonder if using dictionary here would make it work when we want to use both x/y and y2/x2 (e.g., for line segment rules)?

It seems like this logic will make fieldDef for x2 overrides x's?

That said, it seems to work from your example, but I wonder why. (Perhaps, we need some comment here to explain why it is okay to do it this way.

Copy link
Member Author

Choose a reason for hiding this comment

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

you've discovered a bug, but unfortunately it's not so easy to fix. The bug isn't about whether or not we can project rule, but instead how fit is computed (it appears to work since fit on X2 and Y2 is similar to X and Y). You're correct in your assessment that X2 would override X (my bad).

Copy link
Member Author

Choose a reason for hiding this comment

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

this is non trivial. I will have to think about it more.

Copy link
Member Author

Choose a reason for hiding this comment

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

Resolved.

const points: TypeField[] = [];

const possiblePairs: SingleDefChannel[][] = [[X, Y], [X2, Y2]];
possiblePairs.forEach((posssiblePair) => {
Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I think map is a bad idea here.

@@ -51,6 +54,10 @@ export function parseLegendForChannel(model: UnitModel, channel: NonPositionScal
const fieldDef = model.fieldDef(channel);
const legend = model.legend(channel);

if (!model.getScaleComponent(channel)) {
Copy link
Member

Choose a reason for hiding this comment

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

Move the logic to parseUnitLegend so that the structure of both parseUnitLegend and parseUnitAxis are similar?

return {
...mixins.color(model),
...mixins.text(model, 'tooltip'),
...mixins.nonPosition('opacity', model)
Copy link
Member

Choose a reason for hiding this comment

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

If we merge the href PR before we merge this one, we will have to add href here too :)

#3229

Copy link
Member

Choose a reason for hiding this comment

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

FYI, to make this easier for the future -- I added #3280 (comment)

encodeEntry: (model: UnitModel) => {
const {config} = model;
return {
...mixins.color(model),
Copy link
Member

@kanitw kanitw Jan 15, 2018

Choose a reason for hiding this comment

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

missing a recently added ...mixins.markDefProperties(model.markDef, true)?

}

const projection = component.combine();
const {name, ...rest} = projection;
Copy link
Member

Choose a reason for hiding this comment

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

not sure why you have to extract name and rest here, simply to merge them again at the end?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I do for some weird reason to do with types. if I remove them from the bottom and just extend projection with the return, the type check yells at me. Basically a vega projection is all the properties of the vega-lite projection + name, size, and fit (which are algorithmically decided)

Copy link
Member

Choose a reason for hiding this comment

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

This is fine. I added a comment.

return undefined;
}

function dryMerge(first: ProjectionComponent, second: ProjectionComponent): ProjectionComponent {
Copy link
Member

Choose a reason for hiding this comment

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

Can you add a comment to explain what dryMerge means?

Copy link
Member

Choose a reason for hiding this comment

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

Perhaps, mergeIfNoConflict or mergeIfPropertiesAreConsistent might be a better name?

@@ -50,15 +52,21 @@ function parseUnitScaleCore(model: UnitModel): ScaleComponentIndex {

const channelDef = encoding[channel];

// if mark is geoshape scale channel encodes the geojson
Copy link
Member

@kanitw kanitw Jan 15, 2018

Choose a reason for hiding this comment

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

This comment reads quite confusing.

I think you mean "If mark is geoshape, we use projection instead so there is no need to generate a scale." ?


export type ProjectionType = VgProjectionType;

export interface Projection {
Copy link
Member

Choose a reason for hiding this comment

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

Looks like descriptions of some properties in the Vega docs are a bit more comprehensive and have links to related bl.ock.org examples

src/spec.ts Outdated
const {mark, selection, encoding, ...outerSpec} = spec;
// _ is used to denote a dropped property of the unit spec
// which should not be carried over to the layer spec
const {mark, selection, projection: _p, encoding, ...outerSpec} = spec;
Copy link
Member

Choose a reason for hiding this comment

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

Why should we drop projection for point overlay over lines?

(Suppose we draw lines between different cities and we want to overlay points over each city?)

Copy link
Member

@kanitw kanitw left a comment

Choose a reason for hiding this comment

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

The examples look pretty impressive! I only have a few minor comments.

"$schema": "https://vega.github.io/schema/vega-lite/v2.json",
"width": 500,
"height": 300,
"projection": {
Copy link
Member

Choose a reason for hiding this comment

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

We should be consistent in terms of where to put projection in our example specs.

I think after transform and right before mark seems appropriate? – All other examples follow this format anyway.

"fields": ["rate"]
}
}],
"projection": {
Copy link
Member

Choose a reason for hiding this comment

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

It seems like in a lot of spec, we will just specify projection with type only. I wonder if we should make projection: string supported too so it's consistent with mark.
If you agree, feel free to file an issue so we can deal with this later.

Copy link
Member Author

Choose a reason for hiding this comment

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

nah. we can support that later maybe.

Copy link
Member

@kanitw kanitw left a comment

Choose a reason for hiding this comment

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

parse.ts also looks reasonable, but I think we need some more comments :)

}

function dryMerge(first: ProjectionComponent, second: ProjectionComponent): ProjectionComponent {
const properties = every(PROJECTION_PROPERTIES, (prop) => {
Copy link
Member

Choose a reason for hiding this comment

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

properties needs a better variable name.

Perhaps, allPropertiesAreConsistent or something?

// both have property and an equal value for property
if (first.explicit.hasOwnProperty(prop) &&
second.explicit.hasOwnProperty(prop) &&
hash(first.get(prop)) === hash(second.get(prop))) {
Copy link
Member

Choose a reason for hiding this comment

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

Why do we have to compared the hashed value? Is that because some property can be an object?
(If so, please add a comment.)

return first;
}
}
return null;
Copy link
Member

Choose a reason for hiding this comment

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

So if properties are not consistent, then we don't merge the projection at all?

(If this is intended, please add some comment + explain why this should be the behavior.)

return undefined;
}

function dryMerge(first: ProjectionComponent, second: ProjectionComponent): ProjectionComponent {
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps, mergeIfNoConflict or mergeIfPropertiesAreConsistent might be a better name?

@domoritz
Copy link
Member

domoritz commented Jan 24, 2018

  • Add example for connected scatterplot on a map.

@@ -5,7 +5,12 @@ permalink: /docs/type.html
---

If a field is specified, the channel definition **must** describe the encoded data's [type of measurement (level of measurement)](https://en.wikipedia.org/wiki/Level_of_measurement).
The supported data types are: `"quantitative"`, `"temporal"`, `"ordinal"`, and `"nominal"`.
The supported data types are: `"quantitative"`, `"temporal"`, `"ordinal"`, `"nominal"`, `"latitude"`, `"longitude"`, and `"geojson"`.
Copy link
Member

Choose a reason for hiding this comment

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

Add anchor links for the types?

@domoritz domoritz force-pushed the geo-lite branch 2 times, most recently from 4e9bb76 to 23df794 Compare January 24, 2018 17:22
Copy link
Member

@kanitw kanitw left a comment

Choose a reason for hiding this comment

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

I think we can merge after fixing these minor stuff. :) 🎉

const {encoding, markDef} = model;
const shapeDef = encoding.shape;

if ((isFieldDef(shapeDef) && shapeDef.type === GEOJSON) || markDef.type === GEOSHAPE) {
Copy link
Member

Choose a reason for hiding this comment

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

This comment seems unaddressed?

@@ -26,7 +24,7 @@ export function assembleProjectionForModel(model: Model): VgProjection[] {
}

const projection = component.combine();
const {name, ...rest} = projection;
const {name, ...rest} = projection; // name is always defined
Copy link
Member

Choose a reason for hiding this comment

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

// name is always defined

This confuses me, but Dom just explained to me the real reason behind this.

Please change to .. "we need to extract name so that it is always present in the output and pass TS type validation" or something similar.

@@ -66,25 +75,25 @@ function dryMerge(first: ProjectionComponent, second: ProjectionComponent): Proj
// both have property and an equal value for property
if (first.explicit.hasOwnProperty(prop) &&
second.explicit.hasOwnProperty(prop) &&
// some properties might be signals or objects and require hashing for comparison
hash(first.get(prop)) === hash(second.get(prop))) {
Copy link
Member

Choose a reason for hiding this comment

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

If we want exact comparison, we should use stringify here more than hash?

Copy link
Member Author

Choose a reason for hiding this comment

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

hash is stringify.

src/fielddef.ts Outdated
@@ -143,7 +142,7 @@ export interface FieldDefBase<F> {
export interface FieldDef<F> extends FieldDefBase<F> {
/**
* The encoded field's type of measurement (`"quantitative"`, `"temporal"`, `"ordinal"`, or `"nominal"`).
* It can also be a geo type (`"latitude"`, `"longitude"`, and `"geojson"`) when applicable.
* It can also be a geo type (`"latitude"`, `"longitude"`, and `"geojson"`) when projecting geographical coordinates using a `"point"` mark (on `"x"`, and `"y"` channels), a `"rule"` mark (on `"x"`, `"x2"`, `"y"`, and `"y2"` channels) or projecting a GeoJSON shapefile on a `"geoshape"` mark (on the `"shape"` channel).
Copy link
Member

Choose a reason for hiding this comment

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

"when a geographic projection is applied"?

(and link to projection page)

*/
precision?: String;

/** The following properties are all supported for specific types of projections. Consult the d3-geo-projection library for more information: https://github.com/d3/d3-geo-projection */
Copy link
Member

Choose a reason for hiding this comment

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

If you want do it a lazy way like this, use /* not /** otherwise this comment will be attached to only coefficient, which is not what you want.

@@ -150,6 +150,17 @@ export type VgProjection = {
* Used in conjunction with fit, provides the width and height in pixels of the area to which the projection should be automatically fit.
*/
size?: VgSignalRef | (number | VgSignalRef)[];

/** The following properties are all supported for specific types of projections. Consult the d3-geo-projection library for more information: https://github.com/d3/d3-geo-projection */
Copy link
Member

Choose a reason for hiding this comment

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

/*

@@ -43,6 +43,13 @@ Mapping a field to `text` channel of text mark sets the mark's text value. For e
<span class="vl-example" data-name="text_scatterplot_colored"></span>


## Geo Text

With the [types](type.html) `longitude` and `latitude` instead of `x` and `y` and a corresponding [projection](projection.html), we can show text at accurate locatins. The example below shows the name of every US state capital at the location of the capital.
Copy link
Member

Choose a reason for hiding this comment

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

locations

Copy link
Member

Choose a reason for hiding this comment

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

Done

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.

4 participants