Compare commits
13 Commits
dashboard-
...
fix-explor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
235d4ea516 | ||
|
|
860f8cbe0f | ||
|
|
2fad87569c | ||
|
|
c6f54471dc | ||
|
|
7539138702 | ||
|
|
e0b1b557d7 | ||
|
|
bc5a5c2ac5 | ||
|
|
3a562dbe29 | ||
|
|
73b780a28c | ||
|
|
caeb6a6b7c | ||
|
|
19072074c5 | ||
|
|
f2037fa332 | ||
|
|
6c71800436 |
11694
docs/static/resources/openapi.json
vendored
@@ -48,7 +48,7 @@ dependencies = [
|
||||
"cryptography>=42.0.4, <47.0.0",
|
||||
"deprecation>=2.1.0, <2.2.0",
|
||||
"flask>=2.2.5, <4.0.0",
|
||||
"flask-appbuilder>=5.2.1, <6.0.0",
|
||||
"flask-appbuilder>=5.0.2,<6",
|
||||
"flask-caching>=2.1.0, <3",
|
||||
"flask-compress>=1.13, <2.0",
|
||||
"flask-talisman>=1.0.0, <2.0",
|
||||
|
||||
@@ -120,7 +120,7 @@ flask==2.3.3
|
||||
# flask-session
|
||||
# flask-sqlalchemy
|
||||
# flask-wtf
|
||||
flask-appbuilder==5.2.1
|
||||
flask-appbuilder==5.2.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# apache-superset-core
|
||||
|
||||
@@ -259,7 +259,7 @@ flask==2.3.3
|
||||
# flask-sqlalchemy
|
||||
# flask-testing
|
||||
# flask-wtf
|
||||
flask-appbuilder==5.2.1
|
||||
flask-appbuilder==5.2.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
|
||||
934
superset-frontend/package-lock.json
generated
@@ -128,17 +128,17 @@
|
||||
"@superset-ui/legacy-plugin-chart-chord": "file:./plugins/legacy-plugin-chart-chord",
|
||||
"@superset-ui/legacy-plugin-chart-country-map": "file:./plugins/legacy-plugin-chart-country-map",
|
||||
"@superset-ui/legacy-plugin-chart-horizon": "file:./plugins/legacy-plugin-chart-horizon",
|
||||
"@superset-ui/legacy-plugin-chart-map-box": "file:./plugins/legacy-plugin-chart-map-box",
|
||||
"@superset-ui/legacy-plugin-chart-paired-t-test": "file:./plugins/legacy-plugin-chart-paired-t-test",
|
||||
"@superset-ui/legacy-plugin-chart-parallel-coordinates": "file:./plugins/legacy-plugin-chart-parallel-coordinates",
|
||||
"@superset-ui/legacy-plugin-chart-partition": "file:./plugins/legacy-plugin-chart-partition",
|
||||
"@superset-ui/legacy-plugin-chart-rose": "file:./plugins/legacy-plugin-chart-rose",
|
||||
"@superset-ui/legacy-plugin-chart-world-map": "file:./plugins/legacy-plugin-chart-world-map",
|
||||
"@superset-ui/preset-chart-deckgl": "file:./plugins/preset-chart-deckgl",
|
||||
"@superset-ui/legacy-preset-chart-deckgl": "file:./plugins/legacy-preset-chart-deckgl",
|
||||
"@superset-ui/legacy-preset-chart-nvd3": "file:./plugins/legacy-preset-chart-nvd3",
|
||||
"@superset-ui/plugin-chart-ag-grid-table": "file:./plugins/plugin-chart-ag-grid-table",
|
||||
"@superset-ui/plugin-chart-cartodiagram": "file:./plugins/plugin-chart-cartodiagram",
|
||||
"@superset-ui/plugin-chart-echarts": "file:./plugins/plugin-chart-echarts",
|
||||
"@superset-ui/plugin-chart-point-cluster-map": "file:./plugins/plugin-chart-point-cluster-map",
|
||||
"@superset-ui/plugin-chart-handlebars": "file:./plugins/plugin-chart-handlebars",
|
||||
"@superset-ui/plugin-chart-pivot-table": "file:./plugins/plugin-chart-pivot-table",
|
||||
"@superset-ui/plugin-chart-table": "file:./plugins/plugin-chart-table",
|
||||
|
||||
@@ -41,7 +41,6 @@ export enum VizType {
|
||||
LegacyBubble = 'bubble',
|
||||
Line = 'echarts_timeseries_line',
|
||||
MapBox = 'mapbox',
|
||||
PointClusterMap = 'point_cluster_map',
|
||||
MixedTimeseries = 'mixed_timeseries',
|
||||
PairedTTest = 'paired_ttest',
|
||||
ParallelCoordinates = 'para',
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.20.0](https://github.com/apache/superset/compare/v2021.41.0...v0.20.0) (2024-09-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **deckgl:** deckgl unable to load map ([#17851](https://github.com/apache/superset/issues/17851)) ([52f5dcb](https://github.com/apache/superset/commit/52f5dcb58eec7b188f4387b8781dcda4252a5680))
|
||||
- **explore:** Prevent shared controls from checking feature flags outside React render ([#21315](https://github.com/apache/superset/issues/21315)) ([2285ebe](https://github.com/apache/superset/commit/2285ebe72ec4edded6d195052740b7f9f13d1f1b))
|
||||
|
||||
### Features
|
||||
|
||||
- apply standardized form data to tier 2 charts ([#20530](https://github.com/apache/superset/issues/20530)) ([de524bc](https://github.com/apache/superset/commit/de524bc59f011fd361dcdb7d35c2cb51f7eba442))
|
||||
- **deckgl-map:** use an arbitraty Mabpox style URL ([#26027](https://github.com/apache/superset/issues/26027)) ([#26031](https://github.com/apache/superset/issues/26031)) ([af58784](https://github.com/apache/superset/commit/af587840403d83a7da7fb0f57bc10ad2335d4eeb))
|
||||
|
||||
# [0.19.0](https://github.com/apache/superset/compare/v2021.41.0...v0.19.0) (2024-09-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **deckgl:** deckgl unable to load map ([#17851](https://github.com/apache/superset/issues/17851)) ([52f5dcb](https://github.com/apache/superset/commit/52f5dcb58eec7b188f4387b8781dcda4252a5680))
|
||||
- **explore:** Prevent shared controls from checking feature flags outside React render ([#21315](https://github.com/apache/superset/issues/21315)) ([2285ebe](https://github.com/apache/superset/commit/2285ebe72ec4edded6d195052740b7f9f13d1f1b))
|
||||
|
||||
### Features
|
||||
|
||||
- apply standardized form data to tier 2 charts ([#20530](https://github.com/apache/superset/issues/20530)) ([de524bc](https://github.com/apache/superset/commit/de524bc59f011fd361dcdb7d35c2cb51f7eba442))
|
||||
- **deckgl-map:** use an arbitraty Mabpox style URL ([#26027](https://github.com/apache/superset/issues/26027)) ([#26031](https://github.com/apache/superset/issues/26031)) ([af58784](https://github.com/apache/superset/commit/af587840403d83a7da7fb0f57bc10ad2335d4eeb))
|
||||
|
||||
# [0.18.0](https://github.com/apache-superset/superset-ui/compare/v0.17.87...v0.18.0) (2021-08-30)
|
||||
|
||||
**Note:** Version bump only for package @superset-ui/legacy-plugin-chart-map-box
|
||||
|
||||
## [0.17.61](https://github.com/apache-superset/superset-ui/compare/v0.17.60...v0.17.61) (2021-07-02)
|
||||
|
||||
**Note:** Version bump only for package @superset-ui/legacy-plugin-chart-map-box
|
||||
@@ -0,0 +1,52 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
## @superset-ui/legacy-plugin-chart-map-box
|
||||
|
||||
[](https://www.npmjs.com/package/@superset-ui/legacy-plugin-chart-map-box)
|
||||
[](https://libraries.io/npm/@superset-ui%2Flegacy-plugin-chart-map-box)
|
||||
|
||||
This plugin provides MapBox 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 MapBoxChartPlugin from '@superset-ui/legacy-plugin-chart-map-box';
|
||||
|
||||
new MapBoxChartPlugin().configure({ key: 'map-box' }).register();
|
||||
```
|
||||
|
||||
Then use it via `SuperChart`. See
|
||||
[storybook](https://apache-superset.github.io/superset-ui-plugins/?selectedKind=plugin-chart-map-box)
|
||||
for more details.
|
||||
|
||||
```js
|
||||
<SuperChart
|
||||
chartType="map-box"
|
||||
width={600}
|
||||
height={600}
|
||||
formData={...}
|
||||
queriesData={[{
|
||||
data: {...},
|
||||
}]}
|
||||
/>
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@superset-ui/plugin-chart-point-cluster-map",
|
||||
"version": "1.0.0",
|
||||
"description": "Superset Chart Plugin - Point Cluster Map",
|
||||
"name": "@superset-ui/legacy-plugin-chart-map-box",
|
||||
"version": "0.20.3",
|
||||
"description": "Superset Legacy Chart - MapBox",
|
||||
"keywords": [
|
||||
"superset"
|
||||
],
|
||||
@@ -12,7 +12,7 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/apache/superset.git",
|
||||
"directory": "superset-frontend/plugins/plugin-chart-point-cluster-map"
|
||||
"directory": "superset-frontend/plugins/legacy-plugin-chart-map-box"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"author": "Superset",
|
||||
@@ -27,17 +27,16 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@math.gl/web-mercator": "^4.1.0",
|
||||
"mapbox-gl": "^3.0.0",
|
||||
"maplibre-gl": "^5.0.0",
|
||||
"react-map-gl": "^8.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-map-gl": "^6.1.19",
|
||||
"supercluster": "^8.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^17.0.2 || ^19.0.0",
|
||||
"react-dom": "^17.0.2 || ^19.0.0"
|
||||
"@apache-superset/core": "*",
|
||||
"mapbox-gl": "*",
|
||||
"react": "^17.0.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -16,6 +16,6 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
.maplibre .slice_container div {
|
||||
.mapbox .slice_container div {
|
||||
padding-top: 0px;
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
/* eslint-disable react/jsx-sort-default-props, react/sort-prop-types */
|
||||
/* eslint-disable react/forbid-prop-types, react/require-default-props */
|
||||
import { Component } from 'react';
|
||||
import MapGL from 'react-map-gl';
|
||||
import { WebMercatorViewport } from '@math.gl/web-mercator';
|
||||
import ScatterPlotGlowOverlay from './ScatterPlotGlowOverlay';
|
||||
import './MapBox.css';
|
||||
|
||||
const NOOP = () => {};
|
||||
export const DEFAULT_MAX_ZOOM = 16;
|
||||
export const DEFAULT_POINT_RADIUS = 60;
|
||||
|
||||
interface Viewport {
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
zoom: number;
|
||||
isDragging?: boolean;
|
||||
}
|
||||
|
||||
interface Clusterer {
|
||||
getClusters(bbox: number[], zoom: number): GeoJSONLocation[];
|
||||
}
|
||||
|
||||
interface GeoJSONLocation {
|
||||
geometry: {
|
||||
coordinates: [number, number];
|
||||
};
|
||||
properties: Record<string, number | string | boolean | null | undefined>;
|
||||
}
|
||||
|
||||
interface MapBoxProps {
|
||||
width?: number;
|
||||
height?: number;
|
||||
aggregatorName?: string;
|
||||
clusterer: Clusterer; // Required - used for getClusters()
|
||||
globalOpacity?: number;
|
||||
hasCustomMetric?: boolean;
|
||||
mapStyle?: string;
|
||||
mapboxApiKey: string;
|
||||
onViewportChange?: (viewport: Viewport) => void;
|
||||
pointRadius?: number;
|
||||
pointRadiusUnit?: string;
|
||||
renderWhileDragging?: boolean;
|
||||
rgb?: (string | number)[];
|
||||
bounds?: [[number, number], [number, number]]; // May be undefined for empty datasets
|
||||
viewportLongitude?: number;
|
||||
viewportLatitude?: number;
|
||||
viewportZoom?: number;
|
||||
}
|
||||
|
||||
interface MapBoxState {
|
||||
viewport: Viewport;
|
||||
}
|
||||
|
||||
const defaultProps: Partial<MapBoxProps> = {
|
||||
width: 400,
|
||||
height: 400,
|
||||
globalOpacity: 1,
|
||||
onViewportChange: NOOP,
|
||||
pointRadius: DEFAULT_POINT_RADIUS,
|
||||
pointRadiusUnit: 'Pixels',
|
||||
};
|
||||
|
||||
class MapBox extends Component<MapBoxProps, MapBoxState> {
|
||||
static defaultProps = defaultProps;
|
||||
|
||||
constructor(props: MapBoxProps) {
|
||||
super(props);
|
||||
|
||||
const fitBounds = this.computeFitBoundsViewport();
|
||||
|
||||
this.state = {
|
||||
viewport: this.mergeViewportWithProps(fitBounds),
|
||||
};
|
||||
this.handleViewportChange = this.handleViewportChange.bind(this);
|
||||
}
|
||||
|
||||
handleViewportChange(viewport: Viewport) {
|
||||
this.setState({ viewport });
|
||||
const { onViewportChange } = this.props;
|
||||
onViewportChange!(viewport);
|
||||
}
|
||||
|
||||
mergeViewportWithProps(
|
||||
fitBounds: Viewport,
|
||||
viewport: Viewport = fitBounds,
|
||||
props: MapBoxProps = this.props,
|
||||
useFitBoundsForUnset = true,
|
||||
): Viewport {
|
||||
const { viewportLongitude, viewportLatitude, viewportZoom } = props;
|
||||
|
||||
return {
|
||||
...viewport,
|
||||
longitude:
|
||||
viewportLongitude ??
|
||||
(useFitBoundsForUnset ? fitBounds.longitude : viewport.longitude),
|
||||
latitude:
|
||||
viewportLatitude ??
|
||||
(useFitBoundsForUnset ? fitBounds.latitude : viewport.latitude),
|
||||
zoom:
|
||||
viewportZoom ?? (useFitBoundsForUnset ? fitBounds.zoom : viewport.zoom),
|
||||
};
|
||||
}
|
||||
|
||||
computeFitBoundsViewport(): Viewport {
|
||||
const { width = 400, height = 400, bounds } = this.props;
|
||||
if (bounds && bounds[0] && bounds[1]) {
|
||||
const mercator = new WebMercatorViewport({ width, height }).fitBounds(
|
||||
bounds,
|
||||
);
|
||||
return {
|
||||
latitude: mercator.latitude,
|
||||
longitude: mercator.longitude,
|
||||
zoom: mercator.zoom,
|
||||
};
|
||||
}
|
||||
return { latitude: 0, longitude: 0, zoom: 1 };
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: MapBoxProps) {
|
||||
const { viewport } = this.state;
|
||||
const fitBoundsInputsChanged =
|
||||
prevProps.width !== this.props.width ||
|
||||
prevProps.height !== this.props.height ||
|
||||
prevProps.bounds !== this.props.bounds;
|
||||
const viewportPropsChanged =
|
||||
prevProps.viewportLongitude !== this.props.viewportLongitude ||
|
||||
prevProps.viewportLatitude !== this.props.viewportLatitude ||
|
||||
prevProps.viewportZoom !== this.props.viewportZoom;
|
||||
|
||||
if (!fitBoundsInputsChanged && !viewportPropsChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fitBounds = this.computeFitBoundsViewport();
|
||||
const nextViewport = this.mergeViewportWithProps(
|
||||
fitBounds,
|
||||
viewport,
|
||||
this.props,
|
||||
fitBoundsInputsChanged || viewportPropsChanged,
|
||||
);
|
||||
|
||||
const viewportChanged =
|
||||
nextViewport.longitude !== viewport.longitude ||
|
||||
nextViewport.latitude !== viewport.latitude ||
|
||||
nextViewport.zoom !== viewport.zoom;
|
||||
|
||||
if (viewportChanged) {
|
||||
this.setState({ viewport: nextViewport });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
aggregatorName,
|
||||
clusterer,
|
||||
globalOpacity,
|
||||
mapStyle,
|
||||
mapboxApiKey,
|
||||
pointRadius,
|
||||
pointRadiusUnit,
|
||||
renderWhileDragging,
|
||||
rgb,
|
||||
hasCustomMetric,
|
||||
bounds,
|
||||
} = this.props;
|
||||
const { viewport } = this.state;
|
||||
const isDragging =
|
||||
viewport.isDragging === undefined ? false : viewport.isDragging;
|
||||
|
||||
// Compute the clusters based on the original bounds and current zoom level. Note when zoom/pan
|
||||
// to an area outside of the original bounds, no additional queries are made to the backend to
|
||||
// retrieve additional data.
|
||||
// add this variable to widen the visible area
|
||||
const offsetHorizontal = ((width ?? 400) * 0.5) / 100;
|
||||
const offsetVertical = ((height ?? 400) * 0.5) / 100;
|
||||
|
||||
// Guard against empty datasets where bounds may be undefined
|
||||
const bbox =
|
||||
bounds && bounds[0] && bounds[1]
|
||||
? [
|
||||
bounds[0][0] - offsetHorizontal,
|
||||
bounds[0][1] - offsetVertical,
|
||||
bounds[1][0] + offsetHorizontal,
|
||||
bounds[1][1] + offsetVertical,
|
||||
]
|
||||
: [-180, -90, 180, 90]; // Default to world bounds
|
||||
|
||||
const clusters = clusterer.getClusters(bbox, Math.round(viewport.zoom));
|
||||
|
||||
return (
|
||||
<MapGL
|
||||
{...viewport}
|
||||
mapStyle={mapStyle}
|
||||
width={width}
|
||||
height={height}
|
||||
mapboxApiAccessToken={mapboxApiKey}
|
||||
onViewportChange={this.handleViewportChange}
|
||||
preserveDrawingBuffer
|
||||
>
|
||||
<ScatterPlotGlowOverlay
|
||||
{...viewport}
|
||||
isDragging={isDragging}
|
||||
locations={clusters}
|
||||
dotRadius={pointRadius}
|
||||
pointRadiusUnit={pointRadiusUnit}
|
||||
rgb={rgb}
|
||||
globalOpacity={globalOpacity}
|
||||
compositeOperation="screen"
|
||||
renderWhileDragging={renderWhileDragging}
|
||||
aggregation={hasCustomMetric ? aggregatorName : undefined}
|
||||
lngLatAccessor={(location: GeoJSONLocation) => {
|
||||
const { coordinates } = location.geometry;
|
||||
|
||||
return [coordinates[0], coordinates[1]];
|
||||
}}
|
||||
/>
|
||||
</MapGL>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MapBox;
|
||||
@@ -0,0 +1,425 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
/* eslint-disable react/require-default-props */
|
||||
import { PureComponent } from 'react';
|
||||
import { CanvasOverlay } from 'react-map-gl';
|
||||
import { kmToPixels, MILES_PER_KM } from './utils/geo';
|
||||
import roundDecimal from './utils/roundDecimal';
|
||||
import luminanceFromRGB from './utils/luminanceFromRGB';
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
|
||||
// Shared radius bounds keep cluster and point sizing in sync.
|
||||
export const MIN_CLUSTER_RADIUS_RATIO = 1 / 6;
|
||||
export const MAX_POINT_RADIUS_RATIO = 1 / 3;
|
||||
|
||||
interface GeoJSONLocation {
|
||||
geometry: {
|
||||
coordinates: [number, number];
|
||||
};
|
||||
properties: Record<string, number | string | boolean | null | undefined>;
|
||||
}
|
||||
|
||||
interface RedrawParams {
|
||||
width: number;
|
||||
height: number;
|
||||
ctx: CanvasRenderingContext2D;
|
||||
isDragging: boolean;
|
||||
project: (lngLat: [number, number]) => [number, number];
|
||||
}
|
||||
|
||||
interface DrawTextOptions {
|
||||
fontHeight?: number;
|
||||
label?: string | number;
|
||||
radius?: number;
|
||||
rgb?: (string | number)[];
|
||||
shadow?: boolean;
|
||||
}
|
||||
|
||||
interface ScatterPlotGlowOverlayProps {
|
||||
aggregation?: string;
|
||||
compositeOperation?: string;
|
||||
dotRadius?: number;
|
||||
globalOpacity?: number;
|
||||
lngLatAccessor?: (location: GeoJSONLocation) => [number, number];
|
||||
locations: GeoJSONLocation[];
|
||||
pointRadiusUnit?: string;
|
||||
renderWhileDragging?: boolean;
|
||||
rgb?: (string | number)[];
|
||||
zoom?: number;
|
||||
isDragging?: boolean;
|
||||
}
|
||||
|
||||
const defaultProps: Partial<ScatterPlotGlowOverlayProps> = {
|
||||
// Same as browser default.
|
||||
compositeOperation: 'source-over',
|
||||
dotRadius: 4,
|
||||
lngLatAccessor: (location: GeoJSONLocation) => [
|
||||
location.geometry.coordinates[0],
|
||||
location.geometry.coordinates[1],
|
||||
],
|
||||
renderWhileDragging: true,
|
||||
};
|
||||
|
||||
const computeClusterLabel = (
|
||||
properties: Record<string, number | string | boolean | null | undefined>,
|
||||
aggregation: string | undefined,
|
||||
): number | string => {
|
||||
const count = properties.point_count as number;
|
||||
if (!aggregation) {
|
||||
return count;
|
||||
}
|
||||
if (aggregation === 'sum' || aggregation === 'min' || aggregation === 'max') {
|
||||
return properties[aggregation] as number;
|
||||
}
|
||||
const { sum } = properties as { sum: number };
|
||||
const mean = sum / count;
|
||||
if (aggregation === 'mean') {
|
||||
return Math.round(100 * mean) / 100;
|
||||
}
|
||||
const { squaredSum } = properties as { squaredSum: number };
|
||||
const variance = squaredSum / count - (sum / count) ** 2;
|
||||
if (aggregation === 'var') {
|
||||
return Math.round(100 * variance) / 100;
|
||||
}
|
||||
if (aggregation === 'stdev') {
|
||||
return Math.round(100 * Math.sqrt(variance)) / 100;
|
||||
}
|
||||
|
||||
// fallback to point_count, this really shouldn't happen
|
||||
return count;
|
||||
};
|
||||
|
||||
class ScatterPlotGlowOverlay extends PureComponent<ScatterPlotGlowOverlayProps> {
|
||||
static defaultProps = defaultProps;
|
||||
|
||||
constructor(props: ScatterPlotGlowOverlayProps) {
|
||||
super(props);
|
||||
this.redraw = this.redraw.bind(this);
|
||||
}
|
||||
|
||||
drawText(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
pixel: [number, number],
|
||||
options: DrawTextOptions = {},
|
||||
) {
|
||||
const IS_DARK_THRESHOLD = 110;
|
||||
const {
|
||||
fontHeight = 0,
|
||||
label = '',
|
||||
radius = 0,
|
||||
rgb = [0, 0, 0],
|
||||
shadow = false,
|
||||
} = options;
|
||||
const maxWidth = radius * 1.8;
|
||||
const luminance = luminanceFromRGB(
|
||||
rgb[1] as number,
|
||||
rgb[2] as number,
|
||||
rgb[3] as number,
|
||||
);
|
||||
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
ctx.fillStyle = luminance <= IS_DARK_THRESHOLD ? 'white' : 'black';
|
||||
ctx.font = `${fontHeight}px sans-serif`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
if (shadow) {
|
||||
ctx.shadowBlur = 15;
|
||||
ctx.shadowColor = luminance <= IS_DARK_THRESHOLD ? 'black' : '';
|
||||
}
|
||||
|
||||
const textWidth = ctx.measureText(String(label)).width;
|
||||
if (textWidth > maxWidth) {
|
||||
const scale = fontHeight / textWidth;
|
||||
ctx.font = `${scale * maxWidth}px sans-serif`;
|
||||
}
|
||||
|
||||
const { compositeOperation } = this.props;
|
||||
|
||||
ctx.fillText(String(label), pixel[0], pixel[1]);
|
||||
ctx.globalCompositeOperation = (compositeOperation ??
|
||||
'source-over') as GlobalCompositeOperation;
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.shadowColor = '';
|
||||
}
|
||||
|
||||
// Modified: https://github.com/uber/react-map-gl/blob/master/overlays/scatterplot.react.js
|
||||
redraw({ width, height, ctx, isDragging, project }: RedrawParams) {
|
||||
const {
|
||||
aggregation,
|
||||
compositeOperation,
|
||||
dotRadius,
|
||||
globalOpacity,
|
||||
lngLatAccessor,
|
||||
locations,
|
||||
pointRadiusUnit,
|
||||
renderWhileDragging,
|
||||
rgb,
|
||||
zoom,
|
||||
} = this.props;
|
||||
|
||||
const radius = dotRadius ?? 4;
|
||||
const clusterLabelMap: (number | string)[] = [];
|
||||
|
||||
locations.forEach((location, i) => {
|
||||
if (location.properties.cluster) {
|
||||
clusterLabelMap[i] = computeClusterLabel(
|
||||
location.properties,
|
||||
aggregation,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const finiteClusterLabels = clusterLabelMap
|
||||
.map(value => Number(value))
|
||||
.filter(value => Number.isFinite(value));
|
||||
const safeMaxAbsLabel =
|
||||
finiteClusterLabels.length > 0
|
||||
? Math.max(
|
||||
Math.max(...finiteClusterLabels.map(value => Math.abs(value))),
|
||||
1,
|
||||
)
|
||||
: 1;
|
||||
|
||||
// Calculate min/max radius values for Pixels mode scaling
|
||||
let minRadiusValue = Infinity;
|
||||
let maxRadiusValue = -Infinity;
|
||||
if (pointRadiusUnit === 'Pixels') {
|
||||
locations.forEach(location => {
|
||||
// Accept both null and undefined as "no value" and coerce potential numeric strings
|
||||
if (
|
||||
!location.properties.cluster &&
|
||||
location.properties.radius != null
|
||||
) {
|
||||
const radiusValueRaw = location.properties.radius;
|
||||
const radiusValue =
|
||||
typeof radiusValueRaw === 'string' && radiusValueRaw.trim() === ''
|
||||
? null
|
||||
: Number(radiusValueRaw);
|
||||
if (radiusValue != null && Number.isFinite(radiusValue)) {
|
||||
minRadiusValue = Math.min(minRadiusValue, radiusValue);
|
||||
maxRadiusValue = Math.max(maxRadiusValue, radiusValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.globalCompositeOperation = (compositeOperation ??
|
||||
'source-over') as GlobalCompositeOperation;
|
||||
|
||||
if ((renderWhileDragging || !isDragging) && locations) {
|
||||
locations.forEach(function _forEach(
|
||||
this: ScatterPlotGlowOverlay,
|
||||
location: GeoJSONLocation,
|
||||
i: number,
|
||||
) {
|
||||
const pixel = project(lngLatAccessor!(location)) as [number, number];
|
||||
const pixelRounded: [number, number] = [
|
||||
roundDecimal(pixel[0], 1),
|
||||
roundDecimal(pixel[1], 1),
|
||||
];
|
||||
|
||||
if (
|
||||
pixelRounded[0] + radius >= 0 &&
|
||||
pixelRounded[0] - radius < width &&
|
||||
pixelRounded[1] + radius >= 0 &&
|
||||
pixelRounded[1] - radius < height
|
||||
) {
|
||||
ctx.beginPath();
|
||||
if (location.properties.cluster) {
|
||||
const clusterLabel = clusterLabelMap[i];
|
||||
// Validate clusterLabel is a finite number before using it for radius calculation
|
||||
const numericLabel = Number(clusterLabel);
|
||||
const safeNumericLabel = Number.isFinite(numericLabel)
|
||||
? numericLabel
|
||||
: 0;
|
||||
const minClusterRadius =
|
||||
pointRadiusUnit === 'Pixels'
|
||||
? radius * MAX_POINT_RADIUS_RATIO
|
||||
: radius * MIN_CLUSTER_RADIUS_RATIO;
|
||||
const ratio = Math.abs(safeNumericLabel) / safeMaxAbsLabel;
|
||||
const scaledRadius = roundDecimal(
|
||||
minClusterRadius + ratio ** 0.5 * (radius - minClusterRadius),
|
||||
1,
|
||||
);
|
||||
const fontHeight = roundDecimal(scaledRadius * 0.5, 1);
|
||||
const [x, y] = pixelRounded;
|
||||
const gradient = ctx.createRadialGradient(
|
||||
x,
|
||||
y,
|
||||
scaledRadius,
|
||||
x,
|
||||
y,
|
||||
0,
|
||||
);
|
||||
|
||||
gradient.addColorStop(
|
||||
1,
|
||||
`rgba(${rgb![1]}, ${rgb![2]}, ${rgb![3]}, ${0.8 * (globalOpacity ?? 1)})`,
|
||||
);
|
||||
gradient.addColorStop(
|
||||
0,
|
||||
`rgba(${rgb![1]}, ${rgb![2]}, ${rgb![3]}, 0)`,
|
||||
);
|
||||
ctx.arc(
|
||||
pixelRounded[0],
|
||||
pixelRounded[1],
|
||||
scaledRadius,
|
||||
0,
|
||||
Math.PI * 2,
|
||||
);
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fill();
|
||||
|
||||
if (Number.isFinite(safeNumericLabel)) {
|
||||
let label: string | number = clusterLabel;
|
||||
const absLabel = Math.abs(safeNumericLabel);
|
||||
const sign = safeNumericLabel < 0 ? '-' : '';
|
||||
if (absLabel >= 10000) {
|
||||
label = `${sign}${Math.round(absLabel / 1000)}k`;
|
||||
} else if (absLabel >= 1000) {
|
||||
label = `${sign}${Math.round(absLabel / 100) / 10}k`;
|
||||
}
|
||||
this.drawText(ctx, pixelRounded, {
|
||||
fontHeight,
|
||||
label,
|
||||
radius: scaledRadius,
|
||||
rgb,
|
||||
shadow: true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const defaultRadius = radius * MIN_CLUSTER_RADIUS_RATIO;
|
||||
const rawRadius = location.properties.radius;
|
||||
const numericRadiusProperty =
|
||||
rawRadius != null &&
|
||||
!(typeof rawRadius === 'string' && rawRadius.trim() === '')
|
||||
? Number(rawRadius)
|
||||
: null;
|
||||
const radiusProperty =
|
||||
numericRadiusProperty != null &&
|
||||
Number.isFinite(numericRadiusProperty)
|
||||
? numericRadiusProperty
|
||||
: null;
|
||||
const pointMetric = location.properties.metric ?? null;
|
||||
let pointRadius: number = radiusProperty ?? defaultRadius;
|
||||
let pointLabel: string | number | undefined;
|
||||
|
||||
if (radiusProperty != null) {
|
||||
const pointLatitude = lngLatAccessor!(location)[1];
|
||||
if (pointRadiusUnit === 'Kilometers') {
|
||||
pointLabel = `${roundDecimal(pointRadius, 2)}km`;
|
||||
pointRadius = kmToPixels(pointRadius, pointLatitude, zoom ?? 0);
|
||||
} else if (pointRadiusUnit === 'Miles') {
|
||||
pointLabel = `${roundDecimal(pointRadius, 2)}mi`;
|
||||
pointRadius = kmToPixels(
|
||||
pointRadius * MILES_PER_KM,
|
||||
pointLatitude,
|
||||
zoom ?? 0,
|
||||
);
|
||||
} else if (pointRadiusUnit === 'Pixels') {
|
||||
// Scale pixel values to a reasonable range (radius/6 to radius/3)
|
||||
// This ensures points are visible and proportional to their values
|
||||
const MIN_POINT_RADIUS = radius * MIN_CLUSTER_RADIUS_RATIO;
|
||||
const MAX_POINT_RADIUS = radius * MAX_POINT_RADIUS_RATIO;
|
||||
|
||||
if (
|
||||
Number.isFinite(minRadiusValue) &&
|
||||
Number.isFinite(maxRadiusValue) &&
|
||||
maxRadiusValue > minRadiusValue
|
||||
) {
|
||||
// Normalize the value to 0-1 range, then scale to pixel range
|
||||
const numericPointRadius = Number(pointRadius);
|
||||
if (!Number.isFinite(numericPointRadius)) {
|
||||
// fallback to minimum visible size when the value is not a finite number
|
||||
pointRadius = MIN_POINT_RADIUS;
|
||||
} else {
|
||||
const normalizedValueRaw =
|
||||
(numericPointRadius - minRadiusValue) /
|
||||
(maxRadiusValue - minRadiusValue);
|
||||
const normalizedValue = Math.max(
|
||||
0,
|
||||
Math.min(1, normalizedValueRaw),
|
||||
);
|
||||
pointRadius =
|
||||
MIN_POINT_RADIUS +
|
||||
normalizedValue * (MAX_POINT_RADIUS - MIN_POINT_RADIUS);
|
||||
}
|
||||
pointLabel = `${roundDecimal(radiusProperty, 2)}`;
|
||||
} else if (
|
||||
Number.isFinite(minRadiusValue) &&
|
||||
minRadiusValue === maxRadiusValue
|
||||
) {
|
||||
// All values are the same, use a fixed medium size
|
||||
pointRadius = (MIN_POINT_RADIUS + MAX_POINT_RADIUS) / 2;
|
||||
pointLabel = `${roundDecimal(radiusProperty, 2)}`;
|
||||
} else {
|
||||
// Use raw pixel values if they're already in a reasonable range
|
||||
pointRadius = Math.max(
|
||||
MIN_POINT_RADIUS,
|
||||
Math.min(pointRadius, MAX_POINT_RADIUS),
|
||||
);
|
||||
pointLabel = `${roundDecimal(radiusProperty, 2)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pointMetric !== null) {
|
||||
const numericMetric = parseFloat(String(pointMetric));
|
||||
pointLabel = Number.isFinite(numericMetric)
|
||||
? roundDecimal(numericMetric, 2)
|
||||
: String(pointMetric);
|
||||
}
|
||||
|
||||
// Fall back to default points if pointRadius wasn't a numerical column
|
||||
if (!pointRadius) {
|
||||
pointRadius = defaultRadius;
|
||||
}
|
||||
|
||||
ctx.arc(
|
||||
pixelRounded[0],
|
||||
pixelRounded[1],
|
||||
roundDecimal(pointRadius, 1),
|
||||
0,
|
||||
Math.PI * 2,
|
||||
);
|
||||
ctx.fillStyle = `rgba(${rgb![1]}, ${rgb![2]}, ${rgb![3]}, ${globalOpacity})`;
|
||||
ctx.fill();
|
||||
|
||||
if (pointLabel !== undefined) {
|
||||
this.drawText(ctx, pixelRounded, {
|
||||
fontHeight: roundDecimal(pointRadius, 1),
|
||||
label: pointLabel,
|
||||
radius: pointRadius,
|
||||
rgb,
|
||||
shadow: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <CanvasOverlay redraw={this.redraw} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default ScatterPlotGlowOverlay;
|
||||
@@ -17,6 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { validateMapboxStylesUrl } from '@superset-ui/core';
|
||||
import {
|
||||
columnChoices,
|
||||
ControlPanelConfig,
|
||||
@@ -28,12 +29,12 @@ import {
|
||||
const columnsConfig = sharedControls.entity;
|
||||
|
||||
const colorChoices = [
|
||||
['#008b8b', t('Dark Cyan')],
|
||||
['#800080', t('Purple')],
|
||||
['#ffd700', t('Gold')],
|
||||
['#454545', t('Dim Gray')],
|
||||
['#dc143c', t('Crimson')],
|
||||
['#228b22', t('Forest Green')],
|
||||
['rgb(0, 139, 139)', t('Dark Cyan')],
|
||||
['rgb(128, 0, 128)', t('Purple')],
|
||||
['rgb(255, 215, 0)', t('Gold')],
|
||||
['rgb(69, 69, 69)', t('Dim Gray')],
|
||||
['rgb(220, 20, 60)', t('Crimson')],
|
||||
['rgb(34, 139, 34)', t('Forest Green')],
|
||||
];
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
@@ -109,7 +110,7 @@ const config: ControlPanelConfig = {
|
||||
'Either a numerical column or `Auto`, which scales the point based ' +
|
||||
'on the largest cluster',
|
||||
),
|
||||
mapStateToProps: (state: any) => {
|
||||
mapStateToProps: state => {
|
||||
const datasourceChoices = columnChoices(state.datasource);
|
||||
const choices: [string, string][] = [['Auto', t('Auto')]];
|
||||
return {
|
||||
@@ -144,7 +145,7 @@ const config: ControlPanelConfig = {
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'map_label',
|
||||
name: 'mapbox_label',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
multi: true,
|
||||
@@ -156,7 +157,7 @@ const config: ControlPanelConfig = {
|
||||
'Non-numerical columns will be used to label points. ' +
|
||||
'Leave empty to get a count of points in each cluster.',
|
||||
),
|
||||
mapStateToProps: (state: any) => ({
|
||||
mapStateToProps: state => ({
|
||||
choices: columnChoices(state.datasource),
|
||||
}),
|
||||
},
|
||||
@@ -188,66 +189,21 @@ const config: ControlPanelConfig = {
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Map'),
|
||||
tabOverride: 'customize',
|
||||
expanded: true,
|
||||
label: t('Visual Tweaks'),
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'map_renderer',
|
||||
name: 'render_while_dragging',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Map Renderer'),
|
||||
clearable: false,
|
||||
renderTrigger: true,
|
||||
choices: [
|
||||
['maplibre', t('MapLibre (open-source)')],
|
||||
['mapbox', t('Mapbox (API key required)')],
|
||||
],
|
||||
default: 'maplibre',
|
||||
type: 'CheckboxControl',
|
||||
label: t('Live render'),
|
||||
default: true,
|
||||
description: t(
|
||||
'MapLibre is open-source and requires no API key. Mapbox requires MAPBOX_API_KEY to be configured on the server.',
|
||||
'Points and clusters will update as the viewport is being changed',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'maplibre_style',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Map Style'),
|
||||
clearable: false,
|
||||
renderTrigger: true,
|
||||
freeForm: true,
|
||||
choices: [
|
||||
[
|
||||
'https://tiles.openfreemap.org/styles/liberty',
|
||||
t('Liberty (OpenFreeMap)'),
|
||||
],
|
||||
[
|
||||
'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
|
||||
t('Light (Carto)'),
|
||||
],
|
||||
[
|
||||
'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
|
||||
t('Dark (Carto)'),
|
||||
],
|
||||
[
|
||||
'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json',
|
||||
t('Streets (Carto)'),
|
||||
],
|
||||
],
|
||||
default: 'https://tiles.openfreemap.org/styles/liberty',
|
||||
description: t(
|
||||
'Base layer map style. See MapLibre documentation: %s',
|
||||
'https://maplibre.org/maplibre-style-spec/',
|
||||
),
|
||||
visibility: ({ controls }: any) =>
|
||||
controls?.map_renderer?.value !== 'mapbox',
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'mapbox_style',
|
||||
@@ -257,42 +213,22 @@ const config: ControlPanelConfig = {
|
||||
clearable: false,
|
||||
renderTrigger: true,
|
||||
freeForm: true,
|
||||
validators: [validateMapboxStylesUrl],
|
||||
choices: [
|
||||
['mapbox://styles/mapbox/streets-v12', t('Streets')],
|
||||
['mapbox://styles/mapbox/outdoors-v12', t('Outdoors')],
|
||||
['mapbox://styles/mapbox/light-v11', t('Light')],
|
||||
['mapbox://styles/mapbox/dark-v11', t('Dark')],
|
||||
['mapbox://styles/mapbox/satellite-v9', t('Satellite')],
|
||||
['mapbox://styles/mapbox/streets-v9', t('Streets')],
|
||||
['mapbox://styles/mapbox/dark-v9', t('Dark')],
|
||||
['mapbox://styles/mapbox/light-v9', t('Light')],
|
||||
[
|
||||
'mapbox://styles/mapbox/satellite-streets-v12',
|
||||
'mapbox://styles/mapbox/satellite-streets-v9',
|
||||
t('Satellite Streets'),
|
||||
],
|
||||
['mapbox://styles/mapbox/satellite-v9', t('Satellite')],
|
||||
['mapbox://styles/mapbox/outdoors-v9', t('Outdoors')],
|
||||
],
|
||||
default: 'mapbox://styles/mapbox/light-v11',
|
||||
default: 'mapbox://styles/mapbox/light-v9',
|
||||
description: t(
|
||||
'Base layer map style. Accepts a Mapbox style URL (mapbox://styles/...).',
|
||||
),
|
||||
visibility: ({ controls }: any) =>
|
||||
controls?.map_renderer?.value === 'mapbox',
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Visual Tweaks'),
|
||||
tabOverride: 'customize',
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'render_while_dragging',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Live render'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: t(
|
||||
'Points and clusters will update as the viewport is being changed',
|
||||
'Base layer map style. See Mapbox documentation: %s',
|
||||
'https://docs.mapbox.com/help/glossary/style-url/',
|
||||
),
|
||||
},
|
||||
},
|
||||
@@ -303,9 +239,9 @@ const config: ControlPanelConfig = {
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: t('Opacity'),
|
||||
renderTrigger: true,
|
||||
default: 1,
|
||||
isFloat: true,
|
||||
renderTrigger: true,
|
||||
description: t(
|
||||
'Opacity of all clusters, points, and labels. Between 0 and 1.',
|
||||
),
|
||||
@@ -314,11 +250,10 @@ const config: ControlPanelConfig = {
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'map_color',
|
||||
name: 'mapbox_color',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
renderTrigger: true,
|
||||
label: t('RGB Color'),
|
||||
default: colorChoices[0][0],
|
||||
choices: colorChoices,
|
||||
@@ -343,6 +278,7 @@ const config: ControlPanelConfig = {
|
||||
isFloat: true,
|
||||
description: t('Longitude of default viewport'),
|
||||
places: 8,
|
||||
// Viewport longitude changes shouldn't prompt user to re-run query
|
||||
dontRefreshOnChange: true,
|
||||
},
|
||||
},
|
||||
@@ -356,6 +292,7 @@ const config: ControlPanelConfig = {
|
||||
isFloat: true,
|
||||
description: t('Latitude of default viewport'),
|
||||
places: 8,
|
||||
// Viewport latitude changes shouldn't prompt user to re-run query
|
||||
dontRefreshOnChange: true,
|
||||
},
|
||||
},
|
||||
@@ -371,6 +308,7 @@ const config: ControlPanelConfig = {
|
||||
default: '',
|
||||
description: t('Zoom level of the map'),
|
||||
places: 8,
|
||||
// Viewport zoom shouldn't prompt user to re-run query
|
||||
dontRefreshOnChange: true,
|
||||
},
|
||||
},
|
||||
@@ -387,7 +325,7 @@ const config: ControlPanelConfig = {
|
||||
),
|
||||
},
|
||||
},
|
||||
formDataOverrides: (formData: any) => ({
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
groupby: getStandardizedControls().popAllColumns(),
|
||||
}),
|
||||
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
@@ -28,30 +28,31 @@ import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('Map'),
|
||||
credits: ['https://maplibre.org/'],
|
||||
credits: ['https://www.mapbox.com/mapbox-gl-js/api/'],
|
||||
description: '',
|
||||
exampleGallery: [
|
||||
{ url: example1, urlDark: example1Dark, caption: t('Light mode') },
|
||||
{ url: example2, urlDark: example2Dark, caption: t('Dark mode') },
|
||||
],
|
||||
name: t('Point Cluster Map'),
|
||||
name: t('MapBox'),
|
||||
tags: [
|
||||
t('Business'),
|
||||
t('Intensity'),
|
||||
t('Legacy'),
|
||||
t('Density'),
|
||||
t('Scatter'),
|
||||
t('Transformable'),
|
||||
],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
export default class ScatterMapChartPlugin extends ChartPlugin {
|
||||
export default class MapBoxChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./MapLibre'),
|
||||
loadChart: () => import('./MapBox'),
|
||||
loadTransformProps: () => import('./transformProps'),
|
||||
loadBuildQuery: () => import('./buildQuery'),
|
||||
metadata,
|
||||
controlPanel,
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-magic-numbers */
|
||||
import { SuperChart } from '@superset-ui/core';
|
||||
import { useTheme } from '@apache-superset/core/theme';
|
||||
import MapBoxChartPlugin from '@superset-ui/legacy-plugin-chart-map-box';
|
||||
import { withResizableChartDemo } from '@storybook-shared';
|
||||
import { generateData } from './data';
|
||||
|
||||
new MapBoxChartPlugin().configure({ key: 'map-box' }).register();
|
||||
|
||||
export default {
|
||||
title: 'Legacy Chart Plugins/legacy-plugin-chart-map-box',
|
||||
decorators: [withResizableChartDemo],
|
||||
args: {
|
||||
clusteringRadius: 60,
|
||||
globalOpacity: 1,
|
||||
pointRadius: 'Auto',
|
||||
renderWhileDragging: true,
|
||||
},
|
||||
argTypes: {
|
||||
clusteringRadius: {
|
||||
control: { type: 'range', min: 0, max: 200, step: 10 },
|
||||
description: 'Radius in pixels for clustering points',
|
||||
},
|
||||
globalOpacity: {
|
||||
control: { type: 'range', min: 0, max: 1, step: 0.1 },
|
||||
description: 'Opacity of map markers',
|
||||
},
|
||||
pointRadius: {
|
||||
control: 'select',
|
||||
options: ['Auto', 1, 2, 5, 10, 20, 50],
|
||||
description: 'Size of point markers',
|
||||
},
|
||||
renderWhileDragging: {
|
||||
control: 'boolean',
|
||||
description: 'Render markers while dragging the map',
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'Note: This chart requires a Mapbox API key to display. Without a valid key, the map background will not render.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const MapBoxViz = ({
|
||||
clusteringRadius,
|
||||
globalOpacity,
|
||||
pointRadius,
|
||||
renderWhileDragging,
|
||||
width,
|
||||
height,
|
||||
}: {
|
||||
clusteringRadius: number;
|
||||
globalOpacity: number;
|
||||
pointRadius: string | number;
|
||||
renderWhileDragging: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<SuperChart
|
||||
chartType="map-box"
|
||||
width={width}
|
||||
height={height}
|
||||
queriesData={[{ data: generateData(theme) }]}
|
||||
formData={{
|
||||
all_columns_x: 'LON',
|
||||
all_columns_y: 'LAT',
|
||||
clustering_radius: String(clusteringRadius),
|
||||
global_opacity: globalOpacity,
|
||||
mapbox_color: 'rgb(244, 176, 42)',
|
||||
mapbox_label: [],
|
||||
mapbox_style: 'mapbox://styles/mapbox/light-v9',
|
||||
pandas_aggfunc: 'sum',
|
||||
point_radius: pointRadius,
|
||||
point_radius_unit: 'Pixels',
|
||||
render_while_dragging: renderWhileDragging,
|
||||
viewport_latitude: 37.78711146014447,
|
||||
viewport_longitude: -122.37633433151713,
|
||||
viewport_zoom: 10.026425338292782,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* 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 Supercluster, {
|
||||
type Options as SuperclusterOptions,
|
||||
} from 'supercluster';
|
||||
import { ChartProps } from '@superset-ui/core';
|
||||
import { DEFAULT_POINT_RADIUS, DEFAULT_MAX_ZOOM } from './MapBox';
|
||||
|
||||
const NOOP = () => {};
|
||||
const MIN_LONGITUDE = -180;
|
||||
const MAX_LONGITUDE = 180;
|
||||
const MIN_LATITUDE = -90;
|
||||
const MAX_LATITUDE = 90;
|
||||
const MIN_ZOOM = 0;
|
||||
|
||||
function toFiniteNumber(
|
||||
value: string | number | null | undefined,
|
||||
): number | undefined {
|
||||
if (value === null || value === undefined) return undefined;
|
||||
const normalizedValue = typeof value === 'string' ? value.trim() : value;
|
||||
if (normalizedValue === '') return undefined;
|
||||
const num = Number(normalizedValue);
|
||||
return Number.isFinite(num) ? num : undefined;
|
||||
}
|
||||
|
||||
function clampNumber(
|
||||
value: number | undefined,
|
||||
min: number,
|
||||
max: number,
|
||||
): number | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
interface ClusterProperties {
|
||||
metric: number;
|
||||
sum: number;
|
||||
squaredSum: number;
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { width, height, formData, hooks, queriesData } = chartProps;
|
||||
const { onError = NOOP, setControlValue = NOOP } = hooks;
|
||||
const { bounds, geoJSON, hasCustomMetric, mapboxApiKey } =
|
||||
queriesData[0].data;
|
||||
const {
|
||||
clusteringRadius,
|
||||
globalOpacity,
|
||||
mapboxColor,
|
||||
mapboxStyle,
|
||||
pandasAggfunc,
|
||||
pointRadiusUnit,
|
||||
renderWhileDragging,
|
||||
viewportLongitude,
|
||||
viewportLatitude,
|
||||
viewportZoom,
|
||||
} = formData;
|
||||
|
||||
// Validate mapbox color
|
||||
const rgb = /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/.exec(mapboxColor);
|
||||
if (rgb === null) {
|
||||
onError("Color field must be of form 'rgb(%d, %d, %d)'");
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
const opts: SuperclusterOptions<ClusterProperties, ClusterProperties> = {
|
||||
maxZoom: DEFAULT_MAX_ZOOM,
|
||||
radius: clusteringRadius,
|
||||
};
|
||||
if (hasCustomMetric) {
|
||||
opts.initial = () => ({
|
||||
metric: 0,
|
||||
sum: 0,
|
||||
squaredSum: 0,
|
||||
min: Infinity,
|
||||
max: -Infinity,
|
||||
});
|
||||
opts.map = (prop: ClusterProperties) => ({
|
||||
metric: prop.metric,
|
||||
sum: prop.metric,
|
||||
squaredSum: prop.metric ** 2,
|
||||
min: prop.metric,
|
||||
max: prop.metric,
|
||||
});
|
||||
opts.reduce = (accu: ClusterProperties, prop: ClusterProperties) => {
|
||||
// Temporarily disable param-reassignment linting to work with supercluster's api
|
||||
/* eslint-disable no-param-reassign */
|
||||
accu.sum += prop.sum;
|
||||
accu.squaredSum += prop.squaredSum;
|
||||
accu.min = Math.min(accu.min, prop.min);
|
||||
accu.max = Math.max(accu.max, prop.max);
|
||||
/* eslint-enable no-param-reassign */
|
||||
};
|
||||
}
|
||||
const clusterer = new Supercluster(opts);
|
||||
clusterer.load(geoJSON.features);
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
aggregatorName: pandasAggfunc,
|
||||
bounds,
|
||||
clusterer,
|
||||
hasCustomMetric,
|
||||
mapboxApiKey,
|
||||
mapStyle: mapboxStyle,
|
||||
onViewportChange({
|
||||
latitude,
|
||||
longitude,
|
||||
zoom,
|
||||
}: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
zoom: number;
|
||||
}) {
|
||||
setControlValue('viewport_longitude', longitude);
|
||||
setControlValue('viewport_latitude', latitude);
|
||||
setControlValue('viewport_zoom', zoom);
|
||||
},
|
||||
// Always use DEFAULT_POINT_RADIUS as the base radius for cluster sizing
|
||||
// Individual point radii come from geoJSON properties.radius
|
||||
pointRadius: DEFAULT_POINT_RADIUS,
|
||||
pointRadiusUnit,
|
||||
renderWhileDragging,
|
||||
rgb,
|
||||
viewportLongitude: clampNumber(
|
||||
toFiniteNumber(viewportLongitude),
|
||||
MIN_LONGITUDE,
|
||||
MAX_LONGITUDE,
|
||||
),
|
||||
viewportLatitude: clampNumber(
|
||||
toFiniteNumber(viewportLatitude),
|
||||
MIN_LATITUDE,
|
||||
MAX_LATITUDE,
|
||||
),
|
||||
viewportZoom: clampNumber(
|
||||
toFiniteNumber(viewportZoom),
|
||||
MIN_ZOOM,
|
||||
DEFAULT_MAX_ZOOM,
|
||||
),
|
||||
globalOpacity: Math.min(1, Math.max(0, toFiniteNumber(globalOpacity) ?? 1)),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
/*
|
||||
* 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 { type ReactNode } from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import MapBox from '../src/MapBox';
|
||||
|
||||
// Capture the most recent viewport props passed to MapGL
|
||||
let lastMapGLProps: Record<string, unknown> = {};
|
||||
const mockFitBounds = jest.fn();
|
||||
|
||||
jest.mock('react-map-gl', () => {
|
||||
const MockMapGL = (props: Record<string, unknown>) => {
|
||||
lastMapGLProps = props;
|
||||
return <div data-test="map-gl">{props.children as ReactNode}</div>;
|
||||
};
|
||||
return { __esModule: true, default: MockMapGL };
|
||||
});
|
||||
|
||||
jest.mock('@math.gl/web-mercator', () => ({
|
||||
WebMercatorViewport: jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
({ width, height }: { width: number; height: number }) => ({
|
||||
fitBounds: (bounds: [[number, number], [number, number]]) =>
|
||||
mockFitBounds(bounds, width, height),
|
||||
}),
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../src/ScatterPlotGlowOverlay', () => {
|
||||
const MockOverlay = (props: Record<string, unknown>) => (
|
||||
<div data-test="scatter-overlay" data-opacity={props.globalOpacity} />
|
||||
);
|
||||
return { __esModule: true, default: MockOverlay };
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
width: 800,
|
||||
height: 600,
|
||||
clusterer: {
|
||||
getClusters: jest.fn().mockReturnValue([]),
|
||||
},
|
||||
globalOpacity: 1,
|
||||
mapboxApiKey: 'test-key',
|
||||
mapStyle: 'mapbox://styles/mapbox/light-v9',
|
||||
pointRadius: 60,
|
||||
pointRadiusUnit: 'Pixels',
|
||||
renderWhileDragging: true,
|
||||
rgb: ['', 255, 0, 0] as (string | number)[],
|
||||
hasCustomMetric: false,
|
||||
bounds: [
|
||||
[-74.0, 40.7],
|
||||
[-73.9, 40.8],
|
||||
] as [[number, number], [number, number]],
|
||||
onViewportChange: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
lastMapGLProps = {};
|
||||
jest.clearAllMocks();
|
||||
mockFitBounds.mockImplementation(
|
||||
(
|
||||
bounds: [[number, number], [number, number]],
|
||||
width: number,
|
||||
height: number,
|
||||
) => ({
|
||||
latitude: Number(((bounds[0][1] + bounds[1][1]) / 2).toFixed(2)),
|
||||
longitude: Number(((bounds[0][0] + bounds[1][0]) / 2).toFixed(2)),
|
||||
zoom: Number((10 + width / 1000 + height / 10000).toFixed(2)),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('initializes viewport from bounds', () => {
|
||||
render(<MapBox {...defaultProps} />);
|
||||
expect(lastMapGLProps.latitude).toBe(40.75);
|
||||
expect(lastMapGLProps.longitude).toBe(-73.95);
|
||||
expect(lastMapGLProps.zoom).toBe(10.86);
|
||||
});
|
||||
|
||||
test('updates viewport when viewport props change', () => {
|
||||
const { rerender } = render(
|
||||
<MapBox
|
||||
{...defaultProps}
|
||||
viewportLongitude={-73.95}
|
||||
viewportLatitude={40.75}
|
||||
viewportZoom={10}
|
||||
/>,
|
||||
);
|
||||
|
||||
rerender(
|
||||
<MapBox
|
||||
{...defaultProps}
|
||||
viewportLongitude={-122.4}
|
||||
viewportLatitude={37.8}
|
||||
viewportZoom={5}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastMapGLProps.longitude).toBe(-122.4);
|
||||
expect(lastMapGLProps.latitude).toBe(37.8);
|
||||
expect(lastMapGLProps.zoom).toBe(5);
|
||||
});
|
||||
|
||||
test('does not loop when viewport state matches new props', () => {
|
||||
const { rerender } = render(
|
||||
<MapBox
|
||||
{...defaultProps}
|
||||
viewportLongitude={-73.95}
|
||||
viewportLatitude={40.75}
|
||||
viewportZoom={10}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Re-render with same props that match the initial viewport state
|
||||
rerender(
|
||||
<MapBox
|
||||
{...defaultProps}
|
||||
viewportLongitude={-73.95}
|
||||
viewportLatitude={40.75}
|
||||
viewportZoom={10}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Viewport should still be the fitBounds-computed values since props didn't change
|
||||
expect(lastMapGLProps.longitude).toBe(-73.95);
|
||||
expect(lastMapGLProps.latitude).toBe(40.75);
|
||||
expect(lastMapGLProps.zoom).toBe(10);
|
||||
});
|
||||
|
||||
test('passes globalOpacity to ScatterPlotGlowOverlay', () => {
|
||||
const { getByTestId } = render(
|
||||
<MapBox {...defaultProps} globalOpacity={0.5} />,
|
||||
);
|
||||
const overlay = getByTestId('scatter-overlay');
|
||||
expect(overlay.dataset.opacity).toBe('0.5');
|
||||
});
|
||||
|
||||
test('initializes viewport from props when provided', () => {
|
||||
render(
|
||||
<MapBox
|
||||
{...defaultProps}
|
||||
viewportLongitude={-122.4}
|
||||
viewportLatitude={37.8}
|
||||
viewportZoom={5}
|
||||
/>,
|
||||
);
|
||||
expect(lastMapGLProps.longitude).toBe(-122.4);
|
||||
expect(lastMapGLProps.latitude).toBe(37.8);
|
||||
expect(lastMapGLProps.zoom).toBe(5);
|
||||
});
|
||||
|
||||
test('handles undefined bounds gracefully', () => {
|
||||
render(<MapBox {...defaultProps} bounds={undefined} />);
|
||||
expect(lastMapGLProps.longitude).toBe(0);
|
||||
expect(lastMapGLProps.latitude).toBe(0);
|
||||
expect(lastMapGLProps.zoom).toBe(1);
|
||||
});
|
||||
|
||||
test('applies partial viewport props on update', () => {
|
||||
const { rerender } = render(<MapBox {...defaultProps} />);
|
||||
|
||||
rerender(<MapBox {...defaultProps} viewportLongitude={-122.4} />);
|
||||
|
||||
expect(lastMapGLProps.longitude).toBe(-122.4);
|
||||
expect(lastMapGLProps.latitude).toBe(40.75);
|
||||
expect(lastMapGLProps.zoom).toBe(10.86);
|
||||
});
|
||||
|
||||
test('restores fitBounds when viewport props are cleared', () => {
|
||||
const { rerender } = render(
|
||||
<MapBox
|
||||
{...defaultProps}
|
||||
viewportLongitude={-122.4}
|
||||
viewportLatitude={37.8}
|
||||
viewportZoom={5}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Clear all viewport props (simulates user clearing the controls)
|
||||
rerender(<MapBox {...defaultProps} />);
|
||||
|
||||
// Should revert to fitBounds values
|
||||
expect(lastMapGLProps.longitude).toBe(-73.95);
|
||||
expect(lastMapGLProps.latitude).toBe(40.75);
|
||||
expect(lastMapGLProps.zoom).toBe(10.86);
|
||||
});
|
||||
|
||||
test('restores only cleared viewport props, keeps the rest', () => {
|
||||
const { rerender } = render(
|
||||
<MapBox
|
||||
{...defaultProps}
|
||||
viewportLongitude={-122.4}
|
||||
viewportLatitude={37.8}
|
||||
viewportZoom={5}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Clear only longitude, keep lat/zoom
|
||||
rerender(
|
||||
<MapBox {...defaultProps} viewportLatitude={37.8} viewportZoom={5} />,
|
||||
);
|
||||
|
||||
// Longitude reverts to fitBounds, lat/zoom stay
|
||||
expect(lastMapGLProps.longitude).toBe(-73.95);
|
||||
expect(lastMapGLProps.latitude).toBe(37.8);
|
||||
expect(lastMapGLProps.zoom).toBe(5);
|
||||
});
|
||||
|
||||
test('applies changed viewport props even when another is cleared simultaneously', () => {
|
||||
const { rerender } = render(
|
||||
<MapBox
|
||||
{...defaultProps}
|
||||
viewportLongitude={-122.4}
|
||||
viewportLatitude={37.8}
|
||||
viewportZoom={5}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Clear longitude, change latitude simultaneously
|
||||
rerender(
|
||||
<MapBox {...defaultProps} viewportLatitude={40.0} viewportZoom={5} />,
|
||||
);
|
||||
|
||||
// Longitude reverts to fitBounds, latitude should be the NEW value
|
||||
expect(lastMapGLProps.longitude).toBe(-73.95);
|
||||
expect(lastMapGLProps.latitude).toBe(40.0);
|
||||
expect(lastMapGLProps.zoom).toBe(5);
|
||||
});
|
||||
|
||||
test('falls back to default viewport when cleared with undefined bounds', () => {
|
||||
const { rerender } = render(
|
||||
<MapBox
|
||||
{...defaultProps}
|
||||
bounds={undefined}
|
||||
viewportLongitude={-122.4}
|
||||
viewportLatitude={37.8}
|
||||
viewportZoom={5}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Clear viewport props — no bounds to fitBounds to
|
||||
rerender(<MapBox {...defaultProps} bounds={undefined} />);
|
||||
|
||||
// Should fall back to {0, 0, 1}
|
||||
expect(lastMapGLProps.longitude).toBe(0);
|
||||
expect(lastMapGLProps.latitude).toBe(0);
|
||||
expect(lastMapGLProps.zoom).toBe(1);
|
||||
});
|
||||
|
||||
test('recomputes fitBounds when bounds change and no explicit viewport is set', () => {
|
||||
const { rerender } = render(<MapBox {...defaultProps} />);
|
||||
|
||||
rerender(
|
||||
<MapBox
|
||||
{...defaultProps}
|
||||
bounds={[
|
||||
[-123.2, 36.5],
|
||||
[-121.8, 38.1],
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastMapGLProps.longitude).toBe(-122.5);
|
||||
expect(lastMapGLProps.latitude).toBe(37.3);
|
||||
expect(lastMapGLProps.zoom).toBe(10.86);
|
||||
});
|
||||
|
||||
test('recomputes fitBounds when chart size changes and no explicit viewport is set', () => {
|
||||
const { rerender } = render(<MapBox {...defaultProps} />);
|
||||
|
||||
rerender(<MapBox {...defaultProps} width={1200} height={900} />);
|
||||
|
||||
expect(lastMapGLProps.longitude).toBe(-73.95);
|
||||
expect(lastMapGLProps.latitude).toBe(40.75);
|
||||
expect(lastMapGLProps.zoom).toBe(11.29);
|
||||
});
|
||||
|
||||
test('recomputes only implicit viewport fields when bounds change', () => {
|
||||
const { rerender } = render(
|
||||
<MapBox {...defaultProps} viewportLongitude={-122.4} />,
|
||||
);
|
||||
|
||||
rerender(
|
||||
<MapBox
|
||||
{...defaultProps}
|
||||
viewportLongitude={-122.4}
|
||||
bounds={[
|
||||
[-123.2, 36.5],
|
||||
[-121.8, 38.1],
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastMapGLProps.longitude).toBe(-122.4);
|
||||
expect(lastMapGLProps.latitude).toBe(37.3);
|
||||
expect(lastMapGLProps.zoom).toBe(10.86);
|
||||
});
|
||||
|
||||
test('recomputes only implicit viewport fields when chart size changes', () => {
|
||||
const { rerender } = render(
|
||||
<MapBox {...defaultProps} viewportLatitude={37.8} />,
|
||||
);
|
||||
|
||||
rerender(
|
||||
<MapBox
|
||||
{...defaultProps}
|
||||
viewportLatitude={37.8}
|
||||
width={1200}
|
||||
height={900}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastMapGLProps.longitude).toBe(-73.95);
|
||||
expect(lastMapGLProps.latitude).toBe(37.8);
|
||||
expect(lastMapGLProps.zoom).toBe(11.29);
|
||||
});
|
||||
|
||||
test('recomputes implicit position when zoom stays explicit across bounds changes', () => {
|
||||
const { rerender } = render(<MapBox {...defaultProps} viewportZoom={5} />);
|
||||
|
||||
rerender(
|
||||
<MapBox
|
||||
{...defaultProps}
|
||||
viewportZoom={5}
|
||||
bounds={[
|
||||
[-123.2, 36.5],
|
||||
[-121.8, 38.1],
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastMapGLProps.longitude).toBe(-122.5);
|
||||
expect(lastMapGLProps.latitude).toBe(37.3);
|
||||
expect(lastMapGLProps.zoom).toBe(5);
|
||||
});
|
||||
|
||||
test('does not recompute fitBounds on bounds change when an explicit viewport is set', () => {
|
||||
const { rerender } = render(
|
||||
<MapBox
|
||||
{...defaultProps}
|
||||
viewportLongitude={-122.4}
|
||||
viewportLatitude={37.8}
|
||||
viewportZoom={5}
|
||||
/>,
|
||||
);
|
||||
|
||||
rerender(
|
||||
<MapBox
|
||||
{...defaultProps}
|
||||
viewportLongitude={-122.4}
|
||||
viewportLatitude={37.8}
|
||||
viewportZoom={5}
|
||||
bounds={[
|
||||
[-123.2, 36.5],
|
||||
[-121.8, 38.1],
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastMapGLProps.longitude).toBe(-122.4);
|
||||
expect(lastMapGLProps.latitude).toBe(37.8);
|
||||
expect(lastMapGLProps.zoom).toBe(5);
|
||||
});
|
||||
@@ -18,11 +18,7 @@
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import ScatterPlotOverlay from '../src/components/ScatterPlotOverlay';
|
||||
import {
|
||||
MIN_CLUSTER_RADIUS_RATIO,
|
||||
MAX_POINT_RADIUS_RATIO,
|
||||
} from '../src/components/ScatterPlotOverlay';
|
||||
import ScatterPlotGlowOverlay from '../src/ScatterPlotGlowOverlay';
|
||||
|
||||
type MockGradient = {
|
||||
addColorStop: jest.Mock<void, [number, string]>;
|
||||
@@ -71,20 +67,22 @@ declare global {
|
||||
var mockRedraw: unknown;
|
||||
}
|
||||
|
||||
// Mock the CanvasOverlay component to capture the redraw function
|
||||
jest.mock('../src/components/CanvasOverlay', () => ({
|
||||
__esModule: true,
|
||||
default: ({ redraw }: { redraw: unknown }) => {
|
||||
// Mock react-map-gl's CanvasOverlay
|
||||
jest.mock('react-map-gl', () => ({
|
||||
CanvasOverlay: ({ redraw }: { redraw: unknown }) => {
|
||||
// Store the redraw function so tests can call it
|
||||
global.mockRedraw = redraw;
|
||||
return <div data-testid="canvas-overlay" />;
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock utility functions
|
||||
jest.mock('../src/utils/luminanceFromRGB', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => 150),
|
||||
default: jest.fn(() => 150), // Return a value above the dark threshold
|
||||
}));
|
||||
|
||||
// Test helpers
|
||||
const createMockCanvas = () => {
|
||||
const ctx: MockCanvasContext = {
|
||||
clearRect: jest.fn(),
|
||||
@@ -153,10 +151,8 @@ const defaultProps = {
|
||||
rgb: ['', 255, 0, 0] as [string, number, number, number],
|
||||
globalOpacity: 1,
|
||||
};
|
||||
const MIN_VISIBLE_POINT_RADIUS =
|
||||
defaultProps.dotRadius * MIN_CLUSTER_RADIUS_RATIO;
|
||||
const MAX_VISIBLE_POINT_RADIUS =
|
||||
defaultProps.dotRadius * MAX_POINT_RADIUS_RATIO;
|
||||
const MIN_VISIBLE_POINT_RADIUS = 10;
|
||||
const MAX_VISIBLE_POINT_RADIUS = 20;
|
||||
|
||||
test('renders map with varying radius values in Pixels mode', () => {
|
||||
const locations = [
|
||||
@@ -166,7 +162,7 @@ test('renders map with varying radius values in Pixels mode', () => {
|
||||
];
|
||||
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
pointRadiusUnit="Pixels"
|
||||
@@ -176,11 +172,13 @@ test('renders map with varying radius values in Pixels mode', () => {
|
||||
|
||||
const arcCalls = redrawParams.ctx.arc.mock.calls;
|
||||
|
||||
// With dotRadius=60, pixel-sized points should map to the visible 10-20 range.
|
||||
arcCalls.forEach(call => {
|
||||
expect(call[2]).toBeGreaterThanOrEqual(MIN_VISIBLE_POINT_RADIUS);
|
||||
expect(call[2]).toBeLessThanOrEqual(MAX_VISIBLE_POINT_RADIUS);
|
||||
});
|
||||
|
||||
// Ordering should be preserved: radius 10 < 50 < 100
|
||||
expect(arcCalls[0][2]).toBeLessThan(arcCalls[1][2]);
|
||||
expect(arcCalls[1][2]).toBeLessThan(arcCalls[2][2]);
|
||||
});
|
||||
@@ -194,7 +192,7 @@ test('handles dataset with uniform radius values', () => {
|
||||
|
||||
expect(() => {
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
pointRadiusUnit="Pixels"
|
||||
@@ -213,7 +211,7 @@ test('renders successfully when data contains non-finite values', () => {
|
||||
|
||||
expect(() => {
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
pointRadiusUnit="Pixels"
|
||||
@@ -231,7 +229,7 @@ test('handles radius values provided as strings', () => {
|
||||
];
|
||||
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
pointRadiusUnit="Pixels"
|
||||
@@ -258,7 +256,7 @@ test('treats blank radius strings as missing values', () => {
|
||||
];
|
||||
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
pointRadiusUnit="Pixels"
|
||||
@@ -282,7 +280,7 @@ test('renders points when radius values are missing', () => {
|
||||
|
||||
expect(() => {
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
pointRadiusUnit="Pixels"
|
||||
@@ -306,7 +304,7 @@ test('renders both cluster and non-cluster points correctly', () => {
|
||||
|
||||
expect(() => {
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
pointRadiusUnit="Pixels"
|
||||
@@ -325,7 +323,7 @@ test('renders map with multiple points with different radius values', () => {
|
||||
|
||||
expect(() => {
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
pointRadiusUnit="Pixels"
|
||||
@@ -343,7 +341,7 @@ test('renders map with Kilometers mode', () => {
|
||||
|
||||
expect(() => {
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
pointRadiusUnit="Kilometers"
|
||||
@@ -362,7 +360,7 @@ test('renders map with Miles mode', () => {
|
||||
|
||||
expect(() => {
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
pointRadiusUnit="Miles"
|
||||
@@ -380,7 +378,7 @@ test('displays metric property labels on points', () => {
|
||||
|
||||
expect(() => {
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
pointRadiusUnit="Pixels"
|
||||
@@ -393,7 +391,7 @@ test('displays metric property labels on points', () => {
|
||||
test('handles empty dataset without errors', () => {
|
||||
expect(() => {
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={[]}
|
||||
pointRadiusUnit="Pixels"
|
||||
@@ -412,7 +410,7 @@ test('handles extreme outlier radius values without breaking', () => {
|
||||
|
||||
expect(() => {
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
pointRadiusUnit="Pixels"
|
||||
@@ -433,7 +431,7 @@ test('renders successfully with mixed extreme and negative radius values', () =>
|
||||
|
||||
expect(() => {
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
pointRadiusUnit="Pixels"
|
||||
@@ -458,7 +456,7 @@ test('cluster radius is always >= max individual point radius in Pixels mode', (
|
||||
];
|
||||
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
aggregation="sum"
|
||||
@@ -469,7 +467,9 @@ test('cluster radius is always >= max individual point radius in Pixels mode', (
|
||||
|
||||
const arcCalls = redrawParams.ctx.arc.mock.calls;
|
||||
|
||||
// cluster with label=1 (index 0) should not be smaller than the largest point bubble
|
||||
expect(arcCalls[0][2]).toBeGreaterThanOrEqual(MAX_VISIBLE_POINT_RADIUS);
|
||||
// point radii span the configured pixel range
|
||||
expect(arcCalls[1][2]).toBe(MIN_VISIBLE_POINT_RADIUS);
|
||||
expect(arcCalls[2][2]).toBe(MAX_VISIBLE_POINT_RADIUS);
|
||||
expect(arcCalls[0][2]).toBeGreaterThanOrEqual(arcCalls[2][2]);
|
||||
@@ -490,7 +490,7 @@ test('largest cluster gets full dotRadius', () => {
|
||||
];
|
||||
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
aggregation="sum"
|
||||
@@ -500,6 +500,7 @@ test('largest cluster gets full dotRadius', () => {
|
||||
const redrawParams = triggerRedraw();
|
||||
|
||||
const arcCalls = redrawParams.ctx.arc.mock.calls;
|
||||
// The largest cluster (label=100, maxLabel=100) should get full radius
|
||||
expect(arcCalls[1][2]).toBe(defaultProps.dotRadius);
|
||||
});
|
||||
|
||||
@@ -523,7 +524,7 @@ test('cluster radii preserve proportional ordering', () => {
|
||||
];
|
||||
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
aggregation="sum"
|
||||
@@ -551,7 +552,7 @@ test('negative cluster label produces valid finite radius', () => {
|
||||
];
|
||||
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
aggregation="sum"
|
||||
@@ -580,7 +581,7 @@ test('ignores non-finite cluster labels when computing cluster scaling bounds',
|
||||
];
|
||||
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
aggregation="sum"
|
||||
@@ -605,7 +606,7 @@ test('single cluster with small maxLabel gets full dotRadius', () => {
|
||||
];
|
||||
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
aggregation="sum"
|
||||
@@ -614,6 +615,7 @@ test('single cluster with small maxLabel gets full dotRadius', () => {
|
||||
const redrawParams = triggerRedraw();
|
||||
|
||||
const arcCalls = redrawParams.ctx.arc.mock.calls;
|
||||
// When there's only one cluster, label=maxLabel, so it gets full radius
|
||||
expect(arcCalls[0][2]).toBe(defaultProps.dotRadius);
|
||||
});
|
||||
|
||||
@@ -637,7 +639,7 @@ test('all-negative cluster labels produce differentiated radii by magnitude', ()
|
||||
];
|
||||
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
aggregation="sum"
|
||||
@@ -650,6 +652,7 @@ test('all-negative cluster labels produce differentiated radii by magnitude', ()
|
||||
const rNeg10 = arcCalls[1][2];
|
||||
const rNeg1 = arcCalls[2][2];
|
||||
|
||||
// Higher magnitude = bigger circle: |-100| > |-10| > |-1|
|
||||
expect(rNeg1).toBeLessThan(rNeg10);
|
||||
expect(rNeg10).toBeLessThan(rNeg100);
|
||||
expect(Number.isFinite(rNeg100)).toBe(true);
|
||||
@@ -679,7 +682,7 @@ test('mixed positive-and-negative cluster labels size by magnitude', () => {
|
||||
];
|
||||
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
aggregation="sum"
|
||||
@@ -692,6 +695,7 @@ test('mixed positive-and-negative cluster labels size by magnitude', () => {
|
||||
const rZero = arcCalls[1][2];
|
||||
const r100 = arcCalls[2][2];
|
||||
|
||||
// Magnitude ordering: |0| < |-50| < |100|
|
||||
expect(rZero).toBeLessThan(rNeg50);
|
||||
expect(rNeg50).toBeLessThan(r100);
|
||||
expect(rZero).toBeGreaterThanOrEqual(MIN_VISIBLE_POINT_RADIUS);
|
||||
@@ -718,7 +722,7 @@ test('all-identical negative labels get equal full radii', () => {
|
||||
];
|
||||
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
aggregation="sum"
|
||||
@@ -746,7 +750,7 @@ test('single negative cluster gets full radius', () => {
|
||||
];
|
||||
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
aggregation="sum"
|
||||
@@ -768,7 +772,7 @@ test('large negative cluster labels are abbreviated', () => {
|
||||
];
|
||||
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
aggregation="sum"
|
||||
@@ -808,7 +812,7 @@ test.each([
|
||||
];
|
||||
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
aggregation={aggregation}
|
||||
@@ -842,7 +846,7 @@ test('zero-value cluster is visible with minimum radius', () => {
|
||||
];
|
||||
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
aggregation="sum"
|
||||
@@ -873,7 +877,7 @@ test('all-zero clusters use a finite radius', () => {
|
||||
];
|
||||
|
||||
render(
|
||||
<ScatterPlotOverlay
|
||||
<ScatterPlotGlowOverlay
|
||||
{...defaultProps}
|
||||
locations={locations}
|
||||
aggregation="sum"
|
||||
@@ -42,23 +42,19 @@ type TransformPropsResult = {
|
||||
viewportLongitude?: number;
|
||||
viewportLatitude?: number;
|
||||
viewportZoom?: number;
|
||||
rgb?: string[] | null;
|
||||
};
|
||||
|
||||
const baseFormData = {
|
||||
all_columns_x: 'lon',
|
||||
all_columns_y: 'lat',
|
||||
clustering_radius: 60,
|
||||
global_opacity: 0.8,
|
||||
map_color: 'rgb(0, 139, 139)',
|
||||
map_renderer: 'maplibre',
|
||||
maplibre_style: 'https://tiles.openfreemap.org/styles/liberty',
|
||||
pandas_aggfunc: 'sum',
|
||||
point_radius_unit: 'Pixels',
|
||||
render_while_dragging: true,
|
||||
viewport_longitude: -73.935242,
|
||||
viewport_latitude: 40.73061,
|
||||
viewport_zoom: 9,
|
||||
clusteringRadius: 60,
|
||||
globalOpacity: 0.8,
|
||||
mapboxColor: 'rgb(0, 139, 139)',
|
||||
mapboxStyle: 'mapbox://styles/mapbox/light-v9',
|
||||
pandasAggfunc: 'sum',
|
||||
pointRadiusUnit: 'Pixels',
|
||||
renderWhileDragging: true,
|
||||
viewportLongitude: -73.935242,
|
||||
viewportLatitude: 40.73061,
|
||||
viewportZoom: 9,
|
||||
};
|
||||
|
||||
const baseQueriesData = [
|
||||
@@ -70,6 +66,7 @@ const baseQueriesData = [
|
||||
] as [[number, number], [number, number]],
|
||||
geoJSON: { features: [] },
|
||||
hasCustomMetric: false,
|
||||
mapboxApiKey: 'test-api-key',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -91,15 +88,15 @@ function getTransformPropsResult(
|
||||
}
|
||||
|
||||
test('extracts globalOpacity from formData', () => {
|
||||
const result = getTransformPropsResult({ global_opacity: 0.5 });
|
||||
const result = getTransformPropsResult({ globalOpacity: 0.5 });
|
||||
expect(result.globalOpacity).toBe(0.5);
|
||||
});
|
||||
|
||||
test('extracts viewport values from formData', () => {
|
||||
const result = getTransformPropsResult({
|
||||
viewport_longitude: -122.4,
|
||||
viewport_latitude: 37.8,
|
||||
viewport_zoom: 12,
|
||||
viewportLongitude: -122.4,
|
||||
viewportLatitude: 37.8,
|
||||
viewportZoom: 12,
|
||||
});
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
@@ -112,9 +109,9 @@ test('extracts viewport values from formData', () => {
|
||||
|
||||
test('clamps viewport values to safe map ranges', () => {
|
||||
const result = getTransformPropsResult({
|
||||
viewport_longitude: 190,
|
||||
viewport_latitude: -100,
|
||||
viewport_zoom: 99,
|
||||
viewportLongitude: 190,
|
||||
viewportLatitude: -100,
|
||||
viewportZoom: 99,
|
||||
});
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
@@ -151,9 +148,9 @@ test('provides onViewportChange callback that updates control values', () => {
|
||||
|
||||
test('normalizes string viewport values to numbers', () => {
|
||||
const result = getTransformPropsResult({
|
||||
viewport_longitude: '-122.4',
|
||||
viewport_latitude: '37.8',
|
||||
viewport_zoom: '12',
|
||||
viewportLongitude: '-122.4',
|
||||
viewportLatitude: '37.8',
|
||||
viewportZoom: '12',
|
||||
});
|
||||
expect(result.viewportLongitude).toBe(-122.4);
|
||||
expect(result.viewportLatitude).toBe(37.8);
|
||||
@@ -162,9 +159,9 @@ test('normalizes string viewport values to numbers', () => {
|
||||
|
||||
test('normalizes empty viewport values to undefined', () => {
|
||||
const result = getTransformPropsResult({
|
||||
viewport_longitude: '',
|
||||
viewport_latitude: '',
|
||||
viewport_zoom: '',
|
||||
viewportLongitude: '',
|
||||
viewportLatitude: '',
|
||||
viewportZoom: '',
|
||||
});
|
||||
expect(result.viewportLongitude).toBeUndefined();
|
||||
expect(result.viewportLatitude).toBeUndefined();
|
||||
@@ -173,9 +170,9 @@ test('normalizes empty viewport values to undefined', () => {
|
||||
|
||||
test('normalizes whitespace-only viewport values to undefined', () => {
|
||||
const result = getTransformPropsResult({
|
||||
viewport_longitude: ' ',
|
||||
viewport_latitude: '\t',
|
||||
viewport_zoom: ' \n ',
|
||||
viewportLongitude: ' ',
|
||||
viewportLatitude: '\t',
|
||||
viewportZoom: ' \n ',
|
||||
});
|
||||
expect(result.viewportLongitude).toBeUndefined();
|
||||
expect(result.viewportLatitude).toBeUndefined();
|
||||
@@ -183,31 +180,31 @@ test('normalizes whitespace-only viewport values to undefined', () => {
|
||||
});
|
||||
|
||||
test('normalizes string opacity to number', () => {
|
||||
const result = getTransformPropsResult({ global_opacity: '0.5' });
|
||||
const result = getTransformPropsResult({ globalOpacity: '0.5' });
|
||||
expect(result.globalOpacity).toBe(0.5);
|
||||
});
|
||||
|
||||
test('defaults empty opacity to 1', () => {
|
||||
const result = getTransformPropsResult({ global_opacity: '' });
|
||||
const result = getTransformPropsResult({ globalOpacity: '' });
|
||||
expect(result.globalOpacity).toBe(1);
|
||||
});
|
||||
|
||||
test('defaults whitespace-only opacity to 1', () => {
|
||||
const result = getTransformPropsResult({ global_opacity: ' ' });
|
||||
const result = getTransformPropsResult({ globalOpacity: ' ' });
|
||||
expect(result.globalOpacity).toBe(1);
|
||||
});
|
||||
|
||||
test('clamps opacity to [0, 1] range', () => {
|
||||
expect(getTransformPropsResult({ global_opacity: 5 }).globalOpacity).toBe(1);
|
||||
expect(getTransformPropsResult({ global_opacity: -1 }).globalOpacity).toBe(0);
|
||||
expect(getTransformPropsResult({ globalOpacity: 5 }).globalOpacity).toBe(1);
|
||||
expect(getTransformPropsResult({ globalOpacity: -1 }).globalOpacity).toBe(0);
|
||||
});
|
||||
|
||||
test('passes through numeric values unchanged', () => {
|
||||
const result = getTransformPropsResult({
|
||||
viewport_longitude: -122.4,
|
||||
viewport_latitude: 37.8,
|
||||
viewport_zoom: 12,
|
||||
global_opacity: 0.8,
|
||||
viewportLongitude: -122.4,
|
||||
viewportLatitude: 37.8,
|
||||
viewportZoom: 12,
|
||||
globalOpacity: 0.8,
|
||||
});
|
||||
expect(result.viewportLongitude).toBe(-122.4);
|
||||
expect(result.viewportLatitude).toBe(37.8);
|
||||
@@ -215,18 +212,19 @@ test('passes through numeric values unchanged', () => {
|
||||
expect(result.globalOpacity).toBe(0.8);
|
||||
});
|
||||
|
||||
test('calls onError and falls back to black for invalid color', () => {
|
||||
test('calls onError and returns empty object for invalid color', () => {
|
||||
const onError = jest.fn();
|
||||
const chartProps = new ChartProps({
|
||||
formData: { ...baseFormData, map_color: 'invalid-color' },
|
||||
formData: { ...baseFormData, mapboxColor: 'invalid-color' },
|
||||
width: 800,
|
||||
height: 600,
|
||||
queriesData: baseQueriesData,
|
||||
hooks: { onError },
|
||||
theme: supersetTheme,
|
||||
});
|
||||
const result = transformProps(chartProps) as TransformPropsResult;
|
||||
expect(onError).toHaveBeenCalled();
|
||||
// Falls back to black instead of returning empty object
|
||||
expect(result.rgb).toEqual(['', '0', '0', '0']);
|
||||
const result = transformProps(chartProps);
|
||||
expect(onError).toHaveBeenCalledWith(
|
||||
"Color field must be of form 'rgb(%d, %d, %d)'",
|
||||
);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"emitDeclarationOnly": false,
|
||||
"rootDir": "."
|
||||
},
|
||||
"extends": "../../../tsconfig.json",
|
||||
"include": ["**/*", "../types/**/*", "../../../types/**/*"]
|
||||
}
|
||||
@@ -17,12 +17,12 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
declare module '*.png' {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
import roundDecimal from '../../src/utils/roundDecimal';
|
||||
|
||||
declare module '*.jpg' {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
describe('roundDecimal', () => {
|
||||
test('rounding method to limit the number of decimal digits', () => {
|
||||
expect(roundDecimal(1.139, 2)).toBe(1.14);
|
||||
expect(roundDecimal(1.13929, 3)).toBe(1.139);
|
||||
expect(roundDecimal(1.13929)).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
// Path Resolution: Override baseUrl to maintain correct path mappings from parent config
|
||||
// (e.g., "@apache-superset/core" -> "./packages/superset-core/src")
|
||||
"baseUrl": "../..",
|
||||
|
||||
// Directory Overrides: Parent config paths are relative to frontend root,
|
||||
// but packages need paths relative to their own directory
|
||||
"outDir": "lib",
|
||||
"rootDir": "src",
|
||||
"declarationDir": "lib"
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
|
||||
"exclude": [
|
||||
"src/**/*.js",
|
||||
"src/**/*.jsx",
|
||||
"src/**/*.test.*",
|
||||
"src/**/*.stories.*"],
|
||||
"references": [
|
||||
{ "path": "../../packages/superset-core" },
|
||||
{ "path": "../../packages/superset-ui-core" },
|
||||
{ "path": "../../packages/superset-ui-chart-controls" }
|
||||
]
|
||||
}
|
||||
101
superset-frontend/plugins/legacy-plugin-chart-map-box/types/external.d.ts
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
declare module '*.png' {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module 'supercluster' {
|
||||
interface Options<P = Record<string, unknown>, C = Record<string, unknown>> {
|
||||
minZoom?: number;
|
||||
maxZoom?: number;
|
||||
minPoints?: number;
|
||||
radius?: number;
|
||||
extent?: number;
|
||||
nodeSize?: number;
|
||||
log?: boolean;
|
||||
initial?: () => C;
|
||||
map?: (props: P) => C;
|
||||
reduce?: (accumulated: C, props: C) => void;
|
||||
}
|
||||
|
||||
interface GeoJSONFeature {
|
||||
type: string;
|
||||
geometry: {
|
||||
type: string;
|
||||
coordinates: [number, number];
|
||||
};
|
||||
properties: Record<string, unknown>;
|
||||
}
|
||||
|
||||
class Supercluster<P = Record<string, unknown>, C = Record<string, unknown>> {
|
||||
constructor(options?: Options<P, C>);
|
||||
load(points: GeoJSONFeature[]): Supercluster<P, C>;
|
||||
getClusters(bbox: number[], zoom: number): GeoJSONFeature[];
|
||||
getTile(z: number, x: number, y: number): GeoJSONFeature[] | null;
|
||||
getChildren(clusterId: number): GeoJSONFeature[];
|
||||
getLeaves(
|
||||
clusterId: number,
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
): GeoJSONFeature[];
|
||||
getClusterExpansionZoom(clusterId: number): number;
|
||||
}
|
||||
|
||||
export default Supercluster;
|
||||
export { Options, GeoJSONFeature };
|
||||
}
|
||||
|
||||
declare module 'react-map-gl' {
|
||||
import { Component, ReactNode } from 'react';
|
||||
|
||||
interface MapGLProps {
|
||||
width?: number;
|
||||
height?: number;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
zoom?: number;
|
||||
mapStyle?: string;
|
||||
mapboxApiAccessToken?: string;
|
||||
onViewportChange?: Function;
|
||||
preserveDrawingBuffer?: boolean;
|
||||
children?: ReactNode;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export default class MapGL extends Component<MapGLProps> {}
|
||||
|
||||
interface CanvasOverlayProps {
|
||||
redraw: (params: {
|
||||
width: number;
|
||||
height: number;
|
||||
ctx: CanvasRenderingContext2D;
|
||||
isDragging: boolean;
|
||||
project: (lngLat: [number, number]) => [number, number];
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export class CanvasOverlay extends Component<CanvasOverlayProps> {}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.20.4](https://github.com/apache/superset/compare/v0.20.3...v0.20.4) (2024-12-10)
|
||||
|
||||
**Note:** Version bump only for package @superset-ui/legacy-preset-chart-deckgl
|
||||
|
||||
# [0.20.0](https://github.com/apache/superset/compare/v2021.41.0...v0.20.0) (2024-09-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Dashboard:** Color inconsistency on refreshes and conflicts ([#27439](https://github.com/apache/superset/issues/27439)) ([313ee59](https://github.com/apache/superset/commit/313ee596f5435894f857d72be7269d5070c8c964))
|
||||
- deck.gl Geojson path not visible ([#24428](https://github.com/apache/superset/issues/24428)) ([6bb930e](https://github.com/apache/superset/commit/6bb930ef4ed26ea381e7f8e889851aa7867ba0eb))
|
||||
- deck.gl GeoJsonLayer Autozoom & fill/stroke options ([#19778](https://github.com/apache/superset/issues/19778)) ([d65b77e](https://github.com/apache/superset/commit/d65b77ec7dac4c2368fcaa1fe6e98db102966198))
|
||||
- **deck.gl Multiple Layer Chart:** Add Contour and Heatmap Layer as options ([#25923](https://github.com/apache/superset/issues/25923)) ([64ba579](https://github.com/apache/superset/commit/64ba5797df92d0f8067ccd2b30ba6ff58e0bd791))
|
||||
- deck.gl Scatterplot min/max radius ([#24363](https://github.com/apache/superset/issues/24363)) ([c728cdf](https://github.com/apache/superset/commit/c728cdf501ec292beb14a0982265052bf2274bec))
|
||||
- **deck.gl:** multiple layers map size is shrunk ([#18939](https://github.com/apache/superset/issues/18939)) ([2cb3635](https://github.com/apache/superset/commit/2cb3635256ee8e91f0bac2f3091684673c04ff2b))
|
||||
- **deck.gl:** update view state on property changes ([#17720](https://github.com/apache/superset/issues/17720)) ([#17826](https://github.com/apache/superset/issues/17826)) ([97d918b](https://github.com/apache/superset/commit/97d918b6927f572dca3b33c61b89c8b3ebdc4376))
|
||||
- DeckGL legend layout ([#30140](https://github.com/apache/superset/issues/30140)) ([af066a4](https://github.com/apache/superset/commit/af066a46306f2f476aa2944b14df3de1faf1e96d))
|
||||
- **deckgl:** deckgl unable to load map ([#17851](https://github.com/apache/superset/issues/17851)) ([52f5dcb](https://github.com/apache/superset/commit/52f5dcb58eec7b188f4387b8781dcda4252a5680))
|
||||
- **explore:** Fix chart standalone URL for report/thumbnail generation ([#20673](https://github.com/apache/superset/issues/20673)) ([84d4302](https://github.com/apache/superset/commit/84d4302628d18aa19c13cc5322e68abbc690ea4d))
|
||||
- **explore:** Prevent shared controls from checking feature flags outside React render ([#21315](https://github.com/apache/superset/issues/21315)) ([2285ebe](https://github.com/apache/superset/commit/2285ebe72ec4edded6d195052740b7f9f13d1f1b))
|
||||
- weight tooltip issue ([#19397](https://github.com/apache/superset/issues/19397)) ([f6d550b](https://github.com/apache/superset/commit/f6d550b7fc3643350483850064e65dbd3d026dc4))
|
||||
|
||||
### Features
|
||||
|
||||
- Add Deck.gl Contour Layer ([#24154](https://github.com/apache/superset/issues/24154)) ([512fb9a](https://github.com/apache/superset/commit/512fb9a0bdd428b94b0c121158b8b15b7631e0fb))
|
||||
- Add deck.gl Heatmap Visualization ([#23551](https://github.com/apache/superset/issues/23551)) ([fc8c537](https://github.com/apache/superset/commit/fc8c537118ce6c7b3a4624f88a31e2e7fb287327))
|
||||
- Add line width unit control in deckgl Polygon and Path ([#24755](https://github.com/apache/superset/issues/24755)) ([d26ea98](https://github.com/apache/superset/commit/d26ea980acc7d2a20757efc360d810afe83d5c65))
|
||||
- apply standardized form data to deckgl ([#20579](https://github.com/apache/superset/issues/20579)) ([290b89c](https://github.com/apache/superset/commit/290b89c7b4ae702c55f611bfac9cedb245ea8bd8))
|
||||
- **deck.gl:** add color range for deck.gl 3D ([#19520](https://github.com/apache/superset/issues/19520)) ([c0a00fd](https://github.com/apache/superset/commit/c0a00fd302ec66fbe0ca766cf73978c99ba00d82))
|
||||
- **deckgl-map:** use an arbitraty Mabpox style URL ([#26027](https://github.com/apache/superset/issues/26027)) ([#26031](https://github.com/apache/superset/issues/26031)) ([af58784](https://github.com/apache/superset/commit/af587840403d83a7da7fb0f57bc10ad2335d4eeb))
|
||||
- **explore:** Frontend implementation of dataset creation from infobox ([#19855](https://github.com/apache/superset/issues/19855)) ([ba0c37d](https://github.com/apache/superset/commit/ba0c37d3df85b1af39404af1d578daeb0ff2d278))
|
||||
- improve color consistency (save all labels) ([#19038](https://github.com/apache/superset/issues/19038)) ([dc57508](https://github.com/apache/superset/commit/dc575080d7e43d40b1734bb8f44fdc291cb95b11))
|
||||
- **legacy-preset-chart-deckgl:** Add ,.1f and ,.2f value formats to deckgl charts ([#18945](https://github.com/apache/superset/issues/18945)) ([c56dc8e](https://github.com/apache/superset/commit/c56dc8eace6a71b45240d1bb6768d75661052a2e))
|
||||
- make data tables support html ([#24368](https://github.com/apache/superset/issues/24368)) ([d2b0b8e](https://github.com/apache/superset/commit/d2b0b8eac52ad8b68639c6581a1ed174a593f564))
|
||||
- **viz picker:** Remove some tags, refactor Recommended section ([#27708](https://github.com/apache/superset/issues/27708)) ([c314999](https://github.com/apache/superset/commit/c3149994ac0d4392e0462421b62cd0c034142082))
|
||||
|
||||
# [0.19.0](https://github.com/apache/superset/compare/v2021.41.0...v0.19.0) (2024-09-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Dashboard:** Color inconsistency on refreshes and conflicts ([#27439](https://github.com/apache/superset/issues/27439)) ([313ee59](https://github.com/apache/superset/commit/313ee596f5435894f857d72be7269d5070c8c964))
|
||||
- deck.gl Geojson path not visible ([#24428](https://github.com/apache/superset/issues/24428)) ([6bb930e](https://github.com/apache/superset/commit/6bb930ef4ed26ea381e7f8e889851aa7867ba0eb))
|
||||
- deck.gl GeoJsonLayer Autozoom & fill/stroke options ([#19778](https://github.com/apache/superset/issues/19778)) ([d65b77e](https://github.com/apache/superset/commit/d65b77ec7dac4c2368fcaa1fe6e98db102966198))
|
||||
- **deck.gl Multiple Layer Chart:** Add Contour and Heatmap Layer as options ([#25923](https://github.com/apache/superset/issues/25923)) ([64ba579](https://github.com/apache/superset/commit/64ba5797df92d0f8067ccd2b30ba6ff58e0bd791))
|
||||
- deck.gl Scatterplot min/max radius ([#24363](https://github.com/apache/superset/issues/24363)) ([c728cdf](https://github.com/apache/superset/commit/c728cdf501ec292beb14a0982265052bf2274bec))
|
||||
- **deck.gl:** multiple layers map size is shrunk ([#18939](https://github.com/apache/superset/issues/18939)) ([2cb3635](https://github.com/apache/superset/commit/2cb3635256ee8e91f0bac2f3091684673c04ff2b))
|
||||
- **deck.gl:** update view state on property changes ([#17720](https://github.com/apache/superset/issues/17720)) ([#17826](https://github.com/apache/superset/issues/17826)) ([97d918b](https://github.com/apache/superset/commit/97d918b6927f572dca3b33c61b89c8b3ebdc4376))
|
||||
- DeckGL legend layout ([#30140](https://github.com/apache/superset/issues/30140)) ([af066a4](https://github.com/apache/superset/commit/af066a46306f2f476aa2944b14df3de1faf1e96d))
|
||||
- **deckgl:** deckgl unable to load map ([#17851](https://github.com/apache/superset/issues/17851)) ([52f5dcb](https://github.com/apache/superset/commit/52f5dcb58eec7b188f4387b8781dcda4252a5680))
|
||||
- **explore:** Fix chart standalone URL for report/thumbnail generation ([#20673](https://github.com/apache/superset/issues/20673)) ([84d4302](https://github.com/apache/superset/commit/84d4302628d18aa19c13cc5322e68abbc690ea4d))
|
||||
- **explore:** Prevent shared controls from checking feature flags outside React render ([#21315](https://github.com/apache/superset/issues/21315)) ([2285ebe](https://github.com/apache/superset/commit/2285ebe72ec4edded6d195052740b7f9f13d1f1b))
|
||||
- weight tooltip issue ([#19397](https://github.com/apache/superset/issues/19397)) ([f6d550b](https://github.com/apache/superset/commit/f6d550b7fc3643350483850064e65dbd3d026dc4))
|
||||
|
||||
### Features
|
||||
|
||||
- Add Deck.gl Contour Layer ([#24154](https://github.com/apache/superset/issues/24154)) ([512fb9a](https://github.com/apache/superset/commit/512fb9a0bdd428b94b0c121158b8b15b7631e0fb))
|
||||
- Add deck.gl Heatmap Visualization ([#23551](https://github.com/apache/superset/issues/23551)) ([fc8c537](https://github.com/apache/superset/commit/fc8c537118ce6c7b3a4624f88a31e2e7fb287327))
|
||||
- Add line width unit control in deckgl Polygon and Path ([#24755](https://github.com/apache/superset/issues/24755)) ([d26ea98](https://github.com/apache/superset/commit/d26ea980acc7d2a20757efc360d810afe83d5c65))
|
||||
- apply standardized form data to deckgl ([#20579](https://github.com/apache/superset/issues/20579)) ([290b89c](https://github.com/apache/superset/commit/290b89c7b4ae702c55f611bfac9cedb245ea8bd8))
|
||||
- **deck.gl:** add color range for deck.gl 3D ([#19520](https://github.com/apache/superset/issues/19520)) ([c0a00fd](https://github.com/apache/superset/commit/c0a00fd302ec66fbe0ca766cf73978c99ba00d82))
|
||||
- **deckgl-map:** use an arbitraty Mabpox style URL ([#26027](https://github.com/apache/superset/issues/26027)) ([#26031](https://github.com/apache/superset/issues/26031)) ([af58784](https://github.com/apache/superset/commit/af587840403d83a7da7fb0f57bc10ad2335d4eeb))
|
||||
- **explore:** Frontend implementation of dataset creation from infobox ([#19855](https://github.com/apache/superset/issues/19855)) ([ba0c37d](https://github.com/apache/superset/commit/ba0c37d3df85b1af39404af1d578daeb0ff2d278))
|
||||
- improve color consistency (save all labels) ([#19038](https://github.com/apache/superset/issues/19038)) ([dc57508](https://github.com/apache/superset/commit/dc575080d7e43d40b1734bb8f44fdc291cb95b11))
|
||||
- **legacy-preset-chart-deckgl:** Add ,.1f and ,.2f value formats to deckgl charts ([#18945](https://github.com/apache/superset/issues/18945)) ([c56dc8e](https://github.com/apache/superset/commit/c56dc8eace6a71b45240d1bb6768d75661052a2e))
|
||||
- make data tables support html ([#24368](https://github.com/apache/superset/issues/24368)) ([d2b0b8e](https://github.com/apache/superset/commit/d2b0b8eac52ad8b68639c6581a1ed174a593f564))
|
||||
- **viz picker:** Remove some tags, refactor Recommended section ([#27708](https://github.com/apache/superset/issues/27708)) ([c314999](https://github.com/apache/superset/commit/c3149994ac0d4392e0462421b62cd0c034142082))
|
||||
@@ -0,0 +1,57 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
## @superset-ui/legacy-preset-chart-deckgl
|
||||
|
||||
[](https://img.shields.io/npm/v/@superset-ui/legacy-preset-chart-deckgl.svg?style=flat-square)
|
||||
[](https://libraries.io/npm/@superset-ui%2Flegacy-preset-chart-deckgl)
|
||||
|
||||
This plugin provides `deck.gl` for Superset.
|
||||
|
||||
### Usage
|
||||
|
||||
Import the preset and register. This will register all the chart plugins under `deck.gl`.
|
||||
|
||||
```js
|
||||
import { DeckGLChartPreset } from '@superset-ui/legacy-preset-chart-deckgl';
|
||||
|
||||
new DeckGLChartPreset().register();
|
||||
```
|
||||
|
||||
or register charts one by one. 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 { ArcChartPlugin } from '@superset-ui/legacy-preset-chart-deckgl';
|
||||
|
||||
new ArcChartPlugin().configure({ key: 'deck_arc' }).register();
|
||||
```
|
||||
|
||||
Then use it via `SuperChart`. See [storybook](https://apache-superset.github.io/superset-ui-plugins-deckgl) for more details.
|
||||
|
||||
```js
|
||||
<SuperChart
|
||||
chartType="deck_arc"
|
||||
width={600}
|
||||
height={600}
|
||||
formData={...}
|
||||
queriesData={[{
|
||||
data: {...},
|
||||
}]}
|
||||
/>
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@superset-ui/preset-chart-deckgl",
|
||||
"version": "1.0.0",
|
||||
"description": "Superset Chart Plugin - deck.gl (MapLibre)",
|
||||
"name": "@superset-ui/legacy-preset-chart-deckgl",
|
||||
"version": "0.20.4",
|
||||
"description": "Superset Legacy Chart - deck.gl",
|
||||
"keywords": [
|
||||
"superset"
|
||||
],
|
||||
@@ -12,7 +12,7 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/apache/superset.git",
|
||||
"directory": "superset-frontend/plugins/preset-chart-deckgl"
|
||||
"directory": "superset-frontend/packages/legacy-preset-chart-deckgl"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"author": "Superset",
|
||||
@@ -29,13 +29,14 @@
|
||||
"@deck.gl/extensions": "~9.2.9",
|
||||
"@deck.gl/geo-layers": "~9.2.5",
|
||||
"@deck.gl/layers": "~9.2.5",
|
||||
"@deck.gl/mapbox": "~9.2.5",
|
||||
"@deck.gl/mesh-layers": "~9.2.5",
|
||||
"@deck.gl/react": "~9.2.11",
|
||||
"@luma.gl/constants": "~9.2.5",
|
||||
"@luma.gl/core": "~9.2.5",
|
||||
"@luma.gl/engine": "~9.2.6",
|
||||
"@luma.gl/shadertools": "~9.2.6",
|
||||
"@luma.gl/webgl": "~9.2.6",
|
||||
"@mapbox/tiny-sdf": "^2.0.7",
|
||||
"@mapbox/geojson-extent": "^1.0.1",
|
||||
"@math.gl/web-mercator": "^4.1.0",
|
||||
"@types/d3-array": "^3.2.2",
|
||||
@@ -46,12 +47,10 @@
|
||||
"d3-scale": "^4.0.2",
|
||||
"handlebars": "^4.7.9",
|
||||
"lodash": "^4.18.1",
|
||||
"maplibre-gl": "^5.0.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
"ngeohash": "^0.6.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-map-gl": "^8.0.0",
|
||||
"underscore": "^1.13.7",
|
||||
"underscore": "^1.13.8",
|
||||
"urijs": "^1.19.11",
|
||||
"xss": "^1.0.15"
|
||||
},
|
||||
@@ -66,14 +65,10 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"dayjs": "^1.11.19",
|
||||
"mapbox-gl": ">=1.0.0",
|
||||
"react": "^17.0.2 || ^19.0.0",
|
||||
"react-dom": "^17.0.2 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"mapbox-gl": {
|
||||
"optional": true
|
||||
}
|
||||
"mapbox-gl": "*",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-map-gl": "^6.1.19"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
import type { Layer } from '@deck.gl/core';
|
||||
import Legend from './components/Legend';
|
||||
import { hexToRGB } from './utils/colors';
|
||||
import { getMapboxApiKey } from './utils/mapbox';
|
||||
import sandboxedEval from './utils/sandbox';
|
||||
import fitViewport, { Viewport } from './utils/fitViewport';
|
||||
import {
|
||||
@@ -84,6 +83,7 @@ function getCategories(fd: QueryFormData, data: JsonObject[]) {
|
||||
export type CategoricalDeckGLContainerProps = {
|
||||
datasource: Datasource;
|
||||
formData: QueryFormData;
|
||||
mapboxApiKey: string;
|
||||
getPoints: (data: JsonObject[]) => Point[];
|
||||
height: number;
|
||||
width: number;
|
||||
@@ -155,7 +155,7 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
|
||||
|
||||
switch (selectedColorScheme) {
|
||||
case COLOR_SCHEME_TYPES.fixed_color: {
|
||||
color = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
|
||||
color = fd.color_picker || { r: 0, g: 0, b: 0, a: 100 };
|
||||
const colorArray = [color.r, color.g, color.b, color.a * 255];
|
||||
|
||||
return data.map(d => ({ ...d, color: colorArray }));
|
||||
@@ -166,7 +166,7 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
a: 1,
|
||||
a: 100,
|
||||
};
|
||||
const colorArray = [
|
||||
fallbackColor.r,
|
||||
@@ -325,15 +325,8 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
|
||||
viewport={viewport}
|
||||
layers={getLayers()}
|
||||
setControlValue={props.setControlValue}
|
||||
mapStyle={
|
||||
props.formData.map_renderer === 'mapbox'
|
||||
? props.formData.mapbox_style
|
||||
: props.formData.maplibre_style
|
||||
}
|
||||
mapProvider={
|
||||
props.formData.map_renderer === 'mapbox' ? 'mapbox' : 'maplibre'
|
||||
}
|
||||
mapboxApiKey={getMapboxApiKey()}
|
||||
mapStyle={props.formData.mapbox_style}
|
||||
mapboxApiAccessToken={props.mapboxApiKey}
|
||||
width={props.width}
|
||||
height={props.height}
|
||||
/>
|
||||
@@ -1,3 +1,7 @@
|
||||
/* eslint-disable react/jsx-sort-default-props */
|
||||
/* eslint-disable react/sort-prop-types */
|
||||
/* eslint-disable react/jsx-handler-names */
|
||||
/* eslint-disable react/forbid-prop-types */
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
@@ -26,32 +30,32 @@ import {
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
isValidElement,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
import { Map as MapLibreMap } from 'react-map-gl/maplibre';
|
||||
import { Map as MapboxMap } from 'react-map-gl/mapbox';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import { StaticMap } from 'react-map-gl';
|
||||
import DeckGL from '@deck.gl/react';
|
||||
import type { Layer } from '@deck.gl/core';
|
||||
import { JsonObject, JsonValue, usePrevious } from '@superset-ui/core';
|
||||
import { styled, useTheme } from '@apache-superset/core/theme';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import DeckGLOverlayMapLibre from './components/DeckGLOverlayMapLibre';
|
||||
import DeckGLOverlayMapbox from './components/DeckGLOverlayMapbox';
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
import { Device } from '@luma.gl/core';
|
||||
import Tooltip, { TooltipProps } from './components/Tooltip';
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import { Viewport } from './utils/fitViewport';
|
||||
import {
|
||||
MAPBOX_LAYER_PREFIX,
|
||||
OSM_LAYER_KEYWORDS,
|
||||
TILE_LAYER_PREFIX,
|
||||
buildTileLayer,
|
||||
} from './utils';
|
||||
|
||||
const TICK = 250; // milliseconds
|
||||
|
||||
const DEFAULT_MAP_STYLE =
|
||||
'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json';
|
||||
|
||||
export type DeckGLContainerProps = {
|
||||
viewport: Viewport;
|
||||
setControlValue?: (control: string, value: JsonValue) => void;
|
||||
mapStyle?: string;
|
||||
mapProvider?: 'maplibre' | 'mapbox';
|
||||
mapboxApiKey?: string;
|
||||
mapboxApiAccessToken: string;
|
||||
children?: ReactNode;
|
||||
width: number;
|
||||
height: number;
|
||||
@@ -65,6 +69,14 @@ export const DeckGLContainer = memo(
|
||||
const [lastUpdate, setLastUpdate] = useState<number | null>(null);
|
||||
const [viewState, setViewState] = useState(props.viewport);
|
||||
const prevViewport = usePrevious(props.viewport);
|
||||
const glContextRef = useRef<WebGL2RenderingContext | null>(null);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
glContextRef.current?.getExtension('WEBGL_lose_context')?.loseContext();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => ({ setTooltip }), []);
|
||||
|
||||
@@ -81,7 +93,7 @@ export const DeckGLContainer = memo(
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(tick, TICK);
|
||||
return () => clearInterval(timer);
|
||||
return clearInterval(timer);
|
||||
}, [tick]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -90,12 +102,31 @@ export const DeckGLContainer = memo(
|
||||
}
|
||||
}, [prevViewport, props.viewport]);
|
||||
|
||||
const onMove = useCallback((evt: { viewState: JsonObject }) => {
|
||||
setViewState(evt.viewState as Viewport);
|
||||
setLastUpdate(Date.now());
|
||||
}, []);
|
||||
const onViewStateChange = useCallback(
|
||||
({ viewState }: { viewState: JsonObject }) => {
|
||||
setViewState(viewState as Viewport);
|
||||
setLastUpdate(Date.now());
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const layers = useCallback(() => {
|
||||
if (
|
||||
(props.mapStyle?.startsWith(TILE_LAYER_PREFIX) ||
|
||||
OSM_LAYER_KEYWORDS.some((tilek: string) =>
|
||||
props.mapStyle?.includes(tilek),
|
||||
)) &&
|
||||
props.layers.some(
|
||||
l => typeof l !== 'function' && l?.id === 'tile-layer',
|
||||
) === false
|
||||
) {
|
||||
props.layers.unshift(
|
||||
buildTileLayer(
|
||||
(props.mapStyle ?? '').replace(TILE_LAYER_PREFIX, ''),
|
||||
'tile-layer',
|
||||
),
|
||||
);
|
||||
}
|
||||
// Support for layer factory
|
||||
if (props.layers.some(l => typeof l === 'function')) {
|
||||
return props.layers.map(l =>
|
||||
@@ -104,7 +135,7 @@ export const DeckGLContainer = memo(
|
||||
}
|
||||
|
||||
return props.layers as Layer[];
|
||||
}, [props.layers]);
|
||||
}, [props.layers, props.mapStyle]);
|
||||
|
||||
const isCustomTooltip = (content: ReactNode): boolean =>
|
||||
isValidElement(content) &&
|
||||
@@ -120,35 +151,7 @@ export const DeckGLContainer = memo(
|
||||
return <Tooltip tooltip={tooltipState} />;
|
||||
};
|
||||
|
||||
const theme = useTheme();
|
||||
const { children = null, height, width } = props;
|
||||
const isMapbox = props.mapProvider === 'mapbox';
|
||||
const mapStyle = props.mapStyle || DEFAULT_MAP_STYLE;
|
||||
|
||||
if (isMapbox && !props.mapboxApiKey) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 16,
|
||||
textAlign: 'center',
|
||||
color: theme.colorTextSecondary,
|
||||
}}
|
||||
>
|
||||
{t(
|
||||
'Mapbox requires a MAPBOX_API_KEY to be configured on the server.',
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isMapbox && props.mapboxApiKey) {
|
||||
mapboxgl.accessToken = props.mapboxApiKey;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -159,25 +162,28 @@ export const DeckGLContainer = memo(
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{isMapbox ? (
|
||||
<MapboxMap
|
||||
{...viewState}
|
||||
onMove={onMove}
|
||||
mapStyle={mapStyle}
|
||||
style={{ width, height }}
|
||||
>
|
||||
<DeckGLOverlayMapbox layers={layers()} />
|
||||
</MapboxMap>
|
||||
) : (
|
||||
<MapLibreMap
|
||||
{...viewState}
|
||||
onMove={onMove}
|
||||
mapStyle={mapStyle}
|
||||
style={{ width, height }}
|
||||
>
|
||||
<DeckGLOverlayMapLibre layers={layers()} />
|
||||
</MapLibreMap>
|
||||
)}
|
||||
<DeckGL
|
||||
controller
|
||||
width={width}
|
||||
height={height}
|
||||
layers={layers()}
|
||||
viewState={viewState}
|
||||
onViewStateChange={onViewStateChange}
|
||||
onAfterRender={(context: {
|
||||
device: Device;
|
||||
gl: WebGL2RenderingContext;
|
||||
}) => {
|
||||
glContextRef.current = context.gl;
|
||||
}}
|
||||
>
|
||||
{props.mapStyle?.startsWith(MAPBOX_LAYER_PREFIX) && (
|
||||
<StaticMap
|
||||
preserveDrawingBuffer
|
||||
mapStyle={props.mapStyle || 'light'}
|
||||
mapboxApiAccessToken={props.mapboxApiAccessToken}
|
||||
/>
|
||||
)}
|
||||
</DeckGL>
|
||||
{children}
|
||||
</div>
|
||||
{renderTooltip(tooltip)}
|
||||
@@ -58,7 +58,7 @@ const baseMockProps = {
|
||||
viz_type: 'deck_multi',
|
||||
deck_slices: [1, 2],
|
||||
autozoom: false,
|
||||
map_style: 'mapbox://styles/mapbox/light-v9',
|
||||
mapbox_style: 'mapbox://styles/mapbox/light-v9',
|
||||
},
|
||||
payload: {
|
||||
data: {
|
||||
@@ -50,7 +50,6 @@ import {
|
||||
import { getExploreLongUrl } from '../utils/explore';
|
||||
import layerGenerators from '../layers';
|
||||
import fitViewport, { Viewport } from '../utils/fitViewport';
|
||||
import { getMapboxApiKey } from '../utils/mapbox';
|
||||
import { TooltipProps } from '../components/Tooltip';
|
||||
|
||||
import { getPoints as getPointsArc } from '../layers/Arc/Arc';
|
||||
@@ -378,7 +377,7 @@ const DeckMulti = (props: DeckMultiProps) => {
|
||||
);
|
||||
|
||||
if (deckSlicesChanged || visibilityFilterChanged) {
|
||||
loadLayers(formData, payload, visibleDeckLayersFromRedux);
|
||||
loadLayers(formData, payload, undefined);
|
||||
}
|
||||
}, [
|
||||
loadLayers,
|
||||
@@ -388,7 +387,7 @@ const DeckMulti = (props: DeckMultiProps) => {
|
||||
props,
|
||||
]);
|
||||
|
||||
const { formData, setControlValue, height, width } = props;
|
||||
const { payload, formData, setControlValue, height, width } = props;
|
||||
|
||||
const layers = useMemo(
|
||||
() =>
|
||||
@@ -402,15 +401,10 @@ const DeckMulti = (props: DeckMultiProps) => {
|
||||
<MultiWrapper height={height} width={width}>
|
||||
<DeckGLContainerStyledWrapper
|
||||
ref={containerRef}
|
||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
||||
viewport={viewport}
|
||||
layers={layers}
|
||||
mapStyle={
|
||||
formData.map_renderer === 'mapbox'
|
||||
? formData.mapbox_style
|
||||
: formData.maplibre_style
|
||||
}
|
||||
mapProvider={formData.map_renderer === 'mapbox' ? 'mapbox' : 'maplibre'}
|
||||
mapboxApiKey={getMapboxApiKey()}
|
||||
mapStyle={formData.mapbox_style}
|
||||
setControlValue={setControlValue}
|
||||
onViewportChange={setViewport}
|
||||
height={height}
|
||||
@@ -18,13 +18,7 @@
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { validateNonEmpty } from '@superset-ui/core';
|
||||
import {
|
||||
viewport,
|
||||
mapboxStyle,
|
||||
maplibreStyle,
|
||||
mapProvider,
|
||||
autozoom,
|
||||
} from '../utilities/Shared_DeckGL';
|
||||
import { viewport, mapboxStyle, autozoom } from '../utilities/Shared_DeckGL';
|
||||
|
||||
export default {
|
||||
controlPanelSections: [
|
||||
@@ -32,9 +26,7 @@ export default {
|
||||
label: t('Map'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[mapProvider],
|
||||
[mapboxStyle],
|
||||
[maplibreStyle],
|
||||
[viewport],
|
||||
[autozoom],
|
||||
[
|
||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 256 KiB After Width: | Height: | Size: 256 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 251 KiB After Width: | Height: | Size: 251 KiB |
@@ -113,14 +113,8 @@ const Legend = ({
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
toggleCategory(k);
|
||||
}}
|
||||
onDoubleClick={e => {
|
||||
e.preventDefault();
|
||||
showSingleCategory(k);
|
||||
}}
|
||||
onClick={() => toggleCategory(k)}
|
||||
onDoubleClick={() => showSingleCategory(k)}
|
||||
>
|
||||
<span style={style}>{icon}</span> {formatCategoryLabel(k)}
|
||||
</a>
|
||||
@@ -42,8 +42,8 @@ const StyledDiv = styled.div<{
|
||||
position: absolute;
|
||||
top: ${top}px;
|
||||
left: ${left}px;
|
||||
z-index: 9;
|
||||
pointer-events: none;
|
||||
zIndex: 9;
|
||||
pointerEvents: none;
|
||||
${
|
||||
variant === 'default'
|
||||
? `
|
||||
@@ -51,8 +51,8 @@ const StyledDiv = styled.div<{
|
||||
margin: ${theme.sizeUnit * 2}px;
|
||||
background: ${theme.colorBgElevated};
|
||||
color: ${theme.colorText};
|
||||
max-width: 300px;
|
||||
font-size: ${theme.fontSizeSM}px;
|
||||
maxWidth: 300px;
|
||||
fontSize: ${theme.fontSizeSM}px;
|
||||
border: 1px solid ${theme.colorBorder};
|
||||
border-radius: ${theme.borderRadius}px;
|
||||
box-shadow: ${theme.boxShadowSecondary};
|
||||
@@ -19,7 +19,6 @@
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
import type { Layer } from '@deck.gl/core';
|
||||
import { getMapboxApiKey } from './utils/mapbox';
|
||||
import {
|
||||
Datasource,
|
||||
QueryFormData,
|
||||
@@ -183,23 +182,16 @@ export function createDeckGLComponent(
|
||||
}
|
||||
}, [computeLayers, prevFormData, prevFilterState, prevPayload, props]);
|
||||
|
||||
const { formData, setControlValue, height, width } = props;
|
||||
const { formData, payload, setControlValue, height, width } = props;
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<DeckGLContainerStyledWrapper
|
||||
ref={containerRef}
|
||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
||||
viewport={viewport}
|
||||
layers={layers}
|
||||
mapStyle={
|
||||
formData.map_renderer === 'mapbox'
|
||||
? formData.mapbox_style
|
||||
: formData.maplibre_style
|
||||
}
|
||||
mapProvider={
|
||||
formData.map_renderer === 'mapbox' ? 'mapbox' : 'maplibre'
|
||||
}
|
||||
mapboxApiKey={getMapboxApiKey()}
|
||||
mapStyle={formData.mapbox_style}
|
||||
setControlValue={setControlValue}
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -240,6 +232,7 @@ export function createCategoricalDeckGLComponent(
|
||||
<CategoricalDeckGLContainer
|
||||
datasource={datasource}
|
||||
formData={formData}
|
||||
mapboxApiKey={payload.data.mapboxApiKey}
|
||||
setControlValue={setControlValue}
|
||||
viewport={viewport}
|
||||
getLayer={getLayer}
|
||||
@@ -39,8 +39,6 @@ import {
|
||||
legendPosition,
|
||||
viewport,
|
||||
mapboxStyle,
|
||||
maplibreStyle,
|
||||
mapProvider,
|
||||
tooltipContents,
|
||||
tooltipTemplate,
|
||||
deckGLCategoricalColor,
|
||||
@@ -88,12 +86,7 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
{
|
||||
label: t('Map'),
|
||||
controlSetRows: [
|
||||
[mapProvider],
|
||||
[mapboxStyle],
|
||||
[maplibreStyle],
|
||||
[autozoom, viewport],
|
||||
],
|
||||
controlSetRows: [[mapboxStyle], [autozoom, viewport]],
|
||||
},
|
||||
{
|
||||
label: t('Arc'),
|
||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
@@ -20,7 +20,7 @@
|
||||
/* eslint-disable sort-keys */
|
||||
/* eslint-disable no-magic-numbers */
|
||||
import { SuperChart } from '@superset-ui/core';
|
||||
import { ArcChartPlugin } from '@superset-ui/preset-chart-deckgl';
|
||||
import { ArcChartPlugin } from '@superset-ui/legacy-preset-chart-deckgl';
|
||||
import { withResizableChartDemo } from '@storybook-shared';
|
||||
import payload from './payload';
|
||||
import { dummyDatasource } from '@storybook-shared';
|
||||
@@ -28,7 +28,7 @@ import { dummyDatasource } from '@storybook-shared';
|
||||
new ArcChartPlugin().configure({ key: 'deck_arc' }).register();
|
||||
|
||||
export default {
|
||||
title: 'Chart Plugins/preset-chart-deckgl/ArcChartPlugin',
|
||||
title: 'Legacy Chart Plugins/legacy-preset-chart-deckgl/ArcChartPlugin',
|
||||
decorators: [withResizableChartDemo],
|
||||
args: {
|
||||
strokeWidth: 1,
|
||||
@@ -90,8 +90,7 @@ export const ArcChartViz = ({
|
||||
row_limit: 5000,
|
||||
filter_nulls: true,
|
||||
adhoc_filters: [],
|
||||
map_style:
|
||||
'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
|
||||
mapbox_style: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
viewport: {
|
||||
altitude: 1.5,
|
||||
bearing: 8.546256357301871,
|
||||
@@ -30,8 +30,6 @@ import {
|
||||
jsOnclickHref,
|
||||
jsTooltip,
|
||||
mapboxStyle,
|
||||
maplibreStyle,
|
||||
mapProvider,
|
||||
spatial,
|
||||
viewport,
|
||||
tooltipContents,
|
||||
@@ -57,9 +55,7 @@ const config: ControlPanelConfig = {
|
||||
label: t('Map'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[mapProvider],
|
||||
[mapboxStyle],
|
||||
[maplibreStyle],
|
||||
[autozoom, viewport],
|
||||
[
|
||||
{
|
||||
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 203 KiB After Width: | Height: | Size: 203 KiB |
@@ -24,10 +24,9 @@ import {
|
||||
computeGeoJsonIconOptionsFromFormData,
|
||||
} from './Geojson';
|
||||
|
||||
jest.mock('react-map-gl/maplibre', () => ({
|
||||
jest.mock('@deck.gl/react', () => ({
|
||||
__esModule: true,
|
||||
Map: () => null,
|
||||
useControl: () => null,
|
||||
default: () => null,
|
||||
}));
|
||||
|
||||
test('computeGeoJsonTextOptionsFromJsOutput returns an empty object for non-object input', () => {
|
||||
@@ -123,8 +123,6 @@ function setTooltipContent(o: JsonObject) {
|
||||
}
|
||||
|
||||
const getFillColor = (feature: JsonObject, filterStateValue: unknown[]) => {
|
||||
const baseColor = feature?.properties?.fillColor;
|
||||
|
||||
if (filterStateValue) {
|
||||
if (
|
||||
JSON.stringify(feature.geometry.coordinates) ===
|
||||
@@ -133,14 +131,11 @@ const getFillColor = (feature: JsonObject, filterStateValue: unknown[]) => {
|
||||
return HIGHLIGHT_COLOR_ARRAY;
|
||||
}
|
||||
|
||||
if (Array.isArray(baseColor) && baseColor.length >= 4) {
|
||||
return [baseColor[0], baseColor[1], baseColor[2], 125];
|
||||
}
|
||||
|
||||
return baseColor ?? HIGHLIGHT_COLOR_ARRAY;
|
||||
const fillColor = feature?.properties?.fillColor;
|
||||
fillColor[3] = 125;
|
||||
return fillColor;
|
||||
}
|
||||
|
||||
return baseColor;
|
||||
return feature?.properties?.fillColor;
|
||||
};
|
||||
const getLineColor = (feature: JsonObject) => feature?.properties?.strokeColor;
|
||||
|
||||
@@ -415,9 +410,10 @@ const DeckGLGeoJson = (props: DeckGLGeoJsonProps) => {
|
||||
return (
|
||||
<DeckGLContainerStyledWrapper
|
||||
ref={containerRef}
|
||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
||||
viewport={viewport}
|
||||
layers={[layer]}
|
||||
mapStyle={formData.map_style}
|
||||
mapStyle={formData.mapbox_style}
|
||||
setControlValue={setControlValue}
|
||||
height={height}
|
||||
width={width}
|
||||
@@ -37,8 +37,6 @@ import {
|
||||
extruded,
|
||||
viewport,
|
||||
mapboxStyle,
|
||||
maplibreStyle,
|
||||
mapProvider,
|
||||
autozoom,
|
||||
lineWidth,
|
||||
tooltipContents,
|
||||
@@ -81,12 +79,7 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
{
|
||||
label: t('Map'),
|
||||
controlSetRows: [
|
||||
[mapProvider],
|
||||
[mapboxStyle],
|
||||
[maplibreStyle],
|
||||
[viewport, autozoom],
|
||||
],
|
||||
controlSetRows: [[mapboxStyle, viewport], [autozoom]],
|
||||
},
|
||||
{
|
||||
label: t('GeoJson Settings'),
|
||||
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 219 KiB After Width: | Height: | Size: 219 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
@@ -22,6 +22,7 @@ import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import example from './images/example.png';
|
||||
import exampleDark from './images/example-dark.png';
|
||||
import transformProps from '../../transformProps';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
@@ -34,6 +35,7 @@ const metadata = new ChartMetadata({
|
||||
name: t('deck.gl Geojson'),
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
tags: [t('deckGL'), t('2D')],
|
||||
behaviors: [Behavior.InteractiveChart],
|
||||
});
|
||||
@@ -42,10 +44,9 @@ export default class GeojsonChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./Geojson'),
|
||||
loadTransformProps: () => import('./transformProps'),
|
||||
loadBuildQuery: () => import('./buildQuery'),
|
||||
controlPanel,
|
||||
metadata,
|
||||
transformProps,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -34,8 +34,6 @@ import {
|
||||
viewport,
|
||||
spatial,
|
||||
mapboxStyle,
|
||||
maplibreStyle,
|
||||
mapProvider,
|
||||
tooltipContents,
|
||||
tooltipTemplate,
|
||||
legendPosition,
|
||||
@@ -61,9 +59,7 @@ const config: ControlPanelConfig = {
|
||||
{
|
||||
label: t('Map'),
|
||||
controlSetRows: [
|
||||
[mapProvider],
|
||||
[mapboxStyle],
|
||||
[maplibreStyle],
|
||||
[viewport],
|
||||
...generateDeckGLColorSchemeControls({
|
||||
defaultSchemeType: COLOR_SCHEME_TYPES.categorical_palette,
|
||||
|
Before Width: | Height: | Size: 366 KiB After Width: | Height: | Size: 366 KiB |
|
Before Width: | Height: | Size: 369 KiB After Width: | Height: | Size: 369 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 448 KiB After Width: | Height: | Size: 448 KiB |
@@ -20,14 +20,14 @@
|
||||
/* eslint-disable sort-keys */
|
||||
/* eslint-disable no-magic-numbers */
|
||||
import { SuperChart } from '@superset-ui/core';
|
||||
import { GridChartPlugin } from '@superset-ui/preset-chart-deckgl';
|
||||
import { GridChartPlugin } from '@superset-ui/legacy-preset-chart-deckgl';
|
||||
import { withResizableChartDemo, dummyDatasource } from '@storybook-shared';
|
||||
import payload from './payload';
|
||||
|
||||
new GridChartPlugin().configure({ key: 'deck_grid' }).register();
|
||||
|
||||
export default {
|
||||
title: 'Chart Plugins/preset-chart-deckgl/GridChartPlugin',
|
||||
title: 'Legacy Chart Plugins/legacy-preset-chart-deckgl/GridChartPlugin',
|
||||
decorators: [withResizableChartDemo],
|
||||
args: {
|
||||
gridSize: 120,
|
||||
@@ -79,8 +79,7 @@ export const GridChartViz = ({
|
||||
row_limit: 5000,
|
||||
filter_nulls: true,
|
||||
adhoc_filters: [],
|
||||
map_style:
|
||||
'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
|
||||
mapbox_style: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
viewport: {
|
||||
bearing: 155.80099696026355,
|
||||
latitude: 37.7942314882596,
|
||||