@kanaries/graphic-walker 0.4.4 → 0.4.6

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.
@@ -0,0 +1,3 @@
1
+ import { IGeoUrl } from '../interfaces';
2
+ import type { FeatureCollection } from "geojson";
3
+ export declare function useGeoJSON(geojson?: FeatureCollection, url?: IGeoUrl): FeatureCollection<import("geojson").Geometry, import("geojson").GeoJsonProperties> | undefined;
@@ -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;
@@ -3,6 +3,7 @@ import { IDarkMode, IThemeKey, IComputationFunction } from '../interfaces';
3
3
  import { IReactVegaHandler } from '../vis/react-vega';
4
4
  interface RendererProps {
5
5
  themeKey?: IThemeKey;
6
+ themeConfig?: any;
6
7
  dark?: IDarkMode;
7
8
  computationFunction: IComputationFunction;
8
9
  }
@@ -3,6 +3,7 @@ import type { IDarkMode, IRow, IThemeKey, DraggableFieldState, IVisualConfig, IC
3
3
  declare const _default: ((props: {
4
4
  name?: string | undefined;
5
5
  themeKey?: IThemeKey | undefined;
6
+ themeConfig?: any;
6
7
  dark?: IDarkMode | undefined;
7
8
  visualState: DraggableFieldState;
8
9
  visualConfig: IVisualConfig;
@@ -13,6 +13,7 @@ interface SpecRendererProps {
13
13
  onChartResize?: ((width: number, height: number) => void) | undefined;
14
14
  locale?: string;
15
15
  computationFunction: IComputationFunction;
16
+ themeConfig?: any;
16
17
  }
17
18
  /**
18
19
  * Sans-store renderer of GraphicWalker.
@@ -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;
@@ -0,0 +1,6 @@
1
+ import { IThemeKey } from "../interfaces";
2
+ export declare function useTheme(props: {
3
+ themeKey?: IThemeKey;
4
+ themeConfig?: any;
5
+ mediaTheme: 'dark' | 'light';
6
+ }): any;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kanaries/graphic-walker",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "scripts": {
5
5
  "dev:front_end": "vite --host",
6
6
  "dev": "npm run dev:front_end",
package/src/App.tsx CHANGED
@@ -38,6 +38,7 @@ export interface IGWProps {
38
38
  fieldKeyGuard?: boolean;
39
39
  /** @default "vega" */
40
40
  themeKey?: IThemeKey;
41
+ themeConfig?: any;
41
42
  dark?: IDarkMode;
42
43
  storeRef?: React.MutableRefObject<IGlobalStore | null>;
43
44
  computation?: IComputationFunction;
@@ -67,6 +68,7 @@ const App = observer<IGWProps>(function App(props) {
67
68
  hideDataSourceConfig,
68
69
  fieldKeyGuard = true,
69
70
  themeKey = 'vega',
71
+ themeConfig,
70
72
  dark = 'media',
71
73
  computation,
72
74
  toolbar,
@@ -185,7 +187,7 @@ const App = observer<IGWProps>(function App(props) {
185
187
  <VisualSettings rendererHandler={rendererRef} darkModePreference={dark} exclude={toolbar?.exclude} extra={toolbar?.extra} />
186
188
  <CodeExport />
187
189
  <VisualConfig />
188
- <GeoConfigPanel />
190
+ {commonStore.showGeoJSONConfigPanel && <GeoConfigPanel />}
189
191
  <div className="md:grid md:grid-cols-12 xl:grid-cols-6">
190
192
  <div className="md:col-span-3 xl:col-span-1">
191
193
  <DatasetFields />
@@ -209,7 +211,7 @@ const App = observer<IGWProps>(function App(props) {
209
211
  // }}
210
212
  >
211
213
  {datasets.length > 0 && (
212
- <ReactiveRenderer ref={rendererRef} themeKey={themeKey} dark={dark} computationFunction={vizStore.computationFunction} />
214
+ <ReactiveRenderer ref={rendererRef} themeKey={themeKey} themeConfig={themeConfig} dark={dark} computationFunction={vizStore.computationFunction} />
213
215
  )}
214
216
  {/* {vizEmbededMenu.show && (
215
217
  <ClickMenu x={vizEmbededMenu.position[0]} y={vizEmbededMenu.position[1]}>
@@ -5,4 +5,5 @@ export interface ButtonBaseProps {
5
5
  text: string;
6
6
  disabled?: boolean;
7
7
  className?: string;
8
+ icon?: JSX.Element;
8
9
  }
@@ -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 { runInAction } from 'mobx';
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
- JSON.stringify(json, null, 2)
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}
@@ -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
+ GeoJSONDict[key] = json;
22
+ } else {
23
+ GeoJSONDict[key] = feature(json, Object.keys(json.objects)[0]) as unknown as FeatureCollection;
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;
@@ -15,6 +15,7 @@ import { initVisualConfig } from '../utils/save';
15
15
 
16
16
  interface RendererProps {
17
17
  themeKey?: IThemeKey;
18
+ themeConfig?: any;
18
19
  dark?: IDarkMode;
19
20
  computationFunction: IComputationFunction;
20
21
  }
@@ -23,7 +24,7 @@ interface RendererProps {
23
24
  * Depending on global store.
24
25
  */
25
26
  const Renderer = forwardRef<IReactVegaHandler, RendererProps>(function (props, ref) {
26
- const { themeKey, dark, computationFunction } = props;
27
+ const { themeKey, dark, computationFunction, themeConfig } = props;
27
28
  const { vizStore, commonStore } = useGlobalStore();
28
29
  const {
29
30
  allFields,
@@ -131,6 +132,7 @@ const Renderer = forwardRef<IReactVegaHandler, RendererProps>(function (props, r
131
132
  data={viewData}
132
133
  ref={ref}
133
134
  themeKey={themeKey}
135
+ themeConfig={themeConfig}
134
136
  dark={dark}
135
137
  locale={i18n.language}
136
138
  draggableFieldState={encodings}
@@ -23,6 +23,7 @@ type IPureRendererProps =
23
23
  | {
24
24
  name?: string;
25
25
  themeKey?: IThemeKey;
26
+ themeConfig?: any;
26
27
  dark?: IDarkMode;
27
28
  visualState: DraggableFieldState;
28
29
  visualConfig: IVisualConfig;
@@ -86,7 +87,6 @@ const PureRenderer = forwardRef<IReactVegaHandler, IPureRendererProps>(function
86
87
  limit: limit ?? -1,
87
88
  computationFunction: computation,
88
89
  });
89
- console.log(computation)
90
90
  // Dependencies that should not trigger effect individually
91
91
  const latestFromRef = useRef({ data });
92
92
  latestFromRef.current = { data };
@@ -9,6 +9,7 @@ import { DeepReadonly, DraggableFieldState, IDarkMode, IRow, IThemeKey, IVisualC
9
9
  import LoadingLayer from '../components/loadingLayer';
10
10
  import { useCurrentMediaTheme } from '../utils/media';
11
11
  import { builtInThemes } from '../vis/theme';
12
+ import { useTheme } from '../utils/useTheme';
12
13
 
13
14
  interface SpecRendererProps {
14
15
  name?: string;
@@ -22,13 +23,14 @@ interface SpecRendererProps {
22
23
  onChartResize?: ((width: number, height: number) => void) | undefined;
23
24
  locale?: string;
24
25
  computationFunction: IComputationFunction;
26
+ themeConfig?: any;
25
27
  }
26
28
  /**
27
29
  * Sans-store renderer of GraphicWalker.
28
30
  * This is a pure component, which means it will not depend on any global state.
29
31
  */
30
32
  const SpecRenderer = forwardRef<IReactVegaHandler, SpecRendererProps>(function (
31
- { name, themeKey, dark, data, loading, draggableFieldState, visualConfig, onGeomClick, onChartResize, locale, computationFunction },
33
+ { name, themeKey, dark, data, loading, draggableFieldState, visualConfig, onGeomClick, onChartResize, locale, computationFunction, themeConfig: customizedThemeConfig },
32
34
  ref
33
35
  ) {
34
36
  // const { draggableFieldState, visualConfig } = vizStore;
@@ -58,7 +60,11 @@ const SpecRenderer = forwardRef<IReactVegaHandler, SpecRendererProps>(function (
58
60
 
59
61
  const enableResize = size.mode === 'fixed' && !hasFacet && Boolean(onChartResize);
60
62
  const mediaTheme = useCurrentMediaTheme(dark);
61
- const themeConfig = builtInThemes[themeKey ?? 'vega']?.[mediaTheme];
63
+ const themeConfig = useTheme({
64
+ themeKey,
65
+ mediaTheme,
66
+ themeConfig: customizedThemeConfig
67
+ })
62
68
 
63
69
  const vegaConfig = useMemo<VegaGlobalConfig>(() => {
64
70
  const config: VegaGlobalConfig = {
@@ -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 "geojson";
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) => Promise.race([f(...args), new Promise<never>((_, reject) => {
108
- setTimeout(() => reject(new Error('timeout')), timeout)
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 === "showTableSummary":
387
- case configKey === "coordSystem":
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 = data.type === 'GeoJSON' ? data.data : feature(data.data, data.objectKey || Object.keys(data.data.objects)[0]) as unknown as FeatureCollection;
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;
@@ -0,0 +1,8 @@
1
+ import { IThemeKey } from "../interfaces";
2
+ import { builtInThemes } from "../vis/theme";
3
+
4
+ export function useTheme (props: { themeKey?: IThemeKey; themeConfig?: any; mediaTheme: 'dark' | 'light' }) {
5
+ const { themeConfig, themeKey, mediaTheme } = props;
6
+ const config = (themeConfig ?? builtInThemes[themeKey ?? 'vega'])?.[mediaTheme];
7
+ return config
8
+ }