@kanaries/graphic-walker 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/button/base.d.ts +1 -0
- package/dist/components/leafletRenderer/ChoroplethRenderer.d.ts +2 -1
- package/dist/components/spinner.d.ts +3 -1
- package/dist/graphic-walker.es.js +12120 -12095
- package/dist/graphic-walker.es.js.map +1 -1
- package/dist/graphic-walker.umd.js +123 -123
- package/dist/graphic-walker.umd.js.map +1 -1
- package/dist/hooks/service.d.ts +3 -0
- package/dist/interfaces.d.ts +5 -0
- package/dist/store/visualSpecStore.d.ts +6 -2
- package/package.json +1 -1
- package/src/App.tsx +1 -1
- package/src/components/button/base.ts +1 -0
- package/src/components/button/default.tsx +2 -1
- package/src/components/leafletRenderer/ChoroplethRenderer.tsx +6 -2
- package/src/components/leafletRenderer/geoConfigPanel.tsx +52 -48
- package/src/components/leafletRenderer/index.tsx +2 -1
- package/src/components/loadingLayer.tsx +5 -1
- package/src/components/spinner.tsx +3 -2
- package/src/fields/datasetFields/meaFields.tsx +2 -1
- package/src/hooks/service.ts +30 -0
- package/src/interfaces.ts +6 -0
- package/src/renderer/index.tsx +1 -1
- package/src/store/visualSpecStore.ts +21 -10
package/dist/interfaces.d.ts
CHANGED
|
@@ -223,8 +223,13 @@ export interface IVisualConfig {
|
|
|
223
223
|
};
|
|
224
224
|
geojson?: FeatureCollection;
|
|
225
225
|
geoKey?: string;
|
|
226
|
+
geoUrl?: IGeoUrl;
|
|
226
227
|
limit: number;
|
|
227
228
|
}
|
|
229
|
+
export interface IGeoUrl {
|
|
230
|
+
type: 'GeoJSON' | 'TopoJSON';
|
|
231
|
+
url: string;
|
|
232
|
+
}
|
|
228
233
|
export interface IVisSpec {
|
|
229
234
|
readonly visId: string;
|
|
230
235
|
readonly name?: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DataSet, DraggableFieldState, IFilterRule, IGeographicData, ISortMode, IStackMode, IViewField, IVisualConfig, Specification, IComputationFunction } from '../interfaces';
|
|
1
|
+
import { DataSet, DraggableFieldState, IFilterRule, IGeographicData, ISortMode, IStackMode, IViewField, IVisualConfig, Specification, IComputationFunction, IGeoUrl } from '../interfaces';
|
|
2
2
|
import { VisSpecWithHistory } from '../models/visSpecHistory';
|
|
3
3
|
import { IStoInfo } from '../utils/save';
|
|
4
4
|
import { CommonStore } from './commonStore';
|
|
@@ -723,12 +723,16 @@ export declare class VizSpecStore {
|
|
|
723
723
|
bbox?: import("geojson").BBox | undefined;
|
|
724
724
|
} | undefined;
|
|
725
725
|
readonly geoKey?: string | undefined;
|
|
726
|
+
readonly geoUrl?: {
|
|
727
|
+
type: "GeoJSON" | "TopoJSON";
|
|
728
|
+
url: string;
|
|
729
|
+
} | undefined;
|
|
726
730
|
readonly limit: number;
|
|
727
731
|
};
|
|
728
732
|
}[];
|
|
729
733
|
importStoInfo(stoInfo: IStoInfo): void;
|
|
730
734
|
importRaw(raw: string): void;
|
|
731
|
-
setGeographicData(data: IGeographicData, geoKey: string): void;
|
|
735
|
+
setGeographicData(data: IGeographicData, geoKey: string, geoUrl?: IGeoUrl): void;
|
|
732
736
|
updateGeoKey(key: string): void;
|
|
733
737
|
private visSpecEncoder;
|
|
734
738
|
get limit(): number;
|
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -185,7 +185,7 @@ const App = observer<IGWProps>(function App(props) {
|
|
|
185
185
|
<VisualSettings rendererHandler={rendererRef} darkModePreference={dark} exclude={toolbar?.exclude} extra={toolbar?.extra} />
|
|
186
186
|
<CodeExport />
|
|
187
187
|
<VisualConfig />
|
|
188
|
-
<GeoConfigPanel />
|
|
188
|
+
{commonStore.showGeoJSONConfigPanel && <GeoConfigPanel />}
|
|
189
189
|
<div className="md:grid md:grid-cols-12 xl:grid-cols-6">
|
|
190
190
|
<div className="md:col-span-3 xl:col-span-1">
|
|
191
191
|
<DatasetFields />
|
|
@@ -2,7 +2,7 @@ import React from "react";
|
|
|
2
2
|
import { ButtonBaseProps } from "./base";
|
|
3
3
|
|
|
4
4
|
const DefaultButton: React.FC<ButtonBaseProps> = (props) => {
|
|
5
|
-
const { text, onClick, disabled, className } = props;
|
|
5
|
+
const { text, onClick, disabled, className, icon } = props;
|
|
6
6
|
let btnClassName = "inline-flex items-center rounded border border-gray-300 bg-white dark:bg-zinc-900 px-2.5 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-200 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-50"
|
|
7
7
|
if (className) {
|
|
8
8
|
btnClassName = btnClassName + " " + className;
|
|
@@ -14,6 +14,7 @@ const DefaultButton: React.FC<ButtonBaseProps> = (props) => {
|
|
|
14
14
|
disabled={disabled}
|
|
15
15
|
>
|
|
16
16
|
{text}
|
|
17
|
+
{icon}
|
|
17
18
|
</button>
|
|
18
19
|
);
|
|
19
20
|
};
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import React, { Fragment, forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from "react";
|
|
2
2
|
import { CircleMarker, MapContainer, Polygon, Marker, TileLayer, Tooltip } from "react-leaflet";
|
|
3
3
|
import { type Map, divIcon } from "leaflet";
|
|
4
|
-
import type { DeepReadonly, IRow, IViewField, VegaGlobalConfig } from "../../interfaces";
|
|
4
|
+
import type { DeepReadonly, IGeoUrl, IRow, IViewField, VegaGlobalConfig } from "../../interfaces";
|
|
5
5
|
import type { FeatureCollection, Geometry } from "geojson";
|
|
6
6
|
import { getMeaAggKey } from "../../utils";
|
|
7
7
|
import { useColorScale, useOpacityScale } from "./encodings";
|
|
8
8
|
import { isValidLatLng } from "./POIRenderer";
|
|
9
9
|
import { TooltipContent } from "./tooltip";
|
|
10
10
|
import { useAppRootContext } from "../appRoot";
|
|
11
|
+
import { useGeoJSON } from "../../hooks/service";
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
export interface IChoroplethRendererProps {
|
|
@@ -15,6 +16,7 @@ export interface IChoroplethRendererProps {
|
|
|
15
16
|
data: IRow[];
|
|
16
17
|
allFields: DeepReadonly<IViewField[]>;
|
|
17
18
|
features: FeatureCollection | undefined;
|
|
19
|
+
featuresUrl?: IGeoUrl;
|
|
18
20
|
geoKey: string;
|
|
19
21
|
defaultAggregated: boolean;
|
|
20
22
|
geoId: DeepReadonly<IViewField>;
|
|
@@ -88,10 +90,12 @@ const resolveCenter = (coordinates: [lat: number, lng: number][]): [lng: number,
|
|
|
88
90
|
};
|
|
89
91
|
|
|
90
92
|
const ChoroplethRenderer = forwardRef<IChoroplethRendererRef, IChoroplethRendererProps>(function ChoroplethRenderer (props, ref) {
|
|
91
|
-
const { name, data, allFields, features, geoKey, defaultAggregated, geoId, color, opacity, text, details, vegaConfig, scaleIncludeUnmatchedChoropleth } = props;
|
|
93
|
+
const { name, data, allFields, features: localFeatures, featuresUrl, geoKey, defaultAggregated, geoId, color, opacity, text, details, vegaConfig, scaleIncludeUnmatchedChoropleth } = props;
|
|
92
94
|
|
|
93
95
|
useImperativeHandle(ref, () => ({}));
|
|
94
96
|
|
|
97
|
+
const features = useGeoJSON(localFeatures, featuresUrl)
|
|
98
|
+
|
|
95
99
|
const geoIndices = useMemo(() => {
|
|
96
100
|
if (geoId) {
|
|
97
101
|
return data.map(row => row[geoId.fid]);
|
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
import { observer } from 'mobx-react-lite';
|
|
2
2
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import
|
|
4
|
+
import Spinner from '../spinner';
|
|
5
5
|
import { useGlobalStore } from '../../store';
|
|
6
6
|
import Modal from '../modal';
|
|
7
7
|
import PrimaryButton from '../button/primary';
|
|
8
8
|
import DefaultButton from '../button/default';
|
|
9
|
-
import type { Topology } from '../../interfaces';
|
|
9
|
+
import type { IGeoUrl, Topology } from '../../interfaces';
|
|
10
10
|
|
|
11
11
|
const GeoConfigPanel: React.FC = (props) => {
|
|
12
12
|
const { commonStore, vizStore } = useGlobalStore();
|
|
13
13
|
const { showGeoJSONConfigPanel } = commonStore;
|
|
14
14
|
const { visualConfig } = vizStore;
|
|
15
|
-
const { geoKey, geojson } = visualConfig;
|
|
15
|
+
const { geoKey, geojson, geoUrl } = visualConfig;
|
|
16
16
|
const { t: tGlobal } = useTranslation('translation');
|
|
17
17
|
const { t } = useTranslation('translation', { keyPrefix: 'main.tabpanel.settings' });
|
|
18
18
|
|
|
19
|
-
const [dataMode, setDataMode] = useState<'GeoJSON' | 'TopoJSON'>('GeoJSON');
|
|
19
|
+
const [dataMode, setDataMode] = useState<'GeoJSON' | 'TopoJSON'>(geoUrl?.type ?? 'GeoJSON');
|
|
20
20
|
const [featureId, setFeatureId] = useState('');
|
|
21
|
-
const [url, setUrl] = useState('');
|
|
21
|
+
const [url, setUrl] = useState(geoUrl?.url ?? '');
|
|
22
22
|
const [geoJSON, setGeoJSON] = useState('');
|
|
23
23
|
const [topoJSON, setTopoJSON] = useState('');
|
|
24
24
|
const [topoJSONKey, setTopoJSONKey] = useState('');
|
|
25
|
+
const [loadedUrl, setLoadedUrl] = useState<IGeoUrl | undefined>(geoUrl);
|
|
26
|
+
const [loading, setLoading] = useState(false);
|
|
25
27
|
|
|
26
28
|
const defaultTopoJSONKey = useMemo(() => {
|
|
27
29
|
try {
|
|
@@ -40,13 +42,37 @@ const GeoConfigPanel: React.FC = (props) => {
|
|
|
40
42
|
setGeoJSON(geojson ? JSON.stringify(geojson, null, 2) : '');
|
|
41
43
|
}, [geojson]);
|
|
42
44
|
|
|
45
|
+
const handleSubmit = () => {
|
|
46
|
+
try {
|
|
47
|
+
const json = JSON.parse(dataMode === 'GeoJSON' ? geoJSON : topoJSON);
|
|
48
|
+
if (dataMode === 'TopoJSON') {
|
|
49
|
+
vizStore.setGeographicData(
|
|
50
|
+
{
|
|
51
|
+
type: 'TopoJSON',
|
|
52
|
+
data: json,
|
|
53
|
+
objectKey: topoJSONKey || defaultTopoJSONKey,
|
|
54
|
+
},
|
|
55
|
+
featureId,
|
|
56
|
+
loadedUrl?.type === 'TopoJSON' ? loadedUrl : undefined
|
|
57
|
+
);
|
|
58
|
+
} else {
|
|
59
|
+
vizStore.setGeographicData(
|
|
60
|
+
{
|
|
61
|
+
type: 'GeoJSON',
|
|
62
|
+
data: json,
|
|
63
|
+
},
|
|
64
|
+
featureId,
|
|
65
|
+
loadedUrl?.type === 'GeoJSON' ? loadedUrl : undefined
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
commonStore.setShowGeoJSONConfigPanel(false);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error(err);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
43
74
|
return (
|
|
44
|
-
<Modal
|
|
45
|
-
show={showGeoJSONConfigPanel}
|
|
46
|
-
onClose={() => {
|
|
47
|
-
commonStore.setShowGeoJSONConfigPanel(false);
|
|
48
|
-
}}
|
|
49
|
-
>
|
|
75
|
+
<Modal show={showGeoJSONConfigPanel} onClose={() => commonStore.setShowGeoJSONConfigPanel(false)}>
|
|
50
76
|
<div>
|
|
51
77
|
<h2 className="text-lg mb-4">{t('geography')}</h2>
|
|
52
78
|
<div>
|
|
@@ -57,9 +83,7 @@ const GeoConfigPanel: React.FC = (props) => {
|
|
|
57
83
|
type="text"
|
|
58
84
|
className="block w-full rounded-md border-0 py-1 px-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
|
59
85
|
value={featureId}
|
|
60
|
-
onChange={(e) =>
|
|
61
|
-
setFeatureId(e.target.value);
|
|
62
|
-
}}
|
|
86
|
+
onChange={(e) => setFeatureId(e.target.value)}
|
|
63
87
|
/>
|
|
64
88
|
</div>
|
|
65
89
|
</div>
|
|
@@ -112,14 +136,20 @@ const GeoConfigPanel: React.FC = (props) => {
|
|
|
112
136
|
<DefaultButton
|
|
113
137
|
text={t('geography_settings.load')}
|
|
114
138
|
className="mr-2"
|
|
139
|
+
disabled={loading}
|
|
140
|
+
icon={loading ? <Spinner className='text-black' /> : undefined}
|
|
115
141
|
onClick={() => {
|
|
116
142
|
if (url) {
|
|
143
|
+
setLoading(true);
|
|
117
144
|
fetch(url)
|
|
118
145
|
.then((res) => res.json())
|
|
119
146
|
.then((json) => {
|
|
120
|
-
(dataMode === 'GeoJSON' ? setGeoJSON : setTopoJSON)(
|
|
121
|
-
|
|
122
|
-
);
|
|
147
|
+
(dataMode === 'GeoJSON' ? setGeoJSON : setTopoJSON)(JSON.stringify(json, null, 2));
|
|
148
|
+
setLoadedUrl({ type: dataMode, url });
|
|
149
|
+
setLoading(false);
|
|
150
|
+
})
|
|
151
|
+
.catch(() => {
|
|
152
|
+
setLoading(false);
|
|
123
153
|
});
|
|
124
154
|
}
|
|
125
155
|
}}
|
|
@@ -131,13 +161,14 @@ const GeoConfigPanel: React.FC = (props) => {
|
|
|
131
161
|
placeholder={t('geography_settings.jsonInputPlaceholder', { format: dataMode.toLowerCase() })}
|
|
132
162
|
onChange={(e) => {
|
|
133
163
|
(dataMode === 'GeoJSON' ? setGeoJSON : setTopoJSON)(e.target.value);
|
|
164
|
+
if (loadedUrl?.type === dataMode) {
|
|
165
|
+
setLoadedUrl(undefined);
|
|
166
|
+
}
|
|
134
167
|
}}
|
|
135
168
|
/>
|
|
136
169
|
{dataMode === 'TopoJSON' && (
|
|
137
170
|
<div className="flex items-center space-x-2">
|
|
138
|
-
<label className="text-xs whitespace-nowrap capitalize">
|
|
139
|
-
{t('geography_settings.objectKey')}
|
|
140
|
-
</label>
|
|
171
|
+
<label className="text-xs whitespace-nowrap capitalize">{t('geography_settings.objectKey')}</label>
|
|
141
172
|
<input
|
|
142
173
|
type="text"
|
|
143
174
|
className="block w-full rounded-md border-0 py-1 px-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
|
@@ -153,34 +184,7 @@ const GeoConfigPanel: React.FC = (props) => {
|
|
|
153
184
|
</div>
|
|
154
185
|
</div>
|
|
155
186
|
<div className="mt-4">
|
|
156
|
-
<PrimaryButton
|
|
157
|
-
text={tGlobal('actions.confirm')}
|
|
158
|
-
className="mr-2"
|
|
159
|
-
onClick={() => {
|
|
160
|
-
try {
|
|
161
|
-
const json = JSON.parse(dataMode === 'GeoJSON' ? geoJSON : topoJSON);
|
|
162
|
-
if (dataMode === 'TopoJSON') {
|
|
163
|
-
runInAction(() => {
|
|
164
|
-
vizStore.setGeographicData({
|
|
165
|
-
type: 'TopoJSON',
|
|
166
|
-
data: json,
|
|
167
|
-
objectKey: topoJSONKey || defaultTopoJSONKey,
|
|
168
|
-
}, featureId);
|
|
169
|
-
});
|
|
170
|
-
} else {
|
|
171
|
-
runInAction(() => {
|
|
172
|
-
vizStore.setGeographicData({
|
|
173
|
-
type: 'GeoJSON',
|
|
174
|
-
data: json,
|
|
175
|
-
}, featureId);
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
commonStore.setShowGeoJSONConfigPanel(false);
|
|
179
|
-
} catch (err) {
|
|
180
|
-
console.error(err);
|
|
181
|
-
}
|
|
182
|
-
}}
|
|
183
|
-
/>
|
|
187
|
+
<PrimaryButton text={tGlobal('actions.confirm')} className="mr-2" onClick={handleSubmit} />
|
|
184
188
|
<DefaultButton
|
|
185
189
|
text={tGlobal('actions.cancel')}
|
|
186
190
|
className="mr-2"
|
|
@@ -20,7 +20,7 @@ export const LEAFLET_DEFAULT_HEIGHT = 600;
|
|
|
20
20
|
const LeafletRenderer = forwardRef<ILeafletRendererRef, ILeafletRendererProps>(function LeafletRenderer (props, ref) {
|
|
21
21
|
const { name, draggableFieldState, data, visualConfig, vegaConfig = {} } = props;
|
|
22
22
|
const { latitude: [lat], longitude: [lng], geoId: [geoId], dimensions, measures, size: [size], color: [color], opacity: [opacity], text: [text], details } = draggableFieldState;
|
|
23
|
-
const { defaultAggregated, geoms: [markType], geojson, geoKey = '', scaleIncludeUnmatchedChoropleth = false } = visualConfig;
|
|
23
|
+
const { defaultAggregated, geoms: [markType], geojson, geoKey = '', geoUrl, scaleIncludeUnmatchedChoropleth = false } = visualConfig;
|
|
24
24
|
const allFields = useMemo(() => [...dimensions, ...measures], [dimensions, measures]);
|
|
25
25
|
const latField = useMemo(() => allFields.find((f) => f.geoRole === 'latitude'), [allFields]);
|
|
26
26
|
const lngField = useMemo(() => allFields.find((f) => f.geoRole === 'longitude'), [allFields]);
|
|
@@ -50,6 +50,7 @@ const LeafletRenderer = forwardRef<ILeafletRendererRef, ILeafletRendererProps>(f
|
|
|
50
50
|
data={data}
|
|
51
51
|
allFields={allFields}
|
|
52
52
|
features={geojson}
|
|
53
|
+
featuresUrl={geoUrl}
|
|
53
54
|
geoKey={geoKey}
|
|
54
55
|
defaultAggregated={defaultAggregated}
|
|
55
56
|
geoId={geoId}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import Spinner from './spinner';
|
|
2
3
|
|
|
3
4
|
export default function LoadingLayer () {
|
|
4
5
|
return <div className="bg-gray-100/50 dark:bg-gray-700/50 absolute top-0 left-0 right-0 bottom-0 z-50 flex items-center justify-center">
|
|
5
|
-
|
|
6
|
+
<Spinner className="text-indigo-500" />
|
|
7
|
+
<span className="text-sm text-indigo-500">
|
|
8
|
+
Loading...
|
|
9
|
+
</span>
|
|
6
10
|
</div>
|
|
7
11
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
-
export default function Spinner() {
|
|
3
|
+
export default function Spinner(props: { className?: string }) {
|
|
4
|
+
const className = props.className || 'text-white';
|
|
4
5
|
return (
|
|
5
|
-
<svg className=
|
|
6
|
+
<svg className={`animate-spin ml-2 mr-2 h-5 w-5 ${className}`} viewBox="0 0 24 24">
|
|
6
7
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
7
8
|
<path
|
|
8
9
|
className="opacity-75"
|
|
@@ -5,6 +5,7 @@ import { useGlobalStore } from "../../store";
|
|
|
5
5
|
import DataTypeIcon from "../../components/dataTypeIcon";
|
|
6
6
|
import { FieldPill } from "./fieldPill";
|
|
7
7
|
import DropdownContext, { IDropdownContextOption } from "../../components/dropdownContext";
|
|
8
|
+
import { COUNT_FIELD_ID } from "../../constants";
|
|
8
9
|
|
|
9
10
|
interface Props {
|
|
10
11
|
provided: DroppableProvided;
|
|
@@ -50,7 +51,7 @@ const MeaFields: React.FC<Props> = (props) => {
|
|
|
50
51
|
return (
|
|
51
52
|
<div className="block">
|
|
52
53
|
<DropdownContext
|
|
53
|
-
disable={snapshot.isDragging}
|
|
54
|
+
disable={snapshot.isDragging || f.fid === COUNT_FIELD_ID}
|
|
54
55
|
options={MEA_ACTION_OPTIONS}
|
|
55
56
|
onSelect={(v, opIndex) => {
|
|
56
57
|
fieldActionHandler(v, opIndex, index);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { IGeoUrl } from '../interfaces';
|
|
2
|
+
import { useState, useEffect, useRef } from 'react';
|
|
3
|
+
import { feature } from 'topojson-client';
|
|
4
|
+
import type { FeatureCollection } from "geojson";
|
|
5
|
+
|
|
6
|
+
const GeoJSONDict: Record<string, FeatureCollection> = {};
|
|
7
|
+
export function useGeoJSON(geojson?: FeatureCollection, url?: IGeoUrl) {
|
|
8
|
+
const key = url ? `${url.type}(${url.url})` : '';
|
|
9
|
+
const data = (geojson || GeoJSONDict[key] || url) as IGeoUrl | FeatureCollection;
|
|
10
|
+
const [_, setLastFetched] = useState(0);
|
|
11
|
+
const lastFetchedRef = useRef(0);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (data === url && url) {
|
|
14
|
+
const timestamp = Date.now();
|
|
15
|
+
lastFetchedRef.current = timestamp;
|
|
16
|
+
fetch(url.url)
|
|
17
|
+
.then((res) => res.json())
|
|
18
|
+
.then((json) => {
|
|
19
|
+
if (timestamp !== lastFetchedRef.current) return;
|
|
20
|
+
if (url.type === 'GeoJSON') {
|
|
21
|
+
data[key] = json;
|
|
22
|
+
} else {
|
|
23
|
+
data[key] = feature(json, Object.keys(json.objects)[0]);
|
|
24
|
+
}
|
|
25
|
+
setLastFetched(timestamp);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}, [data]);
|
|
29
|
+
return data === url ? undefined : data as FeatureCollection;
|
|
30
|
+
}
|
package/src/interfaces.ts
CHANGED
|
@@ -253,9 +253,15 @@ export interface IVisualConfig {
|
|
|
253
253
|
};
|
|
254
254
|
geojson?: FeatureCollection;
|
|
255
255
|
geoKey?: string;
|
|
256
|
+
geoUrl?: IGeoUrl;
|
|
256
257
|
limit: number;
|
|
257
258
|
}
|
|
258
259
|
|
|
260
|
+
export interface IGeoUrl {
|
|
261
|
+
type: 'GeoJSON' | 'TopoJSON',
|
|
262
|
+
url: string,
|
|
263
|
+
}
|
|
264
|
+
|
|
259
265
|
export interface IVisSpec {
|
|
260
266
|
readonly visId: string;
|
|
261
267
|
readonly name?: string;
|
package/src/renderer/index.tsx
CHANGED
|
@@ -76,7 +76,7 @@ const Renderer = forwardRef<IReactVegaHandler, RendererProps>(function (props, r
|
|
|
76
76
|
setViewConfig(latestFromRef.current.visualConfig);
|
|
77
77
|
});
|
|
78
78
|
}
|
|
79
|
-
}, [waiting, vizStore]);
|
|
79
|
+
}, [waiting, data, vizStore]);
|
|
80
80
|
|
|
81
81
|
useChartIndexControl({
|
|
82
82
|
count: visList.length,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { IReactionDisposer, makeAutoObservable, observable, computed, reaction, toJS } from 'mobx';
|
|
2
2
|
import produce from 'immer';
|
|
3
3
|
import { feature } from 'topojson-client';
|
|
4
|
-
import type { FeatureCollection } from
|
|
4
|
+
import type { FeatureCollection } from 'geojson';
|
|
5
5
|
import {
|
|
6
6
|
DataSet,
|
|
7
7
|
DraggableFieldState,
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
IVisualConfig,
|
|
17
17
|
Specification,
|
|
18
18
|
IComputationFunction,
|
|
19
|
+
IGeoUrl,
|
|
19
20
|
} from '../interfaces';
|
|
20
21
|
import { CHANNEL_LIMIT, GEMO_TYPES, MetaFieldKeys } from '../config';
|
|
21
22
|
import { VisSpecWithHistory } from '../models/visSpecHistory';
|
|
@@ -103,10 +104,14 @@ function isDraggableStateEmpty(state: DeepReadonly<DraggableFieldState>): boolea
|
|
|
103
104
|
return Object.values(state).every((value) => value.length === 0);
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
function withTimeout<T extends any[], U>(f: (...args: T) => Promise<U>, timeout: number){
|
|
107
|
-
return (...args: T) =>
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
function withTimeout<T extends any[], U>(f: (...args: T) => Promise<U>, timeout: number) {
|
|
108
|
+
return (...args: T) =>
|
|
109
|
+
Promise.race([
|
|
110
|
+
f(...args),
|
|
111
|
+
new Promise<never>((_, reject) => {
|
|
112
|
+
setTimeout(() => reject(new Error('timeout')), timeout);
|
|
113
|
+
}),
|
|
114
|
+
]);
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
export class VizSpecStore {
|
|
@@ -383,8 +388,8 @@ export class VizSpecStore {
|
|
|
383
388
|
return ((config as unknown as { [k: string]: boolean })[configKey] = Boolean(value));
|
|
384
389
|
}
|
|
385
390
|
case configKey === 'geoms' && Array.isArray(value):
|
|
386
|
-
case configKey ===
|
|
387
|
-
case configKey ===
|
|
391
|
+
case configKey === 'showTableSummary':
|
|
392
|
+
case configKey === 'coordSystem':
|
|
388
393
|
case configKey === 'size' && typeof value === 'object':
|
|
389
394
|
case configKey === 'sorted':
|
|
390
395
|
case configKey === 'zeroScale':
|
|
@@ -817,9 +822,10 @@ export class VizSpecStore {
|
|
|
817
822
|
const content = parseGWContent(raw);
|
|
818
823
|
this.importStoInfo(content);
|
|
819
824
|
}
|
|
820
|
-
|
|
821
|
-
public setGeographicData(data: IGeographicData, geoKey: string) {
|
|
822
|
-
const geoJSON =
|
|
825
|
+
|
|
826
|
+
public setGeographicData(data: IGeographicData, geoKey: string, geoUrl?: IGeoUrl) {
|
|
827
|
+
const geoJSON =
|
|
828
|
+
data.type === 'GeoJSON' ? data.data : (feature(data.data, data.objectKey || Object.keys(data.data.objects)[0]) as unknown as FeatureCollection);
|
|
823
829
|
if (!('features' in geoJSON)) {
|
|
824
830
|
console.error('Invalid GeoJSON: GeoJSON must be a FeatureCollection, but got', geoJSON);
|
|
825
831
|
return;
|
|
@@ -827,6 +833,7 @@ export class VizSpecStore {
|
|
|
827
833
|
this.useMutable(({ config }) => {
|
|
828
834
|
config.geojson = geoJSON;
|
|
829
835
|
config.geoKey = geoKey;
|
|
836
|
+
config.geoUrl = geoUrl;
|
|
830
837
|
});
|
|
831
838
|
}
|
|
832
839
|
public updateGeoKey(key: string) {
|
|
@@ -856,6 +863,10 @@ export class VizSpecStore {
|
|
|
856
863
|
...visSpec.encodings,
|
|
857
864
|
filters: updatedFilters,
|
|
858
865
|
},
|
|
866
|
+
config: {
|
|
867
|
+
...visSpec.config,
|
|
868
|
+
geojson: visSpec.config.geoUrl ? undefined : visSpec.config.geojson
|
|
869
|
+
}
|
|
859
870
|
};
|
|
860
871
|
});
|
|
861
872
|
return updatedVisList;
|