Commit e4fc4173e243afacd7383984c9041661030ff0bf

Authored by ly525
1 parent 71637956

feat(component): add echarts component !#zh: 支持echarts 图表组件📈

front-end/h5/package.json
@@ -21,6 +21,7 @@ @@ -21,6 +21,7 @@
21 "animate.css": "^3.7.2", 21 "animate.css": "^3.7.2",
22 "ant-design-vue": "^1.3.14", 22 "ant-design-vue": "^1.3.14",
23 "core-js": "^2.6.5", 23 "core-js": "^2.6.5",
  24 + "echarts": "^4.8.0",
24 "element-ui": "^2.13.0", 25 "element-ui": "^2.13.0",
25 "font-awesome": "4.7.0", 26 "font-awesome": "4.7.0",
26 "hotkeys-js": "^3.7.6", 27 "hotkeys-js": "^3.7.6",
@@ -28,7 +29,9 @@ @@ -28,7 +29,9 @@
28 "proxy-agent": "^3.1.0", 29 "proxy-agent": "^3.1.0",
29 "qrcode": "^1.4.1", 30 "qrcode": "^1.4.1",
30 "register-service-worker": "^1.6.2", 31 "register-service-worker": "^1.6.2",
  32 + "resize-detector": "^0.2.2",
31 "strapi-sdk-javascript": "^0.3.1", 33 "strapi-sdk-javascript": "^0.3.1",
  34 + "v-charts": "^1.19.0",
32 "v-click-outside": "^2.1.4", 35 "v-click-outside": "^2.1.4",
33 "vant": "^2.2.12", 36 "vant": "^2.2.12",
34 "vue": "^2.6.10", 37 "vue": "^2.6.10",
front-end/h5/src/components/plugins/charts/chart-mixin.js 0 → 100644
  1 +import echarts from 'echarts/lib/echarts'
  2 +import debounce from 'lodash/debounce'
  3 +import { addListener, removeListener } from 'resize-detector'
  4 +import { LineChart } from './chart-model'
  5 +
  6 +// TODO 工具函数:获取某个时间段内的时间
  7 +function addDays (date, dayStep = 1) {
  8 + const next = new Date(date)
  9 + next.setDate(next.getDate() + dayStep)
  10 + return next
  11 +}
  12 +
  13 +function dateRange (start, end, range = []) {
  14 + start = new Date(start)
  15 + end = new Date(end)
  16 + if (start > end) return range
  17 + range = [start]
  18 + while (start < end) {
  19 + start = addDays(start, 1)
  20 + range.push(start)
  21 + }
  22 + // const next = addDays(start, 1)
  23 + // return dateRange(next, end, [...range, start])
  24 + return range
  25 +}
  26 +
  27 +export default {
  28 + watch: {
  29 + dataset: {
  30 + handler (items) {
  31 + this.renderChart()
  32 + }
  33 + }
  34 + },
  35 + methods: {
  36 + getXAxis () {
  37 + const [start, end] = this.dashboard.currentFilterState.daterange
  38 + if (start === end) return Array.from({ length: 24 }, (_, i) => `${i}`.padStart(2, '0'))
  39 + return dateRange(start, end).map(date => date.toISOString().slice(0, 10))
  40 + },
  41 + /**
  42 + *
  43 + */
  44 + getDataSet () {
  45 + // const [start, end] = daterange
  46 + // if (start === end) {
  47 + // // return this.dataset.map(item => ({...item, s: s.replace(/[\d]{4}-[\d]{2}-[\d]{2}\s+([\d]{2}):[\d]{2}:[\d]{2}/, '$1'))
  48 + // return this.dataset.map(item => ({
  49 + // ...item,
  50 + // x: `${new Date(item.x).getHours()}`.padStart(2, '0')
  51 + // }))
  52 + // }
  53 + return this.dataset
  54 + },
  55 + renderChart () {
  56 + const option = new LineChart({
  57 + dataset: this.getDataSet(),
  58 + yIndexMap: { count: 0, sum_base_cpm: 1 }
  59 + // xAxis: this.getXAxis()
  60 + }).getOption()
  61 + // this.option = Object.freeze(option)
  62 + this.$nextTick(() => {
  63 + const ele = this.$refs.chart
  64 + if (ele) {
  65 + // const chart = window.echarts.init(ele)
  66 + const chart = echarts.init(ele)
  67 + this.chart = chart
  68 + chart.clear()
  69 + chart.setOption(option)
  70 + }
  71 + })
  72 + },
  73 + autoResize () {
  74 + this.lastArea = this.getArea()
  75 + this.__resizeHandler = debounce(
  76 + () => {
  77 + if (this.lastArea === 0) {
  78 + // emulate initial render for initially hidden charts
  79 + this.mergeOptions({}, true)
  80 + this.resize()
  81 + this.mergeOptions(this.options || this.manualOptions || {}, true)
  82 + } else {
  83 + this.resize()
  84 + }
  85 + this.lastArea = this.getArea()
  86 + },
  87 + 100,
  88 + { leading: true }
  89 + )
  90 + addListener(this.$el, this.__resizeHandler)
  91 + }
  92 + },
  93 + mounted () {
  94 + this.__resizeHandler = debounce(
  95 + () => {
  96 + this.chart.resize()
  97 + },
  98 + 500,
  99 + { leading: true }
  100 + )
  101 + addListener(this.$el, this.__resizeHandler)
  102 + },
  103 + destroy () {
  104 + removeListener(this.$el, this.__resizeHandler)
  105 + }
  106 +}
front-end/h5/src/components/plugins/charts/chart-model.js 0 → 100644
  1 +/**
  2 + * echarts demo: https://www.echartsjs.com/examples/zh/editor.html?c=line-simple
  3 + * 通过输入固定格式的 echarts dataset,得到 echarts options
  4 + *
  5 + * TODO X轴 formatter:1.只针对 X轴,不针对 tooltip 2. tooltip 和 X轴同时生效
  6 + * TODO legend(series) 到多Y轴的映射
  7 + */
  8 +import _ from 'lodash'
  9 +
  10 +export class LineChart {
  11 + constructor ({ xAxis = [], yIndexMap = {}, dataset }) {
  12 + this.chartType = 'line'
  13 + this.colors = {
  14 + series: [
  15 + { value: '#57b2ff' },
  16 + { value: '#d70b24' },
  17 + { value: '#48c765' },
  18 + { value: '#fbb64e' },
  19 + { value: '#f95e58' }
  20 + ],
  21 + labelText: {
  22 + xAxis: 'rgba(0,0,0,.6)',
  23 + yAxis: 'rgba(0,0,0,.6)'
  24 + },
  25 + splitLine: 'rgba(236, 237, 239, 0.26)'
  26 + }
  27 + this.yIndexMap = yIndexMap
  28 + this.dataset = dataset
  29 + this.keys = { xAxis: 'x', yAxis: 'y', legend: 's' }
  30 + this.legends = this.getLegends()
  31 + this.xAxis = (xAxis.length && xAxis) || this.getXAxis()
  32 + this.yAxis = this.getYAxis(this.legends)
  33 + this.series = this.getSeries(this.legends, this.xAxis)
  34 + this.tooltip = this.getTooltip()
  35 + }
  36 + /**
  37 + * 填补缺失值,说白了其实就是 merge
  38 + * seriesTemplate: [{x: '01-01', y: null}, {x: '02-01', y: null}, {x: '03-01', y: null}]
  39 + * seriesItemsFromDataSet: [{ x: '01-01', y: 11}, {x: '03-01', y: 33 }]
  40 + *
  41 + * => [{x: '01-01', y: 11}, {x: '02-01', y: null}, {x: '03-01', y: 33}]
  42 + *
  43 + * seriesTemplate (X轴所有点): ['01-01', '01-02', '01-03]
  44 + * seriesItemsFromDataSet: 可能缺失的数据作为 填充值
  45 + * options: { legend: string // 某条折线图名字 }
  46 + *
  47 + * seriesTemplate -> 循环填充默认值 -> [Object, Object]
  48 + * seriesItemsFromDataSet -> 字典{} -> 循环模板(X轴所有点)-> 找到X轴上点在 seriesItemsFromDataSet字典中的值 ->,替换模板中的默认值 -> 返回填充后的模板
  49 + */
  50 + padMissingValues (seriesTemplate, seriesItemsFromDataSet, options) {
  51 + const { xAxis, yAxis, legend } = this.keys
  52 + const seriesItemsFromDataSetDict = seriesItemsFromDataSet.reduce((obj, curr) => (Object.assign(Object.assign({}, obj), { [curr[xAxis]]: curr })), {})
  53 + const templateWithValue = seriesTemplate.map(xAxis => {
  54 + return (seriesItemsFromDataSetDict[xAxis] || {
  55 + [legend]: options.legend,
  56 + [xAxis]: xAxis,
  57 + [yAxis]: null
  58 + })
  59 + })
  60 + return templateWithValue
  61 + }
  62 + /**
  63 + * 对于复杂的数据转换,需要写 ts,方便看懂代码之间的数据流转
  64 + * @returns: ['折线图1', '折线图2']
  65 + */
  66 + getLegends () {
  67 + const legendKey = this.keys.legend
  68 + const allLegends = _.uniqBy(this.dataset, legendKey).map((item) => item[legendKey])
  69 + const legends = Array.from(new Set(allLegends))
  70 + return legends
  71 + }
  72 + /**
  73 + * 获取X轴的数据
  74 + * demo: ['01-01', '02-01', '03-01', '04-01']
  75 + */
  76 + getXAxis () {
  77 + const xAxis = this.dataset.map(item => item[this.keys.xAxis])
  78 + return xAxis
  79 + }
  80 + getTooltip () {
  81 + return {
  82 + trigger: 'axis'
  83 + }
  84 + }
  85 + getDefaultYAxis (option) {
  86 + const { show, axisLabelFormatter } = option
  87 + return {
  88 + show,
  89 + type: 'value',
  90 + axisLabel: {
  91 + // Y轴文字颜色,
  92 + color: this.colors.labelText.xAxis,
  93 + // formatter: '{value}',
  94 + formatter: axisLabelFormatter
  95 + },
  96 + // 不需要对 Y 轴顶部的文字做定制
  97 + // nameTextStyle: {
  98 + // color: [yAxisTitleFontColor],
  99 + // fontSize: 14,
  100 + // fontWeight: 600,
  101 + // },
  102 + // y轴坐标轴轴线, 也就是y轴的一条竖线
  103 + axisLine: {
  104 + show: false,
  105 + lineStyle: {
  106 + // y轴上数字的颜色
  107 + fontSize: 15,
  108 + color: this.colors.labelText.yAxis
  109 + // opacity: 1
  110 + }
  111 + },
  112 + // 显示轴线与数值之间的 「-」
  113 + axisTick: { show: false },
  114 + splitLine: {
  115 + lineStyle: {
  116 + // 使用深浅的间隔色
  117 + // 类似四条三格的横线的颜色
  118 + color: [this.colors.splitLine]
  119 + }
  120 + }
  121 + }
  122 + }
  123 + /**
  124 + * Y轴数据
  125 + * @param legends
  126 + */
  127 + getYAxis (legends) {
  128 + // TODO 如果返回值 函数怎么办呢?
  129 + function getAxisLabelFormatter (legend) {
  130 + return function (value) {
  131 + // const format = formatDict[legend] || "0.0a";
  132 + // return numeral(value).format(format);
  133 + // TODO 自己实现 formatter 函数
  134 + return value
  135 + }
  136 + }
  137 + const yAxis = legends.map(legend => this.getDefaultYAxis({
  138 + show: window.innerWidth > 768,
  139 + // TODO 外界传进来legendValueFormatter
  140 + axisLabelFormatter: getAxisLabelFormatter(legend)
  141 + }))
  142 + return yAxis
  143 + }
  144 + /**
  145 + *
  146 + * @param {*} legends
  147 + * @param {*} xAxis
  148 + *
  149 + interface Series {
  150 + name: string,
  151 + data: seriesData,
  152 +
  153 + }
  154 + */
  155 + getSeries (legends, xAxis) {
  156 + /**
  157 + interface groupbyLegend {
  158 + [legendKey1]: [
  159 + [legendKey1]: legendValue,
  160 + [xAxiskey]: xAxisValue,
  161 + [yAxiskey]: yAxisValue
  162 + ]
  163 + }
  164 + */
  165 + const groupbyLegend = _.groupBy(this.dataset, this.keys.legend)
  166 + const series = legends.map((legend, index) => {
  167 + const seriesItemsFromDataSet = groupbyLegend[legend]
  168 + const data = this.padMissingValues(xAxis, seriesItemsFromDataSet, { legend }).map(item => item[this.keys.yAxis])
  169 + return {
  170 + name: legend,
  171 + type: this.chartType,
  172 + // TODO 值和 value 对应起来
  173 + data,
  174 + yAxisIndex: this.yIndexMap[legend] || 0,
  175 + itemStyle: {
  176 + normal: {
  177 + color: this.colors.series[index].value
  178 + }
  179 + // emphasis: {
  180 + // color: '#59a4ed',
  181 + // },
  182 + }
  183 + // TODO 指定 Y轴
  184 + // TODO zlevel: 0,
  185 + }
  186 + })
  187 + return series
  188 + }
  189 + getDefaultOption () {
  190 + return {
  191 + tooltip: {
  192 + show: true,
  193 + trigger: 'axis'
  194 + },
  195 + axisTick: { show: false },
  196 + xAxis: {
  197 + type: 'category',
  198 + boundaryGap: true,
  199 + axisTick: { show: false },
  200 + axisLine: {
  201 + lineStyle: {
  202 + // x轴颜色, 包含:x轴的颜色、文字颜色
  203 + // 图例: https://jietu.qq.com/upload/index.html?image=http://jietu-10024907.file.myqcloud.com/hwfezrujmosnhjesfoweuqdguwrwyekw.jpg
  204 + color: 'rgba(0,0,0,.6)'
  205 + }
  206 + }
  207 + },
  208 + grid: {
  209 + left: '3%',
  210 + right: '4%',
  211 + bottom: '3%',
  212 + containLabel: true
  213 + },
  214 + color: this.colors.series
  215 + }
  216 + }
  217 + getOption () {
  218 + const option = {
  219 + textStyle: {
  220 + fontFamily: 'Roboto'
  221 + },
  222 + tooltip: this.tooltip,
  223 + series: this.series,
  224 + legend: {
  225 + data: this.legends,
  226 + show: true
  227 + },
  228 + xAxis: {
  229 + data: this.xAxis
  230 + },
  231 + yAxis: this.yAxis
  232 + }
  233 + return _.merge(option, this.getDefaultOption())
  234 + }
  235 +}
front-end/h5/src/components/plugins/charts/chart-model.ts 0 → 100644
  1 +/**
  2 + * echarts demo: https://www.echartsjs.com/examples/zh/editor.html?c=line-simple
  3 + * 通过输入固定格式的 echarts dataset,得到 echarts options
  4 + *
  5 + * TODO X轴 formatter:1.只针对 X轴,不针对 tooltip 2. tooltip 和 X轴同时生效
  6 + * TODO legend(series) 到多Y轴的映射
  7 + */
  8 +import _ from "lodash";
  9 +// import numeral from "numeral";
  10 +
  11 +/**
  12 + *
  13 + yIndexMap: {
  14 + 系列1: 0,
  15 + 系列2: 1
  16 + }
  17 +
  18 + const users = [
  19 + {
  20 + x: "2010/01/01 00:00:00",
  21 + y: 500,
  22 + s: "系列1"
  23 + },
  24 + {
  25 + x: "2010/01/01 00:00:00",
  26 + y: 180,
  27 + s: "系列2"
  28 + },
  29 + {
  30 + x: "2010/02/01 00:00:00",
  31 + y: 250,
  32 + s: "系列1"
  33 + },
  34 + {
  35 + x: "2010/02/01 00:00:00",
  36 + y: 100,
  37 + s: "系列2"
  38 + },
  39 + {
  40 + x: "2010/03/01 00:00:00",
  41 + y: 325,
  42 + s: "系列1"
  43 + },
  44 + {
  45 + x: "2010/03/01 00:00:00",
  46 + y: 175,
  47 + s: "系列2"
  48 + },
  49 + {
  50 + x: "2010/04/01 00:00:00",
  51 + y: 190,
  52 + s: "系列1"
  53 + },
  54 + {
  55 + x: "2010/04/01 00:00:00",
  56 + y: 110,
  57 + s: "系列2"
  58 + },
  59 + {
  60 + x: "2010/05/01 00:00:00",
  61 + y: 260,
  62 + s: "系列1"
  63 + },
  64 + {
  65 + x: "2010/05/01 00:00:00",
  66 + y: 60,
  67 + s: "系列2"
  68 + },
  69 + {
  70 + x: "2010/05/19 00:00:00",
  71 + y: 90,
  72 + s: "系列2"
  73 + },
  74 + {
  75 + x: "2010/05/23 00:00:00",
  76 + y: 90,
  77 + s: "系列2"
  78 + }
  79 + ];
  80 +
  81 + *
  82 + */
  83 +
  84 +// 固定格式
  85 +interface DataSetItem {
  86 + x: string; // x轴
  87 + y: string | number; // y轴,也就是 value
  88 + s: string | number; // s 也就是 legend
  89 + [propName: string]: any;
  90 +}
  91 +
  92 +interface SeriesItemDict {
  93 + [propName: string]: any;
  94 +}
  95 +
  96 +interface SeriesItem {
  97 + name?: string; // 可选
  98 + type: string;
  99 + data: Array<string | number>;
  100 + itemStyle: {
  101 + normal: {
  102 + color: string;
  103 + };
  104 + // emphasis: {
  105 + // color: '#59a4ed',
  106 + // },
  107 + };
  108 + yAxisIndex?: number
  109 + // TODO 指定 Y轴
  110 + // TODO zlevel: 0,
  111 +}
  112 +
  113 +interface YAxisOption {
  114 + show: boolean;
  115 + axisLabelFormatter: (value: any) => any;
  116 +}
  117 +
  118 +interface formatDictType {
  119 + [propName: string]: string;
  120 +}
  121 +
  122 +interface uniqByResultType {
  123 + [legendKey: string]: string // legendValue
  124 +}
  125 +
  126 +interface LineChartConfig {
  127 + xAxis?: Array<string|number>;
  128 + dataset: DataSetItem[];
  129 + yIndexMap: {
  130 + [legendkey: string]: number
  131 + }
  132 +}
  133 +
  134 +const formatDict: formatDictType = {
  135 + gp: "$0.00",
  136 + gross_profit: "$0.00",
  137 + revenue: "$0.00",
  138 + reveue_f: "$0.00",
  139 + cvr: "0.00%",
  140 + click_count: "0,00",
  141 + conv: "0,00",
  142 + show_rate: "0.00%",
  143 + fill_rate: "0.00%"
  144 +};
  145 +
  146 +export class LineChart {
  147 + // 指定折线采用哪一条Y轴
  148 + yIndexMap: {
  149 + // 纬度/折线
  150 + [legendName: string]: number
  151 + };
  152 + chartType: string; // 'line' | 'bar' | 'pie'
  153 + // TODO class 里面如何定义 ts
  154 + colors: {
  155 + series: any;
  156 + labelText: any;
  157 + splitLine: string;
  158 + };
  159 + // 数据源
  160 + dataset: DataSetItem[];
  161 + keys: {
  162 + xAxis: string;
  163 + yAxis: string;
  164 + legend: string;
  165 + };
  166 + legends: string[];
  167 + xAxis: Array<string | number>;
  168 + yAxis: any[];
  169 + series: Array<SeriesItem>;
  170 + tooltip: any;
  171 +
  172 + constructor({
  173 + xAxis = [],
  174 + yIndexMap = {},
  175 + dataset
  176 + } : LineChartConfig
  177 + ) {
  178 + this.chartType = "line";
  179 + this.colors = {
  180 + series: [
  181 + { value: "#57b2ff" },
  182 + { value: "#d70b24" },
  183 + { value: "#48c765" },
  184 + { value: "#fbb64e" },
  185 + { value: "#f95e58" }
  186 + ],
  187 + labelText: {
  188 + xAxis: "rgba(0,0,0,.6)",
  189 + yAxis: "rgba(0,0,0,.6)"
  190 + },
  191 + splitLine: "rgba(236, 237, 239, 0.26)"
  192 + };
  193 + this.yIndexMap = yIndexMap;
  194 + this.dataset = dataset;
  195 + this.keys = { xAxis: "x", yAxis: "y", legend: "s" };
  196 +
  197 + this.legends = this.getLegends();
  198 + this.xAxis = (xAxis.length && xAxis) || this.getXAxis();
  199 + this.yAxis = this.getYAxis(this.legends);
  200 + this.series = this.getSeries(this.legends, this.xAxis);
  201 + this.tooltip = this.getTooltip();
  202 + }
  203 +
  204 + /**
  205 + * 填补缺失值,说白了其实就是 merge
  206 + * seriesTemplate: [{x: '01-01', y: null}, {x: '02-01', y: null}, {x: '03-01', y: null}]
  207 + * seriesItemsFromDataSet: [{ x: '01-01', y: 11}, {x: '03-01', y: 33 }]
  208 + *
  209 + * => [{x: '01-01', y: 11}, {x: '02-01', y: null}, {x: '03-01', y: 33}]
  210 + *
  211 + * seriesTemplate (X轴所有点): ['01-01', '01-02', '01-03]
  212 + * seriesItemsFromDataSet: 可能缺失的数据作为 填充值
  213 + * options: { legend: string // 某条折线图名字 }
  214 + *
  215 + * seriesTemplate -> 循环填充默认值 -> [Object, Object]
  216 + * seriesItemsFromDataSet -> 字典{} -> 循环模板(X轴所有点)-> 找到X轴上点在 seriesItemsFromDataSet字典中的值 ->,替换模板中的默认值 -> 返回填充后的模板
  217 + */
  218 + padMissingValues(
  219 + seriesTemplate: Array<string | number>,
  220 + seriesItemsFromDataSet: DataSetItem[],
  221 + options: { legend: string }
  222 + ): DataSetItem[] {
  223 + const { xAxis, yAxis, legend } = this.keys;
  224 +
  225 + const seriesItemsFromDataSetDict: SeriesItemDict = seriesItemsFromDataSet.reduce(
  226 + (obj, curr: DataSetItem) => ({
  227 + ...obj,
  228 + [curr[xAxis]]: curr
  229 + }),
  230 + {}
  231 + );
  232 +
  233 + const templateWithValue = seriesTemplate.map(xAxis => {
  234 + return (
  235 + seriesItemsFromDataSetDict[xAxis] || {
  236 + [legend]: options.legend,
  237 + [xAxis]: xAxis,
  238 + [yAxis]: null
  239 + }
  240 + );
  241 + });
  242 +
  243 + return templateWithValue;
  244 + }
  245 +
  246 + /**
  247 + * 对于复杂的数据转换,需要写 ts,方便看懂代码之间的数据流转
  248 + * @returns: ['折线图1', '折线图2']
  249 + */
  250 + getLegends(): string[] {
  251 + const legendKey = this.keys.legend;
  252 + const allLegends: string[] = _.uniqBy(this.dataset, legendKey).map(
  253 + (item: uniqByResultType) => item[legendKey]
  254 + );
  255 + const legends: string[] = Array.from(new Set(allLegends));
  256 + return legends;
  257 + }
  258 +
  259 + /**
  260 + * 获取X轴的数据
  261 + * demo: ['01-01', '02-01', '03-01', '04-01']
  262 + */
  263 + getXAxis(): Array<string | number> {
  264 + const xAxis = this.dataset.map(item => item[this.keys.xAxis]);
  265 + return xAxis;
  266 + }
  267 +
  268 + getTooltip(): any {
  269 + return {
  270 + trigger: "axis"
  271 + };
  272 + }
  273 +
  274 + getDefaultYAxis(option: YAxisOption) {
  275 + const { show, axisLabelFormatter } = option;
  276 + return {
  277 + show,
  278 + type: "value",
  279 + axisLabel: {
  280 + // Y轴文字颜色,
  281 + color: this.colors.labelText.xAxis,
  282 + // formatter: '{value}',
  283 + formatter: axisLabelFormatter
  284 + },
  285 + // 不需要对 Y 轴顶部的文字做定制
  286 + // nameTextStyle: {
  287 + // color: [yAxisTitleFontColor],
  288 + // fontSize: 14,
  289 + // fontWeight: 600,
  290 + // },
  291 + // y轴坐标轴轴线, 也就是y轴的一条竖线
  292 + axisLine: {
  293 + show: false,
  294 + lineStyle: {
  295 + // y轴上数字的颜色
  296 + fontSize: 15,
  297 + color: this.colors.labelText.yAxis
  298 + // opacity: 1
  299 + }
  300 + },
  301 + // 显示轴线与数值之间的 「-」
  302 + axisTick: { show: false },
  303 + splitLine: {
  304 + lineStyle: {
  305 + // 使用深浅的间隔色
  306 + // 类似四条三格的横线的颜色
  307 + color: [this.colors.splitLine]
  308 + }
  309 + }
  310 + };
  311 + }
  312 +
  313 + /**
  314 + * Y轴数据
  315 + * @param legends
  316 + */
  317 + getYAxis(legends: string[]): any[] {
  318 + // TODO 如果返回值 函数怎么办呢?
  319 + function getAxisLabelFormatter(legend: string) {
  320 + return function(value: any) {
  321 + // const format = formatDict[legend] || "0.0a";
  322 + // return numeral(value).format(format);
  323 + // TODO 自己实现 formatter 函数
  324 + return value
  325 + };
  326 + }
  327 + const yAxis = legends.map(legend =>
  328 + this.getDefaultYAxis({
  329 + show: window.innerWidth > 768,
  330 + // TODO 外界传进来legendValueFormatter
  331 + axisLabelFormatter: getAxisLabelFormatter(legend)
  332 + })
  333 + );
  334 + return yAxis;
  335 + }
  336 +
  337 + /**
  338 + *
  339 + * @param {*} legends
  340 + * @param {*} xAxis
  341 + *
  342 + interface Series {
  343 + name: string,
  344 + data: seriesData,
  345 +
  346 + }
  347 + */
  348 + getSeries(legends: string[], xAxis: Array<string | number>): SeriesItem[] {
  349 + /**
  350 + interface groupbyLegend {
  351 + [legendKey1]: [
  352 + [legendKey1]: legendValue,
  353 + [xAxiskey]: xAxisValue,
  354 + [yAxiskey]: yAxisValue
  355 + ]
  356 + }
  357 + */
  358 + const groupbyLegend = _.groupBy(this.dataset, this.keys.legend);
  359 + const series = legends.map((legend: string, index: number) => {
  360 + const seriesItemsFromDataSet: DataSetItem[] = groupbyLegend[legend];
  361 + const data: Array<string | number> = this.padMissingValues(
  362 + xAxis,
  363 + seriesItemsFromDataSet,
  364 + { legend }
  365 + ).map(item => item[this.keys.yAxis]);
  366 + return {
  367 + name: legend, // 可选
  368 + type: this.chartType,
  369 + // TODO 值和 value 对应起来
  370 + data,
  371 + yAxisIndex: this.yIndexMap[legend] || 0,
  372 + itemStyle: {
  373 + normal: {
  374 + color: this.colors.series[index].value
  375 + }
  376 + // emphasis: {
  377 + // color: '#59a4ed',
  378 + // },
  379 + }
  380 + // TODO 指定 Y轴
  381 + // TODO zlevel: 0,
  382 + };
  383 + });
  384 + return series;
  385 + }
  386 +
  387 + getDefaultOption() {
  388 + return {
  389 + tooltip: {
  390 + show: true,
  391 + trigger: "axis"
  392 + },
  393 + axisTick: { show: false },
  394 + xAxis: {
  395 + type: "category",
  396 + boundaryGap: true,
  397 + axisTick: { show: false },
  398 + axisLine: {
  399 + lineStyle: {
  400 + // x轴颜色, 包含:x轴的颜色、文字颜色
  401 + // 图例: https://jietu.qq.com/upload/index.html?image=http://jietu-10024907.file.myqcloud.com/hwfezrujmosnhjesfoweuqdguwrwyekw.jpg
  402 + color: "rgba(0,0,0,.6)"
  403 + }
  404 + }
  405 + },
  406 + grid: {
  407 + y: 40, // 图表和legend之间的距离
  408 + left: "5%", // 图表的x轴的零点距离容器左侧的距离
  409 + right: "4%", // 图表的x轴的最右点距离容器右侧的距离
  410 + bottom: 30
  411 + },
  412 + color: this.colors.series
  413 + };
  414 + }
  415 +
  416 + getOption() {
  417 + const option = {
  418 + textStyle: {
  419 + fontFamily: "Roboto"
  420 + },
  421 + tooltip: this.tooltip,
  422 + series: this.series,
  423 + legend: {
  424 + data: this.legends,
  425 + show: true
  426 + },
  427 + xAxis: {
  428 + data: this.xAxis
  429 + },
  430 + yAxis: this.yAxis
  431 + };
  432 + return _.merge(option, this.getDefaultOption());
  433 + }
  434 +}
0 \ No newline at end of file 435 \ No newline at end of file
front-end/h5/src/components/plugins/charts/line.js 0 → 100644
  1 +import VeLine from 'v-charts/lib/line.common'
  2 +import VePie from 'v-charts/lib/pie.common'
  3 +import VeHistogram from 'v-charts/lib/histogram.common'
  4 +import VeFunnel from 'v-charts/lib/funnel.common'
  5 +import PropTypes from '@luban-h5/plugin-common-props'
  6 +import 'echarts/lib/component/legend'
  7 +import 'echarts/lib/component/markLine'
  8 +import 'echarts/lib/component/markPoint'
  9 +import 'echarts/lib/component/markArea'
  10 +import Parser from '../../../utils/excel-parser'
  11 +
  12 +export default {
  13 + extra: {
  14 + defaultStyle: {
  15 + width: 320,
  16 + height: 400
  17 + }
  18 + },
  19 + name: 'lbp-line-chart',
  20 + // mixins: [ChartMixin],
  21 + props: {
  22 + dataset: PropTypes.excel({
  23 + defaultValue: () => [
  24 + ['日期', '销售量'],
  25 + ['1月1日', 123],
  26 + ['1月2日', 1223],
  27 + ['1月3日', 2123],
  28 + ['1月4日', 4123],
  29 + ['1月5日', 3123],
  30 + ['1月6日', 7123]
  31 + ]
  32 + }),
  33 + type: PropTypes.string({
  34 + label: '类型',
  35 + defaultValue: 'line',
  36 + visible: false
  37 + })
  38 + },
  39 + data () {
  40 + return {
  41 + option: {}
  42 + }
  43 + },
  44 + render () {
  45 + const chartData = Parser.csv2VChartJson(this.dataset)
  46 + const settings = {
  47 + // metrics: ['日期', '销售量'],
  48 + // dimension: ['日期']
  49 + }
  50 + switch (this.type) {
  51 + case 'line':
  52 + return <VeLine data={chartData} settings={settings} />
  53 + case 'histogram':
  54 + return <VeHistogram data={chartData} settings={settings} />
  55 + case 'pie':
  56 + return <VePie data={chartData} settings={settings} />
  57 + case 'funnel':
  58 + return <VeFunnel data={chartData} settings={settings} />
  59 + default:
  60 + return null
  61 + }
  62 + },
  63 + mounted () {
  64 + // this.renderChart()
  65 + }
  66 +}
front-end/h5/src/utils/excel-parser.js 0 → 100644
  1 +/**
  2 + *
  3 + declare module ExcelRows {
  4 + export interface cell {
  5 + text: string;
  6 + }
  7 + export interface Cells {
  8 + 0: cell;
  9 + 1: cell;
  10 + 2: cell;
  11 + }
  12 + export interface ExcelRows {
  13 + cells: Cells;
  14 + }
  15 +}
  16 + */
  17 +
  18 +/**
  19 + *
  20 + BinaryMatrix = [
  21 + [any, any, any, ...],
  22 + [any, any, any, ...],
  23 + [any, any, any, ...],
  24 + ]
  25 +
  26 + ExcelDataType = [
  27 + {
  28 + cells: {
  29 + 0: { text: any },
  30 + 1: { text: any },
  31 + 2: { text: any }
  32 + }
  33 + },
  34 + {
  35 + cells: {
  36 + 0: { text: any },
  37 + 1: { text: any },
  38 + 2: { text: any }
  39 + }
  40 + },
  41 + ]
  42 + */
  43 +
  44 +export default class Parser {
  45 + /**
  46 + *
  47 + * @param {*} dataset ExcelDataType
  48 + */
  49 + static dataset2excel (dataset) {
  50 + return dataset.map(item => ({
  51 + cells: {
  52 + 0: { text: item.x },
  53 + 1: { text: item.y },
  54 + 2: { text: item.s }
  55 + }
  56 + }))
  57 + }
  58 +
  59 + /**
  60 + *
  61 + [
  62 + [1,2,3,4],
  63 + [5,6,7,8],
  64 + [9,10,11,12]
  65 + ]
  66 + * @param {Object} BinaryMatrix
  67 + * @returns {Object} ExcelDataType
  68 + */
  69 + static binaryMatrix2excel (binaryMatrix) {
  70 + const excelData = binaryMatrix.map((row, rowIndex) => {
  71 + // cells: {
  72 + // 0: { text: item.x },
  73 + // 1: { text: item.y },
  74 + // 2: { text: item.s }
  75 + // }
  76 + const cells = {}
  77 + row.forEach((cellValue, cellIndex) => {
  78 + cells[cellIndex] = { text: cellValue }
  79 + })
  80 + return { cells }
  81 + })
  82 + return excelData
  83 + }
  84 +
  85 + static excel2chartDataSet (excelData) {
  86 + const rowsArray = Object.values(excelData.rows).filter(item => typeof item === 'object')
  87 + const dataset = rowsArray.map(row => {
  88 + const [x, y, s] = Object.values(row.cells).map(item => item.text)
  89 + return {
  90 + x: x,
  91 + y: y,
  92 + s: s
  93 + }
  94 + })
  95 + return dataset
  96 + }
  97 +
  98 + static excel2BinaryMatrix (excelData) {
  99 + const rowsArray = Object.values(excelData.rows).filter(item => typeof item === 'object')
  100 + const dataset = rowsArray.map(row => {
  101 + // [1,2,3,4]
  102 + const cells = Object.values(row.cells).map(item => item.text)
  103 + return cells
  104 + })
  105 + console.log('dataset', dataset)
  106 + return dataset
  107 + }
  108 +
  109 + /**
  110 + *
  111 + * @param {Array} csvArray
  112 + * [
  113 + ['日期', '销售量'],
  114 + ["1月1日",123],
  115 + ["1月2日",1223],
  116 + ["1月3日",2123],
  117 + ["1月4日",4123],
  118 + ["1月5日",3123],
  119 + ["1月6日",7123]
  120 + ]
  121 + * @returns {Object}
  122 + {
  123 + columns: ['日期', '销售量'],
  124 + rows:[
  125 + { '日期': '1月1日', '销售量': 123 },
  126 + { '日期': '1月2日', '销售量': 1223 },
  127 + { '日期': '1月3日', '销售量': 2123 },
  128 + { '日期': '1月4日', '销售量': 4123 },
  129 + { '日期': '1月5日', '销售量': 3123 },
  130 + { '日期': '1月6日', '销售量': 7123 }
  131 + ]
  132 + }
  133 + */
  134 + static csv2VChartJson (csvArray) {
  135 + const columns = csvArray[0]
  136 + const rows = csvArray.slice(1)
  137 + const json = {
  138 + columns,
  139 + rows: rows.map((row, index) => {
  140 + const obj = {}
  141 + columns.forEach((col, colIndex) => {
  142 + obj[col] = row[colIndex]
  143 + })
  144 + return obj
  145 + })
  146 + }
  147 + return json
  148 + }
  149 +}
front-end/h5/yarn.lock
@@ -3684,6 +3684,28 @@ ecc-jsbn@~0.1.1: @@ -3684,6 +3684,28 @@ ecc-jsbn@~0.1.1:
3684 jsbn "~0.1.0" 3684 jsbn "~0.1.0"
3685 safer-buffer "^2.1.0" 3685 safer-buffer "^2.1.0"
3686 3686
  3687 +echarts-amap@1.0.0-rc.6:
  3688 + version "1.0.0-rc.6"
  3689 + resolved "https://registry.npm.taobao.org/echarts-amap/download/echarts-amap-1.0.0-rc.6.tgz#5782a74daee52ed44ce3f8f62577561783f09e16"
  3690 + integrity sha1-V4KnTa7lLtRM4/j2JXdWF4PwnhY=
  3691 +
  3692 +echarts-liquidfill@^2.0.2:
  3693 + version "2.0.6"
  3694 + resolved "https://registry.npm.taobao.org/echarts-liquidfill/download/echarts-liquidfill-2.0.6.tgz#0668dc61d87a6262003090bd32c55aa8108c252e"
  3695 + integrity sha1-BmjcYdh6YmIAMJC9MsVaqBCMJS4=
  3696 +
  3697 +echarts-wordcloud@^1.1.3:
  3698 + version "1.1.3"
  3699 + resolved "https://registry.npm.taobao.org/echarts-wordcloud/download/echarts-wordcloud-1.1.3.tgz#07b140c8ba76b19c317b43c310f3d5dc99289ff2"
  3700 + integrity sha1-B7FAyLp2sZwxe0PDEPPV3Jkon/I=
  3701 +
  3702 +echarts@^4.8.0:
  3703 + version "4.8.0"
  3704 + resolved "https://registry.npm.taobao.org/echarts/download/echarts-4.8.0.tgz?cache=0&sync_timestamp=1596214937248&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fecharts%2Fdownload%2Fecharts-4.8.0.tgz#b2c1cfb9229b13d368ee104fc8eea600b574d4c4"
  3705 + integrity sha1-ssHPuSKbE9No7hBPyO6mALV01MQ=
  3706 + dependencies:
  3707 + zrender "4.3.1"
  3708 +
3687 editorconfig@^0.15.3: 3709 editorconfig@^0.15.3:
3688 version "0.15.3" 3710 version "0.15.3"
3689 resolved "https://registry.npm.taobao.org/editorconfig/download/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5" 3711 resolved "https://registry.npm.taobao.org/editorconfig/download/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5"
@@ -7556,6 +7578,11 @@ number-is-nan@^1.0.0: @@ -7556,6 +7578,11 @@ number-is-nan@^1.0.0:
7556 resolved "https://registry.npm.taobao.org/number-is-nan/download/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" 7578 resolved "https://registry.npm.taobao.org/number-is-nan/download/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
7557 integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= 7579 integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
7558 7580
  7581 +numerify@1.2.9:
  7582 + version "1.2.9"
  7583 + resolved "https://registry.npm.taobao.org/numerify/download/numerify-1.2.9.tgz#af4696bb1d57f8d3970a615d8b0cd53d932bd559"
  7584 + integrity sha1-r0aWux1X+NOXCmFdiwzVPZMr1Vk=
  7585 +
7559 nwsapi@^2.0.7: 7586 nwsapi@^2.0.7:
7560 version "2.2.0" 7587 version "2.2.0"
7561 resolved "https://registry.npm.taobao.org/nwsapi/download/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" 7588 resolved "https://registry.npm.taobao.org/nwsapi/download/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
@@ -9208,6 +9235,11 @@ reselect@^3.0.1: @@ -9208,6 +9235,11 @@ reselect@^3.0.1:
9208 resolved "https://registry.npm.taobao.org/reselect/download/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147" 9235 resolved "https://registry.npm.taobao.org/reselect/download/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147"
9209 integrity sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc= 9236 integrity sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc=
9210 9237
  9238 +resize-detector@^0.2.2:
  9239 + version "0.2.2"
  9240 + resolved "https://registry.npm.taobao.org/resize-detector/download/resize-detector-0.2.2.tgz#b207e72912bef0bda9fb825fe894ed9686ca965e"
  9241 + integrity sha1-sgfnKRK+8L2p+4Jf6JTtlobKll4=
  9242 +
9211 resize-observer-polyfill@^1.5.0, resize-observer-polyfill@^1.5.1: 9243 resize-observer-polyfill@^1.5.0, resize-observer-polyfill@^1.5.1:
9212 version "1.5.1" 9244 version "1.5.1"
9213 resolved "https://registry.npm.taobao.org/resize-observer-polyfill/download/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" 9245 resolved "https://registry.npm.taobao.org/resize-observer-polyfill/download/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
@@ -10681,6 +10713,11 @@ utila@^0.4.0, utila@~0.4: @@ -10681,6 +10713,11 @@ utila@^0.4.0, utila@~0.4:
10681 resolved "https://registry.npm.taobao.org/utila/download/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" 10713 resolved "https://registry.npm.taobao.org/utila/download/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
10682 integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= 10714 integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=
10683 10715
  10716 +utils-lite@0.1.10:
  10717 + version "0.1.10"
  10718 + resolved "https://registry.npm.taobao.org/utils-lite/download/utils-lite-0.1.10.tgz#d2908c0482e23c31e6b082558540e7134ffad7d7"
  10719 + integrity sha1-0pCMBILiPDHmsIJVhUDnE0/619c=
  10720 +
10684 utils-merge@1.0.1: 10721 utils-merge@1.0.1:
10685 version "1.0.1" 10722 version "1.0.1"
10686 resolved "https://registry.npm.taobao.org/utils-merge/download/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 10723 resolved "https://registry.npm.taobao.org/utils-merge/download/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
@@ -10691,6 +10728,17 @@ uuid@^3.0.1, uuid@^3.3.2: @@ -10691,6 +10728,17 @@ uuid@^3.0.1, uuid@^3.3.2:
10691 resolved "https://registry.npm.taobao.org/uuid/download/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" 10728 resolved "https://registry.npm.taobao.org/uuid/download/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866"
10692 integrity sha1-RWjwIW54dg7h2/Ok0s9T4iQRKGY= 10729 integrity sha1-RWjwIW54dg7h2/Ok0s9T4iQRKGY=
10693 10730
  10731 +v-charts@^1.19.0:
  10732 + version "1.19.0"
  10733 + resolved "https://registry.npm.taobao.org/v-charts/download/v-charts-1.19.0.tgz#07b701800b159bd514264ffc8bf12b0405142da3"
  10734 + integrity sha1-B7cBgAsVm9UUJk/8i/ErBAUULaM=
  10735 + dependencies:
  10736 + echarts-amap "1.0.0-rc.6"
  10737 + echarts-liquidfill "^2.0.2"
  10738 + echarts-wordcloud "^1.1.3"
  10739 + numerify "1.2.9"
  10740 + utils-lite "0.1.10"
  10741 +
10694 v-click-outside@^2.1.4: 10742 v-click-outside@^2.1.4:
10695 version "2.1.4" 10743 version "2.1.4"
10696 resolved "https://registry.npm.taobao.org/v-click-outside/download/v-click-outside-2.1.4.tgz#7d7813b09a0fd4a9d0fbccc2ca12ca7c5591e087" 10744 resolved "https://registry.npm.taobao.org/v-click-outside/download/v-click-outside-2.1.4.tgz#7d7813b09a0fd4a9d0fbccc2ca12ca7c5591e087"
@@ -11459,3 +11507,8 @@ yorkie@^2.0.0: @@ -11459,3 +11507,8 @@ yorkie@^2.0.0:
11459 is-ci "^1.0.10" 11507 is-ci "^1.0.10"
11460 normalize-path "^1.0.0" 11508 normalize-path "^1.0.0"
11461 strip-indent "^2.0.0" 11509 strip-indent "^2.0.0"
  11510 +
  11511 +zrender@4.3.1:
  11512 + version "4.3.1"
  11513 + resolved "https://registry.npm.taobao.org/zrender/download/zrender-4.3.1.tgz?cache=0&sync_timestamp=1596718864550&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fzrender%2Fdownload%2Fzrender-4.3.1.tgz#baf8aa6dc8187a2f819692d7d5f9bedfa2b90fa3"
  11514 + integrity sha1-uviqbcgYei+BlpLX1fm+36K5D6M=