Merge pull request #11225 from dataease/pr@dev-v2@chart-extremum-slider-fix

fix(图表): 处理快速滑动缩略轴导致最值显示不正确的问题,以及简化显示最值的代码逻辑
This commit is contained in:
jianneng-fit2cloud 2024-07-29 16:51:08 +08:00 committed by GitHub
commit c9dc374560
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 287 additions and 506 deletions

View File

@ -0,0 +1,224 @@
import { valueFormatter } from '@/views/chart/components/js/formatter'
import { parseJson } from '@/views/chart/components/js/util'
import { isEmpty } from 'lodash-es'
export const clearExtremum = chart => {
// 清除图表标注
const pointElement = document.getElementById('point_' + chart.id)
if (pointElement) {
pointElement.remove()
pointElement.parentNode?.removeChild(pointElement)
}
}
/**
* 判断给定的RGBA字符串表示的颜色是亮色还是暗色
* 通过计算RGB颜色值的加权平均值灰度值判断颜色的明暗
* 如果给定的字符串不包含有效的RGBA值则原样返回该字符串
*
* @param rgbaString 一个RGBA颜色字符串例如 "rgba(255, 255, 255, 1)"
* @param greyValue 灰度值默认128
* @returns 如果计算出的灰度值大于等于128则返回true表示亮色否则返回false表示暗色
* 如果rgbaString不包含有效的RGBA值则返回原字符串
*/
const isColorLight = (rgbaString: string, greyValue = 128) => {
const lastRGBA = getRgbaColorLastRgba(rgbaString)
if (!isEmpty(lastRGBA)) {
// 计算灰度值的公式
const grayLevel = lastRGBA.r * 0.299 + lastRGBA.g * 0.587 + lastRGBA.b * 0.114
return grayLevel >= greyValue
} else {
return false
}
}
/**
* 从给定的rgba颜色字符串中提取最后一个rgba值
* @param rgbaString 包含一个或多个rgba颜色值的字符串
* @returns 返回最后一个解析出的rgba对象如果未找到rgba值则返回null
*/
const getRgbaColorLastRgba = (rgbaString: string) => {
const rgbaPattern = /rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)/g
let match: string[]
let lastRGBA = null
while ((match = rgbaPattern.exec(rgbaString)) !== null) {
const r = parseInt(match[1])
const g = parseInt(match[2])
const b = parseInt(match[3])
const a = parseFloat(match[4])
lastRGBA = { r, g, b, a }
}
return lastRGBA
}
function createExtremumDiv(id, value, formatterCfg, chartId) {
// 装标注的div
const parentElement = document.getElementById('point_' + chartId)
if (parentElement) {
// 标注div
const element = document.getElementById(id)
if (element) {
return
}
const div = document.createElement('div')
div.id = id
div.setAttribute(
'style',
`width: auto;
height: auto;
border-radius: 2px;
position: relative;
padding: 4px 5px 4px 5px;
display:none;
transform: translateX(-50%);
white-space:nowrap;`
)
div.textContent = valueFormatter(value, formatterCfg)
const span = document.createElement('span')
span.setAttribute(
'style',
`display: block;
width: 0px;
height: 0px;
border: 4px solid transparent;
border-top-color: red;
position: absolute;
left: calc(50% - 4px);
margin-top:4px;`
)
div.appendChild(span)
parentElement.appendChild(div)
}
}
/**
* 没有子类别字段的图表
* @param chart
*/
const noChildrenFieldChart = chart => {
return ['area', 'bar'].includes(chart.type)
}
export const extremumEvt = (newChart, chart, _options, container) => {
chart.container = container
newChart.on('afterrender', ev => {
createExtremumPoint(chart, ev)
})
}
const findMinMax = (data): { minItem; maxItem } => {
return data.reduce(
({ minItem, maxItem }, currentItem) => {
if (minItem === undefined || currentItem._origin.value < minItem._origin.value) {
minItem = currentItem
}
if (maxItem === undefined || currentItem._origin.value > maxItem._origin.value) {
maxItem = currentItem
}
return { minItem, maxItem }
},
{ minItem: undefined, maxItem: undefined }
)
}
export const createExtremumPoint = (chart, ev) => {
// 获取标注样式
const { label: labelAttr, basicStyle } = parseJson(chart.customAttr)
const pointSize = basicStyle.lineSymbolSize
const { yAxis } = parseJson(chart)
clearExtremum(chart)
// 创建标注父元素
const divParentElement = document.getElementById('point_' + chart.id)
if (!divParentElement) {
const divParent = document.createElement('div')
divParent.id = 'point_' + chart.id
divParent.style.position = 'fixed'
divParent.style.zIndex = '1'
// 将父标注加入到图表中
const containerElement = document.getElementById(chart.container)
containerElement.insertBefore(divParent, containerElement.firstChild)
}
let geometriesDataArray = []
// 获取数据点
const intervalPoint = ev.view
.getGeometries()
.find((intervalItem: { type: string }) => intervalItem.type === 'interval')
if (intervalPoint) {
geometriesDataArray = intervalPoint.dataArray
}
const pointPoint = ev.view
.getGeometries()
.find((pointItem: { type: string }) => pointItem.type === 'point')
if (pointPoint) {
geometriesDataArray = pointPoint.dataArray
}
geometriesDataArray?.forEach(pointObjList => {
if (pointObjList && pointObjList.length > 0) {
const pointObj = pointObjList[0]
const { minItem, maxItem } = findMinMax(pointObjList.reverse())
let attr
let showExtremum = false
if (noChildrenFieldChart(chart) || yAxis.length > 1) {
const seriesLabelFormatter = labelAttr.seriesLabelFormatter.find(
d => d.name === pointObj._origin.category
)
showExtremum = seriesLabelFormatter?.showExtremum
attr = seriesLabelFormatter
} else {
showExtremum = labelAttr.seriesLabelFormatter[0]?.showExtremum
attr = labelAttr.seriesLabelFormatter[0]
}
const fontSize = attr ? attr.fontSize : labelAttr.fontSize
const maxKey = 'point_' + pointObj._origin.category + '-' + maxItem._origin.value
const minKey = 'point_' + pointObj._origin.category + '-' + minItem._origin.value
// 最值标注
if (showExtremum) {
createExtremumDiv(
maxKey,
maxItem._origin.value,
attr ? attr.formatterCfg : labelAttr.labelFormatter,
chart.id
)
createExtremumDiv(
minKey,
minItem._origin.value,
attr ? attr.formatterCfg : labelAttr.labelFormatter,
chart.id
)
pointObjList.forEach(point => {
const pointElement = document.getElementById(
'point_' + point._origin.category + '-' + point._origin.value
)
if (pointElement) {
pointElement.style.position = 'absolute'
pointElement.style.position = 'absolute'
pointElement.style.top =
(point.y[1] ? point.y[1] : point.y) -
(fontSize + (pointSize ? pointSize : 0) + 12) +
'px'
pointElement.style.left = point.x + 'px'
pointElement.style.zIndex = '10'
pointElement.style.fontSize = fontSize + 'px'
pointElement.style.lineHeight = fontSize + 'px'
// 渐变颜色时需要获取最后一个rgba的值作为背景
const { r, b, g, a } = getRgbaColorLastRgba(point.color)
pointElement.style.backgroundColor = 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'
pointElement.style.color = isColorLight(point.color) ? '#000' : '#fff'
pointElement.children[0]['style'].borderTopColor =
'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'
pointElement.style.display = 'table'
}
})
} else {
removeDivElement(maxKey)
removeDivElement(minKey)
}
}
})
function removeDivElement(key) {
const element = document.getElementById(key)
if (element) {
element.remove()
element.parentNode?.removeChild(element)
}
}
}

View File

@ -8,8 +8,6 @@ import {
flow,
hexColorToRGBA,
parseJson,
registerExtremumPointEvt,
setExtremumPosition,
setUpGroupSeriesColor,
setUpStackSeriesColor
} from '@/views/chart/components/js/util'
@ -23,6 +21,7 @@ import {
import { getPadding, setGradientColor } from '@/views/chart/components/js/panel/common/common_antv'
import { useI18n } from '@/hooks/web/useI18n'
import { DEFAULT_LABEL } from '@/views/chart/components/editor/util/chart'
import { clearExtremum, extremumEvt } from '@/views/chart/components/js/extremumUitl'
const { t } = useI18n()
const DEFAULT_DATA: any[] = []
@ -97,6 +96,7 @@ export class Bar extends G2PlotChartView<ColumnOptions, Column> {
drawChart(drawOptions: G2PlotDrawOptions<Column>): Column {
const { chart, container, action } = drawOptions
if (!chart?.data?.data?.length) {
clearExtremum(chart)
return
}
const data = cloneDeep(drawOptions.chart.data?.data)
@ -109,7 +109,7 @@ export class Bar extends G2PlotChartView<ColumnOptions, Column> {
const newChart = new Column(container, options)
newChart.on('interval:click', action)
registerExtremumPointEvt(newChart, chart, options, container)
extremumEvt(newChart, chart, options, container)
return newChart
}
@ -121,7 +121,7 @@ export class Bar extends G2PlotChartView<ColumnOptions, Column> {
label: false
}
}
const { label: labelAttr, basicStyle } = parseJson(chart.customAttr)
const { label: labelAttr } = parseJson(chart.customAttr)
const formatterMap = labelAttr.seriesLabelFormatter?.reduce((pre, next) => {
pre[next.id] = next
return pre
@ -131,7 +131,7 @@ export class Bar extends G2PlotChartView<ColumnOptions, Column> {
const label = {
fields: [],
...tmpOptions.label,
formatter: (data: Datum, point) => {
formatter: (data: Datum, _point) => {
if (!labelAttr.seriesLabelFormatter?.length) {
return data.value
}
@ -139,31 +139,24 @@ export class Bar extends G2PlotChartView<ColumnOptions, Column> {
if (!labelCfg) {
return data.value
}
let showLabel = true
if (labelCfg.showExtremum) {
showLabel = setExtremumPosition(data, point, chart, labelCfg, basicStyle.lineSymbolSize)
}
if (!labelCfg.show) {
return
}
if (showLabel) {
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
}
return null
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
}
}
return {
@ -352,20 +345,13 @@ export class GroupBar extends StackBar {
if (!baseOptions.label) {
return baseOptions
}
const { label: labelAttr, basicStyle } = parseJson(chart.customAttr)
const { label: labelAttr } = parseJson(chart.customAttr)
baseOptions.label.style.fill = labelAttr.color
const label = {
...baseOptions.label,
formatter: function (param: Datum, point) {
let showLabel = true
if (labelAttr.showExtremum) {
showLabel = setExtremumPosition(param, point, chart, labelAttr, basicStyle.lineSymbolSize)
}
if (!labelAttr.childrenShow) {
return null
}
formatter: function (param: Datum, _point) {
const value = valueFormatter(param.value, labelAttr.labelFormatter)
return showLabel ? value : null
return labelAttr.childrenShow ? value : null
}
}
return {

View File

@ -9,8 +9,6 @@ import {
flow,
hexColorToRGBA,
parseJson,
registerExtremumPointEvt,
setExtremumPosition,
setUpStackSeriesColor
} from '@/views/chart/components/js/util'
import { valueFormatter } from '@/views/chart/components/js/formatter'
@ -23,6 +21,7 @@ import { Label } from '@antv/g2plot/lib/types/label'
import { Datum } from '@antv/g2plot/esm/types/common'
import { useI18n } from '@/hooks/web/useI18n'
import { DEFAULT_LABEL } from '@/views/chart/components/editor/util/chart'
import { clearExtremum, extremumEvt } from '@/views/chart/components/js/extremumUitl'
const { t } = useI18n()
const DEFAULT_DATA = []
@ -98,6 +97,7 @@ export class Area extends G2PlotChartView<AreaOptions, G2Area> {
drawChart(drawOptions: G2PlotDrawOptions<G2Area>): G2Area {
const { chart, container, action } = drawOptions
if (!chart.data.data?.length) {
clearExtremum(chart)
return
}
// data
@ -114,7 +114,7 @@ export class Area extends G2PlotChartView<AreaOptions, G2Area> {
const newChart = new G2Area(container, options)
newChart.on('point:click', action)
registerExtremumPointEvt(newChart, chart, options, container)
extremumEvt(newChart, chart, options, container)
return newChart
}
@ -126,7 +126,7 @@ export class Area extends G2PlotChartView<AreaOptions, G2Area> {
label: false
}
}
const { label: labelAttr, basicStyle } = parseJson(chart.customAttr)
const { label: labelAttr } = parseJson(chart.customAttr)
const formatterMap = labelAttr.seriesLabelFormatter?.reduce((pre, next) => {
pre[next.id] = next
return pre
@ -135,7 +135,7 @@ export class Area extends G2PlotChartView<AreaOptions, G2Area> {
const label = {
fields: [],
...tmpOptions.label,
formatter: (data: Datum, point) => {
formatter: (data: Datum, _point) => {
if (!labelAttr.seriesLabelFormatter?.length) {
return data.value
}
@ -143,34 +143,24 @@ export class Area extends G2PlotChartView<AreaOptions, G2Area> {
if (!labelCfg) {
return data.value
}
let showLabel = true
if (labelCfg.showExtremum) {
showLabel = setExtremumPosition(data, point, chart, labelCfg, basicStyle.lineSymbolSize)
}
if (!labelCfg.show) {
return
}
const has = chart.filteredData?.filter(
item => JSON.stringify(item) === JSON.stringify(data)
)
if (has?.length > 0 && showLabel) {
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
}
return null
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
}
}
return {

View File

@ -8,8 +8,6 @@ import {
flow,
hexColorToRGBA,
parseJson,
registerExtremumPointEvt,
setExtremumPosition,
setUpGroupSeriesColor
} from '@/views/chart/components/js/util'
import { cloneDeep, isEmpty } from 'lodash-es'
@ -22,6 +20,7 @@ import {
import { Datum } from '@antv/g2plot/esm/types/common'
import { useI18n } from '@/hooks/web/useI18n'
import { DEFAULT_LABEL } from '@/views/chart/components/editor/util/chart'
import { clearExtremum, extremumEvt } from '@/views/chart/components/js/extremumUitl'
const { t } = useI18n()
const DEFAULT_DATA = []
@ -50,6 +49,7 @@ export class Line extends G2PlotChartView<LineOptions, G2Line> {
drawChart(drawOptions: G2PlotDrawOptions<G2Line>): G2Line {
const { chart, action, container } = drawOptions
if (!chart.data.data?.length) {
clearExtremum(chart)
return
}
const data = cloneDeep(chart.data.data)
@ -109,7 +109,7 @@ export class Line extends G2PlotChartView<LineOptions, G2Line> {
const newChart = new G2Line(container, options)
newChart.on('point:click', action)
registerExtremumPointEvt(newChart, chart, options, container)
extremumEvt(newChart, chart, options, container)
return newChart
}
@ -121,7 +121,7 @@ export class Line extends G2PlotChartView<LineOptions, G2Line> {
label: false
}
}
const { label: labelAttr, basicStyle } = parseJson(chart.customAttr)
const { label: labelAttr } = parseJson(chart.customAttr)
const formatterMap = labelAttr.seriesLabelFormatter?.reduce((pre, next) => {
pre[next.id] = next
return pre
@ -132,7 +132,7 @@ export class Line extends G2PlotChartView<LineOptions, G2Line> {
...tmpOptions.label,
offsetY: -8,
layout: [{ type: 'hide-overlap' }, { type: 'limit-in-plot' }],
formatter: (data: Datum, point) => {
formatter: (data: Datum, _point) => {
if (!labelAttr.seriesLabelFormatter?.length) {
return data.value
}
@ -140,34 +140,24 @@ export class Line extends G2PlotChartView<LineOptions, G2Line> {
if (!labelCfg) {
return data.value
}
let showLabel = true
if (labelCfg.showExtremum) {
showLabel = setExtremumPosition(data, point, chart, labelCfg, basicStyle.lineSymbolSize)
}
if (!labelCfg.show) {
return
}
const has = chart.filteredData?.filter(
item => JSON.stringify(item) === JSON.stringify(data)
)
if (has?.length > 0 && showLabel) {
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
}
return null
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
}
}
return {

View File

@ -942,415 +942,6 @@ export function setUpSingleDimensionSeriesColor(
return result
}
/**
* 注册极值点事件处理函数
* 该函数用于在新建的图表上注册极值点显示的事件处理逻辑根据图表类型和配置数据处理极值点的显示
*
* @param newChart 新建的图表对象用于绑定事件
* @param chart 原有的图表对象用于存储处理后的数据和配置
* @param options 图表的配置选项包含数据和各种配置项
* @param container 图表的容器用于设置图表的容器
*/
export const registerExtremumPointEvt = (newChart, chart, options, container) => {
chart.container = container
const { label: labelAttr } = parseJson(chart.customAttr)
let seriesFields = []
// 针对不是序列字段的图表通过获取分类字段的值作为序列字段,在标签配置时使用
const seriesFieldObjs = []
// 分组柱状图这种字段分类的图表需要按照分类字段的值作为序列字段
const xAxisExt = chart.xAxisExt || []
if (['bar-group'].includes(chart.type) || xAxisExt.length > 0) {
seriesFields = [...new Set(options.data.map(item => item.category))]
if (xAxisExt.length === 0) {
seriesFields = ['@']
}
seriesFields.forEach(field => {
seriesFieldObjs.push({
dataeaseName:
xAxisExt.length === 0
? 'f_' + chart.xAxis[0].dataeaseName + '_' + field
: chart.xAxisExt[0]?.dataeaseName + '_' + field,
name: field,
showExtremum: true,
formatterCfg: labelAttr.labelFormatter
})
})
chart.seriesFieldObjs = seriesFieldObjs
} else {
seriesFields = chart.yAxis.map(item => item.name)
}
// 筛选数据区间默认所有数据
let filterDataRange = [0, options.data.length - 1]
const senior = parseJson(chart.senior)
// 高级配置了缩略轴按照缩略轴默认配置进行区间配置
if (senior.functionCfg) {
if (senior.functionCfg.sliderShow) {
const cfg = {
start: senior.functionCfg.sliderRange[0] / 100,
end: senior.functionCfg.sliderRange[1] / 100
}
const dataLength = options.data.length / seriesFields.length
// 使用round方法与antv 内置过滤数据方式一致否则会出现数据区间错误
const startIndex = Math.round(cfg.start * (dataLength - 1))
const endIndex = Math.round(cfg.end * (dataLength - 1))
filterDataRange = [startIndex, endIndex]
}
}
// 通过区间筛选的数据
const filteredData = []
// 如果是根据字段值分类的图表时并且没有子类别时
if (seriesFieldObjs.length > 0 && chart.xAxisExt[0]) {
// 按照字段值分类维度聚合数据
const fieldGroupList = options.data.reduce((groups, item) => {
const field = item.field
if (!groups[field]) {
groups[field] = []
}
groups[field].push(item)
return groups
}, {})
// 需要重新计算数据区间因为数据区间是根据字段值分类维度聚合的数据
if (senior.functionCfg) {
if (senior.functionCfg.sliderShow) {
const cfg = {
start: senior.functionCfg.sliderRange[0] / 100,
end: senior.functionCfg.sliderRange[1] / 100
}
const dataLength = Object.keys(fieldGroupList).length
const startIndex = Math.round(cfg.start * (dataLength - 1))
const endIndex = Math.round(cfg.end * (dataLength - 1))
filterDataRange = [startIndex, endIndex]
Object.keys(fieldGroupList)
.slice(filterDataRange[0], filterDataRange[1] + 1)
.forEach(field => {
filteredData.push(...fieldGroupList[field])
})
}
}
} else {
seriesFields.forEach(field => {
const seriesFieldData = options.data.filter(
item => (item.category === '' ? '@' : item.category) === field
)
filteredData.push(...seriesFieldData.slice(filterDataRange[0], filterDataRange[1] + 1))
})
}
chart.filteredData = filteredData
if (options.legend) {
newChart.on('legend-item:click', ev => {
hideExtremumPoint(ev, chart)
})
}
if (options.slider) {
newChart.once('slider:valuechanged', _ev => {
newChart.on('beforerender', ev => {
sliderHandleExtremumPoint(ev, chart, options)
})
})
}
configExtremum(chart)
}
/**
* 创建极值point
* @param key
* @param value
* @param formatterCfg
* @param chartId
*/
const createExtremumPointDiv = (key, value, formatterCfg, chartId) => {
const id = key.split('@')[1] + '_' + value
const parentElement = document.getElementById(chartId)
if (parentElement) {
const element = document.getElementById(id)
if (!element) {
const div = document.createElement('div')
div.id = id
div.setAttribute(
'style',
`width: auto;
height: auto;
border-radius: 2px;
position: relative;
padding: 4px 5px 4px 5px;
display:none;
transform: translateX(-50%);
white-space:nowrap;`
)
div.textContent = valueFormatter(value, formatterCfg)
const span = document.createElement('span')
span.setAttribute(
'style',
`display: block;
width: 0px;
height: 0px;
border: 4px solid transparent;
border-top-color: red;
position: absolute;
left: calc(50% - 4px);
margin-top:4px;`
)
div.appendChild(span)
parentElement.appendChild(div)
}
}
}
/**
* 根据序列字段以及数据获取极值
* @param seriesLabelFormatter
* @param data
* @param chartId
*/
export const getExtremumValues = (seriesLabelFormatter, data, chartId) => {
const extremumValues = new Map()
seriesLabelFormatter.forEach((item: any) => {
if (!data.length || !item.showExtremum) return
const filteredData = data.filter(d => (d.category == '' ? '@' : d.category) === item.name)
const maxValue = Math.max(...filteredData.map(d => d.value))
const minValue = Math.min(...filteredData.map(d => d.value))
const maxObj = filteredData.find(d => d.value === maxValue)
const minObj = filteredData.find(d => d.value === minValue)
extremumValues.set(item.name + '@' + item.dataeaseName + '_' + chartId, {
cfg: item.formatterCfg,
value: [maxObj, minObj]
})
})
return extremumValues
}
/**
* 配置极值点dom
* @param chart
*/
export const configExtremum = (chart: Chart) => {
let customAttr: DeepPartial<ChartAttr>
// 清除图表标注
const pointElement = document.getElementById('point_' + chart.id)
if (pointElement) {
pointElement.remove()
pointElement.parentNode?.removeChild(pointElement)
}
if (chart.customAttr) {
customAttr = parseJson(chart.customAttr)
// label
if (customAttr.label?.show) {
const label = customAttr.label
let seriesLabelFormatter = []
if (chart.seriesFieldObjs && chart.seriesFieldObjs.length > 0) {
seriesLabelFormatter = chart.seriesFieldObjs
} else {
seriesLabelFormatter = deepCopy(label.seriesLabelFormatter)
}
if (seriesLabelFormatter.length > 0) {
chart.extremumValues = getExtremumValues(
seriesLabelFormatter,
chart.filteredData && chart.filteredData.length > 0
? chart.filteredData
: chart.data.data,
chart.id
)
// 创建标注父元素
const divParent = document.createElement('div')
divParent.id = 'point_' + chart.id
divParent.style.position = 'fixed'
divParent.style.zIndex = '1'
const containerElement = document.getElementById(chart.container)
containerElement.insertBefore(divParent, containerElement.firstChild)
chart.extremumValues?.forEach((value, key) => {
value.value?.forEach(extremumValue => {
if (extremumValue) {
createExtremumPointDiv(key, extremumValue.value, value.cfg, 'point_' + chart.id)
}
})
})
}
}
}
}
/**
* 设置极值位置,并返回是否显示原始标签值
* @param data 数据点数据
* @param point 数据点信息
* @param chart
* @param labelCfg 标签样式
* @param pointSize 数据点大小
*/
export const setExtremumPosition = (data, point, chart, labelCfg, pointSize) => {
if (chart.extremumValues) {
v: for (const [key, value] of chart.extremumValues.entries()) {
for (let i = 0; i < value.value.length; i++) {
if (
value.value[i] &&
data.category === value.value[i].category &&
data.value === value.value[i].value
) {
const id = key.split('@')[1] + '_' + data.value
const element = document.getElementById(id)
if (element) {
element.style.position = 'absolute'
element.style.top =
(point.y[1] ? point.y[1] : point.y) -
(labelCfg.fontSize + (pointSize ? pointSize : 0) + 12) +
'px'
element.style.left = point.x + 'px'
element.style.zIndex = '10'
element.style.fontSize = labelCfg.fontSize + 'px'
element.style.lineHeight = labelCfg.fontSize + 'px'
// 渐变颜色时需要获取最后一个rgba的值作为背景
const { r, b, g, a } = getRgbaColorLastRgba(point.color)
element.style.backgroundColor = 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'
element.style.color = isColorLight(point.color) ? '#000' : '#fff'
element.children[0]['style'].borderTopColor =
'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'
element.style.display = 'table'
return false
}
}
}
}
}
return true
}
/**
* 判断给定的RGBA字符串表示的颜色是亮色还是暗色
* 通过计算RGB颜色值的加权平均值灰度值判断颜色的明暗
* 如果给定的字符串不包含有效的RGBA值则原样返回该字符串
*
* @param rgbaString 一个RGBA颜色字符串例如 "rgba(255, 255, 255, 1)"
* @param greyValue 灰度值默认128
* @returns 如果计算出的灰度值大于等于128则返回true表示亮色否则返回false表示暗色
* 如果rgbaString不包含有效的RGBA值则返回原字符串
*/
const isColorLight = (rgbaString: string, greyValue = 128) => {
const lastRGBA = getRgbaColorLastRgba(rgbaString)
if (!isEmpty(lastRGBA)) {
// 计算灰度值的公式
const grayLevel = lastRGBA.r * 0.299 + lastRGBA.g * 0.587 + lastRGBA.b * 0.114
return grayLevel >= greyValue
} else {
return false
}
}
/**
* 从给定的rgba颜色字符串中提取最后一个rgba值
* @param rgbaString 包含一个或多个rgba颜色值的字符串
* @returns 返回最后一个解析出的rgba对象如果未找到rgba值则返回null
*/
const getRgbaColorLastRgba = (rgbaString: string) => {
const rgbaPattern = /rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)/g
let match: string[]
let lastRGBA = null
while ((match = rgbaPattern.exec(rgbaString)) !== null) {
const r = parseInt(match[1])
const g = parseInt(match[2])
const b = parseInt(match[3])
const a = parseFloat(match[4])
lastRGBA = { r, g, b, a }
}
return lastRGBA
}
/**
* 隐藏图表中的极端数据点
* 根据图例的选中状态动态隐藏或显示图表中对应数据点的详细信息div
* @param ev 图表的事件对象包含图例的选中状态
* @param chart 图表实例用于获取图表的配置和数据
*/
export const hideExtremumPoint = (ev, chart) => {
// 获取图例中被取消选中的项这些项对应的数据点将被隐藏
const hideLegendObj = ev.view
.getController('legend')
.components[0].component.cfg.items.filter(l => l.unchecked)
// 遍历图表数据对每个数据点进行处理
chart.data.data.forEach(item => {
// 根据图表的系列字段配置获取数据点对应的dataeaseName
let dataeaseName = ''
if (chart.seriesFieldObjs && chart.seriesFieldObjs.length > 0) {
dataeaseName = chart.seriesFieldObjs.find(
obj => obj.name === (item.category === '' ? item.field : item.category)
)?.dataeaseName
} else {
dataeaseName = chart.data.fields.find(
field => field.id === item.quotaList[0].id
)?.dataeaseName
}
// 根据数据点的信息生成唯一id用于查找对应的详细信息div
const divElementId = `${dataeaseName}_${chart.id}_${item.value}`
const divElement = document.getElementById(divElementId)
// 如果找到了对应的数据点详细信息div则根据图例的选中状态动态隐藏或显示该div
if (divElement) {
const shouldHide = hideLegendObj?.some(
hide => hide.id === (item.category === '' ? item.field : item.category)
)
divElement.style.display = shouldHide ? 'none' : 'table'
}
})
}
/**
* 根据滑动操作更新图表的显示数据
* 此函数用于处理滑动组件的操作事件根据滑动组件的位置动态更新图表显示的数据范围
* @param ev 滑动操作的事件对象包含当前滑动位置的信息
* @param chart 图表对象用于更新图表的显示数据
* @param options 滑动组件的配置选项包含滑动组件的初始数据等信息
*/
export const sliderHandleExtremumPoint = (ev, chart, options) => {
let seriesFields = []
// 如果chart中存在seriesFieldObjs且不为空则使用seriesFieldObjs中的name作为系列字段
// 否则使用yAxis中的name作为系列字段
if (chart.seriesFieldObjs && chart.seriesFieldObjs.length > 0) {
seriesFields = chart.seriesFieldObjs.map(item => item.name)
} else {
seriesFields = chart.yAxis.map(item => item.name)
}
// 筛选当前视图中已过滤的数据的类别并去重
const filteredDataSeriesFields = [
...new Set(ev.view.filteredData.map(({ category }) => category))
]
// 如果筛选后的数据类别的数量与系列字段的数量相等说明所有数据都被筛选出来了
// 此时直接使用视图中的过滤数据
if (filteredDataSeriesFields.length === seriesFields.length) {
chart.filteredData = ev.view.filteredData
} else {
// 否则找出当前筛选位置的起始和结束数据对象
// 获取筛选后的数据的起止索引
const objList = ev.view.filteredData.filter(
item => item.category === filteredDataSeriesFields[0]
)
const startObj = objList[0]
const endObj = objList[objList.length - 1]
let start = 0
let end = 0
// 遍历options中的数据找到起始和结束索引
options.data
.filter(item => filteredDataSeriesFields[0] === item.category)
.forEach((item, index) => {
if (JSON.stringify(startObj) === JSON.stringify(item)) {
start = index
}
if (JSON.stringify(endObj) === JSON.stringify(item)) {
end = index
}
})
const filteredData = []
// 重新计算被隐藏的序列字段数据
seriesFields
.filter(field => !filteredDataSeriesFields.includes(field))
?.forEach(field => {
const seriesFieldData = options.data.filter(item => item.category === field)
filteredData.push(...seriesFieldData.slice(start, end + 1))
})
// 将筛选出的数据与当前视图中的过滤数据合并更新图表的显示数据
chart.filteredData = [...filteredData, ...ev.view.filteredData]
}
configExtremum(chart)
}
export function isAlphaColor(color: string): boolean {
if (!color?.trim()) {
return false