From f2582cb0d06f9b9ed3bb111110f261b5eda75db2 Mon Sep 17 00:00:00 2001 From: limengnan <420004014@qq.com> Date: Tue, 24 Jun 2025 09:17:57 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=89=8D=E7=AB=AFsrc/views?= =?UTF-8?q?=EF=BC=88chart=E5=92=8Ctemplate=EF=BC=89=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/views/chart/ChartView.vue | 4 +- .../editor/chart-type/ChartType.vue | 12 +- .../editor/common/ChartTemplateInfo.vue | 109 ++- .../components/editor/common/TemplateTips.vue | 76 ++ .../editor/dataset-select/DatasetSelect.vue | 7 +- .../editor/drag-item/DimensionItem.vue | 60 +- .../components/editor/drag-item/DrillItem.vue | 13 +- .../components/editor/drag-item/QuotaItem.vue | 45 +- .../drag-item/components/CustomSortEdit.vue | 16 + .../components/ValueFormatterEdit.vue | 68 +- .../components/editor/drag-item/utils.ts | 1 + .../editor/editor-senior/Senior.vue | 18 +- .../editor-senior/components/AssistLine.vue | 10 +- .../editor-senior/components/Threshold.vue | 13 +- .../components/dialog/AssistLineEdit.vue | 46 +- .../components/dialog/LineThresholdEdit.vue | 12 +- .../dialog/PictureGroupThresholdEdit.vue | 17 +- .../components/dialog/TableThresholdEdit.vue | 35 +- .../components/dialog/TextThresholdEdit.vue | 10 +- .../editor/editor-style/ChartStyle.vue | 66 +- .../editor-style/ChartStyleBatchSet.vue | 2 +- .../editor/editor-style/VQueryChartStyle.vue | 107 ++- .../components/BasicStyleSelector.vue | 234 +++++-- .../components/CustomColorStyleSelect.vue | 17 +- .../components/DualBasicStyleSelector.vue | 37 +- .../components/DualYAxisSelector.vue | 5 +- .../components/DualYAxisSelectorInner.vue | 172 +++-- .../components/FlowMapLineSelector.vue | 7 +- .../components/FlowMapPointSelector.vue | 5 +- .../components/GradientColorSelector.vue | 4 +- .../components/IndicatorNameSelector.vue | 15 +- .../components/IndicatorValueSelector.vue | 15 +- .../editor-style/components/LabelSelector.vue | 660 +++++++++++------- .../components/LegendSelector.vue | 133 +++- .../editor-style/components/MiscSelector.vue | 539 ++++++-------- .../components/MiscStyleSelector.vue | 4 +- .../components/QuadrantSelector.vue | 2 +- .../components/SummarySelector.vue | 194 +++++ .../components/SymbolicStyleSelector.vue | 4 +- .../editor-style/components/TitleSelector.vue | 11 +- .../components/TooltipSelector.vue | 270 ++++--- .../editor-style/components/XAxisSelector.vue | 141 ++-- .../editor-style/components/YAxisSelector.vue | 156 +++-- .../bullet/BulletMeasureSelector.vue | 118 ++++ .../components/bullet/BulletRangeSelector.vue | 259 +++++++ .../bullet/BulletTargetSelector.vue | 183 +++++ .../components/table/CustomAggrEdit.vue | 11 +- .../components/table/TableCellSelector.vue | 8 +- .../table/TableHeaderGroupConfig.vue | 84 ++- .../components/table/TableHeaderSelector.vue | 44 +- .../components/table/TableTotalSelector.vue | 90 ++- .../components/editor/filter/FilterTree.vue | 4 +- .../filter/auth-tree-chart/AuthTree.vue | 14 +- .../filter/auth-tree-chart/FilterFiled.vue | 12 +- .../editor/filter/auth-tree/AuthTree.vue | 1 - .../editor/filter/auth-tree/FilterFiled.vue | 16 +- .../views/chart/components/editor/index.vue | 181 +++-- .../chart/components/editor/util/chart.ts | 76 +- .../editor/util/dataVisualization.ts | 1 + .../views/chart/components/js/extremumUitl.ts | 12 +- .../views/chart/components/js/formatter.ts | 121 +++- .../components/js/g2plot_tooltip_carousel.ts | 656 +++++++++++++++++ .../components/js/panel/charts/bar/bar.ts | 121 +++- .../js/panel/charts/bar/bidirectional-bar.ts | 20 +- .../js/panel/charts/bar/bullet-graph.ts | 507 ++++++++++++++ .../js/panel/charts/bar/horizontal-bar.ts | 80 ++- .../js/panel/charts/bar/progress-bar.ts | 44 +- .../js/panel/charts/bar/range-bar.ts | 47 +- .../js/panel/charts/bar/waterfall.ts | 3 +- .../components/js/panel/charts/line/area.ts | 9 +- .../components/js/panel/charts/line/line.ts | 87 ++- .../js/panel/charts/line/stock-line.ts | 1 - .../js/panel/charts/liquid/liquid.ts | 2 +- .../js/panel/charts/map/bubble-map.ts | 3 +- .../components/js/panel/charts/map/common.ts | 26 + .../js/panel/charts/map/flow-map.ts | 114 +-- .../js/panel/charts/map/heat-map.ts | 109 +-- .../components/js/panel/charts/map/map.ts | 28 +- .../js/panel/charts/map/symbolic-map.ts | 72 +- .../js/panel/charts/others/chart-mix.ts | 35 +- .../js/panel/charts/others/circle-packing.ts | 6 +- .../js/panel/charts/others/gauge.ts | 10 +- .../js/panel/charts/others/indicator.ts | 3 +- .../js/panel/charts/others/quadrant.ts | 6 +- .../js/panel/charts/others/scatter.ts | 49 +- .../components/js/panel/charts/pie/pie.ts | 24 +- .../components/js/panel/charts/pie/rose.ts | 1 + .../js/panel/charts/table/common.ts | 1 + .../js/panel/charts/table/t-heatmap.ts | 9 +- .../js/panel/charts/table/table-info.ts | 187 +++-- .../js/panel/charts/table/table-normal.ts | 108 ++- .../js/panel/charts/table/table-pivot.ts | 405 +++++++++-- .../components/js/panel/common/common_antv.ts | 613 ++++++++++++++-- .../js/panel/common/common_table.ts | 572 ++++++++++++--- .../components/js/panel/types/impl/g2plot.ts | 6 +- .../components/js/panel/types/impl/l7.ts | 4 +- .../components/js/panel/types/impl/s2.ts | 22 +- .../src/views/chart/components/js/util.ts | 151 ++-- .../views/components/ChartComponentG2Plot.vue | 261 ++++--- .../views/components/ChartComponentS2.vue | 49 +- .../views/components/ChartError.vue | 2 +- .../components/views/components/DrillPath.vue | 40 +- .../views/components/ScrollShadow.vue | 16 + .../views/chart/components/views/index.vue | 104 ++- .../template/component/DeTemplateImport.vue | 4 +- .../src/views/template/indexInject.vue | 21 + 106 files changed, 7041 insertions(+), 2204 deletions(-) create mode 100644 core/core-frontend/src/views/chart/components/editor/common/TemplateTips.vue create mode 100644 core/core-frontend/src/views/chart/components/editor/editor-style/components/SummarySelector.vue create mode 100644 core/core-frontend/src/views/chart/components/editor/editor-style/components/bullet/BulletMeasureSelector.vue create mode 100644 core/core-frontend/src/views/chart/components/editor/editor-style/components/bullet/BulletRangeSelector.vue create mode 100644 core/core-frontend/src/views/chart/components/editor/editor-style/components/bullet/BulletTargetSelector.vue create mode 100644 core/core-frontend/src/views/chart/components/js/g2plot_tooltip_carousel.ts create mode 100644 core/core-frontend/src/views/chart/components/js/panel/charts/bar/bullet-graph.ts create mode 100644 core/core-frontend/src/views/chart/components/views/components/ScrollShadow.vue create mode 100644 core/core-frontend/src/views/template/indexInject.vue diff --git a/core/core-frontend/src/views/chart/ChartView.vue b/core/core-frontend/src/views/chart/ChartView.vue index c8183af..42ceecd 100644 --- a/core/core-frontend/src/views/chart/ChartView.vue +++ b/core/core-frontend/src/views/chart/ChartView.vue @@ -29,6 +29,7 @@ const DashboardPanel = defineAsyncComponent( () => import('@/views/dashboard/DashboardPreviewShow.vue') ) const Copilot = defineAsyncComponent(() => import('@/views/copilot/index.vue')) +const TemplateManage = defineAsyncComponent(() => import('@/views/template/indexInject.vue')) const AsyncXpackComponent = defineAsyncComponent(() => import('@/components/plugin/src/index.vue')) @@ -42,7 +43,8 @@ const componentMap = { Datasource, ScreenPanel, DashboardPanel, - Copilot + Copilot, + TemplateManage } const iframeStyle = ref(null) const setStyle = debounce(() => { diff --git a/core/core-frontend/src/views/chart/components/editor/chart-type/ChartType.vue b/core/core-frontend/src/views/chart/components/editor/chart-type/ChartType.vue index 55b3c34..0c02628 100644 --- a/core/core-frontend/src/views/chart/components/editor/chart-type/ChartType.vue +++ b/core/core-frontend/src/views/chart/components/editor/chart-type/ChartType.vue @@ -127,19 +127,19 @@ const groupActiveChange = category => { .chart-light { color: #646a73 !important; :deep(.group-right) { - border-left: 1px solid @side-outline-border-color-light; + border-left: 1px solid @side-outline-border-color-light!important; } :deep(.item-top) { - background-color: #f5f6f7; + background-color: #f5f6f7 !important; } :deep(.ul-custom) { color: @chart-change-font-color-light!important; } :deep(.item-bottom) { - color: @chart-change-font-color-light; + color: @chart-change-font-color-light!important; } :deep(.item-top-icon) { - color: @chart-change-font-color-light; + color: @chart-change-font-color-light!important; } } .group { @@ -250,8 +250,8 @@ const groupActiveChange = category => { } .group .group-left .ul-custom{ color: #a6a6a6 !important; -} +} .chart-light .item-top{ background-color: #1a1a1a !important; -} +} diff --git a/core/core-frontend/src/views/chart/components/editor/common/ChartTemplateInfo.vue b/core/core-frontend/src/views/chart/components/editor/common/ChartTemplateInfo.vue index 7dd2256..4222402 100644 --- a/core/core-frontend/src/views/chart/components/editor/common/ChartTemplateInfo.vue +++ b/core/core-frontend/src/views/chart/components/editor/common/ChartTemplateInfo.vue @@ -1,45 +1,98 @@ - - - - - {{ - t('visualization.template_view_tips') - }} - - + + + + + {{ t('visualization.template_view_tips') }} + + + + + + + diff --git a/core/core-frontend/src/views/chart/components/editor/common/TemplateTips.vue b/core/core-frontend/src/views/chart/components/editor/common/TemplateTips.vue new file mode 100644 index 0000000..745cc55 --- /dev/null +++ b/core/core-frontend/src/views/chart/components/editor/common/TemplateTips.vue @@ -0,0 +1,76 @@ + + + + + + 你好,我是 DataEase 智能客服点击一下,开启高效解答模式~ + + + + + + + + + + + + + diff --git a/core/core-frontend/src/views/chart/components/editor/dataset-select/DatasetSelect.vue b/core/core-frontend/src/views/chart/components/editor/dataset-select/DatasetSelect.vue index e6d62e0..dee7b51 100644 --- a/core/core-frontend/src/views/chart/components/editor/dataset-select/DatasetSelect.vue +++ b/core/core-frontend/src/views/chart/components/editor/dataset-select/DatasetSelect.vue @@ -373,13 +373,13 @@ onMounted(() => { - + @@ -395,7 +395,7 @@ onMounted(() => { :deep(.ed-input__wrapper) { cursor: pointer; padding: 1px 11px; - + .ed-input__inner { cursor: pointer; font-size: 12px; @@ -405,7 +405,6 @@ onMounted(() => { margin-bottom: 0; } :deep(.ed-form-item.is-error .ed-input__wrapper) { - box-shadow: none !important; input { color: var(--ed-color-danger); } diff --git a/core/core-frontend/src/views/chart/components/editor/drag-item/DimensionItem.vue b/core/core-frontend/src/views/chart/components/editor/drag-item/DimensionItem.vue index 0399dc4..abee9c6 100644 --- a/core/core-frontend/src/views/chart/components/editor/drag-item/DimensionItem.vue +++ b/core/core-frontend/src/views/chart/components/editor/drag-item/DimensionItem.vue @@ -67,7 +67,7 @@ const emit = defineEmits([ const { item } = toRefs(props) const toolTip = computed(() => { - return props.themes === 'dark' ? 'ndark' : 'dark' + return props.themes || 'dark' }) const showValueFormatter = computed(() => { return ( @@ -192,17 +192,25 @@ const showCustomSort = item => { } return !item.chartId && (item.deType === 0 || item.deType === 5) } -const showSort = () => { + +const NOT_SUPPORT_SORT = ['word-cloud', 'stock-line', 'treemap', 'circle-packing'] +const showSort = computed(() => { const { type: chartType } = props.chart const { type: propType } = props - const notShowSort = ['word-cloud', 'stock-line'].includes(chartType) + const notShowSort = NOT_SUPPORT_SORT.includes(chartType) if (notShowSort || propType === 'extColor') { return false } const isChartMix = chartType.includes('chart-mix') const isDimensionType = ['dimension', 'dimensionStack', 'dimensionExt'].includes(propType) return !isChartMix || isDimensionType -} +}) +const showSortPriority = computed(() => { + if (props.chart.type.includes('chart-mix')) { + return false + } + return showSort.value +}) const toggleHide = () => { item.value.index = props.index item.value.hide = !item.value.hide @@ -212,6 +220,7 @@ const toggleHide = () => { const showHideIcon = computed(() => { return ['table-info', 'table-normal'].includes(props.chart.type) && item.value.hide }) + onMounted(() => { getItemTagType() }) @@ -226,17 +235,17 @@ onMounted(() => { :style="{ backgroundColor: tagType + '0a', border: '1px solid ' + tagType }" > - + - + - + @@ -283,7 +292,7 @@ onMounted(() => { class="item-span-style" :class="{ 'hidden-status': showHideIcon, - 'sort-status': showSort() && item.sort !== 'none' + 'sort-status': showSort && item.sort !== 'none' }" > {{ item.chartShowName ? item.chartShowName : item.name }} @@ -312,7 +321,7 @@ onMounted(() => { class="drop-style" :class="themes === 'dark' ? 'dark-dimension-quota' : ''" > - + { - + { + + + {{ t('chart.y_M_d_H') }} + + + + + { > - + @@ -450,9 +449,18 @@ span { background-color: rgba(31, 35, 41, 0.1); } &.dark-dimension-quota { + background-color: #292929; + border: 1px solid #434343; + :deep(.ed-dropdown-menu__item--divided) { + border-color: #ebebeb26; + } .inner-dropdown-menu { color: rgba(235, 235, 235, 1); } + :deep(.ed-dropdown-menu__item:not(.is-disabled):hover) { + background-color: #ebebeb1a; + } + :deep(.ed-dropdown-menu__item) { color: rgba(235, 235, 235, 1); } @@ -531,6 +539,7 @@ span { } } .dark-dimension-quota { + background-color: #292929; span { color: #ebebeb; } diff --git a/core/core-frontend/src/views/chart/components/editor/drag-item/QuotaItem.vue b/core/core-frontend/src/views/chart/components/editor/drag-item/QuotaItem.vue index 2550dde..47575dc 100644 --- a/core/core-frontend/src/views/chart/components/editor/drag-item/QuotaItem.vue +++ b/core/core-frontend/src/views/chart/components/editor/drag-item/QuotaItem.vue @@ -80,7 +80,7 @@ const emit = defineEmits([ const { item, chart } = toRefs(props) const toolTip = computed(() => { - return props.themes === 'dark' ? 'ndark' : 'dark' + return props.themes || 'dark' }) watch( [() => props.quotaData, () => props.item, () => props.chart.type], @@ -210,11 +210,6 @@ const beforeSort = type => { } } -const switchChartType = param => { - item.value.chartType = param.type - emit('onQuotaItemChange', item.value) -} - const summary = param => { item.value.summary = param.type emit('onQuotaItemChange', item.value) @@ -226,12 +221,6 @@ const beforeSummary = type => { } } -const beforeSwitchType = type => { - return { - type: type - } -} - const showRename = () => { item.value.index = props.index item.value.renameType = props.type @@ -315,14 +304,23 @@ const showHideIcon = computed(() => { return ['tale-info', 'table-normal'].includes(props.chart.type) && item.value.hide }) +const NOT_SUPPORT_SORT = [ + 'circle-packing', + 'indicator', + 'liquid', + 'gauge', + 'word-cloud', + 'stock-line', + 'treemap' +] + const showSort = computed(() => { return ( props.type !== 'extLabel' && props.type !== 'extTooltip' && props.type !== 'extBubble' && - !['chart-mix', 'indicator', 'liquid', 'gauge', 'word-cloud', 'stock-line'].includes( - chart.value.type - ) + !NOT_SUPPORT_SORT.includes(chart.value.type) && + !chart.value.type.includes('chart-mix') ) }) @@ -613,7 +611,9 @@ onMounted(() => { { class="menu-item-padding" :disabled="state.disableEditCompare" :command="beforeQuickCalc('setting')" + v-if="!(chart.type.includes('chart-mix') && type === 'quotaExt')" > { - + [], + required: false } }) @@ -54,6 +59,17 @@ const init = () => { reqMethod(param) .then(response => { const strArr = response.data + if (props.originSortList?.length) { + const tmp = [] + props.originSortList.forEach(ele => { + const index = strArr.findIndex(item => item === ele) + if (index !== -1) { + tmp.push(strArr[index]) + strArr.splice(index, 1) + } + }) + strArr.unshift(...tmp) + } state.sortList = strArr.map(ele => { return transStr2Obj(ele) }) diff --git a/core/core-frontend/src/views/chart/components/editor/drag-item/components/ValueFormatterEdit.vue b/core/core-frontend/src/views/chart/components/editor/drag-item/components/ValueFormatterEdit.vue index b4d1166..ba8dea5 100644 --- a/core/core-frontend/src/views/chart/components/editor/drag-item/components/ValueFormatterEdit.vue +++ b/core/core-frontend/src/views/chart/components/editor/drag-item/components/ValueFormatterEdit.vue @@ -1,7 +1,14 @@ - + { {{ t('chart.table_layout_tree') }} - + + + + + + + + {{ t('chart.quota_position_col') }} + {{ t('chart.quota_position_row') }} + + + + + {{ t('chart.not_alpha') }} @@ -353,9 +500,16 @@ onMounted(() => { :effect="themes" v-model="state.basicStyleForm.radiusColumnBar" @change="changeBasicStyle('radiusColumnBar')" + class="radius-class" > {{ t('chart.rightAngle') }} {{ t('chart.roundAngle') }} + {{ t('chart.topRoundAngle') }} @@ -434,7 +588,7 @@ onMounted(() => { - + {{ t('chart.chart_map') + ' ' + t('chart.map_pitch') }} @@ -726,7 +880,7 @@ onMounted(() => { {{ t('chart.page_pager_simple') }} {{ t('chart.page_pager_general') }} @@ -824,34 +978,6 @@ onMounted(() => { % - - - {{ t('chart.table_show_summary') }} - - - - - { :max="100" class="basic-input-number" :controls="false" + @input="validateInput($event, 'innerRadius')" @change="changeBasicStyle('innerRadius')" + @keydown="preventInvalidKeydown" > % @@ -1382,7 +1510,9 @@ onMounted(() => { :max="100" class="basic-input-number" :controls="false" + @input="validateInput($event, 'radius')" @change="changeBasicStyle('radius')" + @keydown="preventInvalidKeydown" > % @@ -1454,7 +1584,7 @@ onMounted(() => { - + diff --git a/core/core-frontend/src/views/chart/components/editor/editor-style/components/CustomColorStyleSelect.vue b/core/core-frontend/src/views/chart/components/editor/editor-style/components/CustomColorStyleSelect.vue index 4bc0c28..fa62813 100644 --- a/core/core-frontend/src/views/chart/components/editor/editor-style/components/CustomColorStyleSelect.vue +++ b/core/core-frontend/src/views/chart/components/editor/editor-style/components/CustomColorStyleSelect.vue @@ -234,11 +234,18 @@ const changeColorOption = (option?) => { } } const resetCustomColor = () => { - if (props.chart.type.includes('map')) { + const { type } = props.chart + const { basicStyleForm } = state.value + + if (type.includes('map')) { changeColorOption() } else { - state.value.basicStyleForm[seriesColorName.value] = [] + basicStyleForm[seriesColorName.value] = [] changeBasicStyle(seriesColorName.value) + const colorScheme = basicStyleForm[colorSchemeName.value] + basicStyleForm[colorsName.value] = + colorCases.find(ele => ele.value === colorScheme)?.colors ?? colorCases[0].colors + changeBasicStyle(colorsName.value) setupSeriesColor() } } @@ -300,7 +307,8 @@ const colorItemBorderColor = (index, state) => { - @@ -510,7 +518,7 @@ const colorItemBorderColor = (index, state) => { /> - + diff --git a/core/core-frontend/src/views/chart/components/editor/editor-style/components/DualYAxisSelector.vue b/core/core-frontend/src/views/chart/components/editor/editor-style/components/DualYAxisSelector.vue index 8b55883..f076986 100644 --- a/core/core-frontend/src/views/chart/components/editor/editor-style/components/DualYAxisSelector.vue +++ b/core/core-frontend/src/views/chart/components/editor/editor-style/components/DualYAxisSelector.vue @@ -1,13 +1,10 @@ - + props.chart.type === 'gauge') v-model="state.miscForm.gaugeMin" size="small" controls-position="right" - @change="changeMisc('gaugeMin')" + @blur="changeFixedValidate('gaugeMin')" /> - + props.chart.type === 'gauge') - + props.chart.type === 'gauge') v-model="state.miscForm.gaugeMinField.summary" @change="changeQuotaField('min')" > - - - - - - + + + + + + + + props.chart.type === 'gauge') v-model="state.miscForm.gaugeMax" size="small" controls-position="right" - @change="changeMaxValidate('gaugeMax')" + value-on-clear="gaugeLiquidYaxisDefaultValue.gaugeMax" + @blur="changeFixedValidate('gaugeMax')" /> - + props.chart.type === 'gauge') - + props.chart.type === 'gauge') :placeholder="t('chart.summary')" @change="changeQuotaField('max')" > - - - - - - + + + + + + + + props.chart.type === 'gauge') v-model="state.miscForm.liquidMax" size="small" controls-position="right" - @blur="changeMaxValidate('liquidMax')" + @blur="changeFixedValidate('liquidMax')" /> @@ -731,7 +634,7 @@ const isGauge = computed(() => props.chart.type === 'gauge') :gutter="8" v-if="showProperty('liquidMaxField') && state.miscForm.liquidMaxType === 'dynamic'" > - + props.chart.type === 'gauge') - + props.chart.type === 'gauge') :placeholder="t('chart.summary')" @change="changeQuotaField('max')" > - - - - - - + + + + + + + + { - return props.themes === 'dark' ? 'ndark' : 'dark' + return props.themes === 'dark' ? 'light' : 'dark' }) const predefineColors = COLOR_PANEL @@ -83,7 +83,7 @@ onMounted(() => { - + { - return props.themes === 'dark' ? 'ndark' : 'dark' + return props.themes === 'dark' ? 'light' : 'dark' }) const emit = defineEmits(['onChangeQuadrantForm']) diff --git a/core/core-frontend/src/views/chart/components/editor/editor-style/components/SummarySelector.vue b/core/core-frontend/src/views/chart/components/editor/editor-style/components/SummarySelector.vue new file mode 100644 index 0000000..fdf386c --- /dev/null +++ b/core/core-frontend/src/views/chart/components/editor/editor-style/components/SummarySelector.vue @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + {{ t('chart.table_show_summary') }} + + + + + + + + + + + + + + + + diff --git a/core/core-frontend/src/views/chart/components/editor/editor-style/components/SymbolicStyleSelector.vue b/core/core-frontend/src/views/chart/components/editor/editor-style/components/SymbolicStyleSelector.vue index f1d15f4..2624455 100644 --- a/core/core-frontend/src/views/chart/components/editor/editor-style/components/SymbolicStyleSelector.vue +++ b/core/core-frontend/src/views/chart/components/editor/editor-style/components/SymbolicStyleSelector.vue @@ -154,7 +154,7 @@ onMounted(() => { - + @@ -303,7 +303,7 @@ onMounted(() => { - + diff --git a/core/core-frontend/src/views/chart/components/editor/editor-style/components/bullet/BulletRangeSelector.vue b/core/core-frontend/src/views/chart/components/editor/editor-style/components/bullet/BulletRangeSelector.vue new file mode 100644 index 0000000..ed1bdf7 --- /dev/null +++ b/core/core-frontend/src/views/chart/components/editor/editor-style/components/bullet/BulletRangeSelector.vue @@ -0,0 +1,259 @@ + + + + + + + + + + + {{ t('chart.dynamic') }} + {{ t('chart.fix') }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/core-frontend/src/views/chart/components/editor/editor-style/components/bullet/BulletTargetSelector.vue b/core/core-frontend/src/views/chart/components/editor/editor-style/components/bullet/BulletTargetSelector.vue new file mode 100644 index 0000000..53ddab6 --- /dev/null +++ b/core/core-frontend/src/views/chart/components/editor/editor-style/components/bullet/BulletTargetSelector.vue @@ -0,0 +1,183 @@ + + + + + + + + {{ t('chart.dynamic') }} + {{ t('chart.fix') }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/core-frontend/src/views/chart/components/editor/editor-style/components/table/CustomAggrEdit.vue b/core/core-frontend/src/views/chart/components/editor/editor-style/components/table/CustomAggrEdit.vue index eb200df..a5ae29c 100644 --- a/core/core-frontend/src/views/chart/components/editor/editor-style/components/table/CustomAggrEdit.vue +++ b/core/core-frontend/src/views/chart/components/editor/editor-style/components/table/CustomAggrEdit.vue @@ -4,13 +4,12 @@ import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlin import icon_adjustment_outlined from '@/assets/svg/icon_adjustment_outlined.svg' import icon_edit_outlined from '@/assets/svg/icon_edit_outlined.svg' import icon_deleteTrash_outlined from '@/assets/svg/icon_delete-trash_outlined.svg' -import { ref, reactive, onMounted, onBeforeUnmount, watch, unref, computed, nextTick } from 'vue' +import { ref, reactive, onMounted, onBeforeUnmount, watch, nextTick } from 'vue' import { useI18n } from '@/hooks/web/useI18n' import CodeMirror from '@/views/visualized/data/dataset/form/CodeMirror.vue' import { getFunction } from '@/api/dataset' import { fieldType } from '@/utils/attr' import { cloneDeep } from 'lodash-es' -import { guid } from '@/views/visualized/data/dataset/form/util' import { iconFieldMap } from '@/components/icon-group/field-list' export interface CalcFieldType { @@ -82,7 +81,7 @@ const setNameIdTrans = (from, to, originName, name2Auto?: string[]) => { pre[next[from]] = next[to] return pre }, {}) - const on = originName.match(/\[(.+?)\]/g) + const on = originName.match(/\[(.+?)\]/g) || [] if (on) { on.forEach(itm => { const ele = itm.slice(1, -1) @@ -398,10 +397,8 @@ initFunction() .mr0 { margin-right: 0; - :deep(.ed-select__prefix--light) { - padding: 0; - border: none; - margin: 0; + :deep(.ed-select__prefix::after) { + display: none; } } diff --git a/core/core-frontend/src/views/chart/components/editor/editor-style/components/table/TableCellSelector.vue b/core/core-frontend/src/views/chart/components/editor/editor-style/components/table/TableCellSelector.vue index ccdfd05..09f6f1b 100644 --- a/core/core-frontend/src/views/chart/components/editor/editor-style/components/table/TableCellSelector.vue +++ b/core/core-frontend/src/views/chart/components/editor/editor-style/components/table/TableCellSelector.vue @@ -75,12 +75,14 @@ const init = () => { tableCell.mergeCells = tableCell.mergeCells === undefined ? false : tableCell.mergeCells state.tableCellForm = defaultsDeep(cloneDeep(tableCell), cloneDeep(DEFAULT_TABLE_CELL)) const alpha = props.chart.customAttr.basicStyle.alpha + if (!isAlphaColor(state.tableCellForm.tableItemBgColor)) { state.tableCellForm.tableItemBgColor = convertToAlphaColor( state.tableCellForm.tableItemBgColor, alpha ) } + if (!isAlphaColor(state.tableCellForm.tableItemSubBgColor)) { state.tableCellForm.tableItemSubBgColor = convertToAlphaColor( state.tableCellForm.tableItemSubBgColor, @@ -97,12 +99,12 @@ onMounted(() => { - + { :class="'form-item-' + themes" class="form-item" label="" - v-if="showProperty('tableItemSubBgColor')" + v-if="showProperty('tableItemSubBgColor') && state.tableCellForm.tableItemSubBgColor" > { } if (headerGroupConfig?.columns?.length) { const allAxis = showColumns.map(item => item.key) - const leafNodes = getLeafNodes(headerGroupConfig.columns as Array) + const leafNodes = getLeafNodes(headerGroupConfig.columns) const leafKeys = leafNodes.map(item => item.key) if (!isEqual(allAxis, leafKeys)) { const { columns, meta } = headerGroupConfig @@ -166,6 +169,14 @@ const renderTable = (chart: ChartObj) => { position: 'absolute', borderRadius: '4px' } + }, + interaction: { + rangeSelection: false, + resize: { + colCellHorizontal: false, + colCellVertical: false, + rowCellVertical: false + } } } s2 = new TableSheet(containerDom, s2DataConfig, s2Options) @@ -321,7 +332,7 @@ const renderTable = (chart: ChartObj) => { //如果有多个cell都在同一个层级,并且parent相同,那就是可以进行合并分组操作 if (activeColumns?.length > 1) { const sameParent = activeCells.every( - cell => cell.getMeta().parent === curCell.getMeta().parent + cell => cell.getMeta().parent.id === curCell.getMeta().parent.id ) if (!sameParent) { return @@ -443,6 +454,57 @@ const renderTable = (chart: ChartObj) => { return } }) + s2.on(S2Event.COL_CELL_CLICK, e => { + const lastCell = s2.store.get('lastClickedCell') as ColCell + const originEvent = e.originalEvent as MouseEvent + if (!lastCell || !(originEvent?.ctrlKey || originEvent?.metaKey || originEvent?.shiftKey)) { + const cell = s2.getCell(e.target) + s2.store.set('lastClickedCell', cell) + return + } + if (originEvent?.shiftKey) { + if (!lastCell) { + const cell = s2.getCell(e.target) + s2.store.set('lastClickedCell', cell) + return + } + const curCell = s2.getCell(e.target) + const lastMeta = lastCell.getMeta() + const curMeta = curCell.getMeta() + if ( + lastMeta.key === curMeta.key || + lastMeta.level !== curMeta.level || + lastMeta.parent !== curMeta.parent + ) { + return + } + const parent = curMeta.parent as Node + const lastIndex = parent.children.findIndex(item => item.key === lastMeta.key) + const curIndex = parent.children.findIndex(item => item.key === curMeta.key) + const startIndex = Math.min(lastIndex, curIndex) + const endIndex = Math.max(lastIndex, curIndex) + const activeCells = parent.children.slice(startIndex, endIndex + 1) + s2.interaction.clearState() + activeCells.forEach(cell => { + s2.interaction.selectHeaderCell({ cell: cell.belongsCell, isMultiSelection: true }) + }) + } + }) + s2.once(S2Event.LAYOUT_AFTER_HEADER_LAYOUT, (e: LayoutResult) => { + const initialized = s2.store.get('initialized') + if (!initialized) { + s2.store.set('initialized', true) + s2.changeSheetSize(e.colsHierarchy.width) + const length = s2.dataCfg.data?.length || 0 + const headerHeight = e.colsHierarchy.height + const rowHeight = s2.options.style.cellCfg.height + const totalHeight = headerHeight + rowHeight * length + if (containerDom.offsetHeight > totalHeight) { + containerDom.style.height = totalHeight + 'px' + } + s2.render(false) + } + }) s2.render() } @@ -486,9 +548,14 @@ const getTreesMaxDepth = (nodes: Array): number => { return Math.max(...rootDepths) } -const resize = debounce((width, height) => { +const resize = debounce(height => { if (s2) { - s2.changeSheetSize(width, height) + const tableHeight = s2.container.cfg.height + if (height > tableHeight) { + const dom = document.getElementById(containerId.value) + dom.style.height = tableHeight + 'px' + } + s2.changeSheetSize(undefined, height) s2.render(false) } }, 500) @@ -504,14 +571,13 @@ onMounted(() => { preSize[0] = size.inlineSize preSize[1] = size.blockSize } - const widthOffset = Math.abs(size.inlineSize - preSize[0]) const heightOffset = Math.abs(size.blockSize - preSize[1]) - if (widthOffset < TOLERANCE && heightOffset < TOLERANCE) { + if (heightOffset < TOLERANCE) { return } preSize[0] = size.inlineSize preSize[1] = size.blockSize - resize(size.inlineSize, Math.round(size.blockSize)) + resize(Math.round(size.blockSize)) }) resizeObserver.observe(document.getElementById(containerId.value)) }) @@ -536,6 +602,8 @@ class GroupMenu extends BaseTooltip { position: relative; width: 100%; height: 40vh; + overflow-x: auto; + overflow-y: hidden; } .group-menu { diff --git a/core/core-frontend/src/views/chart/components/editor/editor-style/components/table/TableHeaderSelector.vue b/core/core-frontend/src/views/chart/components/editor/editor-style/components/table/TableHeaderSelector.vue index 0cc7704..f2fe2c4 100644 --- a/core/core-frontend/src/views/chart/components/editor/editor-style/components/table/TableHeaderSelector.vue +++ b/core/core-frontend/src/views/chart/components/editor/editor-style/components/table/TableHeaderSelector.vue @@ -9,11 +9,12 @@ import { computed, onMounted, PropType, reactive, watch } from 'vue' import { useI18n } from '@/hooks/web/useI18n' import { COLOR_PANEL, DEFAULT_TABLE_HEADER } from '@/views/chart/components/editor/util/chart' import { ElDivider, ElSpace } from 'element-plus-secondary' -import { cloneDeep, defaultsDeep } from 'lodash-es' +import { cloneDeep, defaultsDeep, isEqual } from 'lodash-es' import { convertToAlphaColor, isAlphaColor } from '@/views/chart/components/js/util' import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain' import { storeToRefs } from 'pinia' import TableHeaderGroupConfig from './TableHeaderGroupConfig.vue' +import { getLeafNodes } from '@/views/chart/components/js/panel/common/common_table' const dvMainStore = dvMainStoreWithOut() const { batchOptStatus, mobileInPc } = storeToRefs(dvMainStore) @@ -86,6 +87,28 @@ const enableGroupConfig = computed(() => { ) }) +const groupConfigValid = computed(() => { + const columns = props.chart?.customAttr?.tableHeader?.headerGroupConfig?.columns + if (!columns?.length) { + return false + } + const noGroup = columns.every(item => !item.children?.length) + if (noGroup) { + return false + } + const xAxis = props.chart.xAxis + const showColumns = [] + xAxis?.forEach(axis => { + axis.hide !== true && showColumns.push({ key: axis.dataeaseName }) + }) + if (!showColumns.length) { + return false + } + const allAxis = showColumns.map(item => item.key) + const leafNodes = getLeafNodes(columns as Array) + const leafKeys = leafNodes.map(item => item.key) + return isEqual(allAxis, leafKeys) +}) const init = () => { const tableHeader = props.chart?.customAttr?.tableHeader if (tableHeader) { @@ -128,6 +151,7 @@ onMounted(() => { :disabled="!state.tableHeaderForm.showTableHeader" ref="tableHeaderForm" label-position="top" + size="small" > { {{ t('chart.table_header_show_vertical_border') }} + + + {{ t('chart.table_row_header_freeze') }} + + { {{ t('chart.table_header_group_config') }} - + {{ t('visualization.already_setting') }} -import { onMounted, PropType, reactive, watch, ref, inject, nextTick } from 'vue' +import { onMounted, PropType, reactive, watch, ref, inject, nextTick, computed } from 'vue' import { useI18n } from '@/hooks/web/useI18n' import { DEFAULT_BASIC_STYLE, @@ -32,6 +32,7 @@ watch( ) const aggregations = [ + { name: t('chart.none'), value: 'NONE' }, { name: t('chart.sum'), value: 'SUM' }, { name: t('chart.avg'), value: 'AVG' }, { name: t('chart.max'), value: 'MAX' }, @@ -53,6 +54,26 @@ const state = reactive({ basicStyleForm: JSON.parse(JSON.stringify(DEFAULT_BASIC_STYLE)) as ChartBasicStyle }) +const showColFieldTotalLabel = computed(() => { + const chart = props.chart + return ( + chart.customAttr.basicStyle.quotaPosition !== 'row' && + chart.xAxisExt.length && + chart.yAxis.length > 1 + ) +}) + +const showRowFieldTotalLabel = computed(() => { + const chart = props.chart + return ( + chart.customAttr.basicStyle.quotaPosition === 'row' && + chart.customAttr.basicStyle.tableLayoutMode !== 'tree' && + chart.xAxis.length && + chart.xAxisExt.length && + chart.yAxis.length > 1 + ) +}) + function onSelectedSubTotalDimensionNameChange(name) { state.selectedSubTotalDimension = find(state.subTotalDimensionList, d => d.name === name) } @@ -159,6 +180,7 @@ const init = () => { total.dataeaseName = totalCfg[0].dataeaseName total.aggregation = totalCfg[0].aggregation total.originName = totalCfg[0].originName + total.label = totalCfg[0].label } }) @@ -174,6 +196,7 @@ const changeTotal = (totalItem, totals) => { if (item.dataeaseName === totalItem.dataeaseName) { totalItem.aggregation = item.aggregation totalItem.originName = item.originName + totalItem.label = item.label return } } @@ -183,6 +206,7 @@ const changeTotalAggr = (totalItem, totals, colOrNum) => { const item = totals[i] if (item.dataeaseName === totalItem.dataeaseName) { item.aggregation = totalItem.aggregation + item.label = totalItem.label break } } @@ -196,7 +220,8 @@ const setupTotalCfg = (totalCfg, axis) => { axis.forEach(i => { totalCfg.push({ dataeaseName: i.dataeaseName, - aggregation: 'SUM' + aggregation: 'SUM', + label: i.chartShowName ?? i.name }) }) return @@ -214,7 +239,10 @@ const setupTotalCfg = (totalCfg, axis) => { totalCfg.push({ dataeaseName: i.dataeaseName, aggregation: cfgMap[i.dataeaseName] ? cfgMap[i.dataeaseName].aggregation : 'SUM', - originName: cfgMap[i.dataeaseName] ? cfgMap[i.dataeaseName].originName : '' + originName: cfgMap[i.dataeaseName] ? cfgMap[i.dataeaseName].originName : '', + label: cfgMap[i.dataeaseName]?.label + ? cfgMap[i.dataeaseName].label + : i.chartShowName ?? i.name }) }) } @@ -264,7 +292,7 @@ onMounted(() => { - + { { + + + { {{ t('chart.total_pos_top') }} @@ -587,13 +637,13 @@ onMounted(() => { { + + + - diff --git a/core/core-frontend/src/views/chart/components/editor/filter/auth-tree-chart/AuthTree.vue b/core/core-frontend/src/views/chart/components/editor/filter/auth-tree-chart/AuthTree.vue index 8597c0f..13408f7 100644 --- a/core/core-frontend/src/views/chart/components/editor/filter/auth-tree-chart/AuthTree.vue +++ b/core/core-frontend/src/views/chart/components/editor/filter/auth-tree-chart/AuthTree.vue @@ -191,7 +191,7 @@ const del = (index, child) => { .operate-title{ border-right: none; } - + .operate-icon { display: inline-block; } @@ -254,7 +254,7 @@ const del = (index, child) => { border-radius: 2px; } } - + } .ed-dropdown__popper .ed-dropdown-menu{ @@ -266,10 +266,10 @@ const del = (index, child) => { } :deep(.ed-popper.is-light){ border:1px solid #434343; - background: rgba(41, 41, 41, 1); + background: rgba(41, 41, 41, 1); } :deep(.ed-dropdown__popper.ed-popper){ - border-color: rgba(70, 70, 70, 1); + border-color: rgba(70, 70, 70, 1); } :deep(.ed-dropdown-menu__item:not(.is-disabled):hover){ background-color: rgba(41,41,41, 1); @@ -278,11 +278,11 @@ const del = (index, child) => { \ No newline at end of file + diff --git a/core/core-frontend/src/views/chart/components/editor/filter/auth-tree-chart/FilterFiled.vue b/core/core-frontend/src/views/chart/components/editor/filter/auth-tree-chart/FilterFiled.vue index 093aa5b..9cb9826 100644 --- a/core/core-frontend/src/views/chart/components/editor/filter/auth-tree-chart/FilterFiled.vue +++ b/core/core-frontend/src/views/chart/components/editor/filter/auth-tree-chart/FilterFiled.vue @@ -493,6 +493,7 @@ const emits = defineEmits(['update:item', 'del']) {{ t('auth.screen_method') }} { } } } - diff --git a/core/core-frontend/src/views/chart/components/editor/filter/auth-tree/FilterFiled.vue b/core/core-frontend/src/views/chart/components/editor/filter/auth-tree/FilterFiled.vue index 2969c1f..98ea1bb 100644 --- a/core/core-frontend/src/views/chart/components/editor/filter/auth-tree/FilterFiled.vue +++ b/core/core-frontend/src/views/chart/components/editor/filter/auth-tree/FilterFiled.vue @@ -375,6 +375,7 @@ const emits = defineEmits(['update:item', 'del']) size="small" @change="filterTypeChange" v-model="item.filterType" + class="w181" :placeholder="t('auth.select')" > onTableColumnWidthChange(args) }) +useEmitt({ + name: 'set-page-size', + callback: args => onTablePageSizeChange(args) +}) const props = defineProps({ view: { type: Object as PropType, @@ -171,11 +174,15 @@ const editComponentName = () => { }) } const toolTip = computed(() => { - return props.themes === 'dark' ? 'ndark' : 'dark' + return props.themes || 'dark' }) const templateStatusShow = computed(() => { - return view.value['dataFrom'] === 'template' && !mobileInPc.value + return ( + view.value['dataFrom'] === 'template' && + view.value.type !== 'picture-group' && + !mobileInPc.value + ) }) const { view } = toRefs(props) @@ -208,7 +215,7 @@ const isDataEaseBi = computed(() => appStore.getIsDataEaseBi || appStore.getIsIf const itemFormRules = reactive({ chartShowName: [ { required: true, message: t('commons.input_content'), trigger: 'change' }, - { max: 50, message: t('commons.char_can_not_more_50'), trigger: 'change' } + { max: 200, message: t('commons.char_count_limit', { count: 200 }), trigger: 'change' } ] }) @@ -916,7 +923,16 @@ const calcData = (view, resetDrill = false, updateQuery = '') => { if (resetDrill) { useEmitt().emitter.emit('resetDrill-' + view.id, 0) } else { - useEmitt().emitter.emit('calcData-' + view.id, view) + if (mobileInPc.value) { + //移动端设计 + useEmitt().emitter.emit('onMobileStatusChange', { + type: 'componentStyleChange', + value: { type: 'calcData', component: JSON.parse(JSON.stringify(view)) } + }) + } else { + useEmitt().emitter.emit('calcData-' + view.id, view) + snapshotStore.recordSnapshotCache('renderChart', view.id) + } } snapshotStore.recordSnapshotCache('calcData', view.id) if (updateQuery === 'updateQuery') { @@ -1026,12 +1042,13 @@ const onTypeChange = (render, type) => { } const onBasicStyleChange = (chartForm: ChartEditorForm, prop: string) => { - const { data, requestData } = chartForm + const { data, requestData, render } = chartForm const val = get(data, prop) set(view.value.customAttr.basicStyle, prop, val) if (requestData) { calcData(view.value) - } else { + } + if (render !== false) { renderChart(view.value) } } @@ -1064,7 +1081,7 @@ const onMiscChange = val => { } const onLabelChange = (chartForm: ChartEditorForm, prop: string) => { - const { data, requestData, render } = chartForm + const { data, render } = chartForm let labelObj = data if (!data) { labelObj = chartForm as unknown as ChartLabelAttr @@ -1250,6 +1267,14 @@ const onTableColumnWidthChange = val => { snapshotStore.recordSnapshotCache('renderChart', view.value.id) } +const onTablePageSizeChange = val => { + if (editMode.value !== 'edit') { + return + } + view.value.customAttr.basicStyle.tablePageSize = val + snapshotStore.recordSnapshotCache('renderChart', view.value.id) +} + const onExtTooltipChange = val => { view.value.extTooltip = val } @@ -1504,13 +1529,18 @@ const addDsWindow = () => { const editDs = () => { const path = embeddedStore.getToken && appStore.getIsIframe ? 'dataset-embedded-form' : '/dataset-form' + const openType = wsCache.get('open-backend') === '1' ? '_self' : '_blank' + // 此处校验提前 防止router返回时找到错误的路径 + if (openType === '_self' && !dvInfo.value.id) { + ElMessage.warning(t('visualization.save_page_tips')) + return + } let routeData = router.resolve({ path: path, query: { id: view.value.tableId } }) - const openType = wsCache.get('open-backend') === '1' ? '_self' : '_blank' // 检查是否保存 if (openType === '_self') { if (!dvInfo.value.id) { @@ -1518,6 +1548,7 @@ const editDs = () => { return } canvasSave(() => { + wsCache.delete('DE-DV-CATCH-' + dvInfo.value.id) const newWindow = window.open(routeData.href, openType) initOpenHandler(newWindow) }) @@ -1575,6 +1606,7 @@ const closeSortPriority = () => { } const saveSortPriority = () => { view.value.sortPriority = state.sortPriority as ChartViewField[] + recordSnapshotInfo('render') closeSortPriority() } const onPriorityChange = val => { @@ -1710,14 +1742,14 @@ const { y, isDragging } = useDraggable(el, { draggingElement: elDrag }) const previewHeight = ref(0) -const calcEle = () => { +const calcEle = debounce(() => { nextTick(() => { previewHeight.value = (elDrag.value as HTMLDivElement).offsetHeight y.value = previewHeight.value / 2 + 200 }) -} +}, 500) -const setCacheId = () => { +const setCacheId = debounce(() => { nextTick(() => { // 富文本不使用cacheId if ( @@ -1729,7 +1761,7 @@ const setCacheId = () => { return view.value.tableId = cacheId as unknown as number }) -} +}, 500) watch( () => curComponent.value, val => { @@ -1831,7 +1863,7 @@ const setActiveShift = (ele, type = 'dimension') => { const isDrag = ref(false) -const dragStartD = (e: DragEvent) => { +const dragStartD = () => { isDrag.value = true setTimeout(() => { isDraggingItem.value = true @@ -1848,7 +1880,7 @@ const singleDragStartD = (e: DragEvent, ele, type) => { startToMove(e, unref(activeDimension.value)) } -const dragStart = (e: DragEvent) => { +const dragStart = () => { isDrag.value = true setTimeout(() => { isDraggingItem.value = true @@ -2040,10 +2072,10 @@ const deleteChartFieldItem = id => { > - + {{ t('visualization.view_id') }} - + {{ view.id }} @@ -3828,7 +3860,7 @@ const deleteChartFieldItem = id => { - + { } .editor-light { - border-left: solid 1px @side-outline-border-color-light; - color: @canvas-main-font-color-light; - background-color: @side-area-background-light; + border-left: solid 1px @side-outline-border-color-light !important; + color: @canvas-main-font-color-light!important; + background-color: @side-area-background-light!important; :deep(.ed-tabs__header) { - border-top: solid 1px @side-outline-border-color-light; + border-top: solid 1px @side-outline-border-color-light !important; } :deep(.drag_main_area) { border-top: solid 1px @side-outline-border-color-light !important; @@ -4130,7 +4162,7 @@ const deleteChartFieldItem = id => { border-top: 1px solid @side-outline-border-color-light !important; } :deep(.dataset-main) { - border-left: 1px solid @side-outline-border-color-light; + border-left: 1px solid @side-outline-border-color-light !important; } :deep(input) { font-size: 12px; @@ -4140,14 +4172,14 @@ const deleteChartFieldItem = id => { background-color: @side-outline-border-color-light !important; } :deep(.item-span-style) { - color: @canvas-main-font-color-light; + color: @canvas-main-font-color-light!important; } :deep(.editor-title) { - color: #1f2329; + color: #1f2329 !important; } :deep(.collapse-title) { - color: #1f2329; + color: #1f2329 !important; } :deep(.collapse-icon) { color: #646a73 !important; @@ -4214,7 +4246,7 @@ const deleteChartFieldItem = id => { // editor form 全局样式 .editor-dark { - border-left: solid 1px @main-collapse-border-dark; + border-left: solid 1px @main-collapse-border-dark !important; .dataset-selector { :deep(.ed-input__inner), :deep(.ed-input__wrapper), @@ -4227,12 +4259,10 @@ const deleteChartFieldItem = id => { border: none; } :deep(.ed-input__wrapper) { - box-shadow: none; - border: 1px solid #636363; + box-shadow: 0 0 0 1px hsla(0, 0%, 100%, 0.15) inset !important; } :deep(.ed-input__wrapper:hover) { - box-shadow: none; - border: 1px solid #3370ff; + box-shadow: 0 0 0 1px var(--ed-color-primary, #3370ff) inset !important; } } .query-style-tab { @@ -4316,63 +4346,8 @@ span { overflow-x: hidden; height: 100%; - :deep(.ed-collapse-item__header) { - height: 36px !important; - line-height: 36px !important; - font-size: 12px !important; - padding: 0 !important; - font-weight: 500 !important; - border-top: unset; - - &.is-active { - border-bottom-color: var(--ed-collapse-border-color); - color: #ffffff; - } - - .ed-collapse-item__arrow { - margin: 0 6px 0 8px; - - &.is-active { - color: #A6A6A6; - } - } - } - :deep(.ed-collapse-item__content) { padding: 16px 10px 0; - border: none; - :deep(.ed-checkbox) { - height: 20px; - } - .ed-checkbox { - height: 20px; - } - } - - :deep(.style-dark) { - .ed-collapse-item__header { - &.is-active { - color: #fff; - } - - .ed-collapse-item__arrow { - &.is-active { - color: #a6a6a6; - } - } - } - } - :deep(.ed-collapse-item.ed-collapse--dark .ed-collapse-item__header) { - border-color: rgba(255, 255, 255, 0.15); - - &.is-active { - color: #fff; - } - .ed-collapse-item__arrow { - &.is-active { - color: #a6a6a6; - } - } } } @@ -4397,11 +4372,14 @@ span { font-size: 12px; padding: 0 8px !important; margin-right: 12px; + } + + :deep(.ed-tabs__item:not(.is-active)) { color: var(--custom-tab-color); } - :deep(.is-active) { + + :deep(.ed-tabs__item.is-active) { font-weight: 500; - color: var(--ed-color-primary, #3370ff); } :deep(.ed-tabs__nav-scroll) { @@ -4736,6 +4714,7 @@ span { align-items: center; justify-content: space-between; padding: 0 8px; + line-height: 22px; span { width: calc(100% - 24px); @@ -4771,7 +4750,7 @@ span { .result-style-dark { :deep(.ed-button) { color: #ffffff; - background-color: #0089ff !important; + background-color: var(--ed-color-primary, #3370ff); border: none; border-radius: 0; } @@ -4807,7 +4786,6 @@ span { height: 40px; width: 100%; border-radius: 0; - background-color: #0089ff; } .switch-chart-dark { @@ -4831,7 +4809,7 @@ span { display: flex; align-items: center; justify-content: space-between; - color: #fff; + color: #1f2329; font-weight: 500; &.dark { @@ -5085,6 +5063,7 @@ span { display: flex; flex-wrap: nowrap; align-items: center; + z-index: 1000; border-top: 1px solid rgba(255, 255, 255, 0.15); } .style-collapse { @@ -5121,7 +5100,7 @@ span { .field-setting { position: absolute; right: 8px; - color: #a6a6a6; + color: #646a73; &.remove-icon--dark { color: #a6a6a6; } @@ -5160,10 +5139,13 @@ span { .chart-type-select { width: 100%; margin-top: 8px; - :deep(.ed-input__prefix-inner > div) { + :deep(.ed-select__prefix) { padding: 0; margin: 0; - border: none; + &::after { + display: none; + } + height: 20px; .chart-type-select-icon { width: 23px; height: 16px; @@ -5211,6 +5193,9 @@ span { \ No newline at end of file + diff --git a/core/core-frontend/src/views/chart/components/editor/util/chart.ts b/core/core-frontend/src/views/chart/components/editor/util/chart.ts index a850982..7dfc59d 100644 --- a/core/core-frontend/src/views/chart/components/editor/util/chart.ts +++ b/core/core-frontend/src/views/chart/components/editor/util/chart.ts @@ -1,6 +1,6 @@ import { useI18n } from '@/hooks/web/useI18n' import { deepCopy } from '@/utils/utils' -import { formatterItem } from '@/views/chart/components/js/formatter' +import { formatterItem, isEnLocal } from '@/views/chart/components/js/formatter' const { t } = useI18n() export const DEFAULT_COLOR_CASE: DeepPartial = { @@ -318,6 +318,32 @@ export const DEFAULT_MISC: ChartMiscAttr = { min: 0, max: 0, fieldId: undefined + }, + bullet: { + bar: { + ranges: { + fill: ['rgba(0,128,255,0.3)'], + size: 20, + showType: 'dynamic', + fixedRangeNumber: 3, + symbol: 'circle', + symbolSize: 4 + }, + measures: { + fill: ['rgba(0,128,255,1)'], + size: 15, + symbol: 'circle', + symbolSize: 4 + }, + target: { + fill: 'rgb(0,0,0)', + size: 20, + showType: 'dynamic', + value: 0, + symbol: 'line', + symbolSize: 4 + } + } } } @@ -453,7 +479,8 @@ export const DEFAULT_TABLE_HEADER: ChartTableHeaderAttr = { headerGroupConfig: { columns: [], meta: [] - } + }, + rowHeaderFreeze: true } export const DEFAULT_TABLE_CELL: ChartTableCellAttr = { tableFontColor: '#000000', @@ -552,17 +579,6 @@ export const DEFAULT_TITLE_STYLE_DARK = { remarkBackgroundColor: '#5A5C62' } -export const DEFAULT_LEGEND_STYLE: ChartLegendStyle = { - show: true, - hPosition: 'center', - vPosition: 'bottom', - orient: 'horizontal', - icon: 'circle', - color: '#333333', - fontSize: 12, - size: 4 -} - export const DEFAULT_LEGEND_STYLE_BASE: ChartLegendStyle = { show: true, hPosition: 'center', @@ -571,7 +587,24 @@ export const DEFAULT_LEGEND_STYLE_BASE: ChartLegendStyle = { icon: 'circle', color: '#333333', fontSize: 12, - size: 4 + size: 4, + showRange: true, + sort: 'none', + customSort: [] +} + +export const DEFAULT_LEGEND_STYLE: ChartLegendStyle = { + show: true, + hPosition: 'center', + vPosition: 'bottom', + orient: 'horizontal', + icon: 'circle', + color: '#333333', + fontSize: 12, + size: 4, + showRange: true, + sort: 'none', + customSort: [] } export const DEFAULT_LEGEND_STYLE_LIGHT: ChartLegendStyle = { @@ -634,6 +667,7 @@ export const DEFAULT_XAXIS_STYLE: ChartAxisStyle = { }, axisLabelFormatter: { type: 'auto', + unitLanguage: isEnLocal ? 'en' : 'ch', unit: 1, suffix: '', decimalCount: 2, @@ -680,6 +714,7 @@ export const DEFAULT_YAXIS_STYLE: ChartAxisStyle = { }, axisLabelFormatter: { type: 'auto', + unitLanguage: isEnLocal ? 'en' : 'ch', unit: 1, suffix: '', decimalCount: 2, @@ -724,6 +759,7 @@ export const DEFAULT_YAXIS_EXT_STYLE: ChartAxisStyle = { }, axisLabelFormatter: { type: 'auto', + unitLanguage: isEnLocal ? 'en' : 'ch', unit: 1, suffix: '', decimalCount: 2, @@ -1395,6 +1431,13 @@ export const CHART_TYPE_CONFIGS = [ value: 'stock-line', title: t('chart.chart_stock_line'), icon: 'stock-line' + }, + { + render: 'antv', + category: 'compare', + value: 'bullet-graph', + title: t('chart.bullet_chart'), + icon: 'bullet-graph' } ] }, @@ -1651,6 +1694,7 @@ export const DEFAULT_BASIC_STYLE: ChartBasicStyle = { zoomButtonColor: '#aaa', zoomBackground: '#fff', tableLayoutMode: 'grid', + defaultExpandLevel: 1, calcTopN: false, topN: 5, topNLabel: t('datasource.other'), @@ -1679,7 +1723,9 @@ export const DEFAULT_BASIC_STYLE: ChartBasicStyle = { radarAreaColor: true, circleBorderColor: '#fff', circleBorderWidth: 0, - circlePadding: 0 + circlePadding: 0, + quotaPosition: 'col', + quotaColLabel: t('dataset.value') } export const BASE_VIEW_CONFIG = { diff --git a/core/core-frontend/src/views/chart/components/editor/util/dataVisualization.ts b/core/core-frontend/src/views/chart/components/editor/util/dataVisualization.ts index fece2c7..53ac94e 100644 --- a/core/core-frontend/src/views/chart/components/editor/util/dataVisualization.ts +++ b/core/core-frontend/src/views/chart/components/editor/util/dataVisualization.ts @@ -61,6 +61,7 @@ export const MOBILE_SETTING_DARK = { export const DEFAULT_DASHBOARD_STYLE_BASE = { gap: 'yes', gapSize: 5, + gapMode: 'middle', showGrid: false, matrixBase: 4, // 当前matrix的基数 (是pcMatrixCount的几倍) resultMode: 'all', // 图表结果显示模式 all 图表 custom 仪表板自定义 diff --git a/core/core-frontend/src/views/chart/components/js/extremumUitl.ts b/core/core-frontend/src/views/chart/components/js/extremumUitl.ts index f3efd02..a9d4ab5 100644 --- a/core/core-frontend/src/views/chart/components/js/extremumUitl.ts +++ b/core/core-frontend/src/views/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/core/core-frontend/src/views/chart/components/js/formatter.ts b/core/core-frontend/src/views/chart/components/js/formatter.ts index a297fbb..1fef635 100644 --- a/core/core-frontend/src/views/chart/components/js/formatter.ts +++ b/core/core-frontend/src/views/chart/components/js/formatter.ts @@ -1,7 +1,13 @@ -import { Datum } from '@antv/g2plot' +import { find } from 'lodash-es' +import { useI18n } from '@/hooks/web/useI18n' +import { getLocale } from '@/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/core/core-frontend/src/views/chart/components/js/g2plot_tooltip_carousel.ts b/core/core-frontend/src/views/chart/components/js/g2plot_tooltip_carousel.ts new file mode 100644 index 0000000..75bcb42 --- /dev/null +++ b/core/core-frontend/src/views/chart/components/js/g2plot_tooltip_carousel.ts @@ -0,0 +1,656 @@ +import { DualAxes, Plot } from '@antv/g2plot' + +/** + * 使用 Map 来存储实例,键为 chart.container 对象 + */ +export const CAROUSEL_MANAGER_INSTANCES = new Map() +/** + * 支持的图表类型 + */ +const CHART_CATEGORY = { + COLUMN: ['bar', 'bar-stack', 'bar-group', 'bar-group-stack', 'percentage-bar-stack'], + LINE: ['line', 'area', 'area-stack'], + MIX: ['chart-mix', 'chart-mix-group', 'chart-mix-stack', 'chart-mix-dual-line'], + PIE: ['pie', 'pie-donut'] +} + +/** + * 判断是否为柱状图 + * @param chartType + */ +export function isColumn(chartType: string) { + return CHART_CATEGORY.COLUMN.includes(chartType) +} + +/** + * 判断是否为折线图 + * @param chartType + */ +export function isLine(chartType: string) { + return CHART_CATEGORY.LINE.includes(chartType) +} + +/** + * 判断是否为饼图 + * @param chartType + */ +export function isPie(chartType: string) { + return CHART_CATEGORY.PIE.includes(chartType) +} + +/** + * 判断是否为组合图 + * @param chartType + */ +export function isMix(chartType: string) { + return CHART_CATEGORY.MIX.includes(chartType) +} + +export function isSupport(chartType: string) { + return Object.values(CHART_CATEGORY).some(category => category.includes(chartType)) +} + +// 轮播配置默认值 +const DEFAULT_CAROUSEL_CONFIG: Required = { + xField: '', + duration: 2000, + interval: 2000, + loop: true +} + +type CarouselConfig = { + xField: string + duration?: number + interval?: number + loop?: boolean +} + +/** + * 图表轮播提示管理类 + * */ +class ChartCarouselTooltip { + private plot: Plot | DualAxes + private config: Required + private currentIndex = 0 + private values: string[] = [] + // 合并定时器管理 + private timers = { interval: null, carousel: null } + private states = { paused: false, destroyed: false } + // 图表可视性变化 + private observers: Map = new Map() + // 图表元素大小变化 + private resizeObservers: Map = new Map() + // 图表是否在可视范围内 + private chartIsVisible: boolean + + private constructor(plot: Plot | DualAxes, private chart: Chart, config: CarouselConfig) { + this.plot = plot + this.config = { ...DEFAULT_CAROUSEL_CONFIG, ...config } + this.init() + } + + /** + * 创建或更新实例 + * */ + static manage(plot: Plot | DualAxes, chart: Chart, config: CarouselConfig) { + if (!isSupport(chart.type)) return null + const container = chart.container + let instance = CAROUSEL_MANAGER_INSTANCES.get(container) + + CAROUSEL_MANAGER_INSTANCES.forEach(instance => { + if (container.includes('viewDialog')) { + instance.paused() + } + }) + + if (instance) { + instance.update(plot, chart, config) + return instance + } + if (isSupport(chart.type)) { + instance = new this(plot, chart, config) + CAROUSEL_MANAGER_INSTANCES.set(container, instance) + } + + return instance + } + + /** + * 销毁实例 + * @param container + */ + static destroyByContainer(container: string) { + const instance = CAROUSEL_MANAGER_INSTANCES.get(container) + if (instance) { + instance.destroy() + } + } + + /** + * 通过容器DOM获取对应实例 + * */ + static getInstanceByContainer(container: string) { + const instance = CAROUSEL_MANAGER_INSTANCES.get(container) + if (instance) { + return instance + } + return null + } + + /** + * 通过chart.id销毁对应实例 + * 关闭放大图表弹窗,销毁对应实例 + * 重启图表自身轮播 + * */ + static closeEnlargeDialogDestroy(id?: string) { + // 首先,暂停并删除包含 'viewDialog' 的实例 + CAROUSEL_MANAGER_INSTANCES?.forEach((instance, key) => { + if (instance.chart.id === id && instance.chart.container.includes('viewDialog')) { + const dialogInstance = CAROUSEL_MANAGER_INSTANCES.get(key) + if (dialogInstance) { + dialogInstance.destroy() + } + } + }) + setTimeout(() => { + // 然后,恢复 + CAROUSEL_MANAGER_INSTANCES?.forEach(instance => { + if (instance.chartIsVisible) { + instance.resume() + } + }) + }, 400) + } + + /** + * 暂停轮播 + * @param id + */ + static paused(id?: string) { + CAROUSEL_MANAGER_INSTANCES?.forEach(instance => { + if (id && instance.chart.id === id) { + setTimeout(() => instance.paused(), 200) + } + if (!id) { + setTimeout(() => instance.paused(), 200) + } + }) + } + + /** + * @param id + */ + static resume(id?: string) { + CAROUSEL_MANAGER_INSTANCES?.forEach(instance => { + if (instance.chart.id === id) { + instance.paused() + setTimeout(() => instance.resume(), 500) + } + if (!id) { + setTimeout(() => instance.resume(), 200) + } + }) + } + + /** + * 初始化核心逻辑 + * */ + private init() { + this.values = [].concat(this.getUniqueValues()) + if (!this.values.length) return + this.chartIsVisible = true + this.states.paused = false + this.states.destroyed = false + this.bindEventListeners() + this.startCarousel() + } + + /** + * 获取图表唯一值集合 + * */ + private getUniqueValues() { + const data = + this.plot instanceof DualAxes + ? [...this.plot.options.data[0], ...this.plot.options.data[1]] + : this.plot.options.data + + return [...new Set(data.map(item => item[this.config.xField]))] + } + + /** + * 启动轮播 + * */ + private startCarousel() { + if (!this.shouldStart()) { + this.stop() + return + } + // 定义启动嵌套定时器的函数 + const startNestedTimers = () => { + // 重置当前索引 + this.currentIndex = 0 + // 定义递归处理数据数组的函数 + const processArray = () => { + if (this.states.paused || this.states.destroyed || !this.isElementFullyVisible()) return + // 获取当前需要显示的值 + const currentValue = this.values[this.currentIndex] + // 计算 Tooltip 显示的位置 + const point = this.calculatePosition(currentValue) + // 高亮当前数据点 + this.highlightElement(currentValue) + if (point) { + // 显示 Tooltip,并设置其位置为顶部 + this.plot.chart.showTooltip(point) + this.plot.chart.getController('tooltip').update() + } + // 更新索引,指向下一个数据点 + this.currentIndex++ + if (this.currentIndex > this.values.length) { + this.currentIndex = 0 + this.hideTooltip() + this.plot.chart.showTooltip({ x: 0, y: 0 }) + this.plot.chart.getController('tooltip').update() + this.unHighlightPoint(currentValue) + this.timers.interval = setTimeout(() => processArray(), this.config.interval) + } else { + // 如果未遍历完,继续处理下一个数据点 + this.timers.carousel = setTimeout(() => processArray(), this.config.duration) + } + } + processArray() + } + this.stop() + startNestedTimers() + } + + /** + * 判断是否满足启动条件' */ + private shouldStart() { + return ( + this.chart.customAttr?.tooltip?.show && + this.chart.customAttr?.tooltip?.carousel?.enable && + this.values.length > 0 && + this.chartIsVisible + ) + } + + /** + * 判断图表是否在可视范围内 + * */ + private isElementFullyVisible(): boolean { + // 全屏 + const isFullscreen = document.fullscreenElement !== null + // 新页面或公共连接 + const isNewPagePublicLink = document + .getElementById('enlarge-inner-content-' + this.chart.id) + ?.getBoundingClientRect() + const isMobileEdit = document.getElementsByClassName('panel-mobile')?.length > 0 + const isMobileList = document.getElementsByClassName('mobile-com-list')?.length > 0 + if (isMobileList) { + return false + } + const rect = this.plot.chart.ele.getBoundingClientRect() + return ( + rect.top >= (isFullscreen || isNewPagePublicLink || isMobileEdit ? 0 : 64) && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ) + } + /** + * 计算元素位置(核心定位逻辑) + * */ + private calculatePosition(value: string) { + const view = this.plot.chart.views?.[0] || this.plot.chart + // 饼图特殊处理 + if (CHART_CATEGORY.PIE.includes(this.chart.type)) { + return this.getPieTooltipPosition(view, value) + } + if (this.plot instanceof DualAxes) { + return this.getDualAxesTooltipPosition(view, value) + } + const types = view + .scale() + .getGeometries() + .map(item => item.type) + let point = { x: 0, y: 0 } + if (!types.length) return point + types.forEach(type => { + if (type === 'interval' || type === 'point') { + point = view + .scale() + .getGeometries() + .find(item => item.type === type) + .elements.find(item => item.data.field === value && (item.model.x || item.model.y))?.model + } + }) + // 处理柱状图和折线图,柱状图固定y轴位置 + const y = CHART_CATEGORY.COLUMN.includes(this.chart.type) ? 0 : [].concat(point?.y)?.[0] + return { x: [].concat(point?.x)?.[0], y: y } + } + + /** + * 计算饼图元素位置 + * */ + private getPieTooltipPosition(view, value: string) { + const piePoint = view + .scale() + .getGeometries()[0] + ?.elements.find(item => item.data.field === value) + ?.getModel() + if (!piePoint) { + return { x: 0, y: 0 } + } + const coordinates = [ + { x: [].concat(piePoint.x)[0], y: piePoint.y[0] }, + { x: piePoint.x[0], y: piePoint.y[1] }, + { x: piePoint.x[1], y: piePoint.y[0] }, + { x: piePoint.x[1], y: piePoint.y[1] } + ] + const index = coordinates.findIndex(coord => { + const items = this.plot.chart.getTooltipItems(coord) + return items.some(item => item.data.field === value) + }) + if (index !== -1) { + return coordinates[index] + } else { + return { + x: piePoint.x[0], + y: piePoint.y[0] + } + } + } + + /** + * 获取双轴图表的 Tooltip 位置 + * @param view + * @param value + * @private + */ + private getDualAxesTooltipPosition(view, value: string) { + const xScale = view.getXScale() + if (!xScale) return { x: 0, y: 0 } + const values = xScale.values + if (values.length < 2) { + const point = view + .getGeometries()?.[0] + .elements[view.getGeometries()?.[0].elements?.length - 1].getModel() + return point || { x: 0, y: 0 } + } + const [rangeStart, rangeEnd] = xScale.range + const totalMonths = values.length + const bandWidth = (rangeEnd - rangeStart) / totalMonths + const index = values.indexOf(value) + const xPos = rangeStart + bandWidth * (index + 0.5) + return view.getCoordinate().convert({ x: xPos, y: 0 }) + } + + /** + * 高亮指定元素 + * */ + private highlightElement(value: string) { + if (CHART_CATEGORY.LINE.includes(this.chart.type)) return + this.unHighlightPoint(value) + this.plot.setState( + this.getHighlightType(), + (data: any) => data[this.config.xField] === value, + true + ) + } + + /** + * 取消高亮 + * **/ + private unHighlightPoint(value?: string) { + if (CHART_CATEGORY.LINE.includes(this.chart.type)) return + this.plot.setState( + this.getHighlightType(), + (data: any) => data[this.config.xField] !== value, + false + ) + } + private getHighlightType() { + return 'active' + } + + /** + * 隐藏工具提示 + * */ + private hideTooltip() { + const container = this.getTooltipContainer() + if (container) { + container.style.display = 'none' + } + } + + /** + * 获取工具提示容器 + * */ + private getTooltipContainer() { + const tooltipCtl = this.plot.chart.getController('tooltip') + if (!tooltipCtl) { + return + } + return tooltipCtl.tooltip?.cfg?.container + } + + /** + * 绑定事件监听 + * */ + private bindEventListeners() { + // 定义图表元素ID前缀数组 + // 图表在不同的显示页面可能有不同的ID前缀 + const chartElementIds = ['enlarge-inner-content-', 'enlarge-inner-shape-'] + let chartElement = null + + // 查找图表元素 + for (const idPrefix of chartElementIds) { + chartElement = document.getElementById(idPrefix + this.chart.id) + if (chartElement) break + } + + // 绑定鼠标进入和离开事件 + chartElement?.addEventListener('mouseenter', () => this.paused()) + chartElement?.addEventListener('mouseleave', ev => { + setTimeout(() => { + // 获取鼠标位置 + const mouseX = ev.clientX + const mouseY = ev.clientY + // 获取div的边界信息 + const rect = chartElement.getBoundingClientRect() + // 判断鼠标位置是否在div内 + const isInside = + mouseX >= rect.left + 10 && + mouseX <= rect.right - 10 && + mouseY >= rect.top + 10 && + mouseY <= rect.bottom - 10 + console.log(isInside) + if (!isInside) { + this.paused() + this.resume() + } + }, 300) + }) + + // 定义鼠标滚轮事件处理函数 + const handleMouseWheel = this.debounce(() => { + CAROUSEL_MANAGER_INSTANCES?.forEach(instance => { + instance.paused() + instance.resume() + }) + }, 50) + // 定义 touchmove 事件处理函数(移动端) + const handleTouchMove = (event: TouchEvent) => { + handleMouseWheel(event) + } + // 获取目标元素,优先全屏预览 + const targetDiv = + document.getElementById('de-preview-content') || + document.getElementById('preview-canvas-main') || + document.getElementById('dv-main-center') || + document.getElementById('edit-canvas-main') || + document.getElementById('canvas-mark-line') || + document.getElementById('de-canvas-canvas-main') + // 绑定目标元素的事件 + if (targetDiv) { + targetDiv.removeEventListener('wheel', handleMouseWheel) + targetDiv.addEventListener('wheel', handleMouseWheel) + //移除和添加 touchmove 事件监听器(移动端) + targetDiv.removeEventListener('touchmove', handleTouchMove) + targetDiv.addEventListener('touchmove', handleTouchMove) + } + // 页面可见性控制 + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'hidden') { + CAROUSEL_MANAGER_INSTANCES?.forEach(instance => { + instance.paused() + }) + } else if (this.chartIsVisible) { + CAROUSEL_MANAGER_INSTANCES?.forEach(instance => { + instance.resume() + }) + } + }) + // 元素可视性观察(交叉观察器) + this.setupIntersectionObserver() + // 元素大小观察(大小观察器) + this.setupResizeObserver() + } + + /** + * 设置暂停状态 + * */ + private setPaused(state: boolean) { + this.states.paused = state + state ? this.stop() : this.startCarousel() + } + /** + * 设置交叉观察器 + * */ + private setupIntersectionObserver() { + setTimeout(() => { + // 监听元素可见性变化,全部可见时开始轮播 + if (!this.observers.get(this.plot.chart.ele.id)) { + this.observers.set( + this.plot.chart.ele.id, + new IntersectionObserver( + entries => { + entries.forEach(entry => { + if (entry.intersectionRatio < 0.7) { + this.paused() + this.chartIsVisible = false + } else { + this.paused() + this.chartIsVisible = true + this.resume() + } + }) + }, + { threshold: [0.7] } + ) + ) + this.observers.get(this.plot.chart.ele.id).observe(this.plot.chart.ele) + } + }, 100) + } + + /** + * 设置元素大小观察器 + * 当元素全部可见时 + * 图表的最外层元素 + * @private + */ + private setupResizeObserver() { + // 放大图表弹窗不需要监听 + if (this.plot.chart.ele.id.includes('viewDialog')) return + // 创建防抖回调函数 + const debouncedCallback = (entries: ResizeObserverEntry[]) => { + for (const entry of entries) { + if (entry.target) { + this.debounce(() => { + this.paused() + this.resume() + }, 200) + } + } + } + // 监听元素大小, 发生变化时重新轮播 + if (!this.resizeObservers.get(this.plot.chart.ele.id)) { + this.resizeObservers.set(this.plot.chart.ele.id, new ResizeObserver(debouncedCallback)) + this.resizeObservers.get(this.plot.chart.ele.id).observe(this.plot.chart.ele) + } + } + + /** + * 更新配置 + * */ + private update(plot: Plot | DualAxes, chart: Chart, config: CarouselConfig) { + this.stop() + this.plot = plot + this.chart = chart + this.config = { ...this.config, ...config } + this.currentIndex = 0 + this.init() + } + + /** + * 停止定时器 + * @private + */ + private stop() { + clearTimeout(this.timers.interval) + clearTimeout(this.timers.carousel) + this.timers = { interval: null, carousel: null } + } + + /** + * 销毁实例 + * */ + destroy() { + this.stop() + this.clearObserver() + this.states.destroyed = true + CAROUSEL_MANAGER_INSTANCES.delete(this.chart.container) + } + /** + * 清除观察器 + * */ + clearObserver() { + const observer = this.observers.get(this.plot.chart.ele.id) + if (observer) { + observer.disconnect() + this.observers.delete(this.plot.chart.ele.id) + } + const resizeObservers = this.resizeObservers.get(this.plot.chart.ele.id) + if (resizeObservers) { + resizeObservers.disconnect() + this.resizeObservers.delete(this.plot.chart.ele.id) + } + } + /** 暂停 */ + paused() { + this.hideTooltip() + this.unHighlightPoint() + this.setPaused(true) + } + + /** 恢复 */ + resume() { + this.setPaused(false) + } + + /** + * 防抖 + */ + private debounce(func: (...args: any[]) => void, delay: number): (...args: any[]) => void { + let timeout: number | null = null + return (...args: any[]) => { + if (timeout) clearTimeout(timeout) + timeout = window.setTimeout(() => { + func(...args) + }, delay) + } + } +} + +export default ChartCarouselTooltip diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/bar/bar.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/bar/bar.ts index d9d091f..8c9feeb 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/bar/bar.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/bar/bar.ts @@ -7,7 +7,6 @@ import { import { flow, hexColorToRGBA, - hexToRgba, parseJson, setUpGroupSeriesColor, setUpStackSeriesColor @@ -21,6 +20,7 @@ import { } from '@/views/chart/components/js/panel/charts/bar/common' import { configPlotTooltipEvent, + configRoundAngle, getLabel, getPadding, getTooltipContainer, @@ -43,7 +43,14 @@ export class Bar extends G2PlotChartView { ...BAR_EDITOR_PROPERTY_INNER, 'basic-style-selector': [...BAR_EDITOR_PROPERTY_INNER['basic-style-selector'], 'seriesColor'], 'label-selector': ['vPosition', 'seriesLabelFormatter', 'showExtremum'], - 'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'seriesTooltipFormatter', 'show'], + 'tooltip-selector': [ + 'fontSize', + 'color', + 'backgroundColor', + 'seriesTooltipFormatter', + 'show', + 'carousel' + ], 'y-axis-selector': [...BAR_EDITOR_PROPERTY_INNER['y-axis-selector'], 'axisLabelFormatter'] } protected baseOptions: ColumnOptions = { @@ -69,11 +76,14 @@ export class Bar extends G2PlotChartView { async drawChart(drawOptions: G2PlotDrawOptions): Promise { const { chart, container, action } = drawOptions + chart.container = container if (!chart?.data?.data?.length) { - chart.container = container clearExtremum(chart) return } + const isGroup = 'bar-group' === this.name && chart.xAxisExt?.length > 0 + const isStack = + ['bar-stack', 'bar-group-stack'].includes(this.name) && chart.extStack?.length > 0 const data = cloneDeep(drawOptions.chart.data?.data) const initOptions: ColumnOptions = { ...this.baseOptions, @@ -108,7 +118,7 @@ export class Bar extends G2PlotChartView { const label = { fields: [], ...tmpOptions.label, - formatter: (data: Datum, _point) => { + formatter: (data: Datum) => { if (data.EXTREME) { return '' } @@ -174,19 +184,9 @@ export class Bar extends G2PlotChartView { color } } - if (basicStyle.radiusColumnBar === 'roundAngle') { - const columnStyle = { - radius: [ - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius - ] - } - options = { - ...options, - columnStyle - } + options = { + ...options, + ...configRoundAngle(chart, 'columnStyle') } let columnWidthRatio const _v = basicStyle.columnWidthRatio ?? DEFAULT_BASIC_STYLE.columnWidthRatio @@ -227,7 +227,10 @@ export class Bar extends G2PlotChartView { tickCount: axisValue.splitCount } } - return { ...tmpOptions, ...axis } + // 根据axis的最小值,过滤options中的data数据,过滤掉小于最小值的数据 + const { data } = options + const newData = data.filter(item => item.value >= axisValue.min) + return { ...tmpOptions, data: newData, ...axis } } return tmpOptions } @@ -276,7 +279,14 @@ export class StackBar extends Bar { 'totalFormatter', 'showStackQuota' ], - 'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'tooltipFormatter', 'show'] + 'tooltip-selector': [ + 'fontSize', + 'color', + 'backgroundColor', + 'tooltipFormatter', + 'show', + 'carousel' + ] } protected configLabel(chart: Chart, options: ColumnOptions): ColumnOptions { let label = getLabel(chart) @@ -438,6 +448,74 @@ export class GroupBar extends StackBar { } } + async drawChart(drawOptions: G2PlotDrawOptions): Promise { + const plot = await super.drawChart(drawOptions) + if (!plot) { + return plot + } + const { chart } = drawOptions + const { xAxis, xAxisExt, yAxis } = chart + let innerSort = !!(xAxis.length && xAxisExt.length && yAxis.length) + if (innerSort && yAxis[0].sort === 'none') { + innerSort = false + } + if (innerSort && xAxisExt[0].sort !== 'none') { + const sortPriority = chart.sortPriority ?? [] + const yAxisIndex = sortPriority?.findIndex(e => e.id === yAxis[0].id) + const xAxisExtIndex = sortPriority?.findIndex(e => e.id === xAxisExt[0].id) + if (xAxisExtIndex <= yAxisIndex) { + innerSort = false + } + } + if (!innerSort) { + return plot + } + plot.chart.once('beforepaint', () => { + const geo = plot.chart.geometries[0] + const originMapping = geo.beforeMapping.bind(geo) + geo.beforeMapping = originData => { + const values = geo.getXScale().values + const valueMap = values.reduce((p, n) => { + if (!p?.[n]) { + p[n] = { + fieldArr: [], + indexArr: [], + dataArr: [] + } + } + originData.forEach((arr, arrIndex) => { + arr.forEach((item, index) => { + if (item._origin.field === n) { + p[n].fieldArr.push(item.field) + p[n].indexArr.push([arrIndex, index]) + p[n].dataArr.push(item) + } + }) + }) + return p + }, {}) + values.forEach(v => { + const item = valueMap[v] + item.dataArr.sort((a, b) => { + if (yAxis[0].sort === 'asc') { + return a.value - b.value + } + if (yAxis[0].sort === 'desc') { + return b.value - a.value + } + return 0 + }) + item.indexArr.forEach((index, i) => { + item.dataArr[i].field = item.fieldArr[i] + originData[index[0]][index[1]] = item.dataArr[i] + }) + }) + return originMapping(originData) + } + }) + return plot + } + protected configLabel(chart: Chart, options: ColumnOptions): ColumnOptions { const tmpLabel = getLabel(chart) if (!tmpLabel) { @@ -448,7 +526,7 @@ export class GroupBar extends StackBar { baseOptions.label.style.fill = labelAttr.color const label = { ...baseOptions.label, - formatter: function (param: Datum, _point) { + formatter: function (param: Datum) { if (param.EXTREME) { return '' } @@ -492,6 +570,7 @@ export class GroupBar extends StackBar { super(name) this.baseOptions = { ...this.baseOptions, + marginRatio: 0, isGroup: true, isStack: false, meta: { @@ -606,7 +685,7 @@ export class PercentageStackBar extends GroupStackBar { propertyInner = { ...this['propertyInner'], 'label-selector': ['color', 'fontSize', 'vPosition', 'reserveDecimalCount'], - 'tooltip-selector': ['color', 'fontSize', 'backgroundColor', 'show'] + 'tooltip-selector': ['color', 'fontSize', 'backgroundColor', 'show', 'carousel'] } protected configLabel(chart: Chart, options: ColumnOptions): ColumnOptions { const baseOptions = super.configLabel(chart, options) diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/bar/bidirectional-bar.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/bar/bidirectional-bar.ts index 0e81f0b..4ec976a 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/bar/bidirectional-bar.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/bar/bidirectional-bar.ts @@ -6,14 +6,14 @@ import { cloneDeep, defaultTo, isEmpty, map } from 'lodash-es' import { configAxisLabelLengthLimit, configPlotTooltipEvent, + configRoundAngle, getPadding, getTooltipContainer, getTooltipItemConditionColor, getYAxis, getYAxisExt, setGradientColor, - TOOLTIP_TPL, - addConditionsStyleColorToData + TOOLTIP_TPL } from '@/views/chart/components/js/panel/common/common_antv' import type { BidirectionalBar as G2BidirectionalBar, @@ -213,19 +213,9 @@ export class BidirectionalHorizontalBar extends G2PlotChartView< ...options, layout: basicStyle.layout } - if (basicStyle.radiusColumnBar === 'roundAngle') { - const barStyle = { - radius: [ - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius - ] - } - options = { - ...options, - barStyle - } + options = { + ...options, + ...configRoundAngle(chart, 'barStyle') } return options } diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/bar/bullet-graph.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/bar/bullet-graph.ts new file mode 100644 index 0000000..e9a77fe --- /dev/null +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/bar/bullet-graph.ts @@ -0,0 +1,507 @@ +import type { + Bullet as G2Bullet, + BulletOptions as G2BulletOptions +} from '@antv/g2plot/esm/plots/bullet' +import { + G2PlotChartView, + G2PlotDrawOptions +} from '@/views/chart/components/js/panel/types/impl/g2plot' +import { + BAR_AXIS_TYPE, + BAR_EDITOR_PROPERTY, + BAR_EDITOR_PROPERTY_INNER +} from '@/views/chart/components/js/panel/charts/bar/common' +import { useI18n } from '@/hooks/web/useI18n' +import { flow, parseJson } from '@/views/chart/components/js/util' +import { BulletOptions } from '@antv/g2plot' +import { isEmpty } from 'lodash-es' +import { + configAxisLabelLengthLimit, + configPlotTooltipEvent, + getPadding, + getTooltipContainer, + TOOLTIP_TPL +} from '@/views/chart/components/js/panel/common/common_antv' +import { valueFormatter } from '@/views/chart/components/js/formatter' + +const { t } = useI18n() + +/** + * 子弹图 + */ +export class BulletGraph extends G2PlotChartView { + constructor() { + super('bullet-graph', []) + } + + axis: AxisType[] = [...BAR_AXIS_TYPE, 'yAxisExt', 'extBubble'] + axisConfig = { + ...this['axisConfig'], + xAxis: { name: `${t('chart.form_type')} / ${t('chart.dimension')}`, type: 'd', limit: 1 }, + yAxis: { name: `${t('chart.progress_current')} / ${t('chart.quota')}`, type: 'q', limit: 1 }, + yAxisExt: { name: `${t('chart.progress_target')} / ${t('chart.quota')}`, type: 'q', limit: 1 }, + extBubble: { + name: `${t('chart.range_bg')} / ${t('chart.quota')}`, + type: 'q', + allowEmpty: true, + limit: 1 + } + } + properties: EditorProperty[] = [ + ...BAR_EDITOR_PROPERTY.filter( + item => !['function-cfg', 'assist-line', 'threshold'].includes(item) + ), + 'bullet-graph-selector' + ] + propertyInner = { + 'basic-style-selector': ['radiusColumnBar', 'layout'], + 'label-selector': ['hPosition', 'fontSize', 'color', 'labelFormatter'], + 'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'seriesTooltipFormatter', 'show'], + 'x-axis-selector': [ + ...BAR_EDITOR_PROPERTY_INNER['x-axis-selector'].filter(item => item != 'position'), + 'showLengthLimit' + ], + 'y-axis-selector': [ + ...BAR_EDITOR_PROPERTY_INNER['y-axis-selector'].filter( + item => item !== 'axisValue' && item !== 'position' + ), + 'axisLabelFormatter' + ], + 'legend-selector': ['showRange', 'orient', 'fontSize', 'color', 'hPosition', 'vPosition'] + } + + async drawChart(drawOption: G2PlotDrawOptions): Promise { + const { chart, container, action } = drawOption + if (!chart.data?.data?.length) return + const result = mergeBulletData(chart) + // 处理自定义区间 + const { bullet } = parseJson(chart.customAttr).misc + if (bullet.bar.ranges.showType === 'fixed') { + const customRange = bullet.bar.ranges.fixedRange?.map(item => item.fixedRangeValue) || [0] + result.forEach(item => (item.ranges = customRange)) + } else { + result.forEach(item => (item.ranges = item.originalRanges)) + } + // 处理自定义目标值 + if (bullet.bar.target.showType === 'fixed') { + const customTarget = bullet.bar.target.value || 0 + result.forEach(item => (item.target = customTarget)) + } else { + result.forEach(item => (item.target = item.originalTarget)) + } + const initialOptions: BulletOptions = { + appendPadding: getPadding(chart), + data: result.reverse(), + measureField: 'measures', + rangeField: 'ranges', + targetField: 'target', + xField: 'title', + meta: { + title: { + type: 'cat' + } + }, + interactions: [ + { + type: 'active-region', + cfg: { + start: [{ trigger: 'element:mousemove', action: 'active-region:show' }], + end: [{ trigger: 'element:mouseleave', action: 'active-region:hide' }] + } + } + ] + } + const options = this.setupOptions(chart, initialOptions) + let newChart = null + const { Bullet: BulletClass } = await import('@antv/g2plot/esm/plots/bullet') + newChart = new BulletClass(container, options) + newChart.on('element:click', ev => { + const pointData = ev?.data?.data + const dimensionList = options.data.find(item => item.title === pointData.title)?.dimensionList + const actionParams = { + x: ev.x, + y: ev.y, + data: { + data: { + ...pointData, + dimensionList + } + } + } + action(actionParams) + }) + configPlotTooltipEvent(chart, newChart) + configAxisLabelLengthLimit(chart, newChart, null) + return newChart + } + + protected configBasicStyle(chart: Chart, options: BulletOptions): BulletOptions { + const basicStyle = parseJson(chart.customAttr).basicStyle + const { radiusColumnBar, columnBarRightAngleRadius, layout } = basicStyle + let radiusValue = 0 + let rangeLength = 1 + if (radiusColumnBar === 'roundAngle' || radiusColumnBar === 'topRoundAngle') { + radiusValue = columnBarRightAngleRadius + rangeLength = options.data[0]?.ranges?.length + } + const barRadiusStyle = { radius: Array(2).fill(radiusValue) } + const baseRadius = [...barRadiusStyle.radius, ...barRadiusStyle.radius] + options = { + ...options, + bulletStyle: { + range: datum => { + if (!datum.rKey) return { fill: 'rgba(0, 0, 0, 0)' } + if (rangeLength === 1) { + return { + radius: + radiusColumnBar === 'topRoundAngle' ? [...barRadiusStyle.radius, 0, 0] : baseRadius + } + } + if (rangeLength > 1 && datum.rKey === 'ranges_0') { + return { + radius: radiusColumnBar === 'topRoundAngle' ? [] : [0, 0, ...barRadiusStyle.radius] + } + } + if (rangeLength > 1 && datum.rKey === 'ranges_' + (rangeLength - 1)) { + return { radius: [...barRadiusStyle.radius, 0, 0] } + } + }, + measure: datum => { + if (datum.measures) { + return { + radius: + radiusColumnBar === 'topRoundAngle' ? [...barRadiusStyle.radius, 0, 0] : baseRadius + } + } else { + return undefined + } + }, + target: datum => (datum.tKey === 'target' ? { lineWidth: 2 } : undefined) + } + } + if (layout === 'vertical') options = { ...options, layout: 'vertical' } + return options + } + + protected configMisc(chart: Chart, options: BulletOptions): BulletOptions { + const { bullet } = parseJson(chart.customAttr).misc + const isDynamic = bullet.bar.ranges.showType === 'dynamic' + // 动态背景按大小升序 + const rangeColor = isDynamic + ? bullet.bar.ranges.fill + : bullet.bar.ranges.fixedRange + ?.sort((a, b) => (a.fixedRangeValue ?? 0) - (b.fixedRangeValue ?? 0)) + .map(item => item.fill) || [] + return { + ...options, + color: { + measure: [].concat(bullet.bar.measures.fill), + range: [].concat(rangeColor), + target: [].concat(bullet.bar.target.fill) + }, + size: { + measure: bullet.bar.measures.size, + range: bullet.bar.ranges.size, + target: bullet.bar.target.size + } + } + } + + protected configXAxis(chart: Chart, options: BulletOptions): BulletOptions { + const tmpOptions = super.configXAxis(chart, options) + if (!tmpOptions.xAxis || !tmpOptions.xAxis.label) return tmpOptions + + const { layout, xAxis } = tmpOptions + const position = xAxis.position + const style: any = { ...xAxis.label.style } + + if (layout === 'vertical') { + style.textAlign = 'center' + style.textBaseline = position === 'bottom' ? 'top' : 'bottom' + } else { + style.textAlign = position === 'bottom' ? 'end' : 'start' + style.textBaseline = 'middle' + } + + xAxis.label.style = style + return tmpOptions + } + + protected configYAxis(chart: Chart, options: BulletOptions): BulletOptions { + const tmpOptions = super.configYAxis(chart, options) + if (!tmpOptions.yAxis || !tmpOptions.yAxis.label) return tmpOptions + + const yAxis = parseJson(chart.customStyle).yAxis + tmpOptions.yAxis.label.formatter = value => valueFormatter(value, yAxis.axisLabelFormatter) + + const { layout, yAxis: yAxisConfig } = tmpOptions + const position = yAxisConfig.position + const style: any = { ...yAxisConfig.label.style } + + if (layout === 'vertical') { + style.textAlign = position === 'left' ? 'end' : 'start' + style.textBaseline = 'middle' + } else { + style.textAlign = 'center' + style.textBaseline = position === 'left' ? 'top' : 'bottom' + } + + yAxisConfig.label.style = style + return tmpOptions + } + + protected configLabel(chart: Chart, options: BulletOptions): BulletOptions { + const tmpOptions = super.configLabel(chart, options) + if (!tmpOptions.label) return tmpOptions + + const labelAttr = parseJson(chart.customAttr).label + const label: any = { + ...tmpOptions.label, + formatter: param => + param.mKey === 'measures' + ? valueFormatter(param.measures, labelAttr.labelFormatter) + : undefined + } + return { ...tmpOptions, label: { measure: label } } + } + + protected configLegend(chart: Chart, options: BulletOptions): BulletOptions { + const baseLegend = super.configLegend(chart, options).legend + if (!baseLegend) return options + + const { bullet } = parseJson(chart.customAttr).misc + const customStyleLegend = parseJson(chart.customStyle).legend + const items = [] + + const createLegendItem = (value, name, symbol, fill, size = 4) => ({ + value, + name, + marker: { symbol, style: { fill, stroke: value === 'measure' ? '' : fill, r: size } } + }) + + if (customStyleLegend.showRange) { + if (bullet.bar.ranges.showType === 'dynamic') { + if (chart.extBubble.length) { + const rangeName = chart.extBubble[0]?.chartShowName || bullet.bar.ranges.name + items.push( + createLegendItem( + 'dynamic', + rangeName || chart.extBubble[0]?.name, + bullet.bar.ranges.symbol, + [].concat(bullet.bar.ranges.fill)[0], + bullet.bar.ranges.symbolSize + ) + ) + } + } else { + bullet.bar.ranges.fixedRange?.forEach(item => { + items.push( + createLegendItem( + item.name, + item.name, + bullet.bar.ranges.symbol, + item.fill, + bullet.bar.ranges.symbolSize + ) + ) + }) + } + } + + const targetName = chart.yAxisExt[0]?.chartShowName || bullet.bar.target.name + items.push( + createLegendItem( + 'target', + targetName || chart.yAxisExt[0]?.name, + 'line', + [].concat(bullet.bar.target.fill)[0], + bullet.bar.ranges.symbolSize + ) + ) + + const measureName = chart.yAxis[0]?.chartShowName || bullet.bar.measures.name + items.push( + createLegendItem( + 'measure', + measureName || chart.yAxis[0]?.name, + 'square', + [].concat(bullet.bar.measures.fill)[0], + bullet.bar.ranges.symbolSize + ) + ) + + return { + ...options, + legend: { custom: true, position: baseLegend.position, layout: baseLegend.layout, items } + } + } + + protected configTooltip(chart: Chart, options: BulletOptions): BulletOptions { + const customAttr: DeepPartial = parseJson(chart.customAttr) + const tooltipAttr = customAttr.tooltip + const { bullet } = parseJson(chart.customAttr).misc + if (!tooltipAttr.show) return { ...options, tooltip: false } + + const formatterMap = tooltipAttr.seriesTooltipFormatter + ?.filter(i => i.show) + .reduce((pre, next, index) => { + const keys = ['measures', 'target', 'ranges'] + if (keys[index]) pre[keys[index]] = next + return pre + }, {}) as Record + + const tooltip = { + shared: true, + showMarkers: true, + customItems(originalItems) { + if (!tooltipAttr.seriesTooltipFormatter?.length) return originalItems + + const result = [] + const data = options.data.find(item => item.title === originalItems[0].title) + Object.keys(formatterMap).forEach(key => { + if (key === '记录数*') return + const formatter = formatterMap[key] + if (formatter) { + if (key !== 'ranges') { + let value = 0 + if (chart.yAxis[0].id === chart.yAxisExt[0].id) { + value = valueFormatter(parseFloat(data['target'] as string), formatter.formatterCfg) + } else { + value = valueFormatter(parseFloat(data[key] as string), formatter.formatterCfg) + } + const name = isEmpty(formatter.chartShowName) + ? formatter.name + : formatter.chartShowName + result.push({ ...originalItems[0], color: bullet.bar[key].fill, name, value }) + } else { + const ranges = data.ranges + const isDynamic = bullet.bar.ranges.showType === 'dynamic' + ranges.forEach((range, index) => { + const value = valueFormatter( + parseFloat(isDynamic ? data.minRanges[0] : (range as string)), + formatter.formatterCfg + ) + let name = '' + let color: string | string[] + if (bullet.bar.ranges.showType === 'dynamic') { + name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName + color = bullet.bar[key].fill + } else { + const customRange = bullet.bar.ranges.fixedRange[index].name + name = customRange + ? customRange + : isEmpty(formatter.chartShowName) + ? formatter.name + : formatter.chartShowName + color = bullet.bar[key].fixedRange[index].fill + } + result.push({ ...originalItems[0], color, name, value }) + }) + } + } + }) + const dynamicTooltipValue = chart.data.data.find( + d => d.field === originalItems[0]['title'] + )?.dynamicTooltipValue + if (dynamicTooltipValue.length > 0) { + dynamicTooltipValue.forEach(dy => { + const q = tooltipAttr.seriesTooltipFormatter.filter(i => i.id === dy.fieldId) + if (q && q.length > 0) { + const value = valueFormatter(parseFloat(dy.value as string), q[0].formatterCfg) + const name = isEmpty(q[0].chartShowName) ? q[0].name : q[0].chartShowName + result.push({ color: 'grey', name, value }) + } + }) + } + return result + }, + container: getTooltipContainer(`tooltip-${chart.id}`), + itemTpl: TOOLTIP_TPL, + enterable: true + } + return { ...options, tooltip } + } + + setupDefaultOptions(chart: ChartObj): ChartObj { + chart.customAttr.label.position = 'middle' + chart.customStyle.yAxis.splitLine.show = false + return super.setupDefaultOptions(chart) + } + + protected setupOptions(chart: Chart, options: BulletOptions): BulletOptions { + return flow( + this.configTheme, + this.configBasicStyle, + this.configMisc, + this.configXAxis, + this.configYAxis, + this.configLabel, + this.configLegend, + this.configTooltip + )(chart, options, {}, this) + } +} + +/** + * 组装子弹图数据 + * @param chart + */ +function mergeBulletData(chart): any[] { + // 先根据维度分组,再根据指标字段组装成子弹图的格式 + const groupedData = chart.data.data.reduce((acc, item) => { + const field = item.field + if (!acc[field]) { + acc[field] = [] + } + acc[field].push(item) + return acc + }, {}) + const result = [] + // 组装子弹图数据,每个维度对应一个子弹图 + Object.keys(groupedData).forEach(field => { + const items = groupedData[field] + // 初始化子弹图条目结构 + const entry = { + title: field, + ranges: [], + measures: [], + target: [], + dimensionList: items[0].dimensionList, + quotaList: [] + } + + // 防止指标相同时无数据有可能会导致数据不一致 + items.forEach(item => { + const quotaId = item.quotaList[0]?.id + const v = item.value || 0 + if (quotaId === chart.yAxis[0]?.id) { + entry.measures.push(v) + } + if (quotaId === chart.yAxisExt[0]?.id) { + entry.target.push(v) + } + if (quotaId === chart.extBubble[0]?.id) { + entry.ranges.push(v) + } + entry.quotaList.push(item.quotaList[0]) + }) + // 对数据进行累加 + const ranges = chart.extBubble[0]?.id + ? [].concat(entry.ranges?.reduce((acc, curr) => acc + curr, 0)) + : [] + const target = [].concat(entry.target?.reduce((acc, curr) => acc + curr, 0)) + const measures = [].concat(entry.measures?.reduce((acc, curr) => acc + curr, 0)) + const bulletData = { + ...entry, + measures: measures, + target: target, + ranges: ranges, + quotaList: [...entry.quotaList], + minRanges: ranges, + originalRanges: ranges, + originalTarget: target + } + result.push(bulletData) + }) + return result +} diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/bar/horizontal-bar.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/bar/horizontal-bar.ts index 841729c..2d720e5 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/bar/horizontal-bar.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/bar/horizontal-bar.ts @@ -6,6 +6,7 @@ import type { Bar, BarOptions } from '@antv/g2plot/esm/plots/bar' import { configAxisLabelLengthLimit, configPlotTooltipEvent, + configRoundAngle, getPadding, getTooltipContainer, setGradientColor, @@ -101,6 +102,17 @@ export class HorizontalBar extends G2PlotChartView { const newChart = new Bar(container, options) newChart.on('interval:click', action) + if (options.label) { + newChart.on('label:click', e => { + action({ + x: e.x, + y: e.y, + data: { + data: e.target.attrs.data + } + }) + }) + } configPlotTooltipEvent(chart, newChart) configAxisLabelLengthLimit(chart, newChart) return newChart @@ -135,7 +147,10 @@ export class HorizontalBar extends G2PlotChartView { tickCount: axisValue.splitCount } } - return { ...tmpOptions, ...axis } + // 根据axis的最小值,过滤options中的data数据,过滤掉小于最小值的数据 + const { data } = options + const newData = data.filter(item => item.value >= axisValue.min) + return { ...tmpOptions, data: newData, ...axis } } return tmpOptions } @@ -157,19 +172,9 @@ export class HorizontalBar extends G2PlotChartView { color } } - if (basicStyle.radiusColumnBar === 'roundAngle') { - const barStyle = { - radius: [ - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius - ] - } - options = { - ...options, - barStyle - } + options = { + ...options, + ...configRoundAngle(chart, 'barStyle') } let barWidthRatio @@ -234,6 +239,7 @@ export class HorizontalBar extends G2PlotChartView { attrs: { x: 0, y: 0, + data, text: value, textAlign: 'start', textBaseline: 'top', @@ -316,8 +322,24 @@ export class HorizontalStackBar extends HorizontalBar { baseOptions.label.style.fill = labelAttr.color const label = { ...baseOptions.label, - formatter: function (param: Datum) { - return valueFormatter(param.value, labelAttr.labelFormatter) + formatter: function (data: Datum) { + const value = valueFormatter(data.value, labelAttr.labelFormatter) + const group = new Group({}) + group.addShape({ + type: 'text', + attrs: { + x: 0, + y: 0, + data, + text: value, + textAlign: 'start', + textBaseline: 'top', + fontSize: labelAttr.fontSize, + fontFamily: chart.fontFamily, + fill: labelAttr.color + } + }) + return group } } return { @@ -435,11 +457,29 @@ export class HorizontalPercentageStackBar extends HorizontalStackBar { const l = parseJson(customAttr).label const label = { ...baseOptions.label, - formatter: function (param: Datum) { - if (!param.value) { - return '0%' + formatter: function (data: Datum) { + let value = data.value + if (value) { + value = (Math.round(value * 10000) / 100).toFixed(l.reserveDecimalCount) + '%' + } else { + value = '0%' } - return (Math.round(param.value * 10000) / 100).toFixed(l.reserveDecimalCount) + '%' + const group = new Group({}) + group.addShape({ + type: 'text', + attrs: { + x: 0, + y: 0, + data, + text: value, + textAlign: 'start', + textBaseline: 'top', + fontSize: l.fontSize, + fontFamily: chart.fontFamily, + fill: l.color + } + }) + return group } } return { diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/bar/progress-bar.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/bar/progress-bar.ts index 7531722..8b592ba 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/bar/progress-bar.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/bar/progress-bar.ts @@ -3,8 +3,8 @@ import { flow, hexColorToRGBA, parseJson } from '../../../util' import { configAxisLabelLengthLimit, configPlotTooltipEvent, + configRoundAngle, getTooltipContainer, - getTooltipItemConditionColor, setGradientColor, TOOLTIP_TPL } from '../../common/common_antv' @@ -66,7 +66,7 @@ export class ProgressBar extends G2PlotChartView { 'fontSize', 'axisForm', 'axisLabel', - 'position', + // 'position', 'showLengthLimit' ], 'function-cfg': ['emptyDataStrategy'], @@ -166,6 +166,7 @@ export class ProgressBar extends G2PlotChartView { } }) if (basicStyle.gradient) { + // eslint-disable-next-line color1 = color1.map((ele, _index) => { return setGradientColor(ele, true, 0) }) @@ -184,19 +185,9 @@ export class ProgressBar extends G2PlotChartView { } } } - if (basicStyle.radiusColumnBar === 'roundAngle') { - const barStyle = { - radius: [ - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius - ] - } - options = { - ...options, - barStyle - } + options = { + ...options, + ...configRoundAngle(chart, 'barStyle') } let barWidthRatio @@ -297,12 +288,31 @@ export class ProgressBar extends G2PlotChartView { if (!baseOption.yAxis) { return baseOption } - if (baseOption.yAxis.position === 'left') { + baseOption.yAxis.position = 'bottom' + const yAxis = parseJson(chart.customStyle).yAxis + if (yAxis.axisLabel.show) { + const rotate = yAxis.axisLabel.rotate + let textAlign = 'end' + let textBaseline = 'middle' + if (Math.abs(rotate) > 75) { + textAlign = 'center' + } + if (rotate > 75) { + textBaseline = 'top' + } + if (rotate < -75) { + textBaseline = 'bottom' + } + baseOption.yAxis.label.style.textBaseline = textBaseline + baseOption.yAxis.label.style.textAlign = textAlign + } + + /*if (baseOption.yAxis.position === 'left') { baseOption.yAxis.position = 'bottom' } if (baseOption.yAxis.position === 'right') { baseOption.yAxis.position = 'top' - } + }*/ return baseOption } setupDefaultOptions(chart: ChartObj): ChartObj { diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/bar/range-bar.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/bar/range-bar.ts index d2e103f..6520a3a 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/bar/range-bar.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/bar/range-bar.ts @@ -6,6 +6,7 @@ import type { Bar, BarOptions } from '@antv/g2plot/esm/plots/bar' import { configAxisLabelLengthLimit, configPlotTooltipEvent, + configRoundAngle, getPadding, getTooltipContainer, setGradientColor, @@ -22,6 +23,7 @@ import { import { Datum } from '@antv/g2plot/esm/types/common' import { useI18n } from '@/hooks/web/useI18n' import { DEFAULT_BASIC_STYLE } from '@/views/chart/components/editor/util/chart' +import { Group } from '@antv/g-canvas' const { t } = useI18n() const DEFAULT_DATA = [] @@ -170,6 +172,17 @@ export class RangeBar extends G2PlotChartView { const newChart = new BarClass(container, options) newChart.on('interval:click', action) + if (options.label) { + newChart.on('label:click', e => { + action({ + x: e.x, + y: e.y, + data: { + data: e.target.attrs.data + } + }) + }) + } configPlotTooltipEvent(chart, newChart) configAxisLabelLengthLimit(chart, newChart) return newChart @@ -309,19 +322,10 @@ export class RangeBar extends G2PlotChartView { } } } - if (basicStyle.radiusColumnBar === 'roundAngle') { - const barStyle = { - radius: [ - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius, - basicStyle.columnBarRightAngleRadius - ] - } - options = { - ...options, - barStyle - } + + options = { + ...options, + ...configRoundAngle(chart, 'barStyle') } let barWidthRatio const _v = basicStyle.columnWidthRatio ?? DEFAULT_BASIC_STYLE.columnWidthRatio @@ -391,7 +395,22 @@ export class RangeBar extends G2PlotChartView { valueFormatter(param.values[1], labelAttr.labelFormatter) } } - return res + const group = new Group({}) + group.addShape({ + type: 'text', + attrs: { + x: 0, + y: 0, + data: param, + text: res, + textAlign: 'start', + textBaseline: 'top', + fontSize: labelAttr.fontSize, + fontFamily: chart.fontFamily, + fill: labelAttr.color + } + }) + return group } } return { diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/bar/waterfall.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/bar/waterfall.ts index b22cf80..42b429a 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/bar/waterfall.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/bar/waterfall.ts @@ -72,7 +72,8 @@ export class Waterfall extends G2PlotChartView { 'axisForm', 'axisLabel', 'axisLabelFormatter', - 'showLengthLimit' + 'showLengthLimit', + 'axisLine' ], threshold: ['lineThreshold'] } diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/line/area.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/line/area.ts index 65c0d3b..b6bd598 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/line/area.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/line/area.ts @@ -46,7 +46,8 @@ export class Area extends G2PlotChartView { 'label-selector': ['seriesLabelVPosition', 'seriesLabelFormatter', 'showExtremum'], 'tooltip-selector': [ ...LINE_EDITOR_PROPERTY_INNER['tooltip-selector'], - 'seriesTooltipFormatter' + 'seriesTooltipFormatter', + 'carousel' ] } axis: AxisType[] = [...LINE_AXIS_TYPE] @@ -103,8 +104,8 @@ export class Area extends G2PlotChartView { async drawChart(drawOptions: G2PlotDrawOptions): Promise { const { chart, container, action } = drawOptions + chart.container = container if (!chart.data?.data?.length) { - chart.container = container clearExtremum(chart) return } @@ -147,7 +148,7 @@ export class Area extends G2PlotChartView { fields: [], ...tmpOptions.label, layout: labelAttr.fullDisplay ? [{ type: 'limit-in-plot' }] : tmpOptions.label.layout, - formatter: (data: Datum, _point) => { + formatter: (data: Datum) => { if (data.EXTREME) { return '' } @@ -305,7 +306,7 @@ export class StackArea extends Area { propertyInner = { ...this['propertyInner'], 'label-selector': ['vPosition', 'fontSize', 'color', 'labelFormatter'], - 'tooltip-selector': ['fontSize', 'color', 'tooltipFormatter', 'show'] + 'tooltip-selector': ['fontSize', 'color', 'tooltipFormatter', 'show', 'carousel'] } axisConfig = { ...this['axisConfig'], diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/line/line.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/line/line.ts index da92ea4..ae8b45d 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/line/line.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/line/line.ts @@ -10,10 +10,12 @@ import { TOOLTIP_TPL } from '../../common/common_antv' import { + convertToAlphaColor, flow, getLineConditions, getLineLabelColorByCondition, hexColorToRGBA, + isAlphaColor, parseJson, setUpGroupSeriesColor } from '@/views/chart/components/js/util' @@ -43,8 +45,10 @@ export class Line extends G2PlotChartView { 'label-selector': ['seriesLabelVPosition', 'seriesLabelFormatter', 'showExtremum'], 'tooltip-selector': [ ...LINE_EDITOR_PROPERTY_INNER['tooltip-selector'], - 'seriesTooltipFormatter' - ] + 'seriesTooltipFormatter', + 'carousel' + ], + 'legend-selector': [...LINE_EDITOR_PROPERTY_INNER['legend-selector'], 'legendSort'] } axis: AxisType[] = [...LINE_AXIS_TYPE, 'xAxisExt'] axisConfig = { @@ -66,8 +70,8 @@ export class Line extends G2PlotChartView { } async drawChart(drawOptions: G2PlotDrawOptions): Promise { const { chart, action, container } = drawOptions + chart.container = container if (!chart.data?.data?.length) { - chart.container = container clearExtremum(chart) return } @@ -146,7 +150,7 @@ export class Line extends G2PlotChartView { fields: [], ...tmpOptions.label, layout: labelAttr.fullDisplay ? [{ type: 'limit-in-plot' }] : tmpOptions.label.layout, - formatter: (data: Datum, _point) => { + formatter: (data: Datum) => { if (data.EXTREME) { return '' } @@ -321,17 +325,30 @@ export class Line extends G2PlotChartView { if (sort?.length) { // 用值域限定排序,有可能出现新数据但是未出现在图表上,所以这边要遍历一下子维度,加到后面,让新数据显示出来 const data = optionTmp.data - data?.forEach(d => { - const cat = d['category'] - if (cat && !sort.includes(cat)) { - sort.push(cat) + const cats = + data?.reduce((p, n) => { + const cat = n['category'] + if (cat && !p.includes(cat)) { + p.push(cat) + } + return p + }, []) || [] + const values = sort.reduce((p, n) => { + if (cats.includes(n)) { + const index = cats.indexOf(n) + if (index !== -1) { + cats.splice(index, 1) + } + p.push(n) } - }) + return p + }, []) + cats.length > 0 && values.push(...cats) optionTmp.meta = { ...optionTmp.meta, category: { type: 'cat', - values: sort + values } } } @@ -351,6 +368,56 @@ export class Line extends G2PlotChartView { fill: style.stroke } } + const { sort, customSort, icon } = customStyle.legend + if (sort && sort !== 'none' && chart.xAxisExt.length) { + const customAttr = parseJson(chart.customAttr) + const { basicStyle } = customAttr + const seriesMap = + basicStyle.seriesColor?.reduce((p, n) => { + p[n.id] = n + return p + }, {}) || {} + const dupCheck = new Set() + const items = optionTmp.data?.reduce((arr, item) => { + if (!dupCheck.has(item.category)) { + const fill = + seriesMap[item.category]?.color ?? + optionTmp.color[dupCheck.size % optionTmp.color.length] + dupCheck.add(item.category) + arr.push({ + name: item.category, + value: item.category, + marker: { + symbol: icon, + style: { + r: size, + fill: isAlphaColor(fill) ? fill : convertToAlphaColor(fill, basicStyle.alpha) + } + } + }) + } + return arr + }, []) + if (sort !== 'custom') { + items.sort((a, b) => { + return sort !== 'desc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name) + }) + } else { + const tmp = [] + ;(customSort || []).forEach(item => { + const index = items.findIndex(i => i.name === item) + if (index !== -1) { + tmp.push(items[index]) + items.splice(index, 1) + } + }) + items.unshift(...tmp) + } + optionTmp.legend.items = items + if (xAxisExt?.customSort?.length > 0) { + delete optionTmp.meta?.category.values + } + } return optionTmp } protected setupOptions(chart: Chart, options: LineOptions): LineOptions { diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/line/stock-line.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/line/stock-line.ts index 67218c3..e54ed67 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/line/stock-line.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/line/stock-line.ts @@ -440,7 +440,6 @@ export class StockLine extends G2PlotChartView { protected configTooltip(chart: Chart, options: MixOptions): MixOptions { const tooltipAttr = parseJson(chart.customAttr).tooltip - const xAxis = chart.xAxis const newPlots = [] const linePlotList = options.plots.filter(item => item.type === 'line') linePlotList.forEach(item => { diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/liquid/liquid.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/liquid/liquid.ts index 52e87c3..9a22ed3 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/liquid/liquid.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/liquid/liquid.ts @@ -74,7 +74,7 @@ export class Liquid extends G2PlotChartView { }) // 处理空数据, 只要有一个指标是空数据,就不显示图表 const hasNoneData = chart.data?.series.some(s => !s.data?.[0]) - this.configEmptyDataStyle(newChart, hasNoneData ? [] : [1], container) + this.configEmptyDataStyle(hasNoneData ? [] : [1], container, newChart) if (hasNoneData) { return } diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/map/bubble-map.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/map/bubble-map.ts index 41223ca..6a7e0e6 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/map/bubble-map.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/map/bubble-map.ts @@ -402,7 +402,8 @@ export class BubbleMap extends L7PlotChartView { content.push(name) } if (label.showQuota) { - areaMap[name] && content.push(valueFormatter(areaMap[name], label.quotaLabelFormatter)) + ;(areaMap[name] || areaMap[name] === 0) && + content.push(valueFormatter(areaMap[name], label.quotaLabelFormatter)) } item.properties['_DE_LABEL_'] = content.join('\n\n') } diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/map/common.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/map/common.ts index 7589f4d..e9de26b 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/map/common.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/map/common.ts @@ -1,3 +1,7 @@ +import { useI18n } from '@/hooks/web/useI18n' + +const { t } = useI18n() + export const MAP_EDITOR_PROPERTY: EditorProperty[] = [ 'background-overall-component', 'border-style', @@ -51,6 +55,28 @@ export const MAP_AXIS_TYPE: AxisType[] = [ 'extTooltip' ] +export const gaodeMapStyleOptions = [ + { name: t('chart.map_style_normal'), value: 'normal' }, + { name: t('chart.map_style_darkblue'), value: 'darkblue' }, + { name: t('chart.map_style_light'), value: 'light' }, + { name: t('chart.map_style_dark'), value: 'dark' }, + { name: t('chart.map_style_fresh'), value: 'fresh' }, + { name: t('chart.map_style_grey'), value: 'grey' }, + { name: t('chart.map_style_blue'), value: 'blue' }, + { name: t('commons.custom'), value: 'custom' } +] + +export const tdtMapStyleOptions = [ + { name: t('chart.map_style_normal'), value: 'normal' }, + { name: t('chart.map_style_dark'), value: 'black' }, + { name: t('chart.map_style_darkblue'), value: 'indigo' } +] + +export const qqMapStyleOptions = [ + { name: t('chart.map_style_normal'), value: 'normal' }, + { name: t('commons.custom'), value: 'custom' } +] + export declare type MapMouseEvent = MouseEvent & { feature: GeoJSON.Feature } diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/map/flow-map.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/map/flow-map.ts index e53d3de..54ed8c9 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/map/flow-map.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/map/flow-map.ts @@ -8,12 +8,16 @@ import { import { MAP_EDITOR_PROPERTY_INNER } from '@/views/chart/components/js/panel/charts/map/common' import { 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 { LineLayer } from '@antv/l7-layers' import { PointLayer } from '@antv/l7-layers' -import { mapRendered, mapRendering } from '@/views/chart/components/js/panel/common/common_antv' -import { DEFAULT_BASIC_STYLE } from '@/views/chart/components/editor/util/chart' +import { + getMapCenter, + getMapScene, + getMapStyle, + mapRendered, + qqMapRendered +} from '@/views/chart/components/js/panel/common/common_antv' const { t } = useI18n() /** @@ -88,103 +92,16 @@ export class FlowMap extends L7ChartView { const xAxisExt = deepCopy(chart.xAxisExt) const { basicStyle, misc } = deepCopy(parseJson(chart.customAttr)) - let center: [number, number] = [ - DEFAULT_BASIC_STYLE.mapCenter.longitude, - DEFAULT_BASIC_STYLE.mapCenter.latitude - ] - if (basicStyle.autoFit === false) { - center = [basicStyle.mapCenter.longitude, basicStyle.mapCenter.latitude] - } - let mapStyle = basicStyle.mapStyleUrl - if (basicStyle.mapStyle !== 'custom') { - mapStyle = `amap://styles/${basicStyle.mapStyle ? basicStyle.mapStyle : 'normal'}` - } const mapKey = await this.getMapKey() + const mapStyle = getMapStyle(mapKey, basicStyle) // 底层 const chartObj = drawOption.chartObj as unknown as L7Wrapper let scene = chartObj?.getScene() - if(scene){ - if (scene.getLayers()?.length) { - await scene.removeAllLayer() - scene.setPitch(misc.mapPitch) - } - } - if (mapStyle.indexOf('Satellite') == -1) { - scene = new Scene({ - id: container, - logoVisible: false, - map: new GaodeMap({ - token: mapKey?.key ?? undefined, - style: mapStyle, - pitch: misc.mapPitch, - center, - zoom: basicStyle.autoFit === false ? basicStyle.zoomLevel : undefined, - showLabel: !(basicStyle.showLabel === false), - WebGLParams: { - preserveDrawingBuffer: true - } - }) - }) - }else{ - scene = new Scene({ - id: container, - logoVisible: false, - map: new GaodeMap({ - token: mapKey?.key ?? undefined, - style: mapStyle, - features: ['bg', 'road'], // 必须开启路网层 - plugin: ['AMap.TileLayer.Satellite'], // 显式声明卫星图层 - WebGLParams: { - preserveDrawingBuffer: true - } - }) - }) - } - // if (!scene) { - // scene = new Scene({ - // id: container, - // logoVisible: false, - // map: new GaodeMap({ - // token: mapKey?.key ?? undefined, - // style: mapStyle, - // pitch: misc.mapPitch, - // center: basicStyle.autoFit === false ? center : undefined, - // zoom: basicStyle.autoFit === false ? basicStyle.zoomLevel : undefined, - // showLabel: !(basicStyle.showLabel === false), - // WebGLParams: { - // preserveDrawingBuffer: true - // } - // }) - // }) - // } else { - // if (scene.getLayers()?.length) { - // await scene.removeAllLayer() - // scene.setPitch(misc.mapPitch) - // scene.setMapStyle(mapStyle) - // scene.map.showLabel = !(basicStyle.showLabel === false) - // } - // if (basicStyle.autoFit === false) { - // scene.setZoomAndCenter(basicStyle.zoomLevel, center) - // } - // } - mapRendering(container) - if (mapStyle.indexOf('Satellite') == -1) { - scene.once('loaded', () => { - mapRendered(container) - }) - } else { - scene.once('loaded', () => { - // 创建卫星图层实例 - const satelliteLayer = new AMap.TileLayer.Satellite() - // 与矢量图层叠加显示 - satelliteLayer.setMap(scene.map) - mapRendered(container) - }) - } - // scene.once('loaded', () => { - // mapRendered(container) - // }) - this.configZoomButton(chart, scene) + + const center = getMapCenter(basicStyle) + scene = await getMapScene(chart, scene, container, mapKey, basicStyle, misc, mapStyle, center) + + this.configZoomButton(chart, scene, mapKey) if (xAxis?.length < 2 || xAxisExt?.length < 2) { return new L7Wrapper(scene, undefined) } @@ -195,6 +112,11 @@ export class FlowMap extends L7ChartView { configList[0].once('inited', () => { mapRendered(container) }) + for (let i = 0; i < configList.length; i++) { + configList[i].on('inited', () => { + qqMapRendered(scene) + }) + } return new L7Wrapper(scene, configList) } diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/map/heat-map.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/map/heat-map.ts index 088db46..e36ad9e 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/map/heat-map.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/map/heat-map.ts @@ -8,11 +8,16 @@ import { 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 { DEFAULT_BASIC_STYLE } from '@/views/chart/components/editor/util/chart' -import { mapRendered, mapRendering } from '@/views/chart/components/js/panel/common/common_antv' +import { + getMapCenter, + getMapScene, + getMapStyle, + mapRendered, + qqMapRendered +} from '@/views/chart/components/js/panel/common/common_antv' const { t } = useI18n() /** @@ -69,94 +74,31 @@ export class HeatMap extends L7ChartView { basicStyle = parseJson(chart.customAttr).basicStyle miscStyle = parseJson(chart.customAttr).misc } - let center: [number, number] = [ - DEFAULT_BASIC_STYLE.mapCenter.longitude, - DEFAULT_BASIC_STYLE.mapCenter.latitude - ] - if (basicStyle.autoFit === false) { - center = [basicStyle.mapCenter.longitude, basicStyle.mapCenter.latitude] - } - let mapStyle = basicStyle.mapStyleUrl - if (basicStyle.mapStyle !== 'custom') { - mapStyle = `amap://styles/${basicStyle.mapStyle ? basicStyle.mapStyle : 'normal'}` - } const mapKey = await this.getMapKey() + const mapStyle = getMapStyle(mapKey, basicStyle) // 底层 const chartObj = drawOption.chartObj as unknown as L7Wrapper let scene = chartObj?.getScene() - if(scene){ - if (scene.getLayers()?.length) { - await scene.removeAllLayer() - scene.setPitch(miscStyle.mapPitch) - } - } - - if (mapStyle.indexOf('Satellite') == -1) { - scene = new Scene({ - id: container, - logoVisible: false, - map: new GaodeMap({ - token: mapKey?.key ?? undefined, - style: mapStyle, - pitch: miscStyle.mapPitch, - center, - zoom: basicStyle.autoFit === false ? basicStyle.zoomLevel : undefined, - showLabel: !(basicStyle.showLabel === false), - WebGLParams: { - preserveDrawingBuffer: true - } - }) - }) - } else { - scene = new Scene({ - id: container, - logoVisible: false, - map: new GaodeMap({ - token: mapKey?.key ?? undefined, - style: mapStyle, - features: ['bg', 'road'], // 必须开启路网层 - plugin: ['AMap.TileLayer.Satellite'], // 显式声明卫星图层 - WebGLParams: { - preserveDrawingBuffer: true - } - }) - }) - } - - // } else { - // // if (scene.getLayers()?.length) { - // await scene.removeAllLayer() - // scene.setPitch(miscStyle.mapPitch) - // scene.setMapStyle(mapStyle) - // scene.map.showLabel = !(basicStyle.showLabel === false) - // if (basicStyle.autoFit === false) { - // scene.setZoomAndCenter(basicStyle.zoomLevel, center) - // } - // // } - // } - mapRendering(container) - if (mapStyle.indexOf('Satellite') == -1) { - scene.once('loaded', () => { - mapRendered(container) - }) - } else { - scene.once('loaded', () => { - // 创建卫星图层实例 - const satelliteLayer = new AMap.TileLayer.Satellite() - // 与矢量图层叠加显示 - satelliteLayer.setMap(scene.map) - mapRendered(container) - }) - } - - this.configZoomButton(chart, scene) + const center = getMapCenter(basicStyle) + scene = await getMapScene( + chart, + scene, + container, + mapKey, + basicStyle, + miscStyle, + mapStyle, + center + ) + this.configZoomButton(chart, scene, mapKey) if (xAxis?.length < 2 || yAxis?.length < 1) { return new L7Wrapper(scene, undefined) } const config: L7Config = new HeatmapLayer({ name: 'line', blend: 'normal', - autoFit: !(basicStyle.autoFit === false) + autoFit: !(basicStyle.autoFit === false), + zIndex: 10 }) .source(chart.data?.data, { parser: { @@ -177,6 +119,13 @@ export class HeatMap extends L7ChartView { } }) + config.once('inited', () => { + mapRendered(container) + }) + config.on('inited', () => { + qqMapRendered(scene) + }) + return new L7Wrapper(scene, config) } diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/map/map.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/map/map.ts index 418c82a..74b564e 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/map/map.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/map/map.ts @@ -157,6 +157,11 @@ export class Map extends L7PlotChartView { }) }) data = filterChartDataByRange(sourceData, maxValue, minValue) + if (chart.drill) { + getMaxAndMinValueByData(sourceData, 'value', 0, 0, (max, min) => { + data = filterChartDataByRange(sourceData, max, min) + }) + } } else { data = sourceData } @@ -301,7 +306,8 @@ export class Map extends L7PlotChartView { content.push(name) } if (label.showQuota) { - areaMap[name] && content.push(valueFormatter(areaMap[name], label.quotaLabelFormatter)) + ;(areaMap[name] || areaMap[name] === 0) && + content.push(valueFormatter(areaMap[name], label.quotaLabelFormatter)) } item.properties['_DE_LABEL_'] = content.join('\n\n') } @@ -346,11 +352,7 @@ export class Map extends L7PlotChartView { return listDom } - private customConfigLegend( - chart: Chart, - options: ChoroplethOptions, - context: Record - ): ChoroplethOptions { + private customConfigLegend(chart: Chart, options: ChoroplethOptions): ChoroplethOptions { const { basicStyle, misc } = parseJson(chart.customAttr) const colors = basicStyle.colors.map(item => hexColorToRGBA(item, basicStyle.alpha)) if (basicStyle.suspension === false && basicStyle.showZoom === undefined) { @@ -420,14 +422,14 @@ export class Map extends L7PlotChartView { const isLessThanMin = range[0] < ranges[0][0] && range[1] < ranges[0][0] let rangeColor = colors[colorIndex] if (isLessThanMin) { - rangeColor = hexColorToRGBA(basicStyle.areaBaseColor, basicStyle.alpha) + rangeColor = basicStyle.areaBaseColor } items.push({ value: tmpRange, color: rangeColor }) }) - customLegend['customContent'] = (_: string, _items: CategoryLegendListItem[]) => { + customLegend['customContent'] = () => { if (items?.length) { return this.createLegendCustomContent(items) } @@ -435,13 +437,16 @@ export class Map extends L7PlotChartView { } options.color['value'] = ({ value }) => { const item = items.find(item => value >= item.value[0] && value <= item.value[1]) - return item ? item.color : hexColorToRGBA(basicStyle.areaBaseColor, basicStyle.alpha) + return item ? item.color : basicStyle.areaBaseColor } options.color.scale.domain = [ranges[0][0], ranges[ranges.length - 1][1]] } else { customLegend['customContent'] = (_: string, items: CategoryLegendListItem[]) => { const showItems = items?.length > 30 ? items.slice(0, 30) : items if (showItems?.length) { + if (showItems.length === 1) { + showItems[0].value = options.color.scale.domain.slice(0, 2) + } return this.createLegendCustomContent(showItems) } return '' @@ -508,7 +513,7 @@ export class Map extends L7PlotChartView { content.push(area.name) } if (label.showQuota) { - areaMap[area.name] && + ;(areaMap[area.name] || areaMap[area.name] === 0) && content.push(valueFormatter(areaMap[area.name].value, label.quotaLabelFormatter)) } labelLocation.push({ @@ -567,6 +572,9 @@ export class Map extends L7PlotChartView { return result } const head = originalItem.properties + if (!head) { + return result + } const { adcode } = head const areaName = subAreaMap['156' + adcode] const valItem = areaMap[areaName] diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/map/symbolic-map.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/map/symbolic-map.ts index 4b5e296..6a0e00c 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/map/symbolic-map.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/map/symbolic-map.ts @@ -13,13 +13,17 @@ import { svgStrToUrl } 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, Popup } from '@antv/l7' -import { mapRendered, mapRendering } from '@/views/chart/components/js/panel/common/common_antv' +import { + getMapCenter, + getMapScene, + getMapStyle, + mapRendered, + qqMapRendered +} from '@/views/chart/components/js/panel/common/common_antv' import { configCarouselTooltip } from '@/views/chart/components/js/panel/charts/map/tooltip-carousel' -import { DEFAULT_BASIC_STYLE } from '@/views/chart/components/editor/util/chart' import { filter } from 'lodash-es' const { t } = useI18n() @@ -102,18 +106,10 @@ export class SymbolicMap extends L7ChartView { miscStyle = parseJson(chart.customAttr).misc } - let mapStyle = basicStyle.mapStyleUrl - if (basicStyle.mapStyle !== 'custom') { - mapStyle = `amap://styles/${basicStyle.mapStyle ? basicStyle.mapStyle : 'normal'}` - } const mapKey = await this.getMapKey() - let center: [number, number] = [ - DEFAULT_BASIC_STYLE.mapCenter.longitude, - DEFAULT_BASIC_STYLE.mapCenter.latitude - ] - if (basicStyle.autoFit === false) { - center = [basicStyle.mapCenter.longitude, basicStyle.mapCenter.latitude] - } + const mapStyle = getMapStyle(mapKey, basicStyle) + + let center = getMapCenter(basicStyle) // 联动时,聚焦到数据点,多个取第一个 if ( chart.chartExtRequest?.linkageFilters?.length && @@ -128,38 +124,18 @@ export class SymbolicMap extends L7ChartView { } const chartObj = drawOption.chartObj as unknown as L7Wrapper let scene = chartObj?.getScene() - if (!scene) { - scene = new Scene({ - id: container, - logoVisible: false, - map: new GaodeMap({ - token: mapKey?.key ?? undefined, - style: mapStyle, - pitch: miscStyle.mapPitch, - center, - zoom: basicStyle.autoFit === false ? basicStyle.zoomLevel : undefined, - showLabel: !(basicStyle.showLabel === false), - WebGLParams: { - preserveDrawingBuffer: true - } - }) - }) - } else { - if (scene.getLayers()?.length) { - await scene.removeAllLayer() - scene.setPitch(miscStyle.mapPitch) - scene.setMapStyle(mapStyle) - scene.map.showLabel = !(basicStyle.showLabel === false) - } - if (basicStyle.autoFit === false) { - scene.setZoomAndCenter(basicStyle.zoomLevel, center) - } - } - mapRendering(container) - scene.once('loaded', () => { - mapRendered(container) - }) - this.configZoomButton(chart, scene) + scene = await getMapScene( + chart, + scene, + container, + mapKey, + basicStyle, + miscStyle, + mapStyle, + center + ) + + this.configZoomButton(chart, scene, mapKey) if (xAxis?.length < 2) { return new L7Wrapper(scene, undefined) } @@ -171,9 +147,13 @@ export class SymbolicMap extends L7ChartView { scene.addPopup(tooltipLayer) } this.buildLabel(chart, configList) + symbolicLayer.once('inited', () => { + mapRendered(container) + }) symbolicLayer.on('inited', () => { chart.container = container configCarouselTooltip(chart, symbolicLayer, symbolicLayer.sourceOption.data, scene) + qqMapRendered(scene) }) symbolicLayer.on('click', ev => { const data = ev.feature diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/others/chart-mix.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/others/chart-mix.ts index 87f67e5..0b19537 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/others/chart-mix.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/others/chart-mix.ts @@ -4,6 +4,7 @@ import { } from '@/views/chart/components/js/panel/types/impl/g2plot' import type { DualAxes, DualAxesOptions } from '@antv/g2plot/esm/plots/dual-axes' import { + configRoundAngle, configPlotTooltipEvent, getAnalyse, getLabel, @@ -42,6 +43,7 @@ import { } from '@/views/chart/components/editor/util/chart' import type { Options } from '@antv/g2plot/esm' import { Group } from '@antv/g-canvas' +import { extremumEvt } from '@/views/chart/components/js/extremumUitl' const { t } = useI18n() const DEFAULT_DATA = [] @@ -56,7 +58,8 @@ export class ColumnLineMix extends G2PlotChartView { 'label-selector': ['vPosition', 'seriesLabelFormatter'], 'tooltip-selector': [ ...CHART_MIX_EDITOR_PROPERTY_INNER['tooltip-selector'], - 'seriesTooltipFormatter' + 'seriesTooltipFormatter', + 'carousel' ] } axis: AxisType[] = [...CHART_MIX_AXIS_TYPE, 'xAxisExtRight', 'yAxisExt'] @@ -94,6 +97,7 @@ export class ColumnLineMix extends G2PlotChartView { async drawChart(drawOptions: G2PlotDrawOptions): Promise { const { chart, action, container } = drawOptions + chart.container = container if (!chart.data?.left?.data?.length && !chart.data?.right?.data?.length) { return } @@ -117,7 +121,6 @@ export class ColumnLineMix extends G2PlotChartView { valueExt: d.value } }) - // options const initOptions: DualAxesOptions = { data: [data1, data2], @@ -127,6 +130,7 @@ export class ColumnLineMix extends G2PlotChartView { geometryOptions: [ { geometry: data1Type, + marginRatio: 0, color: [], isGroup: isGroup, isStack: isStack, @@ -174,6 +178,7 @@ export class ColumnLineMix extends G2PlotChartView { newChart.on('point:click', action) newChart.on('interval:click', action) + extremumEvt(newChart, chart, options, container) configPlotTooltipEvent(chart, newChart) return newChart } @@ -292,18 +297,9 @@ export class ColumnLineMix extends G2PlotChartView { tempOption.geometryOptions[1].smooth = smooth tempOption.geometryOptions[1].point = point tempOption.geometryOptions[1].lineStyle = lineStyle - - if (s.radiusColumnBar === 'roundAngle') { - const columnStyle = { - radius: [ - s.columnBarRightAngleRadius, - s.columnBarRightAngleRadius, - s.columnBarRightAngleRadius, - s.columnBarRightAngleRadius - ] - } - tempOption.geometryOptions[0].columnStyle = columnStyle - tempOption.geometryOptions[1].columnStyle = columnStyle + tempOption.geometryOptions[0] = { + ...tempOption.geometryOptions[0], + ...configRoundAngle(chart, 'columnStyle') } } @@ -328,7 +324,7 @@ export class ColumnLineMix extends G2PlotChartView { } setupDefaultOptions(chart: ChartObj): ChartObj { - const { customAttr, senior } = chart + const { senior } = chart if ( senior.functionCfg.emptyDataStrategy == undefined || senior.functionCfg.emptyDataStrategy === 'ignoreData' @@ -670,7 +666,8 @@ export class GroupColumnLineMix extends ColumnLineMix { 'label-selector': ['vPosition', 'seriesLabelFormatter'], 'tooltip-selector': [ ...CHART_MIX_EDITOR_PROPERTY_INNER['tooltip-selector'], - 'seriesTooltipFormatter' + 'seriesTooltipFormatter', + 'carousel' ] } axisConfig = { @@ -782,7 +779,8 @@ export class StackColumnLineMix extends ColumnLineMix { 'label-selector': ['vPosition', 'seriesLabelFormatter'], 'tooltip-selector': [ ...CHART_MIX_EDITOR_PROPERTY_INNER['tooltip-selector'], - 'seriesTooltipFormatter' + 'seriesTooltipFormatter', + 'carousel' ] } axisConfig = { @@ -895,7 +893,8 @@ export class DualLineMix extends ColumnLineMix { 'label-selector': ['seriesLabelFormatter'], 'tooltip-selector': [ ...CHART_MIX_EDITOR_PROPERTY_INNER['tooltip-selector'], - 'seriesTooltipFormatter' + 'seriesTooltipFormatter', + 'carousel' ] } axisConfig = { diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/others/circle-packing.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/others/circle-packing.ts index 380fd06..eceae28 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/others/circle-packing.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/others/circle-packing.ts @@ -69,7 +69,7 @@ export class CirclePacking extends G2PlotChartView @@ -123,7 +123,7 @@ export class CirclePacking extends G2PlotChartView { + newChart.on('element:click', param => { const pointData = param?.data?.data if (pointData?.name === t('commons.all')) { return @@ -177,7 +177,7 @@ export class CirclePacking extends G2PlotChartView { + formatter: (d: Datum) => { return d.children.length === 0 ? d.name : '' } } diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/others/gauge.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/others/gauge.ts index e6928d5..4ebc5a5 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/others/gauge.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/others/gauge.ts @@ -11,7 +11,7 @@ import { getScaleValue } from '@/views/chart/components/editor/util/chart' import { valueFormatter } from '@/views/chart/components/js/formatter' -import { getPadding, setGradientColor } from '@/views/chart/components/js/panel/common/common_antv' +import { setGradientColor } from '@/views/chart/components/js/panel/common/common_antv' import { useI18n } from '@/hooks/web/useI18n' import { merge } from 'lodash-es' @@ -74,7 +74,7 @@ export class Gauge extends G2PlotChartView { // options const initOptions: GaugeOptions = { percent: 0, - appendPadding: getPadding(chart), + appendPadding: [0, 10, 15, 10], axis: { tickInterval: 0.2, label: { @@ -109,8 +109,10 @@ export class Gauge extends G2PlotChartView { } }) }) - const hasNoneData = chart.data?.series.some(s => !s.data?.[0]) - this.configEmptyDataStyle(newChart, hasNoneData ? [] : [1], container) + const hasNoneData = chart.data?.series.some( + s => s.data?.[0] === undefined || s.data?.[0] === null + ) + this.configEmptyDataStyle(hasNoneData ? [] : [1], container, newChart) if (hasNoneData) { return } diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/others/indicator.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/others/indicator.ts index 7b99f1f..2626e48 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/others/indicator.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/others/indicator.ts @@ -15,7 +15,8 @@ export class IndicatorChartView extends AbstractChartView { 'indicator-value-selector', 'indicator-name-selector', 'threshold', - 'function-cfg' + 'function-cfg', + 'linkage' ] propertyInner: EditorPropertyInner = { 'background-overall-component': ['all'], diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/others/quadrant.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/others/quadrant.ts index 3162c22..6f5aed3 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/others/quadrant.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/others/quadrant.ts @@ -13,7 +13,8 @@ import { configPlotTooltipEvent, configYaxisTitleLengthLimit, getTooltipContainer, - TOOLTIP_TPL + TOOLTIP_TPL, + getPadding } from '../../common/common_antv' import { DEFAULT_LEGEND_STYLE } from '@/views/chart/components/editor/util/chart' @@ -209,7 +210,7 @@ export class Quadrant extends G2PlotChartView { data: data, xField: 'yAxis', yField: 'yAxisExt', - appendPadding: 30, + appendPadding: getPadding(chart), pointStyle: { fillOpacity: 0.8, stroke: '#bbb' @@ -476,7 +477,6 @@ export class Quadrant extends G2PlotChartView { this.configLegend, this.configXAxis, this.configYAxis, - this.configAnalyse, this.configSlider, this.configBasicStyle )(chart, options, {}, this) diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/others/scatter.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/others/scatter.ts index ea44973..7f144f2 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/others/scatter.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/others/scatter.ts @@ -14,6 +14,8 @@ import { import { useI18n } from '@/hooks/web/useI18n' import { defaults, isEmpty } from 'lodash-es' import { DEFAULT_LEGEND_STYLE } from '@/views/chart/components/editor/util/chart' +import { type Datum } from '@antv/g2plot/esm' +import { Group } from '@antv/g-canvas' const { t } = useI18n() /** @@ -144,6 +146,17 @@ export class Scatter extends G2PlotChartView { const { Scatter: G2Scatter } = await import('@antv/g2plot/esm/plots/scatter') const newChart = new G2Scatter(container, options) newChart.on('point:click', action) + if (options.label) { + newChart.on('label:click', e => { + action({ + x: e.x, + y: e.y, + data: { + data: e.target.attrs.data + } + }) + }) + } configPlotTooltipEvent(chart, newChart) return newChart } @@ -277,6 +290,41 @@ export class Scatter extends G2PlotChartView { return optionTmp } + protected configLabel(chart: Chart, options: ScatterOptions): ScatterOptions { + const tmpOption = super.configLabel(chart, options) + if (!tmpOption.label) { + return options + } + const { label: labelAttr } = parseJson(chart.customAttr) + tmpOption.label.style.fill = labelAttr.color + const label = { + ...tmpOption.label, + formatter: function (data: Datum) { + const value = valueFormatter(data.value, labelAttr.labelFormatter) + const group = new Group({}) + group.addShape({ + type: 'text', + attrs: { + x: 0, + y: 0, + data, + text: value, + textAlign: 'start', + textBaseline: 'top', + fontSize: labelAttr.fontSize, + fontFamily: chart.fontFamily, + fill: labelAttr.color + } + }) + return group + } + } + return { + ...tmpOption, + label + } + } + protected setupOptions(chart: Chart, options: ScatterOptions) { return flow( this.configTheme, @@ -286,7 +334,6 @@ export class Scatter extends G2PlotChartView { this.configLegend, this.configXAxis, this.configYAxis, - this.configAnalyse, this.configSlider, this.configBasicStyle )(chart, options) diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/pie/pie.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/pie/pie.ts index 9947863..fa68234 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/pie/pie.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/pie/pie.ts @@ -27,19 +27,23 @@ import type { Datum } from '@antv/g2plot/esm/types/common' import { add } from 'mathjs' import isEmpty from 'lodash-es/isEmpty' import { cloneDeep } from 'lodash-es' - +import { useI18n } from '@/hooks/web/useI18n' +const { t } = useI18n() const DEFAULT_DATA = [] export class Pie extends G2PlotChartView { axis: AxisType[] = PIE_AXIS_TYPE properties = PIE_EDITOR_PROPERTY propertyInner: EditorPropertyInner = { ...PIE_EDITOR_PROPERTY_INNER, - 'basic-style-selector': ['colors', 'alpha', 'radius', 'topN', 'seriesColor'] + 'basic-style-selector': ['colors', 'alpha', 'radius', 'topN', 'seriesColor'], + 'tooltip-selector': [...PIE_EDITOR_PROPERTY_INNER['tooltip-selector'], 'carousel'] } axisConfig = PIE_AXIS_CONFIG async drawChart(drawOptions: G2PlotDrawOptions): Promise { const { chart, container, action } = drawOptions + this.configEmptyDataStyle(chart.data?.data, container, null, t('chart.no_data_or_not_positive')) + chart.container = container if (!chart.data?.data?.length) { return } @@ -115,12 +119,22 @@ export class Pie extends G2PlotChartView { field: { type: 'cat' } + }, + state: { + active: { + style: { + lineWidth: 2, + fillOpacity: 0.5 + } + } } } const options = this.setupOptions(chart, initOptions) const { Pie: G2Pie } = await import('@antv/g2plot/esm/plots/pie') const newChart = new G2Pie(container, options) - newChart.on('interval:click', action) + newChart.on('interval:click', d => { + d.data?.data?.field !== customAttr.basicStyle.topNLabel && action(d) + }) configPlotTooltipEvent(chart, newChart) return newChart } @@ -244,6 +258,7 @@ export class Pie extends G2PlotChartView { }, container: getTooltipContainer(`tooltip-${chart.id}`), itemTpl: TOOLTIP_TPL, + shared: true, enterable: true } return { @@ -338,7 +353,8 @@ export class Pie extends G2PlotChartView { export class PieDonut extends Pie { propertyInner: EditorPropertyInner = { ...PIE_EDITOR_PROPERTY_INNER, - 'basic-style-selector': ['colors', 'alpha', 'radius', 'innerRadius', 'topN', 'seriesColor'] + 'basic-style-selector': ['colors', 'alpha', 'radius', 'innerRadius', 'topN', 'seriesColor'], + 'tooltip-selector': [...PIE_EDITOR_PROPERTY_INNER['tooltip-selector'], 'carousel'] } protected configBasicStyle(chart: Chart, options: PieOptions): PieOptions { const tmp = super.configBasicStyle(chart, options) diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/pie/rose.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/pie/rose.ts index 424c793..c59e45a 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/pie/rose.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/pie/rose.ts @@ -40,6 +40,7 @@ export class Rose extends G2PlotChartView { async drawChart(drawOptions: G2PlotDrawOptions): Promise { const { chart, container, action } = drawOptions + this.configEmptyDataStyle(chart.data?.data, container, null, t('chart.no_data_or_not_positive')) if (!chart?.data?.data?.length) { return } diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/table/common.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/table/common.ts index 08b146e..eb1ecf1 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/table/common.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/table/common.ts @@ -6,6 +6,7 @@ export const TABLE_EDITOR_PROPERTY: EditorProperty[] = [ 'table-cell-selector', 'title-selector', 'tooltip-selector', + 'summary-selector', 'function-cfg', 'threshold', 'scroll-cfg', diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/table/t-heatmap.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/table/t-heatmap.ts index 2714773..8afb116 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/table/t-heatmap.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/table/t-heatmap.ts @@ -123,7 +123,8 @@ export class TableHeatmap extends G2PlotChartView { const xFieldExt = xAxisExt[0].dataeaseName const extColorField = extColor[0].dataeaseName // data - const data = cloneDeep(chart.data.tableRow) + const tmpData = cloneDeep(chart.data.tableRow) + const data = tmpData.filter(cell => cell[xField] && cell[xFieldExt] && cell[extColorField]) data.forEach(i => { Object.keys(i).forEach(key => { if (key === '*') { @@ -207,6 +208,12 @@ export class TableHeatmap extends G2PlotChartView { return newChart } + protected configTheme(chart: Chart, options: HeatmapOptions): HeatmapOptions { + const tmp = super.configTheme(chart, options) + tmp.theme.innerLabels.offset = 0 + return tmp + } + protected configBasicStyle(chart: Chart, options: HeatmapOptions): HeatmapOptions { const basicStyle = parseJson(chart.customAttr).basicStyle const color = basicStyle.colors?.map(ele => { diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/table/table-info.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/table/table-info.ts index cd49629..391b4ca 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/table/table-info.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/table/table-info.ts @@ -14,7 +14,7 @@ import { hexColorToRGBA, isAlphaColor, parseJson } from '../../../util' import { S2ChartView, S2DrawOptions } from '../../types/impl/s2' import { TABLE_EDITOR_PROPERTY, TABLE_EDITOR_PROPERTY_INNER } from './common' import { useI18n } from '@/hooks/web/useI18n' -import { isEqual, isNumber, merge } from 'lodash-es' +import { filter, isEqual, isNumber, merge } from 'lodash-es' import { copyContent, CustomDataCell, @@ -22,37 +22,19 @@ import { getRowIndex, calculateHeaderHeight, SortTooltip, - configSummaryRow, - summaryRowStyle, configEmptyDataStyle, getLeafNodes, - getColumns + getColumns, + drawImage, + getSummaryRow, + SummaryCell } from '@/views/chart/components/js/panel/common/common_table' const { t } = useI18n() + class ImageCell extends CustomDataCell { protected drawTextShape(): void { - const img = new Image() - const { x, y, width, height, fieldValue } = this.meta - img.src = fieldValue as string - img.setAttribute('crossOrigin', 'anonymous') - img.onload = () => { - !this.cfg.children && (this.cfg.children = []) - const { width: imgWidth, height: imgHeight } = img - const ratio = Math.max(imgWidth / width, imgHeight / height) - // 不铺满,部分留白 - const imgShowWidth = (imgWidth / ratio) * 0.8 - const imgShowHeight = (imgHeight / ratio) * 0.8 - this.textShape = this.addShape('image', { - attrs: { - x: x + (imgShowWidth < width ? (width - imgShowWidth) / 2 : 0), - y: y + (imgShowHeight < height ? (height - imgShowHeight) / 2 : 0), - width: imgShowWidth, - height: imgShowHeight, - img - } - }) - } + drawImage.apply(this) } } /** @@ -75,9 +57,7 @@ export class TableInfo extends S2ChartView { 'alpha', 'tablePageMode', 'showHoverStyle', - 'autoWrap', - 'showSummary', - 'summaryLabel' + 'autoWrap' ], 'table-cell-selector': [ ...TABLE_EDITOR_PROPERTY_INNER['table-cell-selector'], @@ -85,7 +65,8 @@ export class TableInfo extends S2ChartView { 'tableColumnFreezeHead', 'tableRowFreezeHead', 'mergeCells' - ] + ], + 'summary-selector': ['showSummary', 'summaryLabel'] } axis: AxisType[] = ['xAxis', 'filter', 'drill'] axisConfig: AxisConfig = { @@ -140,7 +121,7 @@ export class TableInfo extends S2ChartView { if (value === null || value === undefined) { return value } - if (![2, 3].includes(f.deType) || !isNumber(value)) { + if (![2, 3, 4].includes(f.deType) || !isNumber(value)) { return value } let formatCfg = f.formatterCfg @@ -215,37 +196,6 @@ export class TableInfo extends S2ChartView { s2Options.frozenColCount = tableCell.tableColumnFreezeHead ?? 0 s2Options.frozenRowCount = tableCell.tableRowFreezeHead ?? 0 } - // 开启序号之后,第一列就是序号列,修改 label 即可 - if (s2Options.showSeriesNumber) { - let indexLabel = tableHeader.indexLabel - if (!indexLabel) { - indexLabel = '' - } - s2Options.layoutCoordinate = (_, __, col) => { - if (col.colIndex === 0 && col.rowIndex === 0) { - col.label = indexLabel - col.value = indexLabel - } - } - } - s2Options.dataCell = viewMeta => { - const field = fields.filter(f => f.dataeaseName === viewMeta.valueField)?.[0] - if (field?.deType === 7 && chart.showPosition !== 'dialog') { - return new ImageCell(viewMeta, viewMeta?.spreadsheet) - } - if (viewMeta.colIndex === 0 && s2Options.showSeriesNumber) { - if (tableCell.mergeCells) { - viewMeta.fieldValue = getRowIndex(s2Options.mergedCellsInfo, viewMeta) - } else { - viewMeta.fieldValue = - pageInfo.pageSize * (pageInfo.currentPage - 1) + viewMeta.rowIndex + 1 - } - } - // 配置文本自动换行参数 - viewMeta.autoWrap = tableCell.mergeCells ? false : basicStyle.autoWrap - viewMeta.maxLines = basicStyle.maxLines - return new CustomDataCell(viewMeta, viewMeta?.spreadsheet) - } // tooltip this.configTooltip(chart, s2Options) // 合并单元格 @@ -274,12 +224,12 @@ export class TableInfo extends S2ChartView { return new CustomTableColCell(node, sheet, config) } } - // 总计 - configSummaryRow(chart, s2Options, newData, tableHeader, basicStyle, basicStyle.showSummary) + // 序列号和总计行 + this.configSummaryRowAndIndex(chart, pageInfo, s2Options, s2DataConfig) // 开始渲染 const newChart = new TableSheet(containerDom, s2DataConfig, s2Options) // 总计紧贴在单元格后面 - summaryRowStyle(newChart, newData, tableCell, tableHeader, basicStyle.showSummary) + this.summaryRowStyle(newChart, newData, tableCell, tableHeader, basicStyle.showSummary) // 开启自动换行 if (basicStyle.autoWrap && !tableCell.mergeCells) { // 调整表头宽度时,计算表头高度 @@ -417,13 +367,13 @@ export class TableInfo extends S2ChartView { newChart.on(S2Event.COL_CELL_HOVER, event => this.showTooltip(newChart, event, meta)) newChart.on(S2Event.DATA_CELL_HOVER, event => this.showTooltip(newChart, event, meta)) newChart.on(S2Event.MERGED_CELLS_HOVER, event => this.showTooltip(newChart, event, meta)) + // touch + this.configTouchEvent(newChart, drawOption, meta) } // header resize newChart.on(S2Event.LAYOUT_RESIZE_COL_WIDTH, ev => resizeAction(ev)) // right click newChart.on(S2Event.GLOBAL_CONTEXT_MENU, event => copyContent(newChart, event, meta)) - // touch - this.configTouchEvent(newChart, drawOption, meta) // theme const customTheme = this.configTheme(chart) newChart.setThemeCfg({ theme: customTheme }) @@ -444,6 +394,11 @@ export class TableInfo extends S2ChartView { const fontStyle = tableCell.isItalic ? 'italic' : 'normal' const fontWeight = tableCell.isBolder === false ? 'normal' : 'bold' const mergeCellTheme: S2Theme = { + dataCell: { + cell: { + crossBackgroundColor: tableItemBgColor + } + }, mergedCell: { cell: { backgroundColor: tableItemBgColor, @@ -488,6 +443,92 @@ export class TableInfo extends S2ChartView { return theme } + protected configSummaryRowAndIndex( + chart: Chart, + pageInfo: PageInfo, + s2Options: S2Options, + s2DataConfig: S2DataConfig + ) { + const { tableHeader, basicStyle, tableCell } = parseJson(chart.customAttr) + const fields = chart.data?.fields ?? [] + // 开启序号之后,第一列就是序号列,修改 label 即可 + if (s2Options.showSeriesNumber) { + let indexLabel = tableHeader.indexLabel + if (!indexLabel) { + indexLabel = '' + } + s2Options.layoutCoordinate = (_, __, col) => { + if (col.colIndex === 0 && col.rowIndex === 0) { + col.label = indexLabel + col.value = indexLabel + } + } + } + const { showSummary, summaryLabel } = basicStyle + const data = s2DataConfig.data + const xAxis = chart.xAxis + if (showSummary && data?.length) { + // 设置汇总行高度和表头一致 + const heightByField = {} + heightByField[data.length] = tableHeader.tableTitleHeight + s2Options.style.rowCfg = { heightByField } + // 计算汇总加入到数据里,冻结最后一行 + s2Options.frozenTrailingRowCount = 1 + const axis = filter(xAxis, axis => [2, 3, 4].includes(axis.deType)) + const summaryObj = getSummaryRow(data, axis, basicStyle.seriesSummary) as any + data.push(summaryObj) + } + s2Options.dataCell = viewMeta => { + // 总计行处理 + if (showSummary && viewMeta.rowIndex === data.length - 1) { + if (viewMeta.colIndex === 0) { + if (tableHeader.showIndex) { + viewMeta.fieldValue = summaryLabel ?? t('chart.total_show') + } else { + // 第一列不是数值类型的,显示总计 + if (![2, 3, 4].includes(xAxis?.[0]?.deType)) { + viewMeta.fieldValue = summaryLabel ?? t('chart.total_show') + } + } + } + return new SummaryCell(viewMeta, viewMeta?.spreadsheet) + } + const field = fields.find(f => f.dataeaseName === viewMeta.valueField) + if (field?.deType === 7 && chart.showPosition !== 'dialog') { + return new ImageCell(viewMeta, viewMeta?.spreadsheet) + } + if (viewMeta.colIndex === 0 && s2Options.showSeriesNumber) { + if (tableCell.mergeCells) { + viewMeta.fieldValue = getRowIndex(s2Options.mergedCellsInfo, viewMeta) + } else { + viewMeta.fieldValue = + pageInfo.pageSize * (pageInfo.currentPage - 1) + viewMeta.rowIndex + 1 + } + } + // 配置文本自动换行参数 + viewMeta.autoWrap = tableCell.mergeCells ? false : basicStyle.autoWrap + viewMeta.maxLines = basicStyle.maxLines + return new CustomDataCell(viewMeta, viewMeta?.spreadsheet) + } + } + + protected summaryRowStyle(newChart: TableSheet, newData, tableCell, tableHeader, showSummary) { + if (!showSummary || !newData.length) return + const columns = newChart.dataCfg.fields.columns + const showHeader = tableHeader.showTableHeader === true + // 不显示表头时,减少一个表头的高度 + const headerAndSummaryHeight = showHeader ? getMaxTreeDepth(columns) + 1 : 1 + newChart.on(S2Event.LAYOUT_BEFORE_RENDER, () => { + const totalHeight = + tableHeader.tableTitleHeight * headerAndSummaryHeight + + tableCell.tableItemHeight * (newData.length - 1) + if (totalHeight < newChart.container.cfg.height) { + newChart.options.height = + totalHeight < newChart.container.cfg.height - 8 ? totalHeight + 8 : totalHeight + } + }) + } + constructor() { super('table-info', []) } @@ -508,3 +549,17 @@ function getStartPosition(node) { } return getStartPosition(node.children[0]) } + +function getMaxTreeDepth(nodes) { + if (!nodes?.length) { + return 0 + } + return Math.max( + ...nodes.map(node => { + if (!node.children?.length) { + return 1 + } + return getMaxTreeDepth(node.children) + 1 + }) + ) +} diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/table/table-normal.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/table/table-normal.ts index ac78e09..2cf89ee 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/table/table-normal.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/table/table-normal.ts @@ -2,10 +2,11 @@ import { useI18n } from '@/hooks/web/useI18n' import { formatterItem, valueFormatter } from '@/views/chart/components/js/formatter' import { configEmptyDataStyle, - configSummaryRow, copyContent, + CustomDataCell, + getSummaryRow, SortTooltip, - summaryRowStyle + SummaryCell } from '@/views/chart/components/js/panel/common/common_table' import { S2ChartView, S2DrawOptions } from '@/views/chart/components/js/panel/types/impl/s2' import { parseJson } from '@/views/chart/components/js/util' @@ -19,7 +20,7 @@ import { TableSheet, ViewMeta } from '@antv/s2' -import { cloneDeep, isNumber } from 'lodash-es' +import { isNumber } from 'lodash-es' import { TABLE_EDITOR_PROPERTY, TABLE_EDITOR_PROPERTY_INNER } from './common' const { t } = useI18n() @@ -37,8 +38,7 @@ export class TableNormal extends S2ChartView { ], 'basic-style-selector': [ ...TABLE_EDITOR_PROPERTY_INNER['basic-style-selector'], - 'showSummary', - 'summaryLabel', + 'tablePageMode', 'showHoverStyle' ], 'table-cell-selector': [ @@ -46,7 +46,8 @@ export class TableNormal extends S2ChartView { 'tableFreeze', 'tableColumnFreezeHead', 'tableRowFreezeHead' - ] + ], + 'summary-selector': ['showSummary', 'summaryLabel'] } axis: AxisType[] = ['xAxis', 'yAxis', 'drill', 'filter'] axisConfig: AxisConfig = { @@ -66,7 +67,7 @@ export class TableNormal extends S2ChartView { } drawChart(drawOption: S2DrawOptions): TableSheet { - const { container, chart, action, resizeAction } = drawOption + const { container, chart, action, pageInfo, resizeAction } = drawOption const containerDom = document.getElementById(container) if (!containerDom) return @@ -112,7 +113,7 @@ export class TableNormal extends S2ChartView { if (value === null || value === undefined) { return value } - if (![2, 3].includes(f.deType) || !isNumber(value)) { + if (![2, 3, 4].includes(f.deType) || !isNumber(value)) { return value } let formatCfg = f.formatterCfg @@ -160,19 +161,6 @@ export class TableNormal extends S2ChartView { s2Options.frozenColCount = tableCell.tableColumnFreezeHead ?? 0 s2Options.frozenRowCount = tableCell.tableRowFreezeHead ?? 0 } - // 开启序号之后,第一列就是序号列,修改 label 即可 - if (s2Options.showSeriesNumber) { - let indexLabel = tableHeader.indexLabel - if (!indexLabel) { - indexLabel = '' - } - s2Options.layoutCoordinate = (_, __, col) => { - if (col.colIndex === 0 && col.rowIndex === 0) { - col.label = indexLabel - col.value = indexLabel - } - } - } // tooltip this.configTooltip(chart, s2Options) // 隐藏表头,保留顶部的分割线, 禁用表头横向 resize @@ -193,13 +181,12 @@ export class TableNormal extends S2ChartView { chart.container = container this.configHeaderInteraction(chart, s2Options) } - - // 总计 - configSummaryRow(chart, s2Options, newData, tableHeader, basicStyle, basicStyle.showSummary) + // 配置总计和序号列 + this.configSummaryRowAndIndex(chart, pageInfo, s2Options, s2DataConfig) // 开始渲染 const newChart = new TableSheet(containerDom, s2DataConfig, s2Options) // 总计紧贴在单元格后面 - summaryRowStyle(newChart, newData, tableCell, tableHeader, basicStyle.showSummary) + this.summaryRowStyle(newChart, newData, tableCell, tableHeader, basicStyle.showSummary) // 自适应铺满 if (basicStyle.tableColumnMode === 'adapt') { newChart.on(S2Event.LAYOUT_RESIZE_COL_WIDTH, () => { @@ -281,19 +268,86 @@ export class TableNormal extends S2ChartView { if (show) { newChart.on(S2Event.COL_CELL_HOVER, event => this.showTooltip(newChart, event, meta)) newChart.on(S2Event.DATA_CELL_HOVER, event => this.showTooltip(newChart, event, meta)) + // touch + this.configTouchEvent(newChart, drawOption, meta) } // header resize newChart.on(S2Event.LAYOUT_RESIZE_COL_WIDTH, ev => resizeAction(ev)) // right click newChart.on(S2Event.GLOBAL_CONTEXT_MENU, event => copyContent(newChart, event, meta)) - // touch - this.configTouchEvent(newChart, drawOption, meta) // theme const customTheme = this.configTheme(chart) newChart.setThemeCfg({ theme: customTheme }) return newChart } + + protected configSummaryRowAndIndex( + chart: Chart, + pageInfo: PageInfo, + s2Options: S2Options, + s2DataConfig: S2DataConfig + ) { + const { tableHeader, basicStyle } = parseJson(chart.customAttr) + // 开启序号之后,第一列就是序号列,修改 label 即可 + if (s2Options.showSeriesNumber) { + let indexLabel = tableHeader.indexLabel + if (!indexLabel) { + indexLabel = '' + } + s2Options.layoutCoordinate = (_, __, col) => { + if (col.colIndex === 0 && col.rowIndex === 0) { + col.label = indexLabel + col.value = indexLabel + } + } + } + const { showSummary, summaryLabel } = basicStyle + const data = s2DataConfig.data + const { xAxis, yAxis } = chart + if (showSummary && data?.length) { + // 设置汇总行高度和表头一致 + const heightByField = {} + heightByField[data.length] = tableHeader.tableTitleHeight + s2Options.style.rowCfg = { heightByField } + // 计算汇总加入到数据里,冻结最后一行 + s2Options.frozenTrailingRowCount = 1 + const summaryObj = getSummaryRow(data, yAxis, basicStyle.seriesSummary) as any + data.push(summaryObj) + } + s2Options.dataCell = viewMeta => { + // 总计行处理 + if (showSummary && viewMeta.rowIndex === data.length - 1) { + if (viewMeta.colIndex === 0) { + if (tableHeader.showIndex || xAxis?.length) { + viewMeta.fieldValue = summaryLabel ?? t('chart.total_show') + } + } + return new SummaryCell(viewMeta, viewMeta?.spreadsheet) + } + if (viewMeta.colIndex === 0 && s2Options.showSeriesNumber) { + viewMeta.fieldValue = pageInfo.pageSize * (pageInfo.currentPage - 1) + viewMeta.rowIndex + 1 + } + return new CustomDataCell(viewMeta, viewMeta?.spreadsheet) + } + } + + protected summaryRowStyle(newChart, newData, tableCell, tableHeader, showSummary) { + if (!showSummary || !newData.length) return + newChart.on(S2Event.LAYOUT_BEFORE_RENDER, () => { + const showHeader = tableHeader.showTableHeader === true + // 不显示表头时,减少一个表头的高度 + const headerAndSummaryHeight = showHeader ? 2 : 1 + const totalHeight = + tableHeader.tableTitleHeight * headerAndSummaryHeight + + tableCell.tableItemHeight * (newData.length - 1) + if (totalHeight < newChart.container.cfg.height) { + newChart.options.height = + totalHeight < newChart.container.cfg.height - 8 ? totalHeight + 8 : totalHeight + } + }) + } + constructor() { super('table-normal', []) } diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/table/table-pivot.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/table/table-pivot.ts index 1922e14..8979613 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/table/table-pivot.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/table/table-pivot.ts @@ -13,7 +13,8 @@ import { TotalStatus, Aggregation, S2DataConfig, - MergedCell + MergedCell, + LayoutResult } from '@antv/s2' import { formatterItem, valueFormatter } from '../../../formatter' import { hexColorToRGBA, isAlphaColor, parseJson } from '../../../util' @@ -91,7 +92,8 @@ export class TablePivot extends S2ChartView { 'showColTooltip', 'showRowTooltip', 'showHorizonBorder', - 'showVerticalBorder' + 'showVerticalBorder', + 'rowHeaderFreeze' ], 'table-total-selector': ['row', 'col'], 'basic-style-selector': [ @@ -100,7 +102,9 @@ export class TablePivot extends S2ChartView { 'tableScrollBarColor', 'alpha', 'tableLayoutMode', - 'showHoverStyle' + 'showHoverStyle', + 'quotaPosition', + 'quotaColLabel' ] } axis: AxisType[] = ['xAxis', 'xAxisExt', 'yAxis', 'filter'] @@ -162,7 +166,7 @@ export class TablePivot extends S2ChartView { if (value === null || value === undefined) { return value } - if (![2, 3].includes(f.deType) || !isNumber(value)) { + if (![2, 3, 4].includes(f.deType) || !isNumber(value)) { return value } if (f.formatterCfg) { @@ -175,7 +179,7 @@ export class TablePivot extends S2ChartView { }) // total config - const { basicStyle, tooltip, tableTotal } = parseJson(chart.customAttr) + const { basicStyle, tooltip, tableTotal, tableHeader } = parseJson(chart.customAttr) if (!tableTotal.row.subTotalsDimensionsNew || tableTotal.row.subTotalsDimensions == undefined) { tableTotal.row.subTotalsDimensions = r } @@ -183,6 +187,7 @@ export class TablePivot extends S2ChartView { // 解析合计、小计排序 const sortParams = [] + let rowTotalSort = false if ( tableTotal.row.totalSort && tableTotal.row.totalSort !== 'none' && @@ -190,16 +195,20 @@ export class TablePivot extends S2ChartView { tableTotal.row.showGrandTotals && v.indexOf(tableTotal.row.totalSortField) > -1 ) { - const sort = { - sortFieldId: c[0], - sortMethod: tableTotal.row.totalSort.toUpperCase(), - sortByMeasure: TOTAL_VALUE, - query: { - [EXTRA_FIELD]: tableTotal.row.totalSortField + c.forEach(i => { + const sort = { + sortFieldId: i, + sortMethod: tableTotal.row.totalSort.toUpperCase(), + sortByMeasure: TOTAL_VALUE, + query: { + [EXTRA_FIELD]: tableTotal.row.totalSortField + } } - } - sortParams.push(sort) + sortParams.push(sort) + }) + rowTotalSort = true } + let colTotalSort = false if ( tableTotal.col.totalSort && tableTotal.col.totalSort !== 'none' && @@ -207,15 +216,18 @@ export class TablePivot extends S2ChartView { tableTotal.col.showGrandTotals && v.indexOf(tableTotal.col.totalSortField) > -1 ) { - const sort = { - sortFieldId: r[0], - sortMethod: tableTotal.col.totalSort.toUpperCase(), - sortByMeasure: TOTAL_VALUE, - query: { - [EXTRA_FIELD]: tableTotal.col.totalSortField + r.forEach(i => { + const sort = { + sortFieldId: i, + sortMethod: tableTotal.col.totalSort.toUpperCase(), + sortByMeasure: TOTAL_VALUE, + query: { + [EXTRA_FIELD]: tableTotal.col.totalSortField + } } - } - sortParams.push(sort) + sortParams.push(sort) + }) + colTotalSort = true } //列维度为空,行排序按照指标列来排序,取第一个有排序设置的指标 if (!columnFields?.length) { @@ -244,9 +256,23 @@ export class TablePivot extends S2ChartView { col: chart.xAxisExt, quota: chart.yAxis } - //树形模式下,列维度为空,行小计会变成列总计,特殊处理下 - if (basicStyle.tableLayoutMode === 'tree' && !chart.xAxisExt?.length) { - tableTotal.col.calcTotals = tableTotal.row.calcSubTotals + // 沒有列维度需要特殊处理 + if (!chart.xAxisExt?.length) { + //树形模式下,列维度为空,行小计的配置会变成列总计 + if (basicStyle.tableLayoutMode === 'tree') { + tableTotal.col.calcTotals = tableTotal.row.calcSubTotals + if (!tableTotal.col.calcTotals.cfg?.length) { + tableTotal.col.calcTotals.cfg = chart.yAxis.map(y => { + return { + dataeaseName: y.dataeaseName, + aggregation: 'SUM' + } + }) + } + } else { + // 列总计设置为空 + tableTotal.col.calcTotals.calcFunc = () => '-' + } } totals.forEach(total => { if (total.cfg?.length) { @@ -262,12 +288,93 @@ export class TablePivot extends S2ChartView { }) // 空值处理 const newData = this.configEmptyDataStrategy(chart) + // 行列维度排序 + if (!rowTotalSort) { + c?.forEach((f, i) => { + if (valueFieldMap[f]?.sort === 'none') { + return + } + const sort = { + sortFieldId: f + } + const sortMethod = valueFieldMap[f]?.sort?.toUpperCase() + if (sortMethod === 'CUSTOM_SORT') { + sort.sortBy = valueFieldMap[f].customSort + } else { + if (i === 0) { + sort.sortMethod = sortMethod + } else { + const fieldValues = newData.map(item => item[f]) + const uniqueValues = [...new Set(fieldValues)] + + // 根据配置动态决定排序顺序 + uniqueValues.sort((a, b) => { + if ([2, 3, 4].includes(valueFieldMap[f]?.deType)) { + return sortMethod === 'ASC' ? a - b : b - a + } + if (!a && !b) { + return 0 + } + if (!a) { + return sortMethod === 'ASC' ? -1 : 1 + } + if (!b) { + return sortMethod === 'ASC' ? 1 : -1 + } + return sortMethod === 'ASC' ? a.localeCompare(b) : b.localeCompare(a) + }) + sort.sortBy = uniqueValues + } + } + sortParams.push(sort) + }) + } + if (!colTotalSort) { + r?.forEach((f, i) => { + if (valueFieldMap[f]?.sort === 'none') { + return + } + const sort = { + sortFieldId: f + } + const sortMethod = valueFieldMap[f]?.sort?.toUpperCase() + if (sortMethod === 'CUSTOM_SORT') { + sort.sortBy = valueFieldMap[f].customSort + } else { + if (i === 0) { + sort.sortMethod = sortMethod + } else { + const fieldValues = newData.map(item => item[f]) + const uniqueValues = [...new Set(fieldValues)] + // 根据配置动态决定排序顺序 + uniqueValues.sort((a, b) => { + if ([2, 3, 4].includes(valueFieldMap[f]?.deType)) { + return sortMethod === 'ASC' ? a - b : b - a + } + if (!a && !b) { + return 0 + } + if (!a) { + return sortMethod === 'ASC' ? -1 : 1 + } + if (!b) { + return sortMethod === 'ASC' ? 1 : -1 + } + return sortMethod === 'ASC' ? a.localeCompare(b) : b.localeCompare(a) + }) + sort.sortBy = uniqueValues + } + } + sortParams.push(sort) + }) + } // data config const s2DataConfig: S2DataConfig = { fields: { rows: r, columns: c, - values: v + values: v, + valueInCols: !(basicStyle.quotaPosition === 'row') }, meta: meta, data: newData, @@ -277,6 +384,7 @@ export class TablePivot extends S2ChartView { width: containerDom.offsetWidth, height: containerDom.offsetHeight, totals: tableTotal as Totals, + cornerExtraFieldText: basicStyle.quotaColLabel ?? t('dataset.value'), conditions: this.configConditions(chart), tooltip: { getContainer: () => containerDom @@ -288,21 +396,204 @@ export class TablePivot extends S2ChartView { }, dataCell: meta => { return new CustomDataCell(meta, meta.spreadsheet) - } + }, + frozenRowHeader: !(tableHeader.rowHeaderFreeze === false) } // options s2Options.style = this.configStyle(chart, s2DataConfig) - s2Options.style.hierarchyCollapse = true + // 默认展开层级 + if (basicStyle.tableLayoutMode === 'tree') { + const { defaultExpandLevel } = basicStyle + if (isNumber(defaultExpandLevel)) { + if (defaultExpandLevel >= chart.xAxis.length) { + s2Options.style.rowExpandDepth = defaultExpandLevel + } else { + s2Options.style.rowExpandDepth = defaultExpandLevel - 2 + } + } + if (defaultExpandLevel === 'all') { + s2Options.style.rowExpandDepth = chart.xAxis.length + } + if (!defaultExpandLevel) { + s2Options.style.hierarchyCollapse = true + } + } + // 列汇总别名 + if (!(basicStyle.quotaPosition === 'row' && basicStyle.tableLayoutMode === 'tree')) { + if ( + basicStyle.quotaPosition !== 'row' && + chart.xAxisExt?.length && + chart.yAxis?.length > 1 && + tableTotal.col.showGrandTotals && + tableTotal.col.calcTotals?.cfg?.length + ) { + const colTotalCfgMap = tableTotal.col.calcTotals.cfg.reduce((p, n) => { + p[n.dataeaseName] = n + return p + }, {}) + s2Options.layoutCoordinate = (_, __, col) => { + if (col?.isGrandTotals) { + if (colTotalCfgMap[col.value]?.label) { + col.label = colTotalCfgMap[col.value].label + } + } + } + } + if ( + basicStyle.quotaPosition === 'row' && + chart.xAxisExt?.length && + chart.yAxis?.length > 1 && + tableTotal.row.showGrandTotals && + tableTotal.row.calcTotals?.cfg?.length + ) { + const rowTotalCfgMap = tableTotal.row.calcTotals.cfg.reduce((p, n) => { + p[n.dataeaseName] = n + return p + }, {}) + // eslint-disable-next-line + s2Options.layoutCoordinate = (_, row, __) => { + if (row?.isGrandTotals) { + if (rowTotalCfgMap[row.value]?.label) { + row.label = rowTotalCfgMap[row.value].label + } + } + } + } + } // tooltip this.configTooltip(chart, s2Options) // 开始渲染 const s2 = new PivotSheet(containerDom, s2DataConfig, s2Options as unknown as S2Options) + // 自适应铺满 + if (basicStyle.tableColumnMode === 'adapt') { + s2.on(S2Event.LAYOUT_RESIZE_COL_WIDTH, () => { + s2.store.set('lastLayoutResult', s2.facet.layoutResult) + }) + // 平铺模式行头resize + s2.on(S2Event.LAYOUT_RESIZE_ROW_WIDTH, () => { + s2.store.set('lastLayoutResult', s2.facet.layoutResult) + }) + // 树形模式行头resize + s2.on(S2Event.LAYOUT_RESIZE_TREE_WIDTH, () => { + s2.store.set('lastLayoutResult', s2.facet.layoutResult) + }) + s2.on(S2Event.LAYOUT_AFTER_HEADER_LAYOUT, (ev: LayoutResult) => { + const lastLayoutResult = s2.store.get('lastLayoutResult') as LayoutResult + if (lastLayoutResult) { + // 拖动 col 表头 resize + const colWidthByFieldValue = s2.options.style?.colCfg?.widthByFieldValue + // 平铺模式拖动 row 表头 resize + const rowWidthByField = s2.options.style?.rowCfg?.widthByField + // 树形模式拖动 row 表头 resize + const treeRowWidth = + s2.options.style?.treeRowsWidth || lastLayoutResult.rowsHierarchy.width + const colWidthMap = + lastLayoutResult.colLeafNodes.reduce((p, n) => { + p[n.id] = colWidthByFieldValue?.[n.value] ?? n.width + return p + }, {}) || {} + const totalColWidth = ev.colLeafNodes.reduce((p, n) => { + n.width = colWidthMap[n.id] || n.width + n.x = p + return p + n.width + }, 0) + ev.colNodes.forEach(n => { + if (n.isLeaf) { + return + } + n.width = this.getColWidth(n) + n.x = this.getLeftChild(n).x + }) + if (basicStyle.tableLayoutMode === 'tree') { + ev.rowNodes.forEach(n => { + n.width = treeRowWidth + }) + ev.rowsHierarchy.width = treeRowWidth + ev.colsHierarchy.width = totalColWidth + } else { + const rowWidthMap = + lastLayoutResult.rowNodes.reduce((p, n) => { + p[n.id] = rowWidthByField?.[n.field] ?? n.width + return p + }, {}) || {} + ev.rowNodes.forEach(n => { + n.x = 0 + n.width = rowWidthMap[n.id] || n.width + let tmp = n + while (tmp.parent.id !== 'root') { + n.x += tmp.parent.width + tmp = tmp.parent + } + }) + const totlaRowWidth = ev.rowsHierarchy.sampleNodesForAllLevels.reduce((p, n) => { + return p + n.width + }, 0) + const maxRowLevel = ev.rowsHierarchy.maxLevel + ev.rowNodes.forEach(n => { + // 总计和中间层级的小计需要重新计算宽度 + if (n.isTotalRoot || (n.isSubTotals && n.level < maxRowLevel)) { + let width = 0 + for (let i = n.level; i <= maxRowLevel; i++) { + width += ev.rowsHierarchy.sampleNodesForAllLevels[i].width + } + n.width = width + } + }) + ev.rowsHierarchy.width = totlaRowWidth + ev.colsHierarchy.width = totalColWidth + } + s2.store.set('lastLayoutResult', undefined) + return + } + const containerWidth = containerDom.getBoundingClientRect().width + const scale = containerWidth / (ev.colsHierarchy.width + ev.rowsHierarchy.width) + if (scale <= 1) { + return + } + const totalRowWidth = Math.round(ev.rowsHierarchy.width * scale) + ev.rowNodes.forEach(n => { + n.width = Math.round(n.width * scale) + }) + if (basicStyle.tableLayoutMode !== 'tree') { + ev.rowNodes.forEach(n => { + n.x = 0 + let tmp = n + while (tmp.parent.id !== 'root') { + n.x += tmp.parent.width + tmp = tmp.parent + } + }) + } + let totalColWidth = ev.colLeafNodes.reduce((p, n) => { + n.width = Math.round(n.width * scale) + n.x = p + return p + n.width + }, 0) + ev.colNodes.forEach(n => { + if (n.isLeaf) { + return + } + n.width = this.getColWidth(n) + n.x = this.getLeftChild(n).x + }) + const totalWidth = totalColWidth + totalRowWidth + if (totalWidth > containerWidth) { + // 从最后一列减掉 + ev.colLeafNodes[ev.colLeafNodes.length - 1].width -= totalWidth - containerWidth + totalColWidth = totalColWidth - (totalWidth - containerWidth) + } + ev.colsHierarchy.width = totalColWidth + ev.rowsHierarchy.width = totalRowWidth + }) + } // tooltip const { show } = tooltip if (show) { s2.on(S2Event.COL_CELL_HOVER, event => this.showTooltip(s2, event, meta)) s2.on(S2Event.ROW_CELL_HOVER, event => this.showTooltip(s2, event, meta)) s2.on(S2Event.DATA_CELL_HOVER, event => this.showTooltip(s2, event, meta)) + // touch + this.configTouchEvent(s2, drawOption, meta) } // empty data tip configEmptyDataStyle(s2, newData) @@ -312,14 +603,29 @@ export class TablePivot extends S2ChartView { s2.on(S2Event.COL_CELL_CLICK, ev => this.headerCellClickAction(chart, ev, s2, action)) // right click s2.on(S2Event.GLOBAL_CONTEXT_MENU, event => copyContent(s2, event, meta)) - // touch - this.configTouchEvent(s2, drawOption, meta) // theme const customTheme = this.configTheme(chart) s2.setThemeCfg({ theme: customTheme }) return s2 } + private getColWidth(node) { + let width = 0 + if (node.children?.length) { + node.children.forEach(child => { + width += this.getColWidth(child) + }) + } else { + width = node.width + } + return width + } + private getLeftChild(node) { + if (!node.children?.length) { + return node + } + return this.getLeftChild(node.children[0]) + } private dataCellClickAction(chart: Chart, ev, s2Instance: PivotSheet, callback) { const cell = s2Instance.getCell(ev.target) const meta = cell.getMeta() @@ -522,7 +828,7 @@ export class TablePivot extends S2ChartView { } function customCalcFunc(query, data, status, chart, totalCfgMap, axisMap, customCalc) { if (!data?.length || !query[EXTRA_FIELD]) { - return 0 + return '-' } const aggregation = totalCfgMap[query[EXTRA_FIELD]]?.aggregation || 'SUM' switch (aggregation) { @@ -549,10 +855,13 @@ function customCalcFunc(query, data, status, chart, totalCfgMap, axisMap, custom }) return result?.[query[EXTRA_FIELD]] } + case 'NONE': { + return '-' + } case 'CUSTOM': { const val = getCustomCalcResult(query, axisMap, chart, status, customCalc || {}) - if (val === '') { - return val + if (val === '' || val === undefined) { + return '-' } return parseFloat(val) } @@ -593,11 +902,17 @@ function getTreeCustomCalcResult(query, axisMap, status: TotalStatus, customCalc // 列小计 if (status.isColSubTotal && !status.isRowTotal && !status.isRowSubTotal) { const { colSubTotal } = customCalc - const subLevel = getSubLevel(query, col) + const subColLevel = getSubLevel(query, col) + const subRowLevel = getSubLevel(query, row) const rowPath = getTreePath(query, row) const colPath = getTreePath(query, col) const path = [...rowPath, ...colPath] - const data = colSubTotal?.[subLevel]?.data + let data = colSubTotal?.[subColLevel]?.data + // 列小计里面的行小计 + if (rowPath.length < row.length) { + const { rowSubInColSub } = customCalc + data = rowSubInColSub?.[subRowLevel]?.[subColLevel]?.data + } let val if (path.length && data) { path.push(quotaField) @@ -647,7 +962,7 @@ function getTreeCustomCalcResult(query, axisMap, status: TotalStatus, customCalc if (status.isRowTotal && status.isColSubTotal) { const { colSubInRowTotal } = customCalc const colLevel = getSubLevel(query, col) - const { data } = colSubInRowTotal?.[colLevel] + const data = colSubInRowTotal?.[colLevel]?.data const colPath = getTreePath(query, col) let val if (colPath.length && colSubInRowTotal) { @@ -669,23 +984,7 @@ function getTreeCustomCalcResult(query, axisMap, status: TotalStatus, customCalc } return val } - // 列小计里面的行小计 - if (status.isColSubTotal && status.isRowSubTotal) { - const { rowSubInColSub } = customCalc - const rowSubLevel = getSubLevel(query, row) - const colSubLevel = getSubLevel(query, col) - const data = rowSubInColSub?.[rowSubLevel]?.[colSubLevel]?.data - const rowPath = getTreePath(query, row) - const colPath = getTreePath(query, col) - const path = [...rowPath, ...colPath] - let val - if (path.length && rowSubInColSub) { - path.push(quotaField) - val = get(data, path) - } - return val - } - return NaN + return '-' } function getGridCustomCalcResult(query, axisMap, status: TotalStatus, customCalc) { @@ -759,7 +1058,7 @@ function getGridCustomCalcResult(query, axisMap, status: TotalStatus, customCalc if (status.isRowTotal && status.isColSubTotal) { const { colSubInRowTotal } = customCalc const colLevel = getSubLevel(query, col) - const { data } = colSubInRowTotal?.[colLevel] + const data = colSubInRowTotal?.[colLevel]?.data const colPath = getTreePath(query, col) let val if (colPath.length && colSubInRowTotal) { diff --git a/core/core-frontend/src/views/chart/components/js/panel/common/common_antv.ts b/core/core-frontend/src/views/chart/components/js/panel/common/common_antv.ts index 7585882..f38b052 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/common/common_antv.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/common/common_antv.ts @@ -33,10 +33,23 @@ import { PositionType } from '@antv/l7-core' import { centroid } from '@turf/centroid' import type { Plot } from '@antv/g2plot' import type { PickOptions } from '@antv/g2plot/lib/core/plot' -import { defaults } from 'lodash-es' +import { defaults, find } from 'lodash-es' import { useI18n } from '@/hooks/web/useI18n' -const { t: tI18n } = useI18n() import { isMobile } from '@/utils/utils' +import { GaodeMap, TMap, TencentMap } from '@antv/l7-maps' +import { + gaodeMapStyleOptions, + qqMapStyleOptions, + tdtMapStyleOptions +} from '@/views/chart/components/js/panel/charts/map/common' +import ChartCarouselTooltip, { + isPie, + isColumn, + isMix, + isSupport +} from '@/views/chart/components/js/g2plot_tooltip_carousel' + +const { t: tI18n } = useI18n() export function getPadding(chart: Chart): number[] { if (chart.drill) { @@ -137,14 +150,22 @@ export function getTheme(chart: Chart) { }, 'g2-tooltip-list-item': { display: 'flex', - 'align-items': 'center' + 'align-items': 'flex-start', + 'justify-content': 'space-between', + 'line-height': tooltipFontsize + 'px' }, 'g2-tooltip-name': { display: 'inline-block', - 'line-height': tooltipFontsize + 'px', - flex: 1 + 'line-height': tooltipFontsize + 'px' + }, + 'g2-tooltip-value': { + flex: 1, + display: 'inline-block', + 'text-align': 'end', + 'line-height': tooltipFontsize + 'px' }, 'g2-tooltip-marker': { + 'margin-top': (tooltipFontsize - 8) / 2 + 'px', 'min-width': '8px', 'min-height': '8px' } @@ -469,7 +490,8 @@ export function getXAxis(chart: Chart) { style: { fill: a.axisLabel.color, fontSize: a.axisLabel.fontSize, - textAlign: textAlign + textAlign: textAlign, + fontFamily: chart.fontFamily }, formatter: value => { return chart.type === 'bidirectional-bar' && value.length > a.axisLabel.lengthLimit @@ -574,7 +596,8 @@ export function getYAxis(chart: Chart) { fill: yAxis.axisLabel.color, fontSize: yAxis.axisLabel.fontSize, textBaseline, - textAlign + textAlign, + fontFamily: chart.fontFamily }, formatter: value => { return value.length > yAxis.axisLabel.lengthLimit @@ -603,7 +626,7 @@ export function getYAxisExt(chart: Chart) { return false } const title = - yAxis.name && yAxis.name !== '' + yAxis.nameShow && yAxis.name && yAxis.name !== '' ? { text: yAxis.name, style: { @@ -629,14 +652,16 @@ export function getYAxisExt(chart: Chart) { ? { style: { stroke: axisCfg.lineStyle.color, - lineWidth: axisCfg.lineStyle.width + lineWidth: axisCfg.lineStyle.width, + lineDash: getLineDash(axisCfg.lineStyle.style) } } : null const tickLine = axisCfg.show ? { style: { - stroke: axisCfg.lineStyle.color + stroke: axisCfg.lineStyle.color, + lineWidth: axisCfg.lineStyle.width } } : null @@ -673,7 +698,8 @@ export function getYAxisExt(chart: Chart) { fill: yAxis.axisLabel.color, fontSize: yAxis.axisLabel.fontSize, textBaseline, - textAlign + textAlign, + fontFamily: chart.fontFamily } } : null @@ -821,10 +847,9 @@ export function getAnalyseHorizontal(chart: Chart) { const assistLineArr = senior.assistLineCfg.assistLine if (assistLineArr?.length > 0) { const customStyle = parseJson(chart.customStyle) - let xAxisPosition, axisFormatterCfg + let axisFormatterCfg if (customStyle.xAxis) { const a = JSON.parse(JSON.stringify(customStyle.xAxis)) - xAxisPosition = transAxisPosition(a.position) axisFormatterCfg = a.axisLabelFormatter ? a.axisLabelFormatter : DEFAULT_XAXIS_STYLE.axisLabelFormatter @@ -894,7 +919,9 @@ export function getLineDash(type) { */ export function setGradientColor(rawColor: string, show = false, angle = 0, start = 0) { const item = rawColor.split(',') - item.splice(3, 1, '0.3)') + const alpha = parseFloat(item[3].replace(')', '')) + const startAlpha = alpha * 0.3 + item.splice(3, 1, `${startAlpha})`) let color: string if (start == 0) { color = `l(${angle}) 0:${item.join(',')} 1:${rawColor}` @@ -993,6 +1020,9 @@ export function configL7Tooltip(chart: Chart): TooltipOptions { return result } const head = originalItem.properties + if (!head) { + return result + } const formatter = formatterMap[head.quotaList?.[0]?.id] if (!isEmpty(formatter)) { const originValue = parseFloat(head.value as string) @@ -1152,13 +1182,27 @@ export class CustomZoom extends Zoom { 'l7-button-control', container, () => { - if (this.controlOption['bounds']) { - this.mapsService.fitBounds(this.controlOption['bounds'], { animate: true }) + if (this.mapsService.map?.deMapProvider == 'qq') { + if (this.mapsService.map.deMapAutoFit) { + this.mapsService.setZoomAndCenter(this.mapsService.map.deMapAutoZoom, [ + this.mapsService.map.deMapAutoLng, + this.mapsService.map.deMapAutoLat + ]) + } else { + this.mapsService.setZoomAndCenter( + this.controlOption['initZoom'], + this.controlOption['center'] + ) + } } else { - this.mapsService.setZoomAndCenter( - this.controlOption['initZoom'], - this.controlOption['center'] - ) + if (this.controlOption['bounds']) { + this.mapsService.fitBounds(this.controlOption['bounds'], { animate: true }) + } else { + this.mapsService.setZoomAndCenter( + this.controlOption['initZoom'], + this.controlOption['center'] + ) + } } } ) @@ -1208,7 +1252,11 @@ export class CustomZoom extends Zoom { } as IZoomControlOption } } -export function configL7Zoom(chart: Chart, scene: Scene) { +export function configL7Zoom( + chart: Chart, + scene: Scene, + mapKey?: { key: string; securityCode: string; mapType: string } +) { const { basicStyle } = parseJson(chart.customAttr) const zoomOption = scene?.getControlByName('zoom') if (zoomOption) { @@ -1220,20 +1268,56 @@ export function configL7Zoom(chart: Chart, scene: Scene) { if (!scene?.getControlByName('zoom')) { if (!scene.map) { scene.once('loaded', () => { - scene.map.on('complete', () => { - const initZoom = basicStyle.autoFit === false ? basicStyle.zoomLevel : scene.getZoom() - const center = - basicStyle.autoFit === false - ? [basicStyle.mapCenter.longitude, basicStyle.mapCenter.latitude] - : [scene.map.getCenter().lng, scene.map.getCenter().lat] - const newZoomOptions = { - initZoom: initZoom, - center: center, - buttonColor: basicStyle.zoomButtonColor, - buttonBackground: basicStyle.zoomBackground - } as any - scene.addControl(new CustomZoom(newZoomOptions)) - }) + switch (mapKey?.mapType) { + case 'tianditu': + //天地图 + { + const initZoom = basicStyle.autoFit === false ? basicStyle.zoomLevel : scene.getZoom() + const center = + basicStyle.autoFit === false + ? [basicStyle.mapCenter.longitude, basicStyle.mapCenter.latitude] + : [scene.map.getCenter().getLng(), scene.map.getCenter().getLat()] + const newZoomOptions = { + initZoom: initZoom, + center: center, + buttonColor: basicStyle.zoomButtonColor, + buttonBackground: basicStyle.zoomBackground + } as any + scene.addControl(new CustomZoom(newZoomOptions)) + } + break + case 'qq': + { + const initZoom = basicStyle.autoFit === false ? basicStyle.zoomLevel : scene.getZoom() + const center = + basicStyle.autoFit === false + ? [basicStyle.mapCenter.longitude, basicStyle.mapCenter.latitude] + : [scene.map.getCenter().lng, scene.map.getCenter().lat] + const newZoomOptions = { + initZoom: initZoom, + center: center, + buttonColor: basicStyle.zoomButtonColor, + buttonBackground: basicStyle.zoomBackground + } as any + scene.addControl(new CustomZoom(newZoomOptions)) + } + break + default: + scene.map.on('complete', () => { + const initZoom = basicStyle.autoFit === false ? basicStyle.zoomLevel : scene.getZoom() + const center = + basicStyle.autoFit === false + ? [basicStyle.mapCenter.longitude, basicStyle.mapCenter.latitude] + : [scene.map.getCenter().lng, scene.map.getCenter().lat] + const newZoomOptions = { + initZoom: initZoom, + center: center, + buttonColor: basicStyle.zoomButtonColor, + buttonBackground: basicStyle.zoomBackground + } as any + scene.addControl(new CustomZoom(newZoomOptions)) + }) + } }) } else { const newZoomOptions = { @@ -1335,6 +1419,18 @@ export function mapRendering(dom: HTMLElement | string) { dom.classList.add('de-map-rendering') } +export function qqMapRendered(scene?: Scene) { + if (scene?.map && scene.map.deMapProvider === 'qq') { + setTimeout(() => { + if (scene.map) { + scene.map.deMapAutoZoom = scene.map.getZoom() + scene.map.deMapAutoLng = scene.map.getCenter().getLng() + scene.map.deMapAutoLat = scene.map.getCenter().getLat() + } + }, 1000) + } +} + export function mapRendered(dom: HTMLElement | string) { if (typeof dom === 'string') { dom = document.getElementById(dom) @@ -1342,6 +1438,213 @@ export function mapRendered(dom: HTMLElement | string) { dom.classList.add('de-map-rendered') } +export function getMapCenter(basicStyle: ChartBasicStyle) { + let center: [number, number] + if (basicStyle.autoFit === false) { + const longitude = basicStyle?.mapCenter?.longitude ?? DEFAULT_BASIC_STYLE.mapCenter.longitude + const latitude = basicStyle?.mapCenter?.latitude ?? DEFAULT_BASIC_STYLE.mapCenter.latitude + center = [longitude, latitude] + } else { + center = undefined + } + return center +} + +export function getMapStyle( + mapKey: { key: string; securityCode: string; mapType: string }, + basicStyle: ChartBasicStyle +) { + let mapStyle: string + switch (mapKey.mapType) { + case 'tianditu': + if (!find(tdtMapStyleOptions, s => s.value === basicStyle.mapStyle)) { + mapStyle = 'normal' + } else { + mapStyle = basicStyle.mapStyle + } + break + case 'qq': + if ( + !find(qqMapStyleOptions, s => s.value === basicStyle.mapStyle) || + basicStyle.mapStyle === 'normal' + ) { + mapStyle = 'normal' + } else { + mapStyle = basicStyle.mapStyleUrl + } + break + default: + if (!find(gaodeMapStyleOptions, s => s.value === basicStyle.mapStyle)) { + basicStyle.mapStyle = 'normal' + } + mapStyle = basicStyle.mapStyleUrl + if (basicStyle.mapStyle !== 'custom') { + mapStyle = `amap://styles/${basicStyle.mapStyle ? basicStyle.mapStyle : 'normal'}` + } + break + } + return mapStyle +} + +export async function getMapScene( + chart: Chart, + scene: Scene, + container: string, + mapKey: { key: string; securityCode: string; mapType: string }, + basicStyle: ChartBasicStyle, + miscStyle: ChartMiscAttr, + mapStyle: string, + center?: [number, number] +) { + if (!scene) { + scene = new Scene({ + id: container, + logoVisible: false, + map: getMapObject(mapKey, basicStyle, miscStyle, mapStyle, center) + }) + } else { + if (mapKey.mapType === 'tianditu') { + scene.map?.checkResize() + } + if (scene.getLayers()?.length) { + await scene.removeAllLayer() + try { + scene.setPitch(miscStyle.mapPitch) + } catch (e) {} + if (mapKey.mapType === 'tianditu') { + if (mapStyle === 'normal') { + scene.map?.removeStyle() + } else { + scene.setMapStyle(mapStyle) + } + } else { + scene.setMapStyle(mapStyle) + } + + scene.map.showLabel = !(basicStyle.showLabel === false) + if (mapKey.mapType === 'qq') { + scene.map.setBaseMap({ + //底图设置(参数为:VectorBaseMap对象) + type: 'vector', //类型:失量底图 + features: basicStyle.showLabel === false ? ['base', 'building2d'] : undefined + //仅渲染:道路及底面(base) + 2d建筑物(building2d),以达到隐藏文字的效果 + }) + } + } + if (basicStyle.autoFit === false) { + scene.setZoomAndCenter(basicStyle.zoomLevel, center) + if (mapKey.mapType === 'qq') { + scene.map.deMapAutoFit = false + scene.map.deMapZoom = basicStyle.zoomLevel + scene.map.deMapCenter = center + } + } + } + mapRendering(container) + scene.once('loaded', () => { + mapRendered(container) + if (mapKey.mapType === 'qq') { + scene.map.setBaseMap({ + //底图设置(参数为:VectorBaseMap对象) + type: 'vector', //类型:失量底图 + features: basicStyle.showLabel === false ? ['base', 'building2d'] : undefined + //仅渲染:道路及底面(base) + 2d建筑物(building2d),以达到隐藏文字的效果 + }) + scene.setMapStyle(mapStyle) + + scene.map.deMapProvider = 'qq' + scene.map.deMapAutoFit = !!basicStyle.autoFit + // scene.map.deMapAutoZoom = scene.map.getZoom() + // scene.map.deMapAutoLng = scene.map.getCenter().getLng() + // scene.map.deMapAutoLat = scene.map.getCenter().getLat() + } + // 去除天地图自己的缩放按钮 + if (mapKey.mapType === 'tianditu') { + if (mapStyle === 'normal') { + scene.map?.removeStyle() + } else { + scene.setMapStyle(mapStyle) + } + + const tdtControl = document.querySelector( + `#component${chart.id} .tdt-control-zoom.tdt-bar.tdt-control` + ) + if (tdtControl) { + tdtControl.style.display = 'none' + } + const tdtControlOuter = document.querySelectorAll( + `#wrapper-outer-id-${chart.id} .tdt-control-zoom.tdt-bar.tdt-control` + ) + if (tdtControlOuter && tdtControlOuter.length > 0) { + for (let i = 0; i < tdtControlOuter.length; i++) { + tdtControlOuter[i].style.display = 'none' + } + } + const tdtCopyrightControl = document.querySelector( + `#component${chart.id} .tdt-control-copyright.tdt-control` + ) + if (tdtCopyrightControl) { + tdtCopyrightControl.style.display = 'none' + } + const tdtCopyrightControlOuter = document.querySelectorAll( + `#wrapper-outer-id-${chart.id} .tdt-control-copyright.tdt-control` + ) + if (tdtCopyrightControlOuter && tdtCopyrightControlOuter.length > 0) { + for (let i = 0; i < tdtCopyrightControlOuter.length; i++) { + tdtCopyrightControlOuter[i].style.display = 'none' + } + } + } + }) + return scene +} + +export function getMapObject( + mapKey: { key: string; securityCode: string; mapType: string }, + basicStyle: ChartBasicStyle, + miscStyle: ChartMiscAttr, + mapStyle: string, + center?: [number, number] +) { + switch (mapKey.mapType) { + case 'tianditu': + return new TMap({ + token: mapKey?.key ?? undefined, + style: mapStyle, //不生效 + pitch: undefined, //不支持 + center, + zoom: basicStyle.autoFit === false ? basicStyle.zoomLevel : undefined, + showLabel: !(basicStyle.showLabel === false), //不支持 + WebGLParams: { + preserveDrawingBuffer: true + } + }) + case 'qq': + return new TencentMap({ + token: mapKey?.key ?? undefined, + style: mapStyle, + pitch: miscStyle.mapPitch, + center, + zoom: basicStyle.autoFit === false ? basicStyle.zoomLevel : 12, + showLabel: !(basicStyle.showLabel === false), + WebGLParams: { + preserveDrawingBuffer: true + } + }) + default: + return new GaodeMap({ + token: mapKey?.key ?? undefined, + style: mapStyle, + pitch: miscStyle.mapPitch, + center, + zoom: basicStyle.autoFit === false ? basicStyle.zoomLevel : undefined, + showLabel: !(basicStyle.showLabel === false), + WebGLParams: { + preserveDrawingBuffer: true + } + }) + } +} /** * 隐藏缩放控件 * @param basicStyle @@ -1358,6 +1661,8 @@ export function getTooltipContainer(id) { let wrapperDom = document.getElementById(G2_TOOLTIP_WRAPPER) if (!wrapperDom) { wrapperDom = document.createElement('div') + wrapperDom.style.position = 'absolute' + wrapperDom.style.zIndex = '9999' wrapperDom.id = G2_TOOLTIP_WRAPPER document.body.appendChild(wrapperDom) } @@ -1391,14 +1696,78 @@ export function getTooltipContainer(id) { } return g2Tooltip } + +/** + * 配置提示轮播 + * @param plot + * @param chart + */ +function configCarouselTooltip(plot, chart) { + const start = isSupport(chart.type) && !document.getElementById('multiplexingDrawer') + if (start) { + // 启用轮播 + plot.once('afterrender', () => { + const carousel = chart.customAttr?.tooltip?.carousel + ChartCarouselTooltip.manage(plot, chart, { + xField: 'field', + duration: carousel.enable ? carousel?.stayTime * 1000 : 2000, + interval: carousel.enable ? carousel?.intervalTime * 1000 : 2000 + }) + }) + } +} +/** + * 计算 Tooltip 的位置 + * @param {Chart} chart - 图表实例 + * @param {boolean} isCarousel - 是否为轮播模式 + * @param {object} tooltipCtl - Tooltip 控制器 + * @param {HTMLElement} chartElement - 图表元素 + * @param {Event} event - 事件对象 + * @param {boolean} enlargeElement - 放大弹窗 + * @returns {{x: number, y: number}} - 计算后的 x 和 y 坐标 + */ +function calculateTooltipPosition(chart, isCarousel, tooltipCtl, chartElement, event) { + // 辅助函数: 根据不同图表类型计算 Tooltip 的y位置 + const getTooltipY = () => { + const top = Number(chartElement.getBoundingClientRect().top) + if (isColumn(chart.type)) { + return top + chartElement.getBoundingClientRect().height / 2 + } + if (isMix(chart.type) || isPie(chart.type)) { + return top + tooltipCtl.point.y + } + return top + tooltipCtl.point.y + 60 + } + if (isCarousel) { + return { + x: tooltipCtl.point.x + Number(chartElement.getBoundingClientRect().left), + y: getTooltipY() + } + } else { + return { x: event.clientX, y: event.clientY } + } +} export function configPlotTooltipEvent>( chart: Chart, plot: P ) { const { tooltip } = parseJson(chart.customAttr) if (!tooltip.show) { + ChartCarouselTooltip.destroyByContainer(chart.container) return } + // 图表容器,用于计算 tooltip 的位置 + // 获取图表元素,优先顺序:放大 > 预览 > 公共连接页面 > 默认 + const chartElement = + document.getElementById('container-viewDialog-' + chart.id + '-common') || + document.getElementById('container-preview-' + chart.id + '-common') || + document.getElementById('enlarge-inner-content-' + chart.id) || + document.getElementById('shape-id-' + chart.id) + // 是否是放大弹窗 + const enlargeElement = chartElement?.id.includes('viewDialog') + // 轮播时tooltip的zIndex + const carousel_zIndex = enlargeElement ? '9999' : '1002' + configCarouselTooltip(plot, chart) // 鼠标可移入, 移入之后保持显示, 移出之后隐藏 plot.options.tooltip.container.addEventListener('mouseenter', e => { e.target.style.visibility = 'visible' @@ -1415,10 +1784,25 @@ export function configPlotTooltipEvent> if (!tooltipCtl) { return } + // 处理 tooltip 与下拉菜单的显示冲突问题 + const viewTrackBarElement = document.getElementById('view-track-bar-' + chart.id) const event = plot.chart.interactions.tooltip?.context?.event + // 是否时轮播模式 + const isCarousel = + chart.customAttr?.tooltip?.carousel && + (!event || // 事件触发时,使用event的client坐标 + ['plot:leave', 'plot:mouseleave'].includes(event?.type) || //鼠标离开时,使用tooltipCtl.point + ['pie', 'pie-rose', 'pie-donut'].includes(chart.type)) // 饼图时,使用tooltipCtl.point + plot.options.tooltip.showMarkers = isCarousel ? true : false + const wrapperDom = document.getElementById(G2_TOOLTIP_WRAPPER) + wrapperDom.style.zIndex = isCarousel && wrapperDom ? carousel_zIndex : '9999' if (tooltipCtl.tooltip) { // 处理视图放大后再关闭 tooltip 的 dom 被清除 const container = tooltipCtl.tooltip.cfg.container + // 当下拉菜单不显示时,移除tooltip的hidden-tooltip样式 + if (viewTrackBarElement?.getAttribute('aria-expanded') === 'false') { + container.classList.toggle('hidden-tooltip', false) + } container.style.display = 'block' const dom = document.getElementById(container.id) if (!dom) { @@ -1433,8 +1817,17 @@ export function configPlotTooltipEvent> } plot.chart.getOptions().tooltip.follow = false tooltipCtl.title = Math.random().toString() - plot.chart.getTheme().components.tooltip.x = event.clientX - plot.chart.getTheme().components.tooltip.y = event.clientY + // 当显示提示为事件触发时,使用event的client坐标,否则使用tooltipCtl.point 数据点的位置,在图表中,需要加上图表在绘制区的位置 + const { x, y } = calculateTooltipPosition( + chart, + isCarousel, + tooltipCtl, + chartElement, + event, + enlargeElement + ) + plot.chart.getTheme().components.tooltip.x = x + plot.chart.getTheme().components.tooltip.y = y }) // https://github.com/antvis/G2/blob/master/src/chart/controller/tooltip.ts#hideTooltip plot.on('plot:leave', () => { @@ -1457,14 +1850,22 @@ export function configPlotTooltipEvent> if (!tooltipCtl) { return } - const container = tooltipCtl.tooltip.cfg.container + const container = tooltipCtl.tooltip?.cfg.container for (const ele of wrapperDom.children) { - if (container.id !== ele.id) { + if (!container || container.id !== ele.id) { ele.style.display = 'none' } } } }) + plot.on('tooltip:hidden', () => { + const tooltipCtl = plot.chart.getController('tooltip') + if (!tooltipCtl) { + return + } + const container = tooltipCtl.tooltip?.cfg.container + container && (container.style.display = 'none') + }) } export const TOOLTIP_TPL = @@ -1699,10 +2100,12 @@ export function configYaxisTitleLengthLimit(chart, plot) { ? wrappedTitle.slice(0, wrappedTitle.length - 2) + '...' : wrappedTitle + '...' } - // 更新Y轴标题的原始文本和截断后的文本 - ev.view.options.axes.yAxisExt.title.originalText = yAxis.name - ev.view.options.axes.yAxisExt.title.text = wrappedTitle + const { title } = ev.view.options.axes.yAxisExt + if (title) { + title.originalText = yAxis.name + title.text = wrappedTitle + } }) } @@ -1731,7 +2134,7 @@ export const addConditionsStyleColorToData = (chart: Chart, options) => { }) } else if (item.quotaList?.length) { const quotaList = item.quotaList.map(q => q.id) ?? [] - quotaList.forEach((q, index) => { + quotaList.forEach(q => { // 定义后,在 handleConditionsStyle 函数中使用 let currentValue = item[valueField] if (chart.type === 'progress-bar') { @@ -1798,7 +2201,7 @@ const getColorByConditions = (quotaList: [], values: number | number[], chart) = * @param chart * @param options */ -export function handleConditionsStyle(chart: Chart, options: O) { +export function handleConditionsStyle(chart: Chart, options) { const { threshold } = parseJson(chart.senior) if (!threshold.enable) return options const { basicStyle } = parseJson(chart.customAttr) @@ -1810,8 +2213,6 @@ export function handleConditionsStyle(chart: Chart, options: O) { // 辅助函数:配置柱条样式颜色,条形图为barStyle,柱形图为columnStyle const columnStyle = data => { return { - ...options.columnStyle, - ...options.barStyle, ...(data[colorField]?.[0] ? { fill: data[colorField][0] } : {}) } } @@ -1825,8 +2226,8 @@ export function handleConditionsStyle(chart: Chart, options: O) { const tmpOption = { ...options, rawFields, - columnStyle: columnStyle, - barStyle: columnStyle, + ...configRoundAngle(chart, 'columnStyle', columnStyle), + ...configRoundAngle(chart, 'barStyle', columnStyle), tooltip: { ...options.tooltip, ...(options.tooltip['customItems'] @@ -1934,7 +2335,7 @@ export const getTooltipItemConditionColor = item => { * @param newData * @param container */ -export const configEmptyDataStyle = (newChart, newData, container) => { +export const configEmptyDataStyle = (newData, container, newChart?, content?) => { /** * 辅助函数:移除空数据dom */ @@ -1949,15 +2350,121 @@ export const configEmptyDataStyle = (newChart, newData, container) => { if (!newData.length) { const emptyDom = document.createElement('div') emptyDom.id = container + '_empty' - emptyDom.textContent = tI18n('data_set.no_data') + emptyDom.textContent = content || tI18n('data_set.no_data') emptyDom.setAttribute( 'style', `position: absolute; - left: 45%; - top: 50%;` + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + color: darkgray; + textAlign: center;` ) const parent = document.getElementById(container) parent.insertBefore(emptyDom, parent.firstChild) - newChart.destroy() + newChart?.destroy() + } +} + +export const numberToChineseUnderHundred = (num: number): string => { + // 合法性检查 + if (num <= 0 || num > 99 || !Number.isInteger(num)) { + throw new Error('请输入1-99之间的整数') + } + + const digits = ['', '一', '二', '三', '四', '五', '六', '七', '八', '九'] + + // 处理个位数 + if (num < 10) return digits[num] + + const tens = Math.floor(num / 10) + const ones = num % 10 + + // 处理整十 + if (ones === 0) { + return tens === 1 ? '十' : digits[tens] + '十' + } + + // 处理其他两位数 + return tens === 1 ? '十' + digits[ones] : digits[tens] + '十' + digits[ones] +} + +/** + * 配置柱条图的圆角 + * @param styleName + * @param callBack 自定义其他属性函数 + */ +export const configRoundAngle = (chart: Chart, styleName: string, callBack?: (datum) => {}) => { + const { basicStyle } = parseJson(chart.customAttr) + if (['roundAngle', 'topRoundAngle'].includes(basicStyle.radiusColumnBar)) { + const radius = Array(2).fill(basicStyle.columnBarRightAngleRadius) + const topRadius = [0, 0, ...radius] + const bottomRadius = [...radius, 0, 0] + const finalRadius = [...radius, ...radius] + if (chart.type.includes('-stack')) { + return { + [styleName]: datum => { + if (!datum.value) return { radius: [], ...(callBack ? callBack(datum) : {}) } + return { radius: finalRadius, ...(callBack ? callBack(datum) : {}) } + } + } + } + const isTopRound = basicStyle.radiusColumnBar === 'topRoundAngle' + // 对称条形图 + if (chart.type === 'bidirectional-bar') { + const valueField = basicStyle.layout === 'vertical' ? 'valueExt' : 'value' + return { + [styleName]: datum => ({ + radius: datum[valueField] && isTopRound ? topRadius : isTopRound ? radius : finalRadius, + ...(callBack ? callBack(datum) : {}) + }) + } + } + // 进度条 + if (chart.type === 'progress-bar') { + return { + [styleName]: datum => { + return { + radius: isTopRound ? bottomRadius : finalRadius, + ...(callBack ? callBack(datum) : {}) + } + } + } + } + // 区间条形图 + if (chart.type === 'bar-range') { + return { + [styleName]: datum => { + return { + radius: + datum?.values[0] < datum?.values[1] + ? isTopRound + ? bottomRadius + : finalRadius + : isTopRound + ? topRadius + : finalRadius, + ...(callBack ? callBack(datum) : {}) + } + } + } + } + // 配置柱条样式 + const style = datum => { + if (isTopRound) { + return { radius, ...(callBack ? callBack(datum) : {}) } + } + if (!isTopRound) { + return { radius: finalRadius, ...(callBack ? callBack(datum) : {}) } + } + } + return { + [styleName]: style + } + } + return { + [styleName]: datum => { + return { ...(callBack ? callBack(datum) : {}) } + } } } diff --git a/core/core-frontend/src/views/chart/components/js/panel/common/common_table.ts b/core/core-frontend/src/views/chart/components/js/panel/common/common_table.ts index 098e6bc..2fddd3f 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/common/common_table.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/common/common_table.ts @@ -5,7 +5,9 @@ import { isAlphaColor, isTransparent, parseJson, - resetRgbOpacity + resetRgbOpacity, + safeDecimalSum, + safeDecimalMean } from '../..//util' import { DEFAULT_BASIC_STYLE, @@ -43,13 +45,31 @@ import { updateShapeAttr, ViewMeta } from '@antv/s2' -import { cloneDeep, filter, find, intersection, keys, merge, repeat } from 'lodash-es' +import { + cloneDeep, + filter, + find, + intersection, + keys, + map, + maxBy, + meanBy, + merge, + minBy, + repeat, + sumBy, + size, + sum +} from 'lodash-es' import { createVNode, render } from 'vue' import TableTooltip from '@/views/chart/components/editor/common/TableTooltip.vue' import Exceljs from 'exceljs' import { saveAs } from 'file-saver' import { ElMessage } from 'element-plus-secondary' import { useI18n } from '@/hooks/web/useI18n' +import Decimal from 'decimal.js' + + const { t: i18nt } = useI18n() export function getCustomTheme(chart: Chart): S2Theme { @@ -401,8 +421,7 @@ export function getCustomTheme(chart: Chart): S2Theme { }, dataCell: { cell: { - crossBackgroundColor: - enableTableCrossBG && !tableCell.mergeCells ? tableItemSubBgColor : tableItemBgColor, + crossBackgroundColor: enableTableCrossBG ? tableItemSubBgColor : tableItemBgColor, backgroundColor: tableItemBgColor }, bolderText: { @@ -603,7 +622,7 @@ export function getConditions(chart: Chart) { const dimFields = [...chart.xAxis, ...chart.xAxisExt].map(i => i.dataeaseName) if (conditions?.length > 0) { const { tableCell, basicStyle, tableHeader } = parseJson(chart.customAttr) - // 合并单元格时,班马纹失效 + // 合并单元格时斑马纹失效 const enableTableCrossBG = chart.type === 'table-info' ? tableCell.enableTableCrossBG && !tableCell.mergeCells @@ -783,6 +802,9 @@ export function mappingColor(value, defaultColor, field, type, filedValueMap?, r } } else { // time + if (!tv || !value) { + break + } const fc = field.conditions[i] tv = new Date(tv.replace(/-/g, '/') + ' GMT+8').getTime() const v = new Date(value.replace(/-/g, '/') + ' GMT+8').getTime() @@ -880,6 +902,7 @@ export function handleTableEmptyStrategy(chart: Chart) { } return newData } + export class SortTooltip extends BaseTooltip { show(showOptions) { const { iconName } = showOptions @@ -934,6 +957,7 @@ export class SortTooltip extends BaseTooltip { }) } } + const SORT_DEFAULT = '' const SORT_UP = @@ -1063,7 +1087,14 @@ export function copyContent(s2Instance: SpreadSheet, event, fieldMeta) { if (cells.length === 1) { const curCell = cells[0] if (cell.getMeta().id === curCell.id) { - copyString(cellMeta.value + '', true) + const cellMeta = cell.getMeta() + const value = cellMeta.data?.[cellMeta.valueField] + const metaObj = find(fieldMeta, m => m.field === cellMeta.valueField) + let fieldVal = value?.toString() + if (metaObj) { + fieldVal = metaObj.formatter(value) + } + copyString(fieldVal, true) } s2Instance.interaction.clearState() return @@ -1189,7 +1220,7 @@ export async function exportGridPivot(instance: PivotSheet, chart: ChartObj) { const { meta, fields } = instance.dataCfg const rowLength = fields?.rows?.length || 0 const colLength = fields?.columns?.length || 0 - const colNums = layoutResult.colLeafNodes.length + rowLength + 1 + const colNums = layoutResult.colLeafNodes.length + rowLength if (colNums > 16384) { ElMessage.warning(i18nt('chart.pivot_export_invalid_col_exceed')) return @@ -1346,9 +1377,180 @@ export async function exportGridPivot(instance: PivotSheet, chart: ChartObj) { if (fieldValue === 0 || fieldValue) { const meta = metaMap[dataCellMeta.valueField] const cell = worksheet.getCell(rowIndex + maxColHeight + 1, rowLength + colIndex + 1) - const value = meta?.formatter?.(fieldValue) || fieldValue.toString() + const value = meta?.formatter?.(fieldValue) || fieldValue cell.alignment = { vertical: 'middle', horizontal: 'center' } - cell.value = value + cell.value = isNumeric(value) ? parseFloat(value) : value + } + } + } + const buffer = await workbook.xlsx.writeBuffer() + const dataBlob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8' + }) + saveAs(dataBlob, `${chart.title ?? '透视表'}.xlsx`) +} + +export async function exportRowQuotaGridPivot(instance: PivotSheet, chart: ChartObj) { + const { layoutResult } = instance.facet + const { meta, fields } = instance.dataCfg + const rowLength = fields?.rows?.length || 0 + const colLength = fields?.columns?.length || 0 + const colNums = layoutResult.colLeafNodes.length + rowLength + if (colNums > 16384) { + ElMessage.warning(i18nt('chart.pivot_export_invalid_col_exceed')) + return + } + const workbook = new Exceljs.Workbook() + const worksheet = workbook.addWorksheet(i18nt('chart.chart_data')) + const metaMap: Record = meta?.reduce((p, n) => { + if (n.field) { + p[n.field] = n + } + return p + }, {}) + // 角头 + if (colLength > 1) { + fields.columns.forEach((column: string, index) => { + if (index >= colLength - 1) { + return + } + const cell = worksheet.getCell(index + 1, 1) + cell.value = metaMap[column]?.name ?? column + cell.alignment = { vertical: 'middle', horizontal: 'center' } + cell.border = { + right: { style: 'thick', color: { argb: '00000000' } } + } + worksheet.mergeCells(index + 1, 1, index + 1, rowLength + 1) + }) + } + fields?.rows?.forEach((row, index) => { + const cell = worksheet.getCell(colLength === 0 ? 1 : colLength, index + 1) + cell.value = metaMap[row]?.name ?? row + cell.alignment = { vertical: 'middle', horizontal: 'center' } + cell.border = { bottom: { style: 'thick', color: { argb: '00000000' } } } + }) + const quotaColLabel = chart.customAttr.basicStyle.quotaColLabel ?? t('dataset.value') + const quotaColHeadCell = worksheet.getCell(colLength === 0 ? 1 : colLength, rowLength + 1) + quotaColHeadCell.value = quotaColLabel + quotaColHeadCell.alignment = { vertical: 'middle', horizontal: 'center' } + quotaColHeadCell.border = { + bottom: { style: 'thick', color: { argb: '00000000' } }, + right: { style: 'thick', color: { argb: '00000000' } } + } + // 行头 + const { rowLeafNodes, rowNodes } = layoutResult + const notLeafNodeHeightMap: Record = {} + rowLeafNodes.forEach(node => { + // 行头的高度由子节点相加决定,也就是行头子节点中包含的叶子节点数量 + let curNode = node.parent + while (curNode) { + const height = notLeafNodeHeightMap[curNode.id] ?? 0 + notLeafNodeHeightMap[curNode.id] = height + 1 + curNode = curNode.parent + } + const { rowIndex } = node + const writeRowIndex = rowIndex + 2 + (colLength === 0 ? 1 : colLength - 1) + const writeColIndex = node.level + 1 + const cell = worksheet.getCell(writeRowIndex, writeColIndex) + let value = node.label + if (node.field === '$$extra$$' && metaMap[value]?.name) { + value = metaMap[value].name + } + cell.value = value + cell.alignment = { vertical: 'middle', horizontal: 'center' } + cell.border = { + right: { style: 'thick', color: { argb: '00000000' } } + } + }) + + const getNodeStartRowIndex = (node: Node) => { + if (!node.children?.length) { + return node.rowIndex + 1 + } else { + return getNodeStartRowIndex(node.children[0]) + } + } + rowNodes?.forEach(node => { + if (node.isLeaf) { + return + } + const rowIndex = getNodeStartRowIndex(node) + const height = notLeafNodeHeightMap[node.id] + const writeRowIndex = rowIndex + 1 + (colLength === 0 ? 1 : colLength - 1) + const mergeColCount = node.children[0].level - node.level + const cell = worksheet.getCell(writeRowIndex, node.level + 1) + cell.value = node.label + cell.alignment = { vertical: 'middle', horizontal: 'center' } + if (mergeColCount > 1 || height > 1) { + worksheet.mergeCells( + writeRowIndex, + node.level + 1, + writeRowIndex + height - 1, + node.level + mergeColCount + ) + } + }) + + // 列头 + const { colLeafNodes, colNodes, colsHierarchy } = layoutResult + const maxColHeight = colsHierarchy.maxLevel + 1 + const notLeafNodeWidthMap: Record = {} + colLeafNodes.forEach(node => { + // 列头的宽度由子节点相加决定,也就是列头子节点中包含的叶子节点数量 + let curNode = node.parent + while (curNode) { + const width = notLeafNodeWidthMap[curNode.id] ?? 0 + notLeafNodeWidthMap[curNode.id] = width + 1 + curNode = curNode.parent + } + const { colIndex } = node + const writeRowIndex = node.level + 1 + const writeColIndex = colIndex + rowLength + 2 + const cell = worksheet.getCell(writeRowIndex, writeColIndex) + const value = node.label + cell.value = value + cell.alignment = { vertical: 'middle', horizontal: 'center' } + if (writeRowIndex < maxColHeight) { + worksheet.mergeCells(writeRowIndex, writeColIndex, maxColHeight, writeColIndex) + } + cell.border = { + bottom: { style: 'thick', color: { argb: '00000000' } } + } + }) + const getNodeStartColIndex = (node: Node) => { + if (!node.children?.length) { + return node.colIndex + 1 + } else { + return getNodeStartColIndex(node.children[0]) + } + } + colNodes.forEach(node => { + if (node.isLeaf) { + return + } + const colIndex = getNodeStartColIndex(node) + const width = notLeafNodeWidthMap[node.id] + const writeRowIndex = node.level + 1 + const value = node.label + const writeColIndex = colIndex + rowLength + 1 + const cell = worksheet.getCell(writeRowIndex, writeColIndex) + cell.value = value + cell.alignment = { vertical: 'middle', horizontal: 'center' } + if (width > 1) { + worksheet.mergeCells(writeRowIndex, writeColIndex, writeRowIndex, writeColIndex + width - 1) + } + }) + // 单元格数据 + for (let rowIndex = 0; rowIndex < rowLeafNodes.length; rowIndex++) { + for (let colIndex = 0; colIndex < colLeafNodes.length; colIndex++) { + const dataCellMeta = layoutResult.getCellMeta(rowIndex, colIndex) + const { fieldValue } = dataCellMeta + if (fieldValue === 0 || fieldValue) { + const meta = metaMap[dataCellMeta.valueField] + const cell = worksheet.getCell(rowIndex + maxColHeight + 1, rowLength + colIndex + 2) + const value = meta?.formatter?.(fieldValue) || fieldValue + cell.alignment = { vertical: 'middle', horizontal: 'center' } + cell.value = isNumeric(value) ? parseFloat(value) : value } } } @@ -1361,7 +1563,7 @@ export async function exportGridPivot(instance: PivotSheet, chart: ChartObj) { export async function exportTreePivot(instance: PivotSheet, chart: ChartObj) { const layoutResult = instance.facet.layoutResult - if (layoutResult.colLeafNodes.length + 2 > 16384) { + if (layoutResult.colLeafNodes.length + 1 > 16384) { ElMessage.warning(i18nt('chart.pivot_export_invalid_col_exceed')) return } @@ -1468,9 +1670,9 @@ export async function exportTreePivot(instance: PivotSheet, chart: ChartObj) { if (fieldValue === 0 || fieldValue) { const meta = metaMap[dataCellMeta.valueField] const cell = worksheet.getCell(rowIndex + maxColHeight + 1, colIndex + 1 + 1) - const value = meta?.formatter?.(fieldValue) || fieldValue.toString() + const value = meta?.formatter?.(fieldValue) || fieldValue cell.alignment = { vertical: 'middle', horizontal: 'center' } - cell.value = value + cell.value = isNumeric(value) ? parseFloat(value) : value } } } @@ -1480,6 +1682,135 @@ export async function exportTreePivot(instance: PivotSheet, chart: ChartObj) { }) saveAs(dataBlob, `${chart.title ?? '透视表'}.xlsx`) } + +export async function exportRowQuotaTreePivot(instance: PivotSheet, chart: ChartObj) { + const layoutResult = instance.facet.layoutResult + if (layoutResult.colLeafNodes.length + 1 > 16384) { + ElMessage.warning(i18nt('chart.pivot_export_invalid_col_exceed')) + return + } + const { meta, fields } = instance.dataCfg + const colLength = fields?.columns?.length || 0 + const workbook = new Exceljs.Workbook() + const worksheet = workbook.addWorksheet(i18nt('chart.chart_data')) + const metaMap: Record = meta?.reduce((p, n) => { + if (n.field) { + p[n.field] = n + } + return p + }, {}) + + // 角头 + fields.columns?.forEach((column, index) => { + if (index >= fields.columns.length - 1) { + return + } + const cell = worksheet.getCell(index + 1, 1) + cell.value = metaMap[column]?.name ?? column + cell.alignment = { vertical: 'middle', horizontal: 'center' } + cell.border = { + right: { style: 'thick', color: { argb: '00000000' } } + } + }) + const quotaColLabel = chart.customAttr.basicStyle.quotaColLabel ?? t('dataset.value') + const maxColHeight = layoutResult.colsHierarchy.maxLevel + 1 + const rowName = fields?.rows + ?.map(row => metaMap[row]?.name ?? row) + .concat(quotaColLabel) + .join('/') + const cell = worksheet.getCell(colLength, 1) + cell.value = rowName + cell.alignment = { vertical: 'middle', horizontal: 'center' } + cell.border = { + right: { style: 'thick', color: { argb: '00000000' } }, + bottom: { style: 'thick', color: { argb: '00000000' } } + } + //行头 + const { rowLeafNodes } = layoutResult + rowLeafNodes.forEach((node, index) => { + const cell = worksheet.getCell(maxColHeight + index + 1, 1) + let value = node.label + if (node.field === '$$extra$$' && metaMap[value]?.name) { + value = metaMap[value].name + } + cell.value = repeat(' ', node.level) + value + cell.alignment = { vertical: 'middle', horizontal: 'left' } + cell.border = { + right: { style: 'thick', color: { argb: '00000000' } } + } + }) + // 列头 + const notLeafNodeWidthMap: Record = {} + const { colLeafNodes } = layoutResult + colLeafNodes.forEach(node => { + let curNode = node.parent + while (curNode) { + const width = notLeafNodeWidthMap[curNode.id] ?? 0 + notLeafNodeWidthMap[curNode.id] = width + 1 + curNode = curNode.parent + } + const { colIndex } = node + const writeRowIndex = node.level + 1 + const writeColIndex = colIndex + 2 + const cell = worksheet.getCell(writeRowIndex, writeColIndex) + cell.value = node.label + cell.alignment = { vertical: 'middle', horizontal: 'center' } + if (writeRowIndex < maxColHeight) { + worksheet.mergeCells(writeRowIndex, writeColIndex, maxColHeight, writeColIndex) + } + cell.border = { + bottom: { style: 'thick', color: { argb: '00000000' } } + } + }) + const colNodes = layoutResult.colNodes + const getNodeStartIndex = (node: Node) => { + if (!node.children?.length) { + return node.colIndex + 1 + } else { + return getNodeStartIndex(node.children[0]) + } + } + colNodes.forEach(node => { + if (node.isLeaf) { + return + } + const colIndex = getNodeStartIndex(node) + const width = notLeafNodeWidthMap[node.id] + const writeRowIndex = node.level + 1 + const writeColIndex = colIndex + 1 + const cell = worksheet.getCell(writeRowIndex, writeColIndex) + cell.value = node.label + cell.alignment = { vertical: 'middle', horizontal: 'center' } + if (width > 1) { + worksheet.mergeCells(writeRowIndex, writeColIndex, writeRowIndex, writeColIndex + width - 1) + } + }) + // 单元格数据 + for (let rowIndex = 0; rowIndex < rowLeafNodes.length; rowIndex++) { + for (let colIndex = 0; colIndex < colLeafNodes.length; colIndex++) { + const dataCellMeta = layoutResult.getCellMeta(rowIndex, colIndex) + const { fieldValue } = dataCellMeta + if (fieldValue === 0 || fieldValue) { + const meta = metaMap[dataCellMeta.valueField] + const cell = worksheet.getCell(rowIndex + maxColHeight + 1, colIndex + 2) + const value = meta?.formatter?.(fieldValue) || fieldValue + cell.alignment = { vertical: 'middle', horizontal: 'center' } + cell.value = isNumeric(value) ? parseFloat(value) : value + } + } + } + const buffer = await workbook.xlsx.writeBuffer() + const dataBlob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8' + }) + saveAs(dataBlob, `${chart.title ?? '透视表'}.xlsx`) +} + + +function isNumeric(value: string): boolean { + return /^[+-]?\d+(\.\d+)?$/.test(value) +} + export async function exportPivotExcel(instance: PivotSheet, chart: ChartObj) { const { fields } = instance.dataCfg const rowLength = fields?.rows?.length || 0 @@ -1488,10 +1819,19 @@ export async function exportPivotExcel(instance: PivotSheet, chart: ChartObj) { ElMessage.warning(i18nt('chart.pivot_export_invalid_field')) return } + const { quotaPosition } = chart.customAttr.basicStyle if (chart.customAttr.basicStyle.tableLayoutMode !== 'tree') { - exportGridPivot(instance, chart) + if (quotaPosition === 'row') { + exportRowQuotaGridPivot(instance, chart) + } else { + exportGridPivot(instance, chart) + } } else { - exportTreePivot(instance, chart) + if (quotaPosition === 'row') { + exportRowQuotaTreePivot(instance, chart) + } else { + exportTreePivot(instance, chart) + } } } @@ -1571,6 +1911,7 @@ export function configMergeCells(chart: Chart, options: S2Options, dataConfig: S if (showIndex && meta.colIndex === 0) { meta.fieldValue = getRowIndex(mergedCellsInfo, meta) } + meta.deFieldType = fieldsMap[meta.valueField]?.deType return new CustomMergedCell(sheet, cells, meta) } } @@ -1598,12 +1939,13 @@ export function getRowIndex(mergedCellsInfo: MergedCellInfo[][], meta: ViewMeta) }, 0) return curRangeStartIndex - lostCells + 1 } + class CustomMergedCell extends MergedCell { protected drawBackgroundShape() { const allPoints = getPolygonPoints(this.cells) // 处理条件样式,这里没有用透明度 // 因为合并的单元格是单独的图层,透明度降低的话会显示底下未合并的单元格,需要单独处理被覆盖的单元格 - const { backgroundColor: fill, backgroundColorOpacity: fillOpacity } = this.getBackgroundColor() + const { backgroundColor: fill } = this.getBackgroundColor() const cellTheme = this.theme.dataCell.cell this.backgroundShape = renderPolygon(this, { points: allPoints, @@ -1612,6 +1954,14 @@ class CustomMergedCell extends MergedCell { lineHeight: cellTheme.horizontalBorderWidth }) } + + drawTextShape(): void { + if (this.meta.deFieldType === 7) { + drawImage.apply(this) + } else { + super.drawTextShape() + } + } } export class CustomDataCell extends TableDataCell { @@ -1847,96 +2197,95 @@ const getWrapTextHeight = (wrapText, textStyle, spreadsheet, maxLines) => { return Math.min(lines, maxLines) * maxHeight } -/** - * 设置汇总行 - * @param chart - * @param s2Options - * @param newData - * @param tableHeader - * @param basicStyle - * @param showSummary - */ -export const configSummaryRow = ( - chart, - s2Options, - newData, - tableHeader, - basicStyle, - showSummary -) => { - if (!showSummary || !newData.length) return - // 设置汇总行高度和表头一致 - const heightByField = {} - heightByField[newData.length] = tableHeader.tableTitleHeight - s2Options.style.rowCfg = { heightByField } - // 计算汇总加入到数据里,冻结最后一行 - s2Options.frozenTrailingRowCount = 1 - const yAxis = chart.yAxis - const xAxis = chart.xAxis - const summaryObj = newData.reduce( - (p, n) => { - if (chart.type === 'table-info') { - xAxis - .filter(axis => [2, 3, 4].includes(axis.deType)) - .forEach(axis => { - p[axis.dataeaseName] = - (parseFloat(n[axis.dataeaseName]) || 0) + (parseFloat(p[axis.dataeaseName]) || 0) +// 导出获取汇总行的函数 +export function getSummaryRow(data, axis, sumCon = []) { + const summaryObj = { SUMMARY: true } + for (let i = 0; i < axis.length; i++) { + const a = axis[i].dataeaseName + let savedAxis = find(sumCon, s => s.field === a) + if (savedAxis) { + if (savedAxis.summary == undefined) { + savedAxis.summary = 'sum' // 默认汇总方式为求和 + } + if (savedAxis.show == undefined) { + savedAxis.show = true // 默认显示汇总结果 + } + } else { + savedAxis = { + field: a, + summary: 'sum', + show: true + } + } + // 如果配置为不显示,则跳过该字段 + if (!savedAxis.show) { + continue + } + // 根据汇总方式处理数据 + switch (savedAxis.summary) { + case 'sum': + // 计算字段的总和 + summaryObj[a] = safeDecimalSum(data, a) + break + case 'avg': + // 计算字段的平均值 + summaryObj[a] = safeDecimalMean(data, a) + break + case 'max': + // 计算字段的最大值 + summaryObj[a] = maxBy( + filter(data, d => parseFloat(d[a]) !== undefined), + d => parseFloat(d[a]) // 提取数值 + )[a] + break + case 'min': + // 计算字段的最小值 + summaryObj[a] = minBy( + filter(data, d => parseFloat(d[a]) !== undefined), + d => parseFloat(d[a]) // 提取数值 + )[a] + break + case 'var_pop': + // 计算总体方差(需要至少2个数据点) + if (data.length < 2) { + continue + } else { + const mean = safeDecimalMean(data, a) // 计算平均值 + // 计算每个数据点与平均值的差的平方 + const squaredDeviations = map(data, d => { + const value = new Decimal(d[a] ?? 0) // 获取字段值,如果不存在则使用0 + const dev = value.minus(mean) // 计算差值 + return dev.times(dev) // 计算平方 }) - } else { - yAxis.forEach(axis => { - p[axis.dataeaseName] = - (parseFloat(n[axis.dataeaseName]) || 0) + (parseFloat(p[axis.dataeaseName]) || 0) - }) - } - return p - }, - { SUMMARY: true } - ) - newData.push(summaryObj) - s2Options.dataCell = viewMeta => { - // 配置文本自动换行参数 - viewMeta.autoWrap = basicStyle.autoWrap - viewMeta.maxLines = basicStyle.maxLines - if (viewMeta.rowIndex !== newData.length - 1) { - return new CustomDataCell(viewMeta, viewMeta.spreadsheet) - } - if (viewMeta.colIndex === 0) { - if (tableHeader.showIndex) { - viewMeta.fieldValue = basicStyle.summaryLabel ?? i18nt('chart.total_show') - } else { - if (xAxis.length) { - viewMeta.fieldValue = basicStyle.summaryLabel ?? i18nt('chart.total_show') + // 计算方差(平方差的平均值) + const variance = squaredDeviations.reduce((acc, val) => acc.plus(val), new Decimal(0)) + summaryObj[a] = variance.dividedBy(data.length - 1).toNumber() // 计算总体方差 } - } + break + case 'stddev_pop': + // 计算总体标准差(需要至少2个数据点) + if (data.length < 2) { + continue + } else { + const mean = safeDecimalMean(data, a) // 计算平均值 + // 计算每个数据点与平均值的差的平方 + const squaredDeviations = map(data, d => { + const value = new Decimal(d[a] ?? 0) // 获取字段值,如果不存在则使用0 + const dev = value.minus(mean) // 计算差值 + return dev.times(dev) // 计算平方 + }) + // 计算方差(平方差的平均值) + const variance = squaredDeviations.reduce((acc, val) => acc.plus(val), new Decimal(0)) + summaryObj[a] = variance.dividedBy(data.length - 1).sqrt().toNumber() // 计算总体标准差 + } + break } - return new SummaryCell(viewMeta, viewMeta.spreadsheet) } + + // 返回汇总结果对象 + return summaryObj } -/** - * 汇总行样式,紧贴在单元格后面 - * @param newChart - * @param newData - * @param tableCell - * @param tableHeader - * @param showSummary - */ -export const summaryRowStyle = (newChart, newData, tableCell, tableHeader, showSummary) => { - if (!showSummary || !newData.length) return - newChart.on(S2Event.LAYOUT_BEFORE_RENDER, () => { - const showHeader = tableHeader.showTableHeader === true - // 不显示表头时,减少一个表头的高度 - const headerAndSummaryHeight = showHeader ? 2 : 1 - const totalHeight = - tableHeader.tableTitleHeight * headerAndSummaryHeight + - tableCell.tableItemHeight * (newData.length - 1) - if (totalHeight < newChart.options.height) { - // 6 是阴影高度 - newChart.options.height = - totalHeight < newChart.options.height - 6 ? totalHeight + 6 : totalHeight - } - }) -} export class SummaryCell extends CustomDataCell { getTextStyle() { @@ -1944,6 +2293,7 @@ export class SummaryCell extends CustomDataCell { textStyle.textAlign = this.theme.dataCell.text.textAlign return textStyle } + getBackgroundColor() { const { backgroundColor, backgroundColorOpacity } = this.theme.colCell.cell return { backgroundColor, backgroundColorOpacity } @@ -2019,3 +2369,27 @@ export const getColumns = (fields, cols: Array) => { } return result } + +export function drawImage() { + const img = new Image() + const { x, y, width, height, fieldValue } = this.meta + img.src = fieldValue as string + img.setAttribute('crossOrigin', 'anonymous') + img.onload = () => { + !this.cfg.children && (this.cfg.children = []) + const { width: imgWidth, height: imgHeight } = img + const ratio = Math.max(imgWidth / width, imgHeight / height) + // 不铺满,部分留白 + const imgShowWidth = (imgWidth / ratio) * 0.8 + const imgShowHeight = (imgHeight / ratio) * 0.8 + this.textShape = this.addShape('image', { + attrs: { + x: x + (imgShowWidth < width ? (width - imgShowWidth) / 2 : 0), + y: y + (imgShowHeight < height ? (height - imgShowHeight) / 2 : 0), + width: imgShowWidth, + height: imgShowHeight, + img + } + }) + } +} diff --git a/core/core-frontend/src/views/chart/components/js/panel/types/impl/g2plot.ts b/core/core-frontend/src/views/chart/components/js/panel/types/impl/g2plot.ts index 918d135..d547123 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/types/impl/g2plot.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/types/impl/g2plot.ts @@ -170,7 +170,7 @@ export abstract class G2PlotChartView< public setupSeriesColor(chart: ChartObj, data?: any[]): ChartBasicStyle['seriesColor'] { return setupSeriesColor(chart, data) } - + // eslint-disable-next-line public setupSubSeriesColor(chart: ChartObj, data?: any[]): ChartBasicStyle['seriesColor'] { return undefined } @@ -191,8 +191,8 @@ export abstract class G2PlotChartView< return addConditionsStyleColorToData(chart, data) } - protected configEmptyDataStyle(newChart, newData: any[], container: string) { - configEmptyDataStyle(newChart, newData, container) + protected configEmptyDataStyle(newData, container, newChart?, content?) { + configEmptyDataStyle(newData, container, newChart, content) } /** diff --git a/core/core-frontend/src/views/chart/components/js/panel/types/impl/l7.ts b/core/core-frontend/src/views/chart/components/js/panel/types/impl/l7.ts index ece71af..c9e12b4 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/types/impl/l7.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/types/impl/l7.ts @@ -107,8 +107,8 @@ export abstract class L7ChartView< return options } - protected configZoomButton(chart: Chart, plot: S) { - configL7Zoom(chart, plot) + protected configZoomButton(chart: Chart, plot: S, mapKey?: any) { + configL7Zoom(chart, plot, mapKey) } protected configLabel(chart: Chart, options: O): O { diff --git a/core/core-frontend/src/views/chart/components/js/panel/types/impl/s2.ts b/core/core-frontend/src/views/chart/components/js/panel/types/impl/s2.ts index d573210..b7da8c2 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/types/impl/s2.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/types/impl/s2.ts @@ -136,18 +136,18 @@ export abstract class S2ChartView extends AntVAbstractCha if (duration > 300) { return } + const canvasPosition = canvas.getBoundingClientRect() + const touchPosition = [e.changedTouches[0].pageX, e.changedTouches[0].pageY] + const relativePosition = [ + touchPosition[0] - canvasPosition.x, + touchPosition[1] - canvasPosition.y + ] + const shape = s2Instance.container.getShape(relativePosition[0], relativePosition[1]) + // 图片单元格,表头排序图标点击放大图片 + if (shape.cfg?.type === 'image') { + return + } const callback = () => { - const canvasPosition = canvas.getBoundingClientRect() - const touchPosition = [e.changedTouches[0].pageX, e.changedTouches[0].pageY] - const relativePosition = [ - touchPosition[0] - canvasPosition.x, - touchPosition[1] - canvasPosition.y - ] - const shape = s2Instance.container.getShape(relativePosition[0], relativePosition[1]) - // 图片单元格点击放大图片 - if (shape.cfg?.parent.constructor.name === 'ImageCell') { - return - } e.preventDefault() e.stopPropagation() if (shape) { diff --git a/core/core-frontend/src/views/chart/components/js/util.ts b/core/core-frontend/src/views/chart/components/js/util.ts index 595a29f..f20a5fe 100644 --- a/core/core-frontend/src/views/chart/components/js/util.ts +++ b/core/core-frontend/src/views/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,8 +12,7 @@ import { ElMessage } from 'element-plus-secondary' import { useI18n } from '@/hooks/web/useI18n' import { useLinkStoreWithOut } from '@/store/modules/link' import { useAppStoreWithOut } from '@/store/modules/app' -import { valueFormatter } from '@/views/chart/components/js/formatter' -import { deepCopy } from '@/utils/utils' +import { Decimal } from 'decimal.js' const appStore = useAppStoreWithOut() const isDataEaseBi = computed(() => appStore.getIsDataEaseBi) @@ -286,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) } } @@ -306,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) } } @@ -367,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() @@ -375,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) { @@ -391,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++ }) @@ -405,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 + } } }) } @@ -525,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, @@ -589,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(() => { @@ -782,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 @@ -1155,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 @@ -1210,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/core/core-frontend/src/views/chart/components/views/components/ChartComponentG2Plot.vue b/core/core-frontend/src/views/chart/components/views/components/ChartComponentG2Plot.vue index 218b986..3a02ffe 100644 --- a/core/core-frontend/src/views/chart/components/views/components/ChartComponentG2Plot.vue +++ b/core/core-frontend/src/views/chart/components/views/components/ChartComponentG2Plot.vue @@ -14,12 +14,11 @@ import { ChartLibraryType } from '@/views/chart/components/js/panel/types' import { G2PlotChartView } from '@/views/chart/components/js/panel/types/impl/g2plot' import { L7PlotChartView } from '@/views/chart/components/js/panel/types/impl/l7plot' import chartViewManager from '@/views/chart/components/js/panel' -import { useAppStoreWithOut } from '@/store/modules/app' import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain' import ViewTrackBar from '@/components/visualization/ViewTrackBar.vue' import { storeToRefs } from 'pinia' import { parseJson } from '@/views/chart/components/js/util' -import { defaultsDeep, cloneDeep } from 'lodash-es' +import { defaultsDeep, cloneDeep, concat } from 'lodash-es' import ChartError from '@/views/chart/components/views/components/ChartError.vue' import { BASE_VIEW_CONFIG } from '../../editor/util/chart' import { customAttrTrans, customStyleTrans, recursionTransObj } from '@/utils/canvasStyle' @@ -28,7 +27,8 @@ import { isDashboard, trackBarStyleCheck } from '@/utils/canvasUtils' import { useEmitt } from '@/hooks/web/useEmitt' import { L7ChartView } from '@/views/chart/components/js/panel/types/impl/l7' import { useI18n } from '@/hooks/web/useI18n' -import { ExportImage,Scale } from '@antv/l7' +import { ExportImage } from '@antv/l7' +import { configEmptyDataStyle } from '@/views/chart/components/js/panel/common/common_antv' const { t } = useI18n() const dvMainStore = dvMainStoreWithOut() const { nowPanelTrackInfo, nowPanelJumpInfo, mobileInPc, embeddedCallBack, inMobile } = @@ -75,6 +75,11 @@ const props = defineProps({ type: String, required: false, default: 'inherit' + }, + active: { + type: Boolean, + required: false, + default: true } }) @@ -89,6 +94,14 @@ const emit = defineEmits([ const g2TypeSeries1 = ['bidirectional-bar'] const g2TypeSeries0 = ['bar-range'] const g2TypeTree = ['circle-packing'] +const g2TypeStack = [ + 'bar-stack', + 'bar-group-stack', + 'percentage-bar-stack', + 'bar-stack-horizontal', + 'percentage-bar-stack-horizontal' +] +const g2TypeGroup = ['bar-group'] const { view, showPosition, scale, terminal, suffixId } = toRefs(props) @@ -132,7 +145,10 @@ const clearLinkage = () => { } const reDrawView = () => { linkageActiveHistory.value = false - myChart?.render() + const slider = myChart?.chart?.getController('slider') + if (!slider) { + myChart?.render() + } } const linkageActivePre = () => { if (linkageActiveHistory.value) { @@ -144,43 +160,103 @@ const linkageActivePre = () => { } const linkageActive = () => { linkageActiveHistory.value = true + myChart?.setState('active', () => true, false) + myChart?.setState('inactive', () => true, false) + myChart?.setState('selected', () => true, false) myChart?.setState('active', param => { if (Array.isArray(param)) { return false } else { - if (checkSelected(param)) { - return true - } + return checkSelected(param) } }) myChart?.setState('inactive', param => { if (Array.isArray(param)) { return false } else { - if (!checkSelected(param)) { - return true - } + return !checkSelected(param) + } + }) + myChart?.setState('selected', param => { + if (Array.isArray(param)) { + return false + } else { + return checkSelected(param) } }) } const checkSelected = param => { + // 获取当前视图的所有联动字段ID + const mappingFieldIds = Array.from( + new Set( + (view.value.type.includes('chart-mix') + ? concat(chartData.value?.left?.fields, chartData.value?.right?.fields) + : chartData.value?.fields + ) + .map(item => item?.id) + .filter(id => + Object.keys(nowPanelTrackInfo.value).some( + key => key.startsWith(view.value.id) && key.split('#')[1] === id + ) + ) + ) + ) + // 维度字段匹配 + const [xAxis, xAxisExt, extStack] = ['xAxis', 'xAxisExt', 'extStack'].map(key => + view.value[key].find(item => mappingFieldIds.includes(item.id)) + ) + // 选中字段数据 + const { group, name, category } = state.linkageActiveParam + // 选中字段数据匹配 if (g2TypeSeries1.includes(view.value.type)) { - return state.linkageActiveParam.name === param.field + return name === param.field } else if (g2TypeSeries0.includes(view.value.type)) { - return state.linkageActiveParam.category === param.category + return category === param.category } else if (g2TypeTree.includes(view.value.type)) { - if ( - param.path?.startsWith(state.linkageActiveParam.name) || - state.linkageActiveParam.name === t('commons.all') - ) { + if (param.path?.startsWith(name) || name === t('commons.all')) { return true } - return state.linkageActiveParam.name === param.name + return name === param.name + } else if (g2TypeGroup.includes(view.value.type)) { + const isNameMatch = name === param.name || (name === 'NO_DATA' && !param.name) + const isCategoryMatch = category === param.category + if (xAxis && xAxisExt) { + return isNameMatch && isCategoryMatch + } + if (xAxis && !xAxisExt) { + return isNameMatch + } + if (!xAxis && xAxisExt) { + return isCategoryMatch + } + return false + } else if (g2TypeStack.includes(view.value.type)) { + const isGroupMatch = group === param.group || (group === 'NO_DATA' && !param.group) + const isNameMatch = name === param.name || (name === 'NO_DATA' && !param.name) + const isCategoryMatch = category === param.category + // 全部匹配 + if (xAxis && xAxisExt && extStack) { + return isNameMatch && isGroupMatch && isCategoryMatch + } + // 只匹配到维度 + if (xAxis && !xAxisExt && !extStack) { + return isNameMatch + } else if (!xAxis && xAxisExt && !extStack) { + return isGroupMatch + } else if (!xAxis && !xAxisExt && extStack) { + return isCategoryMatch + } else if (xAxis && xAxisExt && !extStack) { + return isNameMatch && isGroupMatch + } else if (xAxis && !xAxisExt && extStack) { + return isNameMatch && isCategoryMatch + } else if (!xAxis && xAxisExt && extStack) { + return isGroupMatch && isCategoryMatch + } else { + return false + } } else { return ( - (state.linkageActiveParam.name === param.name || - (state.linkageActiveParam.name === 'NO_DATA' && !param.name)) && - state.linkageActiveParam.category === param.category + (name === param.name || (name === 'NO_DATA' && !param.name)) && category === param.category ) } } @@ -274,6 +350,8 @@ const renderG2Plot = async (chart, chartView: G2PlotChartView) => { g2Timer && clearTimeout(g2Timer) g2Timer = setTimeout(async () => { try { + // 在这里清理掉之前图表的空dom + configEmptyDataStyle([1], containerId) myChart?.destroy() myChart = await chartView.drawChart({ chartObj: myChart, @@ -295,7 +373,6 @@ const renderG2Plot = async (chart, chartView: G2PlotChartView) => { const dynamicAreaId = ref('') const country = ref('') -const appStore = useAppStoreWithOut() const chartContainer = ref(null) let scope let mapTimer: number @@ -332,8 +409,6 @@ const renderL7Plot = async (chart: ChartObj, chartView: L7PlotChartView, callback) => { mapL7Timer && clearTimeout(mapL7Timer) mapL7Timer = setTimeout(async () => { @@ -343,25 +418,12 @@ const renderL7 = async (chart: ChartObj, chartView: L7ChartView, callb chart: chart, action }) - - // 清除已有比例尺 - if (scaleControl) { - myChart.getScene()?.removeControl(scaleControl) - scaleControl = null - } - - // 创建并添加新比例尺 - scaleControl = new Scale({ - position: 'bottomleft', - imperial: false - }) - myChart.getScene()?.addControl(scaleControl) - myChart?.render() callback?.() emit('resetLoading') }, 500) } + const pointClickTrans = () => { if (embeddedCallBack.value === 'yes') { trackClick('pointClick') @@ -375,11 +437,8 @@ const actionDefault = param => { if (param.from === 'word-cloud') { emitter.emit('word-cloud-default-data-range', param) } - if (param.from === 'gauge') { - emitter.emit('gauge-default-data', param) - } - if (param.from === 'liquid') { - emitter.emit('liquid-default-data', param) + if (param.from === 'gauge' || param.from === 'liquid') { + emitter.emit('gauge-liquid-y-value', param) } } @@ -399,7 +458,8 @@ const action = param => { // 下钻 联动 跳转 state.linkageActiveParam = { category: state.pointParam.data.category ? state.pointParam.data.category : 'NO_DATA', - name: state.pointParam.data.name ? state.pointParam.data.name : 'NO_DATA' + name: state.pointParam.data.name ? state.pointParam.data.name : 'NO_DATA', + group: state.pointParam.data.group ? state.pointParam.data.group : 'NO_DATA' } if (trackMenu.value.length < 2) { // 只有一个事件直接调用 @@ -429,7 +489,7 @@ const action = param => { state.trackBarStyle.top = trackBarY + 'px' } - viewTrack.value.trackButtonClick() + viewTrack.value.trackButtonClick(view.value.id) } } @@ -438,10 +498,28 @@ const trackClick = trackAction => { if (!param?.data?.dimensionList) { return } - let checkName = state.pointParam.data.name - // 对多维度的处理 取第一个 - if (state.pointParam.data.dimensionList.length > 1) { - checkName = state.pointParam.data.dimensionList[0].id + let checkName = undefined + if (param.data.dimensionList.length > 1) { + // 分组堆叠处理 去能比较出来值的那个维度 + if (view.value.type === 'bar-group-stack') { + const length = param.data.dimensionList.length + // 存在最后一个id + if (param.data.dimensionList[length - 1].id === param.data.dimensionList[length - 2].id) { + param.data.dimensionList.pop() + } + param.data.dimensionList.forEach(dimension => { + if (dimension.value === param.data.category) { + checkName = dimension.id + } + }) + } + if (!checkName) { + // 对多维度的处理 取第一个 + checkName = param.data.dimensionList[0].id + } + } + if (!checkName) { + checkName = param.data.name } // 跳转字段处理 let jumpName = state.pointParam.data.name @@ -480,7 +558,7 @@ const trackClick = trackAction => { } } let quotaList = state.pointParam.data.quotaList - if (['bar-range'].includes(curView.type)) { + if (['bar-range', 'bullet-graph'].includes(curView.type)) { quotaList = state.pointParam.data.dimensionList } else { quotaList[0]['value'] = state.pointParam.data.value @@ -535,37 +613,38 @@ const trackMenu = computed(() => { let trackMenuInfo = [] // 复用、放大状态的仪表板不进行联动、跳转和下钻的动作 if (!['multiplexing', 'viewDialog'].includes(showPosition.value)) { + let drillFields = + curView?.drill && curView?.drillFilters?.length + ? curView.drillFilters.map(item => item.fieldId) + : [] let linkageCount = 0 let jumpCount = 0 if (curView?.type?.includes('chart-mix')) { - chartData.value?.left?.fields?.forEach(item => { - const sourceInfo = view.value.id + '#' + item.id - if (nowPanelTrackInfo.value[sourceInfo]) { - linkageCount++ - } - if (nowPanelJumpInfo.value[sourceInfo]) { - jumpCount++ - } - }) - chartData.value?.right?.fields?.forEach(item => { - const sourceInfo = view.value.id + '#' + item.id - if (nowPanelTrackInfo.value[sourceInfo]) { - linkageCount++ - } - if (nowPanelJumpInfo.value[sourceInfo]) { - jumpCount++ - } + Array.of('left', 'right').forEach(side => { + chartData.value?.[side]?.fields + ?.filter(item => !drillFields.includes(item.id)) + .forEach(item => { + const sourceInfo = view.value.id + '#' + item.id + if (nowPanelTrackInfo.value[sourceInfo]) { + linkageCount++ + } + if (nowPanelJumpInfo.value[sourceInfo]) { + jumpCount++ + } + }) }) } else { - chartData.value?.fields?.forEach(item => { - const sourceInfo = view.value.id + '#' + item.id - if (nowPanelTrackInfo.value[sourceInfo]) { - linkageCount++ - } - if (nowPanelJumpInfo.value[sourceInfo]) { - jumpCount++ - } - }) + chartData.value?.fields + ?.filter(item => !drillFields.includes(item.id)) + .forEach(item => { + const sourceInfo = view.value.id + '#' + item.id + if (nowPanelTrackInfo.value[sourceInfo]) { + linkageCount++ + } + if (nowPanelJumpInfo.value[sourceInfo]) { + jumpCount++ + } + }) } jumpCount && view.value?.jumpActive && @@ -615,7 +694,7 @@ const canvas2Picture = (pictureData, online) => { mapDom.appendChild(imgDom) } const preparePicture = id => { - if (id !== curView.id) { + if (id !== curView?.id) { return } const chartView = chartViewManager.getChartView(curView.render, curView.type) @@ -639,7 +718,7 @@ const preparePicture = id => { } } const unPreparePicture = id => { - if (id !== curView.id) { + if (id !== curView?.id) { return } const chartView = chartViewManager.getChartView(curView.render, curView.type) @@ -667,6 +746,7 @@ defineExpose({ trackMenu, clearLinkage }) +let intersectionObserver let resizeObserver const TOLERANCE = 0.01 const RESIZE_MONITOR_CHARTS = ['map', 'bubble-map', 'flow-map', 'heat-map'] @@ -691,13 +771,32 @@ onMounted(() => { preSize[1] = size.blockSize }) resizeObserver.observe(containerDom) + intersectionObserver = new IntersectionObserver(([entry]) => { + if (RESIZE_MONITOR_CHARTS.includes(view.value.type)) { + return + } + if (entry.intersectionRatio <= 0) { + myChart?.emit('tooltip:hidden') + } + }) + intersectionObserver.observe(containerDom) useEmitt({ name: 'l7-prepare-picture', callback: preparePicture }) useEmitt({ name: 'l7-unprepare-picture', callback: unPreparePicture }) }) +const MAP_CHARTS = ['map', 'bubble-map', 'flow-map', 'heat-map', 'symbolic-map'] +const onWheel = (e: WheelEvent) => { + if (!MAP_CHARTS.includes(view.value.type)) { + return + } + if (!props.active) { + e.stopPropagation() + } +} onBeforeUnmount(() => { try { myChart?.destroy() resizeObserver?.disconnect() + intersectionObserver?.disconnect() } catch (e) { console.warn(e) } @@ -715,7 +814,13 @@ onBeforeUnmount(() => { :style="state.trackBarStyle" @trackClick="trackClick" /> - + diff --git a/core/core-frontend/src/views/chart/components/views/components/ChartComponentS2.vue b/core/core-frontend/src/views/chart/components/views/components/ChartComponentS2.vue index 5dc3879..77d22a4 100644 --- a/core/core-frontend/src/views/chart/components/views/components/ChartComponentS2.vue +++ b/core/core-frontend/src/views/chart/components/views/components/ChartComponentS2.vue @@ -16,7 +16,6 @@ import { } from 'vue' import { getData } from '@/api/chart' import chartViewManager from '@/views/chart/components/js/panel' -import { useAppStoreWithOut } from '@/store/modules/app' import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain' import ViewTrackBar from '@/components/visualization/ViewTrackBar.vue' import { storeToRefs } from 'pinia' @@ -125,6 +124,7 @@ const state = reactive({ imgEnlarge: false, imgSrc: '' }) +const PAGE_CHARTS = ['table-info', 'table-normal'] // 图表数据不用全响应式 let chartData = shallowRef>({ fields: [] @@ -133,17 +133,20 @@ let chartData = shallowRef>({ const containerId = 'container-' + showPosition.value + '-' + view.value.id + '-' + suffixId.value const viewTrack = ref(null) -const calcData = (view: Chart, callback, resetPageInfo = true) => { - if (view.customAttr.basicStyle.tablePageStyle === 'general') { +const calcData = (viewInfo: Chart, callback, resetPageInfo = true) => { + if (viewInfo.customAttr.basicStyle.tablePageStyle === 'general') { if (state.currentPageSize !== 0) { - view.chartExtRequest.pageSize = state.currentPageSize + viewInfo.chartExtRequest.pageSize = state.currentPageSize + state.pageInfo.pageSize = state.currentPageSize + } else { + viewInfo.chartExtRequest.pageSize = state.pageInfo.pageSize } } else { - delete view.chartExtRequest.pageSize + delete viewInfo.chartExtRequest?.pageSize } - if (view.tableId || view['dataFrom'] === 'template') { + if (viewInfo.tableId || viewInfo['dataFrom'] === 'template') { isError.value = false - const v = JSON.parse(JSON.stringify(view)) + const v = JSON.parse(JSON.stringify(viewInfo)) getData(v) .then(res => { if (res.code && res.code !== 0) { @@ -152,7 +155,7 @@ const calcData = (view: Chart, callback, resetPageInfo = true) => { } else { chartData.value = res?.data as Partial state.totalItems = res?.totalItems - dvMainStore.setViewDataDetails(view.id, res) + dvMainStore.setViewDataDetails(viewInfo.id, res) emit('onDrillFilters', res?.drillFilters) renderChart(res as unknown as Chart, resetPageInfo) } @@ -223,7 +226,7 @@ const renderChart = (viewInfo: Chart, resetPageInfo: boolean) => { nextTick(() => debounceRender(resetPageInfo)) } -const debounceRender = debounce(resetPageInfo => { +const debounceRender = debounce(() => { myChart?.facet?.timer?.stop() myChart?.facet?.cancelScrollFrame() myChart?.destroy() @@ -248,19 +251,13 @@ const debounceRender = debounce(resetPageInfo => { const setupPage = (chart: ChartObj, resetPageInfo?: boolean) => { const customAttr = chart.customAttr - if (chart.type !== 'table-info' || customAttr.basicStyle.tablePageMode !== 'page') { + if (!PAGE_CHARTS.includes(chart.type) || customAttr.basicStyle.tablePageMode !== 'page') { state.showPage = false return } const pageInfo = state.pageInfo state.pageStyle = customAttr.basicStyle.tablePageStyle - if (state.pageStyle === 'general') { - if (state.currentPageSize === 0) { - state.currentPageSize = pageInfo.pageSize - } else { - pageInfo.pageSize = state.currentPageSize - } - } else { + if (state.pageStyle !== 'general') { pageInfo.pageSize = customAttr.basicStyle.tablePageSize ?? 20 } if (state.totalItems > state.pageInfo.pageSize || state.pageStyle === 'general') { @@ -272,6 +269,7 @@ const setupPage = (chart: ChartObj, resetPageInfo?: boolean) => { if (resetPageInfo) { state.pageInfo.currentPage = 1 } + dvMainStore.setViewPageInfo(chart.id, state.pageInfo) } const mouseMove = () => { @@ -293,7 +291,8 @@ const initScroll = () => { myChart && senior?.scrollCfg?.open && chartData.value.tableRow?.length && - (view.value.type === 'table-normal' || (view.value.type === 'table-info' && !state.showPage)) + PAGE_CHARTS.includes(props.view.type) && + !state.showPage ) { // 防止多次渲染 myChart.facet.timer?.stop() @@ -337,7 +336,7 @@ const initScroll = () => { } const showPage = computed(() => { - if (view.value.type !== 'table-info') { + if (!PAGE_CHARTS.includes(view.value.type)) { return false } return state.showPage @@ -355,6 +354,7 @@ const handleCurrentChange = pageNum => { const handlePageSizeChange = pageSize => { if (state.pageStyle === 'general') { state.currentPageSize = pageSize + emitter.emit('set-page-size', pageSize) } let extReq = { pageSize: pageSize } if (chartExtRequest.value) { @@ -401,10 +401,9 @@ const action = param => { state.trackBarStyle.top = barStyleTemp.top + 'px' } - viewTrack.value.trackButtonClick() + viewTrack.value.trackButtonClick(view.value.id) } } -const appStore = useAppStoreWithOut() const trackClick = trackAction => { const param = state.pointParam @@ -681,12 +680,6 @@ const autoStyle = computed(() => { } }) -const autoHeightStyle = computed(() => { - return { - height: 20 * scale.value + 8 + 'px' - } -}) - const tabStyle = computed(() => [ { '--de-pager-color': canvasStyleData.value.component.seniorStyleSetting?.pagerColor } ]) @@ -743,7 +736,7 @@ const tablePageClass = computed(() => { v-else class="table-page-content" layout="prev, pager, next, sizes, jumper" - v-model:page-size="state.currentPageSize" + v-model:page-size="state.pageInfo.pageSize" v-model:current-page="state.pageInfo.currentPage" :pager-count="5" :total="state.pageInfo.total" diff --git a/core/core-frontend/src/views/chart/components/views/components/ChartError.vue b/core/core-frontend/src/views/chart/components/views/components/ChartError.vue index 6f98354..13f131d 100644 --- a/core/core-frontend/src/views/chart/components/views/components/ChartError.vue +++ b/core/core-frontend/src/views/chart/components/views/components/ChartError.vue @@ -4,7 +4,7 @@ import { ref } from 'vue' const { t } = useI18n() -const props = defineProps({ +defineProps({ errMsg: { type: String, required: true, diff --git a/core/core-frontend/src/views/chart/components/views/components/DrillPath.vue b/core/core-frontend/src/views/chart/components/views/components/DrillPath.vue index 9bb9e7f..ed90f7c 100644 --- a/core/core-frontend/src/views/chart/components/views/components/DrillPath.vue +++ b/core/core-frontend/src/views/chart/components/views/components/DrillPath.vue @@ -1,7 +1,6 @@ diff --git a/core/core-frontend/src/views/chart/components/views/components/ScrollShadow.vue b/core/core-frontend/src/views/chart/components/views/components/ScrollShadow.vue new file mode 100644 index 0000000..9f96d4c --- /dev/null +++ b/core/core-frontend/src/views/chart/components/views/components/ScrollShadow.vue @@ -0,0 +1,16 @@ + + + + tet + + + diff --git a/core/core-frontend/src/views/chart/components/views/index.vue b/core/core-frontend/src/views/chart/components/views/index.vue index cb4463e..f382760 100644 --- a/core/core-frontend/src/views/chart/components/views/index.vue +++ b/core/core-frontend/src/views/chart/components/views/index.vue @@ -54,7 +54,8 @@ import request from '@/config/axios' import { store } from '@/store' import { clearExtremum } from '@/views/chart/components/js/extremumUitl' import DePreviewPopDialog from '@/components/visualization/DePreviewPopDialog.vue' - +import { useRoute } from 'vue-router' +const route = useRoute() const { wsCache } = useCache() const chartComponent = ref() const { t } = useI18n() @@ -62,12 +63,13 @@ const dvMainStore = dvMainStoreWithOut() const { emitter } = useEmitt() const dePreviewPopDialogRef = ref(null) let innerRefreshTimer = null +let innerSearchCount = 0 const appStore = useAppStoreWithOut() const appearanceStore = useAppearanceStoreWithOut() const isDataEaseBi = computed(() => appStore.getIsDataEaseBi) const isIframe = computed(() => appStore.getIsIframe) -const emit = defineEmits(['onPointClick']) +const emit = defineEmits(['onPointClick', 'onComponentEvent']) const { nowPanelJumpInfo, @@ -77,11 +79,15 @@ const { canvasStyleData, mobileInPc, inMobile, - editMode, - hiddenListStatus + editMode } = storeToRefs(dvMainStore) const props = defineProps({ + // 公共参数集 + commonParams: { + type: Object, + required: false + }, active: { type: Boolean, default: false @@ -235,6 +241,7 @@ const buildInnerRefreshTimer = ( innerRefreshTimer = setInterval(() => { clearViewLinkage() queryData() + innerSearchCount++ }, timerRefreshTime) } } @@ -245,14 +252,6 @@ const clearViewLinkage = () => { useEmitt().emitter.emit('clearPanelLinkage', { viewId: element.value.id }) } -watch( - [() => view.value], - () => { - initTitle() - }, - { deep: true } -) - watch([() => scale.value], () => { initTitle() }) @@ -376,7 +375,7 @@ const chartClick = param => { // 仪表板和大屏所有额外过滤参数都在此处 const filter = (firstLoad?: boolean) => { const { filter } = useFilter(view.value.id, firstLoad) - return { + const result = { user: wsCache.get('user.uid'), filter, linkageFilters: element.value.linkageFilters, @@ -386,6 +385,18 @@ const filter = (firstLoad?: boolean) => { resultCount: resultCount.value, resultMode: resultMode.value } + // 定时报告相关勿动 + if (route.path === '/preview' && route.query.taskId) { + const sceneId = view.value['sceneId'] + const filterJson = window[`de-report-filter-${sceneId}`] + let filterObj = {} + if (filterJson) { + filterObj = JSON.parse(filterJson) + } + filterObj[view.value.id] = result + window[`de-report-filter-${sceneId}`] = JSON.stringify(filterObj) + } + return result } const onDrillFilters = param => { @@ -458,9 +469,16 @@ const jumpClick = param => { if (isDataEaseBi.value) { embeddedBaseUrl = embeddedStore.baseUrl } + const jumpInfoParam = `&jumpInfoParam=${encodeURIComponent( + Base64.encode(JSON.stringify(param)) + )}` + // 内部仪表板跳转 if (jumpInfo.linkType === 'inner') { if (jumpInfo.targetDvId) { + const editPreviewParams = ['canvas', 'edit-preview'].includes(showPosition.value) + ? '&editPreview=true' + : '' const filterOuterParams = {} const curFilter = dvMainStore.getLastViewRequestInfo(param.viewId) const targetViewInfoList = jumpInfo.targetViewInfoList @@ -493,13 +511,11 @@ const jumpClick = param => { if (publicLinkStatus.value) { // 判断是否有公共链接ID if (jumpInfo.publicJumpId) { - let url = `${embeddedBaseUrl}#/de-link/${ - jumpInfo.publicJumpId - }?fromLink=true&jumpInfoParam=${encodeURIComponent( - Base64.encode(JSON.stringify(param)) - )}` + let url = `${embeddedBaseUrl}#/de-link/${jumpInfo.publicJumpId}?fromLink=true&dvType=${jumpInfo.targetDvType}` if (attachParamsInfo) { - url = url + attachParamsInfo + url = url + attachParamsInfo + jumpInfoParam + editPreviewParams + } else { + url = url + '&ignoreParams=true' + jumpInfoParam + editPreviewParams } const currentUrl = window.location.href localStorage.setItem('beforeJumpUrl', currentUrl) @@ -508,11 +524,11 @@ const jumpClick = param => { ElMessage.warning(t('visualization.public_link_tips')) } } else { - let url = `${embeddedBaseUrl}#/preview?dvId=${ - jumpInfo.targetDvId - }&fromLink=true&jumpInfoParam=${encodeURIComponent(Base64.encode(JSON.stringify(param)))}` + let url = `${embeddedBaseUrl}#/preview?dvId=${jumpInfo.targetDvId}&fromLink=true&dvType=${jumpInfo.targetDvType}` if (attachParamsInfo) { - url = url + attachParamsInfo + url = url + attachParamsInfo + jumpInfoParam + editPreviewParams + } else { + url = url + '&ignoreParams=true' + jumpInfoParam + editPreviewParams } const currentUrl = window.location.href localStorage.setItem('beforeJumpUrl', currentUrl) @@ -568,13 +584,13 @@ const calcData = params => { methodName: 'calcData', args: [ params, - res => { + () => { loading.value = false } ] }) } else { - chartComponent?.value?.calcData?.(params, res => { + chartComponent?.value?.calcData?.(params, () => { loading.value = false }) } @@ -692,10 +708,19 @@ const changeChartType = () => { const changeDataset = () => { checkFieldIsAllowEmpty() } + +const loadPlugin = ref(false) + onMounted(() => { if (!view.value.isPlugin) { state.drillClickDimensionList = view.value?.chartExtRequest?.drill ?? [] queryData(!showPosition.value.includes('viewDialog')) + } else { + const searched = dvMainStore.firstLoadMap.includes(element.value.id) + const queryFilter = filter(!searched) + view.value['chartExtRequest'] = queryFilter + chartExtRequest.value = queryFilter + loadPlugin.value = true } if (!listenerEnable.value) { return @@ -830,7 +855,11 @@ onMounted(() => { // 1.开启仪表板刷新 2.首次加载(searchCount =0 )3.正在请求数据 则显示加载状态 const loadingFlag = computed(() => { - return (canvasStyleData.value.refreshViewLoading || searchCount.value === 0) && loading.value + return ( + (canvasStyleData.value.refreshViewLoading || + (searchCount.value === 0 && innerSearchCount === 0)) && + loading.value + ) }) const chartAreaShow = computed(() => { @@ -904,7 +933,7 @@ function onTitleChange() { } const toolTip = computed(() => { - return props.themes === 'dark' ? 'ndark' : 'dark' + return props.themes === 'dark' ? 'light' : 'dark' }) const marginBottom = computed(() => { @@ -1034,6 +1063,14 @@ const titleTooltipWidth = computed(() => { } return '500px' }) +const clearG2Tooltip = () => { + const g2TooltipWrapper = document.getElementById('g2-tooltip-wrapper') + if (g2TooltipWrapper) { + for (const ele of g2TooltipWrapper.children) { + ele.style.display = 'none' + } + } +} @@ -1124,7 +1161,7 @@ const titleTooltipWidth = computed(() => { { :themes="canvasStyleData.dashboard.themeColor" ref="chartComponent" :view="view" + :element="element" :show-position="showPosition" :suffixId="suffixId" :font-family="fontFamily" + :common-params="commonParams" + @touchstart="clearG2Tooltip" + @onChartClick="chartClick" + @onPointClick="onPointClick" + @onDrillFilters="onDrillFilters" + @onJumpClick="jumpClick" + @onComponentEvent="() => emit('onComponentEvent')" /> { :element="element" :suffixId="suffixId" :font-family="fontFamily" + :active="active" v-else-if=" showChartView(ChartLibraryType.G2_PLOT, ChartLibraryType.L7_PLOT, ChartLibraryType.L7) " @@ -1213,6 +1259,7 @@ const titleTooltipWidth = computed(() => { v-if="(!chartAreaShow || showEmpty) && !allEmptyCheck" :themes="canvasStyleData.dashboard.themeColor" :view-icon="view.type" + @touchstart="clearG2Tooltip" > { overflow: hidden; } .title-container { + position: relative; margin: 0; width: 100%; diff --git a/core/core-frontend/src/views/template/component/DeTemplateImport.vue b/core/core-frontend/src/views/template/component/DeTemplateImport.vue index a5a5e0a..74b1782 100644 --- a/core/core-frontend/src/views/template/component/DeTemplateImport.vue +++ b/core/core-frontend/src/views/template/component/DeTemplateImport.vue @@ -200,7 +200,7 @@ const editTemplate = () => { if (response.data.indexOf('exist') > -1) { ElMessage.warning(t('template_manage.exists_name_hint')) } else { - save(state.templateInfo).then(response => { + save(state.templateInfo).then(() => { ElMessage.success(t('template_manage.edit_success')) emits('refresh', getRefreshPInfo()) emits('closeEditTemplateDialog') @@ -252,7 +252,7 @@ const importTemplate = () => { if (response.data.indexOf('exist') > -1) { ElMessage.warning(t('template_manage.exists_name_hint')) } else { - save(state.templateInfo).then(rsp => { + save(state.templateInfo).then(() => { ElMessage.success(t('template_manage.import_success')) emits('refresh', getRefreshPInfo()) emits('closeEditTemplateDialog') diff --git a/core/core-frontend/src/views/template/indexInject.vue b/core/core-frontend/src/views/template/indexInject.vue new file mode 100644 index 0000000..e5410ae --- /dev/null +++ b/core/core-frontend/src/views/template/indexInject.vue @@ -0,0 +1,21 @@ + + + + + + + + +
{{ t('visualization.template_view_tips') }}
+ 你好,我是 DataEase 智能客服点击一下,开启高效解答模式~ +
extends AntVAbstractCha if (duration > 300) { return } + const canvasPosition = canvas.getBoundingClientRect() + const touchPosition = [e.changedTouches[0].pageX, e.changedTouches[0].pageY] + const relativePosition = [ + touchPosition[0] - canvasPosition.x, + touchPosition[1] - canvasPosition.y + ] + const shape = s2Instance.container.getShape(relativePosition[0], relativePosition[1]) + // 图片单元格,表头排序图标点击放大图片 + if (shape.cfg?.type === 'image') { + return + } const callback = () => { - const canvasPosition = canvas.getBoundingClientRect() - const touchPosition = [e.changedTouches[0].pageX, e.changedTouches[0].pageY] - const relativePosition = [ - touchPosition[0] - canvasPosition.x, - touchPosition[1] - canvasPosition.y - ] - const shape = s2Instance.container.getShape(relativePosition[0], relativePosition[1]) - // 图片单元格点击放大图片 - if (shape.cfg?.parent.constructor.name === 'ImageCell') { - return - } e.preventDefault() e.stopPropagation() if (shape) { diff --git a/core/core-frontend/src/views/chart/components/js/util.ts b/core/core-frontend/src/views/chart/components/js/util.ts index 595a29f..f20a5fe 100644 --- a/core/core-frontend/src/views/chart/components/js/util.ts +++ b/core/core-frontend/src/views/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,8 +12,7 @@ import { ElMessage } from 'element-plus-secondary' import { useI18n } from '@/hooks/web/useI18n' import { useLinkStoreWithOut } from '@/store/modules/link' import { useAppStoreWithOut } from '@/store/modules/app' -import { valueFormatter } from '@/views/chart/components/js/formatter' -import { deepCopy } from '@/utils/utils' +import { Decimal } from 'decimal.js' const appStore = useAppStoreWithOut() const isDataEaseBi = computed(() => appStore.getIsDataEaseBi) @@ -286,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) } } @@ -306,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) } } @@ -367,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() @@ -375,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) { @@ -391,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++ }) @@ -405,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 + } } }) } @@ -525,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, @@ -589,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(() => { @@ -782,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 @@ -1155,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 @@ -1210,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/core/core-frontend/src/views/chart/components/views/components/ChartComponentG2Plot.vue b/core/core-frontend/src/views/chart/components/views/components/ChartComponentG2Plot.vue index 218b986..3a02ffe 100644 --- a/core/core-frontend/src/views/chart/components/views/components/ChartComponentG2Plot.vue +++ b/core/core-frontend/src/views/chart/components/views/components/ChartComponentG2Plot.vue @@ -14,12 +14,11 @@ import { ChartLibraryType } from '@/views/chart/components/js/panel/types' import { G2PlotChartView } from '@/views/chart/components/js/panel/types/impl/g2plot' import { L7PlotChartView } from '@/views/chart/components/js/panel/types/impl/l7plot' import chartViewManager from '@/views/chart/components/js/panel' -import { useAppStoreWithOut } from '@/store/modules/app' import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain' import ViewTrackBar from '@/components/visualization/ViewTrackBar.vue' import { storeToRefs } from 'pinia' import { parseJson } from '@/views/chart/components/js/util' -import { defaultsDeep, cloneDeep } from 'lodash-es' +import { defaultsDeep, cloneDeep, concat } from 'lodash-es' import ChartError from '@/views/chart/components/views/components/ChartError.vue' import { BASE_VIEW_CONFIG } from '../../editor/util/chart' import { customAttrTrans, customStyleTrans, recursionTransObj } from '@/utils/canvasStyle' @@ -28,7 +27,8 @@ import { isDashboard, trackBarStyleCheck } from '@/utils/canvasUtils' import { useEmitt } from '@/hooks/web/useEmitt' import { L7ChartView } from '@/views/chart/components/js/panel/types/impl/l7' import { useI18n } from '@/hooks/web/useI18n' -import { ExportImage,Scale } from '@antv/l7' +import { ExportImage } from '@antv/l7' +import { configEmptyDataStyle } from '@/views/chart/components/js/panel/common/common_antv' const { t } = useI18n() const dvMainStore = dvMainStoreWithOut() const { nowPanelTrackInfo, nowPanelJumpInfo, mobileInPc, embeddedCallBack, inMobile } = @@ -75,6 +75,11 @@ const props = defineProps({ type: String, required: false, default: 'inherit' + }, + active: { + type: Boolean, + required: false, + default: true } }) @@ -89,6 +94,14 @@ const emit = defineEmits([ const g2TypeSeries1 = ['bidirectional-bar'] const g2TypeSeries0 = ['bar-range'] const g2TypeTree = ['circle-packing'] +const g2TypeStack = [ + 'bar-stack', + 'bar-group-stack', + 'percentage-bar-stack', + 'bar-stack-horizontal', + 'percentage-bar-stack-horizontal' +] +const g2TypeGroup = ['bar-group'] const { view, showPosition, scale, terminal, suffixId } = toRefs(props) @@ -132,7 +145,10 @@ const clearLinkage = () => { } const reDrawView = () => { linkageActiveHistory.value = false - myChart?.render() + const slider = myChart?.chart?.getController('slider') + if (!slider) { + myChart?.render() + } } const linkageActivePre = () => { if (linkageActiveHistory.value) { @@ -144,43 +160,103 @@ const linkageActivePre = () => { } const linkageActive = () => { linkageActiveHistory.value = true + myChart?.setState('active', () => true, false) + myChart?.setState('inactive', () => true, false) + myChart?.setState('selected', () => true, false) myChart?.setState('active', param => { if (Array.isArray(param)) { return false } else { - if (checkSelected(param)) { - return true - } + return checkSelected(param) } }) myChart?.setState('inactive', param => { if (Array.isArray(param)) { return false } else { - if (!checkSelected(param)) { - return true - } + return !checkSelected(param) + } + }) + myChart?.setState('selected', param => { + if (Array.isArray(param)) { + return false + } else { + return checkSelected(param) } }) } const checkSelected = param => { + // 获取当前视图的所有联动字段ID + const mappingFieldIds = Array.from( + new Set( + (view.value.type.includes('chart-mix') + ? concat(chartData.value?.left?.fields, chartData.value?.right?.fields) + : chartData.value?.fields + ) + .map(item => item?.id) + .filter(id => + Object.keys(nowPanelTrackInfo.value).some( + key => key.startsWith(view.value.id) && key.split('#')[1] === id + ) + ) + ) + ) + // 维度字段匹配 + const [xAxis, xAxisExt, extStack] = ['xAxis', 'xAxisExt', 'extStack'].map(key => + view.value[key].find(item => mappingFieldIds.includes(item.id)) + ) + // 选中字段数据 + const { group, name, category } = state.linkageActiveParam + // 选中字段数据匹配 if (g2TypeSeries1.includes(view.value.type)) { - return state.linkageActiveParam.name === param.field + return name === param.field } else if (g2TypeSeries0.includes(view.value.type)) { - return state.linkageActiveParam.category === param.category + return category === param.category } else if (g2TypeTree.includes(view.value.type)) { - if ( - param.path?.startsWith(state.linkageActiveParam.name) || - state.linkageActiveParam.name === t('commons.all') - ) { + if (param.path?.startsWith(name) || name === t('commons.all')) { return true } - return state.linkageActiveParam.name === param.name + return name === param.name + } else if (g2TypeGroup.includes(view.value.type)) { + const isNameMatch = name === param.name || (name === 'NO_DATA' && !param.name) + const isCategoryMatch = category === param.category + if (xAxis && xAxisExt) { + return isNameMatch && isCategoryMatch + } + if (xAxis && !xAxisExt) { + return isNameMatch + } + if (!xAxis && xAxisExt) { + return isCategoryMatch + } + return false + } else if (g2TypeStack.includes(view.value.type)) { + const isGroupMatch = group === param.group || (group === 'NO_DATA' && !param.group) + const isNameMatch = name === param.name || (name === 'NO_DATA' && !param.name) + const isCategoryMatch = category === param.category + // 全部匹配 + if (xAxis && xAxisExt && extStack) { + return isNameMatch && isGroupMatch && isCategoryMatch + } + // 只匹配到维度 + if (xAxis && !xAxisExt && !extStack) { + return isNameMatch + } else if (!xAxis && xAxisExt && !extStack) { + return isGroupMatch + } else if (!xAxis && !xAxisExt && extStack) { + return isCategoryMatch + } else if (xAxis && xAxisExt && !extStack) { + return isNameMatch && isGroupMatch + } else if (xAxis && !xAxisExt && extStack) { + return isNameMatch && isCategoryMatch + } else if (!xAxis && xAxisExt && extStack) { + return isGroupMatch && isCategoryMatch + } else { + return false + } } else { return ( - (state.linkageActiveParam.name === param.name || - (state.linkageActiveParam.name === 'NO_DATA' && !param.name)) && - state.linkageActiveParam.category === param.category + (name === param.name || (name === 'NO_DATA' && !param.name)) && category === param.category ) } } @@ -274,6 +350,8 @@ const renderG2Plot = async (chart, chartView: G2PlotChartView) => { g2Timer && clearTimeout(g2Timer) g2Timer = setTimeout(async () => { try { + // 在这里清理掉之前图表的空dom + configEmptyDataStyle([1], containerId) myChart?.destroy() myChart = await chartView.drawChart({ chartObj: myChart, @@ -295,7 +373,6 @@ const renderG2Plot = async (chart, chartView: G2PlotChartView) => { const dynamicAreaId = ref('') const country = ref('') -const appStore = useAppStoreWithOut() const chartContainer = ref(null) let scope let mapTimer: number @@ -332,8 +409,6 @@ const renderL7Plot = async (chart: ChartObj, chartView: L7PlotChartView, callback) => { mapL7Timer && clearTimeout(mapL7Timer) mapL7Timer = setTimeout(async () => { @@ -343,25 +418,12 @@ const renderL7 = async (chart: ChartObj, chartView: L7ChartView, callb chart: chart, action }) - - // 清除已有比例尺 - if (scaleControl) { - myChart.getScene()?.removeControl(scaleControl) - scaleControl = null - } - - // 创建并添加新比例尺 - scaleControl = new Scale({ - position: 'bottomleft', - imperial: false - }) - myChart.getScene()?.addControl(scaleControl) - myChart?.render() callback?.() emit('resetLoading') }, 500) } + const pointClickTrans = () => { if (embeddedCallBack.value === 'yes') { trackClick('pointClick') @@ -375,11 +437,8 @@ const actionDefault = param => { if (param.from === 'word-cloud') { emitter.emit('word-cloud-default-data-range', param) } - if (param.from === 'gauge') { - emitter.emit('gauge-default-data', param) - } - if (param.from === 'liquid') { - emitter.emit('liquid-default-data', param) + if (param.from === 'gauge' || param.from === 'liquid') { + emitter.emit('gauge-liquid-y-value', param) } } @@ -399,7 +458,8 @@ const action = param => { // 下钻 联动 跳转 state.linkageActiveParam = { category: state.pointParam.data.category ? state.pointParam.data.category : 'NO_DATA', - name: state.pointParam.data.name ? state.pointParam.data.name : 'NO_DATA' + name: state.pointParam.data.name ? state.pointParam.data.name : 'NO_DATA', + group: state.pointParam.data.group ? state.pointParam.data.group : 'NO_DATA' } if (trackMenu.value.length < 2) { // 只有一个事件直接调用 @@ -429,7 +489,7 @@ const action = param => { state.trackBarStyle.top = trackBarY + 'px' } - viewTrack.value.trackButtonClick() + viewTrack.value.trackButtonClick(view.value.id) } } @@ -438,10 +498,28 @@ const trackClick = trackAction => { if (!param?.data?.dimensionList) { return } - let checkName = state.pointParam.data.name - // 对多维度的处理 取第一个 - if (state.pointParam.data.dimensionList.length > 1) { - checkName = state.pointParam.data.dimensionList[0].id + let checkName = undefined + if (param.data.dimensionList.length > 1) { + // 分组堆叠处理 去能比较出来值的那个维度 + if (view.value.type === 'bar-group-stack') { + const length = param.data.dimensionList.length + // 存在最后一个id + if (param.data.dimensionList[length - 1].id === param.data.dimensionList[length - 2].id) { + param.data.dimensionList.pop() + } + param.data.dimensionList.forEach(dimension => { + if (dimension.value === param.data.category) { + checkName = dimension.id + } + }) + } + if (!checkName) { + // 对多维度的处理 取第一个 + checkName = param.data.dimensionList[0].id + } + } + if (!checkName) { + checkName = param.data.name } // 跳转字段处理 let jumpName = state.pointParam.data.name @@ -480,7 +558,7 @@ const trackClick = trackAction => { } } let quotaList = state.pointParam.data.quotaList - if (['bar-range'].includes(curView.type)) { + if (['bar-range', 'bullet-graph'].includes(curView.type)) { quotaList = state.pointParam.data.dimensionList } else { quotaList[0]['value'] = state.pointParam.data.value @@ -535,37 +613,38 @@ const trackMenu = computed(() => { let trackMenuInfo = [] // 复用、放大状态的仪表板不进行联动、跳转和下钻的动作 if (!['multiplexing', 'viewDialog'].includes(showPosition.value)) { + let drillFields = + curView?.drill && curView?.drillFilters?.length + ? curView.drillFilters.map(item => item.fieldId) + : [] let linkageCount = 0 let jumpCount = 0 if (curView?.type?.includes('chart-mix')) { - chartData.value?.left?.fields?.forEach(item => { - const sourceInfo = view.value.id + '#' + item.id - if (nowPanelTrackInfo.value[sourceInfo]) { - linkageCount++ - } - if (nowPanelJumpInfo.value[sourceInfo]) { - jumpCount++ - } - }) - chartData.value?.right?.fields?.forEach(item => { - const sourceInfo = view.value.id + '#' + item.id - if (nowPanelTrackInfo.value[sourceInfo]) { - linkageCount++ - } - if (nowPanelJumpInfo.value[sourceInfo]) { - jumpCount++ - } + Array.of('left', 'right').forEach(side => { + chartData.value?.[side]?.fields + ?.filter(item => !drillFields.includes(item.id)) + .forEach(item => { + const sourceInfo = view.value.id + '#' + item.id + if (nowPanelTrackInfo.value[sourceInfo]) { + linkageCount++ + } + if (nowPanelJumpInfo.value[sourceInfo]) { + jumpCount++ + } + }) }) } else { - chartData.value?.fields?.forEach(item => { - const sourceInfo = view.value.id + '#' + item.id - if (nowPanelTrackInfo.value[sourceInfo]) { - linkageCount++ - } - if (nowPanelJumpInfo.value[sourceInfo]) { - jumpCount++ - } - }) + chartData.value?.fields + ?.filter(item => !drillFields.includes(item.id)) + .forEach(item => { + const sourceInfo = view.value.id + '#' + item.id + if (nowPanelTrackInfo.value[sourceInfo]) { + linkageCount++ + } + if (nowPanelJumpInfo.value[sourceInfo]) { + jumpCount++ + } + }) } jumpCount && view.value?.jumpActive && @@ -615,7 +694,7 @@ const canvas2Picture = (pictureData, online) => { mapDom.appendChild(imgDom) } const preparePicture = id => { - if (id !== curView.id) { + if (id !== curView?.id) { return } const chartView = chartViewManager.getChartView(curView.render, curView.type) @@ -639,7 +718,7 @@ const preparePicture = id => { } } const unPreparePicture = id => { - if (id !== curView.id) { + if (id !== curView?.id) { return } const chartView = chartViewManager.getChartView(curView.render, curView.type) @@ -667,6 +746,7 @@ defineExpose({ trackMenu, clearLinkage }) +let intersectionObserver let resizeObserver const TOLERANCE = 0.01 const RESIZE_MONITOR_CHARTS = ['map', 'bubble-map', 'flow-map', 'heat-map'] @@ -691,13 +771,32 @@ onMounted(() => { preSize[1] = size.blockSize }) resizeObserver.observe(containerDom) + intersectionObserver = new IntersectionObserver(([entry]) => { + if (RESIZE_MONITOR_CHARTS.includes(view.value.type)) { + return + } + if (entry.intersectionRatio <= 0) { + myChart?.emit('tooltip:hidden') + } + }) + intersectionObserver.observe(containerDom) useEmitt({ name: 'l7-prepare-picture', callback: preparePicture }) useEmitt({ name: 'l7-unprepare-picture', callback: unPreparePicture }) }) +const MAP_CHARTS = ['map', 'bubble-map', 'flow-map', 'heat-map', 'symbolic-map'] +const onWheel = (e: WheelEvent) => { + if (!MAP_CHARTS.includes(view.value.type)) { + return + } + if (!props.active) { + e.stopPropagation() + } +} onBeforeUnmount(() => { try { myChart?.destroy() resizeObserver?.disconnect() + intersectionObserver?.disconnect() } catch (e) { console.warn(e) } @@ -715,7 +814,13 @@ onBeforeUnmount(() => { :style="state.trackBarStyle" @trackClick="trackClick" /> - +