-
Notifications
You must be signed in to change notification settings - Fork 3
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
Moving all the map-related code to TypeScript #100
Conversation
cb92bf0
to
023beb3
Compare
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.
Really nice, it will help a ton 😍 I have a few comments, but overall it's working great!
The any
parts are having me wondering if we should have a no-explicit-any
rule. On the platform, it's a pain, but on a fresh codebase like this one it would be a nice reminder IMO (although it's easy enough).
I really like the comments on types (didn't know they show in the IDE!), we should enforce that more in our code I think.
As I said, we should not forget to import the new Colorscale types in the platform too.
label: string; | ||
/** Value of the key used to match shapes and numeric data */ | ||
key?: string; | ||
}) => string; |
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.
The return type can be auto inferred from the function and some might prefer typying the parameters for reuse? Imo it doesn't hurt to be explicit like this and I really don't mind it.
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.
My reasoning here is that the SDK is exposing an option of this type, and typing even the return value makes sure that if the user passes a function that returns an int or a JSX.Element, their compiler/IDE complains that the SDK expects a function that always returns a string.
|
||
export interface ChoroplethOptions { | ||
shapes: ChoroplethShapeValue; | ||
colorsScale?: ColorsScale; |
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.
More a reminder: at some point we have to import those into the platform as well (and then derive/export what we need). Right now we have definitions on both sides
https://github.com/opendatasoft/platform/blob/develop/ods/react/src/components/PageRenderer/typeRenderers/map/types.ts
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.
As discussed, there is a draft PR on the platform repo for that https://github.com/opendatasoft/platform/pull/9571, which makes the code nicer and tighter indeed
|
||
export type MapRenderTooltipFunction = (f: Feature) => string; | ||
|
||
export type ChoroplethLayer = Omit<FillLayerSpecification, 'id' | 'source'>; |
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.
Learned something 😍
// bounding box to start from, and restrict to it | ||
export let bbox; | ||
|
||
export let bbox: BBox; |
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.
Do we always want a BBox
here or is LngLatBoundsLike
acceptable as well? Or maybe it's just never the case. I am unsure since we accept [LngLatLike, LngLatLike]
in all functions after if I am correct.
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.
I was a bit torn here initially, because LngLatBoundsLike
comes from Maplibre, and I'd rather expose things that are generic. But overall I think I prefer to require a parameter in a specific non-ambiguous format instead of tolerating many and having to support and sometimes transform them, and the bbox one is the most straight-forward ([number, number, number, number]
).
export let colorsScale; | ||
export let dataBounds; | ||
let clientWidth; | ||
$: legendVariant = clientWidth <= 375 ? 'fluid' : 'fixed'; |
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.
Could maybe be constants and then we use the […] as const; … = typeof array[number]
trick? So we have only one source for the constants (although right now it's not really overwhelming).
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.
Indeed I can do something like
const LEGENDVARIANT_FLUID = 'fluid';
const LEGENDVARIANT_FIXED = 'fixed';
export type LegendVariant = typeof LEGENDVARIANT_FLUID | typeof LEGENDVARIANT_FIXED;
But I'm not sure of the best place where to put the constants. I'd like to put them into ColorLegend.svelte, but I'm not sure how imports will work, and also this would be a circular import quickly.
What's the trick you're talking about exactly? Maybe it's precisely the answer to my problems, but I don't think I know about it
|
||
export type ChoroplethLayer = Omit<FillLayerSpecification, 'id' | 'source'>; | ||
|
||
export type MapLayer = ChoroplethLayer; |
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.
Since it's laying ground for future development, maybe we could do Partial<FillLayerSpecification>
(or having only certain fiels as optional) and then have more specific map types (right now only Choropleth) be more restrictive if need be. In case someone need a layer type that's not from us? Maybe its too far fetched though and MapLayer = ChoroplethLayer | PointerMapLayer | …
will be enough 🤷
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.
In the code right now, we're literally just adding our own id
and source
and expecting the result to be a FillLayerSpecification
, so this fits exactly the code and the intent, and if Maplibre evolves its specification, it's going to break instantly, which is nice. Right now I'm expecting that we won't have a lot of supported types of layer, but it really depends on how MapRender
evolves. My experiences with Maplibre in general encourage me to really strenghten the configuration when we can 😅
const thresholdArray = []; | ||
colorsScale.colors.forEach((_color, i) => { | ||
const thresholdArray: number[] = []; | ||
colorsScale.colors.forEach((_color, i: number) => { |
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.
i
Can be auto-infered safely I think (at least it works 😅 )
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.
Absolutely, I'm not sure why I went overboard here but not even with _color
😁
@@ -36,15 +49,15 @@ export const colorShapes = (geoJson, values, colorsScale, emptyValueColor) => { | |||
scale = chroma.scale([colorMin, colorMax]).domain([min, max]); | |||
} | |||
|
|||
const dataMapping = {}; | |||
const dataMapping: any = {}; |
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.
Probably better with a reduce and a map type:
const dataMapping: any = {}; | |
type ChoroPlethDataMapping = { | |
[key: ChoroplethDatavlue['x']]: ChoroplethDataVlue['y']; | |
} | |
const dataMapping: ChoroPlethDataMapping = values.reduce( (mapping, v) => mapping[v.x] = v.y, {}); |
(just the idea, I'm not sure it's fully working 😛 )
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.
I tried the reduce, but it ends up being a bit more verbose because the initial value {}
also needs the same type etc.
But I completely missed that I could have an object where both the key and value can be typed, so I replaced the any
with the inlined type you suggested!
function mergeBboxFromFeaturesWithSameKey(features) { | ||
const mergedBboxes = {}; | ||
function mergeBboxFromFeaturesWithSameKey(features: Feature[]) { | ||
const mergedBboxes: any = {}; |
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.
Same here, I would use a reduce with a stricter type.
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.
Same as above, I didn't end up using a reduce because it complicated the code IMO, but I still replaced the any with a real inline type.
@@ -180,5 +183,5 @@ export const getFixedTooltips = (shapeKeys, features, renderTooltip) => { | |||
return null; | |||
}); | |||
|
|||
return popups; | |||
return popups.filter(Boolean) as ChoroplethFixedTooltipDescription[]; |
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.
So apparently Boolean doesn't have the right signature for TS. I really like the isPresent solution, although we could craft our own function easily too.
Whatever the solution, I think we should do the extra effort to popus.filter<ChoroplethFixedTooltipDescription>(…)
rather than hard casting. But this bug is a bit of pain indeed…
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.
I spent a lot of time trying to do it the right way, but somehow didn't succeed. I'll try using compact
from lodash
which should do the same thing, and has types that should work here.
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.
If we don't want to use the npm package ts-is-present
, the other solution seems to be using type guard returns:
function nonNullable<T>(value: T): value is NonNullable<T> {
return value !== null && value !== undefined;
}
return popus.filter<ChoroplethPopups>(nonNullable)
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.
I used an inline function:
return popups.filter((item): item is NonNullable<ChoroplethFixedTooltipDescription> => Boolean(item)) as ChoroplethFixedTooltipDescription[];
Thank you for teaching us about is
😁
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.
Very nice! Thanks @richterb 🙏
To be honest, I didn't want to delay this PR so I did a somewhat superficial review here. Maps are still a bit confusing for me, but I intend to dive into it and really understand what's going on.
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.
LGTM 👏
import turfBbox from '@turf/bbox'; | ||
import type { SourceSpecification } from 'maplibre-gl'; | ||
// eslint-disable-next-line import/no-unresolved |
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.
Regarding this issue (type only imports), I seems that the version 2.25.2
of eslint-plugin-import
fixed it. I've tested this version and no more linter error 👍.
Here's the PR: import-js/eslint-plugin-import#2220
(same thing in MapRender.svelte line 16)
No description provided.