mirror of
https://github.com/apache/superset.git
synced 2024-09-12 00:29:39 -04:00
feat: make polygon support geojson feature and fix autozoom (#11)
* feat: support standard geojson feature in polygon * fix: viewport autozoom * fix: type * fix: lint * refactor: renames * fix: travis * build: add yarn.lock * fix: travis * fix: error message * fix: storybook * fix: improt * fix: address comments * fix: storybook * fix: remove yarn.lock * refactor: viewport * fix: extension * fix: extension
This commit is contained in:
parent
1a93f58550
commit
940e449bbe
@ -50,7 +50,7 @@ webpack.config.js
|
||||
# Lock files, libs should not have lock files
|
||||
npm-shrinkwrap.json
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
old-yarn.lock
|
||||
.*.swp
|
||||
_gh-pages
|
||||
_gh-pages
|
||||
# Now only allow yarn.lock
|
||||
|
@ -26,6 +26,7 @@
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/d3-array": "^2.0.0",
|
||||
"bootstrap-slider": "^10.0.0",
|
||||
"d3-array": "^1.2.4",
|
||||
"d3-color": "^1.2.0",
|
||||
@ -41,7 +42,7 @@
|
||||
"react-map-gl": "^4.0.10",
|
||||
"underscore": "^1.8.3",
|
||||
"urijs": "^1.18.10",
|
||||
"viewport-mercator-project": "^6.1.1"
|
||||
"@math.gl/web-mercator": "^3.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart": "^0.12.0",
|
||||
|
@ -32,7 +32,8 @@ import Legend from './components/Legend';
|
||||
import { hexToRGB } from './utils/colors';
|
||||
import { getPlaySliderParams } from './utils/time';
|
||||
import sandboxedEval from './utils/sandbox';
|
||||
import { fitViewport } from './layers/common';
|
||||
// eslint-disable-next-line import/extensions
|
||||
import fitViewport from './utils/fitViewport';
|
||||
|
||||
const { getScale } = CategoricalColorNamespace;
|
||||
|
||||
@ -119,9 +120,15 @@ export default class CategoricalDeckGLContainer extends React.PureComponent {
|
||||
|
||||
const { start, end, getStep, values, disabled } = getPlaySliderParams(timestamps, granularity);
|
||||
|
||||
const viewport = props.formData.autozoom
|
||||
? fitViewport(props.viewport, props.getPoints(features))
|
||||
: props.viewport;
|
||||
const { width, height, formData } = props;
|
||||
let { viewport } = props;
|
||||
if (formData.autozoom) {
|
||||
viewport = fitViewport(viewport, {
|
||||
width,
|
||||
height,
|
||||
points: props.getPoints(features),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
start,
|
||||
|
@ -26,7 +26,8 @@ import { isEqual } from 'lodash';
|
||||
|
||||
import DeckGLContainer from './DeckGLContainer';
|
||||
import CategoricalDeckGLContainer from './CategoricalDeckGLContainer';
|
||||
import { fitViewport } from './layers/common';
|
||||
// eslint-disable-next-line import/extensions
|
||||
import fitViewport from './utils/fitViewport';
|
||||
|
||||
const propTypes = {
|
||||
formData: PropTypes.object.isRequired,
|
||||
@ -48,10 +49,17 @@ export function createDeckGLComponent(getLayer, getPoints) {
|
||||
class Component extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const originalViewport = props.viewport;
|
||||
const viewport = props.formData.autozoom
|
||||
? fitViewport(originalViewport, getPoints(props.payload.data.features))
|
||||
: originalViewport;
|
||||
|
||||
const { width, height, formData } = props;
|
||||
let { viewport } = props;
|
||||
if (formData.autozoom) {
|
||||
viewport = fitViewport(viewport, {
|
||||
width,
|
||||
height,
|
||||
points: getPoints(props.payload.data.features),
|
||||
});
|
||||
}
|
||||
|
||||
this.state = {
|
||||
viewport,
|
||||
layer: this.computeLayer(props),
|
||||
|
@ -31,16 +31,16 @@ import Legend from '../../components/Legend';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { getBuckets, getBreakPointColorScaler } from '../../utils';
|
||||
|
||||
import { commonLayerProps, fitViewport } from '../common';
|
||||
import { commonLayerProps } from '../common';
|
||||
import { getPlaySliderParams } from '../../utils/time';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
// eslint-disable-next-line import/extensions
|
||||
import getPointsFromPolygon from '../../utils/getPointsFromPolygon';
|
||||
// eslint-disable-next-line import/extensions
|
||||
import fitViewport from '../../utils/fitViewport';
|
||||
|
||||
const DOUBLE_CLICK_TRESHOLD = 250; // milliseconds
|
||||
|
||||
function getPoints(features) {
|
||||
return features.flatMap(d => d.polygon);
|
||||
}
|
||||
|
||||
function getElevation(d, colorScaler) {
|
||||
/* in deck.gl 5.3.4 (used in Superset as of 2018-10-24), if a polygon has
|
||||
* opacity zero it will make everything behind it have opacity zero,
|
||||
@ -114,7 +114,7 @@ export function getLayer(formData, payload, onAddFilter, setTooltip, selected, o
|
||||
pickable: true,
|
||||
filled: fd.filled,
|
||||
stroked: fd.stroked,
|
||||
getPolygon: d => d.polygon,
|
||||
getPolygon: getPointsFromPolygon,
|
||||
getFillColor: colorScaler,
|
||||
getLineColor: [sc.r, sc.g, sc.b, 255 * sc.a],
|
||||
getLineWidth: fd.line_width,
|
||||
@ -154,26 +154,32 @@ class DeckGLPolygon extends React.Component {
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
const { width, height, formData, payload } = props;
|
||||
|
||||
// the state is computed only from the payload; if it hasn't changed, do
|
||||
// not recompute state since this would reset selections and/or the play
|
||||
// slider position due to changes in form controls
|
||||
if (state && props.payload.form_data === state.formData) {
|
||||
if (state && payload.form_data === state.formData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const features = props.payload.data.features || [];
|
||||
const features = payload.data.features || [];
|
||||
const timestamps = features.map(f => f.__timestamp);
|
||||
|
||||
// the granularity has to be read from the payload form_data, not the
|
||||
// props formData which comes from the instantaneous controls state
|
||||
const granularity =
|
||||
props.payload.form_data.time_grain_sqla || props.payload.form_data.granularity || 'P1D';
|
||||
const granularity = payload.form_data.time_grain_sqla || payload.form_data.granularity || 'P1D';
|
||||
|
||||
const { start, end, getStep, values, disabled } = getPlaySliderParams(timestamps, granularity);
|
||||
|
||||
const viewport = props.formData.autozoom
|
||||
? fitViewport(props.viewport, getPoints(features))
|
||||
: props.viewport;
|
||||
let { viewport } = props;
|
||||
if (formData.autozoom) {
|
||||
viewport = fitViewport(viewport, {
|
||||
width,
|
||||
height,
|
||||
points: features.flatMap(getPointsFromPolygon),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
start,
|
||||
@ -184,7 +190,7 @@ class DeckGLPolygon extends React.Component {
|
||||
viewport,
|
||||
selected: [],
|
||||
lastClick: 0,
|
||||
formData: props.payload.form_data,
|
||||
formData: payload.form_data,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -27,8 +27,10 @@ import { t } from '@superset-ui/translation';
|
||||
import AnimatableDeckGLContainer from '../../AnimatableDeckGLContainer';
|
||||
import { getPlaySliderParams } from '../../utils/time';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { commonLayerProps, fitViewport } from '../common';
|
||||
import { commonLayerProps } from '../common';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
// eslint-disable-next-line import/extensions
|
||||
import fitViewport from '../../utils/fitViewport';
|
||||
|
||||
function getPoints(data) {
|
||||
return data.map(d => d.position);
|
||||
@ -123,10 +125,16 @@ class DeckGLScreenGrid extends React.PureComponent {
|
||||
props.payload.form_data.time_grain_sqla || props.payload.form_data.granularity || 'P1D';
|
||||
|
||||
const { start, end, getStep, values, disabled } = getPlaySliderParams(timestamps, granularity);
|
||||
const { width, height, formData } = props;
|
||||
|
||||
const viewport = props.formData.autozoom
|
||||
? fitViewport(props.viewport, getPoints(features))
|
||||
: props.viewport;
|
||||
let { viewport } = props;
|
||||
if (formData.autozoom) {
|
||||
viewport = fitViewport(viewport, {
|
||||
width,
|
||||
height,
|
||||
points: getPoints(features),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
start,
|
||||
|
@ -16,77 +16,9 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { fitBounds } from 'viewport-mercator-project';
|
||||
import * as d3array from 'd3-array';
|
||||
import sandboxedEval from '../utils/sandbox';
|
||||
|
||||
const PADDING = 0.25;
|
||||
const GEO_BOUNDS = {
|
||||
LAT_MAX: 90,
|
||||
LAT_MIN: -90,
|
||||
LNG_MAX: 180,
|
||||
LNG_MIN: -180,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the latitude bounds if latitude is a single coordinate
|
||||
* @param latExt Latitude range
|
||||
*/
|
||||
function getLatBoundsForSingleCoordinate(latExt) {
|
||||
const latMin =
|
||||
latExt[0] - PADDING < GEO_BOUNDS.LAT_MIN ? GEO_BOUNDS.LAT_MIN : latExt[0] - PADDING;
|
||||
const latMax =
|
||||
latExt[1] + PADDING > GEO_BOUNDS.LAT_MAX ? GEO_BOUNDS.LAT_MAX : latExt[1] + PADDING;
|
||||
|
||||
return [latMin, latMax];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the longitude bounds if longitude is a single coordinate
|
||||
* @param lngExt Longitude range
|
||||
*/
|
||||
function getLngBoundsForSingleCoordinate(lngExt) {
|
||||
const lngMin =
|
||||
lngExt[0] - PADDING < GEO_BOUNDS.LNG_MIN ? GEO_BOUNDS.LNG_MIN : lngExt[0] - PADDING;
|
||||
const lngMax =
|
||||
lngExt[1] + PADDING > GEO_BOUNDS.LNG_MAX ? GEO_BOUNDS.LNG_MAX : lngExt[1] + PADDING;
|
||||
|
||||
return [lngMin, lngMax];
|
||||
}
|
||||
|
||||
export function getBounds(points) {
|
||||
const latExt = d3array.extent(points, d => d[1]);
|
||||
const lngExt = d3array.extent(points, d => d[0]);
|
||||
const latBounds = latExt[0] === latExt[1] ? getLatBoundsForSingleCoordinate(latExt) : latExt;
|
||||
const lngBounds = lngExt[0] === lngExt[1] ? getLngBoundsForSingleCoordinate(lngExt) : lngExt;
|
||||
|
||||
return [
|
||||
[lngBounds[0], latBounds[0]],
|
||||
[lngBounds[1], latBounds[1]],
|
||||
];
|
||||
}
|
||||
|
||||
export function fitViewport(viewport, points, padding = 10) {
|
||||
try {
|
||||
const bounds = getBounds(points);
|
||||
|
||||
return {
|
||||
...viewport,
|
||||
...fitBounds({
|
||||
bounds,
|
||||
height: viewport.height,
|
||||
padding,
|
||||
width: viewport.width,
|
||||
}),
|
||||
};
|
||||
} catch (error) {
|
||||
/* eslint no-console: 0 */
|
||||
console.error('Could not auto zoom', error);
|
||||
|
||||
return viewport;
|
||||
}
|
||||
}
|
||||
|
||||
export function commonLayerProps(formData, setTooltip, setTooltipContent, onSelect) {
|
||||
const fd = formData;
|
||||
let onHover;
|
||||
|
@ -0,0 +1,23 @@
|
||||
import { extent as d3Extent } from 'd3-array';
|
||||
import { Point, Range } from './types';
|
||||
|
||||
const LAT_LIMIT: Range = [-90, 90];
|
||||
const LNG_LIMIT: Range = [-180, 180];
|
||||
|
||||
/**
|
||||
* Expand a coordinate range by `padding` and within limits, if needed
|
||||
*/
|
||||
function expandIfNeeded([curMin, curMax]: Range, [minBound, maxBound]: Range, padding = 0.25) {
|
||||
return curMin < curMax
|
||||
? [curMin, curMax]
|
||||
: [Math.max(minBound, curMin - padding), Math.min(maxBound, curMax + padding)];
|
||||
}
|
||||
|
||||
export default function computeBoundsFromPoints(points: Point[]) {
|
||||
const latBounds = expandIfNeeded(d3Extent(points, (x: Point) => x[1]) as Range, LAT_LIMIT);
|
||||
const lngBounds = expandIfNeeded(d3Extent(points, (x: Point) => x[0]) as Range, LNG_LIMIT);
|
||||
return [
|
||||
[lngBounds[0], latBounds[0]],
|
||||
[lngBounds[1], latBounds[1]],
|
||||
];
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import { fitBounds } from '@math.gl/web-mercator';
|
||||
import computeBoundsFromPoints from './computeBoundsFromPoints';
|
||||
import { Point } from './types';
|
||||
|
||||
type Viewport = {
|
||||
longtitude: number;
|
||||
latitude: number;
|
||||
zoom: number;
|
||||
bearing?: number;
|
||||
pitch?: number;
|
||||
};
|
||||
|
||||
type FitViewportOptions = {
|
||||
points: Point[];
|
||||
width: number;
|
||||
height: number;
|
||||
minExtent?: number;
|
||||
maxZoom?: number;
|
||||
offset?: [number, number];
|
||||
padding?: number;
|
||||
};
|
||||
|
||||
export default function fitViewport(
|
||||
originalViewPort: Viewport,
|
||||
{ points, width, height, minExtent, maxZoom, offset, padding = 20 }: FitViewportOptions,
|
||||
) {
|
||||
const { bearing, pitch } = originalViewPort;
|
||||
const bounds = computeBoundsFromPoints(points);
|
||||
|
||||
try {
|
||||
return {
|
||||
...fitBounds({
|
||||
bounds,
|
||||
width,
|
||||
height,
|
||||
minExtent,
|
||||
maxZoom,
|
||||
offset,
|
||||
padding,
|
||||
}),
|
||||
bearing,
|
||||
pitch,
|
||||
};
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Could not fit viewport', error);
|
||||
}
|
||||
|
||||
return originalViewPort;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { Point } from './types';
|
||||
|
||||
/** Format originally used by the Polygon plugin */
|
||||
type CustomPolygonFeature = {
|
||||
polygon: Point[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Format that is geojson standard
|
||||
* https://geojson.org/geojson-spec.html
|
||||
*/
|
||||
type GeojsonPolygonFeature = {
|
||||
polygon: {
|
||||
type: 'Feature';
|
||||
geometry: {
|
||||
type: 'Polygon';
|
||||
coordinates: Point[][];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export default function getPointsFromPolygon(
|
||||
feature: CustomPolygonFeature | GeojsonPolygonFeature,
|
||||
) {
|
||||
return 'geometry' in feature.polygon ? feature.polygon.geometry.coordinates[0] : feature.polygon;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
// range and point actually have different value ranges
|
||||
// and also are different concept-wise
|
||||
|
||||
export type Range = [number, number];
|
||||
export type Point = [number, number];
|
@ -0,0 +1,38 @@
|
||||
import getPointsFromPolygon from '../../src/utils/getPointsFromPolygon';
|
||||
|
||||
describe('getPointsFromPolygon', () => {
|
||||
it('handle original input', () => {
|
||||
expect(
|
||||
getPointsFromPolygon({
|
||||
polygon: [
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
],
|
||||
}),
|
||||
).toEqual([
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
]);
|
||||
});
|
||||
it('handle geojson features', () => {
|
||||
expect(
|
||||
getPointsFromPolygon({
|
||||
polygon: {
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Polygon',
|
||||
coordinates: [
|
||||
[
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toEqual([
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
]);
|
||||
});
|
||||
});
|
@ -0,0 +1 @@
|
||||
declare module '@math.gl/web-mercator';
|
@ -3,6 +3,7 @@
|
||||
import React from 'react';
|
||||
import { SuperChart } from '@superset-ui/chart';
|
||||
import payload from './payload';
|
||||
import geojsonPayload from './geojsonPayload';
|
||||
import dummyDatasource from '../../../shared/dummyDatasource';
|
||||
|
||||
export default [
|
||||
@ -72,4 +73,52 @@ export default [
|
||||
storyName: 'Basic',
|
||||
storyPath: 'legacy-|preset-chart-deckgl|PolygonChartPlugin',
|
||||
},
|
||||
{
|
||||
renderStory: () => (
|
||||
<SuperChart
|
||||
chartType="deck_polygon"
|
||||
width={400}
|
||||
height={400}
|
||||
datasource={dummyDatasource}
|
||||
queryData={geojsonPayload}
|
||||
formData={{
|
||||
datasource: '9__table',
|
||||
viz_type: 'deck_polygon',
|
||||
time_range: '+:+',
|
||||
line_column: 'contour',
|
||||
line_type: 'json',
|
||||
adhoc_filters: [],
|
||||
metric: 'count',
|
||||
point_radius_fixed: { type: 'fix', value: 1000 },
|
||||
row_limit: 10000,
|
||||
reverse_long_lat: false,
|
||||
filter_nulls: true,
|
||||
mapbox_style: 'mapbox://styles/mapbox/light-v9',
|
||||
viewport: {
|
||||
longitude: 6.85236157047845,
|
||||
latitude: 31.222656842808707,
|
||||
zoom: 1,
|
||||
bearing: 0,
|
||||
pitch: 0,
|
||||
},
|
||||
autozoom: true,
|
||||
fill_color_picker: { a: 1, b: 73, g: 65, r: 3 },
|
||||
stroke_color_picker: { a: 1, b: 135, g: 122, r: 0 },
|
||||
filled: true,
|
||||
stroked: false,
|
||||
extruded: true,
|
||||
multiplier: 1,
|
||||
line_width: 10,
|
||||
linear_color_scheme: 'blue_white_yellow',
|
||||
opacity: 80,
|
||||
num_buckets: 5,
|
||||
table_filter: false,
|
||||
toggle_polygons: true,
|
||||
legend_position: 'tr',
|
||||
}}
|
||||
/>
|
||||
),
|
||||
storyName: 'Single Polygon in geojson format',
|
||||
storyPath: 'legacy-|preset-chart-deckgl|PolygonChartPlugin',
|
||||
},
|
||||
];
|
||||
|
@ -0,0 +1,112 @@
|
||||
export default {
|
||||
cache_key: '31946c4488d1899827d283b668d83281',
|
||||
cached_dttm: '2020-03-04T22:40:59',
|
||||
cache_timeout: 129600,
|
||||
error: null,
|
||||
form_data: {
|
||||
datasource: '93829__table',
|
||||
viz_type: 'deck_polygon',
|
||||
url_params: {},
|
||||
time_range_endpoints: ['inclusive', 'exclusive'],
|
||||
granularity_sqla: null,
|
||||
time_range: '100 years ago : ',
|
||||
line_column: 'geometry',
|
||||
line_type: 'json',
|
||||
adhoc_filters: [
|
||||
{
|
||||
clause: 'WHERE',
|
||||
expressionType: 'SIMPLE',
|
||||
filterOptionName: '4ea1a468-43f9-45f0-9655-576d2ab04bc1',
|
||||
comparator: '',
|
||||
operator: 'IS NOT NULL',
|
||||
subject: 'geometry',
|
||||
},
|
||||
],
|
||||
metric: 'count',
|
||||
point_radius_fixed: {
|
||||
type: 'fix',
|
||||
value: 1000,
|
||||
},
|
||||
row_limit: 1000,
|
||||
reverse_long_lat: false,
|
||||
filter_nulls: true,
|
||||
mapbox_style: 'mapbox://styles/mapbox/light-v9',
|
||||
viewport: {
|
||||
longitude: 6.85236157047845,
|
||||
latitude: 31.222656842808707,
|
||||
zoom: 1,
|
||||
bearing: 0,
|
||||
pitch: 0,
|
||||
},
|
||||
autozoom: true,
|
||||
fill_color_picker: {
|
||||
r: 0,
|
||||
g: 122,
|
||||
b: 135,
|
||||
a: 1,
|
||||
},
|
||||
stroke_color_picker: {
|
||||
r: 0,
|
||||
g: 122,
|
||||
b: 135,
|
||||
a: 1,
|
||||
},
|
||||
filled: true,
|
||||
stroked: false,
|
||||
extruded: true,
|
||||
multiplier: 1,
|
||||
line_width: 10,
|
||||
linear_color_scheme: 'blue_white_yellow',
|
||||
opacity: 80,
|
||||
num_buckets: 5,
|
||||
table_filter: false,
|
||||
toggle_polygons: true,
|
||||
legend_position: 'tr',
|
||||
legend_format: null,
|
||||
js_columns: [],
|
||||
where: '',
|
||||
having: '',
|
||||
having_filters: [],
|
||||
filters: [
|
||||
{
|
||||
col: 'geometry',
|
||||
op: 'IS NOT NULL',
|
||||
val: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
is_cached: true,
|
||||
status: 'success',
|
||||
stacktrace: null,
|
||||
rowcount: 1,
|
||||
data: {
|
||||
features: [
|
||||
{
|
||||
count: 1,
|
||||
polygon: {
|
||||
type: 'Feature',
|
||||
properties: {},
|
||||
geometry: {
|
||||
type: 'Polygon',
|
||||
coordinates: [
|
||||
[
|
||||
[-149.95132113447022, 61.1310423022678],
|
||||
[-149.95386039742468, 61.12975642234931],
|
||||
[-149.9529189574033, 61.128200857856946],
|
||||
[-149.94943860572158, 61.12793112735274],
|
||||
[-149.9468993514573, 61.12921688848983],
|
||||
[-149.94784044016444, 61.1307724989074],
|
||||
[-149.95132113447022, 61.1310423022678],
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
__timestamp: null,
|
||||
elevation: 0,
|
||||
},
|
||||
],
|
||||
mapboxApiKey:
|
||||
'pk.eyJ1IjoiZ2tlZWUiLCJhIjoiY2lvbmN5dXhtMDA4NXRybTJjZWU2ZHVxOSJ9.CJG_6Oz52y5yI5cr3Ct_aQ',
|
||||
metricLabels: ['count'],
|
||||
},
|
||||
};
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user