diff --git a/client/app/components/visualizations/visualizationComponents.jsx b/client/app/components/visualizations/visualizationComponents.jsx index cdaf2d51..5eb05bed 100644 --- a/client/app/components/visualizations/visualizationComponents.jsx +++ b/client/app/components/visualizations/visualizationComponents.jsx @@ -6,6 +6,7 @@ import { Renderer as VisRenderer, Editor as VisEditor, updateVisualizationsSetti import { clientConfig } from "@/services/auth"; import countriesDataUrl from "@redash/viz/lib/visualizations/choropleth/maps/countries.geo.json"; +import usaDataUrl from "@redash/viz/lib/visualizations/choropleth/maps/usa-albers.geo.json"; import subdivJapanDataUrl from "@redash/viz/lib/visualizations/choropleth/maps/japan.prefectures.geo.json"; function wrapComponentWithSettings(WrappedComponent) { @@ -17,10 +18,40 @@ function wrapComponentWithSettings(WrappedComponent) { countries: { name: "Countries", url: countriesDataUrl, + fieldNames: { + name: "Short name", + name_long: "Full name", + abbrev: "Abbreviated name", + iso_a2: "ISO code (2 letters)", + iso_a3: "ISO code (3 letters)", + iso_n3: "ISO code (3 digits)", + }, + }, + usa: { + name: "USA", + url: usaDataUrl, + fieldNames: { + name: "Name", + ns_code: "National Standard ANSI Code (8-character)", + geoid: "Geographic ID", + usps_abbrev: "USPS Abbreviation", + fips_code: "FIPS Code (2-character)", + }, }, subdiv_japan: { name: "Japan/Prefectures", url: subdivJapanDataUrl, + fieldNames: { + name: "Name", + name_alt: "Name (alternative)", + name_local: "Name (local)", + iso_3166_2: "ISO-3166-2", + postal: "Postal Code", + type: "Type", + type_en: "Type (EN)", + region: "Region", + region_code: "Region Code", + }, }, }, ...pick(clientConfig, [ diff --git a/client/cypress/integration/visualizations/choropleth_spec.js b/client/cypress/integration/visualizations/choropleth_spec.js index b8d00035..bb4446f5 100644 --- a/client/cypress/integration/visualizations/choropleth_spec.js +++ b/client/cypress/integration/visualizations/choropleth_spec.js @@ -48,11 +48,11 @@ describe("Choropleth", () => { cy.clickThrough(` VisualizationEditor.Tabs.General Choropleth.Editor.MapType - Choropleth.Editor.MapType.Countries + Choropleth.Editor.MapType.countries Choropleth.Editor.KeyColumn Choropleth.Editor.KeyColumn.name - Choropleth.Editor.KeyType - Choropleth.Editor.KeyType.name + Choropleth.Editor.TargetField + Choropleth.Editor.TargetField.name Choropleth.Editor.ValueColumn Choropleth.Editor.ValueColumn.value `); diff --git a/viz-lib/src/lib/referenceCountingCache.js b/viz-lib/src/lib/referenceCountingCache.js new file mode 100644 index 00000000..35ff4721 --- /dev/null +++ b/viz-lib/src/lib/referenceCountingCache.js @@ -0,0 +1,39 @@ +import { each, debounce } from "lodash"; + +export default function createReferenceCountingCache({ cleanupDelay = 2000 } = {}) { + const items = {}; + + const cleanup = debounce(() => { + each(items, (item, key) => { + if (item.refCount <= 0) { + delete items[key]; + } + }); + }, cleanupDelay); + + function get(key, getter) { + if (!items[key]) { + items[key] = { + value: getter(), + refCount: 0, + }; + } + const item = items[key]; + item.refCount += 1; + return item.value; + } + + function release(key) { + if (items[key]) { + const item = items[key]; + if (item.refCount > 0) { + item.refCount -= 1; + if (item.refCount <= 0) { + cleanup(); + } + } + } + } + + return { get, release }; +} diff --git a/viz-lib/src/visualizations/choropleth/Editor/BoundsSettings.jsx b/viz-lib/src/visualizations/choropleth/Editor/BoundsSettings.jsx index 760103dc..e217c836 100644 --- a/viz-lib/src/visualizations/choropleth/Editor/BoundsSettings.jsx +++ b/viz-lib/src/visualizations/choropleth/Editor/BoundsSettings.jsx @@ -1,10 +1,13 @@ -import { isFinite, cloneDeep } from "lodash"; +import { isArray, isFinite, cloneDeep } from "lodash"; import React, { useState, useEffect, useCallback } from "react"; import { useDebouncedCallback } from "use-debounce"; import * as Grid from "antd/lib/grid"; import { Section, InputNumber, ControlLabel } from "@/components/visualizations/editor"; import { EditorPropTypes } from "@/visualizations/prop-types"; +import useLoadGeoJson from "../hooks/useLoadGeoJson"; +import { getGeoJsonBounds } from "./utils"; + export default function BoundsSettings({ options, onOptionsChange }) { // Bounds may be changed in editor or on preview (by drag/zoom map). // Changes from preview does not come frequently (only when user release mouse button), @@ -16,9 +19,20 @@ export default function BoundsSettings({ options, onOptionsChange }) { const [bounds, setBounds] = useState(options.bounds); const [onOptionsChangeDebounced] = useDebouncedCallback(onOptionsChange, 200); + const [geoJson] = useLoadGeoJson(options.mapType); + + // `options.bounds` could be empty only if user didn't edit bounds yet - through preview or in this editor. + // In this case we should keep empty bounds value because it tells renderer to fit map every time. useEffect(() => { - setBounds(options.bounds); - }, [options.bounds]); + if (options.bounds) { + setBounds(options.bounds); + } else { + const defaultBounds = getGeoJsonBounds(geoJson); + if (defaultBounds) { + setBounds(defaultBounds); + } + } + }, [options.bounds, geoJson]); const updateBounds = useCallback( (i, j, v) => { @@ -33,29 +47,47 @@ export default function BoundsSettings({ options, onOptionsChange }) { [bounds, onOptionsChangeDebounced] ); + const boundsAvailable = isArray(bounds); + return (
- + - updateBounds(1, 0, value)} /> + updateBounds(1, 0, value)} + /> - updateBounds(1, 1, value)} /> + updateBounds(1, 1, value)} + />
- + - updateBounds(0, 0, value)} /> + updateBounds(0, 0, value)} + /> - updateBounds(0, 1, value)} /> + updateBounds(0, 1, value)} + /> diff --git a/viz-lib/src/visualizations/choropleth/Editor/ColorsSettings.jsx b/viz-lib/src/visualizations/choropleth/Editor/ColorsSettings.jsx index 62310e8d..4984252d 100644 --- a/viz-lib/src/visualizations/choropleth/Editor/ColorsSettings.jsx +++ b/viz-lib/src/visualizations/choropleth/Editor/ColorsSettings.jsx @@ -12,7 +12,7 @@ export default function ColorsSettings({ options, onOptionsChange }) {
- Value format + Value Format } @@ -86,7 +78,7 @@ export default function GeneralSettings({ options, onOptionsChange }) { onOptionsChangeDebounced({ noValuePlaceholder: event.target.value })} @@ -100,7 +92,7 @@ export default function GeneralSettings({ options, onOptionsChange }) { data-test="Choropleth.Editor.LegendVisibility" checked={options.legend.visible} onChange={event => onOptionsChange({ legend: { visible: event.target.checked } })}> - Show legend + Show Legend
@@ -108,7 +100,7 @@ export default function GeneralSettings({ options, onOptionsChange }) { Tooltip template {templateFormatHint}} + label={Tooltip Template {templateFormatHint}} data-test="Choropleth.Editor.TooltipTemplate" disabled={!options.tooltip.enabled} defaultValue={options.tooltip.template} @@ -163,13 +155,13 @@ export default function GeneralSettings({ options, onOptionsChange }) { data-test="Choropleth.Editor.PopupEnabled" checked={options.popup.enabled} onChange={event => onOptionsChange({ popup: { enabled: event.target.checked } })}> - Show popup + Show Popup