|
@@ -1,198 +0,0 @@
|
|
|
-import {useEffect, useState} from 'react';
|
|
|
-import {useTheme} from '@emotion/react';
|
|
|
-import type {MapSeriesOption, TooltipComponentOption} from 'echarts';
|
|
|
-import * as echarts from 'echarts/core';
|
|
|
-import max from 'lodash/max';
|
|
|
-
|
|
|
-import {Series, SeriesDataUnit} from 'sentry/types/echarts';
|
|
|
-
|
|
|
-import VisualMap from './components/visualMap';
|
|
|
-import MapSeries from './series/mapSeries';
|
|
|
-import BaseChart, {BaseChartProps} from './baseChart';
|
|
|
-
|
|
|
-interface MapChartSeriesDataUnit extends Omit<SeriesDataUnit, 'name' | 'itemStyle'> {
|
|
|
- // Docs for map itemStyle differ from Series data unit. See https://echarts.apache.org/en/option.html#series-map.data.itemStyle
|
|
|
- itemStyle?: MapSeriesOption['itemStyle'];
|
|
|
- name?: string;
|
|
|
-}
|
|
|
-
|
|
|
-type MapChartSeries = Omit<Series, 'data'> & {
|
|
|
- data: MapChartSeriesDataUnit[];
|
|
|
-};
|
|
|
-
|
|
|
-export interface WorldMapChartProps extends Omit<BaseChartProps, 'series'> {
|
|
|
- series: MapChartSeries[];
|
|
|
- fromDiscover?: boolean;
|
|
|
- fromDiscoverQueryList?: boolean;
|
|
|
- seriesOptions?: MapSeriesOption;
|
|
|
-}
|
|
|
-
|
|
|
-type JSONResult = Record<string, any>;
|
|
|
-
|
|
|
-type State = {
|
|
|
- codeToCountryMap: JSONResult | null;
|
|
|
- countryToCodeMap: JSONResult | null;
|
|
|
- map: JSONResult | null;
|
|
|
-};
|
|
|
-
|
|
|
-const DEFAULT_ZOOM = 1.3;
|
|
|
-const DISCOVER_ZOOM = 1.1;
|
|
|
-const DISCOVER_QUERY_LIST_ZOOM = 0.9;
|
|
|
-const DEFAULT_CENTER_X = 10.97;
|
|
|
-const DISCOVER_QUERY_LIST_CENTER_Y = -12;
|
|
|
-const DEFAULT_CENTER_Y = 9.71;
|
|
|
-
|
|
|
-export function WorldMapChart({
|
|
|
- series,
|
|
|
- seriesOptions,
|
|
|
- fromDiscover,
|
|
|
- fromDiscoverQueryList,
|
|
|
- ...props
|
|
|
-}: WorldMapChartProps) {
|
|
|
- const theme = useTheme();
|
|
|
- const [state, setState] = useState<State>(() => ({
|
|
|
- countryToCodeMap: null,
|
|
|
- map: null,
|
|
|
- codeToCountryMap: null,
|
|
|
- }));
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- let unmounted = false;
|
|
|
-
|
|
|
- if (!unmounted) {
|
|
|
- loadWorldMap();
|
|
|
- }
|
|
|
-
|
|
|
- return () => {
|
|
|
- unmounted = true;
|
|
|
- };
|
|
|
- }, []);
|
|
|
-
|
|
|
- async function loadWorldMap() {
|
|
|
- try {
|
|
|
- const [countryCodesMap, world] = await Promise.all([
|
|
|
- import('sentry/data/countryCodesMap'),
|
|
|
- import('sentry/data/world.json'),
|
|
|
- ]);
|
|
|
-
|
|
|
- const countryToCodeMap = countryCodesMap.default;
|
|
|
- const worldMap = world.default;
|
|
|
-
|
|
|
- // Echarts not available in tests
|
|
|
- echarts.registerMap?.('sentryWorld', worldMap as any);
|
|
|
-
|
|
|
- const codeToCountryMap: Record<string, string> = {};
|
|
|
-
|
|
|
- for (const country in worldMap) {
|
|
|
- codeToCountryMap[countryToCodeMap[country]] = country;
|
|
|
- }
|
|
|
-
|
|
|
- setState({
|
|
|
- countryToCodeMap,
|
|
|
- map: worldMap,
|
|
|
- codeToCountryMap,
|
|
|
- });
|
|
|
- } catch {
|
|
|
- // do nothing
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (state.countryToCodeMap === null || state.map === null) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- const processedSeries = series.map(({seriesName, data, ...options}) =>
|
|
|
- MapSeries({
|
|
|
- ...seriesOptions,
|
|
|
- ...options,
|
|
|
- map: 'sentryWorld',
|
|
|
- name: seriesName,
|
|
|
- nameMap: state.countryToCodeMap ?? undefined,
|
|
|
- aspectScale: 0.85,
|
|
|
- zoom: fromDiscover
|
|
|
- ? DISCOVER_ZOOM
|
|
|
- : fromDiscoverQueryList
|
|
|
- ? DISCOVER_QUERY_LIST_ZOOM
|
|
|
- : DEFAULT_ZOOM,
|
|
|
- center: [
|
|
|
- DEFAULT_CENTER_X,
|
|
|
- fromDiscoverQueryList ? DISCOVER_QUERY_LIST_CENTER_Y : DEFAULT_CENTER_Y,
|
|
|
- ],
|
|
|
- itemStyle: {
|
|
|
- areaColor: theme.gray200,
|
|
|
- borderColor: theme.backgroundSecondary,
|
|
|
- },
|
|
|
- emphasis: {
|
|
|
- itemStyle: {
|
|
|
- areaColor: theme.pink300,
|
|
|
- },
|
|
|
- label: {
|
|
|
- show: false,
|
|
|
- },
|
|
|
- },
|
|
|
- data,
|
|
|
- silent: fromDiscoverQueryList,
|
|
|
- roam: !fromDiscoverQueryList,
|
|
|
- })
|
|
|
- );
|
|
|
-
|
|
|
- // TODO(billy):
|
|
|
- // For absolute values, we want min/max to based on min/max of series
|
|
|
- // Otherwise it should be 0-100
|
|
|
- const maxValue = max(series.map(({data}) => max(data.map(({value}) => value)))) || 1;
|
|
|
-
|
|
|
- const tooltipFormatter: TooltipComponentOption['formatter'] = (format: any) => {
|
|
|
- const {marker, name, value} = Array.isArray(format) ? format[0] : format;
|
|
|
- // If value is NaN, don't show anything because we won't have a country code either
|
|
|
- if (isNaN(value as number)) {
|
|
|
- return '';
|
|
|
- }
|
|
|
-
|
|
|
- // `value` should be a number
|
|
|
- const formattedValue = typeof value === 'number' ? value.toLocaleString() : '';
|
|
|
- const countryOrCode = state.codeToCountryMap?.[name as string] || name;
|
|
|
-
|
|
|
- return [
|
|
|
- `<div class="tooltip-series tooltip-series-solo">
|
|
|
- <div><span class="tooltip-label">${marker} <strong>${countryOrCode}</strong></span> ${formattedValue}</div>
|
|
|
- </div>`,
|
|
|
- '<div class="tooltip-arrow"></div>',
|
|
|
- ].join('');
|
|
|
- };
|
|
|
-
|
|
|
- return (
|
|
|
- <BaseChart
|
|
|
- options={{
|
|
|
- backgroundColor: fromDiscoverQueryList ? undefined : theme.background,
|
|
|
- visualMap: [
|
|
|
- VisualMap({
|
|
|
- show: !fromDiscoverQueryList,
|
|
|
- left: fromDiscover ? undefined : 'right',
|
|
|
- right: fromDiscover ? 5 : undefined,
|
|
|
- min: 0,
|
|
|
- max: maxValue,
|
|
|
- inRange: {
|
|
|
- color: [theme.purple200, theme.purple300],
|
|
|
- },
|
|
|
- text: ['High', 'Low'],
|
|
|
- textStyle: {
|
|
|
- color: theme.textColor,
|
|
|
- },
|
|
|
-
|
|
|
- // Whether show handles, which can be dragged to adjust "selected range".
|
|
|
- // False because the handles are pretty ugly
|
|
|
- calculable: false,
|
|
|
- }),
|
|
|
- ],
|
|
|
- }}
|
|
|
- {...props}
|
|
|
- yAxis={null}
|
|
|
- xAxis={null}
|
|
|
- series={processedSeries}
|
|
|
- tooltip={{
|
|
|
- formatter: tooltipFormatter,
|
|
|
- }}
|
|
|
- height={fromDiscover ? 400 : undefined}
|
|
|
- />
|
|
|
- );
|
|
|
-}
|