feat(图表): 支持散点图

This commit is contained in:
wisonic-s
2025-05-16 15:25:33 +08:00
committed by GitHub
parent 54536a9e29
commit 5ba030d244

View File

@@ -0,0 +1,536 @@
import { G2ChartView, G2DrawOptions } from '../../../types/impl/g2'
import { flow, hexColorToRGBA, parseJson } from '@/views/chart/components/js/util'
import { TOOLTIP_ITEM_TPL, TOOLTIP_TITLE_TPL } from '../../../common/common_antv'
import { useI18n } from '@/hooks/web/useI18n'
import { defaultsDeep, isEmpty, toString } from 'lodash-es'
import { ChartEvent, Chart as G2Chart, G2Spec } from '@antv/g2'
import { valueFormatter } from '../../../../formatter'
const { t } = useI18n()
/**
* 散点图
*/
export class Scatter extends G2ChartView {
properties: EditorProperty[] = [
'background-overall-component',
'border-style',
'basic-style-selector',
'x-axis-selector',
'y-axis-selector',
'title-selector',
'label-selector',
'tooltip-selector',
'legend-selector',
'jump-set',
'linkage'
]
propertyInner: EditorPropertyInner = {
'basic-style-selector': [
'colors',
'alpha',
'scatterSymbol',
'scatterSymbolSize',
'seriesColor'
],
'label-selector': ['fontSize', 'color', 'labelFormatter'],
'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'seriesTooltipFormatter', 'show'],
'x-axis-selector': [
'position',
'name',
'color',
'fontSize',
'axisLine',
'splitLine',
'axisForm',
'axisLabel'
],
'y-axis-selector': [
'position',
'name',
'color',
'fontSize',
'axisValue',
'axisLine',
'splitLine',
'axisForm',
'axisLabel',
'axisLabelFormatter'
],
'title-selector': [
'title',
'fontSize',
'color',
'hPosition',
'isItalic',
'isBolder',
'remarkShow',
'fontFamily',
'letterSpace',
'fontShadow'
],
'legend-selector': ['icon', 'orient', 'color', 'fontSize', 'hPosition', 'vPosition']
}
axis: AxisType[] = ['xAxis', 'yAxis', 'extBubble', 'filter', 'drill', 'extLabel', 'extTooltip']
axisConfig: AxisConfig = {
xAxis: {
name: `${t('chart.drag_block_type_axis')} / ${t('chart.dimension')}`,
type: 'd'
},
yAxis: {
...this['axisConfig'].yAxis,
limit: undefined,
allowEmpty: false
},
extBubble: {
name: `${t('chart.bubble_size')} / ${t('chart.quota')}`,
type: 'q',
limit: 1,
allowEmpty: true
}
}
async drawChart(drawOptions: G2DrawOptions<G2Chart>): Promise<G2Chart> {
const { chart, container, action } = drawOptions
if (!chart.data?.data) {
return
}
const data = chart.data.data
const baseOptions: G2Spec = {
type: 'point',
data: {
value: data
},
autoFit: true,
encode: {
x: 'field',
y: 'value',
color: 'category'
},
legend: {
size: false
}
}
const options: G2Spec = this.setupOptions(chart, baseOptions)
const newChart = new G2Chart({ container })
newChart.options(options)
newChart.on('point:click', action)
if (options.labels) {
newChart.on('label:click', e => {
action({
x: e.x,
y: e.y,
data: {
data: e.data.data
}
})
})
}
return newChart
}
protected configTheme(chart: Chart, options: G2Spec): G2Spec {
const customAttr = parseJson(chart.customAttr)
const colors: string[] = []
if (customAttr.basicStyle) {
const basicStyle = customAttr.basicStyle
basicStyle.colors.forEach(ele => {
colors.push(hexColorToRGBA(ele, basicStyle.alpha))
})
}
const customStyle = parseJson(chart.customStyle)
let bgColor
if (customStyle.background) {
bgColor = hexColorToRGBA(customStyle.background.color, customStyle.background.alpha)
}
const theme = {
color: colors[0],
category10: colors,
category20: colors,
view: {
viewFill: bgColor
}
}
return { ...options, theme }
}
protected configColor(chart: Chart, options: G2Spec): G2Spec {
const basicStyle = parseJson(chart.customAttr).basicStyle
const { seriesColor } = basicStyle
if (seriesColor?.length) {
const { yAxis } = chart
const seriesMap = seriesColor.reduce((p, n) => {
p[n.id] = n
return p
}, {})
const colorRelations = []
yAxis?.forEach(axis => {
const curAxisColor = seriesMap[axis.id]
if (curAxisColor) {
colorRelations.push([
axis.name ?? axis.chartShowName,
hexColorToRGBA(curAxisColor.color, basicStyle.alpha)
])
}
})
if (colorRelations.length) {
defaultsDeep(options, {
scale: {
color: {
relations: colorRelations
}
}
})
}
}
return options
}
protected configBasicStyle(chart: Chart, options: G2Spec): G2Spec {
const customAttr = parseJson(chart.customAttr)
const basicStyle = customAttr.basicStyle
const sizeOptions = {
encode: {
shape: {
type: 'constant',
value: basicStyle.scatterSymbol
}
}
}
if (chart.extBubble?.length) {
const tmpOptions = {
encode: {
size: 'popSize'
},
scale: {
size: {
range: [5, 30]
}
}
}
return defaultsDeep(options, sizeOptions, tmpOptions)
}
const tmp = {
encode: {
size: {
type: 'constant',
value: basicStyle.scatterSymbolSize
}
}
}
return defaultsDeep(options, sizeOptions, tmp)
}
protected configLabel(chart: Chart, options: G2Spec): G2Spec {
const { label } = parseJson(chart.customAttr)
if (!label.show) {
return options
}
const labelStyle = {
labels: [
{
text: d => {
const value = valueFormatter(d.value, label.labelFormatter)
return toString(value)
},
position: 'top',
style: {
fill: label.color,
fontSize: label.fontSize,
textBaseline: 'bottom',
fillOpacity: 1
},
transform: label.fullDisplay ? [] : [{ type: 'overlapHide' }, { type: 'exceedAdjust' }]
}
]
}
return defaultsDeep(options, labelStyle)
}
protected configTooltip(chart: Chart, options: G2Spec): G2Spec {
const customAttr: DeepPartial<ChartAttr> = parseJson(chart.customAttr)
const tooltipAttr = customAttr.tooltip
if (!tooltipAttr.show) {
return { ...options, tooltip: false }
}
const formatterMap = tooltipAttr.seriesTooltipFormatter
?.filter(i => i.show)
.reduce((pre, next) => {
pre[next.seriesId] = next
return pre
}, {}) as Record<string, SeriesFormatter>
let g2TooltipWrapper = document.getElementById('G2-TOOLTIP-WRAPPER')
if (!g2TooltipWrapper) {
g2TooltipWrapper = document.createElement('div')
g2TooltipWrapper.id = 'G2-TOOLTIP-WRAPPER'
g2TooltipWrapper.style.position = 'absolute'
g2TooltipWrapper.style.pointerEvents = 'none'
g2TooltipWrapper.style.zIndex = '9999'
document.body.appendChild(g2TooltipWrapper)
}
const { yAxis, extBubble } = chart
const tooltipOptions: G2Spec = {
tooltip: d => d,
interaction: {
tooltip: {
mount: g2TooltipWrapper,
css: {
'.g2-tooltip': {
background: tooltipAttr.backgroundColor
},
'.g2-tooltip-title': {
color: tooltipAttr.color,
'font-size': `${tooltipAttr.fontSize}px`
},
'.g2-tooltip-list-item-name-label': {
color: tooltipAttr.color,
'font-size': `${tooltipAttr.fontSize}px`
},
'.g2-tooltip-list-item-value': {
color: tooltipAttr.color,
'font-size': `${tooltipAttr.fontSize}px`
}
},
render: (_, { items }) => {
const head = items[0]
const title = head.field
const titleHtml = TOOLTIP_TITLE_TPL.replace('{title}', title)
const validField = []
if (tooltipAttr.seriesTooltipFormatter?.length) {
const seriesId = `${head.quotaList[0].id}-yAxis`
let formatter = formatterMap[seriesId]
if (formatter) {
validField.push('value')
}
if (extBubble?.length) {
const extBubbleId = `${head.quotaList[1].id}-extBubble`
formatter = formatterMap[extBubbleId]
if (formatter) {
validField.push('popSize')
}
}
} else {
validField.push('value')
if (extBubble?.length) {
validField.push('popSize')
}
}
const result = []
validField.forEach(field => {
let seriesId = `${head.quotaList[0].id}-yAxis`
if (field === 'popSize') {
seriesId = `${head.quotaList[1].id}-extBubble`
}
const formatter = formatterMap[seriesId] ?? yAxis[0]
const value = valueFormatter(head[field], formatter.formatterCfg)
const name = isEmpty(formatter.chartShowName)
? formatter.name
: formatter.chartShowName
result.push({ color: head.color, name, value })
})
head.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 })
}
})
const itemsHtml = result
.map(item => {
const marker = item.color
const label = item.name
const value = item.value
return TOOLTIP_ITEM_TPL.replace('{marker}', marker)
.replace('{label}', label)
.replace('{value}', value)
})
.join('')
const listHtml = `<ul class="g2-tooltip-list" style="margin: 0px; list-style-type: none; padding: 0px;">${itemsHtml}</ul>`
return `${titleHtml}${listHtml}`
}
}
}
}
return defaultsDeep(options, tooltipOptions)
}
protected configXAxis(chart: Chart, options: G2Spec): G2Spec {
const { xAxis } = parseJson(chart.customStyle)
if (!xAxis.show) {
const axisHide = {
axis: {
x: false
}
}
return defaultsDeep(options, axisHide)
}
let lineLineDash = undefined
if (xAxis.axisLine.lineStyle.style === 'dashed') {
lineLineDash = [10, 8]
}
if (xAxis.axisLine.lineStyle.style === 'dotted') {
lineLineDash = [1, 2]
}
let gridLineDash = undefined
if (xAxis.splitLine.lineStyle.style === 'dashed') {
gridLineDash = [10, 8]
}
if (xAxis.splitLine.lineStyle.style === 'dotted') {
gridLineDash = [1, 2]
}
const axisStyle = {
axis: {
x: {
position: xAxis.position,
title: xAxis.nameShow === false ? false : xAxis.name,
titleFontSize: xAxis.fontSize,
titleFill: xAxis.color,
line: xAxis.axisLine.show,
lineStroke: xAxis.axisLine.lineStyle.color,
lineStrokeOpacity: 1,
lineLineWidth: xAxis.axisLine.lineStyle.width,
lineLineDash,
label: xAxis.axisLabel.show,
labelFill: xAxis.axisLabel.color,
labelFillOpacity: 1,
labelFontSize: xAxis.axisLabel.fontSize,
grid: xAxis.splitLine.show,
gridStroke: xAxis.splitLine.lineStyle.color,
gridStrokeOpacity: 1,
gridLineWidth: xAxis.splitLine.lineStyle.width,
gridLineDash,
transform: xAxis.axisLabel.rotate
? [
{
type: 'rotate',
optionalAngles: [xAxis.axisLabel.rotate],
recoverWhenFailed: false
}
]
: []
}
}
}
return defaultsDeep(options, axisStyle)
}
protected configYAxis(chart: Chart, options: G2Spec): G2Spec {
const { yAxis } = parseJson(chart.customStyle)
if (!yAxis.show) {
const axisHide = {
axis: {
y: false
}
}
return defaultsDeep(options, axisHide)
}
let lineLineDash = undefined
if (yAxis.axisLine.lineStyle.style === 'dashed') {
lineLineDash = [10, 8]
}
if (yAxis.axisLine.lineStyle.style === 'dotted') {
lineLineDash = [1, 2]
}
let gridLineDash = [0, 0]
if (yAxis.splitLine.lineStyle.style === 'dashed') {
gridLineDash = [10, 8]
}
if (yAxis.splitLine.lineStyle.style === 'dotted') {
gridLineDash = [1, 2]
}
const axisOption = {
axis: {
y: {
position: yAxis.position,
title: yAxis.nameShow === false ? false : yAxis.name,
titleFontSize: yAxis.fontSize,
titleFill: yAxis.color,
line: yAxis.axisLine.show,
lineStroke: yAxis.axisLine.lineStyle.color,
lineStrokeOpacity: 1,
lineLineWidth: yAxis.axisLine.lineStyle.width,
lineLineDash,
label: yAxis.axisLabel.show,
labelFill: yAxis.axisLabel.color,
labelFillOpacity: 1,
labelFontSize: yAxis.axisLabel.fontSize,
grid: yAxis.splitLine.show,
gridStroke: yAxis.splitLine.lineStyle.color,
gridStrokeOpacity: 1,
gridLineWidth: yAxis.splitLine.lineStyle.width,
gridLineDash,
transform: yAxis.axisLabel.rotate
? [
{
type: 'rotate',
optionalAngles: [yAxis.axisLabel.rotate],
recoverWhenFailed: false
}
]
: [],
labelFormatter: d => {
return valueFormatter(d, yAxis.axisLabelFormatter)
}
}
}
}
if (!yAxis.axisValue.auto) {
const scaleOpt = {
scale: {
y: {
domainMin: yAxis.axisValue.min,
domainMax: yAxis.axisValue.max,
tickCount: yAxis.axisValue.splitCount,
tickMethod: (min, max, count) => {
const step = (max - min) / count
const ticks = []
for (let i = 0; i <= count; i++) {
ticks.push(min + step * i)
}
return ticks
}
}
}
}
defaultsDeep(axisOption, scaleOpt)
}
return defaultsDeep(options, axisOption)
}
protected configLegend(chart: Chart, options: G2Spec): G2Spec {
const { legend } = parseJson(chart.customStyle)
if (!legend.show) {
return { ...options, legend: false }
}
const baseLegend = this.getLegend(chart)
const tmpLegend = {
legend: {
color: {
...baseLegend,
itemMarkerSize: legend.size,
itemMarker: legend.icon
}
}
}
defaultsDeep(options, tmpLegend)
return options
}
protected setupOptions(chart: Chart, options: G2Spec) {
return flow(
this.configTheme,
this.configColor,
this.configBasicStyle,
this.configLabel,
this.configTooltip,
this.configXAxis,
this.configYAxis,
this.configLegend
)(chart, options, {}, this)
}
constructor() {
super('scatter', [])
}
}