mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
refactor: 使用EChart重构图表元素
This commit is contained in:
parent
352ac2601d
commit
0a78197a1d
45
package-lock.json
generated
45
package-lock.json
generated
@ -14,6 +14,7 @@
|
|||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dexie": "3.0.3",
|
"dexie": "3.0.3",
|
||||||
|
"echarts": "^5.5.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"hfmath": "^0.0.2",
|
"hfmath": "^0.0.2",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
@ -2317,6 +2318,15 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/echarts": {
|
||||||
|
"version": "5.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.5.1.tgz",
|
||||||
|
"integrity": "sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.3.0",
|
||||||
|
"zrender": "5.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
@ -4933,6 +4943,11 @@
|
|||||||
"typescript": ">=4.2.0"
|
"typescript": ">=4.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
||||||
|
},
|
||||||
"node_modules/txml": {
|
"node_modules/txml": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/txml/-/txml-5.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/txml/-/txml-5.1.1.tgz",
|
||||||
@ -5250,6 +5265,14 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zrender": {
|
||||||
|
"version": "5.6.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.6.0.tgz",
|
||||||
|
"integrity": "sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.3.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -6850,6 +6873,15 @@
|
|||||||
"is-obj": "^2.0.0"
|
"is-obj": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"echarts": {
|
||||||
|
"version": "5.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.5.1.tgz",
|
||||||
|
"integrity": "sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==",
|
||||||
|
"requires": {
|
||||||
|
"tslib": "2.3.0",
|
||||||
|
"zrender": "5.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"emoji-regex": {
|
"emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
@ -8929,6 +8961,11 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
|
"tslib": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
||||||
|
},
|
||||||
"txml": {
|
"txml": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/txml/-/txml-5.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/txml/-/txml-5.1.1.tgz",
|
||||||
@ -9137,6 +9174,14 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
|
},
|
||||||
|
"zrender": {
|
||||||
|
"version": "5.6.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.6.0.tgz",
|
||||||
|
"integrity": "sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==",
|
||||||
|
"requires": {
|
||||||
|
"tslib": "2.3.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dexie": "3.0.3",
|
"dexie": "3.0.3",
|
||||||
|
"echarts": "^5.5.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"hfmath": "^0.0.2",
|
"hfmath": "^0.0.2",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
|
54
src/configs/chart.ts
Normal file
54
src/configs/chart.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import type { ChartData } from '@/types/slides'
|
||||||
|
|
||||||
|
export const CHART_DEFAULT_DATA: { [key: string]: ChartData } = {
|
||||||
|
'bar': {
|
||||||
|
labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
|
||||||
|
legends: ['系列1', '系列2'],
|
||||||
|
series: [[12, 19, 5, 2, 18], [7, 11, 13, 21, 9]],
|
||||||
|
},
|
||||||
|
'column': {
|
||||||
|
labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
|
||||||
|
legends: ['系列1', '系列2'],
|
||||||
|
series: [[12, 19, 5, 2, 18], [7, 11, 13, 21, 9]],
|
||||||
|
},
|
||||||
|
'line': {
|
||||||
|
labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
|
||||||
|
legends: ['系列1', '系列2'],
|
||||||
|
series: [[12, 19, 5, 2, 18], [7, 11, 13, 21, 9]],
|
||||||
|
},
|
||||||
|
'pie': {
|
||||||
|
labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
|
||||||
|
legends: ['值'],
|
||||||
|
series: [[12, 19, 5, 2, 18]],
|
||||||
|
},
|
||||||
|
'ring': {
|
||||||
|
labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
|
||||||
|
legends: ['值'],
|
||||||
|
series: [[12, 19, 5, 2, 18]],
|
||||||
|
},
|
||||||
|
'area': {
|
||||||
|
labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
|
||||||
|
legends: ['系列1', '系列2'],
|
||||||
|
series: [[12, 19, 5, 2, 18], [7, 11, 13, 21, 9]],
|
||||||
|
},
|
||||||
|
'scatter': {
|
||||||
|
labels: ['坐标1', '坐标2', '坐标3', '坐标4', '坐标5'],
|
||||||
|
legends: ['X', 'Y'],
|
||||||
|
series: [[12, 19, 5, 2, 18], [7, 11, 13, 21, 9]],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CHART_PRESET_THEMES = [
|
||||||
|
['#d87c7c', '#919e8b', '#d7ab82', '#6e7074', '#61a0a8', '#efa18d'],
|
||||||
|
['#dd6b66', '#759aa0', '#e69d87', '#8dc1a9', '#ea7e53', '#eedd78'],
|
||||||
|
['#516b91', '#59c4e6', '#edafda', '#93b7e3', '#a5e7f0', '#cbb0e3'],
|
||||||
|
['#893448', '#d95850', '#eb8146', '#ffb248', '#f2d643', '#ebdba4'],
|
||||||
|
['#4ea397', '#22c3aa', '#7bd9a5', '#d0648a', '#f58db2', '#f2b3c9'],
|
||||||
|
['#3fb1e3', '#6be6c1', '#626c91', '#a0a7e6', '#c4ebad', '#96dee8'],
|
||||||
|
['#fc97af', '#87f7cf', '#f7f494', '#72ccff', '#f7c5a0', '#d4a4eb'],
|
||||||
|
['#c1232b', '#27727b', '#fcce10', '#e87c25', '#b5c334', '#fe8463'],
|
||||||
|
['#2ec7c9', '#b6a2de', '#5ab1ef', '#ffb980', '#d87a80', '#8d98b3'],
|
||||||
|
['#e01f54', '#001852', '#f5e8c8', '#b8d2c7', '#c6b38e', '#a4d8c2'],
|
||||||
|
['#c12e34', '#e6b600', '#0098d9', '#2b821d', '#005eaa', '#339ca8'],
|
||||||
|
['#8a7ca8', '#e098c7', '#8fd3e8', '#71669e', '#cc70af', '#7cb4cc'],
|
||||||
|
]
|
@ -1,15 +0,0 @@
|
|||||||
import type { ChartType } from '@/types/slides'
|
|
||||||
|
|
||||||
interface ChartTypes {
|
|
||||||
[propName: string]: ChartType
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CHART_TYPES: ChartTypes = {
|
|
||||||
bar: 'bar',
|
|
||||||
horizontalBar: 'bar',
|
|
||||||
line: 'line',
|
|
||||||
area: 'line',
|
|
||||||
scatter: 'line',
|
|
||||||
pie: 'pie',
|
|
||||||
ring: 'pie',
|
|
||||||
}
|
|
@ -2,10 +2,10 @@ import { storeToRefs } from 'pinia'
|
|||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
import { useMainStore, useSlidesStore } from '@/store'
|
import { useMainStore, useSlidesStore } from '@/store'
|
||||||
import { getImageSize } from '@/utils/image'
|
import { getImageSize } from '@/utils/image'
|
||||||
import type { PPTLineElement, PPTElement, TableCell, TableCellStyle, PPTShapeElement, PPTChartElement, ChartOptions, PresetChartType } from '@/types/slides'
|
import type { PPTLineElement, PPTElement, TableCell, TableCellStyle, PPTShapeElement, ChartType } from '@/types/slides'
|
||||||
import { type ShapePoolItem, SHAPE_PATH_FORMULAS } from '@/configs/shapes'
|
import { type ShapePoolItem, SHAPE_PATH_FORMULAS } from '@/configs/shapes'
|
||||||
import type { LinePoolItem } from '@/configs/lines'
|
import type { LinePoolItem } from '@/configs/lines'
|
||||||
import { CHART_TYPES } from '@/configs/chartTypes'
|
import { CHART_DEFAULT_DATA } from '@/configs/chart'
|
||||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||||
|
|
||||||
interface CommonElementPosition {
|
interface CommonElementPosition {
|
||||||
@ -86,40 +86,19 @@ export default () => {
|
|||||||
* 创建图表元素
|
* 创建图表元素
|
||||||
* @param chartType 图表类型
|
* @param chartType 图表类型
|
||||||
*/
|
*/
|
||||||
const createChartElement = (type: PresetChartType) => {
|
const createChartElement = (type: ChartType) => {
|
||||||
const newElement: PPTChartElement = {
|
createElement({
|
||||||
type: 'chart',
|
type: 'chart',
|
||||||
id: nanoid(10),
|
id: nanoid(10),
|
||||||
chartType: CHART_TYPES[type],
|
chartType: type,
|
||||||
left: 300,
|
left: 300,
|
||||||
top: 81.25,
|
top: 81.25,
|
||||||
width: 400,
|
width: 400,
|
||||||
height: 400,
|
height: 400,
|
||||||
rotate: 0,
|
rotate: 0,
|
||||||
themeColor: [theme.value.themeColor],
|
themeColors: [theme.value.themeColor],
|
||||||
gridColor: theme.value.fontColor,
|
textColor: theme.value.fontColor,
|
||||||
data: {
|
data: CHART_DEFAULT_DATA[type],
|
||||||
labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
|
|
||||||
legends: ['系列1'],
|
|
||||||
series: [
|
|
||||||
[12, 19, 5, 2, 18],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: ChartOptions = {
|
|
||||||
...(type === 'bar' ? { horizontalBars: false, stackBars: false } : {}),
|
|
||||||
...(type === 'horizontalBar' ? { horizontalBars: true, stackBars: false } : {}),
|
|
||||||
...(type === 'line' ? { showLine: true, lineSmooth: true, showArea: false } : {}),
|
|
||||||
...(type === 'area' ? { showLine: true, lineSmooth: true, showArea: true } : {}),
|
|
||||||
...(type === 'scatter' ? { showLine: false, lineSmooth: true, showArea: false } : {}),
|
|
||||||
...(type === 'pie' ? { donut: false } : {}),
|
|
||||||
...(type === 'ring' ? { donut: true } : {}),
|
|
||||||
}
|
|
||||||
|
|
||||||
createElement({
|
|
||||||
...newElement,
|
|
||||||
options,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -611,12 +611,12 @@ export default () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let chartColors: string[] = []
|
let chartColors: string[] = []
|
||||||
if (el.themeColor.length === 10) chartColors = el.themeColor.map(color => formatColor(color).color)
|
if (el.themeColors.length === 10) chartColors = el.themeColors.map(color => formatColor(color).color)
|
||||||
else if (el.themeColor.length === 1) chartColors = tinycolor(el.themeColor[0]).analogous(10).map(color => formatColor(color.toHexString()).color)
|
else if (el.themeColors.length === 1) chartColors = tinycolor(el.themeColors[0]).analogous(10).map(color => formatColor(color.toHexString()).color)
|
||||||
else {
|
else {
|
||||||
const len = el.themeColor.length
|
const len = el.themeColors.length
|
||||||
const supplement = tinycolor(el.themeColor[len - 1]).analogous(10 + 1 - len).map(color => color.toHexString())
|
const supplement = tinycolor(el.themeColors[len - 1]).analogous(10 + 1 - len).map(color => color.toHexString())
|
||||||
chartColors = [...el.themeColor.slice(0, len - 1), ...supplement].map(color => formatColor(color).color)
|
chartColors = [...el.themeColors.slice(0, len - 1), ...supplement].map(color => formatColor(color).color)
|
||||||
}
|
}
|
||||||
|
|
||||||
const options: pptxgen.IChartOpts = {
|
const options: pptxgen.IChartOpts = {
|
||||||
@ -624,40 +624,63 @@ export default () => {
|
|||||||
y: el.top / ratioPx2Inch.value,
|
y: el.top / ratioPx2Inch.value,
|
||||||
w: el.width / ratioPx2Inch.value,
|
w: el.width / ratioPx2Inch.value,
|
||||||
h: el.height / ratioPx2Inch.value,
|
h: el.height / ratioPx2Inch.value,
|
||||||
chartColors: el.chartType === 'pie' ? chartColors : chartColors.slice(0, el.data.series.length),
|
chartColors: (el.chartType === 'pie' || el.chartType === 'ring') ? chartColors : chartColors.slice(0, el.data.series.length),
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el.fill) options.plotArea = { fill: { color: formatColor(el.fill).color } }
|
const textColor = formatColor(el.textColor || '#000000').color
|
||||||
if (el.legend) {
|
options.catAxisLabelColor = textColor
|
||||||
|
options.valAxisLabelColor = textColor
|
||||||
|
|
||||||
|
const fontSize = 14 / ratioPx2Pt.value
|
||||||
|
options.catAxisLabelFontSize = fontSize
|
||||||
|
options.valAxisLabelFontSize = fontSize
|
||||||
|
|
||||||
|
if (el.fill || el.outline) {
|
||||||
|
const plotArea: pptxgen.IChartPropsFillLine = {}
|
||||||
|
if (el.fill) {
|
||||||
|
plotArea.fill = { color: formatColor(el.fill).color }
|
||||||
|
}
|
||||||
|
if (el.outline) {
|
||||||
|
plotArea.border = {
|
||||||
|
pt: el.outline.width! / ratioPx2Pt.value,
|
||||||
|
color: formatColor(el.outline.color!).color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
options.plotArea = plotArea
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((el.data.series.length > 1 && el.chartType !== 'scatter') || el.chartType === 'pie' || el.chartType === 'ring') {
|
||||||
options.showLegend = true
|
options.showLegend = true
|
||||||
options.legendPos = el.legend === 'top' ? 't' : 'b'
|
options.legendPos = 'b'
|
||||||
options.legendColor = formatColor(el.gridColor || '#000000').color
|
options.legendColor = textColor
|
||||||
options.legendFontSize = 14 / ratioPx2Pt.value
|
options.legendFontSize = fontSize
|
||||||
}
|
}
|
||||||
|
|
||||||
let type = pptx.ChartType.bar
|
let type = pptx.ChartType.bar
|
||||||
if (el.chartType === 'bar') {
|
if (el.chartType === 'bar') {
|
||||||
type = pptx.ChartType.bar
|
type = pptx.ChartType.bar
|
||||||
options.barDir = el.options?.horizontalBars ? 'bar' : 'col'
|
options.barDir = 'col'
|
||||||
|
}
|
||||||
|
else if (el.chartType === 'column') {
|
||||||
|
type = pptx.ChartType.bar
|
||||||
|
options.barDir = 'bar'
|
||||||
}
|
}
|
||||||
else if (el.chartType === 'line') {
|
else if (el.chartType === 'line') {
|
||||||
if (el.options?.showArea) type = pptx.ChartType.area
|
type = pptx.ChartType.line
|
||||||
else if (el.options?.showLine === false) {
|
}
|
||||||
|
else if (el.chartType === 'area') {
|
||||||
|
type = pptx.ChartType.area
|
||||||
|
}
|
||||||
|
else if (el.chartType === 'scatter') {
|
||||||
type = pptx.ChartType.scatter
|
type = pptx.ChartType.scatter
|
||||||
|
|
||||||
chartData.unshift({ name: 'X-Axis', values: Array(el.data.series[0].length).fill(0).map((v, i) => i) })
|
|
||||||
options.lineSize = 0
|
options.lineSize = 0
|
||||||
}
|
}
|
||||||
else type = pptx.ChartType.line
|
|
||||||
|
|
||||||
if (el.options?.lineSmooth) options.lineSmooth = true
|
|
||||||
}
|
|
||||||
else if (el.chartType === 'pie') {
|
else if (el.chartType === 'pie') {
|
||||||
if (el.options?.donut) {
|
type = pptx.ChartType.pie
|
||||||
type = pptx.ChartType.doughnut
|
|
||||||
options.holeSize = 75
|
|
||||||
}
|
}
|
||||||
else type = pptx.ChartType.pie
|
else if (el.chartType === 'ring') {
|
||||||
|
type = pptx.ChartType.doughnut
|
||||||
|
options.holeSize = 60
|
||||||
}
|
}
|
||||||
|
|
||||||
pptxSlide.addChart(type, chartData, options)
|
pptxSlide.addChart(type, chartData, options)
|
||||||
|
@ -14,7 +14,6 @@ import type {
|
|||||||
TableCellStyle,
|
TableCellStyle,
|
||||||
TableCell,
|
TableCell,
|
||||||
ChartType,
|
ChartType,
|
||||||
ChartOptions,
|
|
||||||
SlideBackground,
|
SlideBackground,
|
||||||
PPTShapeElement,
|
PPTShapeElement,
|
||||||
PPTLineElement,
|
PPTLineElement,
|
||||||
@ -389,10 +388,9 @@ export default () => {
|
|||||||
let series: number[][]
|
let series: number[][]
|
||||||
|
|
||||||
if (el.chartType === 'scatterChart' || el.chartType === 'bubbleChart') {
|
if (el.chartType === 'scatterChart' || el.chartType === 'bubbleChart') {
|
||||||
const data = el.data
|
labels = el.data[0].map((item, index) => `坐标${index + 1}`)
|
||||||
labels = data[0].map(item => item + '')
|
legends = ['X', 'Y']
|
||||||
legends = ['系列1']
|
series = el.data
|
||||||
series = [data[1]]
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const data = el.data as ChartItem[]
|
const data = el.data as ChartItem[]
|
||||||
@ -401,32 +399,32 @@ export default () => {
|
|||||||
series = data.map(item => item.values.map(v => v.y))
|
series = data.map(item => item.values.map(v => v.y))
|
||||||
}
|
}
|
||||||
|
|
||||||
const options: ChartOptions = {}
|
|
||||||
|
|
||||||
let chartType: ChartType = 'bar'
|
let chartType: ChartType = 'bar'
|
||||||
|
|
||||||
switch (el.chartType) {
|
switch (el.chartType) {
|
||||||
case 'barChart':
|
case 'barChart':
|
||||||
case 'bar3DChart':
|
case 'bar3DChart':
|
||||||
chartType = 'bar'
|
chartType = 'bar'
|
||||||
if (el.barDir === 'bar') options.horizontalBars = true
|
if (el.barDir === 'bar') chartType = 'column'
|
||||||
if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stackBars = true
|
|
||||||
break
|
break
|
||||||
case 'lineChart':
|
case 'lineChart':
|
||||||
case 'line3DChart':
|
case 'line3DChart':
|
||||||
|
chartType = 'line'
|
||||||
|
break
|
||||||
case 'areaChart':
|
case 'areaChart':
|
||||||
case 'area3DChart':
|
case 'area3DChart':
|
||||||
|
chartType = 'area'
|
||||||
|
break
|
||||||
case 'scatterChart':
|
case 'scatterChart':
|
||||||
case 'bubbleChart':
|
case 'bubbleChart':
|
||||||
chartType = 'line'
|
chartType = 'scatter'
|
||||||
if (el.chartType === 'areaChart' || el.chartType === 'area3DChart') options.showArea = true
|
|
||||||
if (el.chartType === 'scatterChart' || el.chartType === 'bubbleChart') options.showLine = false
|
|
||||||
break
|
break
|
||||||
case 'pieChart':
|
case 'pieChart':
|
||||||
case 'pie3DChart':
|
case 'pie3DChart':
|
||||||
case 'doughnutChart':
|
|
||||||
chartType = 'pie'
|
chartType = 'pie'
|
||||||
if (el.chartType === 'doughnutChart') options.donut = true
|
break
|
||||||
|
case 'doughnutChart':
|
||||||
|
chartType = 'ring'
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
@ -440,14 +438,13 @@ export default () => {
|
|||||||
left: el.left,
|
left: el.left,
|
||||||
top: el.top,
|
top: el.top,
|
||||||
rotate: 0,
|
rotate: 0,
|
||||||
themeColor: [theme.value.themeColor],
|
themeColors: [theme.value.themeColor],
|
||||||
gridColor: theme.value.fontColor,
|
textColor: theme.value.fontColor,
|
||||||
data: {
|
data: {
|
||||||
labels,
|
labels,
|
||||||
legends,
|
legends,
|
||||||
series,
|
series,
|
||||||
},
|
},
|
||||||
options,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
else if (el.type === 'group' || el.type === 'diagram') {
|
else if (el.type === 'group' || el.type === 'diagram') {
|
||||||
|
@ -395,9 +395,7 @@ export interface PPTLineElement extends Omit<PPTBaseElement, 'height' | 'rotate'
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export type PresetChartType = 'bar' | 'horizontalBar' | 'line' | 'area' | 'scatter' | 'pie' | 'ring'
|
export type ChartType = 'bar' | 'column' | 'line' | 'pie' | 'ring' | 'area' | 'scatter'
|
||||||
export type ChartType = 'bar' | 'line' | 'pie'
|
|
||||||
export type ChartOptions = LineChartOptions & BarChartOptions & PieChartOptions
|
|
||||||
export interface ChartData {
|
export interface ChartData {
|
||||||
labels: string[]
|
labels: string[]
|
||||||
legends: string[]
|
legends: string[]
|
||||||
@ -415,26 +413,20 @@ export interface ChartData {
|
|||||||
*
|
*
|
||||||
* data: 图表数据
|
* data: 图表数据
|
||||||
*
|
*
|
||||||
* options?: 图表配置项
|
|
||||||
*
|
|
||||||
* outline?: 边框
|
* outline?: 边框
|
||||||
*
|
*
|
||||||
* themeColor: 主题色
|
* themeColors: 主题色
|
||||||
*
|
*
|
||||||
* gridColor?: 网格&坐标颜色
|
* textColor?: 文字颜色
|
||||||
*
|
|
||||||
* legend?: 图例/位置
|
|
||||||
*/
|
*/
|
||||||
export interface PPTChartElement extends PPTBaseElement {
|
export interface PPTChartElement extends PPTBaseElement {
|
||||||
type: 'chart'
|
type: 'chart'
|
||||||
fill?: string
|
fill?: string
|
||||||
chartType: ChartType
|
chartType: ChartType
|
||||||
data: ChartData
|
data: ChartData
|
||||||
options?: ChartOptions
|
|
||||||
outline?: PPTElementOutline
|
outline?: PPTElementOutline
|
||||||
themeColor: string[]
|
themeColors: string[]
|
||||||
gridColor?: string
|
textColor?: string
|
||||||
legend?: '' | 'top' | 'bottom'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<IconChartLine size="24" v-if="chart === 'line'" />
|
<IconChartLine size="24" v-if="chart === 'line'" />
|
||||||
<IconChartHistogram size="24" v-else-if="chart === 'bar'" />
|
<IconChartHistogram size="24" v-else-if="chart === 'bar'" />
|
||||||
<IconChartPie size="24" v-else-if="chart === 'pie'" />
|
<IconChartPie size="24" v-else-if="chart === 'pie'" />
|
||||||
<IconChartHistogramOne size="24" v-else-if="chart === 'horizontalBar'" />
|
<IconChartHistogramOne size="24" v-else-if="chart === 'column'" />
|
||||||
<IconChartLineArea size="24" v-else-if="chart === 'area'" />
|
<IconChartLineArea size="24" v-else-if="chart === 'area'" />
|
||||||
<IconChartRing size="24" v-else-if="chart === 'ring'" />
|
<IconChartRing size="24" v-else-if="chart === 'ring'" />
|
||||||
<IconChartScatter size="24" v-else-if="chart === 'scatter'" />
|
<IconChartScatter size="24" v-else-if="chart === 'scatter'" />
|
||||||
@ -15,15 +15,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { PresetChartType } from '@/types/slides'
|
import type { ChartType } from '@/types/slides'
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: 'select', payload: PresetChartType): void
|
(event: 'select', payload: ChartType): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const chartList: PresetChartType[] = ['bar', 'horizontalBar', 'line', 'area', 'scatter', 'pie', 'ring']
|
const chartList: ChartType[] = ['bar', 'column', 'line', 'area', 'scatter', 'pie', 'ring']
|
||||||
|
|
||||||
const selectChart = (chart: PresetChartType) => {
|
const selectChart = (chart: ChartType) => {
|
||||||
emit('select', chart)
|
emit('select', chart)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -6,63 +6,6 @@
|
|||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<template v-if="handleChartElement.chartType === 'line'">
|
|
||||||
<div class="row">
|
|
||||||
<Checkbox
|
|
||||||
@update:value="value => updateOptions({ showArea: value })"
|
|
||||||
:value="showArea"
|
|
||||||
style="flex: 1;"
|
|
||||||
>面积图样式</Checkbox>
|
|
||||||
<Checkbox
|
|
||||||
@update:value="value => updateOptions({ showLine: value })"
|
|
||||||
:value="!showLine"
|
|
||||||
style="flex: 1;"
|
|
||||||
>散点图样式</Checkbox>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<Checkbox
|
|
||||||
@update:value="value => updateOptions({ lineSmooth: value })"
|
|
||||||
:value="lineSmooth"
|
|
||||||
>使用平滑曲线</Checkbox>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="row" v-if="handleChartElement.chartType === 'bar'">
|
|
||||||
<Checkbox
|
|
||||||
@update:value="value => updateOptions({ horizontalBars: value })"
|
|
||||||
:value="horizontalBars"
|
|
||||||
style="flex: 1;"
|
|
||||||
>条形图样式</Checkbox>
|
|
||||||
<Checkbox
|
|
||||||
@update:value="value => updateOptions({ stackBars: value })"
|
|
||||||
:value="stackBars"
|
|
||||||
style="flex: 1;"
|
|
||||||
>堆叠样式</Checkbox>
|
|
||||||
</div>
|
|
||||||
<div class="row" v-if="handleChartElement.chartType === 'pie'">
|
|
||||||
<Checkbox
|
|
||||||
@update:value="value => updateOptions({ donut: value })"
|
|
||||||
:value="donut"
|
|
||||||
>环形图样式</Checkbox>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div style="width: 40%;">图例:</div>
|
|
||||||
<Select
|
|
||||||
style="width: 60%;"
|
|
||||||
:value="legend"
|
|
||||||
@update:value="value => updateLegend(value as '' | 'top' | 'bottom')"
|
|
||||||
:options="[
|
|
||||||
{ label: '不显示', value: '' },
|
|
||||||
{ label: '显示在上方', value: 'top' },
|
|
||||||
{ label: '显示在下方', value: 'bottom' },
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div style="width: 40%;">背景填充:</div>
|
<div style="width: 40%;">背景填充:</div>
|
||||||
<Popover trigger="click" style="width: 60%;">
|
<Popover trigger="click" style="width: 60%;">
|
||||||
@ -76,21 +19,21 @@
|
|||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div style="width: 40%;">网格颜色:</div>
|
<div style="width: 40%;">文字颜色:</div>
|
||||||
<Popover trigger="click" style="width: 60%;">
|
<Popover trigger="click" style="width: 60%;">
|
||||||
<template #content>
|
<template #content>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
:modelValue="gridColor"
|
:modelValue="textColor"
|
||||||
@update:modelValue="value => updateGridColor(value)"
|
@update:modelValue="value => updateTextColor(value)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<ColorButton :color="gridColor" />
|
<ColorButton :color="textColor" />
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<div class="row" v-for="(color, index) in themeColor" :key="index">
|
<div class="row" v-for="(color, index) in themeColors" :key="index">
|
||||||
<div style="width: 40%;">{{index === 0 ? '主题配色:' : ''}}</div>
|
<div style="width: 40%;">{{index === 0 ? '主题配色:' : ''}}</div>
|
||||||
<Popover trigger="click" style="width: 60%;">
|
<Popover trigger="click" style="width: 60%;">
|
||||||
<template #content>
|
<template #content>
|
||||||
@ -109,7 +52,7 @@
|
|||||||
<Popover trigger="click" v-model:open="presetThemesVisible" style="width: 40%;">
|
<Popover trigger="click" v-model:open="presetThemesVisible" style="width: 40%;">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="preset-themes">
|
<div class="preset-themes">
|
||||||
<div class="preset-theme" v-for="(item, index) in presetChartThemes" :key="index">
|
<div class="preset-theme" v-for="(item, index) in CHART_PRESET_THEMES" :key="index">
|
||||||
<div
|
<div
|
||||||
class="preset-theme-color"
|
class="preset-theme-color"
|
||||||
:class="{ 'select': presetThemeColorHoverIndex[0] === index && itemIndex <= presetThemeColorHoverIndex[1] }"
|
:class="{ 'select': presetThemeColorHoverIndex[0] === index && itemIndex <= presetThemeColorHoverIndex[1] }"
|
||||||
@ -127,7 +70,7 @@
|
|||||||
</Popover>
|
</Popover>
|
||||||
<Button
|
<Button
|
||||||
last
|
last
|
||||||
:disabled="themeColor.length >= 10"
|
:disabled="themeColors.length >= 10"
|
||||||
style="width: 60%;"
|
style="width: 60%;"
|
||||||
@click="addThemeColor()"
|
@click="addThemeColor()"
|
||||||
>
|
>
|
||||||
@ -156,9 +99,10 @@
|
|||||||
import { onUnmounted, ref, watch, type Ref } from 'vue'
|
import { onUnmounted, ref, watch, type Ref } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useMainStore, useSlidesStore } from '@/store'
|
import { useMainStore, useSlidesStore } from '@/store'
|
||||||
import type { ChartData, ChartOptions, PPTChartElement } from '@/types/slides'
|
import type { ChartData, PPTChartElement } from '@/types/slides'
|
||||||
import emitter, { EmitterEvents } from '@/utils/emitter'
|
import emitter, { EmitterEvents } from '@/utils/emitter'
|
||||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||||
|
import { CHART_PRESET_THEMES } from '@/configs/chart'
|
||||||
|
|
||||||
import ElementOutline from '../../common/ElementOutline.vue'
|
import ElementOutline from '../../common/ElementOutline.vue'
|
||||||
import ChartDataEditor from './ChartDataEditor.vue'
|
import ChartDataEditor from './ChartDataEditor.vue'
|
||||||
@ -166,27 +110,10 @@ import ColorButton from '@/components/ColorButton.vue'
|
|||||||
import ColorPicker from '@/components/ColorPicker/index.vue'
|
import ColorPicker from '@/components/ColorPicker/index.vue'
|
||||||
import Modal from '@/components/Modal.vue'
|
import Modal from '@/components/Modal.vue'
|
||||||
import Divider from '@/components/Divider.vue'
|
import Divider from '@/components/Divider.vue'
|
||||||
import Checkbox from '@/components/Checkbox.vue'
|
|
||||||
import Button from '@/components/Button.vue'
|
import Button from '@/components/Button.vue'
|
||||||
import ButtonGroup from '@/components/ButtonGroup.vue'
|
import ButtonGroup from '@/components/ButtonGroup.vue'
|
||||||
import Select from '@/components/Select.vue'
|
|
||||||
import Popover from '@/components/Popover.vue'
|
import Popover from '@/components/Popover.vue'
|
||||||
|
|
||||||
const presetChartThemes = [
|
|
||||||
['#d87c7c', '#919e8b', '#d7ab82', '#6e7074', '#61a0a8', '#efa18d'],
|
|
||||||
['#dd6b66', '#759aa0', '#e69d87', '#8dc1a9', '#ea7e53', '#eedd78'],
|
|
||||||
['#516b91', '#59c4e6', '#edafda', '#93b7e3', '#a5e7f0', '#cbb0e3'],
|
|
||||||
['#893448', '#d95850', '#eb8146', '#ffb248', '#f2d643', '#ebdba4'],
|
|
||||||
['#4ea397', '#22c3aa', '#7bd9a5', '#d0648a', '#f58db2', '#f2b3c9'],
|
|
||||||
['#3fb1e3', '#6be6c1', '#626c91', '#a0a7e6', '#c4ebad', '#96dee8'],
|
|
||||||
['#fc97af', '#87f7cf', '#f7f494', '#72ccff', '#f7c5a0', '#d4a4eb'],
|
|
||||||
['#c1232b', '#27727b', '#fcce10', '#e87c25', '#b5c334', '#fe8463'],
|
|
||||||
['#2ec7c9', '#b6a2de', '#5ab1ef', '#ffb980', '#d87a80', '#8d98b3'],
|
|
||||||
['#e01f54', '#001852', '#f5e8c8', '#b8d2c7', '#c6b38e', '#a4d8c2'],
|
|
||||||
['#c12e34', '#e6b600', '#0098d9', '#2b821d', '#005eaa', '#339ca8'],
|
|
||||||
['#8a7ca8', '#e098c7', '#8fd3e8', '#71669e', '#cc70af', '#7cb4cc'],
|
|
||||||
]
|
|
||||||
|
|
||||||
const mainStore = useMainStore()
|
const mainStore = useMainStore()
|
||||||
const slidesStore = useSlidesStore()
|
const slidesStore = useSlidesStore()
|
||||||
const { handleElement, handleElementId } = storeToRefs(mainStore)
|
const { handleElement, handleElementId } = storeToRefs(mainStore)
|
||||||
@ -202,42 +129,15 @@ const { addHistorySnapshot } = useHistorySnapshot()
|
|||||||
|
|
||||||
const fill = ref<string>('#000')
|
const fill = ref<string>('#000')
|
||||||
|
|
||||||
const themeColor = ref<string[]>([])
|
const themeColors = ref<string[]>([])
|
||||||
const gridColor = ref('')
|
const textColor = ref('')
|
||||||
const legend = ref('')
|
|
||||||
|
|
||||||
const lineSmooth = ref(true)
|
|
||||||
const showLine = ref(true)
|
|
||||||
const showArea = ref(false)
|
|
||||||
const horizontalBars = ref(false)
|
|
||||||
const donut = ref(false)
|
|
||||||
const stackBars = ref(false)
|
|
||||||
|
|
||||||
watch(handleElement, () => {
|
watch(handleElement, () => {
|
||||||
if (!handleElement.value || handleElement.value.type !== 'chart') return
|
if (!handleElement.value || handleElement.value.type !== 'chart') return
|
||||||
fill.value = handleElement.value.fill || '#fff'
|
fill.value = handleElement.value.fill || '#fff'
|
||||||
|
|
||||||
if (handleElement.value.options) {
|
themeColors.value = handleElement.value.themeColors
|
||||||
const {
|
textColor.value = handleElement.value.textColor || '#333'
|
||||||
lineSmooth: _lineSmooth,
|
|
||||||
showLine: _showLine,
|
|
||||||
showArea: _showArea,
|
|
||||||
horizontalBars: _horizontalBars,
|
|
||||||
donut: _donut,
|
|
||||||
stackBars: _stackBars,
|
|
||||||
} = handleElement.value.options
|
|
||||||
|
|
||||||
lineSmooth.value = !!_lineSmooth
|
|
||||||
showLine.value = !!_showLine
|
|
||||||
showArea.value = !!_showArea
|
|
||||||
horizontalBars.value = !!_horizontalBars
|
|
||||||
donut.value = !!_donut
|
|
||||||
stackBars.value = !!_stackBars
|
|
||||||
}
|
|
||||||
|
|
||||||
themeColor.value = handleElement.value.themeColor
|
|
||||||
gridColor.value = handleElement.value.gridColor || '#333'
|
|
||||||
legend.value = handleElement.value.legend || ''
|
|
||||||
}, { deep: true, immediate: true })
|
}, { deep: true, immediate: true })
|
||||||
|
|
||||||
const updateElement = (props: Partial<PPTChartElement>) => {
|
const updateElement = (props: Partial<PPTChartElement>) => {
|
||||||
@ -256,18 +156,10 @@ const updateFill = (value: string) => {
|
|||||||
updateElement({ fill: value })
|
updateElement({ fill: value })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置其他选项:柱状图转条形图、折线图转面积图、折线图转散点图、饼图转环形图、折线图开关平滑曲线
|
|
||||||
const updateOptions = (optionProps: ChartOptions) => {
|
|
||||||
const _handleElement = handleElement.value as PPTChartElement
|
|
||||||
|
|
||||||
const newOptions = { ..._handleElement.options, ...optionProps }
|
|
||||||
updateElement({ options: newOptions })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置主题色
|
// 设置主题色
|
||||||
const updateTheme = (color: string, index: number) => {
|
const updateTheme = (color: string, index: number) => {
|
||||||
const props = {
|
const props = {
|
||||||
themeColor: themeColor.value.map((c, i) => i === index ? color : c),
|
themeColors: themeColors.value.map((c, i) => i === index ? color : c),
|
||||||
}
|
}
|
||||||
updateElement(props)
|
updateElement(props)
|
||||||
}
|
}
|
||||||
@ -275,34 +167,29 @@ const updateTheme = (color: string, index: number) => {
|
|||||||
// 添加主题色
|
// 添加主题色
|
||||||
const addThemeColor = () => {
|
const addThemeColor = () => {
|
||||||
const props = {
|
const props = {
|
||||||
themeColor: [...themeColor.value, theme.value.themeColor],
|
themeColors: [...themeColors.value, theme.value.themeColor],
|
||||||
}
|
}
|
||||||
updateElement(props)
|
updateElement(props)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用预置主题配色
|
// 使用预置主题配色
|
||||||
const applyPresetTheme = (colors: string[], index: number) => {
|
const applyPresetTheme = (colors: string[], index: number) => {
|
||||||
const themeColor = colors.slice(0, index + 1)
|
const themeColors = colors.slice(0, index + 1)
|
||||||
updateElement({ themeColor })
|
updateElement({ themeColors })
|
||||||
presetThemesVisible.value = false
|
presetThemesVisible.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除主题色
|
// 删除主题色
|
||||||
const deleteThemeColor = (index: number) => {
|
const deleteThemeColor = (index: number) => {
|
||||||
const props = {
|
const props = {
|
||||||
themeColor: themeColor.value.filter((c, i) => i !== index),
|
themeColors: themeColors.value.filter((c, i) => i !== index),
|
||||||
}
|
}
|
||||||
updateElement(props)
|
updateElement(props)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置网格颜色
|
// 设置文字颜色
|
||||||
const updateGridColor = (gridColor: string) => {
|
const updateTextColor = (textColor: string) => {
|
||||||
updateElement({ gridColor })
|
updateElement({ textColor })
|
||||||
}
|
|
||||||
|
|
||||||
// 设置图例位置/不显示
|
|
||||||
const updateLegend = (legend: '' | 'top' | 'bottom') => {
|
|
||||||
updateElement({ legend })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const openDataEditor = () => chartDataEditorVisible.value = true
|
const openDataEditor = () => chartDataEditorVisible.value = true
|
||||||
|
@ -23,16 +23,13 @@
|
|||||||
:outline="elementInfo.outline"
|
:outline="elementInfo.outline"
|
||||||
/>
|
/>
|
||||||
<Chart
|
<Chart
|
||||||
:width="elementInfo.width * zoom"
|
:width="elementInfo.width"
|
||||||
:height="elementInfo.height * zoom"
|
:height="elementInfo.height"
|
||||||
:type="elementInfo.chartType"
|
:type="elementInfo.chartType"
|
||||||
:data="elementInfo.data"
|
:data="elementInfo.data"
|
||||||
:options="elementInfo.options"
|
:themeColors="elementInfo.themeColors"
|
||||||
:themeColor="elementInfo.themeColor"
|
:textColor="elementInfo.textColor"
|
||||||
:gridColor="elementInfo.gridColor"
|
|
||||||
:legends="elementInfo.data.legends"
|
:legends="elementInfo.data.legends"
|
||||||
:legend="elementInfo.legend || ''"
|
|
||||||
:style="{ zoom: 1 / zoom }"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -40,9 +37,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, ref } from 'vue'
|
|
||||||
import type { PPTChartElement } from '@/types/slides'
|
import type { PPTChartElement } from '@/types/slides'
|
||||||
import { injectKeySlideScale } from '@/types/injectKey'
|
|
||||||
|
|
||||||
import ElementOutline from '@/views/components/element/ElementOutline.vue'
|
import ElementOutline from '@/views/components/element/ElementOutline.vue'
|
||||||
import Chart from './Chart.vue'
|
import Chart from './Chart.vue'
|
||||||
@ -50,11 +45,6 @@ import Chart from './Chart.vue'
|
|||||||
defineProps<{
|
defineProps<{
|
||||||
elementInfo: PPTChartElement
|
elementInfo: PPTChartElement
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const slideScale = inject(injectKeySlideScale) || ref(1)
|
|
||||||
|
|
||||||
const needScaleSize = computed(() => slideScale.value < 1)
|
|
||||||
const zoom = computed(() => needScaleSize.value ? 1 / slideScale.value : 1)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -1,37 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div class="chart" ref="chartRef"></div>
|
||||||
class="chart"
|
|
||||||
:style="{ flexDirection: legend === 'top' ? 'column-reverse' : 'column' }"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="chart-content"
|
|
||||||
ref="chartRef"
|
|
||||||
:style="{
|
|
||||||
width: width + 'px',
|
|
||||||
height: chartHeight + 'px',
|
|
||||||
transform: `scale(${1 / slideScale})`,
|
|
||||||
}"
|
|
||||||
></div>
|
|
||||||
<div class="legends" :style="{ transform: `scale(${1 / slideScale})` }" v-if="legend">
|
|
||||||
<div
|
|
||||||
class="legend"
|
|
||||||
v-for="(legend, index) in legends"
|
|
||||||
:key="index"
|
|
||||||
:style="{ color: gridColor }"
|
|
||||||
>
|
|
||||||
<div class="block" :style="{ backgroundColor: themeColors[index] }"></div>
|
|
||||||
{{legend}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, nextTick, onMounted, ref, watch } from 'vue'
|
import { onMounted, ref, computed, watch } from 'vue'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
import tinycolor from 'tinycolor2'
|
import tinycolor from 'tinycolor2'
|
||||||
import { BarChart, LineChart, PieChart } from 'chartist'
|
import type { ChartData, ChartType } from '@/types/slides'
|
||||||
import type { ChartData, ChartOptions, ChartType } from '@/types/slides'
|
import { getChartOption } from './chartOption'
|
||||||
import { injectKeySlideScale } from '@/types/injectKey'
|
|
||||||
|
|
||||||
import 'chartist/dist/index.css'
|
import 'chartist/dist/index.css'
|
||||||
|
|
||||||
@ -40,178 +16,54 @@ const props = defineProps<{
|
|||||||
height: number
|
height: number
|
||||||
type: ChartType
|
type: ChartType
|
||||||
data: ChartData
|
data: ChartData
|
||||||
themeColor: string[]
|
themeColors: string[]
|
||||||
legends: string[]
|
legends: string[]
|
||||||
options?: ChartOptions
|
textColor?: string
|
||||||
gridColor?: string
|
|
||||||
legend?: '' | 'top' | 'bottom'
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
let chart: echarts.ECharts | null = null
|
||||||
const chartRef = ref<HTMLElement>()
|
const chartRef = ref<HTMLElement>()
|
||||||
const slideScale = inject(injectKeySlideScale) || ref(1)
|
|
||||||
|
|
||||||
let chart: LineChart | BarChart | PieChart | undefined
|
|
||||||
|
|
||||||
const chartHeight = computed(() => {
|
|
||||||
if (props.legend) return props.height - 20
|
|
||||||
return props.height
|
|
||||||
})
|
|
||||||
|
|
||||||
const getPieChartData = () => ({ ...props.data, series: props.data.series[0] })
|
|
||||||
|
|
||||||
const getOptions = () => {
|
|
||||||
const propsOptopns = props.options || {}
|
|
||||||
return {
|
|
||||||
...propsOptopns,
|
|
||||||
width: props.width * slideScale.value,
|
|
||||||
height: chartHeight.value * slideScale.value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderChart = () => {
|
|
||||||
if (!chartRef.value) return
|
|
||||||
|
|
||||||
const options = getOptions()
|
|
||||||
if (props.type === 'bar') chart = new BarChart(chartRef.value, props.data, options)
|
|
||||||
if (props.type === 'line') chart = new LineChart(chartRef.value, props.data, options)
|
|
||||||
if (props.type === 'pie') chart = new PieChart(chartRef.value, getPieChartData(), options)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateChart = () => {
|
|
||||||
nextTick(() => {
|
|
||||||
if (!chart) {
|
|
||||||
renderChart()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const options = getOptions()
|
|
||||||
const data = props.type === 'pie' ? getPieChartData() : props.data
|
|
||||||
chart.update(data, options)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
watch([
|
|
||||||
() => props.width,
|
|
||||||
() => props.height,
|
|
||||||
() => props.data,
|
|
||||||
() => props.options,
|
|
||||||
slideScale,
|
|
||||||
], updateChart)
|
|
||||||
|
|
||||||
onMounted(renderChart)
|
|
||||||
|
|
||||||
const themeColors = computed(() => {
|
const themeColors = computed(() => {
|
||||||
let colors: string[] = []
|
let colors: string[] = []
|
||||||
if (props.themeColor.length >= 10) colors = props.themeColor
|
if (props.themeColors.length >= 10) colors = props.themeColors
|
||||||
else if (props.themeColor.length === 1) colors = tinycolor(props.themeColor[0]).analogous(10).map(color => color.toRgbString())
|
else if (props.themeColors.length === 1) colors = tinycolor(props.themeColors[0]).analogous(10).map(color => color.toRgbString())
|
||||||
else {
|
else {
|
||||||
const len = props.themeColor.length
|
const len = props.themeColors.length
|
||||||
const supplement = tinycolor(props.themeColor[len - 1]).analogous(10 + 1 - len).map(color => color.toRgbString())
|
const supplement = tinycolor(props.themeColors[len - 1]).analogous(10 + 1 - len).map(color => color.toRgbString())
|
||||||
colors = [...props.themeColor.slice(0, len - 1), ...supplement]
|
colors = [...props.themeColors.slice(0, len - 1), ...supplement]
|
||||||
}
|
}
|
||||||
return colors
|
return colors
|
||||||
})
|
})
|
||||||
|
|
||||||
// 更新主题配色:
|
const updateOption = () => {
|
||||||
// 如果当前所设置的主题色数小于10,剩余部分获取最后一个主题色的相近颜色作为配色
|
const option = getChartOption({
|
||||||
const updateTheme = () => {
|
type: props.type,
|
||||||
if (!chartRef.value) return
|
data: props.data,
|
||||||
|
themeColors: themeColors.value,
|
||||||
for (let i = 0; i < 10; i++) {
|
textColor: props.textColor,
|
||||||
chartRef.value.style.setProperty(`--theme-color-${i + 1}`, themeColors.value[i])
|
})
|
||||||
}
|
if (option) chart!.setOption(option, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(themeColors, updateTheme)
|
onMounted(() => {
|
||||||
onMounted(updateTheme)
|
chart = echarts.init(chartRef.value, null, { renderer: 'svg' })
|
||||||
|
updateOption()
|
||||||
|
|
||||||
// 更新网格颜色,包括坐标的文字部分
|
const resizeListener = () => chart!.resize()
|
||||||
const updateGridColor = () => {
|
const resizeObserver = new ResizeObserver(resizeListener)
|
||||||
if (!chartRef.value) return
|
resizeObserver.observe(chartRef.value!)
|
||||||
if (props.gridColor) chartRef.value.style.setProperty(`--grid-color`, props.gridColor)
|
})
|
||||||
}
|
|
||||||
|
|
||||||
watch(() => props.gridColor, updateGridColor)
|
watch(() => props.type, updateOption)
|
||||||
onMounted(updateGridColor)
|
watch(() => props.data, updateOption)
|
||||||
|
watch(() => props.themeColors, updateOption)
|
||||||
|
watch(() => props.textColor, updateOption)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.chart {
|
.chart {
|
||||||
display: flex;
|
width: 100%;
|
||||||
}
|
height: 100%;
|
||||||
|
|
||||||
.chart-content {
|
|
||||||
transform-origin: 0 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.chart-content {
|
|
||||||
$ct-series-names: (a, b, c, d, e, f, g, h, i, j);
|
|
||||||
|
|
||||||
--theme-color-1: #666;
|
|
||||||
--theme-color-2: #666;
|
|
||||||
--theme-color-3: #666;
|
|
||||||
--theme-color-4: #666;
|
|
||||||
--theme-color-5: #666;
|
|
||||||
--theme-color-6: #666;
|
|
||||||
--theme-color-7: #666;
|
|
||||||
--theme-color-8: #666;
|
|
||||||
--theme-color-9: #666;
|
|
||||||
--theme-color-10: #666;
|
|
||||||
|
|
||||||
@for $i from 1 to length($ct-series-names) {
|
|
||||||
$color: var(--theme-color-#{$i});
|
|
||||||
|
|
||||||
.ct-series-#{nth($ct-series-names, $i)} .ct-line {
|
|
||||||
stroke: $color;
|
|
||||||
}
|
|
||||||
.ct-series-#{nth($ct-series-names, $i)} .ct-point {
|
|
||||||
stroke: $color;
|
|
||||||
}
|
|
||||||
.ct-series-#{nth($ct-series-names, $i)} .ct-area {
|
|
||||||
fill: $color;
|
|
||||||
}
|
|
||||||
.ct-series-#{nth($ct-series-names, $i)} .ct-bar {
|
|
||||||
stroke: $color;
|
|
||||||
}
|
|
||||||
.ct-series-#{nth($ct-series-names, $i)} .ct-slice-pie {
|
|
||||||
fill: $color;
|
|
||||||
}
|
|
||||||
.ct-series-#{nth($ct-series-names, $i)} .ct-slice-donut {
|
|
||||||
stroke: $color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
--grid-color: rgba(0, 0, 0, 0.4);
|
|
||||||
|
|
||||||
.ct-grid {
|
|
||||||
stroke: var(--grid-color);
|
|
||||||
}
|
|
||||||
.ct-label {
|
|
||||||
fill: var(--grid-color);
|
|
||||||
color: var(--grid-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.legends {
|
|
||||||
height: 20px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.legend {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
& + .legend {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
276
src/views/components/element/ChartElement/chartOption.ts
Normal file
276
src/views/components/element/ChartElement/chartOption.ts
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
import * as echarts from 'echarts'
|
||||||
|
import type { ChartData, ChartType } from '@/types/slides'
|
||||||
|
|
||||||
|
export interface ChartOptionPayload {
|
||||||
|
type: ChartType
|
||||||
|
data: ChartData
|
||||||
|
themeColors: string[]
|
||||||
|
textColor?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getChartOption = ({
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
themeColors,
|
||||||
|
textColor,
|
||||||
|
}: ChartOptionPayload): echarts.EChartsOption | null => {
|
||||||
|
if(type === 'bar') {
|
||||||
|
return {
|
||||||
|
color: themeColors,
|
||||||
|
textStyle: textColor ? {
|
||||||
|
color: textColor,
|
||||||
|
} : {},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'shadow',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: data.series.length > 1 ? {
|
||||||
|
top: 'bottom',
|
||||||
|
textStyle: textColor ? {
|
||||||
|
color: textColor,
|
||||||
|
} : {},
|
||||||
|
} : undefined,
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: data.labels,
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
},
|
||||||
|
series: data.series.map((item, index) => ({
|
||||||
|
data: item,
|
||||||
|
name: data.legends[index],
|
||||||
|
type: 'bar',
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(type === 'column') {
|
||||||
|
return {
|
||||||
|
color: themeColors,
|
||||||
|
textStyle: textColor ? {
|
||||||
|
color: textColor,
|
||||||
|
} : {},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'shadow',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: data.series.length > 1 ? {
|
||||||
|
top: 'bottom',
|
||||||
|
textStyle: textColor ? {
|
||||||
|
color: textColor,
|
||||||
|
} : {},
|
||||||
|
} : undefined,
|
||||||
|
yAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: data.labels,
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'value',
|
||||||
|
},
|
||||||
|
series: data.series.map((item, index) => ({
|
||||||
|
data: item,
|
||||||
|
name: data.legends[index],
|
||||||
|
type: 'bar',
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(type === 'line') {
|
||||||
|
return {
|
||||||
|
color: themeColors,
|
||||||
|
textStyle: textColor ? {
|
||||||
|
color: textColor,
|
||||||
|
} : {},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'shadow',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: data.series.length > 1 ? {
|
||||||
|
top: 'bottom',
|
||||||
|
textStyle: textColor ? {
|
||||||
|
color: textColor,
|
||||||
|
} : {},
|
||||||
|
} : undefined,
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: data.labels,
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
},
|
||||||
|
series: data.series.map((item, index) => ({
|
||||||
|
data: item,
|
||||||
|
name: data.legends[index],
|
||||||
|
type: 'line',
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(type === 'pie') {
|
||||||
|
return {
|
||||||
|
color: themeColors,
|
||||||
|
textStyle: textColor ? {
|
||||||
|
color: textColor,
|
||||||
|
} : {},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'shadow',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
top: 'bottom',
|
||||||
|
textStyle: textColor ? {
|
||||||
|
color: textColor,
|
||||||
|
} : {},
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
data: data.series[0].map((item, index) => ({ value: item, name: data.labels[index] })),
|
||||||
|
label: textColor ? {
|
||||||
|
color: textColor,
|
||||||
|
} : {},
|
||||||
|
type: 'pie',
|
||||||
|
radius: '70%',
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 'bold'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(type === 'ring') {
|
||||||
|
return {
|
||||||
|
color: themeColors,
|
||||||
|
textStyle: textColor ? {
|
||||||
|
color: textColor,
|
||||||
|
} : {},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'shadow',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
top: 'bottom',
|
||||||
|
textStyle: textColor ? {
|
||||||
|
color: textColor,
|
||||||
|
} : {},
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
data: data.series[0].map((item, index) => ({ value: item, name: data.labels[index] })),
|
||||||
|
label: textColor ? {
|
||||||
|
color: textColor,
|
||||||
|
} : {},
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['40%', '70%'],
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
itemStyle: {
|
||||||
|
borderRadius: 10,
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 2
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 'bold'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(type === 'area') {
|
||||||
|
return {
|
||||||
|
color: themeColors,
|
||||||
|
textStyle: textColor ? {
|
||||||
|
color: textColor,
|
||||||
|
} : {},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'shadow',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: data.series.length > 1 ? {
|
||||||
|
top: 'bottom',
|
||||||
|
textStyle: textColor ? {
|
||||||
|
color: textColor,
|
||||||
|
} : {},
|
||||||
|
} : undefined,
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
boundaryGap: false,
|
||||||
|
data: data.labels,
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
},
|
||||||
|
series: data.series.map((item, index) => ({
|
||||||
|
data: item,
|
||||||
|
name: data.legends[index],
|
||||||
|
type: 'line',
|
||||||
|
areaStyle: {},
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(type === 'scatter') {
|
||||||
|
const formatedData = []
|
||||||
|
for(let i = 0; i < data.series[0].length; i++) {
|
||||||
|
const x = data.series[0][i]
|
||||||
|
const y = data.series[1] ? data.series[1][i] : x
|
||||||
|
formatedData.push([x, y])
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
color: themeColors,
|
||||||
|
textStyle: textColor ? {
|
||||||
|
color: textColor,
|
||||||
|
} : {},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'shadow',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
xAxis: {},
|
||||||
|
yAxis: {},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
symbolSize: 12,
|
||||||
|
data: formatedData,
|
||||||
|
type: 'scatter',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
@ -32,11 +32,9 @@
|
|||||||
:height="elementInfo.height"
|
:height="elementInfo.height"
|
||||||
:type="elementInfo.chartType"
|
:type="elementInfo.chartType"
|
||||||
:data="elementInfo.data"
|
:data="elementInfo.data"
|
||||||
:options="elementInfo.options"
|
:themeColors="elementInfo.themeColors"
|
||||||
:themeColor="elementInfo.themeColor"
|
:textColor="elementInfo.textColor"
|
||||||
:gridColor="elementInfo.gridColor"
|
|
||||||
:legends="elementInfo.data.legends"
|
:legends="elementInfo.data.legends"
|
||||||
:legend="elementInfo.legend || ''"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user