From e7904b667ea9db65794f91820e9ae15286448aff Mon Sep 17 00:00:00 2001 From: limengnan <420004014@qq.com> Date: Tue, 1 Jul 2025 16:29:15 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9views\chart\components\views?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chart/components/js/extremumUitl.ts | 12 +- .../chart/components/js/formatter.ts | 121 ++++++++++---- .../chart/components/js/util.ts | 156 +++++++++++++----- .../views/components/ChartComponentG2Plot.vue | 140 ++++++++++++---- .../views/components/ChartComponentS2.vue | 51 +++--- .../views/components/ChartError.vue | 2 +- .../components/views/components/DrillPath.vue | 40 +---- .../views/components/ScrollShadow.vue | 16 ++ .../chart/components/views/index.vue | 121 +++++++++----- 9 files changed, 440 insertions(+), 219 deletions(-) create mode 100644 frontend/src/data-visualization/chart/components/views/components/ScrollShadow.vue diff --git a/frontend/src/data-visualization/chart/components/js/extremumUitl.ts b/frontend/src/data-visualization/chart/components/js/extremumUitl.ts index 6413f62..714aa62 100644 --- a/frontend/src/data-visualization/chart/components/js/extremumUitl.ts +++ b/frontend/src/data-visualization/chart/components/js/extremumUitl.ts @@ -79,7 +79,8 @@ function createExtremumDiv(id, value, formatterCfg, chart) { transform: translateX(-50%); opacity: 1; transition: opacity 0.2s ease-in-out; - white-space:nowrap;` + white-space:nowrap; + overflow:auto;` ) div.textContent = valueFormatter(value, formatterCfg) const span = document.createElement('span') @@ -109,7 +110,7 @@ const noChildrenFieldChart = chart => { * 支持最值图表的折线图,面积图,柱状图,分组柱状图 * @param chart */ -const supportExtremumChartType = chart => { +export const supportExtremumChartType = chart => { return ['line', 'area', 'bar', 'bar-group'].includes(chart.type) } @@ -138,8 +139,8 @@ function removeDivsWithPrefix(parentDivId, prefix) { export const extremumEvt = (newChart, chart, _options, container) => { chart.container = container + clearExtremum(chart) if (!supportExtremumChartType(chart)) { - clearExtremum(chart) return } const { label: labelAttr } = parseJson(chart.customAttr) @@ -150,7 +151,9 @@ export const extremumEvt = (newChart, chart, _options, container) => { i.forEach(item => { delete item._origin.EXTREME }) - const { minItem, maxItem } = findMinMax(i.filter(item => item._origin.value)) + const { minItem, maxItem } = findMinMax( + i.filter(item => item?._origin?.value !== null && item?._origin?.value !== undefined) + ) if (!minItem || !maxItem) { return } @@ -223,6 +226,7 @@ export const createExtremumPoint = (chart, ev) => { divParent.style.zIndex = '1' divParent.style.opacity = '0' divParent.style.transition = 'opacity 0.2s ease-in-out' + divParent.style.overflow = 'visible' // 将父标注加入到图表中 const containerElement = document.getElementById(chart.container) containerElement.insertBefore(divParent, containerElement.firstChild) diff --git a/frontend/src/data-visualization/chart/components/js/formatter.ts b/frontend/src/data-visualization/chart/components/js/formatter.ts index a297fbb..222c12a 100644 --- a/frontend/src/data-visualization/chart/components/js/formatter.ts +++ b/frontend/src/data-visualization/chart/components/js/formatter.ts @@ -1,7 +1,13 @@ -import { Datum } from '@antv/g2plot' +import { find } from 'lodash-es' +import { useI18n } from '@/data-visualization/hooks/web/useI18n' +import { getLocale } from '@/data-visualization/utils/utils' +const { t } = useI18n() + +export const isEnLocal = !['zh', 'zh-cn', 'zh-CN', 'tw'].includes(getLocale()) export const formatterItem = { type: 'auto', // auto,value,percent + unitLanguage: isEnLocal ? 'en' : 'ch', unit: 1, // 换算单位 suffix: '', // 单位后缀 decimalCount: 2, // 小数位数 @@ -10,12 +16,51 @@ export const formatterItem = { // 单位list export const unitType = [ - { name: 'unit_none', value: 1 }, - { name: 'unit_thousand', value: 1000 }, - { name: 'unit_ten_thousand', value: 10000 }, - { name: 'unit_million', value: 1000000 }, - { name: 'unit_hundred_million', value: 100000000 } + { name: t('chart.unit_none'), value: 1 }, + { name: t('chart.unit_thousand'), value: 1000 }, + { name: t('chart.unit_ten_thousand'), value: 10000 }, + { name: t('chart.unit_million'), value: 1000000 }, + { name: t('chart.unit_hundred_million'), value: 100000000 } ] +export const unitEnType = [ + { name: 'None', value: 1 }, + { name: 'Thousand (K)', value: 1000 }, + { name: 'Million (M)', value: 1000000 }, + { name: 'Billion (B)', value: 1000000000 } +] + +export function getUnitTypeList(lang) { + if (isEnLocal) { + return unitEnType + } + if (lang === 'ch') { + return unitType + } + return unitEnType +} + +export function getUnitTypeValue(lang, value) { + const list = getUnitTypeList(lang) + const item = find(list, l => l.value === value) + if (item) { + return value + } + return 1 +} + +export function initFormatCfgUnit(cfg) { + if (cfg && cfg.unitLanguage === undefined) { + cfg.unitLanguage = 'ch' + } + if (cfg && isEnLocal) { + cfg.unitLanguage = 'en' + } + onChangeFormatCfgUnitLanguage(cfg, cfg.unitLanguage) +} + +export function onChangeFormatCfgUnitLanguage(cfg, lang) { + cfg.unit = getUnitTypeValue(lang, cfg.unit) +} // 格式化方式 export const formatterType = [ @@ -47,17 +92,32 @@ export function valueFormatter(value, formatter) { } function transUnit(value, formatter) { + initFormatCfgUnit(formatter) return value / formatter.unit } function transDecimal(value, formatter) { - const resultV = value.toFixed(formatter.decimalCount) + const resultV = retain(value, formatter.decimalCount) as string if (Object.is(parseFloat(resultV), -0)) { return resultV.slice(1) } return resultV } +function retain(value, n) { + if (!n) return Math.round(value) + const tran = Math.round(value * Math.pow(10, n)) / Math.pow(10, n) + let tranV = tran.toString() + const newVal = tranV.indexOf('.') + if (newVal < 0) { + tranV += '.' + } + for (let i = tranV.length - tranV.indexOf('.'); i <= n; i++) { + tranV += '0' + } + return tranV +} + function transSeparatorAndSuffix(value, formatter) { let str = value + '' if (str.match(/^(\d)(\.\d)?e-(\d)/)) { @@ -74,34 +134,27 @@ function transSeparatorAndSuffix(value, formatter) { //百分比没有后缀,直接返回 return str } else { - if (formatter.unit === 1000) { - str += '千' - } else if (formatter.unit === 10000) { - str += '万' - } else if (formatter.unit === 1000000) { - str += '百万' - } else if (formatter.unit === 100000000) { - str += '亿' + const unit = formatter.unit + + if (formatter.unitLanguage === 'ch') { + if (unit === 1000) { + str += t('chart.unit_thousand') + } else if (unit === 10000) { + str += t('chart.unit_ten_thousand') + } else if (unit === 1000000) { + str += t('chart.unit_million') + } else if (unit === 100000000) { + str += t('chart.unit_hundred_million') + } + } else { + if (unit === 1000) { + str += 'K' + } else if (unit === 1000000) { + str += 'M' + } else if (unit === 1000000000) { + str += 'B' + } } } return str + formatter.suffix.replace(/(^\s*)|(\s*$)/g, '') } - -export function singleDimensionTooltipFormatter(param: Datum, chart: Chart, prop = 'category') { - let res - const yAxis = chart.yAxis - const obj = { name: param[prop], value: param.value } - for (let i = 0; i < yAxis.length; i++) { - const f = yAxis[i] - if (f.name === param[prop]) { - if (f.formatterCfg) { - res = valueFormatter(param.value, f.formatterCfg) - } else { - res = valueFormatter(param.value, formatterItem) - } - break - } - } - obj.value = res ?? '' - return obj -} diff --git a/frontend/src/data-visualization/chart/components/js/util.ts b/frontend/src/data-visualization/chart/components/js/util.ts index 33984be..1e7af19 100644 --- a/frontend/src/data-visualization/chart/components/js/util.ts +++ b/frontend/src/data-visualization/chart/components/js/util.ts @@ -1,4 +1,4 @@ -import { isEmpty, isNumber } from 'lodash-es' +import { isNumber } from 'lodash-es' import { DEFAULT_TITLE_STYLE } from '../editor/util/chart' import { equalsAny, includesAny } from '../editor/util/StringUtils' import { FeatureCollection } from '@antv/l7plot/dist/esm/plots/choropleth/types' @@ -12,6 +12,8 @@ import { ElMessage } from 'element-plus-secondary' import { useI18n } from '@/data-visualization/hooks/web/useI18n' import { useLinkStoreWithOut } from '@/data-visualization/store/modules/link' import { useAppStoreWithOut } from '@/data-visualization/store/modules/app' +import { Decimal } from 'decimal.js' + const appStore = useAppStoreWithOut() const isDataEaseBi = computed(() => appStore.getIsDataEaseBi) @@ -283,17 +285,23 @@ export function handleEmptyDataStrategy(chart: Chart, opt } return options } - const { yAxis, xAxisExt, extStack } = chart + const { yAxis, xAxisExt, extStack, extBubble } = chart const multiDimension = yAxis?.length >= 2 || xAxisExt?.length > 0 || extStack?.length > 0 switch (strategy) { case 'breakLine': { - if (multiDimension) { - // 多维度保持空 - if (isChartMix) { - for (let i = 0; i < data.length; i++) { - handleBreakLineMultiDimension(data[i] as Record[]) + if (isChartMix) { + if (data[0]) { + if (xAxisExt?.length > 0 || extStack?.length > 0) { + handleBreakLineMultiDimension(data[0] as Record[]) } - } else { + } + if (data[1]) { + if (extBubble?.length > 0) { + handleBreakLineMultiDimension(data[1] as Record[]) + } + } + } else { + if (multiDimension) { handleBreakLineMultiDimension(data) } } @@ -303,22 +311,27 @@ export function handleEmptyDataStrategy(chart: Chart, opt } } case 'setZero': { - if (multiDimension) { - // 多维度置0 - if (isChartMix) { - for (let i = 0; i < data.length; i++) { - handleSetZeroMultiDimension(data[i] as Record[]) + if (isChartMix) { + if (data[0]) { + if (xAxisExt?.length > 0 || extStack?.length > 0) { + handleSetZeroMultiDimension(data[0] as Record[]) + } else { + handleSetZeroSingleDimension(data[0] as Record[]) + } + } + if (data[1]) { + if (extBubble?.length > 0) { + handleSetZeroMultiDimension(data[1] as Record[], true) + } else { + handleSetZeroSingleDimension(data[1] as Record[], true) } - } else { - handleSetZeroMultiDimension(data) } } else { - // 单维度置0 - if (isChartMix) { - for (let i = 0; i < data.length; i++) { - handleSetZeroSingleDimension(data[i] as Record[]) - } + if (multiDimension) { + // 多维度置0 + handleSetZeroMultiDimension(data) } else { + // 单维度置0 handleSetZeroSingleDimension(data) } } @@ -364,7 +377,7 @@ function handleBreakLineMultiDimension(data) { }) } -function handleSetZeroMultiDimension(data: Record[]) { +function handleSetZeroMultiDimension(data: Record[], isExt = false) { const dimensionInfoMap = new Map() const subDimensionSet = new Set() const quotaMap = new Map() @@ -372,6 +385,9 @@ function handleSetZeroMultiDimension(data: Record[]) { const item = data[i] if (item.value === null) { item.value = 0 + if (isExt) { + item.valueExt = 0 + } } const dimensionInfo = dimensionInfoMap.get(item.field) if (dimensionInfo) { @@ -388,12 +404,17 @@ function handleSetZeroMultiDimension(data: Record[]) { let subInsertIndex = 0 subDimensionSet.forEach(dimension => { if (!dimensionInfo.set.has(dimension)) { - data.splice(dimensionInfo.index + insertCount + subInsertIndex, 0, { + const _temp = { field, value: 0, category: dimension, quotaList: quotaMap.get(dimension as string) - }) + } as any + if (isExt) { + _temp.valueExt = 0 + } + + data.splice(dimensionInfo.index + insertCount + subInsertIndex, 0, _temp) } subInsertIndex++ }) @@ -402,10 +423,14 @@ function handleSetZeroMultiDimension(data: Record[]) { }) } -function handleSetZeroSingleDimension(data: Record[]) { +function handleSetZeroSingleDimension(data: Record[], isExt = false) { data.forEach(item => { if (item.value === null) { - item.value = 0 + if (!isExt) { + item.value = 0 + } else { + item.valueExt = 0 + } } }) } @@ -489,7 +514,7 @@ const getExcelDownloadRequest = (data, type?) => { const tableRow = JSON.parse(JSON.stringify(data.tableRow)) const excelHeader = fields.map(item => item.chartShowName ?? item.name) const excelTypes = fields.map(item => item.deType) - const excelHeaderKeys = fields.map(item => item.dataeaseName) + const excelHeaderKeys = fields.map(item => item.gisbiName) let excelData = tableRow.map(item => excelHeaderKeys.map(i => item[i])) let detailFields = [] if (data.detailFields?.length) { @@ -497,7 +522,7 @@ const getExcelDownloadRequest = (data, type?) => { return { name: item.name, deType: item.deType, - dataeaseName: item.dataeaseName + gisbiName: item.gisbiName } }) excelData = tableRow.map(item => { @@ -505,7 +530,7 @@ const getExcelDownloadRequest = (data, type?) => { if (i === 'detail' && !item[i] && Array.isArray(item['details'])) { const arr = item['details'] if (arr?.length) { - return arr.map(ele => detailFields.map(field => ele[field.dataeaseName])) + return arr.map(ele => detailFields.map(field => ele[field.gisbiName])) } return null } @@ -522,8 +547,20 @@ const getExcelDownloadRequest = (data, type?) => { } } -export const exportExcelDownload = (chart, callBack?) => { - const excelName = chart.title +function getChartExcelTitle(preFix, viewTitle) { + const now = new Date() + const pad = n => n.toString().padStart(2, '0') + const year = now.getFullYear() + const month = pad(now.getMonth() + 1) // 月份从 0 开始 + const day = pad(now.getDate()) + const hour = pad(now.getHours()) + const minute = pad(now.getMinutes()) + const second = pad(now.getSeconds()) + return `${preFix}_${viewTitle}_${year}${month}${day}_${hour}${minute}${second}` +} + +export const exportExcelDownload = (chart, preFix, callBack?) => { + const excelName = getChartExcelTitle(preFix, chart.title) let request: any = { proxy: null, dvId: chart.sceneId, @@ -586,18 +623,21 @@ export const exportExcelDownload = (chart, callBack?) => { } export const copyString = (content: string, notify = false) => { - const clipboard = navigator.clipboard || { - writeText: data => { - return new Promise(resolve => { - const textareaDom = document.createElement('textarea') - textareaDom.setAttribute('style', 'z-index: -1;position: fixed;opacity: 0;') - textareaDom.value = data - document.body.appendChild(textareaDom) - textareaDom.select() - document.execCommand('copy') - textareaDom.remove() - resolve() - }) + let clipboard = navigator.clipboard as Pick + if (!clipboard || window.top !== window.self) { + clipboard = { + writeText: data => { + return new Promise(resolve => { + const textareaDom = document.createElement('textarea') + textareaDom.setAttribute('style', 'z-index: -1;position: fixed;opacity: 0;') + textareaDom.value = data + document.body.appendChild(textareaDom) + textareaDom.select() + document.execCommand('copy') + textareaDom.remove() + resolve() + }) + } } } clipboard.writeText(content).then(() => { @@ -779,7 +819,7 @@ export function getColor(chart: Chart) { } } -export function setupSeriesColor(chart: ChartObj, data?: any[]): ChartBasicStyle['seriesColor'] { +export function setupSeriesColor(chart: ChartObj): ChartBasicStyle['seriesColor'] { const result: ChartBasicStyle['seriesColor'] = [] const seriesSet = new Set() const colors = chart.customAttr.basicStyle.colors @@ -1152,8 +1192,10 @@ export function getLineLabelColorByCondition(conditions, value, fieldId) { if (fieldConditions.length) { fieldConditions.some(item => { if ( - (item.term === 'lt' && value <= item.value) || - (item.term === 'gt' && value >= item.value) || + (item.term === 'lt' && value < item.value) || + (item.term === 'le' && value <= item.value) || + (item.term === 'gt' && value > item.value) || + (item.term === 'ge' && value >= item.value) || (item.term === 'between' && value >= item.min && value <= item.max) ) { color = item.color @@ -1207,3 +1249,27 @@ export const hexToRgba = (hex, alpha = 1) => { // 返回 RGBA 格式 return `rgba(${r}, ${g}, ${b}, ${a})` } + +// 安全计算数值字段的总和,使用 Decimal 避免浮点数精度问题 +export function safeDecimalSum(data, field) { + // 使用 reduce 累加所有行的指定字段值 + return data + .reduce((acc, row) => { + // 将字段值转换为 Decimal 类型并累加到累加器 + return acc.plus(new Decimal(row[field] ?? 0)) + }, new Decimal(0)) + .toNumber() // 最终结果转换为普通数字返回 +} + +// 安全计算数值字段的平均值,使用 Decimal 避免浮点数精度问题 +export function safeDecimalMean(data, field) { + // 如果数据为空,直接返回 0 + if (!data.length) return 0 + // 计算所有行的指定字段值的总和 + const sum = data.reduce((acc, row) => { + // 将字段值转换为 Decimal 类型并累加到累加器 + return acc.plus(new Decimal(row[field] ?? 0)) + }, new Decimal(0)) + // 将总和除以数据行数,得到平均值,并转换为普通数字返回 + return sum.dividedBy(data.length).toNumber() +} diff --git a/frontend/src/data-visualization/chart/components/views/components/ChartComponentG2Plot.vue b/frontend/src/data-visualization/chart/components/views/components/ChartComponentG2Plot.vue index 850343f..df0eef1 100644 --- a/frontend/src/data-visualization/chart/components/views/components/ChartComponentG2Plot.vue +++ b/frontend/src/data-visualization/chart/components/views/components/ChartComponentG2Plot.vue @@ -28,7 +28,8 @@ import { isDashboard, trackBarStyleCheck } from '@/data-visualization/utils/canv import { useEmitt } from '@/data-visualization/hooks/web/useEmitt' import { L7ChartView } from '@/data-visualization/chart/components/js/panel/types/impl/l7' import { useI18n } from '@/data-visualization/hooks/web/useI18n' -import { ExportImage,Scale } from '@antv/l7' +import { ExportImage, Scale, Fullscreen, Control, Scene, TileLayer } from '@antv/l7' +import { GaodeMap } from '@antv/l7-maps'; const { t } = useI18n() const dvMainStore = dvMainStoreWithOut() const { nowPanelTrackInfo, nowPanelJumpInfo, mobileInPc, embeddedCallBack, inMobile } = @@ -111,7 +112,8 @@ const state = reactive({ }, linkageActiveParam: null, pointParam: null, - data: { fields: [] } // 图表数据 + data: { fields: [] }, // 图表数据 + satelliteVisible: false, // 新增卫星图层状态 }) let chartData = shallowRef>({ fields: [] @@ -333,35 +335,100 @@ const renderL7Plot = async (chart: ChartObj, chartView: L7PlotChartView, callback) => { - mapL7Timer && clearTimeout(mapL7Timer) + mapL7Timer && clearTimeout(mapL7Timer); mapL7Timer = setTimeout(async () => { myChart = await chartView.drawChart({ chartObj: myChart, container: containerId, chart: chart, action - }) - - // 清除已有比例尺 - if (scaleControl) { - myChart.getScene()?.removeControl(scaleControl) - scaleControl = null + }); + + // 清除已有比例尺 + if (!scaleControl) { + scaleControl = new Scale({ + position: 'bottomleft', + imperial: false + }); + myChart.getScene()?.addControl(scaleControl); } // 创建并添加新比例尺 - scaleControl = new Scale({ - position: 'bottomleft', - imperial: false - }) - myChart.getScene()?.addControl(scaleControl) - myChart?.render() - callback?.() - emit('resetLoading') - }, 500) -} + + // 添加全屏控件 + if (fullscreenControl) { + + } else { + fullscreenControl = new Fullscreen({ + position: 'bottomright', + }); + myChart.getScene()?.addControl(fullscreenControl, 'bottomright'); + } + + + // ====== 使用高德地图原生API实现卫星图层切换 ====== + let satelliteLayer: any = null; + let isSatelliteVisible = false; + + class SatelliteControl extends Control { + protected onAdd() { + const btn = document.createElement('button'); + btn.className = 'l7-control-button l7-satellite-control'; + btn.innerHTML = '卫星'; + // btn.title = '切换到卫星视图'; + btn.style.backgroundColor = '#000'; + btn.style.color = '#fff'; + btn.style.padding = '2px'; + btn.style.borderRadius = '4px'; + btn.style.cursor = 'pointer' + btn.style.fontSize = '11px'; + const scene = myChart.getScene() + // 确保地图加载完成 + scene.on('loaded', () => { + // 创建高德卫星图层 + satelliteLayer = new window.AMap.TileLayer.Satellite(); + btn.onclick = () => { + isSatelliteVisible = !isSatelliteVisible; + + if (isSatelliteVisible) { + // 使用 scene.addLayer 方法添加卫星图层 + btn.style.backgroundColor = '#409eff'; + scene.map.add(satelliteLayer) + } else { + // 使用 scene.removeLayer 方法移除卫星图层 + btn.style.backgroundColor = '#000'; + scene.map.remove(satelliteLayer) + + } + }; + }); + + return btn; + } + } + + // 添加控件到地图 + // 移除之前的卫星控件(如果存在) + if (satelliteControlInstance) { + + } else { + // 添加新的卫星控件到地图 + satelliteControlInstance = new SatelliteControl({ position: 'bottomright' }); + myChart.getScene()?.addControl(satelliteControlInstance); + } + + + // ====== 修复完成 ====== + + myChart?.render(); + callback?.(); + emit('resetLoading'); + }, 500); +}; const pointClickTrans = () => { if (embeddedCallBack.value === 'yes') { trackClick('pointClick') @@ -706,15 +773,8 @@ onBeforeUnmount(() => {