feat(视图): 支持K线图
This commit is contained in:
parent
71d616dd81
commit
c8488c335b
11
core/frontend/src/icons/svg/stock-line.svg
Normal file
11
core/frontend/src/icons/svg/stock-line.svg
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<svg width="80" height="56" viewBox="0 0 80 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="15" y="27" width="2" height="22" fill="#BBBFC4"/>
|
||||||
|
<rect x="31" y="17" width="2" height="28" fill="#646A73"/>
|
||||||
|
<rect x="47" y="4" width="2" height="41" fill="#BBBFC4"/>
|
||||||
|
<rect x="63" y="4" width="2" height="45" fill="#646A73"/>
|
||||||
|
<path d="M11 37.5C11 37.2239 11.2239 37 11.5 37H20.5C20.7761 37 21 37.2239 21 37.5V46.5C21 46.7761 20.7761 47 20.5 47H11.5C11.2239 47 11 46.7761 11 46.5V37.5Z" fill="#BBBFC4"/>
|
||||||
|
<path d="M27 27.5C27 27.2239 27.2239 27 27.5 27H36.5C36.7761 27 37 27.2239 37 27.5V38.5C37 38.7761 36.7761 39 36.5 39H27.5C27.2239 39 27 38.7761 27 38.5V27.5Z" fill="#646A73"/>
|
||||||
|
<path d="M43 8.5C43 8.22386 43.2239 8 43.5 8H52.5C52.7761 8 53 8.22386 53 8.5V31.5C53 31.7761 52.7761 32 52.5 32H43.5C43.2239 32 43 31.7761 43 31.5V8.5Z" fill="#BBBFC4"/>
|
||||||
|
<path d="M59 8.5C59 8.22386 59.2239 8 59.5 8H68.5C68.7761 8 69 8.22386 69 8.5V45.5C69 45.7761 68.7761 46 68.5 46H59.5C59.2239 46 59 45.7761 59 45.5V8.5Z" fill="#646A73"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 51H76L76 54H4V51H5Z" fill="#646A73"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@ -1412,6 +1412,7 @@ export default {
|
|||||||
chart_bar_stack_horizontal: 'Stack Horizontal Bar',
|
chart_bar_stack_horizontal: 'Stack Horizontal Bar',
|
||||||
chart_percentage_bar_stack_horizontal: 'Horizontal Percentage Stack Bar',
|
chart_percentage_bar_stack_horizontal: 'Horizontal Percentage Stack Bar',
|
||||||
chart_bidirectional_bar: 'Bidirectional Bar',
|
chart_bidirectional_bar: 'Bidirectional Bar',
|
||||||
|
chart_stock_line: 'Stock Line',
|
||||||
chart_line: 'Base Line',
|
chart_line: 'Base Line',
|
||||||
chart_line_stack: 'Stack Line',
|
chart_line_stack: 'Stack Line',
|
||||||
chart_pie: 'Pie',
|
chart_pie: 'Pie',
|
||||||
|
|||||||
@ -1411,6 +1411,7 @@ export default {
|
|||||||
chart_bar_stack_horizontal: '橫嚮堆疊柱狀圖',
|
chart_bar_stack_horizontal: '橫嚮堆疊柱狀圖',
|
||||||
chart_percentage_bar_stack_horizontal: '橫嚮百分比柱狀圖',
|
chart_percentage_bar_stack_horizontal: '橫嚮百分比柱狀圖',
|
||||||
chart_bidirectional_bar: '對稱柱狀圖',
|
chart_bidirectional_bar: '對稱柱狀圖',
|
||||||
|
chart_stock_line: 'K 線圖',
|
||||||
chart_line: '基礎摺線圖',
|
chart_line: '基礎摺線圖',
|
||||||
chart_line_stack: '堆疊摺線圖',
|
chart_line_stack: '堆疊摺線圖',
|
||||||
chart_pie: '餅圖',
|
chart_pie: '餅圖',
|
||||||
|
|||||||
@ -1408,6 +1408,7 @@ export default {
|
|||||||
chart_bar_stack_horizontal: '横向堆叠柱状图',
|
chart_bar_stack_horizontal: '横向堆叠柱状图',
|
||||||
chart_percentage_bar_stack_horizontal: '横向百分比柱状图',
|
chart_percentage_bar_stack_horizontal: '横向百分比柱状图',
|
||||||
chart_bidirectional_bar: '对称柱状图',
|
chart_bidirectional_bar: '对称柱状图',
|
||||||
|
chart_stock_line: 'K 线图',
|
||||||
chart_line: '基础折线图',
|
chart_line: '基础折线图',
|
||||||
chart_line_stack: '堆叠折线图',
|
chart_line_stack: '堆叠折线图',
|
||||||
chart_pie: '饼图',
|
chart_pie: '饼图',
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Column, Bar, BidirectionalBar } from '@antv/g2plot'
|
import {Column, Bar, BidirectionalBar, Mix} from '@antv/g2plot'
|
||||||
import {
|
import {
|
||||||
getTheme,
|
getTheme,
|
||||||
getLabel,
|
getLabel,
|
||||||
@ -17,6 +17,14 @@ import {
|
|||||||
import { antVCustomColor, getColors, handleEmptyDataStrategy, hexColorToRGBA, handleStackSort } from '@/views/chart/chart/util'
|
import { antVCustomColor, getColors, handleEmptyDataStrategy, hexColorToRGBA, handleStackSort } from '@/views/chart/chart/util'
|
||||||
import { cloneDeep, find, groupBy, each } from 'lodash-es'
|
import { cloneDeep, find, groupBy, each } from 'lodash-es'
|
||||||
import { formatterItem, valueFormatter } from '@/views/chart/chart/formatter'
|
import { formatterItem, valueFormatter } from '@/views/chart/chart/formatter'
|
||||||
|
import {
|
||||||
|
calculateMinMax,
|
||||||
|
calculateMovingAverage,
|
||||||
|
configXAxis, configYAxis,
|
||||||
|
configBasicStyle,
|
||||||
|
configTooltip,
|
||||||
|
registerEvent, customConfigEmptyDataStrategy
|
||||||
|
} from "@/views/chart/chart/bar/stock_line_util";
|
||||||
|
|
||||||
export function baseBarOptionAntV(container, chart, action, isGroup, isStack) {
|
export function baseBarOptionAntV(container, chart, action, isGroup, isStack) {
|
||||||
// theme
|
// theme
|
||||||
@ -587,3 +595,201 @@ export function baseBidirectionalBarOptionAntV(container, chart, action, isGroup
|
|||||||
configPlotTooltipEvent(chart, plot)
|
configPlotTooltipEvent(chart, plot)
|
||||||
return plot
|
return plot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function stockLineOptionAntV(container, chart, action) {
|
||||||
|
if (!chart.data?.data?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const xAxis = JSON.parse(chart.xaxis)
|
||||||
|
const yAxis = JSON.parse(chart.yaxis)
|
||||||
|
if (yAxis.length !== 4) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const theme = getTheme(chart)
|
||||||
|
const legend = getLegend(chart)
|
||||||
|
const basicStyle = JSON.parse(chart.customAttr).color
|
||||||
|
const colors = []
|
||||||
|
const alpha = basicStyle.alpha
|
||||||
|
basicStyle.colors.forEach(ele => {
|
||||||
|
colors.push(hexColorToRGBA(ele, alpha))
|
||||||
|
})
|
||||||
|
const data = cloneDeep(chart.data?.tableRow ?? [])
|
||||||
|
|
||||||
|
// 时间字段
|
||||||
|
const xAxisDataeaseName = xAxis[0].dataeaseName
|
||||||
|
const averages = [5, 10, 20, 60, 120, 180]
|
||||||
|
const legendItems = [
|
||||||
|
{
|
||||||
|
name: '日K',
|
||||||
|
value: 'k',
|
||||||
|
marker: {
|
||||||
|
symbol: (x, y, r) => {
|
||||||
|
const width = r * 1
|
||||||
|
const height = r
|
||||||
|
return [
|
||||||
|
// 矩形框
|
||||||
|
['M', x - width - 1 / 2, y - height / 2],
|
||||||
|
['L', x + width + 1 / 2, y - height / 2],
|
||||||
|
['L', x + width + 1 / 2, y + height / 2],
|
||||||
|
['L', x - width - 1 / 2, y + height / 2],
|
||||||
|
['Z'],
|
||||||
|
// 中线
|
||||||
|
['M', x, y + 10 / 2],
|
||||||
|
['L', x, y - 10 / 2]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
style: { fill: 'red', stroke: 'red', lineWidth: 2 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
// 计算均线数据
|
||||||
|
const averagesLineData = new Map()
|
||||||
|
averages.forEach(item => {
|
||||||
|
averagesLineData.set('ma' + item, calculateMovingAverage(data, item, chart))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 将均线数据设置到主数据中
|
||||||
|
data.forEach((item) => {
|
||||||
|
const date = item[xAxisDataeaseName]
|
||||||
|
for (const [key, value] of averagesLineData) {
|
||||||
|
item[key] = value.find(m => m[xAxisDataeaseName] === date)?.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const averageLines = []
|
||||||
|
let index = 0
|
||||||
|
const start = 0.5
|
||||||
|
const end = 1
|
||||||
|
const startIndex = Math.floor(start * data.length)
|
||||||
|
const endIndex = Math.ceil(end * data.length)
|
||||||
|
const filteredData = data.slice(startIndex, endIndex)
|
||||||
|
const { maxValue, minValue } = calculateMinMax(filteredData)
|
||||||
|
for (const key of averagesLineData.keys()) {
|
||||||
|
index++
|
||||||
|
averageLines.push({
|
||||||
|
type: 'line',
|
||||||
|
top: true,
|
||||||
|
options: {
|
||||||
|
smooth: false,
|
||||||
|
xField: xAxisDataeaseName,
|
||||||
|
yField: key,
|
||||||
|
color: colors[index - 1],
|
||||||
|
xAxis: null,
|
||||||
|
yAxis: {
|
||||||
|
label: false,
|
||||||
|
min: minValue,
|
||||||
|
max: maxValue,
|
||||||
|
grid: null,
|
||||||
|
line: null
|
||||||
|
},
|
||||||
|
lineStyle: {
|
||||||
|
lineWidth: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
legendItems.push({
|
||||||
|
name: key.toUpperCase(),
|
||||||
|
value: key,
|
||||||
|
marker: { symbol: 'hyphen', style: { stroke: colors[index - 1], lineWidth: 2 } }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const axis =JSON.parse(chart.xaxis) ?? []
|
||||||
|
let dateFormat
|
||||||
|
const dateSplit = axis[0]?.datePattern === 'date_split' ? '/' : '-'
|
||||||
|
switch (axis[0]?.dateStyle) {
|
||||||
|
case 'y':
|
||||||
|
dateFormat = 'YYYY'
|
||||||
|
break
|
||||||
|
case 'y_M':
|
||||||
|
dateFormat = 'YYYY' + dateSplit + 'MM'
|
||||||
|
break
|
||||||
|
case 'y_M_d':
|
||||||
|
dateFormat = 'YYYY' + dateSplit + 'MM' + dateSplit + 'DD'
|
||||||
|
break
|
||||||
|
case 'y_M_d_H':
|
||||||
|
dateFormat = 'YYYY' + dateSplit + 'MM' + dateSplit + 'DD' + ' HH'
|
||||||
|
break
|
||||||
|
case 'y_M_d_H_m':
|
||||||
|
dateFormat = 'YYYY' + dateSplit + 'MM' + dateSplit + 'DD' + ' HH:mm'
|
||||||
|
break
|
||||||
|
case 'y_M_d_H_m_s':
|
||||||
|
dateFormat = 'YYYY' + dateSplit + 'MM' + dateSplit + 'DD' + ' HH:mm:ss'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
dateFormat = 'YYYY-MM-dd HH:mm:ss'
|
||||||
|
}
|
||||||
|
let option = {
|
||||||
|
data,
|
||||||
|
theme,
|
||||||
|
appendPadding: getPadding(chart),
|
||||||
|
slider: {
|
||||||
|
start: 0.5,
|
||||||
|
end: 1
|
||||||
|
},
|
||||||
|
plots: [
|
||||||
|
...averageLines,
|
||||||
|
{
|
||||||
|
type: 'stock',
|
||||||
|
top: true,
|
||||||
|
options: {
|
||||||
|
meta: {
|
||||||
|
[xAxisDataeaseName]: {
|
||||||
|
mask: dateFormat
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stockStyle: {
|
||||||
|
stroke: 'black',
|
||||||
|
lineWidth: 0.5
|
||||||
|
},
|
||||||
|
xField: xAxisDataeaseName,
|
||||||
|
yField: [
|
||||||
|
yAxis[0].dataeaseName,
|
||||||
|
yAxis[1].dataeaseName,
|
||||||
|
yAxis[2].dataeaseName,
|
||||||
|
yAxis[3].dataeaseName
|
||||||
|
],
|
||||||
|
legend: !legend?false:{
|
||||||
|
position: 'top',
|
||||||
|
custom: true,
|
||||||
|
items: legendItems
|
||||||
|
},
|
||||||
|
fallingFill: hexColorToRGBA('#ef5350', alpha),
|
||||||
|
risingFill: hexColorToRGBA('#26a69a', alpha),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
option = configBasicStyle(chart, option)
|
||||||
|
option = configXAxis(chart, option)
|
||||||
|
option = configYAxis(chart, option)
|
||||||
|
option = configTooltip(chart, option)
|
||||||
|
option = customConfigEmptyDataStrategy(chart,option)
|
||||||
|
const plot = new Mix(container, option)
|
||||||
|
registerEvent(data, plot, averagesLineData)
|
||||||
|
plot.on('schema:click', evt => {
|
||||||
|
const selectSchema = evt.data.data[xAxisDataeaseName]
|
||||||
|
const paramData = cloneDeep(chart.data?.data ?? [])
|
||||||
|
const selectData = paramData.filter(item => item.field === selectSchema)
|
||||||
|
const quotaList = []
|
||||||
|
selectData.forEach(item => {
|
||||||
|
quotaList.push({ ...item.quotaList[0], value: item.value })
|
||||||
|
})
|
||||||
|
if (selectData.length) {
|
||||||
|
const param = {
|
||||||
|
x: evt.x,
|
||||||
|
y: evt.y,
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
...evt.data.data,
|
||||||
|
value: quotaList[0].value,
|
||||||
|
name: selectSchema,
|
||||||
|
dimensionList: selectData[0].dimensionList,
|
||||||
|
quotaList: quotaList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
action(param)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return plot
|
||||||
|
}
|
||||||
|
|||||||
423
core/frontend/src/views/chart/chart/bar/stock_line_util.js
Normal file
423
core/frontend/src/views/chart/chart/bar/stock_line_util.js
Normal file
@ -0,0 +1,423 @@
|
|||||||
|
import {getXAxis, getYAxis} from '@/views/chart/chart/common/common_antv'
|
||||||
|
import { valueFormatter } from '@/views/chart/chart/formatter'
|
||||||
|
import {cloneDeep} from "lodash";
|
||||||
|
import {handleEmptyDataStrategy} from "@/views/chart/chart/util";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算收盘价平均值
|
||||||
|
* @param data
|
||||||
|
* @param dayCount
|
||||||
|
* @param chart
|
||||||
|
*/
|
||||||
|
export const calculateMovingAverage = (data, dayCount, chart) => {
|
||||||
|
const xAxis = JSON.parse(chart.xaxis)
|
||||||
|
const yAxis = JSON.parse(chart.yaxis)
|
||||||
|
// 时间字段
|
||||||
|
const xAxisDataeaseName = xAxis[0].dataeaseName
|
||||||
|
// 收盘价字段
|
||||||
|
const yAxisDataeaseName = yAxis[1].dataeaseName
|
||||||
|
const result = []
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
if (i < dayCount) {
|
||||||
|
result.push({
|
||||||
|
[xAxisDataeaseName]: data[i][xAxisDataeaseName],
|
||||||
|
value: null
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const sum = data
|
||||||
|
.slice(i - dayCount + 1, i + 1)
|
||||||
|
.reduce((sum, item) => sum + item[yAxisDataeaseName], 0)
|
||||||
|
result.push({
|
||||||
|
[xAxisDataeaseName]: data[i][xAxisDataeaseName],
|
||||||
|
value: parseFloat((sum / dayCount).toFixed(3))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据集合中对象属性值的最大最小值
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export const calculateMinMax = data => {
|
||||||
|
return data.reduce(
|
||||||
|
(acc, current) => {
|
||||||
|
// 获取 current 对象的所有属性值
|
||||||
|
const values = Object.values(current)
|
||||||
|
// 过滤出数字值
|
||||||
|
const numericValues = values.filter(value => typeof value === 'number') ?? []
|
||||||
|
// 找到 current 对象的数字属性值中的最大值和最小值
|
||||||
|
// 如果存在数字值,则计算当前对象的最大值和最小值
|
||||||
|
if (numericValues.length > 0) {
|
||||||
|
const currentMax = Math.max(...numericValues)
|
||||||
|
const currentMin = Math.min(...numericValues)
|
||||||
|
// 更新全局最大值和最小值
|
||||||
|
acc.maxValue = Math.max(acc.maxValue, currentMax)
|
||||||
|
acc.minValue = Math.min(acc.minValue, currentMin)
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{ maxValue: Number.NEGATIVE_INFINITY, minValue: Number.POSITIVE_INFINITY }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册图表事件
|
||||||
|
* @param data
|
||||||
|
* @param plot
|
||||||
|
* @param averagesLineData
|
||||||
|
*/
|
||||||
|
export const registerEvent = (data, plot, averagesLineData) => {
|
||||||
|
// 监听图例点击事件,显示隐藏
|
||||||
|
let risingVisible = true
|
||||||
|
plot.on('legend-item:click', evt => {
|
||||||
|
const { value } = evt.target.get('delegateObject').item
|
||||||
|
if (value === 'k') {
|
||||||
|
risingVisible = !risingVisible
|
||||||
|
plot.chart.geometries.forEach(geom => {
|
||||||
|
if (geom.type === 'schema') {
|
||||||
|
geom.changeVisible(risingVisible)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const lines = plot.chart.geometries.filter(item => item.type === 'line')
|
||||||
|
const points = plot.chart.geometries.filter(item => item.type === 'point')
|
||||||
|
let lineIndex = 0
|
||||||
|
for (const key of averagesLineData.keys()) {
|
||||||
|
lineIndex++
|
||||||
|
if (key === value) {
|
||||||
|
lines[lineIndex - 1].changeVisible(!lines[lineIndex - 1].visible)
|
||||||
|
points[lineIndex - 1].changeVisible(!points[lineIndex - 1].visible)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// 监听图表渲染事件
|
||||||
|
plot.on('afterrender', e => {
|
||||||
|
let first = false
|
||||||
|
if (plot.chart.options.slider.start === 0.5 && plot.chart.options.slider.end === 1) {
|
||||||
|
first = true
|
||||||
|
}
|
||||||
|
if (e.view?.options?.scales) {
|
||||||
|
const startIndex = Math.floor(0.5 * data.length)
|
||||||
|
const endIndex = Math.ceil(1 * data.length)
|
||||||
|
const filteredData = data.slice(startIndex, endIndex)
|
||||||
|
const { maxValue, minValue } = calculateMinMax(
|
||||||
|
first ? filteredData : e.view.filteredData
|
||||||
|
)
|
||||||
|
const a = e.view.options.scales
|
||||||
|
Object.keys(a).forEach(item => {
|
||||||
|
if (a[item].max) {
|
||||||
|
a[item].max = maxValue
|
||||||
|
}
|
||||||
|
if (a[item].min) {
|
||||||
|
a[item].min = minValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// 监听图例组点击事件,设置缩放
|
||||||
|
plot.on('legend-item-group:click', e => {
|
||||||
|
if (e.view?.options?.scales) {
|
||||||
|
const { maxValue, minValue } = calculateMinMax(e.view.filteredData)
|
||||||
|
const a = e.view.options.scales
|
||||||
|
Object.keys(a).forEach(item => {
|
||||||
|
if (a[item].max) {
|
||||||
|
a[item].max = maxValue
|
||||||
|
}
|
||||||
|
if (a[item].min) {
|
||||||
|
a[item].min = minValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// 监听滑块事件,设置缩放
|
||||||
|
plot.on('slider:valuechanged', e => {
|
||||||
|
const start = e.gEvent.currentTarget.cfg.component.cfg.start
|
||||||
|
const end = e.gEvent.currentTarget.cfg.component.cfg.end
|
||||||
|
plot.chart.options.slider.start = start
|
||||||
|
plot.chart.options.slider.end = end
|
||||||
|
const startIndex = Math.floor(start * data.length)
|
||||||
|
const endIndex = Math.ceil(end * data.length)
|
||||||
|
const filteredData = data.slice(startIndex, endIndex)
|
||||||
|
const { maxValue, minValue } = calculateMinMax(filteredData)
|
||||||
|
const a = e.view.options.scales
|
||||||
|
Object.keys(a).forEach(item => {
|
||||||
|
if (a[item].max) {
|
||||||
|
a[item].max = maxValue
|
||||||
|
}
|
||||||
|
if (a[item].min) {
|
||||||
|
a[item].min = minValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const configBasicStyle = (chart, options) => {
|
||||||
|
// size
|
||||||
|
const customAttr = JSON.parse(chart.customAttr)
|
||||||
|
const s = JSON.parse(JSON.stringify(customAttr.size))
|
||||||
|
const smooth = s.lineSmooth
|
||||||
|
const point = {
|
||||||
|
size: s.lineSymbolSize,
|
||||||
|
shape: s.lineSymbol
|
||||||
|
}
|
||||||
|
const lineStyle = {
|
||||||
|
lineWidth: s.lineWidth
|
||||||
|
}
|
||||||
|
const plots = []
|
||||||
|
options.plots.forEach(item => {
|
||||||
|
if (item.type === 'line') {
|
||||||
|
plots.push({ ...item, options: { ...item.options, smooth, point, lineStyle } })
|
||||||
|
}
|
||||||
|
if (item.type === 'stock') {
|
||||||
|
plots.push({ ...item })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
plots
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const configTooltip = (chart, options)=> {
|
||||||
|
const tooltipAttr = JSON.parse(chart.customAttr).tooltip
|
||||||
|
const newPlots = []
|
||||||
|
const linePlotList = options.plots.filter(item => item.type === 'line')
|
||||||
|
linePlotList.forEach(item => {
|
||||||
|
newPlots.push(item)
|
||||||
|
})
|
||||||
|
const stockPlot = options.plots.filter(item => item.type === 'stock')[0]
|
||||||
|
if (!tooltipAttr.show) {
|
||||||
|
const stockOption = {
|
||||||
|
...stockPlot.options,
|
||||||
|
tooltip: {
|
||||||
|
showContent: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newPlots.push({ ...stockPlot, options: stockOption })
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
plots: newPlots
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const showFiled = chart.data.fields
|
||||||
|
const yAxis = cloneDeep(JSON.parse(chart.yaxis))
|
||||||
|
const customTooltipItems = originalItems => {
|
||||||
|
const formattedItems = originalItems.map(item => {
|
||||||
|
const fieldObj = showFiled.find(q => q.dataeaseName === item.name)
|
||||||
|
const displayName = fieldObj?.chartShowName || fieldObj?.name || item.name
|
||||||
|
const formattedName = displayName.startsWith('ma') ? displayName.toUpperCase() : displayName
|
||||||
|
if(!yAxis[0].formatterCfg){
|
||||||
|
yAxis[0].formatterCfg = {
|
||||||
|
type: 'value', // auto,value,percent
|
||||||
|
unit: 1, // 换算单位
|
||||||
|
suffix: '', // 单位后缀
|
||||||
|
decimalCount: 3, // 小数位数
|
||||||
|
thousandSeparator: true// 千分符
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(yAxis[0].formatterCfg.type === 'auto'){
|
||||||
|
yAxis[0].formatterCfg.type = 'value'
|
||||||
|
yAxis[0].formatterCfg.decimalCount = 3
|
||||||
|
}
|
||||||
|
const formattedValue = valueFormatter(item.value, yAxis[0].formatterCfg)
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
name: formattedName,
|
||||||
|
value: formattedValue,
|
||||||
|
color: item.color
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasKLine = formattedItems.some(item => !item.name.startsWith('MA'))
|
||||||
|
const kLines = formattedItems.filter(item => !item.name.startsWith('MA'))
|
||||||
|
return hasKLine
|
||||||
|
? [
|
||||||
|
{ name: '日K', value: '', marker: true, color: kLines[0]?.color },
|
||||||
|
...kLines,
|
||||||
|
...formattedItems.filter(item => item.name.startsWith('MA'))
|
||||||
|
]
|
||||||
|
: formattedItems
|
||||||
|
}
|
||||||
|
const formatTooltipItem = (item) => {
|
||||||
|
const size = item.name.startsWith('MA') || !item.value ? 10 : 5
|
||||||
|
const markerMarginRight = item.name.startsWith('MA') || !item.value ? 5 : 9
|
||||||
|
const markerMarginLeft = item.name.startsWith('MA') || !item.value ? 0 : 2
|
||||||
|
return `
|
||||||
|
<li style="display: flex; align-items: center; margin-bottom: 10px;">
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
style="
|
||||||
|
background-color: ${item.color};
|
||||||
|
width: ${size}px;
|
||||||
|
height: ${size}px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: ${markerMarginRight}px;
|
||||||
|
margin-left: ${markerMarginLeft}px;
|
||||||
|
"></span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: space-between; width: 100%;">
|
||||||
|
<span style="margin-right: 15px;">${item.name}</span>
|
||||||
|
<span>${item.name.startsWith('MA') && item.value === '0' ? '-' : item.value}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
const generateCustomTooltipContent = (title, items) => {
|
||||||
|
return `
|
||||||
|
<div style="padding: 10px 0;">
|
||||||
|
<div style="margin-bottom: 10px;">${title}</div>
|
||||||
|
<ul style="list-style: none; padding: 0;">
|
||||||
|
${items.map(formatTooltipItem).join('')}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
const stockOption = {
|
||||||
|
...stockPlot.options,
|
||||||
|
tooltip: {
|
||||||
|
showMarkers: true,
|
||||||
|
showCrosshairs: true,
|
||||||
|
showNil: true,
|
||||||
|
crosshairs: {
|
||||||
|
follow: true,
|
||||||
|
text: (axisType, value, data) => {
|
||||||
|
if (axisType === 'y') {
|
||||||
|
return { content: value ? value.toFixed(0) : value }
|
||||||
|
}
|
||||||
|
return { content: data[0].title, position: 'end' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showContent: true,
|
||||||
|
customItems: customTooltipItems,
|
||||||
|
customContent: generateCustomTooltipContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newPlots.push({ ...stockPlot, options: stockOption })
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
plots: newPlots
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const configXAxis = (chart, options) => {
|
||||||
|
const xAxisOptions = getXAxis(chart)
|
||||||
|
if (!xAxisOptions) {
|
||||||
|
options.plots.forEach(item => {
|
||||||
|
if(item.type === 'stock'){
|
||||||
|
item.options.xAxis = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
const newPlots = []
|
||||||
|
const linePlotList = options.plots.filter(item => item.type === 'line')
|
||||||
|
|
||||||
|
const stockPlot = options.plots.filter(item => item.type === 'stock')[0]
|
||||||
|
const newStockPlot = {
|
||||||
|
...stockPlot,
|
||||||
|
options: {
|
||||||
|
...stockPlot.options,
|
||||||
|
xAxis: xAxisOptions
|
||||||
|
? {
|
||||||
|
...stockPlot.options['xAxis'],
|
||||||
|
...xAxisOptions
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
label: false,
|
||||||
|
line: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newPlots.push(newStockPlot)
|
||||||
|
linePlotList.forEach(item => {
|
||||||
|
newPlots.push(item)
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
plots: newPlots
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const configYAxis = (chart, options) => {
|
||||||
|
const yAxisOptions = getYAxis(chart)
|
||||||
|
if (!yAxisOptions) {
|
||||||
|
options.plots.forEach(item => {
|
||||||
|
if(item.type === 'stock'){
|
||||||
|
item.options.yAxis = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
const yAxis = JSON.parse(chart.yaxis)
|
||||||
|
const newPlots = []
|
||||||
|
const linePlotList = options.plots.filter(item => item.type === 'line')
|
||||||
|
|
||||||
|
const stockPlot = options.plots.filter(item => item.type === 'stock')[0]
|
||||||
|
let label = false
|
||||||
|
if (yAxisOptions.label) {
|
||||||
|
label = {
|
||||||
|
...yAxisOptions.label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const newStockPlot = {
|
||||||
|
...stockPlot,
|
||||||
|
options: {
|
||||||
|
...stockPlot.options,
|
||||||
|
yAxis: label
|
||||||
|
? {
|
||||||
|
...stockPlot.options['yAxis'],
|
||||||
|
...yAxisOptions,
|
||||||
|
label
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
label,
|
||||||
|
grid: null,
|
||||||
|
line: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newPlots.push(newStockPlot)
|
||||||
|
linePlotList.forEach(item => {
|
||||||
|
newPlots.push(item)
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
plots: newPlots
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const customConfigEmptyDataStrategy = (chart, options) => {
|
||||||
|
const { data } = options
|
||||||
|
if (!data?.length) {
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
const strategy = JSON.parse(chart.senior)?.functionCfg?.emptyDataStrategy
|
||||||
|
if (strategy === 'ignoreData') {
|
||||||
|
for (let i = data.length - 1; i >= 0; i--) {
|
||||||
|
const item = data[i]
|
||||||
|
Object.keys(item).forEach(key => {
|
||||||
|
if (key.startsWith('C_') && item[key] === null) {
|
||||||
|
data.splice(i, 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const updateValues = (strategy, data) => {
|
||||||
|
data.forEach(obj => {
|
||||||
|
Object.keys(obj).forEach(key => {
|
||||||
|
if (key.startsWith('C_') && obj[key] === null) {
|
||||||
|
obj[key] = strategy === 'breakLine' ? null : 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (strategy === 'breakLine' || strategy === 'setZero') {
|
||||||
|
updateValues(strategy, data)
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
@ -1738,7 +1738,74 @@ export const TYPE_CONFIGS = [
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
render: 'antv',
|
||||||
|
category: 'chart.chart_type_compare',
|
||||||
|
value: 'stock-line',
|
||||||
|
title: 'chart.chart_stock_line',
|
||||||
|
icon: 'stock-line',
|
||||||
|
properties: [
|
||||||
|
'color-selector',
|
||||||
|
'size-selector',
|
||||||
|
'tooltip-selector-ant-v',
|
||||||
|
'x-axis-selector-ant-v',
|
||||||
|
'y-axis-selector-ant-v',
|
||||||
|
'title-selector-ant-v',
|
||||||
|
'legend-selector-ant-v'
|
||||||
|
],
|
||||||
|
propertyInner: {
|
||||||
|
'color-selector': [
|
||||||
|
'value',
|
||||||
|
'colorPanel',
|
||||||
|
'customColor',
|
||||||
|
'alpha'
|
||||||
|
],
|
||||||
|
'size-selector': [
|
||||||
|
'lineWidth',
|
||||||
|
'lineSymbol',
|
||||||
|
'lineSymbolSize',
|
||||||
|
'lineSmooth'
|
||||||
|
],
|
||||||
|
'tooltip-selector-ant-v': [
|
||||||
|
'show',
|
||||||
|
'textStyle'
|
||||||
|
],
|
||||||
|
'x-axis-selector-ant-v': [
|
||||||
|
'show',
|
||||||
|
'position',
|
||||||
|
'splitLine',
|
||||||
|
'axisForm',
|
||||||
|
'axisLabel'
|
||||||
|
],
|
||||||
|
'y-axis-selector-ant-v': [
|
||||||
|
'show',
|
||||||
|
'position',
|
||||||
|
'name',
|
||||||
|
'nameTextStyle',
|
||||||
|
'axisValue',
|
||||||
|
'splitLine',
|
||||||
|
'axisForm',
|
||||||
|
'axisLabel'
|
||||||
|
],
|
||||||
|
'title-selector-ant-v': [
|
||||||
|
'show',
|
||||||
|
'title',
|
||||||
|
'fontSize',
|
||||||
|
'color',
|
||||||
|
'hPosition',
|
||||||
|
'isItalic',
|
||||||
|
'isBolder',
|
||||||
|
'remarkShow',
|
||||||
|
'fontFamily',
|
||||||
|
'letterSpace',
|
||||||
|
'fontShadow'
|
||||||
|
],
|
||||||
|
'legend-selector-ant-v': [
|
||||||
|
'show',
|
||||||
|
'textStyle',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
render: 'antv',
|
render: 'antv',
|
||||||
category: 'chart.chart_type_distribute',
|
category: 'chart.chart_type_distribute',
|
||||||
|
|||||||
@ -45,7 +45,13 @@ import { baseLiquid } from '@/views/chart/chart/liquid/liquid'
|
|||||||
import { uuid } from 'vue-uuid'
|
import { uuid } from 'vue-uuid'
|
||||||
import ViewTrackBar from '@/components/canvas/components/editor/ViewTrackBar'
|
import ViewTrackBar from '@/components/canvas/components/editor/ViewTrackBar'
|
||||||
import { adjustPosition, getRemark, hexColorToRGBA } from '@/views/chart/chart/util'
|
import { adjustPosition, getRemark, hexColorToRGBA } from '@/views/chart/chart/util'
|
||||||
import { baseBarOptionAntV, hBaseBarOptionAntV, baseBidirectionalBarOptionAntV, timeRangeBarOptionAntV } from '@/views/chart/chart/bar/bar_antv'
|
import {
|
||||||
|
baseBarOptionAntV,
|
||||||
|
hBaseBarOptionAntV,
|
||||||
|
baseBidirectionalBarOptionAntV,
|
||||||
|
timeRangeBarOptionAntV,
|
||||||
|
stockLineOptionAntV
|
||||||
|
} from '@/views/chart/chart/bar/bar_antv'
|
||||||
import { baseAreaOptionAntV, baseLineOptionAntV } from '@/views/chart/chart/line/line_antv'
|
import { baseAreaOptionAntV, baseLineOptionAntV } from '@/views/chart/chart/line/line_antv'
|
||||||
import { basePieOptionAntV, basePieRoseOptionAntV } from '@/views/chart/chart/pie/pie_antv'
|
import { basePieOptionAntV, basePieRoseOptionAntV } from '@/views/chart/chart/pie/pie_antv'
|
||||||
import { baseScatterOptionAntV } from '@/views/chart/chart/scatter/scatter_antv'
|
import { baseScatterOptionAntV } from '@/views/chart/chart/scatter/scatter_antv'
|
||||||
@ -298,6 +304,8 @@ export default {
|
|||||||
this.myChart = await baseFlowMapOption(this.chartId, chart, this.antVAction)
|
this.myChart = await baseFlowMapOption(this.chartId, chart, this.antVAction)
|
||||||
} else if (chart.type === 'bidirectional-bar') {
|
} else if (chart.type === 'bidirectional-bar') {
|
||||||
this.myChart = baseBidirectionalBarOptionAntV(this.chartId, chart, this.antVAction)
|
this.myChart = baseBidirectionalBarOptionAntV(this.chartId, chart, this.antVAction)
|
||||||
|
} else if (chart.type === 'stock-line'){
|
||||||
|
this.myChart = stockLineOptionAntV(this.chartId, chart, this.antVAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.myChart && !equalsAny(chart.type, 'liquid', 'flow-map') && this.searchCount > 0) {
|
if (this.myChart && !equalsAny(chart.type, 'liquid', 'flow-map') && this.searchCount > 0) {
|
||||||
|
|||||||
@ -78,7 +78,7 @@
|
|||||||
v-model="functionForm.emptyDataStrategy"
|
v-model="functionForm.emptyDataStrategy"
|
||||||
@change="changeFunctionCfg"
|
@change="changeFunctionCfg"
|
||||||
>
|
>
|
||||||
<el-radio :label="'breakLine'">{{ $t('chart.break_line') }}</el-radio>
|
<el-radio v-show="showBreakOption" :label="'breakLine'">{{ $t('chart.break_line') }}</el-radio>
|
||||||
<el-radio :label="'setZero'">{{ $t('chart.set_zero') }}</el-radio>
|
<el-radio :label="'setZero'">{{ $t('chart.set_zero') }}</el-radio>
|
||||||
<el-radio
|
<el-radio
|
||||||
v-show="showIgnoreOption"
|
v-show="showIgnoreOption"
|
||||||
@ -114,6 +114,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { DEFAULT_FUNCTION_CFG, COLOR_PANEL } from '../../chart/chart'
|
import { DEFAULT_FUNCTION_CFG, COLOR_PANEL } from '../../chart/chart'
|
||||||
import { equalsAny, includesAny } from '@/utils/StringUtils'
|
import { equalsAny, includesAny } from '@/utils/StringUtils'
|
||||||
|
import {cloneDeep} from "lodash";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FunctionCfg',
|
name: 'FunctionCfg',
|
||||||
@ -134,7 +135,7 @@ export default {
|
|||||||
showSlider() {
|
showSlider() {
|
||||||
return this.chart.type !== 'bidirectional-bar' &&
|
return this.chart.type !== 'bidirectional-bar' &&
|
||||||
!equalsAny(this.chart.type, 'map') &&
|
!equalsAny(this.chart.type, 'map') &&
|
||||||
!includesAny(this.chart.type, 'table', 'text')
|
!includesAny(this.chart.type, 'table', 'text','stock-line')
|
||||||
},
|
},
|
||||||
showEmptyStrategy() {
|
showEmptyStrategy() {
|
||||||
return (this.chart.render === 'antv' &&
|
return (this.chart.render === 'antv' &&
|
||||||
@ -148,7 +149,10 @@ export default {
|
|||||||
return this.showEmptyStrategy &&
|
return this.showEmptyStrategy &&
|
||||||
includesAny(this.chart.type, 'table') &&
|
includesAny(this.chart.type, 'table') &&
|
||||||
this.functionForm.emptyDataStrategy !== 'breakLine'
|
this.functionForm.emptyDataStrategy !== 'breakLine'
|
||||||
}
|
},
|
||||||
|
showBreakOption() {
|
||||||
|
return !equalsAny(this.chart.type, 'stock-line')
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'chart': {
|
'chart': {
|
||||||
@ -170,10 +174,14 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
senior = JSON.parse(chart.senior)
|
senior = JSON.parse(chart.senior)
|
||||||
}
|
}
|
||||||
|
const defaultFunctionCfg = cloneDeep(DEFAULT_FUNCTION_CFG)
|
||||||
|
if (equalsAny(this.chart.type, 'stock-line')) {
|
||||||
|
defaultFunctionCfg.emptyDataStrategy = 'setZero'
|
||||||
|
}
|
||||||
if (senior.functionCfg) {
|
if (senior.functionCfg) {
|
||||||
this.functionForm = { ...DEFAULT_FUNCTION_CFG, ...senior.functionCfg }
|
this.functionForm = { ...defaultFunctionCfg, ...senior.functionCfg }
|
||||||
} else {
|
} else {
|
||||||
this.functionForm = JSON.parse(JSON.stringify(DEFAULT_FUNCTION_CFG))
|
this.functionForm = JSON.parse(JSON.stringify(defaultFunctionCfg))
|
||||||
}
|
}
|
||||||
this.initFieldCtrl()
|
this.initFieldCtrl()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2194,7 +2194,7 @@ export default {
|
|||||||
return equalsAny(this.view.type, 'table-normal', 'table-info', 'table-pivot')
|
return equalsAny(this.view.type, 'table-normal', 'table-info', 'table-pivot')
|
||||||
},
|
},
|
||||||
showAnalyseCfg() {
|
showAnalyseCfg() {
|
||||||
if (this.view.type === 'bidirectional-bar' || this.view.type === 'bar-time-range') {
|
if (this.view.type === 'bidirectional-bar' || this.view.type === 'bar-time-range' || this.view.type === 'stock-line') {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return includesAny(this.view.type, 'bar', 'line', 'area', 'gauge', 'liquid') ||
|
return includesAny(this.view.type, 'bar', 'line', 'area', 'gauge', 'liquid') ||
|
||||||
|
|||||||
@ -161,6 +161,10 @@ export default {
|
|||||||
margin:5px;
|
margin:5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-radio:last-child{
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.radio-row{
|
.radio-row{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user