feat(图表): 支持气泡地图
This commit is contained in:
parent
b26415b7df
commit
9e180ed6f1
80
core/core-frontend/src/assets/svg/bubble-map.svg
Normal file
80
core/core-frontend/src/assets/svg/bubble-map.svg
Normal file
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 |
@ -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(() => {
|
||||
|
||||
@ -679,6 +679,7 @@ export default {
|
||||
chart_radar: '雷达图',
|
||||
chart_gauge: '仪表盘',
|
||||
chart_map: '地图',
|
||||
chart_bubble_map: '气泡地图',
|
||||
dateStyle: '日期显示',
|
||||
datePattern: '日期格式',
|
||||
y: '年',
|
||||
|
||||
@ -54,7 +54,7 @@ declare interface ChartBasicStyle {
|
||||
/**
|
||||
* 表格列宽模式: 自适应和自定义
|
||||
*/
|
||||
tableColumnMode: 'adapt' | 'custom' | 'field'
|
||||
tableColumnMode: 'adapt' | 'custom' | 'field' | 'dialog'
|
||||
/**
|
||||
* 表格列宽
|
||||
*/
|
||||
|
||||
@ -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'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 || []
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user