mirror of
https://github.com/apache/superset.git
synced 2024-09-19 12:09:42 -04:00
feat(plugin-chart-choropleth-map): add package (#560)
* feat(plugin-chart-choropleth-map): scaffold and load map (#527) * feat: add package * feat: storybook working * feat: load usa and world map * refactor: clean up * fix: remove test data * refactor: utilize dynamic import * build: remove unused dependencies * fix: address pr comments * fix: comment * feat(plugin-chart-choropleth-map): add more country maps (#529) * feat(plugin-chart-choropleth-map): add zooming (#528) * feat: add zooming * feat: can zoom in and out * feat: add zoom controls * refactor: extract controls * fix: address comments * feat(plugin-chart-choropleth-map): add encoding (#541) * feat: add encoder * feat: add encoding * docs: add categorical * fix: any * docs: update storybook * feat(plugin-chart-choropleth-map): add tooltip (#548) * feat: support tooltip * feat: support tooltip fields * fix: default projection * build: bump dependency * build: update dependency * build: mark private
This commit is contained in:
parent
c966fc41a9
commit
72c2b7afc0
@ -1,4 +1,3 @@
|
||||
/* eslint-disable sort-keys */
|
||||
export default [
|
||||
{
|
||||
country_id: 'FR-01',
|
||||
|
@ -0,0 +1,107 @@
|
||||
import React from 'react';
|
||||
import { SuperChart } from '@superset-ui/chart';
|
||||
import {
|
||||
maps,
|
||||
ChoroplethMapChartPlugin,
|
||||
} from '../../../../../../plugins/plugin-chart-choropleth-map/src';
|
||||
import { withKnobs, select } from '@storybook/addon-knobs';
|
||||
import useFakeMapData from './useFakeMapData';
|
||||
|
||||
new ChoroplethMapChartPlugin().configure({ key: 'choropleth-map' }).register();
|
||||
|
||||
export default {
|
||||
title: 'Chart Plugins|plugin-chart-choropleth-map',
|
||||
decorators: [withKnobs],
|
||||
};
|
||||
|
||||
export const worldMap = () => {
|
||||
const map = select(
|
||||
'Map',
|
||||
maps.map(m => m.key),
|
||||
'world',
|
||||
'map',
|
||||
);
|
||||
|
||||
return (
|
||||
<SuperChart
|
||||
chartType="choropleth-map"
|
||||
width={800}
|
||||
height={450}
|
||||
queryData={{ data: useFakeMapData(map) }}
|
||||
formData={{
|
||||
map,
|
||||
encoding: {
|
||||
key: {
|
||||
field: 'key',
|
||||
title: 'Location',
|
||||
},
|
||||
fill: {
|
||||
type: 'quantitative',
|
||||
field: 'numStudents',
|
||||
scale: {
|
||||
range: ['#cecee5', '#3f007d'],
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const usa = () => (
|
||||
<SuperChart
|
||||
chartType="choropleth-map"
|
||||
width={800}
|
||||
height={450}
|
||||
queryData={{ data: useFakeMapData('usa') }}
|
||||
formData={{
|
||||
map: 'usa',
|
||||
encoding: {
|
||||
key: {
|
||||
field: 'key',
|
||||
title: 'State',
|
||||
},
|
||||
fill: {
|
||||
type: 'quantitative',
|
||||
field: 'numStudents',
|
||||
title: 'No. of students',
|
||||
scale: {
|
||||
range: ['#fdc28c', '#7f2704'],
|
||||
},
|
||||
},
|
||||
tooltip: [
|
||||
{
|
||||
field: 'favoriteFruit',
|
||||
title: 'Fruit',
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const categoricalColor = () => (
|
||||
<SuperChart
|
||||
chartType="choropleth-map"
|
||||
width={800}
|
||||
height={450}
|
||||
queryData={{ data: useFakeMapData('usa') }}
|
||||
formData={{
|
||||
map: 'usa',
|
||||
encoding: {
|
||||
key: {
|
||||
field: 'key',
|
||||
title: 'State',
|
||||
},
|
||||
fill: {
|
||||
type: 'nominal',
|
||||
field: 'favoriteFruit',
|
||||
scale: {
|
||||
domain: ['apple', 'banana', 'grape'],
|
||||
range: ['#e74c3c', '#f1c40f', '#9b59b6'],
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
@ -0,0 +1,26 @@
|
||||
import loadMap from '../../../../../../plugins/plugin-chart-choropleth-map/src/chart/loadMap';
|
||||
|
||||
const FRUITS = ['apple', 'banana', 'grape'];
|
||||
|
||||
export type FakeMapData = {
|
||||
key: string;
|
||||
favoriteFruit: string;
|
||||
numStudents: number;
|
||||
}[];
|
||||
|
||||
/**
|
||||
* Generate mock data for the given map
|
||||
* Output is a promise of an array
|
||||
* { key, favoriteFruit, numStudents }[]
|
||||
* @param map map name
|
||||
*/
|
||||
export default async function generateFakeMapData(map: string) {
|
||||
const { object, metadata } = await loadMap(map);
|
||||
return object.features
|
||||
.map(f => metadata.keyAccessor(f))
|
||||
.map(key => ({
|
||||
key,
|
||||
favoriteFruit: FRUITS[Math.round(Math.random() * 2)],
|
||||
numStudents: Math.round(Math.random() * 100),
|
||||
}));
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import generateFakeMapData, { FakeMapData } from './generateFakeMapData';
|
||||
|
||||
export default function useFakeMapData(map: string) {
|
||||
const [mapData, setMapData] = useState<FakeMapData | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
generateFakeMapData(map).then(mapData => {
|
||||
setMapData(mapData);
|
||||
});
|
||||
}, [map]);
|
||||
|
||||
return mapData;
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
## @superset-ui/plugin-chart-choropleth-map
|
||||
|
||||
[![Version](https://img.shields.io/npm/v/@superset-ui/plugin-chart-choropleth-map.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/plugin-chart-choropleth-map.svg?style=flat-square)
|
||||
[![David (path)](https://img.shields.io/david/apache-superset/superset-ui.svg?path=packages%2Fsuperset-ui-plugin-chart-choropleth-map&style=flat-square)](https://david-dm.org/apache-superset/superset-ui?path=packages/superset-ui-plugin-chart-choropleth-map)
|
||||
|
||||
This plugin provides Choropleth Map for Superset.
|
||||
|
||||
### Usage
|
||||
|
||||
Configure `key`, which can be any `string`, and register the plugin. This `key` will be used to lookup this chart throughout the app.
|
||||
|
||||
```js
|
||||
import ChoroplethMapChartPlugin from '@superset-ui/plugin-chart-choropleth-map';
|
||||
|
||||
new ChoroplethMapChartPlugin()
|
||||
.configure({ key: 'choropleth-map' })
|
||||
.register();
|
||||
```
|
||||
|
||||
Then use it via `SuperChart`. See [storybook](https://apache-superset.github.io/superset-ui/?selectedKind=plugin-chart-choropleth-map) for more details.
|
||||
|
||||
```js
|
||||
<SuperChart
|
||||
chartType="choropleth-map"
|
||||
width={600}
|
||||
height={600}
|
||||
formData={...}
|
||||
queryData={{
|
||||
data: {...},
|
||||
}}
|
||||
/>
|
||||
```
|
@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "@superset-ui/plugin-chart-choropleth-map",
|
||||
"version": "0.0.0",
|
||||
"description": "Superset Chart - Choropleth Map",
|
||||
"sideEffects": false,
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
"files": [
|
||||
"esm",
|
||||
"lib"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/apache-superset/superset-ui.git"
|
||||
},
|
||||
"keywords": [
|
||||
"superset"
|
||||
],
|
||||
"author": "Superset",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/apache-superset/superset-ui/issues"
|
||||
},
|
||||
"homepage": "https://github.com/apache-superset/superset-ui#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/react": "^16.9.35",
|
||||
"@types/d3-geo": "^1.11.1",
|
||||
"@types/geojson": "^7946.0.3",
|
||||
"@types/topojson-client": "^3.0.0",
|
||||
"@types/topojson-specification": "^1.0.0",
|
||||
"@vx/clip-path": "^0.0.196",
|
||||
"@vx/event": "^0.0.196",
|
||||
"@vx/pattern": "^0.0.196",
|
||||
"@vx/tooltip": "^0.0.196",
|
||||
"@vx/zoom": "^0.0.196",
|
||||
"encodable": "^0.3.7",
|
||||
"geojson": "^0.5.0",
|
||||
"lodash": "^4.17.15",
|
||||
"topojson-client": "^3.1.0",
|
||||
"d3-geo": "^1.12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1",
|
||||
"@superset-ui/chart": "^0.13.3",
|
||||
"@superset-ui/chart-composition": "^0.13.3",
|
||||
"@superset-ui/translation": "^0.13.3",
|
||||
"@superset-ui/style": "^0.13.3"
|
||||
}
|
||||
}
|
@ -0,0 +1,322 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { Zoom } from '@vx/zoom';
|
||||
import { localPoint } from '@vx/event';
|
||||
import { RectClipPath } from '@vx/clip-path';
|
||||
import { withTooltip } from '@vx/tooltip';
|
||||
import { keyBy } from 'lodash';
|
||||
import { geoPath } from 'd3-geo';
|
||||
import type { FeatureCollection } from 'geojson';
|
||||
import { WithTooltipProvidedProps } from '@vx/tooltip/lib/enhancers/withTooltip';
|
||||
import loadMap from './loadMap';
|
||||
import MapMetadata from './MapMetadata';
|
||||
import {
|
||||
PADDING,
|
||||
RelativeDiv,
|
||||
IconButton,
|
||||
TextButton,
|
||||
ZoomControls,
|
||||
MiniMapControl,
|
||||
} from './components';
|
||||
import {
|
||||
ChoroplethMapEncoding,
|
||||
choroplethMapEncoderFactory,
|
||||
DefaultChannelOutputs,
|
||||
} from './Encoder';
|
||||
import MapTooltip, { MapDataPoint } from './MapTooltip';
|
||||
|
||||
const INITIAL_TRANSFORM = {
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
translateX: 0,
|
||||
translateY: 0,
|
||||
skewX: 0,
|
||||
skewY: 0,
|
||||
};
|
||||
|
||||
/**
|
||||
* These props should be stored when saving the chart.
|
||||
*/
|
||||
export type ChoroplethMapVisualProps = {
|
||||
encoding?: Partial<ChoroplethMapEncoding>;
|
||||
map?: string;
|
||||
};
|
||||
|
||||
export type ChoroplethMapProps = ChoroplethMapVisualProps &
|
||||
WithTooltipProvidedProps<MapDataPoint> & {
|
||||
data: Record<string, unknown>[];
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
data: [],
|
||||
encoding: {},
|
||||
map: 'world',
|
||||
};
|
||||
|
||||
const missingItem = DefaultChannelOutputs;
|
||||
|
||||
class ChoroplethMap extends React.PureComponent<
|
||||
ChoroplethMapProps & typeof defaultProps,
|
||||
{
|
||||
mapShape?: {
|
||||
metadata: MapMetadata;
|
||||
object: FeatureCollection;
|
||||
};
|
||||
mapData: {
|
||||
[key: string]: MapDataPoint;
|
||||
};
|
||||
showMiniMap: boolean;
|
||||
}
|
||||
> {
|
||||
static defaultProps = defaultProps;
|
||||
|
||||
createEncoder = choroplethMapEncoderFactory.createSelector();
|
||||
|
||||
constructor(props: ChoroplethMapProps & typeof defaultProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
mapData: {},
|
||||
mapShape: undefined,
|
||||
showMiniMap: true,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadMap();
|
||||
this.processData();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: ChoroplethMapProps) {
|
||||
if (prevProps.map !== this.props.map) {
|
||||
this.loadMap();
|
||||
}
|
||||
if (prevProps.data !== this.props.data || prevProps.encoding !== this.props.encoding) {
|
||||
this.processData();
|
||||
}
|
||||
}
|
||||
|
||||
processData() {
|
||||
const { data, encoding } = this.props;
|
||||
const encoder = this.createEncoder(encoding);
|
||||
const { key, fill, opacity, stroke, strokeWidth } = encoder.channels;
|
||||
|
||||
encoder.setDomainFromDataset(data);
|
||||
|
||||
const mapData = keyBy(
|
||||
data.map(d => ({
|
||||
key: key.getValueFromDatum<string>(d, DefaultChannelOutputs.key),
|
||||
fill: fill.encodeDatum(d, DefaultChannelOutputs.fill),
|
||||
opacity: opacity.encodeDatum(d, DefaultChannelOutputs.opacity),
|
||||
stroke: stroke.encodeDatum(d, DefaultChannelOutputs.stroke),
|
||||
strokeWidth: strokeWidth.encodeDatum(d, DefaultChannelOutputs.strokeWidth),
|
||||
datum: d,
|
||||
})),
|
||||
d => d.key,
|
||||
);
|
||||
|
||||
this.setState({ mapData });
|
||||
}
|
||||
|
||||
loadMap() {
|
||||
const { map } = this.props;
|
||||
this.setState({ mapShape: undefined });
|
||||
loadMap(map).then(mapShape => {
|
||||
this.setState({ mapShape });
|
||||
});
|
||||
}
|
||||
|
||||
toggleMiniMap = () => {
|
||||
const { showMiniMap } = this.state;
|
||||
this.setState({
|
||||
showMiniMap: !showMiniMap,
|
||||
});
|
||||
};
|
||||
|
||||
handleMouseOver = (event: React.MouseEvent<SVGPathElement>, datum?: MapDataPoint) => {
|
||||
const coords = localPoint(event);
|
||||
this.props.showTooltip({
|
||||
tooltipLeft: coords?.x,
|
||||
tooltipTop: coords?.y,
|
||||
tooltipData: datum,
|
||||
});
|
||||
};
|
||||
|
||||
renderMap() {
|
||||
const { height, width, hideTooltip } = this.props;
|
||||
const { mapShape, mapData } = this.state;
|
||||
|
||||
if (typeof mapShape !== 'undefined') {
|
||||
const { metadata, object } = mapShape;
|
||||
const { keyAccessor } = metadata;
|
||||
const projection = metadata.createProjection().fitExtent(
|
||||
[
|
||||
[PADDING, PADDING],
|
||||
[width - PADDING * 2, height - PADDING * 2],
|
||||
],
|
||||
object,
|
||||
);
|
||||
const path = geoPath().projection(projection);
|
||||
|
||||
return object.features.map(f => {
|
||||
const key = keyAccessor(f);
|
||||
const encodedDatum = mapData[key] || missingItem;
|
||||
const { stroke, fill, strokeWidth, opacity } = encodedDatum;
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/mouse-events-have-key-events
|
||||
<path
|
||||
key={key}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
stroke={stroke}
|
||||
strokeWidth={strokeWidth}
|
||||
fill={fill}
|
||||
opacity={opacity}
|
||||
d={path(f) || ''}
|
||||
onMouseOver={event => this.handleMouseOver(event, encodedDatum)}
|
||||
onMouseMove={event => this.handleMouseOver(event, encodedDatum)}
|
||||
onMouseOut={hideTooltip}
|
||||
onBlur={hideTooltip}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
height,
|
||||
width,
|
||||
encoding,
|
||||
tooltipOpen,
|
||||
tooltipLeft,
|
||||
tooltipTop,
|
||||
tooltipData,
|
||||
} = this.props;
|
||||
const { showMiniMap } = this.state;
|
||||
const encoder = this.createEncoder(encoding);
|
||||
|
||||
const renderedMap = this.renderMap();
|
||||
const miniMapTransform = `translate(${(width * 3) / 4 - PADDING}, ${
|
||||
(height * 3) / 4 - PADDING
|
||||
}) scale(0.25)`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Zoom
|
||||
style={{ width, height }}
|
||||
width={width}
|
||||
height={height}
|
||||
scaleXMin={0.75}
|
||||
scaleXMax={8}
|
||||
scaleYMin={0.75}
|
||||
scaleYMax={8}
|
||||
transformMatrix={INITIAL_TRANSFORM}
|
||||
>
|
||||
{zoom => (
|
||||
<RelativeDiv>
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
style={{ cursor: zoom.isDragging ? 'grabbing' : 'grab' }}
|
||||
>
|
||||
<RectClipPath id="zoom-clip" width={width} height={height} />
|
||||
<g
|
||||
onWheel={zoom.handleWheel}
|
||||
// eslint-disable-next-line react/jsx-handler-names
|
||||
onMouseDown={zoom.dragStart}
|
||||
// eslint-disable-next-line react/jsx-handler-names
|
||||
onMouseMove={zoom.dragMove}
|
||||
// eslint-disable-next-line react/jsx-handler-names
|
||||
onMouseUp={zoom.dragEnd}
|
||||
onMouseLeave={() => {
|
||||
if (!zoom.isDragging) return;
|
||||
zoom.dragEnd();
|
||||
}}
|
||||
onDoubleClick={event => {
|
||||
const point = localPoint(event) || undefined;
|
||||
zoom.scale({ scaleX: 1.1, scaleY: 1.1, point });
|
||||
}}
|
||||
>
|
||||
<rect width={width} height={height} fill="transparent" />
|
||||
<g transform={zoom.toString()}>{renderedMap}</g>
|
||||
</g>
|
||||
{showMiniMap && (
|
||||
<g clipPath="url(#zoom-clip)" transform={miniMapTransform}>
|
||||
<rect width={width} height={height} fill="#fff" stroke="#999" />
|
||||
{renderedMap}
|
||||
<rect
|
||||
width={width}
|
||||
height={height}
|
||||
fill="white"
|
||||
fillOpacity={0.2}
|
||||
stroke="#999"
|
||||
strokeWidth={4}
|
||||
transform={zoom.toStringInvert()}
|
||||
/>
|
||||
</g>
|
||||
)}
|
||||
</svg>
|
||||
<ZoomControls>
|
||||
<IconButton type="button" onClick={() => zoom.scale({ scaleX: 1.2, scaleY: 1.2 })}>
|
||||
+
|
||||
</IconButton>
|
||||
<IconButton type="button" onClick={() => zoom.scale({ scaleX: 0.8, scaleY: 0.8 })}>
|
||||
-
|
||||
</IconButton>
|
||||
<TextButton
|
||||
type="button"
|
||||
// eslint-disable-next-line react/jsx-handler-names
|
||||
onClick={zoom.clear}
|
||||
>
|
||||
Reset
|
||||
</TextButton>
|
||||
</ZoomControls>
|
||||
<MiniMapControl>
|
||||
<TextButton
|
||||
type="button"
|
||||
// eslint-disable-next-line react/jsx-handler-names
|
||||
onClick={this.toggleMiniMap}
|
||||
>
|
||||
{showMiniMap ? t('Hide Mini Map') : t('Show Mini Map')}
|
||||
</TextButton>
|
||||
</MiniMapControl>
|
||||
</RelativeDiv>
|
||||
)}
|
||||
</Zoom>
|
||||
{tooltipOpen && (
|
||||
<MapTooltip
|
||||
encoder={encoder}
|
||||
top={tooltipTop}
|
||||
left={tooltipLeft}
|
||||
tooltipData={tooltipData}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTooltip(ChoroplethMap);
|
@ -0,0 +1,43 @@
|
||||
import { createEncoderFactory, DeriveEncoding, Encoder, DeriveChannelOutputs } from 'encodable';
|
||||
|
||||
type ChoroplethMapEncodingConfig = {
|
||||
key: ['Text', string];
|
||||
fill: ['Color', string];
|
||||
opacity: ['Numeric', number];
|
||||
stroke: ['Color', string];
|
||||
strokeWidth: ['Numeric', number];
|
||||
tooltip: ['Text', string, 'multiple'];
|
||||
};
|
||||
|
||||
export type ChoroplethMapEncoding = DeriveEncoding<ChoroplethMapEncodingConfig>;
|
||||
|
||||
export type ChoroplethMapEncoder = Encoder<ChoroplethMapEncodingConfig>;
|
||||
|
||||
export type ChoroplethMapChannelOutputs = DeriveChannelOutputs<ChoroplethMapEncodingConfig>;
|
||||
|
||||
export const DefaultChannelOutputs = {
|
||||
key: '',
|
||||
fill: '#f0f0f0',
|
||||
opacity: 1,
|
||||
stroke: '#ccc',
|
||||
strokeWidth: 1,
|
||||
};
|
||||
|
||||
export const choroplethMapEncoderFactory = createEncoderFactory<ChoroplethMapEncodingConfig>({
|
||||
channelTypes: {
|
||||
key: 'Text',
|
||||
fill: 'Color',
|
||||
opacity: 'Numeric',
|
||||
stroke: 'Color',
|
||||
strokeWidth: 'Numeric',
|
||||
tooltip: 'Text',
|
||||
},
|
||||
defaultEncoding: {
|
||||
key: { field: 'key', title: 'Location' },
|
||||
fill: { value: DefaultChannelOutputs.fill },
|
||||
opacity: { value: DefaultChannelOutputs.opacity },
|
||||
stroke: { value: DefaultChannelOutputs.stroke },
|
||||
strokeWidth: { value: DefaultChannelOutputs.strokeWidth },
|
||||
tooltip: [],
|
||||
},
|
||||
});
|
@ -0,0 +1,36 @@
|
||||
import type { FeatureCollection } from 'geojson';
|
||||
import { feature } from 'topojson-client';
|
||||
import { get } from 'lodash/fp';
|
||||
import { RawMapMetadata } from '../types';
|
||||
import Projection from './Projection';
|
||||
|
||||
export default class MapMetadata {
|
||||
config: RawMapMetadata;
|
||||
|
||||
keyAccessor: (...args: unknown[]) => string;
|
||||
|
||||
constructor(metadata: RawMapMetadata) {
|
||||
const { keyField } = metadata;
|
||||
|
||||
this.config = metadata;
|
||||
this.keyAccessor = get(keyField);
|
||||
}
|
||||
|
||||
loadMap(): Promise<FeatureCollection> {
|
||||
const { key } = this.config;
|
||||
|
||||
return this.config.type === 'topojson'
|
||||
? this.config.load().then(map => feature(map, map.objects[key]) as FeatureCollection)
|
||||
: this.config.load();
|
||||
}
|
||||
|
||||
createProjection() {
|
||||
const { projection = 'Mercator', rotate } = this.config;
|
||||
const projectionFn = Projection[projection]();
|
||||
if (rotate) {
|
||||
projectionFn.rotate(rotate);
|
||||
}
|
||||
|
||||
return projectionFn;
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { isCompleteFieldDef } from 'encodable';
|
||||
import { uniqBy } from 'lodash';
|
||||
import { Tooltip } from '@vx/tooltip';
|
||||
import { TooltipFrame, TooltipTable } from '@superset-ui/chart-composition';
|
||||
import { ChoroplethMapChannelOutputs, ChoroplethMapEncoder } from './Encoder';
|
||||
|
||||
export type MapDataPoint = Omit<ChoroplethMapChannelOutputs, 'tooltip'> & {
|
||||
datum: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type MapTooltipProps = {
|
||||
top?: number;
|
||||
left?: number;
|
||||
encoder: ChoroplethMapEncoder;
|
||||
tooltipData?: MapDataPoint;
|
||||
};
|
||||
|
||||
export default function MapTooltip({ encoder, left, top, tooltipData }: MapTooltipProps) {
|
||||
if (!tooltipData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { channels } = encoder;
|
||||
const { key, fill, stroke, strokeWidth, opacity, tooltip } = channels;
|
||||
const { datum } = tooltipData;
|
||||
|
||||
const tooltipRows = [
|
||||
{ key: 'key', keyColumn: key.getTitle(), valueColumn: key.formatDatum(datum) },
|
||||
];
|
||||
|
||||
[fill, stroke, opacity, strokeWidth].forEach(channel => {
|
||||
if (isCompleteFieldDef<string | number>(channel.definition)) {
|
||||
tooltipRows.push({
|
||||
key: channel.name as string,
|
||||
keyColumn: channel.getTitle(),
|
||||
valueColumn: channel.formatDatum(datum),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
tooltip.forEach(g => {
|
||||
tooltipRows.push({
|
||||
key: `${g.name}`,
|
||||
keyColumn: g.getTitle(),
|
||||
valueColumn: g.formatDatum(datum),
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<Tooltip top={top} left={left}>
|
||||
<TooltipFrame>
|
||||
<TooltipTable data={uniqBy(tooltipRows, row => row.keyColumn)} />
|
||||
</TooltipFrame>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import { geoMercator, geoEquirectangular, geoAlbers, geoAlbersUsa } from 'd3-geo';
|
||||
|
||||
const Projection = {
|
||||
Mercator: geoMercator,
|
||||
Equirectangular: geoEquirectangular,
|
||||
Albers: geoAlbers,
|
||||
AlbersUsa: geoAlbersUsa,
|
||||
};
|
||||
|
||||
type Projection = keyof typeof Projection;
|
||||
|
||||
export default Projection;
|
@ -0,0 +1,46 @@
|
||||
import styled, { supersetTheme } from '@superset-ui/style';
|
||||
|
||||
export const PADDING = supersetTheme.gridUnit * 4;
|
||||
|
||||
export const RelativeDiv = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const ZoomControls = styled.div`
|
||||
position: absolute;
|
||||
top: ${PADDING}px;
|
||||
right: ${PADDING}px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
`;
|
||||
|
||||
export const MiniMapControl = styled.div`
|
||||
position: absolute;
|
||||
bottom: ${PADDING + 6}px;
|
||||
right: ${PADDING + 1}px;
|
||||
`;
|
||||
|
||||
export const IconButton = styled.button`
|
||||
width: ${({ theme }) => theme.gridUnit * 6}px;
|
||||
font-size: ${({ theme }) => theme.typography.sizes.xl}px;
|
||||
text-align: center;
|
||||
color: #222;
|
||||
margin: 0px;
|
||||
margin-bottom: 2px;
|
||||
background: #f5f8fb;
|
||||
padding: 0px ${({ theme }) => theme.gridUnit}px;
|
||||
border-radius: ${({ theme }) => theme.borderRadius}px;
|
||||
border: none;
|
||||
`;
|
||||
|
||||
export const TextButton = styled.button`
|
||||
text-align: center;
|
||||
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
||||
color: #222;
|
||||
margin: 0px;
|
||||
background: #f5f8fb;
|
||||
padding: ${({ theme }) => theme.gridUnit / 2}px ${({ theme }) => theme.gridUnit * 1.5}px;
|
||||
border-radius: ${({ theme }) => theme.borderRadius}px;
|
||||
border: none;
|
||||
`;
|
@ -0,0 +1,11 @@
|
||||
import { mapsLookup } from '../maps';
|
||||
import MapMetadata from './MapMetadata';
|
||||
|
||||
export default function loadMap(key: string) {
|
||||
if (key in mapsLookup) {
|
||||
const metadata = new MapMetadata(mapsLookup[key]);
|
||||
return metadata.loadMap().then(object => ({ metadata, object }));
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Unknown map: ${key}`));
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
@ -0,0 +1,2 @@
|
||||
export { default as ChoroplethMapChartPlugin } from './plugin';
|
||||
export { maps, mapsLookup } from './maps';
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,216 @@
|
||||
import { keyBy } from 'lodash/fp';
|
||||
import { RawMapMetadata } from '../types';
|
||||
|
||||
// Edit here if you are adding a new map
|
||||
const mapsInfo: Record<string, Omit<RawMapMetadata, 'key'>> = {
|
||||
belgium: {
|
||||
name: 'Belgium',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./belgium-topo.json'),
|
||||
keyField: 'properties.ISO',
|
||||
},
|
||||
brazil: {
|
||||
name: 'Brazil',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./brazil-topo.json'),
|
||||
keyField: 'properties.ISO',
|
||||
},
|
||||
bulgaria: {
|
||||
name: 'Bulgaria',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./bulgaria-topo.json'),
|
||||
keyField: 'properties.ISO',
|
||||
},
|
||||
canada: {
|
||||
name: 'Canada',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./canada-topo.json'),
|
||||
keyField: 'properties.NAME_1',
|
||||
},
|
||||
china: {
|
||||
name: 'China',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./china-topo.json'),
|
||||
keyField: 'properties.NAME_1',
|
||||
},
|
||||
france: {
|
||||
name: 'France',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./france-topo.json'),
|
||||
keyField: 'properties.ISO',
|
||||
},
|
||||
germany: {
|
||||
name: 'Germany',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./germany-topo.json'),
|
||||
keyField: 'properties.ISO',
|
||||
},
|
||||
india: {
|
||||
name: 'India',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./india-topo.json'),
|
||||
keyField: 'properties.ISO',
|
||||
},
|
||||
iran: {
|
||||
name: 'Iran',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./iran-topo.json'),
|
||||
keyField: 'properties.ISO',
|
||||
},
|
||||
italy: {
|
||||
name: 'Italy',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./italy-topo.json'),
|
||||
keyField: 'properties.ISO',
|
||||
},
|
||||
japan: {
|
||||
name: 'Japan',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./japan-topo.json'),
|
||||
keyField: 'properties.ISO',
|
||||
},
|
||||
korea: {
|
||||
name: 'Korea',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./korea-topo.json'),
|
||||
keyField: 'properties.ISO',
|
||||
},
|
||||
liechtenstein: {
|
||||
name: 'Liechtenstein',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./liechtenstein-topo.json'),
|
||||
keyField: 'properties.ISO',
|
||||
},
|
||||
morocco: {
|
||||
name: 'Morocco',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./morocco-topo.json'),
|
||||
keyField: 'properties.ISO',
|
||||
},
|
||||
myanmar: {
|
||||
name: 'Myanmar',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./myanmar-topo.json'),
|
||||
keyField: 'properties.ISO',
|
||||
},
|
||||
netherlands: {
|
||||
name: 'Netherlands',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./netherlands-topo.json'),
|
||||
keyField: 'properties.ISO',
|
||||
},
|
||||
portugal: {
|
||||
name: 'Portugal',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./portugal-topo.json'),
|
||||
keyField: 'properties.ISO',
|
||||
},
|
||||
russia: {
|
||||
name: 'Russia',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./russia-topo.json'),
|
||||
keyField: 'properties.ISO',
|
||||
rotate: [-9, 0, 0],
|
||||
},
|
||||
singapore: {
|
||||
name: 'Singapore',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./singapore-topo.json'),
|
||||
keyField: 'properties.ISO',
|
||||
},
|
||||
spain: {
|
||||
name: 'Spain',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./spain-topo.json'),
|
||||
keyField: 'properties.ISO',
|
||||
},
|
||||
switzerland: {
|
||||
name: 'Switzerland',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./switzerland-topo.json'),
|
||||
keyField: 'properties.ISO',
|
||||
},
|
||||
thailand: {
|
||||
name: 'Thailand',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./thailand-topo.json'),
|
||||
keyField: 'properties.NAME_1',
|
||||
},
|
||||
timorleste: {
|
||||
name: 'Timor-Leste',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./timorleste-topo.json'),
|
||||
keyField: 'properties.ISO',
|
||||
},
|
||||
uk: {
|
||||
name: 'United Kingdom',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./uk-topo.json'),
|
||||
keyField: 'properties.ISO',
|
||||
},
|
||||
ukraine: {
|
||||
name: 'Ukraine',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./ukraine-topo.json'),
|
||||
keyField: 'properties.NAME_1',
|
||||
},
|
||||
usa: {
|
||||
name: 'USA',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./usa-topo.json'),
|
||||
keyField: 'properties.STATE',
|
||||
projection: 'Albers',
|
||||
},
|
||||
world: {
|
||||
name: 'World Map',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./world-topo.json'),
|
||||
keyField: 'id',
|
||||
projection: 'Equirectangular',
|
||||
rotate: [-9, 0, 0],
|
||||
},
|
||||
zambia: {
|
||||
name: 'Zambia',
|
||||
type: 'topojson',
|
||||
// @ts-ignore
|
||||
load: () => import('./zambia-topo.json'),
|
||||
keyField: 'properties.name',
|
||||
},
|
||||
};
|
||||
|
||||
/** List of available maps */
|
||||
export const maps: RawMapMetadata[] = Object.entries(mapsInfo).map(
|
||||
([key, metadata]) =>
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
({ ...metadata, key } as RawMapMetadata),
|
||||
);
|
||||
|
||||
/** All maps indexed by map key */
|
||||
export const mapsLookup = keyBy((m: RawMapMetadata) => m.key)(maps);
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
|
||||
import transformProps from './transformProps';
|
||||
import thumbnail from '../images/thumbnail.png';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
description: t('Choropleth Map'),
|
||||
name: t('ChoroplethMap'),
|
||||
thumbnail,
|
||||
});
|
||||
|
||||
export default class ChoroplethMapChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('../chart/ChoroplethMap'),
|
||||
metadata,
|
||||
transformProps,
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import { ChartProps } from '@superset-ui/chart';
|
||||
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { width, height, formData, queryData } = chartProps;
|
||||
const { map, encoding } = formData;
|
||||
const { data } = queryData;
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
map,
|
||||
encoding,
|
||||
data,
|
||||
};
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import type { Topology } from 'topojson-specification';
|
||||
import type { FeatureCollection } from 'geojson';
|
||||
import type Projection from './chart/Projection';
|
||||
|
||||
interface BaseMapMetadata {
|
||||
key: string;
|
||||
name: string;
|
||||
keyField: string;
|
||||
projection?: Projection;
|
||||
rotate?: [number, number] | [number, number, number];
|
||||
}
|
||||
|
||||
interface TopojsonMapMetadata extends BaseMapMetadata {
|
||||
type: 'topojson';
|
||||
load: () => Promise<Topology>;
|
||||
}
|
||||
|
||||
interface GeojsonMapMetadata extends BaseMapMetadata {
|
||||
type: 'geojson';
|
||||
load: () => Promise<FeatureCollection>;
|
||||
}
|
||||
|
||||
export type RawMapMetadata = TopojsonMapMetadata | GeojsonMapMetadata;
|
@ -0,0 +1,7 @@
|
||||
import { ChoroplethMapChartPlugin } from '../src';
|
||||
|
||||
describe('@superset-ui/plugin-chart-choropleth-map', () => {
|
||||
it('exists', () => {
|
||||
expect(ChoroplethMapChartPlugin).toBeDefined();
|
||||
});
|
||||
});
|
@ -0,0 +1,4 @@
|
||||
declare module '*.png' {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
@ -33,7 +33,8 @@
|
||||
"@types/react": "^16.3.0",
|
||||
"d3-cloud": "^1.2.5",
|
||||
"d3-scale": "^3.0.1",
|
||||
"encodable": "^0.3.3"
|
||||
"emotion-theming": "^10.0.27",
|
||||
"encodable": "^0.3.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart": "^0.13.0",
|
||||
|
@ -3705,6 +3705,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-1.3.1.tgz#35bf88264bd6bcda39251165bb827f67879c4384"
|
||||
integrity sha512-KAWvReOKMDreaAwOjdfQMm0HjcUMlQG47GwqdVKgmm20vTd2pucj0a70c3gUSHrnsmo6H2AMrkBsZU2UhJLq8A==
|
||||
|
||||
"@types/d3-geo@^1.11.1":
|
||||
version "1.11.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-1.11.1.tgz#e96ec91f16221d87507fec66b2cc889f52d2493e"
|
||||
integrity sha512-Ox8WWOG3igDRoep/dNsGbOiSJYdUG3ew/6z0ETvHyAtXZVBjOE0S96zSSmzgl0gqQ3RdZjn2eeJOj9oRcMZPkQ==
|
||||
dependencies:
|
||||
"@types/geojson" "*"
|
||||
|
||||
"@types/d3-interpolate@^1.3.1":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-1.3.1.tgz#1c280511f622de9b0b47d463fa55f9a4fd6f5fc8"
|
||||
@ -3791,6 +3798,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/fetch-mock/-/fetch-mock-6.0.5.tgz#acbc6771d43d7ebc1f0a8b7e3d57147618f8eacb"
|
||||
integrity sha512-rV8O2j/TIi0PtFCOlK55JnfKpE8Hm6PKFgrUZY/3FNHw4uBEMHnM+5ZickDO1duOyKxbpY3VES5T4NIwZXvodA==
|
||||
|
||||
"@types/geojson@*", "@types/geojson@^7946.0.3":
|
||||
version "7946.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.7.tgz#c8fa532b60a0042219cdf173ca21a975ef0666ad"
|
||||
integrity sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ==
|
||||
|
||||
"@types/glob@^7.1.1":
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
|
||||
@ -3945,7 +3957,7 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-dom@^16.9.6":
|
||||
"@types/react-dom@*", "@types/react-dom@^16.9.6":
|
||||
version "16.9.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423"
|
||||
integrity sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA==
|
||||
@ -3988,7 +4000,7 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@^16.3.0", "@types/react@^16.9.11", "@types/react@^16.9.34":
|
||||
"@types/react@*", "@types/react@^16.3.0", "@types/react@^16.9.11", "@types/react@^16.9.34", "@types/react@^16.9.35":
|
||||
version "16.9.35"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368"
|
||||
integrity sha512-q0n0SsWcGc8nDqH2GJfWQWUOmZSJhXV64CjVN5SvcNti3TdEaA3AH0D8DwNmMdzjMAC/78tB8nAZIlV8yTz+zQ==
|
||||
@ -4028,6 +4040,21 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.5.tgz#9adbc12950582aa65ead76bffdf39fe0c27a3c02"
|
||||
integrity sha512-/gG2M/Imw7cQFp8PGvz/SwocNrmKFjFsm5Pb8HdbHkZ1K8pmuPzOX4VeVoiEecFCVf4CsN1r3/BRvx+6sNqwtQ==
|
||||
|
||||
"@types/topojson-client@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/topojson-client/-/topojson-client-3.0.0.tgz#2517fae5abbdd3052eb191747c7d0bc268d69918"
|
||||
integrity sha512-HZH6E8XMhjkDEkkpe3HuIg95COuvjdnyy0EKrh8rAi1f6o/V6P3ly1kGyU2E8bpAffXDd2r+Rk5ceMX4XkqHnA==
|
||||
dependencies:
|
||||
"@types/geojson" "*"
|
||||
"@types/topojson-specification" "*"
|
||||
|
||||
"@types/topojson-specification@*", "@types/topojson-specification@^1.0.0":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/topojson-specification/-/topojson-specification-1.0.1.tgz#a80cb294290b79f2d674d3f5938c544ed2bd9d80"
|
||||
integrity sha512-ZZYZUgkmUls9Uhxx2WZNt9f/h2+H3abUUjOVmq+AaaDFckC5oAwd+MDp95kBirk+XCXrYj0hfpI6DSUiJMrpYQ==
|
||||
dependencies:
|
||||
"@types/geojson" "*"
|
||||
|
||||
"@types/tough-cookie@*":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.0.tgz#fef1904e4668b6e5ecee60c52cc6a078ffa6697d"
|
||||
@ -4184,6 +4211,15 @@
|
||||
dependencies:
|
||||
prop-types "^15.5.10"
|
||||
|
||||
"@vx/bounds@0.0.196":
|
||||
version "0.0.196"
|
||||
resolved "https://registry.yarnpkg.com/@vx/bounds/-/bounds-0.0.196.tgz#1aa77d0ad71b35e4cea0f06d12eb880633eb0f1a"
|
||||
integrity sha512-Az28AdWRheQJj1EefLUTSY3yzr3xPh+1IDgPxJMha1N2ZEa8YG+RA90icq2dtsaZ2PIDyvZx8hkPJkbN+SWgdQ==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
"@types/react-dom" "*"
|
||||
prop-types "^15.5.10"
|
||||
|
||||
"@vx/clip-path@0.0.140":
|
||||
version "0.0.140"
|
||||
resolved "https://registry.yarnpkg.com/@vx/clip-path/-/clip-path-0.0.140.tgz#b2623d004dd5c3c8a6afe8d060de59df51472d94"
|
||||
@ -4194,6 +4230,14 @@
|
||||
resolved "https://registry.yarnpkg.com/@vx/clip-path/-/clip-path-0.0.165.tgz#93cd65cc6a35319c7e403ce7b973ac1c8045b741"
|
||||
integrity sha512-mBCbgguLMVyGvar5FbxqyyY4NQFlnXoSLF0TrhgWYkF/FCXdE1CzBC+Y4iXIJOY0ZTtluqL9XrNdIDpx49AmuA==
|
||||
|
||||
"@vx/clip-path@^0.0.196":
|
||||
version "0.0.196"
|
||||
resolved "https://registry.yarnpkg.com/@vx/clip-path/-/clip-path-0.0.196.tgz#a79e3fd5df0376a23a38b86a5eb7271a2a4fc4d6"
|
||||
integrity sha512-yJmDaxEirHRUqJOGVaIUQQJ3HmV70JlT7TVXW//3LlNbAhR1gzBVnmi8wk02AvVog7h0NlR0jSHXCvGith8uLw==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
prop-types "^15.5.10"
|
||||
|
||||
"@vx/curve@0.0.140":
|
||||
version "0.0.140"
|
||||
resolved "https://registry.yarnpkg.com/@vx/curve/-/curve-0.0.140.tgz#29ef388e8b3718213d66a896d569dc1ebc8edf89"
|
||||
@ -4223,6 +4267,14 @@
|
||||
dependencies:
|
||||
"@vx/point" "0.0.136"
|
||||
|
||||
"@vx/event@0.0.196", "@vx/event@^0.0.196":
|
||||
version "0.0.196"
|
||||
resolved "https://registry.yarnpkg.com/@vx/event/-/event-0.0.196.tgz#be859997ef127819ef14a79e2447d737134b7dd2"
|
||||
integrity sha512-Tv9AaXce3tuT5bSjvi26pHWVdRll44iYJVAmT4SPIK3brVxejngiL/Z+HCWeNjBg+YXexzKETJSbp8XjwFxohw==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
"@vx/point" "0.0.196"
|
||||
|
||||
"@vx/event@^0.0.165":
|
||||
version "0.0.165"
|
||||
resolved "https://registry.yarnpkg.com/@vx/event/-/event-0.0.165.tgz#675d89fdfdc08d0c99c36ff1a381ea50fccfba2e"
|
||||
@ -4378,6 +4430,16 @@
|
||||
classnames "^2.2.5"
|
||||
prop-types "^15.5.10"
|
||||
|
||||
"@vx/pattern@^0.0.196":
|
||||
version "0.0.196"
|
||||
resolved "https://registry.yarnpkg.com/@vx/pattern/-/pattern-0.0.196.tgz#f7caa7ce8a3445e14f532da593eb73cb8bad8f6c"
|
||||
integrity sha512-doMzynNdIpwgXsTFQky3vv+wW9NcxBt8zjHSlseY/MUxAsmxOyi5YrEPPlzDyxhcFwDaA7+Xh1c5z+5Gokd7WA==
|
||||
dependencies:
|
||||
"@types/classnames" "^2.2.9"
|
||||
"@types/react" "*"
|
||||
classnames "^2.2.5"
|
||||
prop-types "^15.5.10"
|
||||
|
||||
"@vx/point@0.0.136":
|
||||
version "0.0.136"
|
||||
resolved "https://registry.yarnpkg.com/@vx/point/-/point-0.0.136.tgz#93b325b4b95c9d5b96df740f4204017f57396559"
|
||||
@ -4633,6 +4695,17 @@
|
||||
classnames "^2.2.5"
|
||||
prop-types "^15.5.10"
|
||||
|
||||
"@vx/tooltip@^0.0.196":
|
||||
version "0.0.196"
|
||||
resolved "https://registry.yarnpkg.com/@vx/tooltip/-/tooltip-0.0.196.tgz#1f35105b42064f593025594c0d8393ab3111bc1f"
|
||||
integrity sha512-Lt45OA2dyIB+Fs+We++wL3bnsJurqugSwirmt+6xh4HtAcwnXUQXOqCdlFYfPKCqNukqw0yAPiaIuLxRRvhEvA==
|
||||
dependencies:
|
||||
"@types/classnames" "^2.2.9"
|
||||
"@types/react" "*"
|
||||
"@vx/bounds" "0.0.196"
|
||||
classnames "^2.2.5"
|
||||
prop-types "^15.5.10"
|
||||
|
||||
"@vx/voronoi@^0.0.165":
|
||||
version "0.0.165"
|
||||
resolved "https://registry.yarnpkg.com/@vx/voronoi/-/voronoi-0.0.165.tgz#11ab585199b0dccf403544a6ad378a505bfb913b"
|
||||
@ -4643,6 +4716,15 @@
|
||||
d3-voronoi "^1.1.2"
|
||||
prop-types "^15.6.1"
|
||||
|
||||
"@vx/zoom@^0.0.196":
|
||||
version "0.0.196"
|
||||
resolved "https://registry.yarnpkg.com/@vx/zoom/-/zoom-0.0.196.tgz#22dcd91f1c058d2d5cd35f1d9a06a6b4d134a4e4"
|
||||
integrity sha512-RGGREy4VcBx0z3miQQ8z/FZTuSptcdip2LiDKAnMlr/SJE8U5MGGAfxtkoLsbuIlJgatSHg0V86lCw7AY7EG9g==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
"@vx/event" "0.0.196"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
"@webassemblyjs/ast@1.9.0":
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964"
|
||||
@ -7518,6 +7600,13 @@ d3-geo@^1.10.0, d3-geo@^1.11.9:
|
||||
dependencies:
|
||||
d3-array "1"
|
||||
|
||||
d3-geo@^1.12.0:
|
||||
version "1.12.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.12.0.tgz#58ddbdf4d9db5f199db69d1b7c93dca6454a6f24"
|
||||
integrity sha512-NalZVW+6/SpbKcnl+BCO67m8gX+nGeJdo6oGL9H6BRUGUL1e+AtPcP4vE4TwCQ/gl8y5KE7QvBzrLn+HsKIl+w==
|
||||
dependencies:
|
||||
d3-array "1"
|
||||
|
||||
d3-hierarchy@^1.1.8:
|
||||
version "1.1.9"
|
||||
resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz#2f6bee24caaea43f8dc37545fa01628559647a83"
|
||||
@ -8348,7 +8437,7 @@ emotion-theming@^10.0.19, emotion-theming@^10.0.27:
|
||||
"@emotion/weak-memoize" "0.2.5"
|
||||
hoist-non-react-statics "^3.3.0"
|
||||
|
||||
encodable@^0.3.3, encodable@^0.3.4:
|
||||
encodable@^0.3.4:
|
||||
version "0.3.5"
|
||||
resolved "https://registry.yarnpkg.com/encodable/-/encodable-0.3.5.tgz#4d273623f86680939474b555183753c4976b3cc6"
|
||||
integrity sha512-ofpVxFEYwFjgk94syrbdTc4nn6nxOO8rvJTieFz/Ko7V5oJMDpYwJYO87eL5xbc84XqD07vUJp50Zi54y+WAhw==
|
||||
@ -8367,6 +8456,25 @@ encodable@^0.3.3, encodable@^0.3.4:
|
||||
vega "^5.9.1"
|
||||
vega-lite "~4.1.0"
|
||||
|
||||
encodable@^0.3.7:
|
||||
version "0.3.7"
|
||||
resolved "https://registry.yarnpkg.com/encodable/-/encodable-0.3.7.tgz#6906ab4b03be36108f70a4f3288882b601caec1c"
|
||||
integrity sha512-aFbrhAsxPUN+OMZ28+N2tOqaeIFs4I1t/Li9+WmQwPFONrsu7InwMehLR3XTOQjmo+ucCBcSV9ABjQqMawgKNw==
|
||||
dependencies:
|
||||
"@types/d3-array" "^2.0.0"
|
||||
"@types/d3-interpolate" "^1.3.1"
|
||||
"@types/d3-scale" "^2.1.1"
|
||||
"@types/d3-time" "^1.0.10"
|
||||
"@types/lodash" "^4.14.149"
|
||||
d3-array "^2.3.1"
|
||||
d3-interpolate "^1.3.2"
|
||||
d3-scale "^3.0.1"
|
||||
d3-time "^1.0.11"
|
||||
lodash "^4.17.15"
|
||||
reselect "^4.0.0"
|
||||
vega "^5.9.1"
|
||||
vega-lite "~4.1.0"
|
||||
|
||||
encodeurl@~1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||
@ -9761,6 +9869,11 @@ geojson-vt@^3.2.1:
|
||||
resolved "https://registry.yarnpkg.com/geojson-vt/-/geojson-vt-3.2.1.tgz#f8adb614d2c1d3f6ee7c4265cad4bbf3ad60c8b7"
|
||||
integrity sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==
|
||||
|
||||
geojson@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/geojson/-/geojson-0.5.0.tgz#3cd6c96399be65b56ee55596116fe9191ce701c0"
|
||||
integrity sha1-PNbJY5m+ZbVu5VWWEW/pGRznAcA=
|
||||
|
||||
get-caller-file@^2.0.1:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||
|
Loading…
Reference in New Issue
Block a user