forked from github/dataease
feat(图表): 新增符号地图
This commit is contained in:
parent
bfb8032398
commit
688a157892
@ -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");
|
||||
}
|
||||
|
@ -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-"));
|
||||
|
93
core/core-frontend/src/assets/svg/symbolic-map.svg
Normal file
93
core/core-frontend/src/assets/svg/symbolic-map.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 742 KiB |
@ -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)
|
||||
})
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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: '仅编辑时生效',
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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[]
|
||||
}
|
||||
|
@ -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
|
||||
>
|
||||
|
@ -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"
|
||||
/>
|
||||
|
@ -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">
|
||||
|
@ -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);
|
||||
|
@ -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')"
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
|
@ -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 = {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user