feat: 图表支持图例(#45)

This commit is contained in:
pipipi-pikachu 2021-08-29 13:50:35 +08:00
parent 3f364e6801
commit 1265c9536d
8 changed files with 149 additions and 29 deletions

View File

@ -94,6 +94,7 @@ export default () => {
gridColor: fontColor.value, gridColor: fontColor.value,
data: { data: {
labels: ['类别1', '类别2', '类别3', '类别4', '类别5'], labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
legends: ['系列1'],
series: [ series: [
[12, 19, 5, 2, 18], [12, 19, 5, 2, 18],
], ],

View File

@ -400,6 +400,12 @@ export default () => {
} }
if (el.fill) options.fill = formatColor(el.fill).color if (el.fill) options.fill = formatColor(el.fill).color
if (el.legend) {
options.showLegend = true
options.legendPos = el.legend === 'top' ? 't' : 'b'
options.legendColor = formatColor(el.gridColor || '#000000').color
options.legendFontSize = 14 * 0.75
}
let type = pptx.ChartType.bar let type = pptx.ChartType.bar
if (el.chartType === 'bar') { if (el.chartType === 'bar') {

View File

@ -325,6 +325,7 @@ export interface PPTLineElement extends Omit<PPTBaseElement, 'height'> {
export type ChartType = 'bar' | 'line' | 'pie' export type ChartType = 'bar' | 'line' | 'pie'
export interface ChartData { export interface ChartData {
labels: string[]; labels: string[];
legends: string[];
series: number[][]; series: number[][];
} }
@ -346,6 +347,8 @@ export interface ChartData {
* themeColor: 主题色 * themeColor: 主题色
* *
* gridColor?: 网格& * gridColor?: 网格&
*
* legend?: 图例/
*/ */
export interface PPTChartElement extends PPTBaseElement { export interface PPTChartElement extends PPTBaseElement {
type: 'chart'; type: 'chart';
@ -356,6 +359,7 @@ export interface PPTChartElement extends PPTBaseElement {
outline?: PPTElementOutline; outline?: PPTElementOutline;
themeColor: string[]; themeColor: string[];
gridColor?: string; gridColor?: string;
legend?: '' | 'top' | 'bottom',
} }

View File

@ -23,8 +23,12 @@
</div> </div>
<table> <table>
<tbody> <tbody>
<tr v-for="rowIndex in 30" :key="rowIndex"> <tr v-for="rowIndex in 31" :key="rowIndex">
<td v-for="colIndex in 7" :key="colIndex" :class="{ 'head': colIndex === 1 && rowIndex <= selectedRange[1] }"> <td
v-for="colIndex in 7"
:key="colIndex"
:class="{ 'head': (colIndex === 1 && rowIndex <= selectedRange[1]) || (rowIndex === 1 && colIndex <= selectedRange[0]) }"
>
<input <input
:class="['item', { 'selected': rowIndex <= selectedRange[1] && colIndex <= selectedRange[0] }]" :class="['item', { 'selected': rowIndex <= selectedRange[1] && colIndex <= selectedRange[0] }]"
:id="`cell-${rowIndex - 1}-${colIndex - 1}`" :id="`cell-${rowIndex - 1}-${colIndex - 1}`"
@ -38,8 +42,13 @@
</div> </div>
<div class="btns"> <div class="btns">
<Button class="btn" @click="closeEditor()">取消</Button> <div class="left">
<Button type="primary" class="btn" @click="getTableData()">确认</Button> <Button class="btn" @click="clear()">清空</Button>
</div>
<div class="right">
<Button class="btn" @click="closeEditor()">取消</Button>
<Button type="primary" class="btn" @click="getTableData()" style="margin-left: 10px;">确认</Button>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -89,10 +98,11 @@ export default defineComponent({
const initData = () => { const initData = () => {
const _data: string[][] = [] const _data: string[][] = []
const { labels, series } = props.data const { labels, legends, series } = props.data
const rowCount = labels.length const rowCount = labels.length
const colCount = series.length const colCount = series.length
_data.push(['', ...legends])
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) { for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
const row = [labels[rowIndex]] const row = [labels[rowIndex]]
for (let colIndex = 0; colIndex < colCount; colIndex++) { for (let colIndex = 0; colIndex < colCount; colIndex++) {
@ -101,7 +111,7 @@ export default defineComponent({
_data.push(row) _data.push(row)
} }
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) { for (let rowIndex = 0; rowIndex < rowCount + 1; rowIndex++) {
for (let colIndex = 0; colIndex < colCount + 1; colIndex++) { for (let colIndex = 0; colIndex < colCount + 1; colIndex++) {
const inputRef = document.querySelector(`#cell-${rowIndex}-${colIndex}`) as HTMLInputElement const inputRef = document.querySelector(`#cell-${rowIndex}-${colIndex}`) as HTMLInputElement
if (!inputRef) continue if (!inputRef) continue
@ -109,7 +119,7 @@ export default defineComponent({
} }
} }
selectedRange.value = [colCount + 1, rowCount] selectedRange.value = [colCount + 1, rowCount + 1]
} }
onMounted(initData) onMounted(initData)
@ -140,19 +150,26 @@ export default defineComponent({
const [col, row] = selectedRange.value const [col, row] = selectedRange.value
const labels: string[] = [] const labels: string[] = []
const legends: string[] = []
const series: number[][] = [] const series: number[][] = []
// //
for (let rowIndex = 0; rowIndex < row; rowIndex++) { for (let rowIndex = 1; rowIndex < row; rowIndex++) {
let labelsItem = `类别${rowIndex + 1}` let labelsItem = `类别${rowIndex}`
const labelInputRef = document.querySelector(`#cell-${rowIndex}-0`) as HTMLInputElement const labelInputRef = document.querySelector(`#cell-${rowIndex}-0`) as HTMLInputElement
if (labelInputRef && labelInputRef.value) labelsItem = labelInputRef.value if (labelInputRef && labelInputRef.value) labelsItem = labelInputRef.value
labels.push(labelsItem) labels.push(labelsItem)
} }
for (let colIndex = 1; colIndex < col; colIndex++) {
let legendsItem = `系列${colIndex}`
const labelInputRef = document.querySelector(`#cell-0-${colIndex}`) as HTMLInputElement
if (labelInputRef && labelInputRef.value) legendsItem = labelInputRef.value
legends.push(legendsItem)
}
for (let colIndex = 1; colIndex < col; colIndex++) { for (let colIndex = 1; colIndex < col; colIndex++) {
const seriesItem = [] const seriesItem = []
for (let rowIndex = 0; rowIndex < row; rowIndex++) { for (let rowIndex = 1; rowIndex < row; rowIndex++) {
const valueInputRef = document.querySelector(`#cell-${rowIndex}-${colIndex}`) as HTMLInputElement const valueInputRef = document.querySelector(`#cell-${rowIndex}-${colIndex}`) as HTMLInputElement
let value = 0 let value = 0
if (valueInputRef && valueInputRef.value && !!(+valueInputRef.value)) { if (valueInputRef && valueInputRef.value && !!(+valueInputRef.value)) {
@ -162,8 +179,19 @@ export default defineComponent({
} }
series.push(seriesItem) series.push(seriesItem)
} }
const data = { labels, series }
emit('save', data) emit('save', { labels, legends, series })
}
//
const clear = () => {
for (let rowIndex = 1; rowIndex < 31; rowIndex++) {
for (let colIndex = 1; colIndex < 7; colIndex++) {
const inputRef = document.querySelector(`#cell-${rowIndex}-${colIndex}`) as HTMLInputElement
if (!inputRef) continue
inputRef.value = ''
}
}
} }
// //
@ -230,6 +258,7 @@ export default defineComponent({
changeSelectRange, changeSelectRange,
getTableData, getTableData,
closeEditor, closeEditor,
clear,
} }
}, },
}) })
@ -346,10 +375,7 @@ table {
} }
.btns { .btns {
margin-top: 10px; margin-top: 10px;
text-align: right; display: flex;
justify-content: space-between;
.btn {
margin-left: 10px;
}
} }
</style> </style>

View File

@ -41,6 +41,17 @@
<Divider /> <Divider />
<div class="row">
<div style="flex: 2;">图例</div>
<Select style="flex: 3;" :value="legend" @change="value => updateLegend(value)">
<SelectOption value="">不显示</SelectOption>
<SelectOption value="top">显示在上方</SelectOption>
<SelectOption value="bottom">显示在下方</SelectOption>
</Select>
</div>
<Divider />
<div class="row"> <div class="row">
<div style="flex: 2;">背景填充</div> <div style="flex: 2;">背景填充</div>
<Popover trigger="click"> <Popover trigger="click">
@ -145,6 +156,7 @@ export default defineComponent({
const themeColor = ref<string[]>([]) const themeColor = ref<string[]>([])
const gridColor = ref('') const gridColor = ref('')
const legend = ref('')
const lineSmooth = ref(true) const lineSmooth = ref(true)
const showLine = ref(true) const showLine = ref(true)
@ -174,6 +186,7 @@ export default defineComponent({
themeColor.value = handleElement.value.themeColor themeColor.value = handleElement.value.themeColor
gridColor.value = handleElement.value.gridColor || 'rgba(0, 0, 0, 0.4)' gridColor.value = handleElement.value.gridColor || 'rgba(0, 0, 0, 0.4)'
legend.value = handleElement.value.legend || ''
}, { deep: true, immediate: true }) }, { deep: true, immediate: true })
// //
@ -234,6 +247,13 @@ export default defineComponent({
addHistorySnapshot() addHistorySnapshot()
} }
// /
const updateLegend = (legend: '' | 'top' | 'bottom') => {
const props = { legend }
store.commit(MutationTypes.UPDATE_ELEMENT, { id: handleElement.value.id, props })
addHistorySnapshot()
}
const openDataEditor = () => chartDataEditorVisible.value = true const openDataEditor = () => chartDataEditorVisible.value = true
emitter.on(EmitterEvents.OPEN_CHART_DATA_EDITOR, openDataEditor) emitter.on(EmitterEvents.OPEN_CHART_DATA_EDITOR, openDataEditor)
@ -255,10 +275,12 @@ export default defineComponent({
updateOptions, updateOptions,
themeColor, themeColor,
gridColor, gridColor,
legend,
updateTheme, updateTheme,
addThemeColor, addThemeColor,
deleteThemeColor, deleteThemeColor,
updateGridColor, updateGridColor,
updateLegend,
} }
}, },
}) })

View File

@ -1,19 +1,33 @@
<template> <template>
<div class="chart"> <div
class="chart"
:style="{ flexDirection: legend === 'top' ? 'column-reverse' : 'column' }"
>
<div <div
class="chart-content" class="chart-content"
ref="chartRef" ref="chartRef"
:style="{ :style="{
width: width + 'px', width: width + 'px',
height: height + 'px', height: chartHeight + 'px',
transform: `scale(${1 / slideScale})`, transform: `scale(${1 / slideScale})`,
}" }"
></div> ></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>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, inject, onMounted, PropType, ref, Ref, watch } from 'vue' import { computed, defineComponent, inject, onMounted, PropType, ref, Ref, watch } from 'vue'
import { upperFirst } from 'lodash' import { upperFirst } from 'lodash'
import tinycolor from 'tinycolor2' import tinycolor from 'tinycolor2'
import Chartist, { import Chartist, {
@ -54,9 +68,16 @@ export default defineComponent({
type: Array as PropType<string[]>, type: Array as PropType<string[]>,
required: true, required: true,
}, },
legends: {
type: Array as PropType<string[]>,
required: true,
},
gridColor: { gridColor: {
type: String, type: String,
}, },
legend: {
type: String as PropType<'' | 'top' | 'bottom'>,
},
}, },
setup(props) { setup(props) {
const chartRef = ref<HTMLElement>() const chartRef = ref<HTMLElement>()
@ -64,12 +85,17 @@ export default defineComponent({
let chart: IChartistLineChart | IChartistBarChart | IChartistPieChart | undefined let chart: IChartistLineChart | IChartistBarChart | IChartistPieChart | undefined
const chartHeight = computed(() => {
if (props.legend) return props.height - 20
return props.height
})
const getDataAndOptions = () => { const getDataAndOptions = () => {
const propsOptopns = props.options || {} const propsOptopns = props.options || {}
const options = { const options = {
...propsOptopns, ...propsOptopns,
width: props.width * slideScale.value, width: props.width * slideScale.value,
height: props.height * slideScale.value, height: chartHeight.value * slideScale.value,
} }
const data = props.type === 'pie' ? { ...props.data, series: props.data.series[0] } : props.data const data = props.type === 'pie' ? { ...props.data, series: props.data.series[0] } : props.data
return { data, options } return { data, options }
@ -101,11 +127,7 @@ export default defineComponent({
onMounted(renderChart) onMounted(renderChart)
// const themeColors = computed(() => {
// 10
const updateTheme = () => {
if (!chartRef.value) return
let colors: string[] = [] let colors: string[] = []
if (props.themeColor.length === 10) colors = props.themeColor if (props.themeColor.length === 10) colors = props.themeColor
else if (props.themeColor.length === 1) colors = tinycolor(props.themeColor[0]).analogous(10).map(color => color.toHexString()) else if (props.themeColor.length === 1) colors = tinycolor(props.themeColor[0]).analogous(10).map(color => color.toHexString())
@ -114,13 +136,20 @@ export default defineComponent({
const supplement = tinycolor(props.themeColor[len - 1]).analogous(10 + 1 - len).map(color => color.toHexString()) const supplement = tinycolor(props.themeColor[len - 1]).analogous(10 + 1 - len).map(color => color.toHexString())
colors = [...props.themeColor.slice(0, len - 1), ...supplement] colors = [...props.themeColor.slice(0, len - 1), ...supplement]
} }
return colors
})
//
// 10
const updateTheme = () => {
if (!chartRef.value) return
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
chartRef.value.style.setProperty(`--theme-color-${i + 1}`, colors[i]) chartRef.value.style.setProperty(`--theme-color-${i + 1}`, themeColors.value[i])
} }
} }
watch(() => props.themeColor, updateTheme) watch(themeColors, updateTheme)
onMounted(updateTheme) onMounted(updateTheme)
// //
@ -133,6 +162,8 @@ export default defineComponent({
onMounted(updateGridColor) onMounted(updateGridColor)
return { return {
chartHeight,
themeColors,
slideScale, slideScale,
chartRef, chartRef,
} }
@ -141,6 +172,10 @@ export default defineComponent({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.chart {
display: flex;
}
.chart-content { .chart-content {
transform-origin: 0 0; transform-origin: 0 0;
} }
@ -194,4 +229,26 @@ export default defineComponent({
color: 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>

View File

@ -26,6 +26,8 @@
:options="elementInfo.options" :options="elementInfo.options"
:themeColor="elementInfo.themeColor" :themeColor="elementInfo.themeColor"
:gridColor="elementInfo.gridColor" :gridColor="elementInfo.gridColor"
:legends="elementInfo.data.legends"
:legend="elementInfo.legend || ''"
/> />
</div> </div>
</div> </div>

View File

@ -30,6 +30,8 @@
:options="elementInfo.options" :options="elementInfo.options"
:themeColor="elementInfo.themeColor" :themeColor="elementInfo.themeColor"
:gridColor="elementInfo.gridColor" :gridColor="elementInfo.gridColor"
:legends="elementInfo.data.legends"
:legend="elementInfo.legend || ''"
/> />
</div> </div>
</div> </div>