/** * 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/sort-prop-types */ import d3 from 'd3'; import PropTypes from 'prop-types'; import { extent as d3Extent } from 'd3-array'; import { getSequentialSchemeRegistry, CategoricalColorNamespace, } from '@superset-ui/core'; import Datamap from 'datamaps/dist/datamaps.all.min'; import { ColorBy } from './utils'; const propTypes = { data: PropTypes.arrayOf( PropTypes.shape({ country: PropTypes.string, code: PropTypes.string, latitude: PropTypes.number, longitude: PropTypes.number, name: PropTypes.string, m1: PropTypes.number, m2: PropTypes.number, }), ), height: PropTypes.number, maxBubbleSize: PropTypes.number, showBubbles: PropTypes.bool, linearColorScheme: PropTypes.string, color: PropTypes.string, colorScheme: PropTypes.string, setDataMask: PropTypes.func, onContextMenu: PropTypes.func, emitCrossFilters: PropTypes.bool, formatter: PropTypes.object, }; function WorldMap(element, props) { const { countryFieldtype, entity, data, width, height, maxBubbleSize, showBubbles, linearColorScheme, color, colorBy, colorScheme, sliceId, theme, onContextMenu, setDataMask, inContextMenu, filterState, emitCrossFilters, formatter, } = props; const div = d3.select(element); div.classed('superset-legacy-chart-world-map', true); div.selectAll('*').remove(); // Ignore XXX's to get better normalization const filteredData = data.filter(d => d.country && d.country !== 'XXX'); const extRadius = d3.extent(filteredData, d => Math.sqrt(d.m2)); const radiusScale = d3.scale .linear() .domain([extRadius[0], extRadius[1]]) .range([1, maxBubbleSize]); let processedData; let colorFn; if (colorBy === ColorBy.Country) { colorFn = CategoricalColorNamespace.getScale(colorScheme); processedData = filteredData.map(d => ({ ...d, radius: radiusScale(Math.sqrt(d.m2)), fillColor: colorFn(d.name, sliceId), })); } else { colorFn = getSequentialSchemeRegistry() .get(linearColorScheme) .createLinearScale(d3Extent(filteredData, d => d.m1)); processedData = filteredData.map(d => ({ ...d, radius: radiusScale(Math.sqrt(d.m2)), fillColor: colorFn(d.m1), })); } const mapData = {}; processedData.forEach(d => { mapData[d.country] = d; }); const getCrossFilterDataMask = source => { const selected = Object.values(filterState.selectedValues || {}); const key = source.id || source.country; const country = countryFieldtype === 'name' ? mapData[key]?.name : mapData[key]?.code; if (!country) { return undefined; } let values; if (selected.includes(key)) { values = []; } else { values = [country]; } return { dataMask: { extraFormData: { filters: values.length ? [ { col: entity, op: 'IN', val: values, }, ] : [], }, filterState: { value: values.length ? values : null, selectedValues: values.length ? [key] : null, }, }, isCurrentValueSelected: selected.includes(key), }; }; const handleClick = source => { if (!emitCrossFilters) { return; } const pointerEvent = d3.event; pointerEvent.preventDefault(); getCrossFilterDataMask(source); const dataMask = getCrossFilterDataMask(source)?.dataMask; if (dataMask) { setDataMask(dataMask); } }; const handleContextMenu = source => { const pointerEvent = d3.event; pointerEvent.preventDefault(); const key = source.id || source.country; const val = countryFieldtype === 'name' ? mapData[key]?.name : mapData[key]?.code; let drillToDetailFilters; let drillByFilters; if (val) { drillToDetailFilters = [ { col: entity, op: '==', val, formattedVal: val, }, ]; drillByFilters = [ { col: entity, op: '==', val, }, ]; } onContextMenu(pointerEvent.clientX, pointerEvent.clientY, { drillToDetail: drillToDetailFilters, crossFilter: getCrossFilterDataMask(source), drillBy: { filters: drillByFilters, groupbyFieldName: 'entity' }, }); }; const map = new Datamap({ element, width, height, data: processedData, fills: { defaultFill: theme.colorBorder, }, geographyConfig: { popupOnHover: !inContextMenu, highlightOnHover: !inContextMenu, borderWidth: 1, borderColor: theme.colorSplit, highlightBorderColor: theme.colorIcon, highlightFillColor: color, highlightBorderWidth: 1, popupTemplate: (geo, d) => `