diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index decf989512e..a13ee1d0e02 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -5333,7 +5333,6 @@ "resolved": "https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-5.2.7.tgz", "integrity": "sha512-tnB9NNund9TwIym8/7DMJe573nlPEQb+fKUV5GL8TBYXjIhDvL0D7mgmNVNQUPhXp+R7RylQeiBdkA4EbOHPGQ==", "license": "OFL-1.1", - "peer": true, "funding": { "url": "https://github.com/sponsors/ayuhito" } @@ -10199,85 +10198,6 @@ "node": ">= 10" } }, - "node_modules/@octokit/auth-token": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.4.tgz", - "integrity": "sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/core": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.2.4.tgz", - "integrity": "sha512-rYKilwgzQ7/imScn3M9/pFfUf4I1AZEH3KhyJmtPdE2zfaXAn2mFfUy4FbKewzc2We5y/LlKLj36fWJLKC2SIQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@octokit/auth-token": "^3.0.0", - "@octokit/graphql": "^5.0.0", - "@octokit/request": "^6.0.0", - "@octokit/request-error": "^3.0.0", - "@octokit/types": "^9.0.0", - "before-after-hook": "^2.2.0", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/endpoint": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.6.tgz", - "integrity": "sha512-5L4fseVRUsDFGR00tMWD/Trdeeihn999rTMGRMC1G/Ldi1uWlWJzI98H4Iak5DB/RVvQuyMYKqSK/R6mbSOQyg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/endpoint/node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@octokit/graphql": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.6.tgz", - "integrity": "sha512-Fxyxdy/JH0MnIB5h+UQ3yCoh1FG4kWXfFKkpWqjZHw/p+Kc8Y44Hu/kCgNBT6nU1shNumEchmW/sUO1JuQnPcw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@octokit/request": "^6.0.0", - "@octokit/types": "^9.0.0", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/@octokit/openapi-types": { "version": "18.1.1", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", @@ -10355,55 +10275,6 @@ "@octokit/openapi-types": "^18.0.0" } }, - "node_modules/@octokit/request": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.8.tgz", - "integrity": "sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@octokit/endpoint": "^7.0.0", - "@octokit/request-error": "^3.0.0", - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/request-error": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz", - "integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@octokit/types": "^9.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/request/node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@octokit/rest": { "version": "20.1.2", "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.1.2.tgz", @@ -19568,8 +19439,7 @@ "version": "1.43.4", "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.43.4.tgz", "integrity": "sha512-8hAxVfo2ImICd69BWlZwZlxe9rxDGDjuUhh+WeWgGDvfBCE+r3lkynkQvIovDz4jcMi8O7bsEaFygaDT+h9sBA==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/acorn": { "version": "7.4.1", @@ -24326,24 +24196,6 @@ "integrity": "sha512-O/gRkjWULp3xVX8K85V0H3tsSGole0WYt77KVpGZO2xTGLuVFuvE6JIsIli3fvFHCYBhGFn/8OHEEyMYF+QehA==", "license": "MIT" }, - "node_modules/date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/runtime": "^7.21.0" - }, - "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" - } - }, "node_modules/dateformat": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.2.tgz", @@ -64956,15 +64808,6 @@ "tinycolor2": "*" } }, - "packages/superset-ui-core/node_modules/@fontsource/fira-code": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-5.2.6.tgz", - "integrity": "sha512-wCkIpPm0BqlkCPLYeY4Vui96ODmVUV0/GpEe3OfJ4v8EJn/BF2SlyxvarFsTs1CKiGjrO2cXlIZbBrKi9F+hUQ==", - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" - } - }, "packages/superset-ui-core/node_modules/@types/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", @@ -64988,12 +64831,6 @@ "dev": true, "license": "MIT" }, - "packages/superset-ui-core/node_modules/ace-builds": { - "version": "1.43.3", - "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.43.3.tgz", - "integrity": "sha512-MCl9rALmXwIty/4Qboijo/yNysx1r6hBTzG+6n/TiOm5LFhZpEvEIcIITPFiEOEFDfgBOEmxu+a4f54LEFM6Sg==", - "license": "BSD-3-Clause" - }, "packages/superset-ui-core/node_modules/d3-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz", diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/package.json b/superset-frontend/plugins/legacy-plugin-chart-country-map/package.json index a0613181941..e12bc0137ec 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/package.json +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/package.json @@ -31,9 +31,9 @@ "prop-types": "^15.8.1" }, "peerDependencies": { + "@apache-superset/core": "*", "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", - "@apache-superset/core": "*", "react": "^17.0.2" } } diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.css b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.css deleted file mode 100644 index f8234e0ec41..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.css +++ /dev/null @@ -1,61 +0,0 @@ -/** - * 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-legacy-chart-country-map svg { - background-color: #feffff; -} - -.superset-legacy-chart-country-map { - position: relative; -} - -.superset-legacy-chart-country-map .background { - fill: rgba(255, 255, 255, 0); - pointer-events: all; -} - -.superset-legacy-chart-country-map .map-layer { - fill: #fff; - stroke: #aaa; -} - -.superset-legacy-chart-country-map .effect-layer { - pointer-events: none; -} - -.superset-legacy-chart-country-map .text-layer { - color: #333333; - text-anchor: middle; - pointer-events: none; -} - -.superset-legacy-chart-country-map text.result-text { - font-weight: 300; - font-size: 24px; -} - -.superset-legacy-chart-country-map text.big-text { - font-weight: 700; - font-size: 16px; -} - -.superset-legacy-chart-country-map path.region { - cursor: pointer; - stroke: #eee; -} diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js index b1cf5016dcf..e8e14d6a44c 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js @@ -90,15 +90,7 @@ function CountryMap(element, props) { .attr('height', height); const g = svg.append('g'); const mapLayer = g.append('g').classed('map-layer', true); - const textLayer = g - .append('g') - .classed('text-layer', true) - .attr('transform', `translate(${width / 2}, 45)`); - const bigText = textLayer.append('text').classed('big-text', true); - const resultText = textLayer - .append('text') - .classed('result-text', true) - .attr('dy', '1em'); + const hoverPopup = div.append('div').attr('class', 'hover-popup'); let centered; @@ -128,45 +120,18 @@ function CountryMap(element, props) { 'transform', `translate(${halfWidth},${halfHeight})scale(${k})translate(${-x},${-y})`, ); - textLayer - .style('opacity', 0) - .attr( - 'transform', - `translate(0,0)translate(${x},${hasCenter ? y - 5 : 45})`, - ) - .transition() - .duration(750) - .style('opacity', 1); - bigText - .transition() - .duration(750) - .style('font-size', hasCenter ? 6 : 16); - resultText - .transition() - .duration(750) - .style('font-size', hasCenter ? 16 : 24); }; backgroundRect.on('click', clicked); - const selectAndDisplayNameOfRegion = function selectAndDisplayNameOfRegion( - feature, - ) { - let name = ''; + const getNameOfRegion = function getNameOfRegion(feature) { if (feature && feature.properties) { if (feature.properties.ID_2) { - name = feature.properties.NAME_2; - } else { - name = feature.properties.NAME_1; + return feature.properties.NAME_2; } + return feature.properties.NAME_1; } - bigText.text(name); - }; - - const updateMetrics = function updateMetrics(region) { - if (region.length > 0) { - resultText.text(format(region[0].metric)); - } + return ''; }; const mouseenter = function mouseenter(d) { @@ -176,17 +141,31 @@ function CountryMap(element, props) { c = d3.rgb(c).darker().toString(); } d3.select(this).style('fill', c); - selectAndDisplayNameOfRegion(d); + // Display information popup const result = data.filter( region => region.country_id === d.properties.ISO, ); - updateMetrics(result); + + const position = d3.mouse(svg.node()); + hoverPopup + .style('display', 'block') + .style('top', `${position[1] + 30}px`) + .style('left', `${position[0]}px`) + .html( + `
${getNameOfRegion(d)}
${result.length > 0 ? format(result[0].metric) : ''}
`, + ); + }; + + const mousemove = function mousemove() { + const position = d3.mouse(svg.node()); + hoverPopup + .style('top', `${position[1] + 30}px`) + .style('left', `${position[0]}px`); }; const mouseout = function mouseout() { d3.select(this).style('fill', colorFn); - bigText.text(''); - resultText.text(''); + hoverPopup.style('display', 'none'); }; function drawMap(mapData) { @@ -225,6 +204,7 @@ function CountryMap(element, props) { .attr('vector-effect', 'non-scaling-stroke') .style('fill', colorFn) .on('mouseenter', mouseenter) + .on('mousemove', mousemove) .on('mouseout', mouseout) .on('click', clicked); } diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/ReactCountryMap.jsx b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/ReactCountryMap.jsx index 7a8ade2a642..aa937377982 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/ReactCountryMap.jsx +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/ReactCountryMap.jsx @@ -22,7 +22,7 @@ import Component from './CountryMap'; const ReactComponent = reactify(Component); -const CountryMap = ({ className, ...otherProps }) => ( +const CountryMap = ({ className = '', ...otherProps }) => (
@@ -43,33 +43,29 @@ export default styled(CountryMap)` pointer-events: all; } + .superset-legacy-chart-country-map .hover-popup { + position: absolute; + color: ${theme.colorTextSecondary}; + display: none; + padding: 4px; + border-radius: 1px; + background-color: ${theme.colorBgElevated}; + box-shadow: ${theme.boxShadow}; + font-size: 12px; + border: 1px solid ${theme.colorBorder}; + z-index: 10001; + } + .superset-legacy-chart-country-map .map-layer { fill: ${theme.colorBgContainer}; stroke: ${theme.colorBorderSecondary}; + pointer-events: all; } .superset-legacy-chart-country-map .effect-layer { pointer-events: none; } - .superset-legacy-chart-country-map .text-layer { - color: ${theme.colorText}; - text-anchor: middle; - pointer-events: none; - } - - .superset-legacy-chart-country-map text.result-text { - fill: ${theme.colorText}; - font-weight: ${theme.fontWeightLight}; - font-size: ${theme.fontSizeXL}px; - } - - .superset-legacy-chart-country-map text.big-text { - fill: ${theme.colorText}; - font-weight: ${theme.fontWeightStrong}; - font-size: ${theme.fontSizeLG}px; - } - .superset-legacy-chart-country-map path.region { cursor: pointer; stroke: ${theme.colorSplit}; diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/test/CountryMap.test.tsx b/superset-frontend/plugins/legacy-plugin-chart-country-map/test/CountryMap.test.tsx new file mode 100644 index 00000000000..b73c5831c71 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/test/CountryMap.test.tsx @@ -0,0 +1,159 @@ +/** + * 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 '@testing-library/jest-dom'; +import { render, fireEvent } from '@testing-library/react'; +import d3 from 'd3'; +import ReactCountryMap from '../src/ReactCountryMap'; + +jest.spyOn(d3, 'json'); + +type Projection = ((...args: unknown[]) => void) & { + scale: () => Projection; + center: () => Projection; + translate: () => Projection; +}; + +type PathFn = (() => string) & { + projection: jest.Mock; + bounds: jest.Mock<[[number, number], [number, number]]>; + centroid: jest.Mock<[number, number]>; +}; + +const mockPath: PathFn = jest.fn(() => 'M10 10 L20 20') as unknown as PathFn; +mockPath.projection = jest.fn(); +mockPath.bounds = jest.fn(() => [ + [0, 0], + [100, 100], +]); +mockPath.centroid = jest.fn(() => [50, 50]); + +jest.spyOn(d3.geo, 'path').mockImplementation(() => mockPath); + +// Mock d3.geo.mercator +jest.spyOn(d3.geo, 'mercator').mockImplementation(() => { + const proj = (() => {}) as Projection; + proj.scale = () => proj; + proj.center = () => proj; + proj.translate = () => proj; + return proj; +}); + +// Mock d3.mouse +jest.spyOn(d3, 'mouse').mockReturnValue([100, 50]); + +const mockMapData = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: { ISO: 'CAN', NAME_1: 'Canada' }, + geometry: {}, + }, + ], +}; + +type D3JsonCallback = (error: Error | null, data: unknown) => void; + +describe('CountryMap (legacy d3)', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders a map after d3.json loads data', async () => { + d3.json.mockImplementation((_url: string, cb: D3JsonCallback) => + cb(null, mockMapData), + ); + + render( + , + ); + + expect(d3.json).toHaveBeenCalledTimes(1); + + const region = document.querySelector('path.region'); + expect(region).not.toBeNull(); + }); + + it('shows tooltip on mouseenter/mousemove/mouseout', async () => { + d3.json.mockImplementation((_url: string, cb: D3JsonCallback) => + cb(null, mockMapData), + ); + + render( + , + ); + + const region = document.querySelector('path.region'); + expect(region).not.toBeNull(); + + const popup = document.querySelector('.hover-popup'); + expect(popup).not.toBeNull(); + + fireEvent.mouseEnter(region!); + expect(popup!).toHaveStyle({ display: 'block' }); + + fireEvent.mouseOut(region!); + expect(popup!).toHaveStyle({ display: 'none' }); + }); + + it('shows tooltip on mouseenter/mousemove/mouseout', async () => { + d3.json.mockImplementation((_url: string, cb: D3JsonCallback) => + cb(null, mockMapData), + ); + + render( + , + ); + + const region = document.querySelector('path.region'); + expect(region).not.toBeNull(); + + const popup = document.querySelector('.hover-popup'); + expect(popup).not.toBeNull(); + + fireEvent.mouseEnter(region!); + expect(popup!).toHaveStyle({ display: 'block' }); + + fireEvent.mouseOut(region!); + expect(popup!).toHaveStyle({ display: 'none' }); + }); +});