refactor: 使用EChart重构图表元素

This commit is contained in:
zxc 2024-09-13 21:28:09 +08:00
parent 352ac2601d
commit 0a78197a1d
14 changed files with 519 additions and 440 deletions

45
package-lock.json generated
View File

@ -14,6 +14,7 @@
"clipboard": "^2.0.11",
"crypto-js": "^4.2.0",
"dexie": "3.0.3",
"echarts": "^5.5.1",
"file-saver": "^2.0.5",
"hfmath": "^0.0.2",
"html-to-image": "^1.11.11",
@ -2317,6 +2318,15 @@
"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": {
"version": "8.0.0",
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
@ -4933,6 +4943,11 @@
"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": {
"version": "5.1.1",
"resolved": "https://registry.npmmirror.com/txml/-/txml-5.1.1.tgz",
@ -5250,6 +5265,14 @@
"engines": {
"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": {
@ -6850,6 +6873,15 @@
"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": {
"version": "8.0.0",
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
@ -8929,6 +8961,11 @@
"dev": true,
"requires": {}
},
"tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
},
"txml": {
"version": "5.1.1",
"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",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"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"
}
}
}
}

View File

@ -19,6 +19,7 @@
"clipboard": "^2.0.11",
"crypto-js": "^4.2.0",
"dexie": "3.0.3",
"echarts": "^5.5.1",
"file-saver": "^2.0.5",
"hfmath": "^0.0.2",
"html-to-image": "^1.11.11",

54
src/configs/chart.ts Normal file
View 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'],
]

View File

@ -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',
}

View File

@ -2,10 +2,10 @@ import { storeToRefs } from 'pinia'
import { nanoid } from 'nanoid'
import { useMainStore, useSlidesStore } from '@/store'
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 { LinePoolItem } from '@/configs/lines'
import { CHART_TYPES } from '@/configs/chartTypes'
import { CHART_DEFAULT_DATA } from '@/configs/chart'
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
interface CommonElementPosition {
@ -86,40 +86,19 @@ export default () => {
*
* @param chartType
*/
const createChartElement = (type: PresetChartType) => {
const newElement: PPTChartElement = {
const createChartElement = (type: ChartType) => {
createElement({
type: 'chart',
id: nanoid(10),
chartType: CHART_TYPES[type],
chartType: type,
left: 300,
top: 81.25,
width: 400,
height: 400,
rotate: 0,
themeColor: [theme.value.themeColor],
gridColor: theme.value.fontColor,
data: {
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,
themeColors: [theme.value.themeColor],
textColor: theme.value.fontColor,
data: CHART_DEFAULT_DATA[type],
})
}

View File

@ -611,12 +611,12 @@ export default () => {
}
let chartColors: string[] = []
if (el.themeColor.length === 10) chartColors = el.themeColor.map(color => formatColor(color).color)
else if (el.themeColor.length === 1) chartColors = tinycolor(el.themeColor[0]).analogous(10).map(color => formatColor(color.toHexString()).color)
if (el.themeColors.length === 10) chartColors = el.themeColors.map(color => formatColor(color).color)
else if (el.themeColors.length === 1) chartColors = tinycolor(el.themeColors[0]).analogous(10).map(color => formatColor(color.toHexString()).color)
else {
const len = el.themeColor.length
const supplement = tinycolor(el.themeColor[len - 1]).analogous(10 + 1 - len).map(color => color.toHexString())
chartColors = [...el.themeColor.slice(0, len - 1), ...supplement].map(color => formatColor(color).color)
const len = el.themeColors.length
const supplement = tinycolor(el.themeColors[len - 1]).analogous(10 + 1 - len).map(color => color.toHexString())
chartColors = [...el.themeColors.slice(0, len - 1), ...supplement].map(color => formatColor(color).color)
}
const options: pptxgen.IChartOpts = {
@ -624,40 +624,63 @@ export default () => {
y: el.top / ratioPx2Inch.value,
w: el.width / 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 } }
if (el.legend) {
const textColor = formatColor(el.textColor || '#000000').color
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.legendPos = el.legend === 'top' ? 't' : 'b'
options.legendColor = formatColor(el.gridColor || '#000000').color
options.legendFontSize = 14 / ratioPx2Pt.value
options.legendPos = 'b'
options.legendColor = textColor
options.legendFontSize = fontSize
}
let type = pptx.ChartType.bar
if (el.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') {
if (el.options?.showArea) type = pptx.ChartType.area
else if (el.options?.showLine === false) {
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
}
else type = pptx.ChartType.line
if (el.options?.lineSmooth) options.lineSmooth = true
type = pptx.ChartType.line
}
else if (el.chartType === 'area') {
type = pptx.ChartType.area
}
else if (el.chartType === 'scatter') {
type = pptx.ChartType.scatter
options.lineSize = 0
}
else if (el.chartType === 'pie') {
if (el.options?.donut) {
type = pptx.ChartType.doughnut
options.holeSize = 75
}
else type = pptx.ChartType.pie
type = pptx.ChartType.pie
}
else if (el.chartType === 'ring') {
type = pptx.ChartType.doughnut
options.holeSize = 60
}
pptxSlide.addChart(type, chartData, options)

View File

@ -14,7 +14,6 @@ import type {
TableCellStyle,
TableCell,
ChartType,
ChartOptions,
SlideBackground,
PPTShapeElement,
PPTLineElement,
@ -389,10 +388,9 @@ export default () => {
let series: number[][]
if (el.chartType === 'scatterChart' || el.chartType === 'bubbleChart') {
const data = el.data
labels = data[0].map(item => item + '')
legends = ['系列1']
series = [data[1]]
labels = el.data[0].map((item, index) => `坐标${index + 1}`)
legends = ['X', 'Y']
series = el.data
}
else {
const data = el.data as ChartItem[]
@ -401,32 +399,32 @@ export default () => {
series = data.map(item => item.values.map(v => v.y))
}
const options: ChartOptions = {}
let chartType: ChartType = 'bar'
switch (el.chartType) {
case 'barChart':
case 'bar3DChart':
chartType = 'bar'
if (el.barDir === 'bar') options.horizontalBars = true
if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stackBars = true
if (el.barDir === 'bar') chartType = 'column'
break
case 'lineChart':
case 'line3DChart':
chartType = 'line'
break
case 'areaChart':
case 'area3DChart':
chartType = 'area'
break
case 'scatterChart':
case 'bubbleChart':
chartType = 'line'
if (el.chartType === 'areaChart' || el.chartType === 'area3DChart') options.showArea = true
if (el.chartType === 'scatterChart' || el.chartType === 'bubbleChart') options.showLine = false
chartType = 'scatter'
break
case 'pieChart':
case 'pie3DChart':
case 'doughnutChart':
chartType = 'pie'
if (el.chartType === 'doughnutChart') options.donut = true
break
case 'doughnutChart':
chartType = 'ring'
break
default:
}
@ -440,14 +438,13 @@ export default () => {
left: el.left,
top: el.top,
rotate: 0,
themeColor: [theme.value.themeColor],
gridColor: theme.value.fontColor,
themeColors: [theme.value.themeColor],
textColor: theme.value.fontColor,
data: {
labels,
legends,
series,
},
options,
})
}
else if (el.type === 'group' || el.type === 'diagram') {

View File

@ -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' | 'line' | 'pie'
export type ChartOptions = LineChartOptions & BarChartOptions & PieChartOptions
export type ChartType = 'bar' | 'column' | 'line' | 'pie' | 'ring' | 'area' | 'scatter'
export interface ChartData {
labels: string[]
legends: string[]
@ -415,26 +413,20 @@ export interface ChartData {
*
* data: 图表数据
*
* options?: 图表配置项
*
* outline?: 边框
*
* themeColor: 主题色
* themeColors: 主题色
*
* gridColor?: 网格&
*
* legend?: 图例/
* textColor?: 文字颜色
*/
export interface PPTChartElement extends PPTBaseElement {
type: 'chart'
fill?: string
chartType: ChartType
data: ChartData
options?: ChartOptions
outline?: PPTElementOutline
themeColor: string[]
gridColor?: string
legend?: '' | 'top' | 'bottom'
themeColors: string[]
textColor?: string
}

View File

@ -5,7 +5,7 @@
<IconChartLine size="24" v-if="chart === 'line'" />
<IconChartHistogram size="24" v-else-if="chart === 'bar'" />
<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'" />
<IconChartRing size="24" v-else-if="chart === 'ring'" />
<IconChartScatter size="24" v-else-if="chart === 'scatter'" />
@ -15,15 +15,15 @@
</template>
<script lang="ts" setup>
import type { PresetChartType } from '@/types/slides'
import type { ChartType } from '@/types/slides'
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)
}
</script>

View File

@ -6,63 +6,6 @@
<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 style="width: 40%;">背景填充</div>
<Popover trigger="click" style="width: 60%;">
@ -76,21 +19,21 @@
</Popover>
</div>
<div class="row">
<div style="width: 40%;">网格颜色</div>
<div style="width: 40%;">文字颜色</div>
<Popover trigger="click" style="width: 60%;">
<template #content>
<ColorPicker
:modelValue="gridColor"
@update:modelValue="value => updateGridColor(value)"
:modelValue="textColor"
@update:modelValue="value => updateTextColor(value)"
/>
</template>
<ColorButton :color="gridColor" />
<ColorButton :color="textColor" />
</Popover>
</div>
<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>
<Popover trigger="click" style="width: 60%;">
<template #content>
@ -109,7 +52,7 @@
<Popover trigger="click" v-model:open="presetThemesVisible" style="width: 40%;">
<template #content>
<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
class="preset-theme-color"
:class="{ 'select': presetThemeColorHoverIndex[0] === index && itemIndex <= presetThemeColorHoverIndex[1] }"
@ -127,7 +70,7 @@
</Popover>
<Button
last
:disabled="themeColor.length >= 10"
:disabled="themeColors.length >= 10"
style="width: 60%;"
@click="addThemeColor()"
>
@ -156,9 +99,10 @@
import { onUnmounted, ref, watch, type Ref } from 'vue'
import { storeToRefs } from 'pinia'
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 useHistorySnapshot from '@/hooks/useHistorySnapshot'
import { CHART_PRESET_THEMES } from '@/configs/chart'
import ElementOutline from '../../common/ElementOutline.vue'
import ChartDataEditor from './ChartDataEditor.vue'
@ -166,27 +110,10 @@ import ColorButton from '@/components/ColorButton.vue'
import ColorPicker from '@/components/ColorPicker/index.vue'
import Modal from '@/components/Modal.vue'
import Divider from '@/components/Divider.vue'
import Checkbox from '@/components/Checkbox.vue'
import Button from '@/components/Button.vue'
import ButtonGroup from '@/components/ButtonGroup.vue'
import Select from '@/components/Select.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 slidesStore = useSlidesStore()
const { handleElement, handleElementId } = storeToRefs(mainStore)
@ -202,42 +129,15 @@ const { addHistorySnapshot } = useHistorySnapshot()
const fill = ref<string>('#000')
const themeColor = ref<string[]>([])
const gridColor = 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)
const themeColors = ref<string[]>([])
const textColor = ref('')
watch(handleElement, () => {
if (!handleElement.value || handleElement.value.type !== 'chart') return
fill.value = handleElement.value.fill || '#fff'
if (handleElement.value.options) {
const {
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 || ''
themeColors.value = handleElement.value.themeColors
textColor.value = handleElement.value.textColor || '#333'
}, { deep: true, immediate: true })
const updateElement = (props: Partial<PPTChartElement>) => {
@ -256,18 +156,10 @@ const updateFill = (value: string) => {
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 props = {
themeColor: themeColor.value.map((c, i) => i === index ? color : c),
themeColors: themeColors.value.map((c, i) => i === index ? color : c),
}
updateElement(props)
}
@ -275,34 +167,29 @@ const updateTheme = (color: string, index: number) => {
//
const addThemeColor = () => {
const props = {
themeColor: [...themeColor.value, theme.value.themeColor],
themeColors: [...themeColors.value, theme.value.themeColor],
}
updateElement(props)
}
// 使
const applyPresetTheme = (colors: string[], index: number) => {
const themeColor = colors.slice(0, index + 1)
updateElement({ themeColor })
const themeColors = colors.slice(0, index + 1)
updateElement({ themeColors })
presetThemesVisible.value = false
}
//
const deleteThemeColor = (index: number) => {
const props = {
themeColor: themeColor.value.filter((c, i) => i !== index),
themeColors: themeColors.value.filter((c, i) => i !== index),
}
updateElement(props)
}
//
const updateGridColor = (gridColor: string) => {
updateElement({ gridColor })
}
// /
const updateLegend = (legend: '' | 'top' | 'bottom') => {
updateElement({ legend })
//
const updateTextColor = (textColor: string) => {
updateElement({ textColor })
}
const openDataEditor = () => chartDataEditorVisible.value = true

View File

@ -23,16 +23,13 @@
:outline="elementInfo.outline"
/>
<Chart
:width="elementInfo.width * zoom"
:height="elementInfo.height * zoom"
:width="elementInfo.width"
:height="elementInfo.height"
:type="elementInfo.chartType"
:data="elementInfo.data"
:options="elementInfo.options"
:themeColor="elementInfo.themeColor"
:gridColor="elementInfo.gridColor"
:themeColors="elementInfo.themeColors"
:textColor="elementInfo.textColor"
:legends="elementInfo.data.legends"
:legend="elementInfo.legend || ''"
:style="{ zoom: 1 / zoom }"
/>
</div>
</div>
@ -40,9 +37,7 @@
</template>
<script lang="ts" setup>
import { computed, inject, ref } from 'vue'
import type { PPTChartElement } from '@/types/slides'
import { injectKeySlideScale } from '@/types/injectKey'
import ElementOutline from '@/views/components/element/ElementOutline.vue'
import Chart from './Chart.vue'
@ -50,11 +45,6 @@ import Chart from './Chart.vue'
defineProps<{
elementInfo: PPTChartElement
}>()
const slideScale = inject(injectKeySlideScale) || ref(1)
const needScaleSize = computed(() => slideScale.value < 1)
const zoom = computed(() => needScaleSize.value ? 1 / slideScale.value : 1)
</script>
<style lang="scss" scoped>

View File

@ -1,37 +1,13 @@
<template>
<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>
<div class="chart" ref="chartRef"></div>
</template>
<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 { BarChart, LineChart, PieChart } from 'chartist'
import type { ChartData, ChartOptions, ChartType } from '@/types/slides'
import { injectKeySlideScale } from '@/types/injectKey'
import type { ChartData, ChartType } from '@/types/slides'
import { getChartOption } from './chartOption'
import 'chartist/dist/index.css'
@ -40,178 +16,54 @@ const props = defineProps<{
height: number
type: ChartType
data: ChartData
themeColor: string[]
themeColors: string[]
legends: string[]
options?: ChartOptions
gridColor?: string
legend?: '' | 'top' | 'bottom'
textColor?: string
}>()
let chart: echarts.ECharts | null = null
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(() => {
let colors: string[] = []
if (props.themeColor.length >= 10) colors = props.themeColor
else if (props.themeColor.length === 1) colors = tinycolor(props.themeColor[0]).analogous(10).map(color => color.toRgbString())
if (props.themeColors.length >= 10) colors = props.themeColors
else if (props.themeColors.length === 1) colors = tinycolor(props.themeColors[0]).analogous(10).map(color => color.toRgbString())
else {
const len = props.themeColor.length
const supplement = tinycolor(props.themeColor[len - 1]).analogous(10 + 1 - len).map(color => color.toRgbString())
colors = [...props.themeColor.slice(0, len - 1), ...supplement]
const len = props.themeColors.length
const supplement = tinycolor(props.themeColors[len - 1]).analogous(10 + 1 - len).map(color => color.toRgbString())
colors = [...props.themeColors.slice(0, len - 1), ...supplement]
}
return colors
})
//
// 10
const updateTheme = () => {
if (!chartRef.value) return
for (let i = 0; i < 10; i++) {
chartRef.value.style.setProperty(`--theme-color-${i + 1}`, themeColors.value[i])
}
const updateOption = () => {
const option = getChartOption({
type: props.type,
data: props.data,
themeColors: themeColors.value,
textColor: props.textColor,
})
if (option) chart!.setOption(option, true)
}
watch(themeColors, updateTheme)
onMounted(updateTheme)
onMounted(() => {
chart = echarts.init(chartRef.value, null, { renderer: 'svg' })
updateOption()
//
const updateGridColor = () => {
if (!chartRef.value) return
if (props.gridColor) chartRef.value.style.setProperty(`--grid-color`, props.gridColor)
}
const resizeListener = () => chart!.resize()
const resizeObserver = new ResizeObserver(resizeListener)
resizeObserver.observe(chartRef.value!)
})
watch(() => props.gridColor, updateGridColor)
onMounted(updateGridColor)
watch(() => props.type, updateOption)
watch(() => props.data, updateOption)
watch(() => props.themeColors, updateOption)
watch(() => props.textColor, updateOption)
</script>
<style lang="scss" scoped>
.chart {
display: flex;
}
.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;
}
width: 100%;
height: 100%;
}
</style>

View 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
}

View File

@ -32,11 +32,9 @@
:height="elementInfo.height"
:type="elementInfo.chartType"
:data="elementInfo.data"
:options="elementInfo.options"
:themeColor="elementInfo.themeColor"
:gridColor="elementInfo.gridColor"
:themeColors="elementInfo.themeColors"
:textColor="elementInfo.textColor"
:legends="elementInfo.data.legends"
:legend="elementInfo.legend || ''"
/>
</div>
</div>