From 9e180ed6f107cb6bd4f60ea004cb87103a38df1a Mon Sep 17 00:00:00 2001 From: wisonic-s Date: Tue, 12 Mar 2024 17:57:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=9B=BE=E8=A1=A8):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=B0=94=E6=B3=A1=E5=9C=B0=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/assets/svg/bubble-map.svg | 80 +++++++ .../src/assets/svg/buddle-map.svg | 1 - .../data-visualization/canvas/Shape.vue | 3 +- core/core-frontend/src/locales/zh-CN.ts | 1 + .../src/models/chart/chart-attr.d.ts | 2 +- .../chart/components/editor/util/chart.ts | 7 + .../js/panel/charts/map/bubble-map.ts | 225 ++++++++++++++++++ .../components/js/panel/charts/map/common.ts | 52 ++++ .../components/js/panel/charts/map/map.ts | 77 +----- .../components/js/panel/types/impl/l7plot.ts | 25 +- .../views/components/ChartComponentG2Plot.vue | 19 +- .../views/chart/components/views/index.vue | 25 +- 12 files changed, 433 insertions(+), 84 deletions(-) create mode 100644 core/core-frontend/src/assets/svg/bubble-map.svg delete mode 100644 core/core-frontend/src/assets/svg/buddle-map.svg create mode 100644 core/core-frontend/src/views/chart/components/js/panel/charts/map/bubble-map.ts create mode 100644 core/core-frontend/src/views/chart/components/js/panel/charts/map/common.ts diff --git a/core/core-frontend/src/assets/svg/bubble-map.svg b/core/core-frontend/src/assets/svg/bubble-map.svg new file mode 100644 index 0000000000..a36609c373 --- /dev/null +++ b/core/core-frontend/src/assets/svg/bubble-map.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/core-frontend/src/assets/svg/buddle-map.svg b/core/core-frontend/src/assets/svg/buddle-map.svg deleted file mode 100644 index 3930ea05b1..0000000000 --- a/core/core-frontend/src/assets/svg/buddle-map.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/core/core-frontend/src/components/data-visualization/canvas/Shape.vue b/core/core-frontend/src/components/data-visualization/canvas/Shape.vue index 26d7ac7379..2be761af1a 100644 --- a/core/core-frontend/src/components/data-visualization/canvas/Shape.vue +++ b/core/core-frontend/src/components/data-visualization/canvas/Shape.vue @@ -285,7 +285,8 @@ const active = computed(() => { }) const boardMoveActive = computed(() => { - return ['map', 'table-info', 'table-normal', 'table-pivot'].includes(element.value.innerType) + const CHARTS = ['map', 'bubble-map', 'table-info', 'table-normal', 'table-pivot'] + return CHARTS.includes(element.value.innerType) }) const dashboardActive = computed(() => { diff --git a/core/core-frontend/src/locales/zh-CN.ts b/core/core-frontend/src/locales/zh-CN.ts index ba15bcd79d..7e179a128b 100644 --- a/core/core-frontend/src/locales/zh-CN.ts +++ b/core/core-frontend/src/locales/zh-CN.ts @@ -679,6 +679,7 @@ export default { chart_radar: '雷达图', chart_gauge: '仪表盘', chart_map: '地图', + chart_bubble_map: '气泡地图', dateStyle: '日期显示', datePattern: '日期格式', y: '年', diff --git a/core/core-frontend/src/models/chart/chart-attr.d.ts b/core/core-frontend/src/models/chart/chart-attr.d.ts index 2302037e69..c7101a3a17 100644 --- a/core/core-frontend/src/models/chart/chart-attr.d.ts +++ b/core/core-frontend/src/models/chart/chart-attr.d.ts @@ -54,7 +54,7 @@ declare interface ChartBasicStyle { /** * 表格列宽模式: 自适应和自定义 */ - tableColumnMode: 'adapt' | 'custom' | 'field' + tableColumnMode: 'adapt' | 'custom' | 'field' | 'dialog' /** * 表格列宽 */ diff --git a/core/core-frontend/src/views/chart/components/editor/util/chart.ts b/core/core-frontend/src/views/chart/components/editor/util/chart.ts index 7930743646..25f252ee40 100644 --- a/core/core-frontend/src/views/chart/components/editor/util/chart.ts +++ b/core/core-frontend/src/views/chart/components/editor/util/chart.ts @@ -1198,6 +1198,13 @@ export const CHART_TYPE_CONFIGS = [ value: 'map', title: t('chart.chart_map'), icon: 'map' + }, + { + render: 'antv', + category: 'map', + value: 'bubble-map', + title: t('chart.chart_bubble_map'), + icon: 'bubble-map' } ] }, diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/map/bubble-map.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/map/bubble-map.ts new file mode 100644 index 0000000000..7fe3d46f42 --- /dev/null +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/map/bubble-map.ts @@ -0,0 +1,225 @@ +import { useI18n } from '@/hooks/web/useI18n' +import { + L7PlotChartView, + L7PlotDrawOptions +} from '@/views/chart/components/js/panel/types/impl/l7plot' +import { Choropleth, ChoroplethOptions } from '@antv/l7plot/dist/esm/plots/choropleth' +import { DotLayer, IPlotLayer } from '@antv/l7plot' +import { DotLayerOptions } from '@antv/l7plot/dist/esm/layers/dot-layer/types' +import { + MAP_AXIS_TYPE, + MAP_EDITOR_PROPERTY, + MAP_EDITOR_PROPERTY_INNER, + MapMouseEvent +} from '@/views/chart/components/js/panel/charts/map/common' +import { flow, getGeoJsonFile, hexColorToRGBA, parseJson } from '@/views/chart/components/js/util' +import { cloneDeep } from 'lodash-es' +import { FeatureCollection } from '@antv/l7plot/dist/esm/plots/choropleth/types' +import { handleGeoJson } from '@/views/chart/components/js/panel/common/common_antv' +import { valueFormatter } from '@/views/chart/components/js/formatter' + +const { t } = useI18n() + +/** + * 气泡地图 + */ +export class BubbleMap extends L7PlotChartView { + properties = MAP_EDITOR_PROPERTY + propertyInner = MAP_EDITOR_PROPERTY_INNER + axis = MAP_AXIS_TYPE + axisConfig: AxisConfig = { + xAxis: { + name: `${t('chart.area')} / ${t('chart.dimension')}`, + type: 'd', + limit: 1 + }, + yAxis: { + name: `${t('chart.bubble_size')} / ${t('chart.quota')}`, + type: 'q', + limit: 1 + } + } + constructor() { + super('bubble-map') + } + + async drawChart(drawOption: L7PlotDrawOptions): Promise { + const { chart, level, areaId, container, action } = drawOption + if (!areaId) { + return + } + const geoJson = cloneDeep(await getGeoJsonFile(areaId)) + let options: ChoroplethOptions = { + preserveDrawingBuffer: true, + map: { + type: 'mapbox', + style: 'blank' + }, + geoArea: { + type: 'geojson' + }, + source: { + data: chart.data?.data || [], + joinBy: { + sourceField: 'name', + geoField: 'name', + geoData: geoJson + } + }, + viewLevel: { + level, + adcode: 'all' + }, + autoFit: true, + chinaBorder: false, + color: { + field: 'value' + }, + style: { + opacity: 1, + lineWidth: 0.6, + lineOpacity: 1 + }, + label: { + field: '_DE_LABEL_', + style: { + textAllowOverlap: true, + textAnchor: 'center' + } + }, + state: { + active: { stroke: 'green', lineWidth: 1 } + }, + tooltip: {}, + zoom: { + position: 'bottomright' + }, + legend: false, + // 禁用线上地图数据 + customFetchGeoData: () => null + } + options = this.setupOptions(chart, options, drawOption, geoJson) + const view = new Choropleth(container, options) + const dotLayer = this.getDotLayer(chart, chart.data?.data, geoJson) + view.once('loaded', () => { + view.addLayer(dotLayer) + view.on('fillAreaLayer:click', (ev: MapMouseEvent) => { + console.log(view) + const data = ev.feature.properties + action({ + x: ev.x, + y: ev.y, + data: { + data, + extra: { adcode: data.adcode } + } + }) + }) + }) + return view + } + + private getDotLayer(chart: Chart, data: any[], geoJson: FeatureCollection): IPlotLayer { + const areaMap = data.reduce((obj, value) => { + obj[value['field']] = value.value + return obj + }, {}) + const dotData = [] + geoJson.features.forEach(item => { + const name = item.properties['name'] + if (areaMap[name]) { + dotData.push({ + x: item.properties['centroid'][0], + y: item.properties['centroid'][1], + size: areaMap[name] + }) + } + }) + const { basicStyle } = parseJson(chart.customAttr) + const options: DotLayerOptions = { + source: { + data: dotData, + parser: { + type: 'json', + x: 'x', + y: 'y' + } + }, + shape: 'circle', + size: { + field: 'size' + }, + visible: true, + zIndex: 0.05, + color: hexColorToRGBA(basicStyle.colors[0], basicStyle.alpha), + name: 'bubbleLayer', + style: { + opacity: 1 + }, + state: { + active: true + } + } + return new DotLayer(options) + } + + private configBasicStyle( + chart: Chart, + options: ChoroplethOptions, + extra: any[] + ): ChoroplethOptions { + const { areaId }: L7PlotDrawOptions = extra[0] + const geoJson: FeatureCollection = extra[1] + const { basicStyle, label } = parseJson(chart.customAttr) + const senior = parseJson(chart.senior) + const curAreaNameMapping = senior.areaMapping?.[areaId] + handleGeoJson(geoJson, curAreaNameMapping) + options.color = hexColorToRGBA(basicStyle.areaBaseColor, basicStyle.alpha) + const suspension = basicStyle.suspension + if (!suspension) { + options = { + ...options, + zoom: false + } + } + if (!chart.data?.data?.length || !geoJson?.features?.length) { + options.label && (options.label.field = 'name') + return options + } + const data = chart.data.data + const areaMap = data.reduce((obj, value) => { + obj[value['field']] = value.value + return obj + }, {}) + geoJson.features.forEach(item => { + const name = item.properties['name'] + // trick, maybe move to configLabel, here for perf + if (label.show) { + const content = [] + if (label.showDimension) { + content.push(name) + } + if (label.showQuota) { + areaMap[name] && content.push(valueFormatter(areaMap[name], label.quotaLabelFormatter)) + } + item.properties['_DE_LABEL_'] = content.join('\n\n') + } + }) + return options + } + + protected setupOptions( + chart: Chart, + options: ChoroplethOptions, + ...extra: any[] + ): ChoroplethOptions { + return flow( + this.configEmptyDataStrategy, + this.configLabel, + this.configStyle, + this.configTooltip, + this.configBasicStyle, + this.configLegend + )(chart, options, extra) + } +} diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/map/common.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/map/common.ts new file mode 100644 index 0000000000..e624352d88 --- /dev/null +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/map/common.ts @@ -0,0 +1,52 @@ +export const MAP_EDITOR_PROPERTY: EditorProperty[] = [ + 'background-overall-component', + 'basic-style-selector', + 'title-selector', + 'label-selector', + 'tooltip-selector', + 'function-cfg', + 'map-mapping', + 'jump-set', + 'linkage' +] + +export const MAP_EDITOR_PROPERTY_INNER: EditorPropertyInner = { + 'background-overall-component': ['all'], + 'basic-style-selector': ['colors', 'alpha', 'areaBorderColor', 'suspension'], + 'title-selector': [ + 'title', + 'fontSize', + 'color', + 'hPosition', + 'isItalic', + 'isBolder', + 'remarkShow', + 'fontFamily', + 'letterSpace', + 'fontShadow' + ], + 'label-selector': [ + 'color', + 'fontSize', + 'labelBgColor', + 'labelShadow', + 'labelShadowColor', + 'showDimension', + 'showQuota' + ], + 'tooltip-selector': ['color', 'fontSize', 'backgroundColor', 'tooltipFormatter'], + 'function-cfg': ['emptyDataStrategy'] +} + +export const MAP_AXIS_TYPE: AxisType[] = [ + 'xAxis', + 'yAxis', + 'drill', + 'filter', + 'extLabel', + 'extTooltip' +] + +export declare type MapMouseEvent = MouseEvent & { + feature: GeoJSON.Feature +} diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/map/map.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/map/map.ts index b02978581f..572b2ef3d2 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/map/map.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/map/map.ts @@ -9,51 +9,22 @@ import { FeatureCollection } from '@antv/l7plot/dist/esm/plots/choropleth/types' import { cloneDeep } from 'lodash-es' import { useI18n } from '@/hooks/web/useI18n' import { valueFormatter } from '../../../formatter' +import { + MAP_AXIS_TYPE, + MAP_EDITOR_PROPERTY, + MAP_EDITOR_PROPERTY_INNER, + MapMouseEvent +} from '@/views/chart/components/js/panel/charts/map/common' const { t } = useI18n() -type MapMouseEvent = MouseEvent & { - feature: GeoJSON.Feature -} + +/** + * 地图 + */ export class Map extends L7PlotChartView { - properties: EditorProperty[] = [ - 'background-overall-component', - 'basic-style-selector', - 'title-selector', - 'label-selector', - 'tooltip-selector', - 'function-cfg', - 'map-mapping', - 'jump-set', - 'linkage' - ] - propertyInner: EditorPropertyInner = { - 'background-overall-component': ['all'], - 'basic-style-selector': ['colors', 'alpha', 'areaBorderColor', 'suspension'], - 'title-selector': [ - 'title', - 'fontSize', - 'color', - 'hPosition', - 'isItalic', - 'isBolder', - 'remarkShow', - 'fontFamily', - 'letterSpace', - 'fontShadow' - ], - 'label-selector': [ - 'color', - 'fontSize', - 'labelBgColor', - 'labelShadow', - 'labelShadowColor', - 'showDimension', - 'showQuota' - ], - 'tooltip-selector': ['color', 'fontSize', 'backgroundColor', 'tooltipFormatter'], - 'function-cfg': ['emptyDataStrategy'] - } - axis: AxisType[] = ['xAxis', 'yAxis', 'area', 'drill', 'filter', 'extLabel', 'extTooltip'] + properties = MAP_EDITOR_PROPERTY + propertyInner = MAP_EDITOR_PROPERTY_INNER + axis = MAP_AXIS_TYPE axisConfig: AxisConfig = { xAxis: { name: `${t('chart.area')} / ${t('chart.dimension')}`, @@ -147,28 +118,6 @@ export class Map extends L7PlotChartView { return view } - private configEmptyDataStrategy(chart: Chart, options: ChoroplethOptions): ChoroplethOptions { - const { functionCfg } = parseJson(chart.senior) - const emptyDataStrategy = functionCfg.emptyDataStrategy - if (!emptyDataStrategy || emptyDataStrategy === 'breakLine') { - return options - } - const data = cloneDeep(options.source.data) - if (emptyDataStrategy === 'setZero') { - data.forEach(item => { - item.value === null && (item.value = 0) - }) - } - if (emptyDataStrategy === 'ignoreData') { - for (let i = data.length - 1; i >= 0; i--) { - if (data[i].value === null) { - data.splice(i, 1) - } - } - } - options.source.data = data - return options - } private configBasicStyle( chart: Chart, options: ChoroplethOptions, diff --git a/core/core-frontend/src/views/chart/components/js/panel/types/impl/l7plot.ts b/core/core-frontend/src/views/chart/components/js/panel/types/impl/l7plot.ts index c22d32c9ca..0f1ae019ba 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/types/impl/l7plot.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/types/impl/l7plot.ts @@ -13,8 +13,9 @@ import { AntVDrawOptions, ChartLibraryType } from '@/views/chart/components/js/panel/types' -import { defaultsDeep } from 'lodash-es' +import { cloneDeep, defaultsDeep } from 'lodash-es' import { ChoroplethOptions } from '@antv/l7plot/dist/esm/plots/choropleth' +import { parseJson } from '@/views/chart/components/js/util' export interface L7PlotDrawOptions

extends AntVDrawOptions

{ areaId?: string @@ -50,6 +51,28 @@ export abstract class L7PlotChartView< defaultsDeep(options.legend, legend) return options } + protected configEmptyDataStrategy(chart: Chart, options: ChoroplethOptions): ChoroplethOptions { + const { functionCfg } = parseJson(chart.senior) + const emptyDataStrategy = functionCfg.emptyDataStrategy + if (!emptyDataStrategy || emptyDataStrategy === 'breakLine') { + return options + } + const data = cloneDeep(options.source.data) + if (emptyDataStrategy === 'setZero') { + data.forEach(item => { + item.value === null && (item.value = 0) + }) + } + if (emptyDataStrategy === 'ignoreData') { + for (let i = data.length - 1; i >= 0; i--) { + if (data[i].value === null) { + data.splice(i, 1) + } + } + } + options.source.data = data + return options + } protected constructor(name: string, defaultData?: any[]) { super(ChartLibraryType.L7_PLOT, name) this.defaultData = defaultData diff --git a/core/core-frontend/src/views/chart/components/views/components/ChartComponentG2Plot.vue b/core/core-frontend/src/views/chart/components/views/components/ChartComponentG2Plot.vue index 559135cd53..ac84688c29 100644 --- a/core/core-frontend/src/views/chart/components/views/components/ChartComponentG2Plot.vue +++ b/core/core-frontend/src/views/chart/components/views/components/ChartComponentG2Plot.vue @@ -59,7 +59,6 @@ const state = reactive({ }, linkageActiveParam: null, pointParam: null, - loading: false, data: { fields: [] } // 图表数据 }) let chartData = shallowRef>({ @@ -71,7 +70,6 @@ const viewTrack = ref(null) const calcData = (view, callback) => { if (view.tableId || view['dataFrom'] === 'template') { - state.loading = true isError.value = false const v = JSON.parse(JSON.stringify(view)) getData(v) @@ -79,6 +77,7 @@ const calcData = (view, callback) => { if (res.code && res.code !== 0) { isError.value = true errMsg.value = res.msg + callback?.() } else { chartData.value = res?.data as Partial emit('onDrillFilters', res?.drillFilters) @@ -93,22 +92,21 @@ const calcData = (view, callback) => { } } dvMainStore.setViewDataDetails(view.id, chartData.value) - renderChart(res) + renderChart(res, callback) } - callback?.() }) .catch(() => { callback?.() }) } else { if (view.type === 'map') { - renderChart(view) + renderChart(view, callback) } callback?.() } } let curView -const renderChart = async view => { +const renderChart = async (view, callback?) => { if (!view) { return } @@ -124,10 +122,11 @@ const renderChart = async view => { recursionTransObj(customStyleTrans, chart.customStyle, scale.value, terminal.value) switch (chartView.library) { case ChartLibraryType.L7_PLOT: - renderL7Plot(chart, chartView as L7PlotChartView) + renderL7Plot(chart, chartView as L7PlotChartView, callback) break case ChartLibraryType.G2_PLOT: renderG2Plot(chart, chartView as G2PlotChartView) + callback?.() break default: break @@ -151,7 +150,7 @@ const country = ref('') const appStore = useAppStoreWithOut() const isDataEaseBi = computed(() => appStore.getIsDataEaseBi) let mapTimer -const renderL7Plot = (chart, chartView: L7PlotChartView) => { +const renderL7Plot = (chart, chartView: L7PlotChartView, callback?) => { const map = parseJson(chart.customAttr).map let areaId = map.id country.value = areaId.slice(0, 3) @@ -168,6 +167,7 @@ const renderL7Plot = (chart, chartView: L7PlotChartView) => { areaId, action }) + callback?.() }, 500) } @@ -265,12 +265,13 @@ defineExpose({ }) let resizeObserver const TOLERANCE = 0.01 +const RESIZE_MONITOR_CHARTS = ['map', 'bubble-map'] onMounted(() => { const containerDom = document.getElementById(containerId) const { offsetWidth, offsetHeight } = containerDom const preSize = [offsetWidth, offsetHeight] resizeObserver = new ResizeObserver(([entry] = []) => { - if (view.value.type !== 'map') { + if (!RESIZE_MONITOR_CHARTS.includes(view.value.type)) { return } const [size] = entry.borderBoxSize || [] diff --git a/core/core-frontend/src/views/chart/components/views/index.vue b/core/core-frontend/src/views/chart/components/views/index.vue index 724a6b67b4..64875c9c3e 100644 --- a/core/core-frontend/src/views/chart/components/views/index.vue +++ b/core/core-frontend/src/views/chart/components/views/index.vue @@ -513,13 +513,24 @@ const loadingFlag = computed(() => { }) const chartAreaShow = computed(() => { - return ( - (view.value.tableId && - (element.value['state'] === undefined || element.value['state'] === 'ready')) || - view.value.type === 'rich-text' || - (view.value.type === 'map' && view.value.customAttr.map.id) || - view.value['dataFrom'] === 'template' - ) + if (view.value.tableId) { + if (element.value['state'] === undefined || element.value['state'] === 'ready') { + return true + } + } + if (view.value.type === 'rich-text') { + return true + } + if (view.value['dataFrom'] === 'template') { + return true + } + if (view.value.customAttr.map.id) { + const MAP_CHARTS = ['map', 'bubble-map'] + if (MAP_CHARTS.includes(view.value.type)) { + return true + } + } + return false }) const titleInputRef = ref()