feat(chart): 支持地图图例数值格式化及分界点编辑
This commit is contained in:
@@ -1314,6 +1314,17 @@ export default {
|
||||
heatmap_range_invalid: 'Enter finite numbers with minimum less than maximum',
|
||||
heatmap_min: 'Minimum',
|
||||
heatmap_max: 'Maximum',
|
||||
map_boundary: 'Boundary',
|
||||
map_boundary_add: 'Add boundary',
|
||||
map_boundary_delete: 'Delete boundary',
|
||||
map_range_count_auto: 'Automatic',
|
||||
map_boundary_invalid: 'Boundaries must be finite numbers in strictly increasing order.',
|
||||
map_boundary_cannot_insert: 'Cannot insert. Check the boundaries or increase the gap.',
|
||||
map_boundary_hint: 'Up to 9 ranges. The minimum and maximum cannot be deleted.',
|
||||
map_format_legacy: 'Original format (integer)',
|
||||
map_format_sync_tooltip: 'Use legend format for tooltip metric',
|
||||
map_range_label: 'Range label',
|
||||
map_range_label_auto: 'Leave blank to show numeric range',
|
||||
heatmap_format: 'Legend number format',
|
||||
heatmap_format_inherit: 'Follow measure',
|
||||
heatmap_format_custom: 'Custom format',
|
||||
|
||||
@@ -1259,6 +1259,17 @@ export default {
|
||||
heatmap_range_invalid: '最小值必須小於最大值,且均為有效數值',
|
||||
heatmap_min: '最小值',
|
||||
heatmap_max: '最大值',
|
||||
map_boundary: '分界點',
|
||||
map_boundary_add: '新增分界點',
|
||||
map_boundary_delete: '刪除分界點',
|
||||
map_range_count_auto: '自動計算',
|
||||
map_boundary_invalid: '邊界必須是有效數值,且嚴格遞增。',
|
||||
map_boundary_cannot_insert: '無法插入,請檢查邊界數值或增大間隔。',
|
||||
map_boundary_hint: '最多 9 個區間,最小值和最大值不可刪除。',
|
||||
map_format_legacy: '原始格式(整數)',
|
||||
map_format_sync_tooltip: '提示框指標同步圖例格式',
|
||||
map_range_label: '區間顯示名稱',
|
||||
map_range_label_auto: '留空顯示數值範圍',
|
||||
heatmap_format: '圖例數值格式',
|
||||
heatmap_format_inherit: '跟隨指標',
|
||||
heatmap_format_custom: '自訂格式',
|
||||
|
||||
@@ -1277,6 +1277,17 @@ export default {
|
||||
heatmap_range_invalid: '最小值必须小于最大值,且均为有效数值',
|
||||
heatmap_min: '最小值',
|
||||
heatmap_max: '最大值',
|
||||
map_boundary: '分界点',
|
||||
map_boundary_add: '添加分界点',
|
||||
map_boundary_delete: '删除分界点',
|
||||
map_range_count_auto: '自动计算',
|
||||
map_boundary_invalid: '边界必须是有效数值,且严格递增。',
|
||||
map_boundary_cannot_insert: '无法插入,请检查边界数值或增大间隔。',
|
||||
map_boundary_hint: '最多 9 个区间,最小值和最大值不可删除。',
|
||||
map_format_legacy: '原始格式(整数)',
|
||||
map_format_sync_tooltip: '提示框指标同步图例格式',
|
||||
map_range_label: '区间显示名称',
|
||||
map_range_label_auto: '留空显示数值范围',
|
||||
heatmap_format: '图例数值格式',
|
||||
heatmap_format_inherit: '跟随指标',
|
||||
heatmap_format_custom: '自定义格式',
|
||||
|
||||
@@ -106,7 +106,16 @@ declare interface HeatmapLegendOptions {
|
||||
prefix?: string
|
||||
}
|
||||
|
||||
declare interface MapLegendOptions {
|
||||
formatMode?: 'legacy' | 'inherit' | 'custom'
|
||||
formatterCfg?: BaseFormatter
|
||||
prefix?: string
|
||||
syncTooltip?: boolean
|
||||
rangeLabels?: Record<string, string>
|
||||
}
|
||||
|
||||
declare interface ChartLegendStyle {
|
||||
map?: MapLegendOptions
|
||||
heatmap?: HeatmapLegendOptions
|
||||
displayMode?: 'pagination' | 'tile'
|
||||
tileOverflow?: 'scroll' | 'adaptive'
|
||||
|
||||
@@ -20,6 +20,9 @@ import CustomSortEdit from '@/views/chart/components/editor/drag-item/components
|
||||
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import chartViewManager from '@/views/chart/components/js/panel'
|
||||
import { mapRangeKey } from '@/views/chart/components/js/panel/charts/map/mapLegend'
|
||||
import MapRangeEditor from './MapRangeEditor.vue'
|
||||
import MapLegendEditor from './MapLegendEditor.vue'
|
||||
import HeatmapLegendEditor from './HeatmapLegendEditor.vue'
|
||||
const dvMainStore = dvMainStoreWithOut()
|
||||
const { batchOptStatus } = storeToRefs(dvMainStore)
|
||||
@@ -121,6 +124,11 @@ const changeHeatmapLegend = (value: HeatmapLegendOptions) => {
|
||||
changeLegendStyle('heatmap')
|
||||
}
|
||||
|
||||
const changeMapLegend = (value: MapLegendOptions) => {
|
||||
state.legendForm.map = value
|
||||
changeLegendStyle('map')
|
||||
}
|
||||
|
||||
const changeMisc = prop => {
|
||||
// 仅对子弹图区间图例字段做合并保护,避免覆盖 fixedRange/showType。
|
||||
if (typeof prop === 'string' && prop.startsWith('bullet.')) {
|
||||
@@ -163,103 +171,60 @@ const init = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 存储地图默认的最大最小值
|
||||
const mapLegendDefaultRange = {
|
||||
max: 0,
|
||||
min: 0
|
||||
}
|
||||
// 缓存原始的区间数据
|
||||
let mapLegendCustomRangeCacheList = []
|
||||
const showProperty = prop => props.propertyInner?.includes(prop)
|
||||
const mapDefaultRange = args => {
|
||||
if (args.from === 'map') {
|
||||
const rangeCustom = state.legendForm.miscForm.mapLegendRangeType === 'custom'
|
||||
if (!rangeCustom) {
|
||||
state.legendForm.miscForm.mapLegendMax = cloneDeep(args.data.max)
|
||||
state.legendForm.miscForm.mapLegendMin = cloneDeep(args.data.min)
|
||||
}
|
||||
state.legendForm.miscForm.mapLegendNumber = cloneDeep(args.data.legendNumber)
|
||||
mapLegendCustomRangeCacheList = []
|
||||
mapLegendDefaultRange.max = cloneDeep(args.data.max)
|
||||
mapLegendDefaultRange.min = cloneDeep(args.data.min)
|
||||
const customRange = getDynamicColorScale(
|
||||
mapLegendDefaultRange.min,
|
||||
mapLegendDefaultRange.max,
|
||||
args.data.legendNumber
|
||||
)
|
||||
customRange.forEach((item, index) => {
|
||||
if (index === 0) {
|
||||
mapLegendCustomRangeCacheList.push(...item.value)
|
||||
} else {
|
||||
mapLegendCustomRangeCacheList.push(item.value[1])
|
||||
}
|
||||
})
|
||||
if (args.from !== 'map' || String(args.chartId) !== String(props.chart.id)) return
|
||||
if (state.legendForm.miscForm.mapLegendRangeType !== 'custom') {
|
||||
state.legendForm.miscForm.mapLegendMax = args.data.max
|
||||
state.legendForm.miscForm.mapLegendMin = args.data.min
|
||||
}
|
||||
}
|
||||
const initMapCustomRange = () => {
|
||||
const legendCustom = state.legendForm.miscForm.mapAutoLegend
|
||||
const rangeCustom = state.legendForm.miscForm.mapLegendRangeType === 'custom'
|
||||
const rangeCustomValue = state.legendForm.miscForm.mapLegendCustomRange
|
||||
// 是自定义,并且自定义类型是自定义区间以及rangeCustomValue长度为0时,根据默认最大最小值计算区间值
|
||||
if (legendCustom && rangeCustom && rangeCustomValue.length === 0) {
|
||||
calcMapCustomRange()
|
||||
const misc = state.legendForm.miscForm
|
||||
if (!misc.mapAutoLegend && misc.mapLegendRangeType === 'custom') {
|
||||
if (misc.mapLegendCustomRange.length < 2) calcMapCustomRange()
|
||||
misc.mapLegendNumber = misc.mapLegendCustomRange.length - 1
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 计算自定义区间
|
||||
* 最大最小值取等分区间的最大最小值
|
||||
*/
|
||||
const calcMapCustomRange = () => {
|
||||
const customRange = getDynamicColorScale(
|
||||
state.legendForm.miscForm.mapLegendMin,
|
||||
state.legendForm.miscForm.mapLegendMax,
|
||||
state.legendForm.miscForm.mapLegendNumber
|
||||
)
|
||||
state.legendForm.miscForm.mapLegendCustomRange = []
|
||||
customRange.forEach((item, index) => {
|
||||
if (index === 0) {
|
||||
state.legendForm.miscForm.mapLegendCustomRange.push(...item.value)
|
||||
} else {
|
||||
state.legendForm.miscForm.mapLegendCustomRange.push(item.value[1])
|
||||
}
|
||||
})
|
||||
const misc = state.legendForm.miscForm
|
||||
const min = Number.isFinite(misc.mapLegendMin) ? misc.mapLegendMin : 0
|
||||
const max =
|
||||
Number.isFinite(misc.mapLegendMax) && misc.mapLegendMax > min ? misc.mapLegendMax : min + 1
|
||||
const count = Math.min(9, Math.max(1, misc.mapLegendNumber || DEFAULT_MISC.mapLegendNumber))
|
||||
const ranges = getDynamicColorScale(min, max, count)
|
||||
misc.mapLegendCustomRange = [ranges[0].value[0], ...ranges.map(item => item.value[1])]
|
||||
}
|
||||
/**
|
||||
* 改变自定义区间类型
|
||||
* @param prop
|
||||
*/
|
||||
const changeLegendCustomType = (prop?) => {
|
||||
const type = state.legendForm.miscForm.mapLegendRangeType
|
||||
if (type === 'custom') {
|
||||
calcMapCustomRange()
|
||||
} else {
|
||||
state.legendForm.miscForm.mapLegendCustomRange = []
|
||||
}
|
||||
// Retain the manual boundaries while another mode is active.
|
||||
initMapCustomRange()
|
||||
prop ? changeMisc(prop) : ''
|
||||
}
|
||||
/**
|
||||
* 改变自定义区间个数
|
||||
* @param prop
|
||||
*/
|
||||
const changeLegendNumber = (prop?) => {
|
||||
if (!state.legendForm.miscForm.mapLegendNumber) {
|
||||
state.legendForm.miscForm.mapLegendNumber = DEFAULT_MISC.mapLegendNumber
|
||||
}
|
||||
calcMapCustomRange()
|
||||
prop ? changeMisc(prop) : ''
|
||||
}
|
||||
const changeRangeItem = (prop, index) => {
|
||||
if (state.legendForm.miscForm.mapLegendCustomRange[index] === null) {
|
||||
state.legendForm.miscForm.mapLegendCustomRange[index] = cloneDeep(
|
||||
mapLegendCustomRangeCacheList[index]
|
||||
const changeMapBoundaries = (values: number[]) => {
|
||||
state.legendForm.miscForm.mapLegendCustomRange = values
|
||||
state.legendForm.miscForm.mapLegendNumber = values.length - 1
|
||||
const labels = state.legendForm.map?.rangeLabels
|
||||
if (labels) {
|
||||
const keys = new Set(
|
||||
values.slice(0, -1).map((value, index) => mapRangeKey([value, values[index + 1]]))
|
||||
)
|
||||
const rangeLabels = Object.fromEntries(Object.entries(labels).filter(([key]) => keys.has(key)))
|
||||
if (Object.keys(rangeLabels).length !== Object.keys(labels).length) {
|
||||
state.legendForm.map = { ...state.legendForm.map, rangeLabels }
|
||||
changeLegendStyle('map')
|
||||
}
|
||||
changeMisc(prop)
|
||||
}
|
||||
const getMapCustomRange = index => {
|
||||
if (index === 0) return t('chart.min')
|
||||
if (index === state.legendForm.miscForm.mapLegendNumber) return t('chart.max')
|
||||
return ''
|
||||
}
|
||||
emit(
|
||||
'onMiscChange',
|
||||
{ data: state.legendForm.miscForm, requestData: false },
|
||||
'mapLegendCustomRange'
|
||||
)
|
||||
}
|
||||
const customSort = []
|
||||
const changeLegendSort = sort => {
|
||||
@@ -463,7 +428,7 @@ onMounted(() => {
|
||||
:effect="themes"
|
||||
v-model="state.legendForm.miscForm.mapAutoLegend"
|
||||
:value="true"
|
||||
@change="changeMisc('mapAutoLegend')"
|
||||
@change="changeLegendCustomType('mapAutoLegend')"
|
||||
style="width: 80px"
|
||||
>
|
||||
{{ t('chart.margin_model_auto') }}
|
||||
@@ -473,7 +438,7 @@ onMounted(() => {
|
||||
:effect="themes"
|
||||
v-model="state.legendForm.miscForm.mapAutoLegend"
|
||||
:value="false"
|
||||
@change="changeMisc('mapAutoLegend')"
|
||||
@change="changeLegendCustomType('mapAutoLegend')"
|
||||
>
|
||||
{{ t('chart.custom_case') }}
|
||||
</el-radio>
|
||||
@@ -511,7 +476,7 @@ onMounted(() => {
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-row v-if="state.legendForm.miscForm.mapLegendRangeType === 'quantize'">
|
||||
<el-col>
|
||||
<el-form-item
|
||||
class="form-item"
|
||||
@@ -532,33 +497,12 @@ onMounted(() => {
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div v-if="state.legendForm.miscForm.mapLegendRangeType === 'custom'">
|
||||
<el-row
|
||||
:gutter="8"
|
||||
:key="index"
|
||||
v-for="(_value, index) in state.legendForm.miscForm.mapLegendCustomRange"
|
||||
>
|
||||
<el-col :span="8">
|
||||
<label class="ed-form-item__label text_ellipsis" :title="getMapCustomRange(index)">
|
||||
{{ getMapCustomRange(index) }}
|
||||
</label>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<el-form-item class="form-item" :class="'form-item-' + themes">
|
||||
<el-input-number
|
||||
:effect="themes"
|
||||
v-model="state.legendForm.miscForm.mapLegendCustomRange[index]"
|
||||
clearable
|
||||
:value-on-clear="mapLegendCustomRangeCacheList[index]"
|
||||
controls-position="right"
|
||||
@change="changeRangeItem('mapLegendCustomRange', index)"
|
||||
style="margin-bottom: 4px"
|
||||
:step="1"
|
||||
<MapRangeEditor
|
||||
v-if="state.legendForm.miscForm.mapLegendRangeType === 'custom'"
|
||||
:model-value="state.legendForm.miscForm.mapLegendCustomRange"
|
||||
:themes="themes"
|
||||
@update:model-value="changeMapBoundaries"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<el-row :gutter="8" v-if="state.legendForm.miscForm.mapLegendRangeType === 'quantize'">
|
||||
<el-col :span="12">
|
||||
<el-form-item
|
||||
@@ -594,6 +538,13 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</el-space>
|
||||
<MapLegendEditor
|
||||
v-if="chartType === 'map'"
|
||||
:model-value="state.legendForm.map"
|
||||
:chart="chart"
|
||||
:themes="themes"
|
||||
@update:model-value="changeMapLegend"
|
||||
/>
|
||||
|
||||
<el-form-item
|
||||
:label="t('chart.orient')"
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, watch } from 'vue'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { formatterItem } from '@/views/chart/components/js/formatter'
|
||||
import { mapRangeKey } from '@/views/chart/components/js/panel/charts/map/mapLegend'
|
||||
import { parseJson } from '@/views/chart/components/js/util'
|
||||
import ValueFormatterEdit from '../../drag-item/components/ValueFormatterEdit.vue'
|
||||
|
||||
const props = defineProps<{ modelValue?: MapLegendOptions; chart: Chart; themes: EditorTheme }>()
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const { t } = useI18n()
|
||||
const normalize = (value?: MapLegendOptions) => ({
|
||||
formatMode: 'legacy',
|
||||
prefix: '',
|
||||
syncTooltip: false,
|
||||
...cloneDeep(value),
|
||||
rangeLabels: { ...value?.rangeLabels },
|
||||
formatterCfg: { ...formatterItem, ...cloneDeep(value?.formatterCfg) }
|
||||
})
|
||||
const draft = reactive(normalize(props.modelValue))
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
value => Object.assign(draft, normalize(value)),
|
||||
{ deep: true }
|
||||
)
|
||||
const save = () => {
|
||||
if (JSON.stringify(draft) !== JSON.stringify(normalize(props.modelValue))) {
|
||||
emit('update:modelValue', cloneDeep(draft))
|
||||
}
|
||||
}
|
||||
watch(() => draft.formatterCfg, save, { deep: true })
|
||||
const ranges = computed(() => {
|
||||
const misc = parseJson(props.chart.customAttr)?.misc
|
||||
if (misc?.mapAutoLegend || misc?.mapLegendRangeType !== 'custom' || props.chart.drill) return []
|
||||
const values = misc.mapLegendCustomRange || []
|
||||
return values.slice(0, -1).map((value, index) => [value, values[index + 1]])
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-form-item :label="t('chart.heatmap_format')" class="form-item" :class="'form-item-' + themes">
|
||||
<el-select v-model="draft.formatMode" :effect="themes" @change="save">
|
||||
<el-option value="legacy" :label="t('chart.map_format_legacy')" />
|
||||
<el-option value="inherit" :label="t('chart.heatmap_format_inherit')" />
|
||||
<el-option value="custom" :label="t('chart.heatmap_format_custom')" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="draft.formatMode === 'custom'"
|
||||
class="form-item"
|
||||
:class="'form-item-' + themes"
|
||||
>
|
||||
<el-popover trigger="click" :width="320" placement="left" :effect="themes">
|
||||
<template #reference
|
||||
><el-button
|
||||
class="format-button"
|
||||
:class="{ 'format-button--dark': themes === 'dark' }"
|
||||
:secondary="themes !== 'dark'"
|
||||
>{{ t('chart.heatmap_format_custom') }}</el-button
|
||||
></template
|
||||
>
|
||||
<el-form
|
||||
class="prefix-form"
|
||||
:class="{ 'prefix-form--dark': themes === 'dark' }"
|
||||
label-position="top"
|
||||
>
|
||||
<el-form-item :label="t('chart.heatmap_prefix')">
|
||||
<el-input
|
||||
:effect="themes"
|
||||
v-model="draft.prefix"
|
||||
maxlength="30"
|
||||
clearable
|
||||
@change="save"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<ValueFormatterEdit
|
||||
:themes="themes"
|
||||
:formatter-item="draft"
|
||||
:chart="chart"
|
||||
:example-prefix="draft.prefix"
|
||||
/>
|
||||
</el-popover>
|
||||
</el-form-item>
|
||||
<el-form-item class="form-item" :class="'form-item-' + themes">
|
||||
<el-checkbox v-model="draft.syncTooltip" :effect="themes" @change="save">
|
||||
{{ t('chart.map_format_sync_tooltip') }}
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-for="range in ranges"
|
||||
:key="mapRangeKey(range)"
|
||||
:label="`${t('chart.map_range_label')} (${range.join(' - ')})`"
|
||||
class="form-item"
|
||||
:class="'form-item-' + themes"
|
||||
>
|
||||
<el-input
|
||||
v-model="draft.rangeLabels[mapRangeKey(range)]"
|
||||
:effect="themes"
|
||||
:placeholder="t('chart.map_range_label_auto')"
|
||||
maxlength="100"
|
||||
clearable
|
||||
@change="save"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.prefix-form--dark :deep(.ed-form-item__label) {
|
||||
color: #ebebeb;
|
||||
}
|
||||
.format-button--dark {
|
||||
--ed-button-text-color: #ebebeb;
|
||||
--ed-button-bg-color: transparent;
|
||||
--ed-button-border-color: #505050;
|
||||
--ed-button-hover-text-color: #ebebeb;
|
||||
--ed-button-hover-bg-color: #414141;
|
||||
--ed-button-hover-border-color: #666666;
|
||||
--ed-button-active-text-color: #ebebeb;
|
||||
--ed-button-active-bg-color: #505050;
|
||||
--ed-button-active-border-color: #666666;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,157 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref, watch } from 'vue'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import icon_deleteTrash_outlined from '@/assets/svg/icon_delete-trash_outlined.svg'
|
||||
import {
|
||||
MAX_MAP_RANGES,
|
||||
insertMapBoundary,
|
||||
removeMapBoundary,
|
||||
validMapBoundaries
|
||||
} from './mapRange'
|
||||
|
||||
const props = defineProps<{ modelValue: number[]; themes: EditorTheme }>()
|
||||
const emit = defineEmits<{ (event: 'update:modelValue', value: number[]): void }>()
|
||||
const { t } = useI18n()
|
||||
const draft = ref<(number | undefined)[]>([...props.modelValue])
|
||||
const host = ref<HTMLElement>()
|
||||
const error = ref('')
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
value => {
|
||||
if (JSON.stringify(value) !== JSON.stringify(draft.value)) {
|
||||
draft.value = [...value]
|
||||
error.value = ''
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
const save = () => {
|
||||
if (!validMapBoundaries(draft.value)) {
|
||||
error.value = t('chart.map_boundary_invalid')
|
||||
return
|
||||
}
|
||||
error.value = ''
|
||||
emit('update:modelValue', [...draft.value])
|
||||
}
|
||||
const insert = async (index: number) => {
|
||||
const next = insertMapBoundary(draft.value as number[], index)
|
||||
if (!next) {
|
||||
error.value = t('chart.map_boundary_cannot_insert')
|
||||
return
|
||||
}
|
||||
draft.value = next
|
||||
save()
|
||||
await nextTick()
|
||||
const input = host.value?.querySelectorAll<HTMLInputElement>('.map-boundary-row input')[index + 1]
|
||||
input?.focus()
|
||||
input?.select()
|
||||
}
|
||||
const remove = (index: number) => {
|
||||
const next = removeMapBoundary(draft.value as number[], index)
|
||||
if (!next) return
|
||||
draft.value = next
|
||||
save()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="host" class="map-boundaries">
|
||||
<div class="map-range-count">
|
||||
<span>{{ t('chart.legend_num') }}</span>
|
||||
<span>{{ draft.length - 1 }}</span>
|
||||
<span class="map-range-hint">{{ t('chart.map_range_count_auto') }}</span>
|
||||
</div>
|
||||
<template v-for="(_, index) in draft" :key="index">
|
||||
<div class="map-boundary-row">
|
||||
<label :for="`map-boundary-${index}`">{{
|
||||
index === 0
|
||||
? t('chart.min')
|
||||
: index === draft.length - 1
|
||||
? t('chart.max')
|
||||
: t('chart.map_boundary')
|
||||
}}</label>
|
||||
<el-input-number
|
||||
:id="`map-boundary-${index}`"
|
||||
v-model="draft[index]"
|
||||
:effect="themes"
|
||||
controls-position="right"
|
||||
@change="save"
|
||||
/>
|
||||
<el-button
|
||||
v-if="index > 0 && index < draft.length - 1"
|
||||
text
|
||||
class="map-boundary-delete"
|
||||
:title="t('chart.map_boundary_delete')"
|
||||
:aria-label="t('chart.map_boundary_delete')"
|
||||
@click="remove(index)"
|
||||
>
|
||||
<el-icon
|
||||
><Icon><icon_deleteTrash_outlined class="svg-icon" /></Icon
|
||||
></el-icon>
|
||||
</el-button>
|
||||
<span v-else class="map-boundary-delete" />
|
||||
</div>
|
||||
<el-button
|
||||
v-if="index < draft.length - 1"
|
||||
class="map-boundary-add"
|
||||
text
|
||||
type="primary"
|
||||
:disabled="draft.length - 1 >= MAX_MAP_RANGES"
|
||||
@click="insert(index)"
|
||||
>+ {{ t('chart.map_boundary_add') }}</el-button
|
||||
>
|
||||
</template>
|
||||
<div v-if="error" class="map-range-error" role="alert">{{ error }}</div>
|
||||
<div class="map-range-hint">{{ t('chart.map_boundary_hint') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.map-boundaries {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.map-range-count {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.map-boundary-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
label {
|
||||
flex: 0 0 48px;
|
||||
}
|
||||
:deep(.ed-input-number) {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
.map-boundary-delete {
|
||||
flex: 0 0 24px;
|
||||
width: 24px;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
}
|
||||
.map-boundary-add {
|
||||
display: flex;
|
||||
margin: 2px auto;
|
||||
height: 28px;
|
||||
}
|
||||
.map-range-hint {
|
||||
font-size: 12px;
|
||||
color: var(--ed-text-color-secondary, #8f959e);
|
||||
line-height: 18px;
|
||||
}
|
||||
.map-boundaries > .map-range-hint {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.map-range-error {
|
||||
color: var(--ed-color-danger);
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,38 @@
|
||||
export const MAX_MAP_RANGES = 9
|
||||
|
||||
export const validMapBoundaries = (values: unknown[]): values is number[] =>
|
||||
values.length >= 2 &&
|
||||
values.length <= MAX_MAP_RANGES + 1 &&
|
||||
values.every(
|
||||
(value, index) =>
|
||||
typeof value === 'number' &&
|
||||
Number.isFinite(value) &&
|
||||
(index === 0 || value > (values[index - 1] as number))
|
||||
)
|
||||
|
||||
export const mapBoundaryMidpoint = (min: number, max: number): number | undefined => {
|
||||
if (!Number.isFinite(min) || !Number.isFinite(max) || min >= max) return
|
||||
const midpoint = min / 2 + max / 2
|
||||
const rounded = Number(midpoint.toPrecision(15))
|
||||
if (rounded > min && rounded < max) return rounded
|
||||
if (midpoint > min && midpoint < max) return midpoint
|
||||
}
|
||||
|
||||
export const insertMapBoundary = (values: number[], index: number): number[] | undefined => {
|
||||
if (
|
||||
!validMapBoundaries(values) ||
|
||||
values.length > MAX_MAP_RANGES ||
|
||||
index < 0 ||
|
||||
index >= values.length - 1
|
||||
)
|
||||
return
|
||||
const midpoint = mapBoundaryMidpoint(values[index], values[index + 1])
|
||||
if (midpoint === undefined) return
|
||||
return [...values.slice(0, index + 1), midpoint, ...values.slice(index + 1)]
|
||||
}
|
||||
|
||||
export const removeMapBoundary = (values: number[], index: number): number[] | undefined => {
|
||||
if (index <= 0 || index >= values.length - 1) return
|
||||
const next = values.filter((_, i) => i !== index)
|
||||
return validMapBoundaries(next) ? next : undefined
|
||||
}
|
||||
@@ -35,11 +35,16 @@ import createDom from '@antv/dom-util/esm/create-dom'
|
||||
import {
|
||||
CONTAINER_TPL,
|
||||
ITEM_TPL,
|
||||
LIST_CLASS
|
||||
LIST_CLASS,
|
||||
VALUE_CLASS
|
||||
} from '@antv/l7plot-component/dist/esm/legend/category/constants'
|
||||
import { configCarouselTooltip } from '@/views/chart/components/js/panel/charts/map/tooltip-carousel'
|
||||
import {
|
||||
configCarouselTooltip,
|
||||
escapeTooltipHtml
|
||||
} from '@/views/chart/components/js/panel/charts/map/tooltip-carousel'
|
||||
import { getCustomGeoArea } from '@/api/map'
|
||||
import { centroid } from '@turf/centroid'
|
||||
import { mapLegendFormatter, mapLegendLabel, mapTooltipValue } from './mapLegend'
|
||||
import { attachMapLabels, geometryPolygons } from './label-layout'
|
||||
import {
|
||||
drawPointFallbackChart,
|
||||
@@ -187,7 +192,7 @@ export class Map extends L7PlotChartView<ChoroplethOptions, Choropleth> {
|
||||
const min = Math.min(...values)
|
||||
const max = Math.max(...values)
|
||||
const step = values.length > 1 ? (max - min) / Math.min(colors.length, 5) : 1
|
||||
const items: { value: number[]; color: string }[] = []
|
||||
const items: CategoryLegendListItem[] = []
|
||||
if (values.length === 1) {
|
||||
items.push({ value: [min, max], color: colors[0] })
|
||||
} else {
|
||||
@@ -199,7 +204,7 @@ export class Map extends L7PlotChartView<ChoroplethOptions, Choropleth> {
|
||||
}
|
||||
opts.legend.customContent = () => {
|
||||
if (items.length) {
|
||||
return this.createLegendCustomContent(items)
|
||||
return this.createLegendCustomContent(c, items)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
@@ -215,17 +220,17 @@ export class Map extends L7PlotChartView<ChoroplethOptions, Choropleth> {
|
||||
if (!misc.mapAutoLegend && legend.show) {
|
||||
let minValue = misc.mapLegendMin
|
||||
let maxValue = misc.mapLegendMax
|
||||
let legendNumber = 9
|
||||
const legendNumber = misc.mapLegendNumber || 9
|
||||
if (misc.mapLegendRangeType === 'custom') {
|
||||
maxValue = 0
|
||||
minValue = 0
|
||||
legendNumber = misc.mapLegendNumber
|
||||
}
|
||||
getMaxAndMinValueByData(sourceData, 'value', maxValue, minValue, (max, min) => {
|
||||
maxValue = max
|
||||
minValue = min
|
||||
action({
|
||||
from: 'map',
|
||||
chartId: chart.id,
|
||||
data: {
|
||||
max: maxValue,
|
||||
min: minValue ?? filterEmptyMinValue(sourceData, 'value'),
|
||||
@@ -377,22 +382,18 @@ export class Map extends L7PlotChartView<ChoroplethOptions, Choropleth> {
|
||||
let colorScale = []
|
||||
let minValue = misc.mapAutoLegend ? 0 : misc.mapLegendMin
|
||||
let maxValue = misc.mapAutoLegend ? 0 : misc.mapLegendMax
|
||||
let mapLegendNumber = misc.mapLegendNumber
|
||||
const mapLegendNumber = misc.mapAutoLegend
|
||||
? this.calculateAutoLegendNumber(sourceData)
|
||||
: misc.mapLegendNumber || 9
|
||||
if (legend.show) {
|
||||
getMaxAndMinValueByData(sourceData, 'value', maxValue, minValue, (max, min) => {
|
||||
maxValue = max
|
||||
minValue = min
|
||||
mapLegendNumber = 9
|
||||
})
|
||||
// 非自动,过滤数据
|
||||
if (!misc.mapAutoLegend) {
|
||||
data = filterChartDataByRange(sourceData, maxValue, minValue)
|
||||
} else {
|
||||
mapLegendNumber = 9
|
||||
}
|
||||
mapLegendNumber = misc.mapAutoLegend
|
||||
? this.calculateAutoLegendNumber(sourceData)
|
||||
: mapLegendNumber
|
||||
// 定义最大值、最小值、区间数量和对应的颜色
|
||||
colorScale = getDynamicColorScale(minValue, maxValue, mapLegendNumber, colors)
|
||||
} else {
|
||||
@@ -474,35 +475,43 @@ export class Map extends L7PlotChartView<ChoroplethOptions, Choropleth> {
|
||||
}
|
||||
|
||||
// 内部函数 创建自定义图例的内容
|
||||
private createLegendCustomContent = showItems => {
|
||||
private createLegendCustomContent = (chart: Chart, showItems: CategoryLegendListItem[]) => {
|
||||
const containerDom = createDom(CONTAINER_TPL) as HTMLElement
|
||||
const listDom = containerDom.getElementsByClassName(LIST_CLASS)[0] as HTMLElement
|
||||
showItems.forEach(item => {
|
||||
let value = '-'
|
||||
if (item.value !== '') {
|
||||
if (Array.isArray(item.value)) {
|
||||
const arr = item.value.every(Number.isNaN) ? item.color.value || [] : item.value
|
||||
value = arr
|
||||
.map(v => (Number.isNaN(v) || String(v) === 'NaN' ? 'NaN' : parseFloat(v).toFixed(0)))
|
||||
.join('-')
|
||||
} else {
|
||||
const tmp = item.value as string
|
||||
value = Number.isNaN(tmp) || tmp === 'NaN' ? 'NaN' : parseFloat(tmp).toFixed(0)
|
||||
}
|
||||
}
|
||||
if (value && value !== '') {
|
||||
const substituteObj = { ...item, value }
|
||||
|
||||
const domStr = substitute(ITEM_TPL, substituteObj)
|
||||
const itemDom = createDom(domStr)
|
||||
// 给 legend 形状用的
|
||||
itemDom.style.setProperty('--bgColor', item.color)
|
||||
showItems.forEach((item, index) => {
|
||||
const raw =
|
||||
Array.isArray(item.value) && item.value.every(Number.isNaN)
|
||||
? (item.color as unknown as { value?: unknown })?.value || item.value
|
||||
: item.value
|
||||
const value = mapLegendLabel(chart, raw)
|
||||
const itemDom = createDom(substitute(ITEM_TPL, { index, color: '', value: '' }))
|
||||
// Prefixes, suffixes and range names are plain text, never interpolated into HTML.
|
||||
const valueDom = itemDom.getElementsByClassName(VALUE_CLASS)[0] as HTMLElement
|
||||
valueDom.textContent = value
|
||||
valueDom.title = value
|
||||
itemDom.style.setProperty('--bgColor', item.color as string)
|
||||
const marker = itemDom.firstElementChild as HTMLElement
|
||||
marker.style.backgroundColor = item.color as string
|
||||
listDom.appendChild(itemDom)
|
||||
}
|
||||
})
|
||||
return listDom
|
||||
}
|
||||
|
||||
protected configTooltip(chart: Chart, options: ChoroplethOptions): ChoroplethOptions {
|
||||
const result = super.configTooltip(chart, options)
|
||||
if (!parseJson(chart.customStyle)?.legend?.map?.syncTooltip || !result.tooltip) return result
|
||||
const customItems = result.tooltip.customItems
|
||||
if (!customItems) return result
|
||||
const format = mapLegendFormatter(chart)
|
||||
result.tooltip.customItems = originalItem =>
|
||||
customItems(originalItem).map(item =>
|
||||
item.quotaList?.[0]?.id === originalItem.properties?.quotaList?.[0]?.id && item.quotaList
|
||||
? { ...item, value: escapeTooltipHtml(format(originalItem.properties.value)) }
|
||||
: item
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
private customConfigLegend(chart: Chart, options: ChoroplethOptions): ChoroplethOptions {
|
||||
const { basicStyle, misc } = parseJson(chart.customAttr)
|
||||
const colors = basicStyle.colors.map(item => hexColorToRGBA(item, basicStyle.alpha))
|
||||
@@ -587,7 +596,7 @@ export class Map extends L7PlotChartView<ChoroplethOptions, Choropleth> {
|
||||
})
|
||||
customLegend['customContent'] = () => {
|
||||
if (items?.length) {
|
||||
return this.createLegendCustomContent(items)
|
||||
return this.createLegendCustomContent(chart, items)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
@@ -633,7 +642,7 @@ export class Map extends L7PlotChartView<ChoroplethOptions, Choropleth> {
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.createLegendCustomContent(showItems)
|
||||
return this.createLegendCustomContent(chart, showItems)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
@@ -752,7 +761,9 @@ export class Map extends L7PlotChartView<ChoroplethOptions, Choropleth> {
|
||||
const formatter = formatterMap[valItem.quotaList?.[0]?.id]
|
||||
if (!isEmpty(formatter)) {
|
||||
const originValue = parseFloat(valItem.value as string)
|
||||
const value = valueFormatter(originValue, formatter.formatterCfg)
|
||||
const value = escapeTooltipHtml(
|
||||
mapTooltipValue(chart, originValue, formatter.formatterCfg)
|
||||
)
|
||||
const name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName
|
||||
result.push({ ...valItem, name, value: `${value ?? ''}` })
|
||||
}
|
||||
@@ -771,6 +782,7 @@ export class Map extends L7PlotChartView<ChoroplethOptions, Choropleth> {
|
||||
}
|
||||
|
||||
setupDefaultOptions(chart: ChartObj): ChartObj {
|
||||
chart.customStyle.legend.map = { formatMode: 'inherit' }
|
||||
chart.customAttr.basicStyle.areaBaseColor = '#f4f4f4'
|
||||
chart.senior.useGlobalAreaMapping = true
|
||||
return chart
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { formatterItem, valueFormatter } from '../../../formatter'
|
||||
import { parseJson } from '@/views/chart/components/js/util'
|
||||
|
||||
export const mapRangeKey = (range: number[]) => JSON.stringify(range)
|
||||
|
||||
export const mapLegendFormatter = (chart: Chart) => {
|
||||
const config: MapLegendOptions = parseJson(chart.customStyle)?.legend?.map || {}
|
||||
const custom = config.formatMode === 'custom'
|
||||
const formatter = {
|
||||
...formatterItem,
|
||||
...(custom ? config.formatterCfg : chart.yAxis?.[0]?.formatterCfg)
|
||||
}
|
||||
return (value: unknown): string => {
|
||||
if (
|
||||
(typeof value !== 'number' && typeof value !== 'string') ||
|
||||
(typeof value === 'string' && !value.trim()) ||
|
||||
!Number.isFinite(Number(value))
|
||||
) {
|
||||
return '-'
|
||||
}
|
||||
const number = Number(value)
|
||||
// Existing maps retain their integer labels until a formatting mode is explicitly selected.
|
||||
if (!config.formatMode || config.formatMode === 'legacy') return number.toFixed(0)
|
||||
return `${custom ? config.prefix || '' : ''}${valueFormatter(number, { ...formatter })}`
|
||||
}
|
||||
}
|
||||
|
||||
export const mapLegendLabel = (chart: Chart, value: unknown): string => {
|
||||
const format = mapLegendFormatter(chart)
|
||||
if (!Array.isArray(value)) return format(value)
|
||||
const misc = parseJson(chart.customAttr)?.misc
|
||||
if (!chart.drill && !misc?.mapAutoLegend && misc?.mapLegendRangeType === 'custom') {
|
||||
const label = parseJson(chart.customStyle)?.legend?.map?.rangeLabels?.[mapRangeKey(value)]
|
||||
if (label?.trim()) return label
|
||||
}
|
||||
const mode = parseJson(chart.customStyle)?.legend?.map?.formatMode
|
||||
if (!mode || mode === 'legacy') return value.map(format).join('-')
|
||||
const [min, max] = value
|
||||
if (min === -Infinity && Number.isFinite(max)) return `< ${format(max)}`
|
||||
if (max === Infinity && Number.isFinite(min)) return `≥ ${format(min)}`
|
||||
if (min === max) return format(min)
|
||||
return `${format(min)} - ${format(max)}`
|
||||
}
|
||||
|
||||
export const mapTooltipValue = (chart: Chart, value: number, formatter: BaseFormatter) =>
|
||||
chart.type === 'map' && parseJson(chart.customStyle)?.legend?.map?.syncTooltip
|
||||
? mapLegendFormatter(chart)(value)
|
||||
: valueFormatter(value, formatter)
|
||||
@@ -1,3 +1,4 @@
|
||||
import { mapTooltipValue } from './mapLegend'
|
||||
import type { ChoroplethOptions } from '@antv/l7plot/dist/esm/plots/choropleth'
|
||||
import type { FeatureCollection } from '@antv/l7plot/dist/esm/plots/choropleth/types'
|
||||
import type { DotOptions } from '@antv/l7plot'
|
||||
@@ -11,7 +12,10 @@ import {
|
||||
mapRendering
|
||||
} from '@/views/chart/components/js/panel/common/common_antv'
|
||||
import { valueFormatter } from '@/views/chart/components/js/formatter'
|
||||
import { configCarouselTooltip } from '@/views/chart/components/js/panel/charts/map/tooltip-carousel'
|
||||
import {
|
||||
configCarouselTooltip,
|
||||
escapeTooltipHtml
|
||||
} from '@/views/chart/components/js/panel/charts/map/tooltip-carousel'
|
||||
import type { L7PlotDrawOptions } from '@/views/chart/components/js/panel/types/impl/l7plot'
|
||||
import type { Choropleth } from '@antv/l7plot/dist/esm/plots/choropleth'
|
||||
import type { MapMouseEvent } from '@/views/chart/components/js/panel/charts/map/common'
|
||||
@@ -278,7 +282,7 @@ export async function drawPointFallbackChart(
|
||||
const formatter = formatterMap[head.quotaList?.[0]?.id]
|
||||
if (!isEmpty(formatter)) {
|
||||
const originValue = parseFloat(head.value as string)
|
||||
const value = valueFormatter(originValue, formatter.formatterCfg)
|
||||
const value = escapeTooltipHtml(mapTooltipValue(chart, originValue, formatter.formatterCfg))
|
||||
const name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName
|
||||
result.push({ ...head, name, value: `${value ?? ''}` })
|
||||
}
|
||||
|
||||
8
tests/map-legend/README.md
Normal file
8
tests/map-legend/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# 地图图例格式回归
|
||||
|
||||
在仓库根目录运行 `node tests/map-legend/verify.mjs`。需要已有前端 TypeScript/lodash-es 依赖。
|
||||
|
||||
测试使用真实数值格式化实现,仅隔离语言和配置解析环境依赖。覆盖旧图兼容、继承/自定义、单位/百分比/千分符、区间别名、钻取和自动区间隔离、空值/非有限值及提示框同步边界。
|
||||
实际渲染与保存重开另在本地 DataEase 合成大屏验证:`1302701735000805376`。
|
||||
|
||||
`node tests/map-legend/verify-count.mjs` 验证实际地图渲染方法在初次推算范围(0/0、null)及已初始化范围下始终采用手动图例数,覆盖1/3/5/9段。
|
||||
29
tests/map-legend/verify-count.mjs
Normal file
29
tests/map-legend/verify-count.mjs
Normal file
@@ -0,0 +1,29 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import ts from '../../core/core-frontend/node_modules/typescript/lib/typescript.js'
|
||||
const base = new URL('../../core/core-frontend/src/views/chart/components/js/', import.meta.url)
|
||||
const mapText = readFileSync(new URL('panel/charts/map/map.ts', base), 'utf8')
|
||||
const utilText = readFileSync(new URL('util.ts', base), 'utf8')
|
||||
const mapAst = ts.createSourceFile('map.ts', mapText, ts.ScriptTarget.Latest, true)
|
||||
const utilAst = ts.createSourceFile('util.ts', utilText, ts.ScriptTarget.Latest, true)
|
||||
const mapClass = mapAst.statements.find(n => ts.isClassDeclaration(n) && n.name.text === 'Map')
|
||||
const members = ['configBasicStyle', 'calculateAutoLegendNumber'].map(name => mapClass.members.find(n => n.name?.getText(mapAst) === name).getText(mapAst)).join('\n')
|
||||
const helpers = ['getDynamicColorScale','getMaxAndMinValueByData','filterChartDataByRange'].map(name => utilAst.statements.find(n => ts.isVariableStatement(n) && n.declarationList.declarations.some(d => d.name.getText(utilAst) === name)).getText(utilAst)).join('\n')
|
||||
const wrapper = mapAst.statements.find(n => ts.isClassDeclaration(n) && n.name.text === 'ColorWrapper').getText(mapAst)
|
||||
// Isolate DOM/geographic styling; retain the actual renderer method and actual range calculations.
|
||||
const source = `const parseJson = x => x; const handleGeoJson = () => {}; const hexColorToRGBA = x => x;
|
||||
${helpers}\n${wrapper}\nexport class TestMap { ${members} }`
|
||||
const js = ts.transpileModule(source, {compilerOptions:{module:ts.ModuleKind.ESNext,target:ts.ScriptTarget.ES2022}}).outputText
|
||||
const {TestMap} = await import(`data:text/javascript;base64,${Buffer.from(js).toString('base64')}`)
|
||||
const renderer = new TestMap()
|
||||
const data = [12500.25,25600.75,42500.25,78900.5].map((value,i)=>({value,field:String(i)}))
|
||||
for (const [min,max] of [[0,0],[null,null],[null,100000],[0,null],[12500.25,78900.5]]) {
|
||||
for (const count of [1,3,5,9]) {
|
||||
const misc = {mapAutoLegend:false,mapLegendNumber:count,mapLegendMin:min,mapLegendMax:max,mapLegendRangeType:'quantize'}
|
||||
const chart = {data:{data},senior:{},customStyle:{legend:{show:true}},customAttr:{misc,basicStyle:{colors:Array(9).fill('#123456')},label:{show:false}}}
|
||||
const result=renderer.configBasicStyle(chart,{source:{data}},{drawOption:{areaId:'156'},geoJson:{features:[{properties:{name:'0'}}]}})
|
||||
assert.equal(result.color.value.length,count,`range ${min},${max}, expected ${count}`)
|
||||
assert.equal(misc.mapLegendNumber,count)
|
||||
}
|
||||
}
|
||||
console.log('PASS: 20 map renderer count cases, including first inferred range and initialized range')
|
||||
62
tests/map-legend/verify.mjs
Normal file
62
tests/map-legend/verify.mjs
Normal file
@@ -0,0 +1,62 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import ts from '../../core/core-frontend/node_modules/typescript/lib/typescript.js'
|
||||
const root = new URL('../../core/core-frontend/src/views/chart/components/js/', import.meta.url)
|
||||
const moduleUrl = source => `data:text/javascript;base64,${Buffer.from(ts.transpileModule(source, {
|
||||
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 }
|
||||
}).outputText).toString('base64')}`
|
||||
// Use the real formatter; only replace environment hooks so the regression can run without Vue.
|
||||
const formatter = readFileSync(new URL('formatter.ts', root), 'utf8')
|
||||
.replace("from 'lodash-es'", `from '${new URL('../../core/core-frontend/node_modules/lodash-es/lodash.js', import.meta.url)}'`)
|
||||
.replace("import { useI18n } from '@/hooks/web/useI18n'", "const useI18n = () => ({t: key => ({'chart.unit_ten_thousand':'万'}[key] || key)})")
|
||||
.replace("import { getLocale } from '@/utils/utils'", "const getLocale = () => 'zh-CN'")
|
||||
const source = readFileSync(new URL('panel/charts/map/mapLegend.ts', root), 'utf8')
|
||||
.replace("'../../../formatter'", JSON.stringify(moduleUrl(formatter)))
|
||||
.replace("import { parseJson } from '@/views/chart/components/js/util'", 'const parseJson = value => typeof value === "string" ? JSON.parse(value) : value')
|
||||
const { mapLegendLabel: label, mapLegendFormatter, mapTooltipValue } = await import(moduleUrl(source))
|
||||
let checks = 0
|
||||
const eq = (actual, expected) => { assert.equal(actual, expected); checks++ }
|
||||
const cfg = {type:'value',unitLanguage:'ch',unit:10000,suffix:'元',decimalCount:2,thousandSeparator:true}
|
||||
const chart = {type:'map',yAxis:[{formatterCfg:cfg}],customStyle:{legend:{}},customAttr:{misc:{mapAutoLegend:false,mapLegendRangeType:'custom'}}}
|
||||
eq(label(chart, [12500.25,25600.75]), '12500-25601')
|
||||
chart.customStyle.legend.map = {formatMode:'inherit'}
|
||||
eq(label(chart, [12500.25,25600.75]), '1.25万元 - 2.56万元')
|
||||
const snapshot = JSON.stringify(chart)
|
||||
label(chart,[1,2]);eq(JSON.stringify(chart),snapshot)
|
||||
const custom = chart.customStyle.legend.map = {formatMode:'custom',prefix:'¥',formatterCfg:{...cfg,decimalCount:1},rangeLabels:{'[0,20000]':'低风险'}}
|
||||
eq(label(chart,[20000,50000]), '¥2.0万元 - ¥5.0万元')
|
||||
eq(label(chart,[0,20000]),'低风险')
|
||||
eq(label(chart,[0,30000]),'¥0.0万元 - ¥3.0万元')
|
||||
chart.drill=true;eq(label(chart,[0,20000]),'¥0.0万元 - ¥2.0万元');chart.drill=false
|
||||
chart.customAttr.misc.mapAutoLegend=true;eq(label(chart,[0,20000]),'¥0.0万元 - ¥2.0万元')
|
||||
eq(label(chart,[-Infinity,20000]),'< ¥2.0万元')
|
||||
eq(label(chart,[20000,Infinity]),'≥ ¥2.0万元')
|
||||
eq(label(chart,[20000,20000]),'¥2.0万元')
|
||||
for (const v of [NaN, Infinity, null, undefined, '', ' ', true, {}, 'invalid']) eq(mapLegendFormatter(chart)(v),'-')
|
||||
eq(label(chart,[-25000,-10000]),'¥-2.5万元 - ¥-1.0万元')
|
||||
eq(mapTooltipValue(chart,12500.25,cfg),'1.25万元')
|
||||
custom.syncTooltip=true;eq(mapTooltipValue(chart,12500.25,cfg),'¥1.3万元')
|
||||
chart.type='bubble-map';eq(mapTooltipValue(chart,12500.25,cfg),'1.25万元')
|
||||
custom.formatterCfg={...cfg,unit:1,decimalCount:2};eq(label(chart,1234567.89),'¥1,234,567.89元')
|
||||
custom.formatterCfg={...cfg,type:'percent',unit:1,decimalCount:1,suffix:''};eq(label(chart,0.125),'¥12.5%')
|
||||
console.log(`PASS: ${checks} map legend assertions`)
|
||||
const rangeSource = readFileSync(new URL('editor/editor-style/components/mapRange.ts', new URL('../../core/core-frontend/src/views/chart/components/', import.meta.url)), 'utf8')
|
||||
const {validMapBoundaries, mapBoundaryMidpoint, insertMapBoundary, removeMapBoundary} = await import(moduleUrl(rangeSource))
|
||||
const bounds = [0,20,50,100]
|
||||
assert.deepEqual(insertMapBoundary(bounds,1),[0,20,35,50,100])
|
||||
assert.deepEqual(bounds,[0,20,50,100])
|
||||
assert.deepEqual(removeMapBoundary([0,20,35,50,100],1),[0,35,50,100])
|
||||
eq(removeMapBoundary(bounds,0),undefined)
|
||||
eq(removeMapBoundary(bounds,3),undefined)
|
||||
eq(validMapBoundaries([0,0,1]),false)
|
||||
eq(validMapBoundaries([0,undefined,1]),false)
|
||||
eq(validMapBoundaries([-5,0,1.5]),true)
|
||||
eq(validMapBoundaries([0,Infinity]),false)
|
||||
eq(mapBoundaryMidpoint(0.1,0.2),0.15)
|
||||
eq(mapBoundaryMidpoint(-20,-10),-15)
|
||||
eq(mapBoundaryMidpoint(Number.MAX_VALUE / 2,Number.MAX_VALUE)>0,true)
|
||||
eq(mapBoundaryMidpoint(1,1+Number.EPSILON),undefined)
|
||||
eq(insertMapBoundary([0,1,2,3,4,5,6,7,8,9],0),undefined)
|
||||
eq(insertMapBoundary(bounds,4),undefined)
|
||||
eq(removeMapBoundary([0,100],1),undefined)
|
||||
console.log('PASS: boundary insertion, deletion, ordering, limits and numeric precision checks')
|
||||
Reference in New Issue
Block a user