feat(图表): 支持气泡地图

This commit is contained in:
wisonic-s 2024-03-12 17:57:15 +08:00
parent b26415b7df
commit 9e180ed6f1
12 changed files with 433 additions and 84 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 742 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -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(() => {

View File

@ -679,6 +679,7 @@ export default {
chart_radar: '雷达图',
chart_gauge: '仪表盘',
chart_map: '地图',
chart_bubble_map: '气泡地图',
dateStyle: '日期显示',
datePattern: '日期格式',
y: '年',

View File

@ -54,7 +54,7 @@ declare interface ChartBasicStyle {
/**
* 表格列宽模式: 自适应和自定义
*/
tableColumnMode: 'adapt' | 'custom' | 'field'
tableColumnMode: 'adapt' | 'custom' | 'field' | 'dialog'
/**
* 表格列宽
*/

View File

@ -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'
}
]
},

View File

@ -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<ChoroplethOptions, Choropleth> {
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<Choropleth>): Promise<Choropleth> {
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<any> = 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)
}
}

View File

@ -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
}

View File

@ -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<ChoroplethOptions, Choropleth> {
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<ChoroplethOptions, Choropleth> {
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,

View File

@ -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<P> extends AntVDrawOptions<P> {
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

View File

@ -59,7 +59,6 @@ const state = reactive({
},
linkageActiveParam: null,
pointParam: null,
loading: false,
data: { fields: [] } //
})
let chartData = shallowRef<Partial<Chart['data']>>({
@ -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<Chart['data']>
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<any, any>)
renderL7Plot(chart, chartView as L7PlotChartView<any, any>, callback)
break
case ChartLibraryType.G2_PLOT:
renderG2Plot(chart, chartView as G2PlotChartView<any, any>)
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<any, any>) => {
const renderL7Plot = (chart, chartView: L7PlotChartView<any, any>, 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<any, any>) => {
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 || []

View File

@ -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()