feat(图表): 支持热力图

* style: import optimize

* feat(图表): 支持热力图
This commit is contained in:
wisonic-s
2025-05-15 14:54:59 +08:00
committed by GitHub
parent dd79cda761
commit 04330f4d9f
2 changed files with 506 additions and 3 deletions

View File

@@ -1,7 +1,5 @@
import {
filterChartDataByRange,
flow,
getMaxAndMinValueByData,
hexColorToRGBA,
parseJson,
setUpSingleDimensionSeriesColor
@@ -9,7 +7,6 @@ import {
import { valueFormatter } from '@/views/chart/components/js/formatter'
import { useI18n } from '@/hooks/web/useI18n'
import { defaultsDeep, isEmpty } from 'lodash-es'
import { DEFAULT_MISC } from '@/views/chart/components/editor/util/chart'
import { Chart as G2Chart, G2Spec } from '@antv/g2'
import { G2ChartView, G2DrawOptions } from '../../../types/impl/g2'
import {
@@ -228,6 +225,7 @@ export class Treemap extends G2ChartView {
}
return defaultsDeep(options, tooltipOptions)
}
protected configLabel(chart: Chart, options: G2Spec, context: Record<string, any>): G2Spec {
const customAttr: DeepPartial<ChartAttr> = parseJson(chart.customAttr)
const labelAttr = customAttr.label

View File

@@ -0,0 +1,505 @@
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, toString } from 'lodash-es'
import { ChartEvent, Chart as G2Chart, G2Spec } from '@antv/g2'
import { Text } from '@antv/g'
const { t } = useI18n()
const DEFAULT_DATA = []
/**
* 热力图
*/
export class TableG2Chart extends G2ChartView {
properties: EditorProperty[] = [
'basic-style-selector',
'background-overall-component',
'label-selector',
'legend-selector',
'x-axis-selector',
'y-axis-selector',
'title-selector',
'tooltip-selector',
'jump-set',
'linkage',
'border-style'
]
propertyInner: EditorPropertyInner = {
'background-overall-component': ['all'],
'basic-style-selector': ['colors'],
'label-selector': ['fontSize', 'color'],
'x-axis-selector': ['name', 'color', 'fontSize', 'position', 'axisLabel', 'axisLine'],
'y-axis-selector': [
'name',
'color',
'fontSize',
'position',
'axisLabel',
'axisLine',
'showLengthLimit'
],
'title-selector': [
'title',
'fontSize',
'color',
'hPosition',
'isItalic',
'isBolder',
'remarkShow',
'fontFamily',
'letterSpace',
'fontShadow'
],
'legend-selector': ['orient', 'color', 'fontSize', 'hPosition', 'vPosition'],
'tooltip-selector': ['show', 'color', 'fontSize', 'backgroundColor'],
'border-style': ['all']
}
axis: AxisType[] = ['xAxis', 'xAxisExt', 'extColor', 'filter']
axisConfig: AxisConfig = {
xAxis: {
name: `${t('chart.x_axis')} / ${t('chart.dimension')}`,
type: 'd',
limit: 1
},
xAxisExt: {
name: `${t('chart.y_axis')} / ${t('chart.dimension')}`,
type: 'd',
limit: 1
},
extColor: {
name: `${t('chart.color')} / ${t('chart.dimension_or_quota')}`,
limit: 1
}
}
protected getDefaultLength = (chart, l) => {
const containerDom = document.getElementById(chart.container)
const containerHeight = containerDom?.clientHeight || 100
const containerWidth = containerDom?.clientWidth || 100
let defaultLength = containerHeight - containerHeight * 0.5
if (l.orient !== 'vertical') {
defaultLength = containerWidth - containerWidth * 0.5
}
return defaultLength
}
async drawChart(drawOptions: G2DrawOptions<G2Chart>): Promise<G2Chart> {
const { chart, container, action } = drawOptions
const { xAxis, xAxisExt, extColor } = chart
if (!xAxis?.length || !xAxisExt?.length || !extColor?.length) {
return
}
const xField = xAxis[0].dataeaseName
const xFieldExt = xAxisExt[0].dataeaseName
const extColorField = extColor[0].dataeaseName
// data
const data = chart.data.tableRow
// options
const initOptions: G2Spec = {
type: 'cell',
autoFit: true,
data: {
value: data
},
encode: {
x: xField,
y: xFieldExt,
color: extColorField
}
}
const axisMap = {
[chart.xAxis[0].dataeaseName]: chart.xAxis[0].chartShowName ?? chart.xAxis[0].name,
[chart.xAxisExt[0].dataeaseName]: chart.xAxisExt[0].chartShowName ?? chart.xAxisExt[0].name,
[chart.extColor[0].dataeaseName]: chart.extColor[0].chartShowName ?? chart.extColor[0].name
}
chart.container = container
const options = this.setupOptions(chart, initOptions, { axisMap, container })
const newChart = new G2Chart({ container })
newChart.options(options)
newChart.on('plot:click', param => {
if (!param.data?.data) {
return
}
const pointData = param.data.data
const dimensionList = []
chart.data.fields.forEach(item => {
Object.keys(pointData).forEach(key => {
if (key.startsWith('f_') && item.dataeaseName === key) {
dimensionList.push({
id: item.id,
dataeaseName: item.dataeaseName,
value: pointData[key]
})
}
})
})
action({
x: param.data.x,
y: param.data.y,
data: {
data: {
...param.data.data,
value: dimensionList[1].value,
name: dimensionList[1].id,
dimensionList: dimensionList,
quotaList: [dimensionList[1]]
}
}
})
})
this.configYAxisLabelLimit(newChart, chart)
return newChart
}
protected configYAxisLabelLimit(chartObj: G2Chart, chart: Chart) {
const { yAxis } = parseJson(chart.customStyle)
if (!yAxis.show || !yAxis.axisLabel.show || !yAxis.axisLabel.lengthLimit) {
return
}
const { tooltip } = parseJson(chart.customAttr)
const labelTipId = `AXIS_LABEL_TIP-${chart.id}`
chartObj.on(`axis-label-item:${ChartEvent.POINTER_OVER}`, e => {
const target = e.target
const { text, originValue } = target.attributes
if (!originValue) {
return
}
if (text !== originValue) {
let parentDom = document.getElementById('G2-TOOLTIP-WRAPPER')
if (!parentDom) {
parentDom = document.createElement('div')
parentDom.id = 'G2-TOOLTIP-WRAPPER'
parentDom.style.position = 'absolute'
parentDom.style.pointerEvents = 'none'
parentDom.style.zIndex = '9999'
document.body.appendChild(parentDom)
}
let labelTipDom = document.getElementById(labelTipId)
if (!labelTipDom) {
labelTipDom = document.createElement('div')
labelTipDom.id = labelTipId
labelTipDom.style.position = 'fixed'
labelTipDom.style.color = tooltip.color
labelTipDom.style.backgroundColor = tooltip.backgroundColor
labelTipDom.style.fontSize = `${tooltip.fontSize}px`
labelTipDom.style.padding = '5px 20px'
labelTipDom.style.boxShadow = 'rgba(0, 0, 0, 0.12) 0px 6px 12px 0px'
labelTipDom.style.borderRadius = '4px'
labelTipDom.style.transition =
'visibility 0.2s cubic-bezier(0.23, 1, 0.32, 1), left 0.4s cubic-bezier(0.23, 1, 0.32, 1), top 0.4s cubic-bezier(0.23, 1, 0.32, 1)'
parentDom.appendChild(labelTipDom)
}
labelTipDom.innerText = originValue
labelTipDom.style.visibility = 'visible'
labelTipDom.style.left = `${e.client.x + 30}px`
labelTipDom.style.top = `${e.client.y + 20}px`
}
})
chartObj.on(`axis-label-item:${ChartEvent.POINTER_OUT}`, e => {
const target = e.target
const { originValue } = target.attributes
if (!originValue) {
return
}
const labelTipDom = document.getElementById(labelTipId)
if (labelTipDom) {
labelTipDom.style.visibility = 'hidden'
}
})
}
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 configLegend(chart: Chart, options: G2Spec, context: Record<string, any>): 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,
title: false
}
}
}
defaultsDeep(options, tmpLegend)
const colorField = chart.extColor[0]
if (colorField.groupType === 'q') {
const { container } = context
const containerDom = document.getElementById(container)
const colors = options.theme.category10
const quotaLegendOption = {
scale: {
color: {
interpolate: () => {
return c => colors[Math.floor(c * (colors.length - 1))]
}
}
},
legend: {
color: {
color: colors,
label: false
}
}
}
if (legend.orient === 'vertical') {
quotaLegendOption.legend.color.height = containerDom?.offsetHeight / 2
} else {
quotaLegendOption.legend.color.width = containerDom?.offsetWidth / 2
}
defaultsDeep(options, quotaLegendOption)
}
return options
}
protected configLabel(chart: Chart, options: G2Spec): G2Spec {
const { label } = parseJson(chart.customAttr)
if (!label.show) {
return options
}
const labelStyle = {
labels: [
{
text: chart.extColor[0].dataeaseName,
position: 'inside',
style: {
fill: label.color,
fontSize: label.fontSize
},
transform: label.fullDisplay ? [] : [{ type: 'overflowHide' }]
}
]
}
return defaultsDeep(options, labelStyle)
}
protected configTooltip(chart: Chart, options: G2Spec, context: Record<string, any>): G2Spec {
const { tooltip } = parseJson(chart.customAttr)
if (!tooltip.show) {
return {
...options,
tooltip: false
}
}
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 { axisMap } = context
const tooltipOptions: G2Spec = {
tooltip: d => d,
interaction: {
tooltip: {
mount: g2TooltipWrapper,
css: {
'.g2-tooltip': {
background: tooltip.backgroundColor
},
'.g2-tooltip-title': {
color: tooltip.color,
'font-size': `${tooltip.fontSize}px`
},
'.g2-tooltip-list-item-name-label': {
color: tooltip.color,
'font-size': `${tooltip.fontSize}px`
},
'.g2-tooltip-list-item-value': {
color: tooltip.color,
'font-size': `${tooltip.fontSize}px`
}
},
render: (_, { items }) => {
const xField = chart.xAxis[0].dataeaseName
const yField = chart.xAxisExt[0].dataeaseName
const colorField = chart.extColor[0].dataeaseName
const head = items[0]
const title = head[xField]
const titleHtml = TOOLTIP_TITLE_TPL.replace('{title}', title)
const result = [
{ marker: head.color, label: axisMap[yField], value: head[yField] },
{ marker: head.color, label: axisMap[colorField], value: head[colorField] }
]
const itemsHtml = result
.map(({ marker, label, 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]
}
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,
lineLineWidth: xAxis.axisLine.lineStyle.width,
lineLineDash,
label: xAxis.axisLabel.show,
labelFill: xAxis.axisLabel.color,
labelFontSize: xAxis.axisLabel.fontSize,
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]
}
const axisStyle = {
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,
lineLineWidth: yAxis.axisLine.lineStyle.width,
lineLineDash,
label: yAxis.axisLabel.show,
labelFill: yAxis.axisLabel.color,
labelFontSize: yAxis.axisLabel.fontSize,
transform: yAxis.axisLabel.rotate
? [
{
type: 'rotate',
optionalAngles: [yAxis.axisLabel.rotate],
recoverWhenFailed: false
}
]
: [],
labelFormatter: d => {
const str = toString(d)
if (!str) {
return ''
}
const lengthLimit = yAxis.axisLabel.lengthLimit
if (lengthLimit) {
const strLength = str.length
if (strLength > lengthLimit) {
const ellipsisText = str.substring(0, lengthLimit) + '...'
return new Text({
style: {
text: ellipsisText,
originValue: `${d}`
}
})
}
}
return d
}
}
}
}
return defaultsDeep(options, axisStyle)
}
setupDefaultOptions(chart: ChartObj): ChartObj {
chart.customStyle.legend.orient = 'vertical'
chart.customStyle.legend.vPosition = 'center'
chart.customStyle.legend.hPosition = 'right'
chart.customStyle.legend['rail'] = { defaultLength: 100 }
return chart
}
protected setupOptions(chart: Chart, options: G2Spec, context: Record<string, any>): G2Spec {
return flow(
this.configTheme,
this.configLegend,
this.configLabel,
this.configTooltip,
this.configXAxis,
this.configYAxis
)(chart, options, context, this)
}
constructor() {
super('t-heatmap', DEFAULT_DATA)
}
}