feat(图表): 地图、气泡地图、符号地图支持提示轮播展示

This commit is contained in:
jianneng-fit2cloud 2024-08-09 13:18:12 +08:00
parent 32bf7d278c
commit d268c5a5ca
8 changed files with 685 additions and 9 deletions

View File

@ -858,6 +858,11 @@ declare interface ChartTooltipAttr {
* 自定义显示内容
*/
customContent?: string
/**
* 轮播设置
*/
carousel: CarouselAttr
}
/**
@ -1057,3 +1062,21 @@ declare interface ChartIndicatorNameStyle {
*/
nameValueSpacing: number
}
/**
* 轮播属性
*/
declare interface CarouselAttr {
/**
* 是否启用
*/
enable: boolean
/**
* 停留时间
*/
stayTime: number
/**
* 轮播间隔时间
*/
intervalTime: number
}

View File

@ -808,6 +808,57 @@ onMounted(() => {
{{ t('chart.show_gap') }}
</el-checkbox>
</el-form-item>
<div class="carousel" v-if="showProperty('carousel')">
<el-form-item class="form-item" :class="'form-item-' + themes">
<el-checkbox
:effect="themes"
@change="changeTooltipAttr('carousel')"
v-model="state.tooltipForm.carousel.enable"
>
开启轮播
</el-checkbox>
</el-form-item>
<el-row :gutter="8">
<el-col :span="12">
<el-form-item
label="停留时长(秒)"
class="form-item w100"
:class="'form-item-' + themes"
>
<el-input-number
style="width: 100%"
:effect="themes"
controls-position="right"
size="middle"
:min="0"
:max="600"
:disabled="!state.tooltipForm.carousel.enable"
@change="changeTooltipAttr('carousel')"
v-model="state.tooltipForm.carousel.stayTime"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
label="轮播间隔(秒)"
class="form-item w100"
:class="'form-item-' + themes"
>
<el-input-number
style="width: 100%"
:effect="themes"
controls-position="right"
size="middle"
:min="0"
:max="600"
:disabled="!state.tooltipForm.carousel.enable"
@change="changeTooltipAttr('carousel')"
v-model="state.tooltipForm.carousel.intervalTime"
/>
</el-form-item>
</el-col>
</el-row>
</div>
</el-form>
</template>

View File

@ -334,7 +334,12 @@ export const DEFAULT_TOOLTIP: ChartTooltipAttr = {
color: '#909399',
tooltipFormatter: formatterItem,
backgroundColor: '#ffffff',
seriesTooltipFormatter: []
seriesTooltipFormatter: [],
carousel: {
enable: false,
stayTime: 3,
intervalTime: 0
}
}
export const DEFAULT_TABLE_TOTAL: ChartTableTotalAttr = {
row: {

View File

@ -21,6 +21,7 @@ import {
} from '@/views/chart/components/js/panel/common/common_antv'
import { valueFormatter } from '@/views/chart/components/js/formatter'
import { deepCopy } from '@/utils/utils'
import { configCarouselTooltip } from '@/views/chart/components/js/panel/charts/map/tooltip-carousel'
const { t } = useI18n()
@ -29,7 +30,10 @@ const { t } = useI18n()
*/
export class BubbleMap extends L7PlotChartView<ChoroplethOptions, Choropleth> {
properties: EditorProperty[] = [...MAP_EDITOR_PROPERTY, 'bubble-animate']
propertyInner = MAP_EDITOR_PROPERTY_INNER
propertyInner = {
...MAP_EDITOR_PROPERTY_INNER,
'tooltip-selector': [...MAP_EDITOR_PROPERTY_INNER['tooltip-selector'], 'carousel']
}
axis = MAP_AXIS_TYPE
axisConfig: AxisConfig = {
xAxis: {
@ -100,7 +104,7 @@ export class BubbleMap extends L7PlotChartView<ChoroplethOptions, Choropleth> {
options = this.setupOptions(chart, options, context)
const tooltip = deepCopy(options.tooltip)
options = { ...options, tooltip: false }
options = { ...options, tooltip: { ...tooltip, showComponent: false } }
const view = new Choropleth(container, options)
const dotLayer = this.getDotLayer(chart, geoJson, drawOption)
dotLayer.options = { ...dotLayer.options, tooltip }
@ -128,6 +132,10 @@ export class BubbleMap extends L7PlotChartView<ChoroplethOptions, Choropleth> {
}
})
})
dotLayer.once('loaded', () => {
chart.container = container
configCarouselTooltip(chart, view, chart.data?.data || [], null)
})
})
return view
}
@ -179,7 +187,7 @@ export class BubbleMap extends L7PlotChartView<ChoroplethOptions, Choropleth> {
opacity: 1
},
state: {
active: true
active: { color: 'rgba(30,90,255,1)' }
},
tooltip: {}
}

View File

@ -35,6 +35,7 @@ import {
LIST_CLASS
} from '@antv/l7plot-component/dist/esm/legend/category/constants'
import substitute from '@antv/util/esm/substitute'
import { configCarouselTooltip } from '@/views/chart/components/js/panel/charts/map/tooltip-carousel'
const { t } = useI18n()
@ -46,7 +47,8 @@ export class Map extends L7PlotChartView<ChoroplethOptions, Choropleth> {
propertyInner: EditorPropertyInner = {
...MAP_EDITOR_PROPERTY_INNER,
'basic-style-selector': ['colors', 'alpha', 'areaBorderColor', 'zoom', 'gradient-color'],
'legend-selector': ['icon', 'fontSize', 'color']
'legend-selector': ['icon', 'fontSize', 'color'],
'tooltip-selector': [...MAP_EDITOR_PROPERTY_INNER['tooltip-selector'], 'carousel']
}
axis = MAP_AXIS_TYPE
axisConfig: AxisConfig = {
@ -161,6 +163,8 @@ export class Map extends L7PlotChartView<ChoroplethOptions, Choropleth> {
}
})
})
chart.container = container
configCarouselTooltip(chart, view, data, null)
})
return view
}

View File

@ -13,6 +13,7 @@ import { Scene } from '@antv/l7-scene'
import { PointLayer } from '@antv/l7-layers'
import { LayerPopup } from '@antv/l7'
import { mapRendered, mapRendering } from '@/views/chart/components/js/panel/common/common_antv'
import { configCarouselTooltip } from '@/views/chart/components/js/panel/charts/map/tooltip-carousel'
const { t } = useI18n()
/**
@ -36,7 +37,8 @@ export class SymbolicMap extends L7ChartView<Scene, L7Config> {
'showFields',
'customContent',
'show',
'backgroundColor'
'backgroundColor',
'carousel'
]
}
axis: AxisType[] = ['xAxis', 'xAxisExt', 'extBubble', 'filter', 'extLabel', 'extTooltip']
@ -102,6 +104,10 @@ export class SymbolicMap extends L7ChartView<Scene, L7Config> {
}
this.buildLabel(chart, configList)
this.configZoomButton(chart, scene)
symbolicLayer.on('inited', ev => {
chart.container = container
configCarouselTooltip(chart, symbolicLayer, symbolicLayer.sourceOption.data, scene)
})
symbolicLayer.on('click', ev => {
const data = ev.feature
const dimensionList = []
@ -251,10 +257,19 @@ export class SymbolicMap extends L7ChartView<Scene, L7Config> {
]
}
// 修改背景色
const styleId = 'tooltip-' + container
const styleElement = document.getElementById(styleId)
if (styleElement) {
styleElement.remove()
styleElement.parentNode?.removeChild(styleElement)
}
const style = document.createElement('style')
style.id = styleId
style.innerHTML = `
#${container} .l7-popup-content {
background-color: ${tooltip.backgroundColor} !important;
padding: 6px 10px 6px;
line-height: 1.6;
}
#${container} .l7-popup-tip {
border-top-color: ${tooltip.backgroundColor} !important;
@ -299,8 +314,9 @@ export class SymbolicMap extends L7ChartView<Scene, L7Config> {
})
} else {
showFields.forEach(field => {
//const value = ${fieldData[field.split('@')[0]] as string
content += `${field.split('@')[1]}: ${fieldData[field.split('@')[0]]}<br>`
content += `<span style="margin-bottom: 4px">${field.split('@')[1]}: ${
fieldData[field.split('@')[0]]
}</span><br>`
})
}
return content

View File

@ -0,0 +1,568 @@
import { Popup } from '@antv/l7'
import { Plot } from '@antv/l7plot/dist/lib/core/plot'
import isEmpty from 'lodash-es/isEmpty'
import { valueFormatter } from '@/views/chart/components/js/formatter'
import { parseJson } from '@/views/chart/components/js/util'
import { Scene } from '@antv/l7-scene'
import { deepCopy } from '@/utils/utils'
export const configCarouselTooltip = (chart, view, data, scene) => {
if (carouselManagerInstances[chart.container]) {
const instances = carouselManagerInstances[chart.container]
instances.update(scene, chart, view, data)
} else {
new CarouselManager(scene, chart, view, data)
}
}
export const carouselManagerInstances: { [key: string]: CarouselManager } = {}
/**
* 轮播管理类
*/
export class CarouselManager {
/**
* 停留时长定时器
* @private
*/
private popupTimeoutId: number | null = null
/**
* 轮播间隔定时器
* @private
*/
private popupIntervalId: number | null = null
/**
* 是否暂停轮播
* @private
*/
private isPaused = false
/**
* 当前显示的数据索引
* @private
*/
private currentIndex = 0
/**
* 地图实例气泡地图用
* @private
*/
private scene: Scene
private chart: Chart
/**
* 轮播弹窗的位置数据
* @private
*/
private view: Plot
private data: any[]
/**
* 停留时长
* @private
*/
private stayTime: number
/**
* 轮播间隔
* @private
*/
private intervalTime: number
/**
* 轮播弹窗
* @private
*/
private popup: Popup
// 保存事件监听函数的引用
private onMouseEnterHandler: () => void
private onMouseLeaveHandler: () => void
private onVisibilityChangeHandler: () => void
constructor(scene, chart, view, data: any[]) {
// 绑定事件处理函数
this.onMouseEnterHandler = this.pauseCarouselPopups.bind(this)
this.onMouseLeaveHandler = this.resumeCarouselPopups.bind(this)
this.onVisibilityChangeHandler = this.handleVisibilityChange.bind(this)
this.clearExistingTimers = this.clearExistingTimers.bind(this)
this.init(scene, chart, view, data)
}
/**
* 更新轮播弹窗对象内容
* @param scene
* @param chart
* @param view
* @param data
*/
public update(scene, chart, view, data: any[]) {
this.init(scene, chart, view, data)
}
/**
* 初始化轮播弹窗
* @param scene
* @param chart
* @param view
* @param data
* @private
*/
private init(scene, chart, view, data: any[]) {
this.view = view
this.chart = chart
this.scene = scene
this.data = data
this.popup = null
this.currentIndex = 0
this.clearPreviousInstance(this.chart.container)
if (
this.chart.customAttr?.tooltip?.show &&
this.chart.customAttr?.tooltip?.carousel?.enable &&
this.data.length > 0
) {
this.popup = new Popup({ closeButton: false, maxWidth: 600 })
const carousel = this.chart.customAttr?.tooltip?.carousel
this.stayTime = carousel.stayTime * 1000
this.intervalTime = carousel.intervalTime * 1000
this.startCarouselPopups()
const divElement = document.getElementById(this.chart.container)
divElement.addEventListener('mouseenter', this.pauseCarouselPopups)
divElement.addEventListener('mouseleave', this.resumeCarouselPopups)
// 监听页面可见性变化
document.addEventListener('visibilitychange', this.handleVisibilityChange)
carouselManagerInstances[this.chart.container] = this
}
}
private handleVisibilityChange = (): void => {
if (document.hidden) {
this.clearPreviousInstance(this.chart.container)
} else {
this.startCarouselPopups()
}
}
/**
* 清除之前的实例数据
* @param containerId
* @private
*/
private clearPreviousInstance(containerId: string): void {
if (carouselManagerInstances[containerId]) {
const instance = carouselManagerInstances[containerId]
this.clearExistingTimers()
instance.popup?.remove()
instance.removeStyle()
}
}
/**
* 开始轮播
* @private
*/
private startCarouselPopups(): void {
this.clearExistingTimers()
this.carouselPopups()
}
/**
* 鼠标移入暂停轮播
*/
private pauseCarouselPopups = (): void => {
if (this.popup) {
this.popup?.remove()
}
this.removeStyle()
this.isPaused = true
this.clearExistingTimers()
}
/**
* 鼠标移出开始轮播
*/
private resumeCarouselPopups = (): void => {
if (this.isPaused) {
this.isPaused = false
this.startCarouselPopups()
}
}
/**
* 管理轮播弹窗的显示
*
* 此方法用于处理轮播弹窗的显示逻辑它会根据当前的索引显示对应的弹窗
* 并在一定时间后自动移除当前弹窗并显示下一个弹窗
*
* @private
*/
private carouselPopups(): void {
const showPopup = (index: number): void => {
this.removeStyle()
const containerElement = document.getElementById(this.chart.container)
if (containerElement) {
if (this.chart.type === 'symbolic-map') {
this.createSymbolicMapPopup(index)
} else {
this.createPopup(index)
}
this.clearExistingTimers()
this.popupTimeoutId = window.setTimeout(() => {
this.currentIndex++
this.popup?.remove()
this.cancelHighlightLayer(index)
if (this.currentIndex >= this.data.length) {
this.currentIndex = 0
}
this.popupIntervalId = window.setTimeout(() => {
showPopup(this.currentIndex)
}, this.intervalTime)
}, this.stayTime)
} else {
this.clearExistingTimers()
}
}
showPopup(this.currentIndex)
}
/**
* 清除定时器
* @private
*/
private readonly clearExistingTimers = (): void => {
if (this.popupTimeoutId !== null) {
clearTimeout(this.popupTimeoutId)
this.popupTimeoutId = 0
}
if (this.popupIntervalId !== null) {
clearInterval(this.popupIntervalId)
this.popupIntervalId = 0
}
}
/**
* 移除样式
* 每次创建弹窗前移除之前的样式
* @private
*/
private removeStyle(): void {
const styleToRemove = document.getElementById('style-' + this.chart.container)
if (styleToRemove) {
styleToRemove.remove()
styleToRemove.parentNode?.removeChild(styleToRemove)
}
}
/**
* 创建弹窗信息
* @param index
* @private
*/
private createPopup(index: number): void {
const tooltipStyle = this.view.tooltip.options.domStyles
const tooltipBackgroundColor = tooltipStyle['l7plot-tooltip']['background-color']
const tooltipFontSize = tooltipStyle['l7plot-tooltip']['font-size']
const style = document.createElement('style')
style.id = 'style-' + this.chart.container
style.innerHTML = `
#${this.chart.container} .l7-popup-content {
background-color: ${tooltipBackgroundColor} !important;
font-size: ${tooltipFontSize};
padding: 10px 10px 6px;
line-height: 1.6;
}
#${this.chart.container} .l7-popup-tip {
border-top-color: ${tooltipBackgroundColor} !important;
}
`
document.head.appendChild(style)
const popupData = this.getPopupData(index)
if (popupData.data) {
let tooltipItem = ''
this.getTooltipItems(popupData.data).forEach(fieldData => {
tooltipItem += `
<li style="list-style-type: none; margin-bottom: 4px; white-space: nowrap; display: flex; justify-content: space-between;">
<span style="${this.objectToSemicolonSeparated(
tooltipStyle['l7plot-tooltip__name']
)}">${fieldData.name}</span>
<span style="${this.objectToSemicolonSeparated(
tooltipStyle['l7plot-tooltip__value']
)}">${fieldData.value}</span>
</li>`
})
const html = `
<div>
<div style="${this.objectToSemicolonSeparated(
tooltipStyle['l7plot-tooltip__title']
)}">${popupData.data.name}</div>
<ul style="${this.objectToSemicolonSeparated(
tooltipStyle['l7plot-tooltip__list']
)}">
${tooltipItem}
</ul>
</div>
`
this.popup.setLngLat({ lng: popupData.centroid[0], lat: popupData.centroid[1] })
this.popup.setHTML(html)
this.popup.closeButton = false
this.view.addLayer(this.popup)
// 地图层高亮
this.view.scene
.getLayers()
?.find(i => i.name === 'highlightLayer')
?.setData(this.getActiveData(index))
if (this.chart.type === 'bubble-map') {
// 气泡地图高亮
const { _id } = this.view.scene
.getLayers()
?.find(i => i.name === 'bubbleLayer')
?.layerSource.data.dataArray.find(i => i.name === this.data[index].name)
this.view.scene
.getLayers()
?.find(i => i.name === 'bubbleLayer' && i.coordCenter)
?.setActive(_id, { color: 'rgba(30,90,255,1)' })
}
}
}
private getActiveData(index): any {
return {
type: 'FeatureCollection',
features: [
this.view.currentDistrictData.features.find(
i => i.properties.name === this.data[index].name
)
]
}
}
/**
* 获取弹窗信息包括原始数据及位置信息
* @param index
* @private
*/
private getPopupData(index: number): any {
return {
data: this.data[index],
centroid: this.view.currentDistrictData.features.find(
i => i.properties.name === this.data[index].name
)?.properties.centroid
}
}
/**
* 将对象转换为 CSS 属性
* @param obj
* @private
*/
private objectToSemicolonSeparated(obj: any): string {
let result = ''
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
result += `${this.convertToSnakeCase(key)}:${obj[key]};`
}
}
return result
}
private cancelHighlightLayer(index?: number): void {
this.view.scene
?.getLayers()
?.find(i => i.name === 'highlightLayer')
?.setData({ type: 'FeatureCollection', features: [] })
if (this.chart.type === 'bubble-map') {
const { _id } = this.view.scene
?.getLayers()
?.find(i => i.name === 'bubbleLayer')
?.layerSource.data.dataArray.find(i => i.name === this.data[index].name)
this.view.scene
.getLayers()
?.find(i => i.name === 'bubbleLayer' && i.coordCenter)
?.setActive(_id, {
color: this.view.scene
.getLayers()
.find(i => i.name === 'bubbleLayer')
.styleAttributeService.getLayerStyleAttribute('color').scale.field
})
}
if (this.chart.type === 'symbolic-map') {
const lngField = this.chart.xAxis[0].dataeaseName
const latField = this.chart.xAxis[1].dataeaseName
const { _id } = this.scene
?.getLayers()
?.find(i => i.type === 'PointLayer')
?.layerSource.data.dataArray.find(i => {
const targetLng = this.data[index][lngField]
const targetLat = this.data[index][latField]
return i[lngField] === targetLng && i[latField] === targetLat
})
this.scene
.getLayers()
?.find(i => i.type === 'PointLayer' && i.coordCenter)
?.setActive(_id, {
color: this.scene
.getLayers()
.find(i => i.type === 'PointLayer')
.styleAttributeService.getLayerStyleAttribute('color').scale.field
})
}
}
/**
* 将驼峰式命名转换为蛇形命名
* @param str
* @private
*/
private convertToSnakeCase(str: string): string {
return str.replace(/([A-Z])/g, match => '-' + match.toLowerCase())
}
/**
* 获取弹窗字段信息
* 与tooltip要显示的内容一致
* @param data
* @private
*/
private getTooltipItems(data) {
const result = []
const customAttr = parseJson(this.chart.customAttr)
const tooltip = customAttr.tooltip
const formatterMap = tooltip.seriesTooltipFormatter
?.filter(i => i.show)
.reduce((pre, next) => {
pre[next.id] = next
return pre
}, {}) as Record<string, SeriesFormatter>
if (isEmpty(formatterMap)) {
return result
}
const head = data
const formatter = formatterMap[head.quotaList?.[0]?.id]
if (!isEmpty(formatter)) {
const originValue = parseFloat(head.value as string)
const value = valueFormatter(originValue, formatter.formatterCfg)
const name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName
result.push({ ...head, name, value: `${value ?? ''}` })
}
head.dynamicTooltipValue?.forEach(item => {
const formatter = formatterMap[item.fieldId]
if (formatter) {
const value = valueFormatter(parseFloat(item.value), formatter.formatterCfg)
const name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName
result.push({ color: 'grey', name, value: `${value ?? ''}` })
}
})
return result
}
/**
* 符号地图特殊处理tooltip的配置可自定义显示内容
* @param index
* @private
*/
private createSymbolicMapPopup(index): void {
const buildTooltip = () => {
const customAttr = this.chart.customAttr ? parseJson(this.chart.customAttr) : null
if (customAttr?.tooltip?.show) {
const { tooltip } = deepCopy(customAttr)
let showFields = tooltip.showFields || []
if (!tooltip.showFields || tooltip.showFields.length === 0) {
showFields = [
...this.chart.xAxisExt.map(i => `${i.dataeaseName}@${i.name}`),
...this.chart.xAxis.map(i => `${i.dataeaseName}@${i.name}`)
]
}
const style = document.createElement('style')
style.id = 'style-' + this.chart.container
style.innerHTML = `
#${this.chart.container} .l7-popup-content {
background-color: ${tooltip.backgroundColor} !important;
padding: 6px 10px 6px;
line-height: 1.6;
}
#${this.chart.container} .l7-popup-tip {
border-top-color: ${tooltip.backgroundColor} !important;
}
`
document.head.appendChild(style)
const lngField = this.chart.xAxis[0].dataeaseName
const latField = this.chart.xAxis[1].dataeaseName
const htmlPrefix = `<div style='font-size:${tooltip.fontSize}px;color:${tooltip.color};'>`
const htmlSuffix = '</div>'
const data = this.view.sourceOption.data[index]
if (data && data.details?.length) {
const fieldData = {
...data,
...Object.fromEntries(mergeDetailsToMap(data.details))
}
const content = buildTooltipContent(tooltip, fieldData, showFields)
const html = `${htmlPrefix}${content}${htmlSuffix}`
this.popup.setLngLat({
lng: data[lngField],
lat: data[latField]
})
this.popup.setHTML(html)
this.popup.closeButton = false
this.scene.addPopup(this.popup)
this.popup.addTo(this.scene)
const { _id } = this.scene
.getLayers()
?.find(i => i.type === 'PointLayer')
?.layerSource.data.dataArray.find(i => {
const targetLng = this.data[index][lngField]
const targetLat = this.data[index][latField]
return i[lngField] === targetLng && i[latField] === targetLat
})
this.scene
.getLayers()
?.find(i => i.type === 'PointLayer' && i.coordCenter)
?.setActive(_id, { color: 'rgba(30,90,255,1)' })
}
}
return undefined
}
/**
* 构建 tooltip 内容
* @param tooltip
* @param fieldData
* @param showFields
* @returns {string}
*/
const buildTooltipContent = (tooltip, fieldData, showFields) => {
let content = ''
if (tooltip.customContent) {
content = tooltip.customContent
showFields.forEach(field => {
content = content.replace(`\${${field.split('@')[1]}}`, fieldData[field.split('@')[0]])
})
} else {
showFields.forEach(field => {
content += `<span style="margin-bottom: 4px">${field.split('@')[1]}: ${
fieldData[field.split('@')[0]]
}</span><br>`
})
}
return content
}
/**
* 合并详情到 map
* @param details
* @returns {Map<string, any>}
*/
const mergeDetailsToMap = details => {
const resultMap = new Map()
details.forEach(item => {
Object.entries(item).forEach(([key, value]) => {
if (resultMap.has(key)) {
const existingValue = resultMap.get(key)
if (existingValue !== value) {
resultMap.set(key, `${existingValue}, ${value}`)
}
} else {
resultMap.set(key, value)
}
})
})
return resultMap
}
buildTooltip()
}
}

View File

@ -893,7 +893,8 @@ export function configL7Tooltip(chart: Chart): TooltipOptions {
domStyles: {
'l7plot-tooltip': {
'background-color': tooltip.backgroundColor,
'font-size': `${tooltip.fontSize}px`
'font-size': `${tooltip.fontSize}px`,
'line-height': 1.6
},
'l7plot-tooltip__name': {
color: tooltip.color