feat(图表): 新增双轴图

#7377 #8710
This commit is contained in:
ulleo 2024-04-15 16:49:22 +08:00
parent 464b2523f0
commit 37855f034d
12 changed files with 1417 additions and 18 deletions

View File

@ -595,6 +595,8 @@ export default {
formatter_plc: '内容格式为空时显示默认格式',
xAxis: '横轴',
yAxis: '纵轴',
yAxisLeft: '左纵轴',
yAxisRight: '右纵轴',
position: '位置',
rotate: '角度',
name: '名称',
@ -752,6 +754,8 @@ export default {
chart_style: '样式',
drag_block_type_axis: '类别轴',
drag_block_value_axis: '值轴',
drag_block_value_axis_left: '左值轴',
drag_block_value_axis_right: '右值轴',
drag_block_table_data_column: '数据列',
drag_block_pie_angel: '扇区角度',
drag_block_pie_label: '扇区标签',
@ -782,7 +786,7 @@ export default {
custom_case: '自定义',
last_layer: '当前已经是最后一级',
radar_size: '大小',
chart_mix: '组合图',
chart_mix: '柱线组合图',
axis_value: '轴值',
axis_value_min: '最小值',
axis_value_max: '最大值',
@ -831,6 +835,7 @@ export default {
chart_type_compare: '柱形图',
chart_type_distribute: '分布图',
chart_type_relation: '关系图',
chart_type_dual_axes: '双轴图',
chart_type_space: '地图',
preview: '上一步',
next: '下一步',

View File

@ -5,6 +5,7 @@ declare type EditorProperty =
| 'tooltip-selector'
| 'x-axis-selector'
| 'y-axis-selector'
| 'dual-y-axis-selector'
| 'title-selector'
| 'legend-selector'
| 'table-header-selector'

View File

@ -178,6 +178,11 @@ 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)
@ -189,6 +194,12 @@ const beforeSummary = type => {
}
}
const beforeSwitchType = type => {
return {
type: type
}
}
const showRename = () => {
item.value.index = props.index
item.value.renameType = props.type
@ -334,6 +345,56 @@ onMounted(() => {
class="drop-style"
:class="themes === 'dark' ? 'dark-dimension-quota' : ''"
>
<el-dropdown-item @click.prevent v-if="chart.type === 'chart-mix'">
<el-dropdown
:effect="themes"
placement="right-start"
style="width: 100%"
@command="switchChartType"
>
<span class="el-dropdown-link inner-dropdown-menu menu-item-padding">
<span class="menu-item-content">
<el-icon>
<Icon name="icon_functions_outlined" />
</el-icon>
<span>{{ t('chart.chart_type') }}</span>
</span>
<el-icon>
<Icon name="icon_right_outlined"></Icon>
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu
:effect="themes"
class="drop-style sub"
:class="themes === 'dark' ? 'dark-dimension-quota' : ''"
>
<el-dropdown-item class="menu-item-padding" :command="beforeSwitchType('bar')">
<span
class="sub-menu-content"
:class="'bar' === item.chartType ? 'content-active' : ''"
>
{{ t('chart.chart_bar') }}
<el-icon class="sub-menu-content--icon">
<Icon name="icon_done_outlined" v-if="'bar' === item.chartType" />
</el-icon>
</span>
</el-dropdown-item>
<el-dropdown-item class="menu-item-padding" :command="beforeSwitchType('line')">
<span
class="sub-menu-content"
:class="'line' === item.chartType ? 'content-active' : ''"
>
{{ t('chart.chart_line') }}
<el-icon class="sub-menu-content--icon">
<Icon name="icon_done_outlined" v-if="'line' === item.chartType" />
</el-icon>
</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-dropdown-item>
<el-dropdown-item
@click.prevent
v-if="!item.chartId && chart.type !== 'table-info' && item.summary !== ''"

View File

@ -6,6 +6,7 @@ import LabelSelector from '@/views/chart/components/editor/editor-style/componen
import TooltipSelector from '@/views/chart/components/editor/editor-style/components/TooltipSelector.vue'
import XAxisSelector from '@/views/chart/components/editor/editor-style/components/XAxisSelector.vue'
import YAxisSelector from '@/views/chart/components/editor/editor-style/components/YAxisSelector.vue'
import DualYAxisSelector from '@/views/chart/components/editor/editor-style/components/DualYAxisSelector.vue'
import TitleSelector from '@/views/chart/components/editor/editor-style/components/TitleSelector.vue'
import LegendSelector from '@/views/chart/components/editor/editor-style/components/LegendSelector.vue'
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
@ -78,6 +79,7 @@ const emit = defineEmits([
'onTooltipChange',
'onChangeXAxisForm',
'onChangeYAxisForm',
'onChangeYAxisExtForm',
'onTextChange',
'onLegendChange',
'onBasicStyleChange',
@ -117,6 +119,11 @@ const onChangeYAxisForm = (val, prop) => {
state.initReady && emit('onChangeYAxisForm', val, prop)
}
const onChangeYAxisExtForm = (val, prop) => {
console.log(val, prop)
state.initReady && emit('onChangeYAxisExtForm', val, prop)
}
const onTextChange = (val, prop) => {
state.initReady && emit('onTextChange', val, prop)
}
@ -442,6 +449,25 @@ watch(
@onChangeYAxisForm="onChangeYAxisForm"
/>
</collapse-switch-item>
<collapse-switch-item
:themes="themes"
v-if="showProperties('dual-y-axis-selector')"
v-model="chart.customStyle.yAxis.show"
:change-model="chart.customStyle.yAxis"
@modelChange="val => onChangeYAxisForm(val, 'show')"
name="yAxis"
:title="$t('chart.yAxis')"
>
<dual-y-axis-selector
class="attr-selector"
:property-inner="propertyInnerAll['y-axis-selector']"
:themes="themes"
:chart="chart"
@onChangeYAxisForm="onChangeYAxisForm"
@onChangeYAxisExtForm="onChangeYAxisExtForm"
/>
</collapse-switch-item>
</el-collapse>
</el-row>
</div>

View File

@ -0,0 +1,144 @@
<script lang="tsx" setup>
import { computed, onMounted, PropType, reactive, ref, watch } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import {
COLOR_PANEL,
DEFAULT_YAXIS_EXT_STYLE,
DEFAULT_YAXIS_STYLE
} from '@/views/chart/components/editor/util/chart'
import { formatterType, unitType } from '@/views/chart/components/js/formatter'
import { ElMessage } from 'element-plus-secondary'
import { cloneDeep } from 'lodash-es'
import DualYAxisSelectorInner from './DualYAxisSelectorInner.vue'
const { t } = useI18n()
const props = defineProps({
themes: {
type: String as PropType<EditorTheme>,
default: 'dark'
},
chart: {
type: Object,
required: true
},
propertyInner: {
type: Array<string>
}
})
const activeName = ref('left')
const state = reactive<any>({
axisForm: JSON.parse(JSON.stringify(DEFAULT_YAXIS_STYLE)),
subAxisForm: JSON.parse(JSON.stringify(DEFAULT_YAXIS_EXT_STYLE))
})
const emit = defineEmits(['onChangeYAxisForm', 'onChangeYAxisExtForm'])
watch(
() => props.chart.customStyle.yAxis,
() => {
init()
},
{ deep: true }
)
const changeAxisStyle = (val, prop) => {
emit('onChangeYAxisForm', val, prop)
}
const changeSubAxisStyle = (val, prop) => {
console.log(val, prop)
emit('onChangeYAxisExtForm', val, prop)
}
const init = () => {
const chart = JSON.parse(JSON.stringify(props.chart))
if (chart.customStyle) {
let customStyle = null
if (Object.prototype.toString.call(chart.customStyle) === '[object Object]') {
customStyle = JSON.parse(JSON.stringify(chart.customStyle))
} else {
customStyle = JSON.parse(chart.customStyle)
}
if (customStyle.yAxis) {
state.axisForm = cloneDeep(customStyle.yAxis)
state.axisForm.position = 'left'
}
if (customStyle.yAxisExt) {
state.subAxisForm = cloneDeep(customStyle.yAxisExt)
}
state.subAxisForm.position = 'right'
state.subAxisForm.show = state.axisForm.show
}
}
onMounted(() => {
init()
})
</script>
<template>
<el-tabs v-model="activeName" id="axis-tabs" stretch>
<el-tab-pane :label="t('chart.yAxisLeft')" name="left">
<dual-y-axis-selector-inner
style="margin-top: 8px"
v-if="state.axisForm"
:form="state.axisForm"
:property-inner="propertyInner"
:themes="themes"
type="left"
@on-change-y-axis-form="changeAxisStyle"
/>
</el-tab-pane>
<el-tab-pane :label="t('chart.yAxisRight')" name="right">
<dual-y-axis-selector-inner
style="margin-top: 8px"
v-if="state.subAxisForm"
:form="state.subAxisForm"
:property-inner="propertyInner"
:themes="themes"
type="right"
@on-change-y-axis-form="changeSubAxisStyle"
/>
</el-tab-pane>
</el-tabs>
</template>
<style lang="less" scoped>
#axis-tabs {
margin-top: -16px;
--ed-tabs-header-height: 34px;
:deep(.ed-tabs__header) {
border-top: none !important;
}
}
.custom-form-item-label {
margin-bottom: 4px;
line-height: 20px;
color: #646a73;
font-size: 12px;
font-style: normal;
font-weight: 400;
padding: 2px 12px 0 0;
&.custom-form-item-label--dark {
color: #a6a6a6;
}
}
.form-item-checkbox {
margin-bottom: 10px !important;
}
.m-divider {
border-color: rgba(31, 35, 41, 0.15);
margin: 0 0 16px;
&.m-divider--dark {
border-color: rgba(235, 235, 235, 0.15);
}
}
</style>

View File

@ -0,0 +1,495 @@
<script lang="tsx" setup>
import { computed, onMounted, reactive, watch } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { COLOR_PANEL, DEFAULT_YAXIS_STYLE } from '@/views/chart/components/editor/util/chart'
import { formatterType, unitType } from '@/views/chart/components/js/formatter'
import { ElMessage } from 'element-plus-secondary'
const { t } = useI18n()
const props = withDefaults(
defineProps<{
themes?: EditorTheme
form: any
propertyInner?: Array<string>
type?: 'left' | 'right'
}>(),
{
themes: 'dark',
type: 'left'
}
)
const predefineColors = COLOR_PANEL
const typeList = formatterType
const unitList = unitType
const toolTip = computed(() => {
return props.themes === 'dark' ? 'ndark' : 'dark'
})
const state = reactive({
axisForm: JSON.parse(JSON.stringify(DEFAULT_YAXIS_STYLE))
})
const emit = defineEmits(['onChangeYAxisForm'])
watch(
() => props.form,
() => {
init()
},
{ deep: true }
)
const fontSizeList = computed(() => {
const arr = []
for (let i = 10; i <= 40; i = i + 2) {
arr.push({
name: i + '',
value: i
})
}
return arr
})
const changeAxisStyle = prop => {
if (
state.axisForm.axisValue.splitCount &&
(state.axisForm.axisValue.splitCount > 100 || state.axisForm.axisValue.splitCount < 0)
) {
ElMessage.error(t('chart.splitCount_less_100'))
return
}
emit('onChangeYAxisForm', state.axisForm, prop)
}
const init = () => {
state.axisForm = JSON.parse(JSON.stringify(props.form))
}
const showProperty = prop => props.propertyInner?.includes(prop)
onMounted(() => {
init()
})
</script>
<template>
<el-form
ref="axisForm"
:disabled="!state.axisForm.show"
:model="state.axisForm"
size="small"
label-position="top"
>
<el-form-item
class="form-item"
:class="'form-item-' + themes"
:label="t('chart.position')"
v-if="showProperty('position')"
>
<el-radio-group
v-model="state.axisForm.position"
size="small"
@change="changeAxisStyle('position')"
>
<el-radio :effect="props.themes" label="left">{{ t('chart.text_pos_left') }}</el-radio>
<el-radio :effect="props.themes" label="right">{{ t('chart.text_pos_right') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
class="form-item"
:class="'form-item-' + themes"
:label="t('chart.name')"
v-if="showProperty('name')"
>
<el-input
:effect="props.themes"
v-model="state.axisForm.name"
size="small"
maxlength="50"
@blur="changeAxisStyle('name')"
/>
</el-form-item>
<label class="custom-form-item-label" :class="'custom-form-item-label--' + themes"
>{{ t('chart.name') }}{{ t('chart.text') }}</label
>
<div style="display: flex">
<el-form-item
class="form-item"
:class="'form-item-' + themes"
v-if="showProperty('color')"
style="padding-right: 4px"
>
<el-color-picker
v-model="state.axisForm.color"
class="color-picker-style"
:predefine="predefineColors"
@change="changeAxisStyle('color')"
:effect="themes"
is-custom
/>
</el-form-item>
<el-form-item
class="form-item"
:class="'form-item-' + themes"
v-if="showProperty('fontSize')"
style="padding-left: 4px"
>
<el-tooltip content="字号" :effect="toolTip" placement="top">
<el-select
style="width: 108px"
:effect="props.themes"
v-model="state.axisForm.fontSize"
:placeholder="t('chart.axis_name_fontsize')"
@change="changeAxisStyle('fontSize')"
>
<el-option
v-for="option in fontSizeList"
:key="option.value"
:label="option.name"
:value="option.value"
/>
</el-select>
</el-tooltip>
</el-form-item>
</div>
<template v-if="showProperty('axisValue')">
<el-divider class="m-divider" :class="'m-divider--' + themes" />
<div style="display: flex; flex-direction: row; justify-content: space-between">
<label class="custom-form-item-label" :class="'custom-form-item-label--' + themes">
{{ t('chart.axis_value') }}
<el-tooltip class="item" :effect="toolTip" placement="top">
<template #content><span v-html="t('chart.axis_tip')"></span></template>
<span style="vertical-align: middle">
<el-icon style="cursor: pointer">
<Icon name="icon_info_outlined" />
</el-icon>
</span>
</el-tooltip>
</label>
<el-form-item class="form-item" :class="'form-item-' + themes">
<el-checkbox
size="small"
:effect="props.themes"
v-model="state.axisForm.axisValue.auto"
@change="changeAxisStyle('axisValue.auto')"
>
{{ t('chart.axis_auto') }}
</el-checkbox>
</el-form-item>
</div>
<template v-if="showProperty('axisValue') && !state.axisForm.axisValue.auto">
<el-row :gutter="8">
<el-col :span="12">
<el-form-item
class="form-item"
:class="'form-item-' + themes"
:label="t('chart.axis_value_max')"
>
<el-input-number
controls-position="right"
:effect="props.themes"
v-model.number="state.axisForm.axisValue.max"
@change="changeAxisStyle('axisValue.max')"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
class="form-item"
:class="'form-item-' + themes"
:label="t('chart.axis_value_min')"
>
<el-input-number
:effect="props.themes"
controls-position="right"
v-model.number="state.axisForm.axisValue.min"
@change="changeAxisStyle('axisValue.min')"
/>
</el-form-item>
</el-col>
</el-row>
<label class="custom-form-item-label" :class="'custom-form-item-label--' + themes">
{{ t('chart.axis_value_split_count') }}
<el-tooltip class="item" :effect="toolTip" placement="top">
<template #content>期望的坐标轴刻度数量非最终结果</template>
<span style="vertical-align: middle">
<el-icon style="cursor: pointer">
<Icon name="icon_info_outlined" />
</el-icon>
</span>
</el-tooltip>
</label>
<el-form-item class="form-item" :class="'form-item-' + themes">
<el-input-number
style="width: 100%"
:effect="props.themes"
controls-position="right"
v-model.number="state.axisForm.axisValue.splitCount"
@change="changeAxisStyle('axisValue.splitCount')"
/>
</el-form-item>
</template>
</template>
<el-divider class="m-divider" :class="'m-divider--' + themes" />
<el-form-item class="form-item" :class="'form-item-' + themes" v-if="showProperty('axisLine')">
<el-checkbox
size="small"
:effect="props.themes"
v-model="state.axisForm.axisLine.show"
@change="changeAxisStyle('axisLine.show')"
>
{{ t('chart.axis_show') }}
</el-checkbox>
</el-form-item>
<el-form-item
class="form-item form-item-checkbox"
:class="{
'form-item-dark': themes === 'dark'
}"
v-if="showProperty('splitLine')"
>
<el-checkbox
size="small"
:effect="props.themes"
v-model="state.axisForm.splitLine.show"
@change="changeAxisStyle('splitLine.show')"
>
{{ t('chart.grid_show') }}
</el-checkbox>
</el-form-item>
<div style="padding-left: 22px" v-if="showProperty('splitLine')">
<div style="flex: 1; display: flex">
<el-form-item class="form-item" :class="'form-item-' + themes" style="padding-right: 4px">
<el-color-picker
:disabled="!state.axisForm.splitLine.show"
v-model="state.axisForm.splitLine.lineStyle.color"
:predefine="predefineColors"
@change="changeAxisStyle('splitLine.lineStyle.color')"
:effect="themes"
is-custom
/>
</el-form-item>
<el-form-item class="form-item" :class="'form-item-' + themes" style="padding-left: 4px">
<el-input-number
:disabled="!state.axisForm.splitLine.show"
style="width: 108px"
:effect="props.themes"
v-model="state.axisForm.splitLine.lineStyle.width"
:min="1"
:max="10"
size="small"
controls-position="right"
@change="changeAxisStyle('splitLine.lineStyle.width')"
/>
</el-form-item>
</div>
</div>
<el-divider class="m-divider" :class="'m-divider--' + themes" />
<el-form-item
class="form-item form-item-checkbox"
:class="{
'form-item-dark': themes === 'dark'
}"
v-if="showProperty('axisLabel')"
>
<el-checkbox
size="small"
:effect="props.themes"
v-model="state.axisForm.axisLabel.show"
@change="changeAxisStyle('axisLabel.show')"
>
{{ t('chart.axis_label_show') }}
</el-checkbox>
</el-form-item>
<div style="padding-left: 22px" v-if="showProperty('axisLabel')">
<div style="flex: 1">
<div style="display: flex">
<el-form-item
class="form-item"
:class="'form-item-' + themes"
style="padding-right: 4px"
:label="t('chart.text')"
>
<el-color-picker
:disabled="!state.axisForm.axisLabel.show"
v-model="state.axisForm.axisLabel.color"
:predefine="predefineColors"
@change="changeAxisStyle('axisLabel.color')"
:effect="themes"
is-custom
/>
</el-form-item>
<el-form-item class="form-item" :class="'form-item-' + themes" style="padding-left: 4px">
<template #label>&nbsp;</template>
<el-tooltip content="字号" :effect="toolTip" placement="top">
<el-select
:disabled="!state.axisForm.axisLabel.show"
style="width: 108px"
:effect="props.themes"
v-model="state.axisForm.axisLabel.fontSize"
:placeholder="t('chart.axis_label_fontsize')"
@change="changeAxisStyle('axisLabel.fontSize')"
>
<el-option
v-for="option in fontSizeList"
:key="option.value"
:label="option.name"
:value="option.value"
/>
</el-select>
</el-tooltip>
</el-form-item>
</div>
<el-form-item class="form-item" :class="'form-item-' + themes" :label="t('chart.rotate')">
<el-input-number
:disabled="!state.axisForm.axisLabel.show"
style="width: 100%"
:effect="props.themes"
v-model="state.axisForm.axisLabel.rotate"
:min="-90"
:max="90"
size="small"
controls-position="right"
@change="changeAxisStyle('axisLabel.rotate')"
/>
</el-form-item>
<template v-if="showProperty('axisLabelFormatter')">
<el-form-item
class="form-item"
:class="'form-item-' + themes"
:label="t('chart.value_formatter_type')"
>
<el-select
:disabled="!state.axisForm.axisLabel.show"
style="width: 100%"
:effect="props.themes"
v-model="state.axisForm.axisLabelFormatter.type"
@change="changeAxisStyle('axisLabelFormatter.type')"
>
<el-option
v-for="type in typeList"
:key="type.value"
:label="t('chart.' + type.name)"
:value="type.value"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="state.axisForm.axisLabelFormatter.type !== 'auto'"
:label="t('chart.value_formatter_decimal_count')"
class="form-item"
:class="'form-item-' + themes"
>
<el-input-number
:disabled="!state.axisForm.axisLabel.show"
style="width: 100%"
:effect="props.themes"
v-model="state.axisForm.axisLabelFormatter.decimalCount"
:precision="0"
:min="0"
:max="10"
size="small"
controls-position="right"
@change="changeAxisStyle('axisLabelFormatter.decimalCount')"
/>
</el-form-item>
<el-row
:gutter="8"
v-if="
state.axisForm.axisLabel.show && state.axisForm.axisLabelFormatter.type !== 'percent'
"
>
<el-col :span="12">
<el-form-item
class="form-item"
:class="'form-item-' + themes"
:label="t('chart.value_formatter_unit')"
>
<el-select
:effect="props.themes"
v-model="state.axisForm.axisLabelFormatter.unit"
:placeholder="t('chart.pls_select_field')"
size="small"
@change="changeAxisStyle('axisLabelFormatter.unit')"
>
<el-option
v-for="item in unitList"
:key="item.value"
:label="t('chart.' + item.name)"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
class="form-item"
:class="'form-item-' + themes"
:label="t('chart.value_formatter_suffix')"
>
<el-input
:disabled="!state.axisForm.axisLabel.show"
:effect="props.themes"
v-model="state.axisForm.axisLabelFormatter.suffix"
size="small"
clearable
:placeholder="t('commons.input_content')"
@change="changeAxisStyle('axisLabelFormatter.suffix')"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item class="form-item" :class="'form-item-' + themes">
<el-checkbox
:disabled="!state.axisForm.axisLabel.show"
size="small"
:effect="props.themes"
v-model="state.axisForm.axisLabelFormatter.thousandSeparator"
@change="changeAxisStyle('axisLabelFormatter.thousandSeparator')"
:label="t('chart.value_formatter_thousand_separator')"
/>
</el-form-item>
</template>
</div>
</div>
</el-form>
</template>
<style lang="less" scoped>
.custom-form-item-label {
margin-bottom: 4px;
line-height: 20px;
color: #646a73;
font-size: 12px;
font-style: normal;
font-weight: 400;
padding: 2px 12px 0 0;
&.custom-form-item-label--dark {
color: #a6a6a6;
}
}
.form-item-checkbox {
margin-bottom: 10px !important;
}
.m-divider {
border-color: rgba(31, 35, 41, 0.15);
margin: 0 0 16px;
&.m-divider--dark {
border-color: rgba(235, 235, 235, 0.15);
}
}
</style>

View File

@ -4,7 +4,7 @@ import { useI18n } from '@/hooks/web/useI18n'
import { COLOR_PANEL, DEFAULT_LABEL } from '@/views/chart/components/editor/util/chart'
import { ElSpace } from 'element-plus-secondary'
import { formatterType, unitType } from '../../../js/formatter'
import { defaultsDeep, cloneDeep, intersection } from 'lodash-es'
import { defaultsDeep, cloneDeep, intersection, union, defaultTo } from 'lodash-es'
import { includesAny } from '../../util/StringUtils'
import { fieldType } from '@/utils/attr'
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
@ -37,8 +37,12 @@ watch(
},
{ deep: true }
)
const yAxis = computed(() => {
return union(defaultTo(props.chart.yAxis, []), defaultTo(props.chart.yAxisExt, []))
})
watch(
[() => props.chart.yAxis, () => props.chart.type],
[() => yAxis.value, () => props.chart.type],
() => {
initSeriesLabel()
},
@ -46,10 +50,7 @@ watch(
)
const curSeriesFormatter = ref<Partial<SeriesFormatter>>({})
const formatterEditable = computed(() => {
return (
showProperty('seriesLabelFormatter') &&
(props.chart.yAxis?.length || props.chart.yAxisExt?.length)
)
return showProperty('seriesLabelFormatter') && yAxis.value?.length
})
const formatterSelector = ref()
//
@ -59,17 +60,17 @@ const initSeriesLabel = () => {
return
}
const formatter = state.labelForm.seriesLabelFormatter
const yAxis = props.chart.yAxis
const seriesAxisMap = formatter.reduce((pre, next) => {
pre[next.id] = next
return pre
}, {})
formatter.splice(0, formatter.length)
if (!yAxis.length) {
if (!yAxis.value.length) {
curSeriesFormatter.value = {}
return
}
const axisMap = yAxis.reduce((pre, next) => {
const axisMap = yAxis.value.reduce((pre, next) => {
let tmp = {
...next,
show: true,

View File

@ -42,7 +42,7 @@ import CustomSortEdit from '@/views/chart/components/editor/drag-item/components
import { snapshotStoreWithOut } from '@/store/modules/data-visualization/snapshot'
import CalcFieldEdit from '@/views/visualized/data/dataset/form/CalcFieldEdit.vue'
import { getFieldName, guid } from '@/views/visualized/data/dataset/form/util'
import { cloneDeep, get } from 'lodash-es'
import { cloneDeep, forEach, get } from 'lodash-es'
import { deleteField, saveField } from '@/api/dataset'
import { getWorldTree } from '@/api/map'
import chartViewManager from '@/views/chart/components/js/panel'
@ -494,12 +494,44 @@ const addAxis = (e, axis: AxisType) => {
emitter.emit('removeAxis', { axisType: 'yAxis', axis, editType: 'remove' })
}
}
if (view.value.type === 'indicator') {
if (view.value.type === 'chart-mix') {
if (axis === 'yAxis') {
if (view.value.yAxisExt.length > 0) {
const chartType = view.value.yAxisExt[0].chartType
forEach(view.value.yAxis, axis => {
if (chartType === 'bar') {
axis.chartType = 'line'
} else if (chartType === 'line') {
axis.chartType = 'bar'
}
})
}
} else if (axis === 'yAxisExt') {
if (view.value.yAxis.length > 0) {
const chartType = view.value.yAxis[0].chartType
forEach(view.value.yAxisExt, axis => {
if (chartType === 'bar') {
axis.chartType = 'line'
} else if (chartType === 'line') {
axis.chartType = 'bar'
}
})
}
}
}
if (view.value.type === 'indicator' || view.value.type === 'chart-mix') {
if (view.value?.yAxis?.length > 1) {
const axis = view.value.yAxis.splice(1)
emitter.emit('removeAxis', { axisType: 'yAxis', axis, editType: 'remove' })
}
}
if (view.value.type === 'chart-mix') {
if (view.value?.yAxisExt?.length > 1) {
const axis = view.value.yAxisExt.splice(1)
emitter.emit('removeAxis', { axisType: 'yAxisExt', axis, editType: 'remove' })
}
}
}
const addXaxis = e => {
@ -518,6 +550,10 @@ const addYaxis = e => {
addAxis(e, 'yAxis')
}
const addYaxisExt = e => {
addAxis(e, 'yAxisExt')
}
const addExtBubble = e => {
addAxis(e, 'extBubble')
}
@ -748,6 +784,12 @@ const onChangeYAxisForm = val => {
renderChart(view.value)
}
const onChangeYAxisExtForm = val => {
console.log('onChangeYAxisExtForm', val)
view.value.customStyle.yAxisExt = val
renderChart(view.value)
}
const onChangeMiscStyleForm = val => {
view.value.customStyle.misc = val
renderChart(view.value)
@ -822,7 +864,15 @@ const closeRename = () => {
}
const removeItems = (
_type: 'xAxis' | 'xAxisExt' | 'extStack' | 'yAxis' | 'extBubble' | 'customFilter' | 'drillFields'
_type:
| 'xAxis'
| 'xAxisExt'
| 'extStack'
| 'yAxis'
| 'yAxisExt'
| 'extBubble'
| 'customFilter'
| 'drillFields'
) => {
recordSnapshotInfo('calcData')
let axis = []
@ -839,6 +889,9 @@ const removeItems = (
case 'yAxis':
axis = view.value.yAxis?.splice(0)
break
case 'yAxisExt':
axis = view.value.yAxisExt?.splice(0)
break
case 'extBubble':
axis = view.value.extBubble?.splice(0)
break
@ -1516,6 +1569,54 @@ const onRefreshChange = val => {
</draggable>
<drag-placeholder :drag-list="view.yAxis" />
</el-row>
<!--yAxisExt-->
<el-row class="padding-lr drag-data" v-if="showAxis('yAxisExt')">
<div class="form-draggable-title">
<span>
{{ chartViewInstance.axisConfig.yAxisExt.name }}
</span>
<el-tooltip :effect="toolTip" placement="top" :content="t('common.delete')">
<el-icon
class="remove-icon"
:class="{ 'remove-icon--dark': themes === 'dark' }"
size="14px"
@click="removeItems('yAxisExt')"
>
<Icon class-name="inner-class" name="icon_delete-trash_outlined" />
</el-icon>
</el-tooltip>
</div>
<draggable
:list="view.yAxisExt"
:move="onMove"
item-key="id"
group="drag"
animation="300"
class="drag-block-style"
:class="{ dark: themes === 'dark' }"
@add="addYaxisExt"
@change="e => onAxisChange(e, 'yAxisExt')"
>
<template #item="{ element, index }">
<quota-item
:dimension-data="state.dimension"
:quota-data="state.quota"
:chart="view"
:item="element"
:index="index"
type="quotaExt"
:themes="props.themes"
@onQuotaItemChange="item => quotaItemChange(item, 'yAxisExt')"
@onQuotaItemRemove="quotaItemRemove"
@onNameEdit="showRename"
@editItemFilter="showQuotaEditFilter"
@editItemCompare="showQuotaEditCompare"
@valueFormatter="valueFormatter"
/>
</template>
</draggable>
<drag-placeholder :drag-list="view.yAxisExt" />
</el-row>
<!-- extBubble -->
<el-row class="padding-lr drag-data" v-if="showAxis('extBubble')">
<div class="form-draggable-title">
@ -1792,6 +1893,7 @@ const onRefreshChange = val => {
@onTooltipChange="onTooltipChange"
@onChangeXAxisForm="onChangeXAxisForm"
@onChangeYAxisForm="onChangeYAxisForm"
@onChangeYAxisExtForm="onChangeYAxisExtForm"
@onTextChange="onTextChange"
@onIndicatorChange="onIndicatorChange"
@onIndicatorNameChange="onIndicatorNameChange"

View File

@ -568,7 +568,7 @@ export const DEFAULT_YAXIS_EXT_STYLE: ChartAxisStyle = {
}
},
splitLine: {
show: true,
show: false,
lineStyle: {
color: '#cccccc',
width: 1,
@ -577,10 +577,10 @@ export const DEFAULT_YAXIS_EXT_STYLE: ChartAxisStyle = {
},
axisValue: {
auto: true,
min: null,
max: null,
split: null,
splitCount: null
min: 10,
max: 100,
split: 10,
splitCount: 10
},
axisLabelFormatter: {
type: 'auto',
@ -1297,6 +1297,20 @@ export const CHART_TYPE_CONFIGS = [
}
]
},
{
category: 'dual_axes',
title: t('chart.chart_type_dual_axes'),
display: 'show',
details: [
{
render: 'antv',
category: 'dual_axes',
value: 'chart-mix',
title: t('chart.chart_mix'),
icon: 'chart-mix'
}
]
},
{
category: 'other',
title: '富文本',

View File

@ -0,0 +1,69 @@
export const CHART_MIX_EDITOR_PROPERTY: EditorProperty[] = [
'background-overall-component',
'basic-style-selector',
'x-axis-selector',
'dual-y-axis-selector',
'title-selector',
'legend-selector',
'label-selector',
'tooltip-selector',
'assist-line',
'function-cfg',
'jump-set',
'linkage'
]
export const CHART_MIX_EDITOR_PROPERTY_INNER: EditorPropertyInner = {
'background-overall-component': ['all'],
'label-selector': ['fontSize', 'color'],
'tooltip-selector': ['fontSize', 'color', 'backgroundColor'],
'basic-style-selector': [
'colors',
'alpha',
'lineWidth',
'lineSymbol',
'lineSymbolSize',
'lineSmooth'
],
'x-axis-selector': [
'name',
'color',
'fontSize',
'position',
'axisLabel',
'axisLine',
'splitLine'
],
'y-axis-selector': [
'name',
'color',
'fontSize',
'axisLabel',
'axisLine',
'splitLine',
'axisValue',
'axisLabelFormatter'
],
'title-selector': [
'title',
'fontSize',
'color',
'hPosition',
'isItalic',
'isBolder',
'remarkShow',
'fontFamily',
'letterSpace',
'fontShadow'
],
'legend-selector': ['icon', 'orient', 'fontSize', 'color', 'hPosition', 'vPosition'],
'function-cfg': ['slider', 'emptyDataStrategy']
}
export const CHART_MIX_AXIS_TYPE: AxisType[] = [
'xAxis',
'yAxis',
'drill',
'filter',
'extLabel',
'extTooltip'
]

View File

@ -0,0 +1,389 @@
import {
G2PlotChartView,
G2PlotDrawOptions
} from '@/views/chart/components/js/panel/types/impl/g2plot'
import { DualAxes, DualAxesOptions } from '@antv/g2plot/esm/plots/dual-axes'
import { getLabel, getPadding, getYAxis, getYAxisExt } from '../../common/common_antv'
import { flow, hexColorToRGBA, parseJson } from '@/views/chart/components/js/util'
import { cloneDeep, isEmpty, defaultTo, map } from 'lodash-es'
import { valueFormatter } from '@/views/chart/components/js/formatter'
import {
CHART_MIX_AXIS_TYPE,
CHART_MIX_EDITOR_PROPERTY,
CHART_MIX_EDITOR_PROPERTY_INNER
} from './chart-mix-common'
import { Datum } from '@antv/g2plot/esm/types/common'
import { useI18n } from '@/hooks/web/useI18n'
import { DEFAULT_LABEL } from '@/views/chart/components/editor/util/chart'
const { t } = useI18n()
const DEFAULT_DATA = []
/**
* 柱线混合图
*/
export class ColumnLineMix extends G2PlotChartView<DualAxesOptions, DualAxes> {
properties = CHART_MIX_EDITOR_PROPERTY
propertyInner = {
...CHART_MIX_EDITOR_PROPERTY_INNER,
'label-selector': ['vPosition', 'seriesLabelFormatter'],
'tooltip-selector': [
...CHART_MIX_EDITOR_PROPERTY_INNER['tooltip-selector'],
'seriesTooltipFormatter'
]
}
axis: AxisType[] = [...CHART_MIX_AXIS_TYPE, 'yAxisExt']
axisConfig = {
...this['axisConfig'],
yAxis: {
name: `${t('chart.drag_block_value_axis_left')} / ${t('chart.quota')}`,
type: 'q'
},
yAxisExt: {
name: `${t('chart.drag_block_value_axis_right')} / ${t('chart.quota')}`,
type: 'q'
}
}
drawChart(drawOptions: G2PlotDrawOptions<DualAxes>): DualAxes {
const { chart, action, container } = drawOptions
if (!chart.data.data?.length) {
return
}
const data = cloneDeep(chart.data.data)
const data1Type = data[0]?.type === 'bar' ? 'column' : data[0]?.type
const data2Type = data[1]?.type === 'bar' ? 'column' : data[1]?.type
const data1 = defaultTo(data[0]?.data, [])
const data2 = map(defaultTo(data[1]?.data, []), d => {
return {
...d,
valueExt: d.value
}
})
// custom color
const customAttr = parseJson(chart.customAttr)
const color = customAttr.basicStyle.colors
// options
const initOptions: DualAxesOptions = {
data: [data1, data2],
xField: 'field',
yField: ['value', 'valueExt'], //这里不能设置成一样的
appendPadding: getPadding(chart),
color,
geometryOptions: [
{
geometry: data1Type
},
{
geometry: data2Type
}
],
interactions: [
{
type: 'legend-active',
cfg: {
start: [{ trigger: 'legend-item:mouseenter', action: ['element-active:reset'] }],
end: [{ trigger: 'legend-item:mouseleave', action: ['element-active:reset'] }]
}
},
{
type: 'legend-filter',
cfg: {
start: [
{
trigger: 'legend-item:click',
action: [
'list-unchecked:toggle',
'data-filter:filter',
'element-active:reset',
'element-highlight:reset'
]
}
]
}
},
{
type: 'tooltip',
cfg: {
start: [{ trigger: 'point:mousemove', action: 'tooltip:show' }],
end: [{ trigger: 'point:mouseleave', action: 'tooltip:hide' }]
}
},
{
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, initOptions)
// 开始渲染
const newChart = new DualAxes(container, options)
newChart.on('point:click', action)
return newChart
}
protected configLabel(chart: Chart, options: DualAxesOptions): DualAxesOptions {
const tempLabel = getLabel(chart)
const tmpOption = { ...options }
if (!tempLabel) {
if (tmpOption.geometryOptions) {
tmpOption.geometryOptions[0].label = false
tmpOption.geometryOptions[1].label = false
}
return tmpOption
}
const labelAttr = parseJson(chart.customAttr).label
const formatterMap = labelAttr.seriesLabelFormatter?.reduce((pre, next) => {
pre[next.id] = next
return pre
}, {})
tempLabel.style.fill = DEFAULT_LABEL.color
const label = {
fields: [],
...tempLabel,
offsetY: -8,
formatter: (data: Datum) => {
if (!labelAttr.seriesLabelFormatter?.length) {
return data.value
}
const labelCfg = formatterMap?.[data.quotaList[0].id] as SeriesFormatter
if (!labelCfg) {
return data.value
}
if (!labelCfg.show) {
return
}
const value = valueFormatter(data.value, labelCfg.formatterCfg)
const group = new G2PlotChartView.engine.Group({})
group.addShape({
type: 'text',
attrs: {
x: 0,
y: 0,
text: value,
textAlign: 'start',
textBaseline: 'top',
fontSize: labelCfg.fontSize,
fill: labelCfg.color
}
})
return group
}
}
if (tmpOption.geometryOptions) {
tmpOption.geometryOptions[0].label = label
tmpOption.geometryOptions[1].label = label
}
return tmpOption
}
protected configBasicStyle(chart: Chart, options: DualAxesOptions): DualAxesOptions {
// size
const customAttr: DeepPartial<ChartAttr> = parseJson(chart.customAttr)
const s = JSON.parse(JSON.stringify(customAttr.basicStyle))
const smooth = s.lineSmooth
const point = {
size: s.lineSymbolSize,
shape: s.lineSymbol
}
const lineStyle = {
lineWidth: s.lineWidth
}
const tempOption = {
...options,
smooth,
point,
lineStyle
}
if (tempOption.geometryOptions) {
tempOption.geometryOptions[0].smooth = smooth
tempOption.geometryOptions[0].point = point
tempOption.geometryOptions[0].lineStyle = lineStyle
tempOption.geometryOptions[1].smooth = smooth
tempOption.geometryOptions[1].point = point
tempOption.geometryOptions[1].lineStyle = lineStyle
}
return tempOption
}
protected configCustomColors(chart: Chart, options: DualAxesOptions): DualAxesOptions {
const basicStyle = parseJson(chart.customAttr).basicStyle
const color = basicStyle.colors.map(item => hexColorToRGBA(item, basicStyle.alpha))
return {
...options,
color
}
}
protected configYAxis(chart: Chart, options: DualAxesOptions): DualAxesOptions {
const yAxis = getYAxis(chart)
const yAxisExt = getYAxisExt(chart)
const tempOption = {
...options
}
if (!yAxis) {
//左右轴都要隐藏
tempOption.yAxis = {
value: false,
valueExt: false
}
} else {
tempOption.yAxis = {
value: undefined,
valueExt: undefined
}
}
const yAxisTmp = parseJson(chart.customStyle).yAxis
if (yAxis.label) {
yAxis.label.formatter = value => {
return valueFormatter(value, yAxisTmp.axisLabelFormatter)
}
}
const axisValue = yAxisTmp.axisValue
if (!axisValue?.auto) {
tempOption.yAxis.value = {
...yAxis,
min: axisValue.min,
max: axisValue.max,
minLimit: axisValue.min,
maxLimit: axisValue.max,
tickCount: axisValue.splitCount
}
} else {
tempOption.yAxis.value = yAxis
}
const yAxisExtTmp = parseJson(chart.customStyle).yAxisExt
if (yAxisExt.label) {
yAxisExt.label.formatter = value => {
return valueFormatter(value, yAxisExtTmp.axisLabelFormatter)
}
}
const axisExtValue = yAxisExtTmp.axisValue
if (!axisExtValue?.auto) {
tempOption.yAxis.valueExt = {
...yAxisExt,
min: axisExtValue.min,
max: axisExtValue.max,
minLimit: axisExtValue.min,
maxLimit: axisExtValue.max,
tickCount: axisExtValue.splitCount
}
} else {
tempOption.yAxis.valueExt = yAxisExt
}
return tempOption
}
protected configTooltip(chart: Chart, options: DualAxesOptions): DualAxesOptions {
const customAttr: DeepPartial<ChartAttr> = parseJson(chart.customAttr)
const tooltipAttr = customAttr.tooltip
if (!tooltipAttr.show) {
return {
...options,
tooltip: false
}
}
const xAxisExt = chart.xAxisExt
const formatterMap = tooltipAttr.seriesTooltipFormatter
?.filter(i => i.show)
.reduce((pre, next) => {
pre[next.id] = next
return pre
}, {}) as Record<string, SeriesFormatter>
const tooltip: DualAxesOptions['tooltip'] = {
shared: true,
showTitle: true,
customItems(originalItems) {
if (!tooltipAttr.seriesTooltipFormatter?.length) {
return originalItems
}
const head = originalItems[0]
// 非原始数据
if (!head.data.quotaList) {
return originalItems
}
const result = []
originalItems
.filter(item => formatterMap[item.data.quotaList[0].id])
.forEach(item => {
const formatter = formatterMap[item.data.quotaList[0].id]
const value = valueFormatter(parseFloat(item.value as string), formatter.formatterCfg)
let name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName
if (xAxisExt?.length > 0) {
name = item.data.category
}
result.push({ ...item, name, value })
})
head.data.dynamicTooltipValue?.forEach(item => {
const formatter = formatterMap[item.fieldId]
if (formatter) {
const value = valueFormatter(parseFloat(item.value), formatter.formatterCfg)
const name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName
result.push({ color: 'grey', name, value })
}
})
return result
}
}
return {
...options,
tooltip
}
}
protected configLegend(chart: Chart, options: DualAxesOptions): DualAxesOptions {
const o = super.configLegend(chart, options)
if (o.legend) {
const data = chart.data.data
o.legend.itemName = {
formatter: (text: string, item: any, index: number) => {
let name = undefined
if (index === 0 && text === 'value') {
name = data[0]?.name
} else if (index === 1 && text === 'valueExt') {
name = data[1]?.name
}
if (name === undefined) {
return text
} else {
return name
}
}
}
}
return o
}
protected setupOptions(chart: Chart, options: DualAxesOptions): DualAxesOptions {
return flow(
this.configTheme,
this.configLabel,
this.configTooltip,
this.configBasicStyle,
this.configCustomColors,
this.configLegend,
this.configXAxis,
this.configYAxis,
this.configSlider,
this.configAnalyse,
this.configEmptyDataStrategy
)(chart, options)
}
constructor(name = 'chart-mix') {
super(name, DEFAULT_DATA)
}
}

View File

@ -506,6 +506,98 @@ export function getYAxis(chart: Chart) {
return axis
}
export function getYAxisExt(chart: Chart) {
let axis: Record<string, any> | boolean = {}
const yAxis = parseJson(chart.customStyle).yAxisExt
if (!yAxis.show) {
return false
}
const title =
yAxis.name && yAxis.name !== ''
? {
text: yAxis.name,
style: {
fill: yAxis.color,
fontSize: yAxis.fontSize
},
spacing: 8
}
: null
const grid = yAxis.splitLine.show
? {
line: {
style: {
stroke: yAxis.splitLine.lineStyle.color,
lineWidth: yAxis.splitLine.lineStyle.width
}
}
}
: null
const axisCfg = yAxis.axisLine ? yAxis.axisLine : DEFAULT_YAXIS_STYLE.axisLine
const line = axisCfg.show
? {
style: {
stroke: axisCfg.lineStyle.color,
lineWidth: axisCfg.lineStyle.width
}
}
: null
const tickLine = axisCfg.show
? {
style: {
stroke: axisCfg.lineStyle.color
}
}
: null
const rotate = yAxis.axisLabel.rotate
let textAlign = 'end'
let textBaseline = 'middle'
if (yAxis.position === 'right') {
textAlign = 'start'
if (Math.abs(rotate) > 75) {
textAlign = 'center'
}
if (rotate > 75) {
textBaseline = 'bottom'
}
if (rotate < -75) {
textBaseline = 'top'
}
}
if (yAxis.position === 'left') {
if (Math.abs(rotate) > 75) {
textAlign = 'center'
}
if (rotate > 75) {
textBaseline = 'top'
}
if (rotate < -75) {
textBaseline = 'bottom'
}
}
const label = yAxis.axisLabel.show
? {
rotate: (rotate * Math.PI) / 180,
style: {
fill: yAxis.axisLabel.color,
fontSize: yAxis.axisLabel.fontSize,
textBaseline,
textAlign
}
}
: null
axis = {
position: yAxis.position,
title,
grid,
label,
line,
tickLine
}
return axis
}
export function getSlider(chart: Chart) {
let cfg
const senior = parseJson(chart.senior)