feat(chart): 支持辅助线跟随绑定指标显隐
This commit is contained in:
@@ -1302,6 +1302,8 @@ export default {
|
||||
load_data: 'Load Data'
|
||||
},
|
||||
chart: {
|
||||
assist_bind_field: 'Bind measure (optional)',
|
||||
assist_follow_field: 'Follow field visibility',
|
||||
heatmap_range: 'Color scale range',
|
||||
heatmap_range_auto: 'Automatic range',
|
||||
heatmap_range_custom: 'Custom range',
|
||||
|
||||
@@ -1247,6 +1247,8 @@ export default {
|
||||
load_data: '載入資料'
|
||||
},
|
||||
chart: {
|
||||
assist_bind_field: '綁定指標(可選)',
|
||||
assist_follow_field: '跟隨欄位顯示',
|
||||
heatmap_range: '色階範圍',
|
||||
heatmap_range_auto: '自動極值',
|
||||
heatmap_range_custom: '自訂範圍',
|
||||
|
||||
@@ -1265,6 +1265,8 @@ export default {
|
||||
load_data: '加载数据'
|
||||
},
|
||||
chart: {
|
||||
assist_bind_field: '绑定指标(可选)',
|
||||
assist_follow_field: '跟随字段显示',
|
||||
heatmap_range: '色阶范围',
|
||||
heatmap_range_auto: '自动极值',
|
||||
heatmap_range_custom: '自定义范围',
|
||||
|
||||
@@ -87,6 +87,10 @@ declare interface ChartAssistLineCfg {
|
||||
* 辅助线
|
||||
*/
|
||||
declare interface AssistLine {
|
||||
/** 跟随绑定指标的图例显隐;历史配置缺省为不跟随 */
|
||||
followField?: boolean
|
||||
/** 固定值辅助线的可选绑定指标,不参与数值计算 */
|
||||
boundFieldId?: string
|
||||
/**
|
||||
* 辅助线名称
|
||||
*/
|
||||
|
||||
@@ -44,6 +44,8 @@ const state = reactive({
|
||||
name: t('chart.assist_line'),
|
||||
field: '0', // 固定值
|
||||
fieldId: '',
|
||||
boundFieldId: '',
|
||||
followField: true,
|
||||
summary: 'avg',
|
||||
axis: 'y', // 主轴
|
||||
yAxisType: 'left',
|
||||
@@ -121,6 +123,7 @@ const removeLine = index => {
|
||||
}
|
||||
|
||||
const changeYAxisType = item => {
|
||||
item.boundFieldId = ''
|
||||
if (props.useQuotaExt && item.yAxisType === 'right') {
|
||||
item.fieldId = props.quotaExtFields ? props.quotaExtFields[0]?.id : null
|
||||
item.curField = getQuotaExtField(item.fieldId)
|
||||
@@ -328,6 +331,31 @@ onMounted(() => {
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="24" class="binding-row">
|
||||
<el-select
|
||||
v-if="item.field === '0'"
|
||||
v-model="item.boundFieldId"
|
||||
clearable
|
||||
:placeholder="t('chart.assist_bind_field')"
|
||||
@change="changeAssistLine"
|
||||
>
|
||||
<el-option
|
||||
v-for="quota in useQuotaExt && item.yAxisType === 'right'
|
||||
? quotaExtFields
|
||||
: quotaFields"
|
||||
:key="quota.id"
|
||||
:label="quota.name"
|
||||
:value="quota.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-checkbox
|
||||
v-model="item.followField"
|
||||
:disabled="item.field === '0' && !item.boundFieldId"
|
||||
@change="changeAssistLine"
|
||||
>
|
||||
{{ t('chart.assist_follow_field') }}
|
||||
</el-checkbox>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<el-button class="circle-button" text style="margin-left: 5px" @click="addLine">
|
||||
@@ -340,6 +368,16 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.binding-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 4px;
|
||||
.ed-select {
|
||||
width: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
.line-item {
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
|
||||
@@ -710,7 +710,8 @@ export class Bar extends G2ChartView<ViewSpec, G2Column> {
|
||||
type: 'lineY',
|
||||
// 辅助线统一退出分类图例,避免后续新增视觉编码时影响图例布局
|
||||
legend: false,
|
||||
data: [value],
|
||||
data: [{ ...item, value }],
|
||||
encode: { y: 'value' },
|
||||
style: {
|
||||
stroke: item.color,
|
||||
...ASSIST_LINE_STYLE,
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { Chart as G2Chart } from '@antv/g2'
|
||||
import { parseJson } from '../../../util'
|
||||
|
||||
/** 指标通过 ID 绑定,图例通过实际数据的 category 映射,兼容别名和子维度。 */
|
||||
export const createAssistLineVisibility = (chart: Chart) => {
|
||||
const lines: AssistLine[] = parseJson(chart.senior)?.assistLineCfg?.assistLine ?? []
|
||||
const series = new Map<string, Set<string>>()
|
||||
const collect = (data: Record<string, any>[], fields: Axis[], axis: string) => {
|
||||
data?.forEach(row => {
|
||||
if (Array.isArray(row.data)) collect(row.data, fields, axis)
|
||||
if (row.category === undefined || row.category === null) return
|
||||
const quotas = row.quotaList?.length ? row.quotaList : fields?.length === 1 ? fields : []
|
||||
quotas?.forEach(field => {
|
||||
const id = `${axis}:${field.id}`
|
||||
if (!series.has(id)) series.set(id, new Set())
|
||||
series.get(id).add(String(row.category))
|
||||
})
|
||||
})
|
||||
}
|
||||
collect(chart.data?.data, chart.yAxis, 'left')
|
||||
collect(chart.data?.left?.data, chart.yAxis, 'left')
|
||||
collect(chart.data?.right?.data, chart.yAxisExt, 'right')
|
||||
const fields = new Set([
|
||||
...(chart.yAxis ?? []).map(f => `left:${f.id}`),
|
||||
...(chart.yAxisExt ?? []).map(f => `right:${f.id}`)
|
||||
])
|
||||
let selected: Set<string> | undefined
|
||||
return {
|
||||
select(values?: unknown[]) {
|
||||
selected = values === undefined ? undefined : new Set(values.map(String))
|
||||
},
|
||||
visible(datum: AssistLine) {
|
||||
// 动态计算响应不必携带新配置;显隐始终从持久化的辅助线配置读取。
|
||||
const line =
|
||||
datum.field === '0'
|
||||
? datum
|
||||
: lines.find(
|
||||
item =>
|
||||
item.field === datum.field &&
|
||||
item.name === datum.name &&
|
||||
item.color === datum.color &&
|
||||
item.lineType === datum.lineType &&
|
||||
item.fieldId === datum.fieldId &&
|
||||
(item.yAxisType ?? 'left') === (datum.yAxisType ?? 'left') &&
|
||||
(item.field === '0' || item.summary === datum.summary)
|
||||
)
|
||||
if (!line?.followField) return true
|
||||
const id = line.field === '1' ? line.fieldId : line.boundFieldId
|
||||
if (!id) return true
|
||||
// 已绑定的指标被移除后不将其阈值误用于其它指标;用户可清空绑定恢复常显。
|
||||
if (!fields.has(`${line.yAxisType ?? 'left'}:${id}`)) return false
|
||||
if (!selected) return true
|
||||
const categories = series.get(`${line.yAxisType ?? 'left'}:${id}`)
|
||||
// 无法可靠映射时不使用名称猜测,避免隐藏无关辅助线。
|
||||
return !categories?.size || [...categories].some(category => selected.has(category))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 在首次 render 前接入,与原有轴域裁剪共同决定辅助线及其标签的可见性。 */
|
||||
export const installG2AssistLineVisibility = (instance: G2Chart, chart: Chart) => {
|
||||
const lines: AssistLine[] = parseJson(chart.senior)?.assistLineCfg?.assistLine ?? []
|
||||
if (!lines.some(line => line.followField)) return () => undefined
|
||||
const state = createAssistLineVisibility(chart)
|
||||
const visit = spec => {
|
||||
if (spec.type === 'lineY' && Array.isArray(spec.data) && spec.data[0]?.name !== undefined) {
|
||||
const previous = spec.style?.visibility
|
||||
spec.style = {
|
||||
...spec.style,
|
||||
visibility: (datum, ...args) =>
|
||||
state.visible(datum)
|
||||
? typeof previous === 'function'
|
||||
? previous(datum, ...args)
|
||||
: previous ?? 'visible'
|
||||
: 'hidden'
|
||||
}
|
||||
spec.animate = { ...spec.animate, update: { type: null } }
|
||||
}
|
||||
if (Array.isArray(spec.children)) spec.children.forEach(visit)
|
||||
}
|
||||
const options = instance.options()
|
||||
visit(options)
|
||||
instance.options(options)
|
||||
const filter = event => {
|
||||
if (event?.data?.channel !== 'color' || !Array.isArray(event.data.values)) return
|
||||
state.select(event.data.values)
|
||||
// G2 原生图例在完成筛选后才发布事件;重放一次筛选使辅助线使用最新状态。
|
||||
// 平铺图例的程序化事件发生在筛选前,无需重放。
|
||||
if (event.nativeEvent) instance.emit('legend:filter', { data: event.data })
|
||||
}
|
||||
const focus = event => {
|
||||
if (event?.data?.channel !== 'color') return
|
||||
state.select([event.data.value])
|
||||
if (event.nativeEvent) instance.emit('legend:focus', { data: event.data })
|
||||
}
|
||||
const reset = event => {
|
||||
state.select()
|
||||
if (event?.nativeEvent) instance.emit('legend:reset', {})
|
||||
}
|
||||
instance.on('legend:filter', filter)
|
||||
instance.on('legend:focus', focus)
|
||||
instance.on('legend:reset', reset)
|
||||
return () => {
|
||||
instance.off('legend:filter', filter)
|
||||
instance.off('legend:focus', focus)
|
||||
instance.off('legend:reset', reset)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { installG2AssistLineVisibility } from '@/views/chart/components/js/panel/types/impl/g2-assist-line'
|
||||
import {
|
||||
applyG2TiledLegend,
|
||||
installG2TiledLegendStateAdapter,
|
||||
@@ -620,6 +621,7 @@ const renderChart = async (view, callback?) => {
|
||||
let myChart = null
|
||||
let g2Timer: number
|
||||
let g2SliderCleanup: (() => void) | undefined
|
||||
let assistLineCleanup: (() => void) | undefined
|
||||
let g2TiledLegendCleanup: (() => void) | undefined
|
||||
let g2LegendPaginationCleanup: (() => void) | undefined
|
||||
const clearG2SliderAdapter = () => {
|
||||
@@ -627,6 +629,8 @@ const clearG2SliderAdapter = () => {
|
||||
g2SliderCleanup = undefined
|
||||
}
|
||||
const clearG2LegendPaginationAdapter = () => {
|
||||
assistLineCleanup?.()
|
||||
assistLineCleanup = undefined
|
||||
g2TiledLegendCleanup?.()
|
||||
g2TiledLegendCleanup = undefined
|
||||
g2LegendPaginationCleanup?.()
|
||||
@@ -736,6 +740,7 @@ const renderG2 = async (chart, chartView: G2ChartView<any, any>) => {
|
||||
if (chart.customStyle?.legend?.displayMode === 'tile') {
|
||||
g2TiledLegendCleanup = installG2TiledLegendStateAdapter(chartInstance)
|
||||
}
|
||||
assistLineCleanup = installG2AssistLineVisibility(chartInstance, chart)
|
||||
// 等待 G2 完成包含轴边界校正的最终布局
|
||||
await chartInstance?.render()
|
||||
installG2SvgCoordinateScaleAdapter(chartInstance)
|
||||
|
||||
28
tests/assist-line/verify.mjs
Normal file
28
tests/assist-line/verify.mjs
Normal file
@@ -0,0 +1,28 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import ts from '../../core/core-frontend/node_modules/typescript/lib/typescript.js'
|
||||
const source = readFileSync(new URL('../../core/core-frontend/src/views/chart/components/js/panel/types/impl/g2-assist-line.ts', import.meta.url), 'utf8').replace("import { parseJson } from '../../../util'", 'const parseJson = value => typeof value === "string" ? JSON.parse(value) : value')
|
||||
const compiled = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 } }).outputText
|
||||
const { createAssistLineVisibility } = await import(`data:text/javascript;base64,${Buffer.from(compiled).toString('base64')}`)
|
||||
const fixed = {name:'limit', field:'0', boundFieldId:'1', followField:true}
|
||||
const dynamic = {name:'average',field:'1',fieldId:'1',summary:'avg',followField:true}
|
||||
const chart = { senior:{assistLineCfg:{assistLine:[fixed,dynamic]}}, yAxis:[{id:'1'},{id:'2'}],data:{data:[{category:'renamed-A',quotaList:[{id:'1'}]},{category:'renamed-B',quotaList:[{id:'1'}]},{category:'other',quotaList:[{id:'2'}]}]} }
|
||||
const s = createAssistLineVisibility(chart)
|
||||
assert.equal(s.visible(fixed),true)
|
||||
s.select(['renamed-A','other']);assert.equal(s.visible(fixed),true)
|
||||
s.select(['other']);assert.equal(s.visible(fixed),false)
|
||||
assert.equal(s.visible({...dynamic,followField:undefined,value:'20'}),false)
|
||||
assert.equal(s.visible({...fixed,followField:false}),true)
|
||||
assert.equal(s.visible({...fixed,followField:undefined}),true)
|
||||
assert.equal(s.visible({...fixed,boundFieldId:''}),true)
|
||||
assert.equal(s.visible({...fixed,boundFieldId:'removed'}),false)
|
||||
s.select([]);assert.equal(s.visible(fixed),false)
|
||||
s.select();assert.equal(s.visible(fixed),true)
|
||||
assert.equal(s.visible(dynamic),true)
|
||||
const dual = createAssistLineVisibility({...chart, yAxisExt:[{id:'1'}], data:{left:{data:[{category:'left',quotaList:[{id:'1'}]}]},right:{data:[{category:'right',quotaList:[{id:'1'}]}]}}})
|
||||
dual.select(['left']);assert.equal(dual.visible({...fixed,yAxisType:'right'}),false)
|
||||
assert.equal(dual.visible({...fixed,yAxisType:'left'}),true)
|
||||
const nested = createAssistLineVisibility({...chart,data:{left:{data:[{data:[{category:'amount',quotaList:[{id:'1'}]}]}]}}})
|
||||
nested.select(['other']);assert.equal(nested.visible(fixed),false)
|
||||
nested.select(['amount']);assert.equal(nested.visible(fixed),true)
|
||||
console.log('PASS: 15 auxiliary-line visibility assertions')
|
||||
Reference in New Issue
Block a user