feat: generate bar race chart from table
diff --git a/index.html b/index.html
index de4a318..725bacc 100644
--- a/index.html
+++ b/index.html
@@ -5,6 +5,7 @@
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ECharts WWW SPA Boilerplate</title>
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/handsontable@6.2.2/dist/handsontable.css">
<style>
body {
padding: 0;
diff --git a/package.json b/package.json
index 9d4a02a..bb3f5b7 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,7 @@
"element-plus": "^1.0.2-beta.44",
"handsontable": "^6.2.2",
"jquery": "^3.6.0",
- "lodash": "^4.17.19",
+ "lodash": "^4.17.21",
"vue": "^3.0.11",
"vue-i18n": "^9.1.6"
}
diff --git a/src/App.vue b/src/App.vue
index e593a7c..77b2d87 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,17 +1,14 @@
<template>
<el-container>
- <!-- <el-aside>
- <h3>Side Nav</h3>
- </el-aside> -->
<el-main>
- <BTable/>
+ <BBody/>
</el-main>
</el-container>
</template>
<script lang='ts' setup>
-import BTable from './components/BTable.vue';
+import BBody from './components/BBody.vue';
</script>
diff --git a/src/components/BBody.vue b/src/components/BBody.vue
new file mode 100644
index 0000000..498a897
--- /dev/null
+++ b/src/components/BBody.vue
@@ -0,0 +1,103 @@
+<template>
+ <div class="w-full h-full">
+ <div class="grid grid-cols-12 h-full text-sm">
+ <el-card class="box-card col-span-3">
+ <h1 slot="header" class="clearfix text-xl">
+ 动态排序柱状图生成器
+ </h1>
+ <div id="el-config" class="align-middle">
+ <!-- <div class="my-3">
+ <el-button onclick="run()" size="medium">运行</el-button>
+ <el-button size="medium">导出</el-button>
+ </div> -->
+ <el-form ref="form">
+ <div class="grid grid-cols-3 form-row">
+ <label class="col-span-1">标题</label>
+ <el-input
+ id="input-title"
+ value="汽车产量动态排名"
+ size="medium"
+ class="col-span-2"
+ v-model="title"
+ >
+ </el-input>
+ </div>
+ <div class="grid grid-cols-3 form-row">
+ <label class="col-span-1">显示排名上限</label>
+ <el-input
+ id="input-max"
+ type="number"
+ value="10"
+ size="medium"
+ class="col-span-2"
+ v-model="maxDataCnt"
+ >
+ </el-input>
+ </div>
+ </el-form>
+ </div>
+ </el-card>
+ <el-card
+ class="box-card col-span-4 relative"
+ body-style="height: 100%"
+ >
+ <BTable
+ @after-change="tableAfterChange"
+ />
+ </el-card>
+ <el-card
+ class="box-card col-span-5 relative"
+ body-style="height: 100%"
+ >
+ <BChart
+ :title="title"
+ :chartData="chartData"
+ />
+ </el-card>
+ </div>
+ </div>
+</template>
+
+<script lang="ts">
+import {defineComponent} from 'vue';
+import BTable, {ChartData} from './BTable.vue';
+import BChart from './BChart.vue';
+
+export default defineComponent({
+ name: 'BBody',
+ data() {
+ return {
+ title: '汽车销量',
+ maxDataCnt: 10,
+ chartData: null
+ }
+ },
+ components: {
+ BTable,
+ BChart
+ },
+ mounted: () => {
+ },
+ methods: {
+ tableAfterChange(data: ChartData) {
+ this.chartData = data;
+ }
+ }
+})
+</script>
+
+<style scoped>
+@layer utilities {
+ .form-row {
+ @apply my-3;
+
+ label {
+ @apply py-1;
+ }
+ }
+
+ .box-card {
+ @apply m-1;
+ }
+}
+</style>
diff --git a/src/components/BChart.vue b/src/components/BChart.vue
new file mode 100644
index 0000000..97dc9db
--- /dev/null
+++ b/src/components/BChart.vue
@@ -0,0 +1,130 @@
+<template>
+ <div>
+ <div slot="header" class="clearfix text-base">
+ 预览{{title}}
+ <a href="#">
+ <i class="el-icon-refresh"></i>
+ </a>
+ </div>
+ <div
+ id="bar-race-preview"
+ ref="chart"
+ class="absolute bottom-4 top-14 left-5 right-5 border">
+ </div>
+ </div>
+</template>
+
+<script lang="ts">
+import {defineComponent} from 'vue';
+import * as echarts from 'echarts';
+
+const headerLength = 2;
+let chart: echarts.ECharts;
+
+export default defineComponent({
+ name: 'BChart',
+ props: {
+ title: String,
+ chartData: Array
+ },
+ data() {
+ return {
+ timeoutHandlers: []
+ };
+ },
+ watch: {
+ chartData() {
+ this.run();
+ }
+ },
+ mounted() {
+ chart = echarts.init(this.$refs.chart as HTMLElement);
+ },
+ methods: {
+ run() {
+ if (!chart) {
+ return;
+ }
+ const option = {
+ dataset: {
+ source: this.chartData
+ },
+ xAxis: {
+ type: 'value'
+ },
+ yAxis: {
+ type: 'category',
+ inverse: true,
+ animationDuration: 300,
+ animationDurationUpdate: 300
+ },
+ series: [{
+ id: 'bar',
+ type: 'bar',
+ encode: {
+ x: 2
+ },
+ seriesLayoutBy: 'row',
+ realtimeSort: true,
+ label: {
+ show: true,
+ position: 'right'
+ }
+ }],
+ grid: {
+ right: 60
+ },
+ animationDurationUpdate: 5000,
+ animationEasing: 'linear',
+ animationEasingUpdate: 'linear'
+ };
+ chart.setOption(option as echarts.EChartsOption, true);
+
+ const dataCnt = this.chartData.length - headerLength - 1;
+ const that = this;
+ for (let i = 0; i < dataCnt; ++i) {
+ (function (i: number) {
+ let timeout: number;
+ const timeoutCb = function () {
+ chart.setOption({
+ // title: [{
+ // text: getDataName(i)
+ // }],
+ series: [{
+ type: 'bar',
+ id: 'bar',
+ encode: {
+ x: i + headerLength + 1
+ }
+ }]
+ });
+ that.removeTimeoutHandlers(timeout);
+ };
+ timeout = window.setTimeout(timeoutCb, i * 5000);
+ that.timeoutHandlers.push(timeout);
+ })(i);
+ }
+ },
+
+ clearTimeoutHandlers() {
+ for (let i = 0; i < this.timeoutHandlers.length; ++i) {
+ clearTimeout(this.timeoutHandlers[i]);
+ this.timeoutHandlers.splice(i, 1);
+ }
+ },
+
+ removeTimeoutHandlers(handler: number) {
+ for (let i = 0; i < this.timeoutHandlers.length; ++i) {
+ if (this.timeoutHandlers[i] === handler) {
+ this.timeoutHandlers.splice(i, 1);
+ }
+ }
+ }
+ }
+})
+</script>
+
+<style scoped>
+@layer utilities {
+}
+</style>
diff --git a/src/components/BTable.vue b/src/components/BTable.vue
index 9f6fc36..34dc0a3 100644
--- a/src/components/BTable.vue
+++ b/src/components/BTable.vue
@@ -1,74 +1,176 @@
<template>
- <div class="w-full h-full">
- <div class="grid grid-cols-12 h-full text-sm">
- <el-card class="box-card col-span-3">
- <h1 slot="header" class="clearfix text-xl">
- Apache ECharts Bar-Race 生成器
- </h1>
- <div id="el-config" class="align-middle">
- <!-- <div class="my-3">
- <el-button onclick="run()" size="medium">运行</el-button>
- <el-button size="medium">导出</el-button>
- </div> -->
- <el-form ref="form">
- <div class="grid grid-cols-3 form-row">
- <label class="col-span-1">标题</label>
- <el-input id="input-title" value="汽车产量动态排名" size="medium" class="col-span-2"></el-input>
- </div>
- <div class="grid grid-cols-3 form-row">
- <label class="col-span-1">显示排名上限</label>
- <el-input id="input-max" type="number" value="10" size="medium" class="col-span-2"></el-input>
- </div>
- </el-form>
- </div>
- </el-card>
- <el-card class="box-card col-span-4 relative" body-style="height: 100%">
- <div slot="header" class="clearfix text-base">
- 数据
- </div>
- <div id="table-panel" class="overflow-auto absolute bottom-4 top-14 left-5 right-5 border">
- </div>
- </el-card>
- <el-card class="box-card col-span-5 relative" body-style="height: 100%">
- <div slot="header" class="clearfix text-base">
- 预览
- <a href="#">
- <i class="el-icon-refresh"></i>
- </a>
- </div>
- <div id="bar-race-preview" class="absolute bottom-4 top-14 left-5 right-5 border">
- </div>
- </el-card>
+ <div>
+ <div slot='header' class='clearfix text-base'>
+ 数据
+ </div>
+ <div ref='table' id='table-panel' class='overflow-auto absolute bottom-4 top-14 left-5 right-5 border'>
</div>
</div>
</template>
-<script lang="ts">
+<script lang='ts'>
+import Handsontable from 'handsontable';
import {defineComponent} from 'vue';
-import btable from './btable';
+import * as _ from 'lodash';
+
+const headerLength = 2;
+export type ChartData = string[][];
export default defineComponent({
name: 'BTable',
props: {
},
- mounted: () => {
- btable.initTable();
+ data() {
+ return {
+ tableData: [
+ ['Name', 'Ford', 'Tesla', 'Toyota', 'Honda'],
+ ['Color', '', '', '', ''],
+ ['2017', '10', '11', '12', '13'],
+ ['2018', '20', '11', '14', '13'],
+ ['2019', '30', '15', '12', '13']
+ ],
+ table: null,
+ debouncedTableChange: null
+ }
+ },
+ mounted() {
+ this.insertEmptyCells();
+ this.table = new Handsontable(this.$refs.table as Element, {
+ data: this.tableData,
+ rowHeaders: true,
+ colHeaders: true,
+ filters: true,
+ dropdownMenu: true,
+ // cell: [{
+ // row: 0,
+ // col: 0,
+ // readOnly: true
+ // }, {
+ // row: 1,
+ // col: 0,
+ // readOnly: true
+ // }],
+ //- cells: function (row, col) {
+ //- if (row === 1) {
+ //- return {
+ //- renderer: colorRenderer
+ //- }
+ //- }
+ //- else {
+ //- return {};
+ //- }
+ //- }
+ });
+ this.table.updateSettings({
+ afterChange: () => {
+ console.log('after')
+ this.debouncedTableChange();
+ }
+ });
+
+ this.debouncedTableChange = _.debounce(() => {
+ this.$emit('afterChange', this.getChartData());
+ }, 500);
+
+ this.$emit('afterChange', this.getChartData());
+ },
+ unmounted() {
+ this.debouncedTableChange.cancel();
+ },
+ methods: {
+ getChartData(): ChartData {
+ let columns = 0;
+ const firstRow = this.tableData[0];
+ for (let i = 0; i < firstRow.length; ++i) {
+ if (!firstRow[i] || !firstRow[i].trim()) {
+ columns = i;
+ break;
+ }
+ }
+
+ let rows = 0;
+ for (let i = 0; i < this.tableData.length; ++i) {
+ if (!this.tableData[i] || !this.tableData[i][0] || !this.tableData[i][0]) {
+ rows = i;
+ break;
+ }
+ }
+
+ return this.tableData.slice(0, rows)
+ .map(row => row.slice(0, columns));
+ },
+
+ insertEmptyCells() {
+ for (let i = 0; i < this.tableData.length; ++i) {
+ for (let j = this.tableData[i].length; j < 50; ++j) {
+ this.tableData[i].push('');
+ }
+ }
+ for (let i = this.tableData.length; i < 100; ++i) {
+ const row = [];
+ for (let j = 0; j < 50; ++j) {
+ row.push('');
+ }
+ this.tableData.push(row);
+ }
+ },
+
+ trimColumns(rowData: (string | number)[]) {
+ for (let i = rowData.length - 1; i > 0; --i) {
+ if (rowData[i] && rowData[i] !== '') {
+ return rowData.slice(1, i + 1);
+ }
+ }
+ return [];
+ },
+
+ trimRows() {
+ const data = this.tableData;
+ if (data.length <= headerLength) {
+ return [];
+ }
+ for (let i = data.length - 1; i >= headerLength; --i) {
+ let isEmpty = true;
+ for (let j = 1; j < data[i].length; ++j) {
+ if (data[i][j] && data[i][j] !== '') {
+ isEmpty = false;
+ break;
+ }
+ }
+ if (!isEmpty) {
+ return data.slice(headerLength, i + 1);
+ }
+ }
+ return [];
+ },
+
+ getYData() {
+ if (this.tableData.length <= headerLength) {
+ return [];
+ }
+ return this.trimColumns(this.tableData[0]);
+ },
+
+ getSeriesData(id: number) {
+ if (this.tableData.length <= id + headerLength) {
+ return [];
+ }
+ return this.trimColumns(this.tableData[id + headerLength]);
+ },
+
+ getDataName(id: number) {
+ if (this.tableData.length <= id + headerLength) {
+ return '';
+ }
+ else {
+ return this.tableData[id + headerLength][0];
+ }
+ }
}
})
</script>
<style scoped>
@layer utilities {
- .form-row {
- @apply my-3;
-
- label {
- @apply py-1;
- }
- }
-
- .box-card {
- @apply m-1;
- }
}
</style>