This is an automated email from the ASF dual-hosted git repository. ovilia pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/echarts-bar-racing.git
commit b2e7210ed122ba0342281a243370ac884bc80d6f Author: Ovilia <zwl.s...@gmail.com> AuthorDate: Thu Jun 10 18:31:30 2021 +0800 feat: generate bar race chart from table --- index.html | 1 + package.json | 2 +- src/App.vue | 7 +- src/components/BBody.vue | 103 ++++++++++++++++++++++ src/components/BChart.vue | 130 ++++++++++++++++++++++++++++ src/components/BTable.vue | 212 ++++++++++++++++++++++++++++++++++------------ 6 files changed, 394 insertions(+), 61 deletions(-) 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> --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@echarts.apache.org For additional commands, e-mail: commits-h...@echarts.apache.org