feat(图表): 新增热力图

#8108
This commit is contained in:
ulleo 2024-05-29 11:45:43 +08:00
parent 7d3e3de281
commit f7aadb877d
7 changed files with 479 additions and 208 deletions

View File

@ -788,6 +788,8 @@ public class ChartDataManage {
mapChart = ChartDataBuild.transMixChartDataAntV(xAxis, yAxis, view, data, isDrill);
} else if (StringUtils.equalsIgnoreCase(view.getType(), "bar-range")) {
mapChart = ChartDataBuild.transBarRangeDataAntV(skipBarRange, barRangeDate, xAxisBase, xAxis, yAxis, view, data, isDrill);
} else if(StringUtils.equalsIgnoreCase(view.getType(), "heat-map")){
mapChart = ChartDataBuild.transHeatMapChartDataAntV(xAxisBase, xAxis, yAxis, view, data, isDrill);
} else {
mapChart = ChartDataBuild.transChartDataAntV(xAxis, yAxis, view, data, isDrill);
}

View File

@ -4,6 +4,7 @@ import io.dataease.api.chart.dto.*;
import io.dataease.i18n.Lang;
import io.dataease.i18n.Translator;
import io.dataease.utils.IDUtils;
import io.dataease.utils.JsonUtil;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.ObjectUtils;
@ -113,6 +114,77 @@ public class ChartDataBuild {
return map;
}
public static Map<String, Object> transHeatMapChartDataAntV(List<ChartViewFieldDTO> xAxisBase, List<ChartViewFieldDTO> xAxis, List<ChartViewFieldDTO> yAxis, ChartViewDTO view, List<String[]> data, boolean isDrill) {
Map<String, Object> map = new HashMap<>();
List<Map<String, Object>> dataList = new ArrayList<>();
if (xAxisBase.size() != 2) {
map.put("data", dataList);
return map;
}
for (int i1 = 0; i1 < data.size(); i1++) {
String[] row = data.get(i1);
StringBuilder a = new StringBuilder();
if (isDrill) {
a.append(row[xAxis.size() - 1]);
} else {
for (int i = 0; i < xAxis.size(); i++) {
if (i == xAxis.size() - 1) {
a.append(row[i]);
} else {
a.append(row[i]).append("\n");
}
}
}
// yAxis最后的数据对应extLabel和extTooltip将他们从yAxis中去掉同时转换成动态值
int size = xAxis.size() + yAxis.size();
int extSize = view.getExtLabel().size() + view.getExtTooltip().size();
for (int i = xAxis.size(); i < size - extSize; i++) {
AxisChartDataAntVDTO axisChartDataDTO = new AxisChartDataAntVDTO();
axisChartDataDTO.setField(a.toString());
axisChartDataDTO.setName(a.toString());
List<ChartDimensionDTO> dimensionList = new ArrayList<>();
List<ChartQuotaDTO> quotaList = new ArrayList<>();
for (int j = 0; j < xAxis.size(); j++) {
ChartDimensionDTO chartDimensionDTO = new ChartDimensionDTO();
chartDimensionDTO.setId(xAxis.get(j).getId());
chartDimensionDTO.setValue(row[j]);
dimensionList.add(chartDimensionDTO);
}
axisChartDataDTO.setDimensionList(dimensionList);
int j = i - xAxis.size();
ChartQuotaDTO chartQuotaDTO = new ChartQuotaDTO();
chartQuotaDTO.setId(yAxis.get(j).getId());
quotaList.add(chartQuotaDTO);
axisChartDataDTO.setQuotaList(quotaList);
try {
axisChartDataDTO.setValue(StringUtils.isEmpty(row[i]) ? null : new BigDecimal(row[i]));
} catch (Exception e) {
axisChartDataDTO.setValue(new BigDecimal(0));
}
axisChartDataDTO.setCategory(StringUtils.defaultIfBlank(yAxis.get(j).getChartShowName(), yAxis.get(j).getName()));
buildDynamicValue(view, axisChartDataDTO, row, size, extSize);
Map<String, Object> object = JsonUtil.parse((String) JsonUtil.toJSONString(axisChartDataDTO) , HashMap.class);
object.put("x", new BigDecimal(row[0]));
object.put("y", new BigDecimal(row[1]));
dataList.add(object);
}
}
map.put("data", dataList);
return map;
}
public static Map<String, Object> transBaseGroupDataAntV(List<ChartViewFieldDTO> xAxisBase, List<ChartViewFieldDTO> xAxis, List<ChartViewFieldDTO> xAxisExt, List<ChartViewFieldDTO> yAxis, ChartViewDTO view, List<String[]> data, boolean isDrill) {
Map<String, Object> map = new HashMap<>();

View File

@ -449,6 +449,7 @@ export default {
condition_style: '标记样式',
longitude: '经度',
latitude: '纬度',
longitude_and_latitude: '经度纬度',
gradient: '渐变',
layer_controller: '指标切换',
show_zoom: '显示缩放按钮',
@ -1060,6 +1061,7 @@ export default {
step: '步长(px)',
no_function: '函数尚未支持直接引用请在字段表达式中手动输入',
chart_flow_map: '流向地图',
chart_heat_map: '热力地图',
start_point: '起点经纬度',
end_point: '终点经纬度',
line: '线条',
@ -1078,10 +1080,15 @@ export default {
map_style_darkblue: '极夜蓝',
map_style_wine: '酱籽',
map_line_type: '类型',
type: '类型',
map_line_width: '线条宽度',
map_line_height: '线条高度',
map_line_linear: '渐变',
map_line_animate: '动画',
heatmap_classics: '经典热力图',
heatmap3D: '3D热力图',
heatMapIntensity: '热力强度',
heatMapRadius: '热力点半径',
map_line_animate_duration: '动画间隔',
map_line_animate_interval: '轨迹间隔',
map_line_animate_trail_length: '轨迹长度',

View File

@ -168,6 +168,9 @@ declare interface ChartBasicStyle {
* 地图主题风格
*/
mapStyle: string
heatMapType?: string
heatMapIntensity?: number
heatMapRadius?: number
/**
* 地图边线颜色
*/

View File

@ -187,6 +187,10 @@ const mapStyleOptions = [
{ name: t('chart.map_style_blue'), value: 'blue' },
{ name: t('chart.map_style_wine'), value: 'wine' }
]
const heatMapTypeOptions = [
{ name: t('chart.heatmap_classics'), value: 'heatmap' },
{ name: t('chart.heatmap3D'), value: 'heatmap3D' }
]
const flowLineTypeOptions = [
{ name: t('chart.map_line_type_line'), value: 'line' },
@ -286,233 +290,286 @@ onMounted(() => {
</el-radio-group>
</el-form-item>
<!--flow map begin-->
<div class="map-setting" v-if="showProperty('mapStyle')">
<div class="map-style">
<el-form-item
v-if="showProperty('heatMapStyle')"
:label="t('chart.type')"
class="form-item"
:class="'form-item-' + themes"
>
<el-select
:effect="themes"
v-model="state.basicStyleForm.heatMapType"
@change="changeBasicStyle('heatMapType')"
>
<el-option
v-for="item in heatMapTypeOptions"
:key="item.name"
:label="item.name"
:value="item.value"
/>
</el-select>
</el-form-item>
<div class="map-style" v-if="showProperty('mapStyle') || showProperty('heatMapStyle')">
<el-row style="flex: 1">
<el-col>
<el-form-item
:label="t('chart.chart_map') + t('chart.map_style')"
class="form-item"
:class="'form-item-' + themes"
>
<el-select
:effect="themes"
v-model="state.basicStyleForm.mapStyle"
@change="changeBasicStyle('mapStyle')"
>
<el-option
v-for="item in mapStyleOptions"
: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.chart_map') + t('chart.map_pitch') }}
</label>
<el-row style="flex: 1" :gutter="8">
<el-col>
<el-form-item class="form-item alpha-slider" :class="'form-item-' + themes">
<el-slider
:effect="themes"
:min="0"
:max="90"
v-model="state.miscForm.mapPitch"
@change="changeMisc('mapPitch')"
/>
</el-form-item>
</el-col>
</el-row>
</div>
</div>
<div class="map-flow-style" v-if="showProperty('mapStyle')">
<el-row style="flex: 1">
<el-col>
<el-form-item
:label="t('chart.line') + t('chart.map_line_type')"
class="form-item"
:class="'form-item-' + themes"
>
<el-select
:effect="themes"
v-model="state.miscForm.mapLineType"
@change="changeMisc('mapLineType')"
>
<el-option
v-for="item in flowLineTypeOptions"
: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.map_line_width') }}
</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.miscForm.mapLineWidth"
@change="changeMisc('mapLineWidth')"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<el-row style="flex: 1">
<el-col>
<el-form-item class="form-item" :class="'form-item-' + themes">
<el-checkbox
size="small"
:effect="themes"
v-model="state.miscForm.mapLineGradient"
:predefine="predefineColors"
@change="changeMisc('mapLineGradient')"
>
{{ t('chart.line') + t('chart.map_line_linear') }}
</el-checkbox>
</el-form-item>
</el-col>
</el-row>
<div v-if="state.miscForm.mapLineGradient">
<el-row style="flex: 1" :gutter="8">
<el-col :span="13">
<el-form-item
:label="t('chart.chart_map') + t('chart.map_style')"
class="form-item"
:class="'form-item-' + themes"
:label="t('chart.map_line_color_source_color')"
>
<el-select
<el-color-picker
is-custom
class="color-picker-style"
v-model="state.miscForm.mapLineSourceColor"
:persistent="false"
:effect="themes"
v-model="state.basicStyleForm.mapStyle"
@change="changeBasicStyle('mapStyle')"
>
<el-option
v-for="item in mapStyleOptions"
:key="item.name"
:label="item.name"
:value="item.value"
/>
</el-select>
:trigger-width="108"
:predefine="predefineColors"
@change="changeMisc('mapLineSourceColor')"
/>
</el-form-item>
</el-col>
</el-row>
<div class="alpha-setting">
<label class="alpha-label" :class="{ dark: 'dark' === themes }">
{{ t('chart.chart_map') + t('chart.map_pitch') }}
</label>
<el-row style="flex: 1" :gutter="8">
<el-col>
<el-form-item class="form-item alpha-slider" :class="'form-item-' + themes">
<el-slider
:effect="themes"
:min="0"
:max="90"
v-model="state.miscForm.mapPitch"
@change="changeMisc('mapPitch')"
/>
</el-form-item>
</el-col>
</el-row>
</div>
</div>
<div class="map-flow-style">
<el-row style="flex: 1">
<el-col>
<el-col :span="13">
<el-form-item
:label="t('chart.line') + t('chart.map_line_type')"
class="form-item"
:class="'form-item-' + themes"
:label="t('chart.map_line_color_target_color')"
>
<el-select
<el-color-picker
is-custom
class="color-picker-style"
v-model="state.miscForm.mapLineTargetColor"
:persistent="false"
:effect="themes"
v-model="state.miscForm.mapLineType"
@change="changeMisc('mapLineType')"
>
<el-option
v-for="item in flowLineTypeOptions"
: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.map_line_width') }}
</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.miscForm.mapLineWidth"
@change="changeMisc('mapLineWidth')"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<el-row style="flex: 1">
<el-col>
<el-form-item class="form-item" :class="'form-item-' + themes">
<el-checkbox
size="small"
:effect="themes"
v-model="state.miscForm.mapLineGradient"
:trigger-width="108"
:predefine="predefineColors"
@change="changeMisc('mapLineGradient')"
>
{{ t('chart.line') + t('chart.map_line_linear') }}
</el-checkbox>
@change="changeMisc('mapLineTargetColor')"
/>
</el-form-item>
</el-col>
</el-row>
<div v-if="state.miscForm.mapLineGradient">
<el-row style="flex: 1" :gutter="8">
<el-col :span="13">
<el-form-item
class="form-item"
:class="'form-item-' + themes"
:label="t('chart.map_line_color_source_color')"
>
<el-color-picker
is-custom
class="color-picker-style"
v-model="state.miscForm.mapLineSourceColor"
:persistent="false"
:effect="themes"
:trigger-width="108"
:predefine="predefineColors"
@change="changeMisc('mapLineSourceColor')"
/>
</el-form-item>
</el-col>
<el-col :span="13">
<el-form-item
class="form-item"
:class="'form-item-' + themes"
:label="t('chart.map_line_color_target_color')"
>
<el-color-picker
is-custom
class="color-picker-style"
v-model="state.miscForm.mapLineTargetColor"
:persistent="false"
:effect="themes"
:trigger-width="108"
:predefine="predefineColors"
@change="changeMisc('mapLineTargetColor')"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<div v-if="!state.miscForm.mapLineGradient">
<el-row style="flex: 1" :gutter="8">
<el-col>
<el-form-item
class="form-item"
:class="'form-item-' + themes"
:label="t('chart.color')"
>
<el-color-picker
is-custom
class="color-picker-style"
v-model="state.miscForm.mapLineSourceColor"
:persistent="false"
:effect="themes"
:trigger-width="108"
:predefine="predefineColors"
@change="changeMisc('mapLineSourceColor')"
/>
</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" :gutter="8">
<el-col :span="13">
<el-form-item class="form-item alpha-slider" :class="'form-item-' + themes">
<el-slider
:effect="themes"
v-model="state.basicStyleForm.alpha"
@change="changeBasicStyle('alpha')"
/>
</el-form-item>
</el-col>
<el-col :span="11" style="padding-top: 2px">
<el-form-item class="form-item" :class="'form-item-' + themes">
<el-input
type="number"
:effect="themes"
v-model="state.basicStyleForm.alpha"
:min="0"
:max="100"
class="basic-input-number"
:controls="false"
@change="changeBasicStyle('alpha')"
>
<template #suffix> % </template>
</el-input>
</el-form-item>
</el-col>
</el-row>
</div>
<el-row style="flex: 1">
<el-col>
<el-form-item class="form-item" :class="'form-item-' + themes">
<el-checkbox
size="small"
:effect="themes"
v-model="state.miscForm.mapLineAnimate"
:predefine="predefineColors"
@change="changeMisc('mapLineAnimate')"
>
{{ t('chart.line') + t('chart.map_line_animate') }}
</el-checkbox>
</el-form-item>
</el-col>
</el-row>
<div class="alpha-setting" v-if="state.miscForm.mapLineAnimate">
<label class="alpha-label" :class="{ dark: 'dark' === themes }">
{{ t('chart.map_line_animate_duration') }}
</label>
<el-row style="flex: 1" :gutter="8">
<el-col>
<el-form-item class="form-item alpha-slider" :class="'form-item-' + themes">
<el-slider
:effect="themes"
:min="0"
:max="20"
v-model="state.miscForm.mapLineAnimateDuration"
@change="changeMisc('mapLineAnimateDuration')"
/>
</el-form-item>
</el-col>
</el-row>
</div>
</div>
<div v-if="!state.miscForm.mapLineGradient">
<el-row style="flex: 1" :gutter="8">
<el-col>
<el-form-item
class="form-item"
:class="'form-item-' + themes"
:label="t('chart.color')"
>
<el-color-picker
is-custom
class="color-picker-style"
v-model="state.miscForm.mapLineSourceColor"
:persistent="false"
:effect="themes"
:trigger-width="108"
:predefine="predefineColors"
@change="changeMisc('mapLineSourceColor')"
/>
</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" :gutter="8">
<el-col :span="13">
<el-form-item class="form-item alpha-slider" :class="'form-item-' + themes">
<el-slider
:effect="themes"
v-model="state.basicStyleForm.alpha"
@change="changeBasicStyle('alpha')"
/>
</el-form-item>
</el-col>
<el-col :span="11" style="padding-top: 2px">
<el-form-item class="form-item" :class="'form-item-' + themes">
<el-input
type="number"
:effect="themes"
v-model="state.basicStyleForm.alpha"
:min="0"
:max="100"
class="basic-input-number"
:controls="false"
@change="changeBasicStyle('alpha')"
>
<template #suffix> % </template>
</el-input>
</el-form-item>
</el-col>
</el-row>
</div>
<el-row style="flex: 1">
<el-col>
<el-form-item class="form-item" :class="'form-item-' + themes">
<el-checkbox
size="small"
:effect="themes"
v-model="state.miscForm.mapLineAnimate"
:predefine="predefineColors"
@change="changeMisc('mapLineAnimate')"
>
{{ t('chart.line') + t('chart.map_line_animate') }}
</el-checkbox>
</el-form-item>
</el-col>
</el-row>
<div class="alpha-setting" v-if="state.miscForm.mapLineAnimate">
<label class="alpha-label" :class="{ dark: 'dark' === themes }">
{{ t('chart.map_line_animate_duration') }}
</label>
<el-row style="flex: 1" :gutter="8">
<el-col>
<el-form-item class="form-item alpha-slider" :class="'form-item-' + themes">
<el-slider
:effect="themes"
:min="0"
:max="20"
v-model="state.miscForm.mapLineAnimateDuration"
@change="changeMisc('mapLineAnimateDuration')"
/>
</el-form-item>
</el-col>
</el-row>
</div>
</div>
<div class="alpha-setting" v-if="showProperty('heatMapStyle')">
<label class="alpha-label" :class="{ dark: 'dark' === themes }">
{{ t('chart.heatMapIntensity') }}
</label>
<el-row style="flex: 1" :gutter="8">
<el-col>
<el-form-item class="form-item alpha-slider" :class="'form-item-' + themes">
<el-slider
:effect="themes"
:min="1"
:max="20"
v-model="state.basicStyleForm.heatMapIntensity"
@change="changeBasicStyle('heatMapIntensity')"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<div class="alpha-setting" v-if="showProperty('heatMapStyle')">
<label class="alpha-label" :class="{ dark: 'dark' === themes }">
{{ t('chart.heatMapRadius') }}
</label>
<el-row style="flex: 1" :gutter="8">
<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.heatMapRadius"
@change="changeBasicStyle('heatMapRadius')"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<!--flow map end-->
<!--map start-->

View File

@ -1317,6 +1317,13 @@ export const CHART_TYPE_CONFIGS = [
value: 'flow-map',
title: t('chart.chart_flow_map'),
icon: 'flow-map'
},
{
render: 'antv',
category: 'map',
value: 'heat-map',
title: t('chart.chart_heat_map'),
icon: 'heat-map'
}
]
},
@ -1421,6 +1428,9 @@ export const DEFAULT_BASIC_STYLE: ChartBasicStyle = {
scatterSymbolSize: 8,
radarShape: 'polygon',
mapStyle: 'normal',
heatMapType: 'heatmap',
heatMapIntensity: 2,
heatMapRadius: 20,
areaBorderColor: '#EBEEF5',
areaBaseColor: '#ffffff',
mapSymbolOpacity: 0.7,

View File

@ -0,0 +1,120 @@
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, 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 { HeatmapLayer } from '@antv/l7-layers'
import { queryMapKeyApi } from '@/api/setting/sysParameter'
import { DEFAULT_BASIC_STYLE } from '@/views/chart/components/editor/util/chart'
const { t } = useI18n()
/**
* 流向地图
*/
export class HeatMap extends L7ChartView<Scene, L7Config> {
properties: EditorProperty[] = [
'background-overall-component',
'basic-style-selector',
'title-selector'
]
propertyInner: EditorPropertyInner = {
...MAP_EDITOR_PROPERTY_INNER,
'basic-style-selector': ['colors', 'heatMapStyle', 'zoom']
}
axis: AxisType[] = ['xAxis', 'yAxis', 'filter']
axisConfig: AxisConfig = {
xAxis: {
name: `${t('chart.longitude_and_latitude')} / ${t('chart.dimension')}`,
type: 'd',
limit: 2
},
yAxis: {
name: `${t('chart.chart_data')} / ${t('chart.quota')}`,
type: 'q',
limit: 1
}
}
constructor() {
super('heat-map', [])
}
async drawChart(drawOption: L7DrawConfig<L7Config>) {
const { chart, container } = drawOption
const xAxis = deepCopy(chart.xAxis)
const yAxis = deepCopy(chart.yAxis)
let basicStyle
let miscStyle
if (chart.customAttr) {
basicStyle = parseJson(chart.customAttr).basicStyle
miscStyle = parseJson(chart.customAttr).misc
}
console.log(basicStyle)
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,
zoom: 2.5
})
})
if (xAxis?.length < 2 || yAxis?.length < 1) {
return new L7Wrapper(scene, undefined)
}
console.log(chart.data?.data)
const config: L7Config = new HeatmapLayer({
name: 'line',
blend: 'normal',
autoFit: true
})
.source(chart.data?.data, {
parser: {
type: 'json',
x: 'x',
y: 'y'
}
})
.size('value', [0, 1.0]) // weight映射通道
.shape(basicStyle.heatMapType ?? DEFAULT_BASIC_STYLE.heatMapType)
config.style({
intensity: basicStyle.heatMapIntensity ?? DEFAULT_BASIC_STYLE.heatMapIntensity,
radius: basicStyle.heatMapRadius ?? DEFAULT_BASIC_STYLE.heatMapRadius,
rampColors: {
colors: basicStyle.colors.reverse(),
positions: [0, 0.11, 0.22, 0.33, 0.44, 0.55, 0.66, 0.77, 0.88, 1.0]
}
})
this.configZoomButton(chart, scene)
return new L7Wrapper(scene, config)
}
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.misc.mapLineAnimate = true
return chart
}
protected setupOptions(chart: Chart, config: L7Config): L7Config {
return flow(this.configEmptyDataStrategy)(chart, config)
}
}