chore: [Map chart widget] Replace fusion charts with Echarts as chart provider (#31482)
# Description This pr replaces fusion chart lib with echarts in the map chart widget. It also checkins the necessary maps. #### PR fixes following issue(s) Fixes https://github.com/appsmithorg/appsmith/issues/31081 #### Type of change - Chore (housekeeping or task changes that don't impact user perception) ## Testing > #### How Has This Been Tested? > Please describe the tests that you ran to verify your changes. Also list any relevant details for your test configuration. > Delete anything that is not relevant - [x] Manual - [ ] JUnit - [ ] Jest - [ ] Cypress > > #### Test Plan > Add Testsmith test cases links that relate to this PR > > #### Issues raised during DP testing > Link issues raised during DP testing for better visiblity and tracking (copy link from comments dropped on this PR) > > > ## Checklist: #### Dev activity - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag #### QA activity: - [ ] [Speedbreak features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-) have been covered - [ ] Test plan covers all impacted features and [areas of interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-) - [ ] Test plan has been peer reviewed by project stakeholders and other QA members - [ ] Manually tested functionality on DP - [ ] We had an implementation alignment call with stakeholders post QA Round 2 - [ ] Cypress test cases have been added and approved by SDET/manual QA - [ ] Added `Test Plan Approved` label after Cypress tests were reviewed - [ ] Added `Test Plan Approved` label after JUnit tests were reviewed <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Updated the Widget interface to make link properties optional, enhancing flexibility. - Added a `retryPromise` function for improved error handling and retry logic in asynchronous operations. - Introduced new mapping data and utilities for the MapChartWidget, enabling detailed country/region information and dynamic map types. - **Enhancements** - Improved error handling in widget callouts to gracefully handle missing links. - Enhanced the MapChartWidget with new functionalities including dynamic map data loading, chart resizing, and skeleton UI for loading state. - **Refactor** - Refactored chart configurations and event handling in MapChartWidget for better performance and readability. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
583c478ab3
commit
1cce29987c
|
|
@ -79,6 +79,7 @@
|
|||
"@tanstack/virtual-core": "^3.0.0-beta.18",
|
||||
"@tinymce/tinymce-react": "^3.13.0",
|
||||
"@types/babel__standalone": "^7.1.7",
|
||||
"@types/d3-geo": "^3.1.0",
|
||||
"@types/google.maps": "^3.51.0",
|
||||
"@types/react-page-visibility": "^6.4.1",
|
||||
"@types/web": "^0.0.99",
|
||||
|
|
@ -106,6 +107,7 @@
|
|||
"craco-babel-loader": "^1.0.4",
|
||||
"cssnano": "^6.0.1",
|
||||
"cypress-log-to-output": "^1.1.2",
|
||||
"d3-geo": "^3.1.0",
|
||||
"dayjs": "^1.10.6",
|
||||
"deep-diff": "^1.0.2",
|
||||
"design-system": "npm:@appsmithorg/design-system@2.1.35",
|
||||
|
|
@ -118,7 +120,6 @@
|
|||
"focus-trap-react": "^8.9.2",
|
||||
"fuse.js": "^3.4.5",
|
||||
"fusioncharts": "^3.18.0",
|
||||
"fusionmaps": "^3.18.0",
|
||||
"graphql": "^16.8.1",
|
||||
"history": "^4.10.1",
|
||||
"http-proxy": "^1.18.1",
|
||||
|
|
@ -170,7 +171,6 @@
|
|||
"react-documents": "^1.0.4",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-full-screen": "^1.1.0",
|
||||
"react-fusioncharts": "^3.1.2",
|
||||
"react-google-recaptcha": "^2.1.0",
|
||||
"react-helmet": "^5.2.1",
|
||||
"react-hook-form": "^7.28.0",
|
||||
|
|
|
|||
1
app/client/public/static/maps/AFRICA.json
Normal file
1
app/client/public/static/maps/AFRICA.json
Normal file
File diff suppressed because one or more lines are too long
1
app/client/public/static/maps/ASIA.json
Normal file
1
app/client/public/static/maps/ASIA.json
Normal file
File diff suppressed because one or more lines are too long
1
app/client/public/static/maps/EUROPE.json
Normal file
1
app/client/public/static/maps/EUROPE.json
Normal file
File diff suppressed because one or more lines are too long
1
app/client/public/static/maps/NORTH_AMERICA.json
Normal file
1
app/client/public/static/maps/NORTH_AMERICA.json
Normal file
File diff suppressed because one or more lines are too long
1
app/client/public/static/maps/OCEANIA.json
Normal file
1
app/client/public/static/maps/OCEANIA.json
Normal file
File diff suppressed because one or more lines are too long
1
app/client/public/static/maps/SOURTH_AMERICA.json
Normal file
1
app/client/public/static/maps/SOURTH_AMERICA.json
Normal file
File diff suppressed because one or more lines are too long
1
app/client/public/static/maps/USA.json
Normal file
1
app/client/public/static/maps/USA.json
Normal file
File diff suppressed because one or more lines are too long
1
app/client/public/static/maps/WORLD.json
Normal file
1
app/client/public/static/maps/WORLD.json
Normal file
File diff suppressed because one or more lines are too long
1
app/client/public/static/maps/WORLD_WITH_ANTARCTICA.json
Normal file
1
app/client/public/static/maps/WORLD_WITH_ANTARCTICA.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -131,7 +131,7 @@ type GetEditorCallouts = (props: WidgetProps) => WidgetCallout[];
|
|||
|
||||
export interface WidgetCallout {
|
||||
message: string;
|
||||
links: [
|
||||
links?: [
|
||||
{
|
||||
text: string;
|
||||
url: string;
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ export function renderWidgetCallouts(props: WidgetProps): JSX.Element[] {
|
|||
if (getEditorCallouts) {
|
||||
const callouts: WidgetCallout[] = getEditorCallouts(props);
|
||||
return callouts.map((callout, index) => {
|
||||
const links = callout.links.map((link) => {
|
||||
const links = callout.links?.map((link) => {
|
||||
return {
|
||||
children: link.text,
|
||||
to: link.url,
|
||||
|
|
|
|||
|
|
@ -343,27 +343,41 @@ export function hexToRgb(hex: string): {
|
|||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Function to call the given function until the promise it returns resolves or the max retries are reached
|
||||
*
|
||||
* @param fn - function that returns a promise
|
||||
* @param retriesLeft - number of retries
|
||||
* @param interval - interval between retries
|
||||
* @param shouldRetry - function to determine if the promise should be retried, helpful when we want to retry only on specific errors
|
||||
* @returns Promise
|
||||
*
|
||||
*/
|
||||
export const retryPromise = async (
|
||||
fn: () => Promise<any>,
|
||||
retriesLeft = 5,
|
||||
interval = 1000,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
shouldRetry = (e: Error) => true, // default to retry on all errors
|
||||
): Promise<any> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fn()
|
||||
.then(resolve)
|
||||
.catch(() => {
|
||||
setTimeout(async () => {
|
||||
if (retriesLeft === 1) {
|
||||
return Promise.reject({
|
||||
code: ERROR_CODES.SERVER_ERROR,
|
||||
message: createMessage(ERROR_500),
|
||||
show: false,
|
||||
});
|
||||
}
|
||||
.catch((e) => {
|
||||
if (shouldRetry(e)) {
|
||||
setTimeout(async () => {
|
||||
if (retriesLeft === 1) {
|
||||
return Promise.reject({
|
||||
code: ERROR_CODES.SERVER_ERROR,
|
||||
message: createMessage(ERROR_500),
|
||||
show: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Passing on "reject" is the important part
|
||||
retryPromise(fn, retriesLeft - 1, interval).then(resolve, reject);
|
||||
}, interval);
|
||||
// Passing on "reject" is the important part
|
||||
retryPromise(fn, retriesLeft - 1, interval).then(resolve, reject);
|
||||
}, interval);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
export const CUSTOM_MAP_PLUGINS: Record<string, any> = {
|
||||
world: require(`fusionmaps/maps/fusioncharts.world.js`),
|
||||
worldwithantarctica: require(
|
||||
`fusionmaps/maps/fusioncharts.worldwithantarctica.js`,
|
||||
),
|
||||
europe: require(`fusionmaps/maps/fusioncharts.europe.js`),
|
||||
northamerica: require(`fusionmaps/maps/fusioncharts.northamerica.js`),
|
||||
southamerica: require(`fusionmaps/maps/fusioncharts.southamerica.js`),
|
||||
asia: require(`fusionmaps/maps/fusioncharts.asia.js`),
|
||||
oceania: require(`fusionmaps/maps/fusioncharts.oceania.js`),
|
||||
africa: require(`fusionmaps/maps/fusioncharts.africa.js`),
|
||||
usa: require(`fusionmaps/maps/fusioncharts.usa.js`),
|
||||
};
|
||||
|
||||
export const CUSTOM_MAP_TYPES = Object.keys(CUSTOM_MAP_PLUGINS).map(
|
||||
(each) => `maps/${each}`,
|
||||
);
|
||||
1161
app/client/src/widgets/MapChartWidget/component/countryDetails.ts
Normal file
1161
app/client/src/widgets/MapChartWidget/component/countryDetails.ts
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -1,27 +1,11 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
// Include the react-fusioncharts component
|
||||
import ReactFC from "react-fusioncharts";
|
||||
// Include the fusioncharts library
|
||||
import type { ChartObject } from "fusioncharts";
|
||||
import FusionCharts from "fusioncharts";
|
||||
|
||||
// Import FusionMaps
|
||||
import FusionMaps from "fusioncharts/fusioncharts.maps";
|
||||
import World from "fusioncharts/maps/fusioncharts.world";
|
||||
import USA from "fusioncharts/maps/fusioncharts.usa";
|
||||
|
||||
// Include the theme as fusion
|
||||
import FusionTheme from "fusioncharts/themes/fusioncharts.theme.fusion";
|
||||
|
||||
// Import the dataset and the colorRange of the map
|
||||
import type { MapColorObject } from "../constants";
|
||||
import { dataSetForWorld, MapTypes } from "../constants";
|
||||
import { CUSTOM_MAP_PLUGINS } from "../CustomMapConstants";
|
||||
import { Colors } from "constants/Colors";
|
||||
|
||||
// Adding the chart and theme as dependency to the core fusioncharts
|
||||
ReactFC.fcRoot(FusionCharts, FusionMaps, World, FusionTheme, USA);
|
||||
import type { MapColorObject, MapTypes } from "../constants";
|
||||
import type { MapData } from "./types";
|
||||
import { getChartOption, loadMap } from "./utilities";
|
||||
import * as echarts from "echarts";
|
||||
import countryDetails from "./countryDetails";
|
||||
import clsx from "clsx";
|
||||
|
||||
const MapChartContainer = styled.div<{
|
||||
borderRadius?: string;
|
||||
|
|
@ -40,217 +24,133 @@ const MapChartContainer = styled.div<{
|
|||
}
|
||||
`;
|
||||
|
||||
export interface MapData {
|
||||
value?: string;
|
||||
displayValue?: string;
|
||||
toolText?: string;
|
||||
color?: string;
|
||||
alpha?: number;
|
||||
link?: string;
|
||||
font?: string;
|
||||
fontSize?: string;
|
||||
fontColor?: string;
|
||||
fontBold?: boolean;
|
||||
showLabel?: boolean;
|
||||
showToolTip?: boolean;
|
||||
labelConnectorColor?: string;
|
||||
labelConnectorAlpha?: number;
|
||||
useHoverColor?: boolean;
|
||||
}
|
||||
export default function EchartComponent(props: MapChartComponentProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
export type MapType = keyof typeof MapTypes;
|
||||
const [key, setKey] = useState(0);
|
||||
|
||||
export interface EntityData {
|
||||
id: string;
|
||||
label: string;
|
||||
originalId: string;
|
||||
shortLabel: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
function MapChartComponent(props: MapChartComponentProps) {
|
||||
const { caption, colorRange, data, onDataPointClick, showLabels, type } =
|
||||
props;
|
||||
|
||||
const fontFamily =
|
||||
props.fontFamily === "System Default" ? "inherit" : props.fontFamily;
|
||||
props.fontFamily === "System Default" ? "" : props.fontFamily;
|
||||
|
||||
// Creating the JSON object to store the chart configurations
|
||||
const defaultChartConfigs: ChartObject = {
|
||||
type: "maps/world", // The chart type
|
||||
width: "100%", // Width of the chart
|
||||
height: "100%", // Height of the chart
|
||||
dataFormat: "json", // Data type
|
||||
dataSource: {
|
||||
// Map Configuration
|
||||
chart: {
|
||||
caption: "Average Annual Population Growth",
|
||||
includevalueinlabels: "1",
|
||||
labelsepchar: ": ",
|
||||
entityFillHoverColor: "#FFF9C4",
|
||||
theme: "fusion",
|
||||
const colorRangePieces = useMemo(() => {
|
||||
return colorRange.map((color) => {
|
||||
const alpha = color.alpha ?? 100;
|
||||
|
||||
// Caption
|
||||
captionFontSize: "24",
|
||||
captionAlignment: "center",
|
||||
captionPadding: "20",
|
||||
captionFontColor: Colors.THUNDER,
|
||||
captionFontBold: "1",
|
||||
return {
|
||||
min: color.minValue,
|
||||
max: color.maxValue,
|
||||
color: color.code,
|
||||
colorAlpha: alpha ? alpha / 100 : 0,
|
||||
label: color.displayValue,
|
||||
};
|
||||
});
|
||||
}, [colorRange]);
|
||||
|
||||
// Legend
|
||||
legendIconSides: "4",
|
||||
legendIconBgAlpha: "100",
|
||||
legendIconAlpha: "100",
|
||||
legendItemFont: fontFamily,
|
||||
legendPosition: "top",
|
||||
valueFont: fontFamily,
|
||||
const transformedData = useMemo(() => {
|
||||
return data.map((each) => ({
|
||||
name: each.id,
|
||||
value: each.value,
|
||||
}));
|
||||
}, [data]);
|
||||
|
||||
// Spacing
|
||||
chartLeftMargin: "10",
|
||||
chartTopMargin: "15",
|
||||
chartRightMargin: "10",
|
||||
chartBottomMargin: "10",
|
||||
const chartContainer = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Base Styling
|
||||
baseFont: fontFamily,
|
||||
bgColor: Colors.WHITE,
|
||||
},
|
||||
// Aesthetics; ranges synced with the slider
|
||||
colorRange: {
|
||||
gradient: "0",
|
||||
},
|
||||
// Source data as JSON --> id represents countries of the world.
|
||||
data: dataSetForWorld,
|
||||
},
|
||||
events: {},
|
||||
};
|
||||
|
||||
const [chartConfigs, setChartConfigs] = useState(defaultChartConfigs);
|
||||
const [chart, setChart] = useState(new FusionCharts(defaultChartConfigs));
|
||||
const chartInstance = useRef<echarts.ECharts | null>();
|
||||
|
||||
useEffect(() => {
|
||||
// Attach event handlers
|
||||
const newChartConfigs: any = {
|
||||
...chartConfigs,
|
||||
chartInstance.current = echarts.init(
|
||||
chartContainer.current!,
|
||||
{},
|
||||
{
|
||||
width: props.width,
|
||||
height: props.height,
|
||||
},
|
||||
);
|
||||
}, [chartContainer]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (event: any) => {
|
||||
const id = event.data.name;
|
||||
|
||||
const regionDetail = countryDetails[type][id];
|
||||
|
||||
onDataPointClick({
|
||||
value: parseFloat(event.data.value),
|
||||
label: regionDetail.label,
|
||||
shortLabel: regionDetail.short_label,
|
||||
originalId: id,
|
||||
id: id.toLowerCase(),
|
||||
});
|
||||
};
|
||||
newChartConfigs["events"]["entityClick"] = onDataPointClick;
|
||||
setChartConfigs(newChartConfigs);
|
||||
|
||||
chartInstance.current?.on("click", "series", handler);
|
||||
|
||||
return () => {
|
||||
chart.removeEventListener("entityClick", onDataPointClick);
|
||||
chartInstance.current?.off("click", handler);
|
||||
};
|
||||
}, [onDataPointClick]);
|
||||
}, [onDataPointClick, chartInstance.current, type]);
|
||||
|
||||
useEffect(() => {
|
||||
const newChartConfigs: any = {
|
||||
...chartConfigs,
|
||||
};
|
||||
const fontFamily =
|
||||
props.fontFamily === "System Default" ? "inherit" : props.fontFamily;
|
||||
setIsLoading(true);
|
||||
|
||||
newChartConfigs["dataSource"]["chart"]["legendItemFont"] = fontFamily;
|
||||
newChartConfigs["dataSource"]["chart"]["valueFont"] = fontFamily;
|
||||
newChartConfigs["dataSource"]["chart"]["baseFont"] = fontFamily;
|
||||
|
||||
setChartConfigs(newChartConfigs);
|
||||
}, [props.fontFamily]);
|
||||
loadMap(type).then(() => {
|
||||
setIsLoading(false);
|
||||
// we are using Math.random to force the chart to set options again
|
||||
// to avoid the race conditions while loading maps
|
||||
setKey(Math.random());
|
||||
});
|
||||
}, [type]);
|
||||
|
||||
useEffect(() => {
|
||||
const newChartConfigs: any = {
|
||||
...chartConfigs,
|
||||
};
|
||||
newChartConfigs["dataSource"]["chart"]["caption"] = caption;
|
||||
setChartConfigs(newChartConfigs);
|
||||
}, [caption]);
|
||||
|
||||
useEffect(() => {
|
||||
const targetValue = showLabels ? "1" : "0";
|
||||
|
||||
const newChartConfigs: any = {
|
||||
...chartConfigs,
|
||||
};
|
||||
newChartConfigs["dataSource"]["chart"]["showLabels"] = targetValue;
|
||||
setChartConfigs(newChartConfigs);
|
||||
}, [showLabels]);
|
||||
|
||||
useEffect(() => {
|
||||
const newChartConfigs: any = {
|
||||
...chartConfigs,
|
||||
};
|
||||
newChartConfigs["dataSource"]["colorRange"]["color"] = colorRange;
|
||||
chart.setChartData(newChartConfigs.dataSource, "json");
|
||||
}, [JSON.stringify(colorRange)]);
|
||||
|
||||
useEffect(() => {
|
||||
const newChartConfigs = {
|
||||
...chartConfigs,
|
||||
dataSource: {
|
||||
...(chartConfigs.dataSource || {}),
|
||||
data,
|
||||
},
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case MapTypes.WORLD_WITH_ANTARCTICA:
|
||||
newChartConfigs.type = "maps/worldwithantarctica";
|
||||
break;
|
||||
case MapTypes.EUROPE:
|
||||
newChartConfigs.type = "maps/europe";
|
||||
break;
|
||||
case MapTypes.NORTH_AMERICA:
|
||||
newChartConfigs.type = "maps/northamerica";
|
||||
break;
|
||||
case MapTypes.SOURTH_AMERICA:
|
||||
newChartConfigs.type = "maps/southamerica";
|
||||
break;
|
||||
case MapTypes.ASIA:
|
||||
newChartConfigs.type = "maps/asia";
|
||||
break;
|
||||
case MapTypes.OCEANIA:
|
||||
newChartConfigs.type = "maps/oceania";
|
||||
break;
|
||||
case MapTypes.AFRICA:
|
||||
newChartConfigs.type = "maps/africa";
|
||||
break;
|
||||
case MapTypes.USA:
|
||||
newChartConfigs.type = "maps/usa";
|
||||
break;
|
||||
|
||||
default:
|
||||
newChartConfigs.type = "maps/world";
|
||||
break;
|
||||
if (!isLoading && !!echarts.getMap(type)) {
|
||||
chartInstance.current?.setOption(
|
||||
getChartOption(
|
||||
caption,
|
||||
showLabels,
|
||||
colorRangePieces,
|
||||
transformedData,
|
||||
type,
|
||||
props.height,
|
||||
props.width,
|
||||
fontFamily,
|
||||
),
|
||||
true,
|
||||
);
|
||||
}
|
||||
}, [
|
||||
isLoading,
|
||||
caption,
|
||||
showLabels,
|
||||
colorRangePieces,
|
||||
transformedData,
|
||||
fontFamily,
|
||||
chartInstance.current,
|
||||
type,
|
||||
key,
|
||||
props.height,
|
||||
props.width,
|
||||
]);
|
||||
|
||||
if (type === MapTypes.WORLD) {
|
||||
setChartConfigs(newChartConfigs);
|
||||
return;
|
||||
}
|
||||
|
||||
initializeMap(newChartConfigs);
|
||||
}, [JSON.stringify(data), type]);
|
||||
|
||||
// Called by FC-React component to return the rendered chart
|
||||
const renderComplete = (chart: FusionCharts.FusionCharts) => {
|
||||
setChart(chart);
|
||||
};
|
||||
|
||||
const initializeMap = (configs: ChartObject) => {
|
||||
const { type: mapType } = configs;
|
||||
if (mapType) {
|
||||
const alias = mapType.substring(5);
|
||||
const mapDefinition = CUSTOM_MAP_PLUGINS[alias];
|
||||
ReactFC.fcRoot(FusionCharts, FusionMaps, mapDefinition, FusionTheme);
|
||||
setChartConfigs(configs);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
chartInstance.current?.resize({
|
||||
width: props.width,
|
||||
height: props.height,
|
||||
});
|
||||
}, [props.width, props.height]);
|
||||
|
||||
return (
|
||||
<MapChartContainer
|
||||
borderRadius={props.borderRadius}
|
||||
boxShadow={props.boxShadow}
|
||||
className={clsx({
|
||||
"bp3-skeleton": isLoading,
|
||||
})}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ReactFC {...chartConfigs} onRender={renderComplete} />
|
||||
<div ref={chartContainer} />
|
||||
</MapChartContainer>
|
||||
);
|
||||
}
|
||||
|
|
@ -262,10 +162,10 @@ export interface MapChartComponentProps {
|
|||
isVisible: boolean;
|
||||
onDataPointClick: (evt: any) => void;
|
||||
showLabels: boolean;
|
||||
type: MapType;
|
||||
type: MapTypes;
|
||||
borderRadius?: string;
|
||||
boxShadow?: string;
|
||||
fontFamily?: string;
|
||||
height: number;
|
||||
width: number;
|
||||
}
|
||||
|
||||
export default MapChartComponent;
|
||||
|
|
|
|||
8
app/client/src/widgets/MapChartWidget/component/types.ts
Normal file
8
app/client/src/widgets/MapChartWidget/component/types.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import type { MapTypes } from "widgets/MapChartWidget/constants";
|
||||
|
||||
export interface MapData {
|
||||
id: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export type MapType = keyof typeof MapTypes;
|
||||
286
app/client/src/widgets/MapChartWidget/component/utilities.ts
Normal file
286
app/client/src/widgets/MapChartWidget/component/utilities.ts
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
import * as echarts from "echarts";
|
||||
import countryDetails from "./countryDetails";
|
||||
import { MapTypes } from "../constants";
|
||||
import { geoAlbers, geoAzimuthalEqualArea, geoMercator } from "d3-geo";
|
||||
import log from "loglevel";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { retryPromise } from "utils/AppsmithUtils";
|
||||
|
||||
interface GeoSpecialAreas {
|
||||
[areaName: string]: {
|
||||
left: number;
|
||||
top: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function getSpecialAreas(map: MapTypes): GeoSpecialAreas {
|
||||
switch (map) {
|
||||
case MapTypes.USA:
|
||||
return {
|
||||
AK: {
|
||||
left: -131,
|
||||
top: 25,
|
||||
width: 15,
|
||||
},
|
||||
HI: {
|
||||
left: -110,
|
||||
top: 25,
|
||||
width: 5,
|
||||
},
|
||||
PR: {
|
||||
left: -76,
|
||||
top: 26,
|
||||
width: 2,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Function to load the map geojson file and register it with echarts
|
||||
*/
|
||||
export const loadMap = (() => {
|
||||
let abortController: AbortController | null = null;
|
||||
|
||||
return async (type: MapTypes) => {
|
||||
if (!echarts.getMap(type)) {
|
||||
if (abortController && abortController.abort) {
|
||||
abortController.abort();
|
||||
}
|
||||
|
||||
if (AbortController) {
|
||||
abortController = new AbortController();
|
||||
}
|
||||
|
||||
return retryPromise(
|
||||
async () => {
|
||||
return fetch(`/static/maps/${type}.json`, {
|
||||
signal: abortController?.signal,
|
||||
});
|
||||
},
|
||||
3,
|
||||
0,
|
||||
(error: any) => error.code !== 20,
|
||||
)
|
||||
.then(
|
||||
(response) => response.json(),
|
||||
(error) => {
|
||||
abortController = null;
|
||||
|
||||
if (error.code !== 20) {
|
||||
log.error({ error });
|
||||
Sentry.captureException(error);
|
||||
}
|
||||
},
|
||||
)
|
||||
.then((geoJson) => {
|
||||
abortController = null;
|
||||
echarts.registerMap(type, geoJson, getSpecialAreas(type));
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
function getProjection(type: string) {
|
||||
switch (type) {
|
||||
case "OCEANIA":
|
||||
return geoAzimuthalEqualArea()
|
||||
.scale(242)
|
||||
.center([22, -39])
|
||||
.translate([480, 319])
|
||||
.rotate([-165, 18, 0]);
|
||||
case "ASIA":
|
||||
return geoAzimuthalEqualArea()
|
||||
.scale(400)
|
||||
.center([-17, -26])
|
||||
.translate([480, 303])
|
||||
.rotate([-103, -55, 7]);
|
||||
case "AFRICA":
|
||||
return geoMercator()
|
||||
.scale(278)
|
||||
.center([110, 4])
|
||||
.translate([936, 288])
|
||||
.rotate([180, 180, 180]);
|
||||
case "SOURTH_AMERICA":
|
||||
return geoAlbers()
|
||||
.scale(363)
|
||||
.center([-15, 24])
|
||||
.translate([450, 271])
|
||||
.rotate([51, 55, 9]);
|
||||
case "NORTH_AMERICA":
|
||||
return geoAzimuthalEqualArea()
|
||||
.scale(400)
|
||||
.center([13, 25])
|
||||
.translate([539, 415])
|
||||
.rotate([-84, 174, -180]);
|
||||
}
|
||||
}
|
||||
|
||||
export function getPositionOffset(
|
||||
type: MapTypes,
|
||||
width: number,
|
||||
height: number,
|
||||
) {
|
||||
switch (type) {
|
||||
case MapTypes.SOURTH_AMERICA:
|
||||
case MapTypes.NORTH_AMERICA:
|
||||
case MapTypes.AFRICA:
|
||||
return {
|
||||
layoutSize: Math.min(width, height - 130),
|
||||
layoutCenter: ["50%", height / 2 + 30],
|
||||
};
|
||||
case MapTypes.ASIA:
|
||||
return {
|
||||
layoutSize: Math.min(width, height - 65),
|
||||
layoutCenter: ["50%", height / 2 + 30],
|
||||
};
|
||||
case MapTypes.EUROPE:
|
||||
case MapTypes.USA:
|
||||
return {
|
||||
layoutSize: Math.min(width, height),
|
||||
layoutCenter: ["50%", height / 2 + 30],
|
||||
};
|
||||
case MapTypes.WORLD:
|
||||
case MapTypes.WORLD_WITH_ANTARCTICA:
|
||||
return {
|
||||
layoutSize: Math.min(width, height),
|
||||
layoutCenter: ["50%", height / 2 + 40],
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export const getChartOption = (
|
||||
caption: string,
|
||||
showLabel: boolean,
|
||||
colorRangePieces: {
|
||||
min: number;
|
||||
max: number;
|
||||
color: string;
|
||||
}[],
|
||||
data: { name: string; value: number }[],
|
||||
type: MapTypes,
|
||||
height: number,
|
||||
width: number,
|
||||
fontFamily?: string,
|
||||
) => {
|
||||
const projection = getProjection(type);
|
||||
|
||||
let projectionConfig = {};
|
||||
|
||||
if (projection) {
|
||||
projectionConfig = {
|
||||
projection: {
|
||||
project: (point: [number, number]) => projection(point),
|
||||
unproject: (point: [number, number]) => projection.invert?.(point),
|
||||
stream: projection.stream,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: {
|
||||
text: caption,
|
||||
left: "center",
|
||||
top: "12px",
|
||||
textStyle: {
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
fontFamily,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
showDelay: 0,
|
||||
transitionDuration: 0.2,
|
||||
},
|
||||
visualMap: {
|
||||
show: true,
|
||||
type: "piecewise",
|
||||
pieces: colorRangePieces,
|
||||
formatter: (min: number, max: number) => {
|
||||
return `${min}-${max}`;
|
||||
},
|
||||
orient: "horizontal",
|
||||
top: "68px",
|
||||
left: "center",
|
||||
itemWidth: 12,
|
||||
itemHeight: 12,
|
||||
textStyle: {
|
||||
fontSize: 14,
|
||||
color: "#4c5664",
|
||||
overflow: "break",
|
||||
},
|
||||
itemSymbol: "rect",
|
||||
},
|
||||
toolbox: {
|
||||
show: false,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: "map",
|
||||
...getPositionOffset(type, width, height),
|
||||
roam: true,
|
||||
map: type,
|
||||
itemStyle: {
|
||||
borderColor: "#ccc",
|
||||
areaColor: "#aeaeae",
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
areaColor: "#FFF9C4",
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
borderColor: "#ccc",
|
||||
padding: [4, 8],
|
||||
fontFamily,
|
||||
formatter: (d: any) => {
|
||||
const key = d?.name as string;
|
||||
const label = countryDetails[type][key]?.["label"];
|
||||
return `${label}, ${d.data?.value || "-"}`;
|
||||
},
|
||||
textStyle: {
|
||||
fontSize: 14,
|
||||
color: "#4c5664",
|
||||
},
|
||||
extraCssText: "border-radius: 0;",
|
||||
},
|
||||
label: {
|
||||
show: showLabel,
|
||||
fontFamily,
|
||||
position: "top",
|
||||
formatter: (d: any) => {
|
||||
const key = d?.name as string;
|
||||
const label = countryDetails[type][key]["short_label"];
|
||||
return `${label}: ${d.data?.value || "-"}`;
|
||||
},
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: "#4c5664",
|
||||
},
|
||||
},
|
||||
scaleLimit: {
|
||||
min: 1,
|
||||
max: 4,
|
||||
},
|
||||
labelLayout: () => {
|
||||
return {
|
||||
hideOverlap: true,
|
||||
};
|
||||
},
|
||||
nameProperty: "id",
|
||||
data,
|
||||
...projectionConfig,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
import React, { lazy, Suspense } from "react";
|
||||
|
||||
import Skeleton from "components/utils/Skeleton";
|
||||
import React, { Suspense, lazy } from "react";
|
||||
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
|
||||
import { ValidationTypes } from "constants/WidgetValidation";
|
||||
import type { SetterConfig, Stylesheet } from "entities/AppTheming";
|
||||
|
|
@ -9,7 +7,6 @@ import { retryPromise } from "utils/AppsmithUtils";
|
|||
import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType";
|
||||
import type { WidgetProps, WidgetState } from "widgets/BaseWidget";
|
||||
import BaseWidget from "widgets/BaseWidget";
|
||||
import type { MapType } from "../component";
|
||||
import type { MapColorObject } from "../constants";
|
||||
import {
|
||||
dataSetForAfrica,
|
||||
|
|
@ -27,6 +24,7 @@ import { DefaultAutocompleteDefinitions } from "widgets/WidgetUtils";
|
|||
import type {
|
||||
AnvilConfig,
|
||||
AutocompletionDefinitions,
|
||||
WidgetCallout,
|
||||
} from "WidgetProvider/constants";
|
||||
import { FILL_WIDGET_MIN_WIDTH } from "constants/minWidthConstants";
|
||||
import {
|
||||
|
|
@ -35,6 +33,8 @@ import {
|
|||
} from "layoutSystems/common/utils/constants";
|
||||
import IconSVG from "../icon.svg";
|
||||
import { WIDGET_TAGS } from "constants/WidgetConstants";
|
||||
import Skeleton from "components/utils/Skeleton";
|
||||
import type { MapType } from "../component/types";
|
||||
|
||||
const MapChartComponent = lazy(async () =>
|
||||
retryPromise(
|
||||
|
|
@ -329,6 +329,15 @@ class MapChartWidget extends BaseWidget<MapChartWidgetProps, WidgetState> {
|
|||
isJSConvertible: true,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: true,
|
||||
additionalAutoComplete: () => ({
|
||||
selectedDataPoint: {
|
||||
value: 1.1,
|
||||
label: "",
|
||||
shortLabel: "",
|
||||
originalId: "",
|
||||
id: "",
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -448,15 +457,31 @@ class MapChartWidget extends BaseWidget<MapChartWidgetProps, WidgetState> {
|
|||
};
|
||||
}
|
||||
|
||||
handleDataPointClick = (evt: any) => {
|
||||
static getMethods() {
|
||||
return {
|
||||
getEditorCallouts(): WidgetCallout[] {
|
||||
return [
|
||||
{
|
||||
message:
|
||||
"Map chart widget switched from using Fusion chart library to Echarts. Please verify that the chart is displaying properly",
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
handleDataPointClick = (data: any) => {
|
||||
const { onDataPointClick } = this.props;
|
||||
|
||||
this.props.updateWidgetMetaProperty("selectedDataPoint", evt.data, {
|
||||
this.props.updateWidgetMetaProperty("selectedDataPoint", data, {
|
||||
triggerPropertyName: "onDataPointClick",
|
||||
dynamicString: onDataPointClick,
|
||||
event: {
|
||||
type: EventType.ON_DATA_POINT_CLICK,
|
||||
},
|
||||
globalContext: {
|
||||
selectedDataPoint: data,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -469,14 +494,16 @@ class MapChartWidget extends BaseWidget<MapChartWidgetProps, WidgetState> {
|
|||
<MapChartComponent
|
||||
borderRadius={this.props.borderRadius}
|
||||
boxShadow={this.props.boxShadow}
|
||||
caption={mapTitle}
|
||||
caption={mapTitle || ""}
|
||||
colorRange={colorRange}
|
||||
data={data}
|
||||
fontFamily={this.props.fontFamily ?? "Nunito Sans"}
|
||||
height={this.props.componentHeight}
|
||||
isVisible={isVisible}
|
||||
onDataPointClick={this.handleDataPointClick}
|
||||
showLabels={showLabels}
|
||||
type={mapType}
|
||||
width={this.props.componentWidth}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10570,6 +10570,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-geo@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "@types/d3-geo@npm:3.1.0"
|
||||
dependencies:
|
||||
"@types/geojson": "*"
|
||||
checksum: a4b2daa8a64012912ce7186891e8554af123925dca344c111b771e168a37477e02d504c6c94ee698440380e8c4f3f373d6755be97935da30eae0904f6745ce40
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/deep-diff@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@types/deep-diff@npm:1.0.0"
|
||||
|
|
@ -10754,6 +10763,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/geojson@npm:*":
|
||||
version: 7946.0.14
|
||||
resolution: "@types/geojson@npm:7946.0.14"
|
||||
checksum: ae511bee6488ae3bd5a3a3347aedb0371e997b14225b8983679284e22fa4ebd88627c6e3ff8b08bf4cc35068cb29310c89427311ffc9322c255615821a922e71
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/glob@npm:^7.1.3":
|
||||
version: 7.2.0
|
||||
resolution: "@types/glob@npm:7.2.0"
|
||||
|
|
@ -13110,6 +13126,7 @@ __metadata:
|
|||
"@tinymce/tinymce-react": ^3.13.0
|
||||
"@types/babel__standalone": ^7.1.7
|
||||
"@types/codemirror": ^0.0.96
|
||||
"@types/d3-geo": ^3.1.0
|
||||
"@types/deep-diff": ^1.0.0
|
||||
"@types/dom-mediacapture-record": ^1.0.11
|
||||
"@types/downloadjs": ^1.4.2
|
||||
|
|
@ -13200,6 +13217,7 @@ __metadata:
|
|||
cypress-terminal-report: ^5.3.6
|
||||
cypress-wait-until: ^1.7.2
|
||||
cypress-xpath: ^1.6.0
|
||||
d3-geo: ^3.1.0
|
||||
dayjs: ^1.10.6
|
||||
deep-diff: ^1.0.2
|
||||
design-system: "npm:@appsmithorg/design-system@2.1.35"
|
||||
|
|
@ -13228,7 +13246,6 @@ __metadata:
|
|||
focus-trap-react: ^8.9.2
|
||||
fuse.js: ^3.4.5
|
||||
fusioncharts: ^3.18.0
|
||||
fusionmaps: ^3.18.0
|
||||
graphql: ^16.8.1
|
||||
history: ^4.10.1
|
||||
http-proxy: ^1.18.1
|
||||
|
|
@ -13301,7 +13318,6 @@ __metadata:
|
|||
react-documents: ^1.0.4
|
||||
react-dom: ^17.0.2
|
||||
react-full-screen: ^1.1.0
|
||||
react-fusioncharts: ^3.1.2
|
||||
react-google-recaptcha: ^2.1.0
|
||||
react-helmet: ^5.2.1
|
||||
react-hook-form: ^7.28.0
|
||||
|
|
@ -16798,6 +16814,24 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-array@npm:2.5.0 - 3":
|
||||
version: 3.2.4
|
||||
resolution: "d3-array@npm:3.2.4"
|
||||
dependencies:
|
||||
internmap: 1 - 2
|
||||
checksum: a5976a6d6205f69208478bb44920dd7ce3e788c9dceb86b304dbe401a4bfb42ecc8b04c20facde486e9adcb488b5d1800d49393a3f81a23902b68158e12cddd0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-geo@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "d3-geo@npm:3.1.0"
|
||||
dependencies:
|
||||
d3-array: 2.5.0 - 3
|
||||
checksum: adf82b0c105c0c5951ae0a833d4dfc479a563791ad7938579fa14e1cffd623b469d8aa7a37dc413a327fb6ac56880f3da3f6c43d4abe3c923972dd98f34f37d1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"damerau-levenshtein@npm:^1.0.7":
|
||||
version: 1.0.8
|
||||
resolution: "damerau-levenshtein@npm:1.0.8"
|
||||
|
|
@ -20017,28 +20051,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fusionmaps@npm:^3.18.0":
|
||||
version: 3.18.0
|
||||
resolution: "fusionmaps@npm:3.18.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.9.2
|
||||
"@fusioncharts/accessibility": ^1.5.0
|
||||
"@fusioncharts/charts": ^3.18.0
|
||||
"@fusioncharts/constructor": ^1.5.0
|
||||
"@fusioncharts/core": ^1.5.0
|
||||
"@fusioncharts/datatable": ^1.5.0
|
||||
"@fusioncharts/features": ^1.5.0
|
||||
"@fusioncharts/fusiontime": ^2.6.0
|
||||
"@fusioncharts/maps": ^3.18.0
|
||||
"@fusioncharts/powercharts": ^3.18.0
|
||||
"@fusioncharts/utils": ^1.5.0
|
||||
"@fusioncharts/widgets": ^3.18.0
|
||||
mutationobserver-shim: ^0.3.5
|
||||
promise-polyfill: ^8.1.3
|
||||
checksum: 0d9de18e59f04a671a076de27cb0e1a1e15d7a8ef655a42c1f5f26b9f4110d02ed0dfee6d09703896767259f262c400c402daec80ca495c73dfcbe5ae06d50fa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"gauge@npm:^4.0.3":
|
||||
version: 4.0.4
|
||||
resolution: "gauge@npm:4.0.4"
|
||||
|
|
@ -21391,6 +21403,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"internmap@npm:1 - 2":
|
||||
version: 2.0.3
|
||||
resolution: "internmap@npm:2.0.3"
|
||||
checksum: 7ca41ec6aba8f0072fc32fa8a023450a9f44503e2d8e403583c55714b25efd6390c38a87161ec456bf42d7bc83aab62eb28f5aef34876b1ac4e60693d5e1d241
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"interpret@npm:^2.2.0":
|
||||
version: 2.2.0
|
||||
resolution: "interpret@npm:2.2.0"
|
||||
|
|
@ -29000,17 +29019,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-fusioncharts@npm:^3.1.2":
|
||||
version: 3.1.2
|
||||
resolution: "react-fusioncharts@npm:3.1.2"
|
||||
dependencies:
|
||||
uuid: ^3.2.1
|
||||
peerDependencies:
|
||||
react: ^0.14.0 || ^15.0.0 || ^16.0.0
|
||||
checksum: a3df0368e580b48c614fcb955e212750b6532c59b7991c0a33298330c21bd75c8b871efa658b1e1b3479d241da6d20f4bde31c2ce7a69d426d07d9a3a6a3ba48
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-google-recaptcha@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "react-google-recaptcha@npm:2.1.0"
|
||||
|
|
@ -33855,15 +33863,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"uuid@npm:^3.2.1":
|
||||
version: 3.4.0
|
||||
resolution: "uuid@npm:3.4.0"
|
||||
bin:
|
||||
uuid: ./bin/uuid
|
||||
checksum: 58de2feed61c59060b40f8203c0e4ed7fd6f99d42534a499f1741218a1dd0c129f4aa1de797bcf822c8ea5da7e4137aa3673431a96dae729047f7aca7b27866f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"uuid@npm:^8.3.2":
|
||||
version: 8.3.2
|
||||
resolution: "uuid@npm:8.3.2"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user