3
0
0
私人模板
文章摘要
|
<template>
<div class="component-share">
<a-row type="flex" class="component-share-row">
<a-col :span="24" class="component-share-children" :style="sizeStyle">
<DropDownSlot :style="sizeStyle" :chartId="chartId">
<div class="chart-container-inner">
<div class="chart" ref="rMain"></div>
</div>
</DropDownSlot>
</a-col>
</a-row>
</div>
</template>
<script setup lang="ts">
import { getGridLayout, getTextWidth } from '@/utils/tools'
import { useD3Edit } from '@/views/stat/details/composables/use-edit-minxin'
import XWCharts from '@xiwicloud/xiwi-charts'
import {
computed,
nextTick,
onBeforeUnmount,
onMounted,
shallowRef,
useTemplateRef,
watch,
} from 'vue'
interface Scale {
width?: string
height?: string
}
const props = withDefaults(
defineProps<{
chartData: Record<any, any>
chartScale?: Scale
chartId: string
}>(),
{
chartScale: () => ({
width: '150px',
height: '150px',
}),
},
)
// Refs
const rMain = useTemplateRef('rMain')
const chart = shallowRef<any>(null)
// 使用 mixin 的逻辑
const { minGetInitRender, titleMxinStyle, subTitleMxinStyle, fillOptions } = useD3Edit()
const sizeStyle = computed(() => ({
width: props.chartScale.width,
height: props.chartScale.height,
}))
//设置一个变量-例如年月日这种距离底部的高度-
const axisLabelsBottomGap = 70
/**
* 获取百分比 Top 值的函数
* @param index
* @param footerArray footer的数组
* @returns '%'
*/
const getTopPercent = (index: number, footerArray: any[]): string => {
// 1. 获取容器总高度(未获取到则默认 400)
const totalHeight = chart.value?.getHeight() || 400
// 2. 计算原本的 px 偏移量
const pxValue = totalHeight - axisLabelsBottomGap + footerArray.length * 10 + 22 * index
// 3. 计算百分比(保留两位小数)
const percentage = ((pxValue / totalHeight) * 100).toFixed(2)
return `${percentage}%`
}
/**
* 计算百分比 Left 值的函数
* @param num
* @return '%'
*/
const getLeftPercent = (num: number): string => {
// 1. 获取容器总宽度(未获取到则默认 600px)
const totalWidth = chart.value?.getWidth() || 600
// 2. 将 15px 转为百分比(保留两位小数)
const percentage = ((num / totalWidth) * 100).toFixed(2)
return `${percentage}%` // 例如:宽度为 600 时返回 "2.50%"
}
/**
* 获取title
* @param chartData
*/
const getTitle = (chartData: Record<string, any>) => {
// 定义所有可能存在的标题配置项
const titleConfigs = [
{
text: chartData.labels?.title || chartData.metadata?.title,
textStyle: titleMxinStyle.value,
type: 'title',
},
{
text: chartData.labels?.secondTitle,
textStyle: subTitleMxinStyle.value,
type: 'subTtile',
},
{
text: chartData.labels?.thirdTitle,
textStyle: subTitleMxinStyle.value,
type: 'subTtile',
},
{
text: chartData.panelLabel,
textStyle: subTitleMxinStyle.value,
type: 'subTtile',
},
]
// 过滤掉 text 为空(undefined / null / 空字符串)的项,并动态计算 top
return titleConfigs
.filter((item) => Boolean(item.text))
.map((item, index) => ({
...item,
textAlign: 'center',
left: 'center',
top: `${index * 20}px`, // 动态根据索引计算高度:0, 20, 40...
}))
}
/**
* 脚注
* @param chartData
*/
const getFooter = (chartData: Record<string, any>) => {
// 定义所有可能存在的脚注配置项
const footerConfigs = [
{ text: chartData.rowFilterFootnote },
{ text: chartData.labels?.secondFootnote },
{ text: chartData.labels?.footnote },
{ text: chartData.panelLayout?.label }, // 使用可选链防止 panelLayout 为 undefined 时报错
]
// 公共的文本样式
const footnoteTextStyle = {
...subTitleMxinStyle.value,
fontStyle: 'italic',
}
// 过滤掉 text 为空的项,并统一映射属性和计算 bottom
return footerConfigs
.filter((item) => Boolean(item.text))
.map((item, index) => ({
...item,
textStyle: footnoteTextStyle,
textAlign: 'center',
left: '0',
bottom: 10 + index * 15, // 利用索引动态计算:10, 25, 40...
type: 'footnote',
}))
}
/**
*
* @param chartData
* @param param1 - { chartCount, chartColumns, chartList }
* @return 每个元素的位置信息 {left, top, width, height}
*/
const getGridConfigs = ({
chartCount,
chartColumns,
chartList,
}): { left: string; top: string; width: string; height: string }[] => {
return getGridLayout(
chartCount,
chartColumns,
[0, chartList.length > 0 ? 20 : 0],
{ top: 20, right: 20, bottom: 20, left: 20 },
{ width: chart.value?.getWidth(), height: chart.value?.getHeight() },
) as { left: string; top: string; width: string; height: string }[]
}
/**
*
* @param param0 - { gridConfig, panelItem }
* @return 返回单个grid配置
*/
const getGrid = ({ gridConfig, panelItem }) => {
// 头部的展示逻辑
const nameTopBox = (panelLabel) => {
if (panelLabel) {
return {
name: panelItem.panelLabel || '',
nameTextStyle: {
show: true,
fontSize: 11,
height: 16,
color: 'var(--colorTextSecondary, #505363)',
padding: [0, 0, 4, 0],
},
}
} else {
return {}
}
}
return {
left: gridConfig.left,
top: gridConfig.top,
width: gridConfig.width,
height: gridConfig.height,
containLabel: true,
show: true,
borderWidth: [
panelItem.xaxis?.highDisplay?.[0] ?? 1,
panelItem.yaxis?.highDisplay?.[0] ?? 1,
panelItem.xaxis?.lowDisplay?.[0] ?? 1,
panelItem.yaxis?.lowDisplay?.[0] ?? 1,
],
borderColor: 'var(--chart-axis-line, #E2E4E9)',
...nameTopBox(panelItem.panelLabel),
}
}
/**获取legend数据 注:缺少一个计算最大宽度的逻辑 */
const getLegend = (chartData) => {
let legendWidth = 0
const getlegendFormatter = () => {
const icons = []
const data = chartData.legend.items.map((it, index) => {
icons.push({
name: it.symbol,
dataIndex: index,
itemWidth: iconWH[it.symbol]?.w,
itemHeight: iconWH[it.symbol]?.h,
itemStyle: {
rotate: it.symbolRotate || 0,
color: it.symbolColor,
borderColor: it.symbolColor,
},
})
icons.push({
name: 'line',
dataIndex: index,
itemStyle: {
color: it.lineColor,
borderType: it.lineDash?.length > 0 ? it.lineDash : 'solid',
},
})
const obj = {}
it.seriesNames.forEach((seriesName, seriesNameIndex) => {
obj[`key${seriesNameIndex}`] = seriesName
})
return obj
})
const resObj: Record<string, any> = {
columns: [{ icons: [...icons], title: '' }],
data: data,
}
chartData.legend.titles.forEach((title, titleIndex) => {
resObj.columns.push({
title: title,
key: `key${titleIndex}`,
customCell: () => {
return {
style: {
'text-align': 'left',
'max-width': '102px',
},
}
},
})
})
const validRows = data
// 修改部分:遍历每列标题,同时比较表头 title 与该列所有行数据的文本宽度
const totalColumnsTextWidth = (chartData.legend.titles || []).reduce(
(sumWidth, title, titleIndex) => {
const key = `key${titleIndex}`
// 1. 计算当前列表头 title 的宽度
const titleWidth = getTextWidth(title)
// 2. 计算当前列所有数据行的最大宽度
const maxRowWidth = Math.max(0, ...validRows.map((item) => getTextWidth(item[key])))
// 3. 取表头和行数据中的较大者作为该列的宽度
const colMaxWidth = Math.max(titleWidth, maxRowWidth)
return sumWidth + colMaxWidth
},
0,
)
// 3. 动态计算总宽度:图标列(约25px) + 所有列文本最宽之和 + 单元格 Padding (列数+1)*8px
const columnCount = (chartData.legend.titles?.length || 0) + 1 // +1 是因为包含图标列
const computedWidth = Math.ceil(25 + totalColumnsTextWidth + columnCount * 8)
// 4. 返回表格结构对象,附带 computedWidth
return {
...resObj,
computedWidth: computedWidth + 10,
}
}
const iconWH = {
square: { w: 8, h: 8 },
circle: { w: 32, h: 32 },
diamond: { w: 16, h: 12 },
triangle: { w: 14, h: 14 },
}
if (chartData.legend?.items && chartData.legend.items.length) {
const legendFormatter = getlegendFormatter()
return {
list: [
{
top: 50,
right: 0,
padding: [0, 0, 0, 10],
width: legendFormatter.computedWidth,
nameTextStyle: { show: true, fontSize: 12 },
itemStyle: { borderWidth: 1, borderColor: 'var(--colorTextSecondary,#505363)' },
formatter: () => {
return legendFormatter
},
},
],
width: legendWidth,
}
}
return {
list: [],
width: legendWidth,
}
}
/**
* 获取X轴ticks
* @param customXaxis
*/
const xAxisAxisTick = (customXaxis, chartData) => {
const targetXaxis = customXaxis || chartData?.xaxis
const obj = { show: true }
if (targetXaxis?.ticks && targetXaxis.ticks.length > 0) {
obj['customValues'] = targetXaxis.ticks.map((t) => t.value)
}
return obj
}
/**
* 获取Y轴ticks
* @param customYaxis
*/
const yAxisAxisTick = (customYaxis, chartData) => {
const targetYaxis = customYaxis || chartData?.yaxis
const obj = { show: true }
if (targetYaxis?.ticks && targetYaxis.ticks.length > 0) {
obj['customValues'] = targetYaxis.ticks.map((t) => t.value)
}
return obj
}
// 抽离单组 X轴 逻辑,支持传入 gridIndex 并赋予特定的 panelXaxis 节点
const createXAxisConfig = ({ targetXaxis, gridIndex, footerArray = [], chartData }) => {
const array = []
//轴的显示与隐藏
const lowMask = targetXaxis.lowDisplay || [true, true, true, false]
const highMask = targetXaxis.highDisplay || [true, true, true, false]
const { min: xmin, max: xmax } = targetXaxis
const baseXAxisFields = {
type: 'value',
min: xmin,
max: xmax,
...(gridIndex !== undefined ? { gridIndex } : {}),
}
//X轴有多种刻度
if (targetXaxis.categoryLevelValues && targetXaxis.categoryLevelValues.length > 0) {
const labels = targetXaxis.categoryLevels
let isNameExit = false
/**
* 获取nameTextStyle
* @param index
*/
const getNameTextStyle: (index: number) => {
nameTextStyle?: Record<string, any>
nameLocation: string
} = (index) => {
if (targetXaxis.categoryLevelValues.length > 1) {
return {
nameLocation: gridIndex !== undefined ? 'center' : 'left',
nameTextStyle:
gridIndex !== undefined
? { color: '#666', fontSize: 12 }
: {
left: getLeftPercent(15),
top: getTopPercent(index, footerArray),
color: '#666',
fontSize: 12,
},
}
} else {
return {
nameLocation: 'center',
}
}
}
targetXaxis.categoryLevelValues.forEach((e, index) => {
if (lowMask.some((v) => v)) {
isNameExit = true
array.push({
...baseXAxisFields,
position: 'bottom',
axisLine: { show: !!lowMask[0] },
axisTick: lowMask[1] ? xAxisAxisTick(targetXaxis, chartData) : { show: false },
axisLabel: {
show: !!lowMask[2],
_baseLineLabels: chartData.timeLabels,
formatter: function (value) {
const res = targetXaxis?.ticks.find((e) => {
return e.value == value
})
if (res) {
return e[res.index]
}
},
margin: 20 * index,
},
name: labels[index],
splitLine: { show: targetXaxis?.showGrid },
nameGap: gridIndex !== undefined ? 25 : 0,
...getNameTextStyle(index),
})
}
if (highMask.some((v) => v)) {
array.push({
...baseXAxisFields,
position: 'top',
axisLine: { show: !!highMask[0] },
axisTick: highMask[1] ? xAxisAxisTick(targetXaxis, chartData) : { show: false },
axisLabel: {
show: !!highMask[2],
formatter: function (value) {
const res = chartData?.xaxis?.ticks.find((e) => {
return e.value == value
})
if (res) {
return e[res.index]
}
},
margin: 20 * index,
},
name: isNameExit ? '' : labels[index],
splitLine: { show: false },
nameGap: gridIndex !== undefined ? 25 : 0,
...getNameTextStyle(index),
})
}
})
} else {
if (lowMask.some((v) => v)) {
array.push({
...baseXAxisFields,
position: 'bottom',
axisLine: { show: !!lowMask[0] },
axisTick: lowMask[2] ? xAxisAxisTick(targetXaxis, chartData) : { show: false },
axisLabel: {
show: !!lowMask[1],
_baseLineLabels: chartData.timeLabels,
formatter: function (value) {
const res = chartData?.xaxis?.ticks.find((e) => {
return e.value == value
})
if (res) {
return chartData.timeLabels[res.index]
}
},
},
name: chartData.metadata?.xaxisLabel,
nameLocation: gridIndex !== undefined ? 'center' : 'center',
nameGap: gridIndex !== undefined ? 0 : 10,
splitLine: { show: targetXaxis?.showGrid },
})
}
if (highMask.some((v) => v)) {
array.push({
...baseXAxisFields,
position: 'top',
axisLine: { show: !!highMask[0] },
axisTick: highMask[2] ? xAxisAxisTick(targetXaxis, chartData) : { show: false },
axisLabel: {
show: !!highMask[1],
formatter: function (value) {
const res = chartData?.xaxis?.ticks.find((e) => {
return e.value == value
})
if (res) {
return chartData.timeLabels[res.index]
}
},
},
splitLine: { show: false },
})
}
}
if (array.length === 0) {
array.push({
type: 'value',
show: false,
name: chartData.metadata?.xaxisLabel,
...(gridIndex !== undefined ? { gridIndex } : {}),
})
}
return array
}
/**
* 获取markLine
* @param chartData
*/
const getMarkLine = (chartData: any) => {
const markLine = []
//参考线
if (chartData.referenceLines && chartData.referenceLines.length > 0) {
chartData.referenceLines.forEach((e) => {
//X轴参考线
if (e.axis === 'X') {
markLine.push({
symbol: ['none', 'none'],
silent: true,
label: [
{
show: !!e.label,
formatter: e.label,
position: 'endTop',
color: '#DF5E5E',
},
],
lineStyle: {
color: '#DF5E5E',
width: 1,
type: 'dashed',
},
data: [
{
xAxis: e.value,
yAxis: chartData.yaxisMax,
},
{
xAxis: e.value,
yAxis: chartData.yaxisMin, // 跟y轴的min一样
},
],
})
//Y轴参考线
} else if (e.axis === 'Y') {
markLine.push({
symbol: ['none', 'none'],
silent: true,
label: [
{
show: !!e.label,
formatter: e.label,
offset: [-23, 0],
position: 'start',
color: '#DF5E5E',
},
],
lineStyle: {
color: '#DF5E5E',
width: 1,
type: 'dashed',
},
data: [{ yAxis: e.value }, { xAxis: 'max' }],
})
}
})
}
return markLine
}
/**
* 根据 graphType 处理数据返回 series
* @param data dataList 结构如:[[[x1,y1], [x2,y2]], [[x1,y1], [x2,y2]]]
* @returns series 数组
*/
const transDataListToSeries = (
data: Array<[string, number][]>,
chartData: any,
gridIndex?: number,
) => {
if (!data || data.length === 0) {
return []
}
const array: any[] = []
data.forEach((e, index) => {
//如果有数据标签就是用这个
const getLabel = () => {
if (chartData.labelType === 'Y_VALUE') {
return {
show: true,
...subTitleMxinStyle.value,
formatter: function (params) {
return params[1]
},
}
} else if (chartData.labelType === 'ROW_NUMBER') {
return {
show: true,
...subTitleMxinStyle.value,
formatter: function (params) {
return params[0]
},
}
} else if (chartData.labelType === 'FROM_COLUMN') {
return {
show: true,
...subTitleMxinStyle.value,
formatter: function () {
return chartData.labelColumnValues[index]
},
}
} else {
return {}
}
}
const currSeries = chartData.series[index]
// 存储断开的null分开
let currentGroup = []
let nameExit = false //如果有null断开的,防止后续加入name
e.forEach((arr) => {
if (arr[1] !== null) {
// 如果值不为 null,就持续加到当前这组数据里
currentGroup.push(arr)
} else {
// 遇到 null 了!如果当前组里有数据,说明前面有一段连续的线则放入array
if (currentGroup.length > 0) {
pushSeriesData(currentGroup, index)
currentGroup = [] // 清空,准备接下一段线
}
}
})
if (currentGroup.length > 0) {
pushSeriesData(currentGroup, index)
}
function pushSeriesData(dataSegment, index) {
const sItem: any = {
data: dataSegment,
type: 'line',
symbol: currSeries.symbol,
symbolRotate: currSeries.symbolRotate ?? 0,
symbolSize: 6,
showAllSymbol: true,
label: getLabel(),
name: nameExit ? undefined : currSeries['seriesName'],
params: {
seriesIndex: index,
//这个参数是让这个线在图形编辑添加label
_type: 'line',
//这个参数用于图形编辑行标签
_rvalues: currSeries.rvalues,
},
lineStyle: {
color: currSeries.color,
width: 1,
type: currSeries.lineDash?.length > 0 ? currSeries.lineDash : 'solid',
},
itemStyle: {
color: currSeries.color,
borderColor: currSeries.color,
},
markLine: getMarkLine(chartData),
}
if (gridIndex !== undefined) {
sItem.xAxisIndex = gridIndex * 2
sItem.yAxisIndex = gridIndex * 2
}
array.push(sItem)
nameExit = true
}
})
return array
}
/**
*
* @param data props.chartData
* @returns option
* @description 根据传入的数据生成图表选项
*/
const getOption = (chartData: Record<any, any>): any => {
/**当前图表的布局模式 */
const layoutMode = 'SEPARATE_PANELS'
//接口的基础数据
const chartList = [
{
columns: 2,
rows: 3,
count: 5,
plane: [
{
series: [
{
type: 'line',
data: [
[1, 1],
[2, 1],
],
symbol: 'none',
},
],
},
{
series: [
{
type: 'line',
data: [
[1, 1],
[2, 1],
],
symbol: 'none',
},
],
},
{
series: [
{
type: 'line',
data: [
[1, 1],
[2, 1],
],
symbol: 'none',
},
],
},
{
series: [
{
type: 'line',
data: [
[1, 1],
[2, 1],
],
symbol: 'none',
},
],
},
{
series: [
{
type: 'line',
data: [
[1, 1],
[2, 1],
],
symbol: 'none',
},
],
},
],
},
// {
// columns: 5,
// rows: 1,
// count: 5,
// plane: [
// {
// series: [
// {
// type: 'line',
// data: [
// [1, 1],
// [2, 1],
// ],
// symbol: 'none',
// },
// ],
// },
// {
// series: [
// {
// type: 'line',
// data: [
// [1, 1],
// [2, 1],
// ],
// symbol: 'none',
// },
// ],
// },
// {
// series: [
// {
// type: 'line',
// data: [
// [1, 1],
// [2, 1],
// ],
// symbol: 'none',
// },
// ],
// },
// {
// series: [
// {
// type: 'line',
// data: [
// [1, 1],
// [2, 1],
// ],
// symbol: 'none',
// },
// ],
// },
// {
// series: [
// {
// type: 'line',
// data: [
// [1, 1],
// [2, 1],
// ],
// symbol: 'none',
// },
// ],
// },
// ],
// },
]
/**图形总数 */
const chartCount = chartList.reduce((pre, cur) => cur.count + pre, 0)
/**列数取第一个就行 */
const chartColumns = chartList[0].columns
/**行数 */
const cahrtRows = chartList.reduce((pre, cur) => cur.rows + pre, 0)
const gridConfigs = getGridConfigs({
chartCount,
chartColumns,
chartList,
})
console.log('gridConfigs', gridConfigs)
const finalGrids = []
const finalXAxes = []
const finalYAxes = []
const finalSeries = []
let globalIndex = 0
chartList.forEach((chartItem) => {
chartItem.plane.forEach((panelItem) => {
finalGrids.push(getGrid({ gridConfig: gridConfigs[globalIndex], panelItem }))
globalIndex++ // 每个panelItem对应一个gridConfig
})
})
console.log('finalGrids', finalGrids)
}
// 初始化图表
const init = async () => {
if (!rMain.value) return
if (chart.value) {
chart.value.dispose()
}
chart.value = new XWCharts(rMain.value)
const option = getOption(props.chartData) // 假设你有一个函数来生成图表选项
console.log('option', option)
minGetInitRender({
graph: chart.value,
options: option,
chartId: props.chartId,
})
chart.value?.setOption(fillOptions(option))
}
// 监听数据变化
watch(
() => props.chartData,
() => {
init()
},
)
onMounted(() => {
nextTick(async () => {
await init()
})
})
const disposeChart = () => {
if (chart.value) {
chart.value.dispose()
chart.value = null
}
}
onBeforeUnmount(() => {
disposeChart()
})
// 暴露常用方法给外部
defineExpose({
init,
disposeChart,
chart,
})
</script>
<style scoped lang="scss">
.component-share {
.component-share-row {
.component-share-children {
margin-top: 0px !important;
}
}
}
.chart-container-inner {
height: 100%;
width: 100%;
}
.chart {
width: 100%;
height: 100%;
}
</style>
