feat(图表): 新增符号地图

This commit is contained in:
jianneng-fit2cloud 2024-06-17 21:49:57 +08:00
parent bfb8032398
commit 688a157892
19 changed files with 796 additions and 26 deletions

View File

@ -214,6 +214,7 @@ public class ChartDataManage {
|| StringUtils.equalsIgnoreCase(view.getType(), "flow-map")
|| StringUtils.equalsIgnoreCase(view.getType(), "sankey")
|| StringUtils.containsIgnoreCase(view.getType(), "chart-mix")
|| StringUtils.equalsIgnoreCase(view.getType(), "symbolic-map")
) {
xAxis.addAll(xAxisExt);
}
@ -740,6 +741,28 @@ public class ChartDataManage {
ExtWhere2Str.extWhere2sqlOjb(sqlMeta, yoyFilterList, transFields(allFields), crossDs, dsMap);
yoySql = SQLProvider.createQuerySQL(sqlMeta, true, needOrder, view);
}
} else if (StringUtils.equalsIgnoreCase("symbolic-map", view.getType())) {
Dimension2SQLObj.dimension2sqlObj(sqlMeta, xAxis, transFields(allFields), crossDs, dsMap);
Quota2SQLObj.quota2sqlObj(sqlMeta, yAxis, transFields(allFields), crossDs, dsMap);
List<ChartViewFieldDTO> yFields = new ArrayList<>();
yFields.addAll(chartViewManege.transFieldDTO(Collections.singletonList(chartViewManege.createCountField(view.getTableId()))));
yFields.addAll(extBubble);
yAxis.addAll(yFields);
Quota2SQLObj.quota2sqlObj(sqlMeta, yAxis, transFields(allFields), crossDs, dsMap);
querySql = SQLProvider.createQuerySQL(sqlMeta, true, needOrder, view);
List<Long> xAxisIds = xAxis.stream().map(ChartViewFieldDTO::getId).toList();
viewFields.addAll(xAxis);
viewFields.addAll(allFields.stream().filter(field -> !xAxisIds.contains(field.getId())).toList());
if (ObjectUtils.isNotEmpty(viewFields)) {
detailFieldList.addAll(viewFields);
SQLMeta sqlMeta1 = new SQLMeta();
BeanUtils.copyBean(sqlMeta1, sqlMeta);
sqlMeta1.setYFields(new ArrayList<>());
Dimension2SQLObj.dimension2sqlObj(sqlMeta1, detailFieldList, transFields(allFields), crossDs, dsMap);
String originSql = SQLProvider.createQuerySQL(sqlMeta1, false, needOrder, view);
String limit = ((pageInfo.getGoPage() != null && pageInfo.getPageSize() != null) ? " LIMIT " + pageInfo.getPageSize() + " OFFSET " + (pageInfo.getGoPage() - 1) * pageInfo.getPageSize() : "");
detailFieldSql = originSql + limit;
}
} else {
Dimension2SQLObj.dimension2sqlObj(sqlMeta, xAxis, transFields(allFields), crossDs, dsMap);
Quota2SQLObj.quota2sqlObj(sqlMeta, yAxis, transFields(allFields), crossDs, dsMap);
@ -782,6 +805,7 @@ public class ChartDataManage {
}
if (StringUtils.isNotBlank(detailFieldSql)) {
detailFieldSql = SqlUtils.rebuildSQL(detailFieldSql, sqlMeta, crossDs, dsMap);
datasourceRequest.setQuery(detailFieldSql);
detailData = (List<String[]>) calciteProvider.fetchResultField(datasourceRequest).get("data");
}

View File

@ -1193,7 +1193,7 @@ public class ChartDataBuild {
Map<String, Object> map = transTableNormal(fields, null, data, desensitizationList);
List<Map<String, Object>> tableRow = (List<Map<String, Object>>) map.get("tableRow");
final int xEndIndex = detailIndex;
Map<String, List<String[]>> groupDataList = detailData.stream().collect(Collectors.groupingBy(item -> "(" + StringUtils.join(ArrayUtils.subarray(item, 0, xEndIndex), "-de-") + ")"));
Map<String, List<String[]>> groupDataList = detailData.stream().collect(Collectors.groupingBy(item -> "(" + StringUtils.join(ArrayUtils.subarray(item, 0, xEndIndex), ")-de-(") + ")"));
tableRow.forEach(row -> {
String key = xAxis.stream().map(x -> String.format(format, row.get(x.getDataeaseName()).toString())).collect(Collectors.joining("-de-"));

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 742 KiB

View File

@ -294,7 +294,15 @@ const active = computed(() => {
})
const boardMoveActive = computed(() => {
const CHARTS = ['flow-map', 'map', 'bubble-map', 'table-info', 'table-normal', 'table-pivot']
const CHARTS = [
'flow-map',
'map',
'bubble-map',
'table-info',
'table-normal',
'table-pivot',
'symbolic-map'
]
return CHARTS.includes(element.value.innerType)
})

View File

@ -175,7 +175,8 @@ const state = reactive({
'label',
'word-cloud',
'flow-map',
'bidirectional-bar'
'bidirectional-bar',
'symbolic-map'
],
linkageExcludeViewType: [
'richTextView',
@ -185,7 +186,8 @@ const state = reactive({
'label',
'word-cloud',
'flow-map',
'bidirectional-bar'
'bidirectional-bar',
'symbolic-map'
],
copyData: null,
hyperlinksSetVisible: false,

View File

@ -1229,7 +1229,12 @@ export default {
progress_current: '实际值',
gauge_axis_label: '显示刻度',
gauge_percentage_tick: '百分比刻度',
add_style: '添加样式'
add_style: '添加样式',
map_symbol_marker: '标记',
map_symbol_pentagon: '五角形',
map_symbol_hexagon: '六角形',
map_symbol_octagon: '八角形',
map_symbol_hexagram: '菱形'
},
dataset: {
scope_edit: '仅编辑时生效',

View File

@ -698,6 +698,18 @@ declare interface ChartLabelAttr {
*/
seriesLabelFormatter: SeriesFormatter[]
/**
* 显示字段通过字段名称显示对应的值
* @example
* ['name', 'value']
*/
showFields?: string[]
/**
* 自定义显示内容
*/
customContent?: string
showGap?: boolean
}
/**
@ -732,6 +744,18 @@ declare interface ChartTooltipAttr {
seriesTooltipFormatter: SeriesFormatter[]
showGap?: boolean
/**
* 显示字段通过字段名称显示对应的值
* @example
* ['name', 'value']
*/
showFields?: string[]
/**
* 自定义显示内容
*/
customContent?: string
}
/**

View File

@ -24,6 +24,7 @@ declare type EditorProperty =
| 'indicator-value-selector'
| 'indicator-name-selector'
| 'quadrant-selector'
| 'map-symbolic-selector'
declare type EditorPropertyInner = {
[key in EditorProperty]?: string[]
}

View File

@ -20,7 +20,7 @@ const props = defineProps({
v-else-if="
props.view.type &&
(includesAny(props.view.type, 'bar', 'line', 'scatter') ||
equalsAny(props.view.type, 'waterfall', 'area', 'area-stack', 'flow-map'))
equalsAny(props.view.type, 'waterfall', 'area', 'area-stack', 'flow-map', 'symbolic-map'))
"
>{{ t('chart.drag_block_value_axis') }}</span
>

View File

@ -75,6 +75,10 @@ const props = defineProps({
default: () => {
return {}
}
},
allFields: {
type: Array,
required: true
}
})
@ -350,6 +354,7 @@ watch(
:themes="themes"
class="attr-selector"
:chart="chart"
:all-fields="props.allFields"
@onLabelChange="onLabelChange"
/>
</collapse-switch-item>
@ -368,6 +373,7 @@ watch(
:property-inner="propertyInnerAll['tooltip-selector']"
:themes="themes"
:chart="chart"
:all-fields="props.allFields"
@onTooltipChange="onTooltipChange"
@onExtTooltipChange="onExtTooltipChange"
/>

View File

@ -198,6 +198,16 @@ const flowLineTypeOptions = [
{ name: t('chart.map_line_type_arc_3d'), value: 'arc3d' }
]
const mapSymbolOptions = [
{ name: t('chart.line_symbol_circle'), value: 'circle' },
{ name: t('chart.line_symbol_rect'), value: 'square' },
{ name: t('chart.line_symbol_triangle'), value: 'triangle' },
{ name: t('chart.map_symbol_pentagon'), value: 'pentagon' },
{ name: t('chart.map_symbol_hexagon'), value: 'hexagon' },
{ name: t('chart.map_symbol_octagon'), value: 'octogon' },
{ name: t('chart.line_symbol_diamond'), value: 'rhombus' }
]
onMounted(() => {
init()
})
@ -326,7 +336,7 @@ onMounted(() => {
/>
</el-select>
</el-form-item>
<div class="map-style" v-if="showProperty('mapStyle') || showProperty('heatMapStyle')">
<div class="map-style" v-if="showProperty('mapBaseStyle') || showProperty('heatMapStyle')">
<el-row style="flex: 1">
<el-col>
<el-form-item
@ -337,7 +347,7 @@ onMounted(() => {
<el-select
:effect="themes"
v-model="state.basicStyleForm.mapStyle"
@change="changeBasicStyle('mapStyle')"
@change="changeBasicStyle('mapBaseStyle')"
>
<el-option
v-for="item in mapStyleOptions"
@ -368,7 +378,7 @@ onMounted(() => {
</el-row>
</div>
</div>
<div class="map-flow-style" v-if="showProperty('mapStyle')">
<div class="map-flow-style" v-if="showProperty('mapLineStyle')">
<el-row style="flex: 1">
<el-col>
<el-form-item
@ -588,6 +598,80 @@ onMounted(() => {
</el-col>
</el-row>
</div>
<div class="map-flow-style" v-if="showProperty('symbolicMapStyle')">
<el-row style="flex: 1">
<el-col>
<el-form-item :label="'符号形状'" class="form-item" :class="'form-item-' + themes">
<el-select
:effect="themes"
v-model="state.basicStyleForm.mapSymbol"
@change="changeBasicStyle('mapSymbol')"
>
<el-option
v-for="item in mapSymbolOptions"
:key="item.name"
:label="item.name"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<div class="alpha-setting">
<label class="alpha-label" :class="{ dark: 'dark' === themes }">
{{ t('chart.size') }}
</label>
<el-row style="flex: 1">
<el-col>
<el-form-item class="form-item alpha-slider" :class="'form-item-' + themes">
<el-slider
:effect="themes"
:min="1"
:max="40"
v-model="state.basicStyleForm.mapSymbolSize"
@change="changeBasicStyle('mapSymbolSize')"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<div class="alpha-setting">
<label class="alpha-label" :class="{ dark: 'dark' === themes }">
{{ t('chart.not_alpha') }}
</label>
<el-row style="flex: 1">
<el-col>
<el-form-item class="form-item alpha-slider" :class="'form-item-' + themes">
<el-slider
:effect="themes"
:min="1"
:max="10"
v-model="state.basicStyleForm.mapSymbolOpacity"
@change="changeBasicStyle('mapSymbolOpacity')"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<div class="alpha-setting">
<label class="alpha-label" :class="{ dark: 'dark' === themes }">
{{ t('visualization.borderWidth') }}
</label>
<el-row style="flex: 1">
<el-col>
<el-form-item class="form-item alpha-slider" :class="'form-item-' + themes">
<el-slider
:effect="themes"
:min="1"
:max="5"
v-model="state.basicStyleForm.mapSymbolStrokeWidth"
@change="changeBasicStyle('mapSymbolStrokeWidth')"
/>
</el-form-item>
</el-col>
</el-row>
</div>
</div>
<!--flow map end-->
<!--map start-->
<el-row :gutter="8">

View File

@ -1,14 +1,16 @@
<script lang="ts" setup>
import { computed, onMounted, PropType, reactive, ref, watch } from 'vue'
import { computed, inject, onMounted, PropType, reactive, ref, watch } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { COLOR_PANEL, DEFAULT_LABEL } from '@/views/chart/components/editor/util/chart'
import { ElSpace } from 'element-plus-secondary'
import { ElIcon, ElSpace } from 'element-plus-secondary'
import { formatterType, unitType } from '../../../js/formatter'
import { defaultsDeep, cloneDeep, intersection, union, defaultTo } from 'lodash-es'
import { includesAny } from '../../util/StringUtils'
import { fieldType } from '@/utils/attr'
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
import { storeToRefs } from 'pinia'
import Icon from '../../../../../../components/icon-custom/src/Icon.vue'
import { deepCopy } from '@/utils/utils'
const { t } = useI18n()
@ -17,10 +19,22 @@ const props = defineProps({
type: Object as PropType<ChartObj>,
required: true
},
dimensionData: {
type: Array<any>,
required: false
},
quotaData: {
type: Array<any>,
required: false
},
themes: {
type: String as PropType<EditorTheme>,
default: 'dark'
},
allFields: {
type: Array<any>,
required: false
},
propertyInner: {
type: Array<string>
}
@ -48,6 +62,7 @@ watch(
},
{ deep: true }
)
const curSeriesFormatter = ref<Partial<SeriesFormatter>>({})
const formatterEditable = computed(() => {
return showProperty('seriesLabelFormatter') && yAxis.value?.length
@ -150,7 +165,6 @@ const state = reactive<{ labelForm: ChartLabelAttr | any }>({
})
const emit = defineEmits(['onLabelChange'])
const changeLabelAttr = prop => {
emit('onLabelChange', state.labelForm, prop)
}
@ -263,6 +277,25 @@ watch(
{ deep: true }
)
const allFields = computed(() => {
return defaultTo(props.allFields, []).map(item => ({
key: item.dataeaseName,
name: item.name,
value: `${item.dataeaseName}@${item.name}`,
disabled: false
}))
})
const defaultPlaceholder = computed(() => {
if (state.labelForm.showFields && state.labelForm.showFields.length > 0) {
return state.labelForm.showFields
.map(field => {
return '${' + field.split('@')[1] + '}'
})
.join(',')
}
return ''
})
onMounted(() => {
init()
})
@ -275,7 +308,7 @@ onMounted(() => {
:model="state.labelForm"
label-position="top"
>
<el-row v-show="showEmpty" style="margin-bottom: 12px"> 无其他可设置的属性 </el-row>
<el-row v-show="showEmpty" style="margin-bottom: 12px"> 无其他可设置的属性</el-row>
<el-space>
<el-form-item
class="form-item"
@ -317,10 +350,56 @@ onMounted(() => {
</el-tooltip>
</el-form-item>
</el-space>
<div v-if="showProperty('showFields')">
<el-form-item :label="t('chart.label')" class="form-item" :class="'form-item-' + themes">
<el-select
size="small"
:effect="themes"
filterable
multiple
collapse-tags
collapse-tags-tooltip
v-model="state.labelForm.showFields"
@change="changeLabelAttr('showFields')"
>
<el-option
v-for="option in allFields"
:key="option.key"
:label="option.name"
:value="option.value"
:disabled="option.disabled"
/>
</el-select>
</el-form-item>
<el-form-item v-if="showProperty('customContent')" class="form-item">
<template #label>
<span class="data-area-label">
<span>
{{ t('chart.content_formatter') }}
</span>
<el-tooltip class="item" :effect="toolTip" placement="bottom">
<template #content>
<div>可以${fieldName}的形式读取字段值不支持换行</div>
</template>
<el-icon class="hint-icon" :class="{ 'hint-icon--dark': themes === 'dark' }">
<Icon name="icon_info_outlined" />
</el-icon>
</el-tooltip>
</span>
</template>
<el-input
style="font-size: smaller; font-weight: normal"
v-model="state.labelForm.customContent"
type="textarea"
:autosize="{ minRows: 4, maxRows: 4 }"
:placeholder="defaultPlaceholder"
@blur="changeLabelAttr('customContent')"
/>
</el-form-item>
</div>
<el-form-item
v-if="showProperty('rPosition')"
:label="t('chart.label_position')"
:label="t('chart.label')"
class="form-item"
:class="'form-item-' + themes"
>
@ -876,22 +955,26 @@ onMounted(() => {
.form-item-checkbox {
margin-bottom: 8px !important;
}
.series-select {
:deep(.ed-select__prefix--light) {
padding-right: unset;
border-right: unset;
}
:deep(.ed-select__prefix--dark) {
padding-right: unset;
border-right: unset;
}
}
.series-select-option {
display: flex;
align-items: center;
justify-content: flex-start;
padding: 0 11px;
}
.m-divider {
margin: 0 0 16px;
border-color: rgba(31, 35, 41, 0.15);

View File

@ -2,16 +2,18 @@
import { PropType, computed, onMounted, reactive, watch, ref, inject } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { COLOR_PANEL, DEFAULT_TOOLTIP } from '@/views/chart/components/editor/util/chart'
import { ElSpace } from 'element-plus-secondary'
import { ElIcon, ElSpace } from 'element-plus-secondary'
import cloneDeep from 'lodash-es/cloneDeep'
import defaultsDeep from 'lodash-es/defaultsDeep'
import { formatterType, unitType } from '../../../js/formatter'
import { fieldType } from '@/utils/attr'
import { partition } from 'lodash-es'
import { defaultTo, partition } from 'lodash-es'
import chartViewManager from '../../../js/panel'
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
import { storeToRefs } from 'pinia'
import { useEmitt } from '@/hooks/web/useEmitt'
import Icon from '../../../../../../components/icon-custom/src/Icon.vue'
import { deepCopy } from '@/utils/utils'
const { t } = useI18n()
@ -24,6 +26,10 @@ const props = defineProps({
type: String as PropType<EditorTheme>,
default: 'dark'
},
allFields: {
type: Array<any>,
required: false
},
propertyInner: {
type: Array<string>
}
@ -40,6 +46,7 @@ const quotaData = ref<Axis[]>(inject('quotaData'))
const showSeriesTooltipFormatter = computed(() => {
return showProperty('seriesTooltipFormatter') && !batchOptStatus.value && props.chart.id
})
//
const changeChartType = () => {
if (!showSeriesTooltipFormatter.value) {
@ -89,6 +96,7 @@ const changeDataset = () => {
}
})
}
const AXIS_PROP: AxisType[] = ['yAxis', 'yAxisExt', 'extBubble']
const quotaAxis = computed(() => {
let result = []
@ -356,6 +364,20 @@ const updateAxis = (form: AxisEditForm) => {
}
})
}
const allFields = computed(() => {
return defaultTo(props.allFields, [])
})
const defaultPlaceholder = computed(() => {
if (state.tooltipForm.showFields && state.tooltipForm.showFields.length > 0) {
return state.tooltipForm.showFields
.map(field => {
const v = field.split('@')
return v[1] + ': ${' + field.split('@')[1] + '}'
})
.join('\n')
}
return ''
})
onMounted(() => {
init()
useEmitt({ name: 'addAxis', callback: updateSeriesTooltipFormatter })
@ -374,6 +396,7 @@ onMounted(() => {
label-position="top"
>
<el-form-item
v-if="props.chart.type !== 'symbolic-map'"
:label="t('chart.background') + t('chart.color')"
class="form-item"
:class="'form-item-' + themes"
@ -430,6 +453,54 @@ onMounted(() => {
</el-tooltip>
</el-form-item>
</el-space>
<div v-if="showProperty('showFields')">
<el-form-item :label="t('chart.tooltip')" class="form-item" :class="'form-item-' + themes">
<el-select
size="small"
:effect="themes"
filterable
multiple
collapse-tags
collapse-tags-tooltip
v-model="state.tooltipForm.showFields"
@change="changeTooltipAttr('showFields')"
>
<el-option
v-for="option in allFields"
:key="option.dataeaseName"
:label="option.name"
:value="option.dataeaseName + '@' + option.name"
/>
</el-select>
</el-form-item>
<el-form-item v-if="showProperty('customContent')" class="form-item">
<template #label>
<span class="data-area-label">
<span>
{{ t('chart.content_formatter') }}
</span>
<el-tooltip class="item" :effect="toolTip" placement="bottom">
<template #content>
<div>可以${fieldName}的形式读取字段值支持HTML</div>
</template>
<el-icon class="hint-icon" :class="{ 'hint-icon--dark': themes === 'dark' }">
<Icon name="icon_info_outlined" />
</el-icon>
</el-tooltip>
</span>
</template>
<el-input
style="font-size: smaller; font-weight: normal"
v-model="state.tooltipForm.customContent"
type="textarea"
:autosize="{ minRows: 4, maxRows: 4 }"
:placeholder="defaultPlaceholder"
@blur="changeTooltipAttr('customContent')"
/>
</el-form-item>
</div>
<template v-if="showProperty('tooltipFormatter') && !isBarRangeTime">
<el-form-item
:label="t('chart.value_formatter_type')"

View File

@ -2458,6 +2458,7 @@ const deleteChartFieldItem = id => {
:themes="themes"
:dimension-data="state.dimension"
:quota-data="state.quota"
:all-fields="allFields"
@onColorChange="onColorChange"
@onMiscChange="onMiscChange"
@onLabelChange="onLabelChange"

View File

@ -1325,6 +1325,13 @@ export const CHART_TYPE_CONFIGS = [
value: 'heat-map',
title: t('chart.chart_heat_map'),
icon: 'heat-map'
},
{
render: 'antv',
category: 'map',
value: 'symbolic-map',
title: '符号地图',
icon: 'symbolic-map'
}
]
},
@ -1454,7 +1461,7 @@ export const DEFAULT_BASIC_STYLE: ChartBasicStyle = {
mapSymbolOpacity: 0.7,
mapSymbolStrokeWidth: 2,
mapSymbol: 'circle',
mapSymbolSize: 20,
mapSymbolSize: 6,
radius: 80,
innerRadius: 60,
showZoom: true,

View File

@ -25,7 +25,7 @@ export class FlowMap extends L7ChartView<Scene, L7Config> {
]
propertyInner: EditorPropertyInner = {
...MAP_EDITOR_PROPERTY_INNER,
'basic-style-selector': ['mapStyle', 'zoom']
'basic-style-selector': ['mapBaseStyle', 'mapLineStyle', 'zoom']
}
axis: AxisType[] = ['xAxis', 'xAxisExt', 'filter']
axisConfig: AxisConfig = {

View File

@ -0,0 +1,339 @@
import { useI18n } from '@/hooks/web/useI18n'
import {
L7ChartView,
L7Config,
L7DrawConfig,
L7Wrapper
} from '@/views/chart/components/js/panel/types/impl/l7'
import { MAP_EDITOR_PROPERTY_INNER } from '@/views/chart/components/js/panel/charts/map/common'
import { flow, hexColorToRGBA, parseJson } from '@/views/chart/components/js/util'
import { deepCopy } from '@/utils/utils'
import { GaodeMap } from '@antv/l7-maps'
import { Scene } from '@antv/l7-scene'
import { PointLayer } from '@antv/l7-layers'
import { LayerPopup } from '@antv/l7'
import { queryMapKeyApi } from '@/api/setting/sysParameter'
const { t } = useI18n()
/**
* 符号地图
*/
export class SymbolicMap extends L7ChartView<Scene, L7Config> {
properties: EditorProperty[] = [
'background-overall-component',
'basic-style-selector',
'title-selector',
'label-selector',
'tooltip-selector',
'jump-set',
'linkage'
]
propertyInner: EditorPropertyInner = {
...MAP_EDITOR_PROPERTY_INNER,
'basic-style-selector': ['colors', 'alpha', 'mapBaseStyle', 'symbolicMapStyle', 'zoom'],
'label-selector': ['color', 'fontSize', 'showFields', 'customContent'],
'tooltip-selector': ['color', 'fontSize', 'showFields', 'customContent', 'show']
}
axis: AxisType[] = ['xAxis', 'xAxisExt', 'extBubble', 'filter', 'extLabel', 'extTooltip']
axisConfig: AxisConfig = {
xAxis: {
name: `经纬度 / ${t('chart.dimension')}`,
type: 'd',
limit: 2
},
xAxisExt: {
name: `颜色 / ${t('chart.dimension')}`,
type: 'd',
limit: 1
},
extBubble: {
name: `${t('chart.bubble_size')} / ${t('chart.quota')}`,
type: 'q',
limit: 1
}
}
constructor() {
super('symbolic-map', [])
}
async drawChart(drawOption: L7DrawConfig<L7Config>) {
const { chart, container, action } = drawOption
const xAxis = deepCopy(chart.xAxis)
const xAxisExt = deepCopy(chart.xAxisExt)
let basicStyle
let miscStyle
if (chart.customAttr) {
basicStyle = parseJson(chart.customAttr).basicStyle
miscStyle = parseJson(chart.customAttr).misc
}
const mapStyle = `amap://styles/${basicStyle.mapStyle ? basicStyle.mapStyle : 'normal'}`
const key = await this.getMapKey()
// 底层
const scene = new Scene({
id: container,
logoVisible: false,
map: new GaodeMap({
token: key ?? undefined,
style: mapStyle,
pitch: miscStyle.mapPitch,
center: [104.434765, 38.256735],
zoom: 1.8
})
})
if (xAxis?.length < 2 || xAxisExt?.length < 1) {
return new L7Wrapper(scene, undefined)
}
const configList: L7Config[] = []
const symbolicLayer = this.buildSymbolicLayer(chart, basicStyle)
configList.push(symbolicLayer)
const tooltipLayer = this.buildTooltip(chart, symbolicLayer)
if (tooltipLayer) {
scene.addPopup(tooltipLayer)
}
this.buildLabel(chart, configList)
this.configZoomButton(chart, scene)
symbolicLayer.on('click', ev => {
const data = ev.feature
action({
x: ev.x,
y: ev.y,
data: {
data: {
...data,
dimensionList: chart.data.data.filter(item => item.field === ev.feature.field)?.[0]
?.dimensionList,
quotaList: chart.data.data.filter(item => item.field === ev.feature.field)?.[0]
?.quotaList
}
}
})
})
return new L7Wrapper(scene, configList)
}
/**
* 构建符号图层
* @param chart
*/
buildSymbolicLayer = (chart, basicStyle) => {
const xAxis = deepCopy(chart.xAxis)
const xAxisExt = deepCopy(chart.xAxisExt)
const extBubble = deepCopy(chart.extBubble)
const { mapSymbolOpacity, mapSymbolSize, mapSymbol, mapSymbolStrokeWidth, colors, alpha } =
deepCopy(basicStyle)
const c = []
colors.forEach(ele => {
c.push(hexColorToRGBA(ele, alpha))
})
const sizeKey = extBubble.length > 0 ? extBubble[0].dataeaseName : ''
const data = chart.data?.tableRow
? chart.data?.tableRow.map((item, index) => ({
...item,
color: c[index % c.length],
size: item[sizeKey] ? item[sizeKey] : mapSymbolSize,
field:
item[xAxis[0].dataeaseName] +
'000\n' +
item[xAxis[1].dataeaseName] +
'000\n' +
item[xAxisExt[0].dataeaseName],
name: item[xAxisExt[0].dataeaseName]
}))
: []
const color = xAxisExt && xAxisExt.length > 0 ? 'color' : c[0]
const pointLayer = new PointLayer()
.source(data, {
parser: {
type: 'json',
x: xAxis[0].dataeaseName,
y: xAxis[1].dataeaseName
}
})
.shape(mapSymbol)
.color(color)
.style({
stroke: {
field: 'color'
},
strokeWidth: mapSymbolStrokeWidth,
opacity: {
field: (mapSymbolOpacity / 100) * 10
}
})
.active(true)
if (sizeKey) {
pointLayer.size('size', [4, 30])
} else {
pointLayer.size(mapSymbolSize)
}
return pointLayer
}
/**
* 合并详情到 map
* @param details
* @returns {Map<string, any>}
*/
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
}
/**
* 构建 tooltip
* @param chart
* @param pointLayer
*/
buildTooltip = (chart, pointLayer) => {
const customAttr = chart.customAttr ? parseJson(chart.customAttr) : null
if (customAttr?.tooltip?.show) {
const { tooltip } = deepCopy(customAttr)
let showFields = tooltip.showFields || []
if (!tooltip.showFields || tooltip.showFields.length === 0) {
showFields = [
...chart.xAxisExt.map(i => `${i.dataeaseName}@${i.name}`),
...chart.xAxis.map(i => `${i.dataeaseName}@${i.name}`)
]
}
const htmlPrefix = `<div style='font-size:${tooltip.fontSize}px;color:${tooltip.color}'>`
const htmlSuffix = '</div>'
return new LayerPopup({
items: [
{
layer: pointLayer,
customContent: item => {
const fieldData = {
...item,
...Object.fromEntries(this.mergeDetailsToMap(item.details))
}
const content = this.buildTooltipContent(tooltip, fieldData, showFields)
return `${htmlPrefix}${content}${htmlSuffix}`
}
}
],
trigger: 'hover'
})
}
return undefined
}
/**
* 构建 tooltip 内容
* @param tooltip
* @param fieldData
* @param showFields
* @returns {string}
*/
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 => {
//const value = ${fieldData[field.split('@')[0]] as string
content += `${field.split('@')[1]}: ${fieldData[field.split('@')[0]]}<br>`
})
}
return content
}
/**
* 构建 label
* @param chart
* @param configList
*/
buildLabel = (chart, configList) => {
const xAxis = deepCopy(chart.xAxis)
const customAttr = chart.customAttr ? parseJson(chart.customAttr) : null
if (customAttr?.label?.show) {
const { label } = customAttr
const data = chart.data?.tableRow || []
let showFields = label.showFields || []
if (!label.showFields || label.showFields.length === 0) {
showFields = [
...chart.xAxisExt.map(i => `${i.dataeaseName}@${i.name}`),
...chart.xAxis.map(i => `${i.dataeaseName}@${i.name}`)
]
}
data.forEach(item => {
const fieldData = {
...item,
...Object.fromEntries(this.mergeDetailsToMap(item.details))
}
let content = label.customContent || ''
if (content) {
showFields.forEach(field => {
const [fieldKey, fieldName] = field.split('@')
content = content.replace(`\${${fieldName}}`, fieldData[fieldKey])
})
} else {
content = showFields.map(field => fieldData[field.split('@')[0]]).join(',')
}
content = content.replace(/\n/g, '')
item.textLayerContent = content
})
configList.push(
new PointLayer()
.source(data, {
parser: {
type: 'json',
x: xAxis[0].dataeaseName,
y: xAxis[1].dataeaseName
}
})
.shape('textLayerContent', 'text')
.color(label.color)
.size(label.fontSize)
.style({
textAnchor: 'center',
textOffset: [0, 0]
})
)
}
}
getMapKey = async () => {
const key = 'online-map-key'
if (!localStorage.getItem(key)) {
await queryMapKeyApi().then(res => localStorage.setItem(key, res.data))
}
return localStorage.getItem(key)
}
setupDefaultOptions(chart: ChartObj): ChartObj {
chart.customAttr.label = {
...chart.customAttr.label,
show: false
}
chart.customAttr.basicStyle = {
...chart.customAttr.basicStyle,
mapSymbolOpacity: 5
}
return chart
}
protected setupOptions(chart: Chart, config: L7Config): L7Config {
return flow(this.configEmptyDataStrategy, this.configTooltip, this.configLabel)(chart, config)
}
}

View File

@ -5,10 +5,14 @@ import {
ChartLibraryType,
ChartWrapper
} from '@/views/chart/components/js/panel/types'
import { cloneDeep } from 'lodash-es'
import { cloneDeep, defaultsDeep } from 'lodash-es'
import { parseJson } from '@/views/chart/components/js/util'
import { ILayer } from '@antv/l7plot'
import { configL7Zoom } from '@/views/chart/components/js/panel/common/common_antv'
import {
configL7Label,
configL7Tooltip,
configL7Zoom
} from '@/views/chart/components/js/panel/common/common_antv'
export type L7DrawConfig<P> = AntVDrawOptions<P>
export interface L7Config extends ILayer {
@ -48,10 +52,12 @@ export class L7Wrapper<
}
handleConfig = (config: L7Config) => {
if (config.handleConfig) {
config.handleConfig?.(this.scene)
} else {
this.scene.addLayer(config)
if (config) {
if (config.handleConfig) {
config.handleConfig?.(this.scene)
} else {
this.scene.addLayer(config)
}
}
}
}
@ -88,6 +94,18 @@ export abstract class L7ChartView<
configL7Zoom(chart, plot)
}
protected configLabel(chart: Chart, options: O): O {
const label = configL7Label(chart)
defaultsDeep(options.label, label)
return options
}
protected configTooltip(chart: Chart, options: O): O {
const tooltip = configL7Tooltip(chart)
defaultsDeep(options.tooltip, tooltip)
return options
}
protected constructor(name: string, defaultData: any[]) {
super(ChartLibraryType.L7, name, defaultData)
}

View File

@ -306,7 +306,11 @@ const action = param => {
}
trackBarStyleCheck(props.element, barStyleTemp, props.scale, trackMenu.value.length)
state.trackBarStyle.left = barStyleTemp.left + 'px'
state.trackBarStyle.top = barStyleTemp.top + 'px'
if (curView.type === 'symbolic-map') {
state.trackBarStyle.top = param.y + 10 + 'px'
} else {
state.trackBarStyle.top = barStyleTemp.top + 'px'
}
viewTrack.value.trackButtonClick()
}
}