forked from github/dataease
Merge pull request #11225 from dataease/pr@dev-v2@chart-extremum-slider-fix
fix(图表): 处理快速滑动缩略轴导致最值显示不正确的问题,以及简化显示最值的代码逻辑
This commit is contained in:
commit
c9dc374560
224
core/core-frontend/src/views/chart/components/js/extremumUitl.ts
Normal file
224
core/core-frontend/src/views/chart/components/js/extremumUitl.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user