mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
feat: 图表支持图例(#45)
This commit is contained in:
parent
3f364e6801
commit
1265c9536d
@ -94,6 +94,7 @@ export default () => {
|
||||
gridColor: fontColor.value,
|
||||
data: {
|
||||
labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
|
||||
legends: ['系列1'],
|
||||
series: [
|
||||
[12, 19, 5, 2, 18],
|
||||
],
|
||||
|
@ -400,6 +400,12 @@ export default () => {
|
||||
}
|
||||
|
||||
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
|
||||
if (el.chartType === 'bar') {
|
||||
|
@ -325,6 +325,7 @@ export interface PPTLineElement extends Omit<PPTBaseElement, 'height'> {
|
||||
export type ChartType = 'bar' | 'line' | 'pie'
|
||||
export interface ChartData {
|
||||
labels: string[];
|
||||
legends: string[];
|
||||
series: number[][];
|
||||
}
|
||||
|
||||
@ -346,6 +347,8 @@ export interface ChartData {
|
||||
* themeColor: 主题色
|
||||
*
|
||||
* gridColor?: 网格&坐标颜色
|
||||
*
|
||||
* legend?: 图例/位置
|
||||
*/
|
||||
export interface PPTChartElement extends PPTBaseElement {
|
||||
type: 'chart';
|
||||
@ -356,6 +359,7 @@ export interface PPTChartElement extends PPTBaseElement {
|
||||
outline?: PPTElementOutline;
|
||||
themeColor: string[];
|
||||
gridColor?: string;
|
||||
legend?: '' | 'top' | 'bottom',
|
||||
}
|
||||
|
||||
|
||||
|
@ -23,8 +23,12 @@
|
||||
</div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr v-for="rowIndex in 30" :key="rowIndex">
|
||||
<td v-for="colIndex in 7" :key="colIndex" :class="{ 'head': colIndex === 1 && rowIndex <= selectedRange[1] }">
|
||||
<tr v-for="rowIndex in 31" :key="rowIndex">
|
||||
<td
|
||||
v-for="colIndex in 7"
|
||||
:key="colIndex"
|
||||
:class="{ 'head': (colIndex === 1 && rowIndex <= selectedRange[1]) || (rowIndex === 1 && colIndex <= selectedRange[0]) }"
|
||||
>
|
||||
<input
|
||||
:class="['item', { 'selected': rowIndex <= selectedRange[1] && colIndex <= selectedRange[0] }]"
|
||||
:id="`cell-${rowIndex - 1}-${colIndex - 1}`"
|
||||
@ -38,8 +42,13 @@
|
||||
</div>
|
||||
|
||||
<div class="btns">
|
||||
<div class="left">
|
||||
<Button class="btn" @click="clear()">清空</Button>
|
||||
</div>
|
||||
<div class="right">
|
||||
<Button class="btn" @click="closeEditor()">取消</Button>
|
||||
<Button type="primary" class="btn" @click="getTableData()">确认</Button>
|
||||
<Button type="primary" class="btn" @click="getTableData()" style="margin-left: 10px;">确认</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -89,10 +98,11 @@ export default defineComponent({
|
||||
const initData = () => {
|
||||
const _data: string[][] = []
|
||||
|
||||
const { labels, series } = props.data
|
||||
const { labels, legends, series } = props.data
|
||||
const rowCount = labels.length
|
||||
const colCount = series.length
|
||||
|
||||
_data.push(['', ...legends])
|
||||
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
||||
const row = [labels[rowIndex]]
|
||||
for (let colIndex = 0; colIndex < colCount; colIndex++) {
|
||||
@ -101,7 +111,7 @@ export default defineComponent({
|
||||
_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++) {
|
||||
const inputRef = document.querySelector(`#cell-${rowIndex}-${colIndex}`) as HTMLInputElement
|
||||
if (!inputRef) continue
|
||||
@ -109,7 +119,7 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
selectedRange.value = [colCount + 1, rowCount]
|
||||
selectedRange.value = [colCount + 1, rowCount + 1]
|
||||
}
|
||||
|
||||
onMounted(initData)
|
||||
@ -140,19 +150,26 @@ export default defineComponent({
|
||||
const [col, row] = selectedRange.value
|
||||
|
||||
const labels: string[] = []
|
||||
const legends: string[] = []
|
||||
const series: number[][] = []
|
||||
|
||||
// 第一列为系列名,实际数据从第二列开始
|
||||
for (let rowIndex = 0; rowIndex < row; rowIndex++) {
|
||||
let labelsItem = `类别${rowIndex + 1}`
|
||||
// 第一行为系列名,第一列为项目名,实际数据从第二行第二列开始
|
||||
for (let rowIndex = 1; rowIndex < row; rowIndex++) {
|
||||
let labelsItem = `类别${rowIndex}`
|
||||
const labelInputRef = document.querySelector(`#cell-${rowIndex}-0`) as HTMLInputElement
|
||||
if (labelInputRef && labelInputRef.value) labelsItem = labelInputRef.value
|
||||
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++) {
|
||||
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
|
||||
let value = 0
|
||||
if (valueInputRef && valueInputRef.value && !!(+valueInputRef.value)) {
|
||||
@ -162,8 +179,19 @@ export default defineComponent({
|
||||
}
|
||||
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,
|
||||
getTableData,
|
||||
closeEditor,
|
||||
clear,
|
||||
}
|
||||
},
|
||||
})
|
||||
@ -346,10 +375,7 @@ table {
|
||||
}
|
||||
.btns {
|
||||
margin-top: 10px;
|
||||
text-align: right;
|
||||
|
||||
.btn {
|
||||
margin-left: 10px;
|
||||
}
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
@ -41,6 +41,17 @@
|
||||
|
||||
<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 style="flex: 2;">背景填充:</div>
|
||||
<Popover trigger="click">
|
||||
@ -145,6 +156,7 @@ export default defineComponent({
|
||||
|
||||
const themeColor = ref<string[]>([])
|
||||
const gridColor = ref('')
|
||||
const legend = ref('')
|
||||
|
||||
const lineSmooth = ref(true)
|
||||
const showLine = ref(true)
|
||||
@ -174,6 +186,7 @@ export default defineComponent({
|
||||
|
||||
themeColor.value = handleElement.value.themeColor
|
||||
gridColor.value = handleElement.value.gridColor || 'rgba(0, 0, 0, 0.4)'
|
||||
legend.value = handleElement.value.legend || ''
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
// 设置图表数据
|
||||
@ -234,6 +247,13 @@ export default defineComponent({
|
||||
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
|
||||
|
||||
emitter.on(EmitterEvents.OPEN_CHART_DATA_EDITOR, openDataEditor)
|
||||
@ -255,10 +275,12 @@ export default defineComponent({
|
||||
updateOptions,
|
||||
themeColor,
|
||||
gridColor,
|
||||
legend,
|
||||
updateTheme,
|
||||
addThemeColor,
|
||||
deleteThemeColor,
|
||||
updateGridColor,
|
||||
updateLegend,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
@ -1,19 +1,33 @@
|
||||
<template>
|
||||
<div class="chart">
|
||||
<div
|
||||
class="chart"
|
||||
:style="{ flexDirection: legend === 'top' ? 'column-reverse' : 'column' }"
|
||||
>
|
||||
<div
|
||||
class="chart-content"
|
||||
ref="chartRef"
|
||||
:style="{
|
||||
width: width + 'px',
|
||||
height: height + '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>
|
||||
|
||||
<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 tinycolor from 'tinycolor2'
|
||||
import Chartist, {
|
||||
@ -54,9 +68,16 @@ export default defineComponent({
|
||||
type: Array as PropType<string[]>,
|
||||
required: true,
|
||||
},
|
||||
legends: {
|
||||
type: Array as PropType<string[]>,
|
||||
required: true,
|
||||
},
|
||||
gridColor: {
|
||||
type: String,
|
||||
},
|
||||
legend: {
|
||||
type: String as PropType<'' | 'top' | 'bottom'>,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const chartRef = ref<HTMLElement>()
|
||||
@ -64,12 +85,17 @@ export default defineComponent({
|
||||
|
||||
let chart: IChartistLineChart | IChartistBarChart | IChartistPieChart | undefined
|
||||
|
||||
const chartHeight = computed(() => {
|
||||
if (props.legend) return props.height - 20
|
||||
return props.height
|
||||
})
|
||||
|
||||
const getDataAndOptions = () => {
|
||||
const propsOptopns = props.options || {}
|
||||
const options = {
|
||||
...propsOptopns,
|
||||
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
|
||||
return { data, options }
|
||||
@ -101,11 +127,7 @@ export default defineComponent({
|
||||
|
||||
onMounted(renderChart)
|
||||
|
||||
// 更新主题配色:
|
||||
// 如果当前所设置的主题色数小于10,剩余部分获取最后一个主题色的相近颜色作为配色
|
||||
const updateTheme = () => {
|
||||
if (!chartRef.value) return
|
||||
|
||||
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.toHexString())
|
||||
@ -114,13 +136,20 @@ export default defineComponent({
|
||||
const supplement = tinycolor(props.themeColor[len - 1]).analogous(10 + 1 - len).map(color => color.toHexString())
|
||||
colors = [...props.themeColor.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}`, colors[i])
|
||||
chartRef.value.style.setProperty(`--theme-color-${i + 1}`, themeColors.value[i])
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.themeColor, updateTheme)
|
||||
watch(themeColors, updateTheme)
|
||||
onMounted(updateTheme)
|
||||
|
||||
// 更新网格颜色,包括坐标的文字部分
|
||||
@ -133,6 +162,8 @@ export default defineComponent({
|
||||
onMounted(updateGridColor)
|
||||
|
||||
return {
|
||||
chartHeight,
|
||||
themeColors,
|
||||
slideScale,
|
||||
chartRef,
|
||||
}
|
||||
@ -141,6 +172,10 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chart {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
@ -194,4 +229,26 @@ export default defineComponent({
|
||||
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>
|
@ -26,6 +26,8 @@
|
||||
:options="elementInfo.options"
|
||||
:themeColor="elementInfo.themeColor"
|
||||
:gridColor="elementInfo.gridColor"
|
||||
:legends="elementInfo.data.legends"
|
||||
:legend="elementInfo.legend || ''"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -30,6 +30,8 @@
|
||||
:options="elementInfo.options"
|
||||
:themeColor="elementInfo.themeColor"
|
||||
:gridColor="elementInfo.gridColor"
|
||||
:legends="elementInfo.data.legends"
|
||||
:legend="elementInfo.legend || ''"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user