UNPKG

d2recharts

Version:

data driven react components of echarts

395 lines (378 loc) 11.9 kB
'use strict'; /** * an class to generate options for ECharts * @module option-generator * @see module:index */ const _ = require('lodash'); const numeral = require('numeral'); const util = require('../util/index'); const constant = require('../constant'); const { formatNumber } = require('../formatter'); // const MIN_DATA_ZOOM_LIMIT = 20; const DEFAULT_OPTION = { animation: false, theme: 'default', tooltip: {}, toolbox: { feature: {}, }, legend: { left: 0, }, textStyle: { fontFamily: '"Helvetica Neue", "Microsoft YaHei"', }, }; const NAME_GAP = 52; class OptionGenerator { constructor(extraOption) { const me = this; extraOption = extraOption || {}; me._option = {}; _.merge(me._option, DEFAULT_OPTION); _.merge(me._option, util.parseExtraOption(extraOption)); this._configLayout(); this.originalData = this._option.originalData || []; } configCoordinates(coordinates, horizontal, extraOption) { const me = this; const option = me._option; extraOption = extraOption || {}; let dimensionAxis = 'xAxis'; let measuresAxis = 'yAxis'; if (horizontal) { const temp = measuresAxis; measuresAxis = dimensionAxis; dimensionAxis = temp; } this.measuresAxis = measuresAxis; this.dimensionAxis = dimensionAxis; option[dimensionAxis] = _.extend(option[dimensionAxis], { type: 'category', data: coordinates, axisLabel: { textStyle: { color: '#666', }, }, }, extraOption.category); if (extraOption.yAxis) { option[measuresAxis] = extraOption.yAxis; } else { option[measuresAxis] = [{ type: 'value' }]; } option[measuresAxis].forEach(item => { _.merge(item, { splitLine: { show: option[measuresAxis].length > 1 ? false : true, lineStyle: { color: '#f5f5f5', }, }, axisLine: { show: false }, nameTextStyle: { color: '#999', }, axisLabel: { textStyle: { color: '#666', }, }, nameLocation: 'middle', // start, middle, end nameGap: NAME_GAP, }); }); _.merge(option[measuresAxis][0], { name: option['data-yAxisName'] || '' }); if (_.isNumber(option['data-yAxisMax'])) { option[measuresAxis][0].max = option['data-yAxisMax']; } if (_.isNumber(option['data-yAxisMin'])) { option[measuresAxis][0].min = option['data-yAxisMin']; } if (_.isNumber(option['data-yAxisMaxRight']) && option[measuresAxis][1]) { option[measuresAxis][1].max = option['data-yAxisMaxRight']; } if (_.isNumber(option['data-yAxisMinRight']) && option[measuresAxis][1]) { option[measuresAxis][1].min = option['data-yAxisMinRight']; } if (option[measuresAxis][1]) { option[measuresAxis][1].name = option['data-yAxisNameRight'] || ''; } if (option['data-showDataZoomInside']) { option.dataZoom = [{ show: true, type: 'slider', start: option['data-dataZoomInsideStart'] || 0, end: option['data-dataZoomInsideEnd'] || 100, }]; this._configDataZoom(); } else { option.xAxis.axisTick = { alignWithLabel: true }; } return me; } configMeasures(type, measures, dataSet, extraSeriesOption) { const me = this; const option = me._option; option.series = []; _.each(measures, (measure) => { const colInfo = dataSet.colByName[measure]; const data = this.originalData.map(v => { if (colInfo.format && colInfo.format.includes('dividedBy') && (!colInfo.format.includes('suffix') || colInfo.format.match(/dividedBy/g).length > 1) ) { // 数据必须修改的情况:分 -> 元 rate变更 // 配合岛煮的format object化重新订正 const result = /dividedBy\(([0-9.]+)\)/.exec(colInfo.format); if (result) { const factor = Number.parseFloat(result[1]); if (_.isNull(v[measure])) { return null; } return v[measure] / factor; } } return _.isNull(v[measure]) ? '-' : v[measure]; }); const formatData = this._getFormatData(dataSet, measure); // 为了在 recharts.js 中通过 JSON 伪序列化之后,可以通过 update 判断,添加一个数据字段 // 因为 JSON.stringify(o) 会去除 function 字段 option._formatData = formatData; // TODO: y轴格式化 if (option[this.measuresAxis]) { if (!_.isArray(option[this.measuresAxis])) { option[this.measuresAxis] = [option[this.measuresAxis]] } option[this.measuresAxis] = option[this.measuresAxis].map((item, index) => ( _.merge(item, { axisLabel: { formatter: v => { let key; if (index === 0) { key = 'data-values'; } else if (index === 1) { key = 'data-valuesRight'; } const format = _.get(dataSet.colByName[_.get(option, `${key}.0`)], 'format'); if (_.includes(format, '%')) { return numeral(v).format('0.00%'); } if (option['data-percent']) { return numeral(v).format('0.00%'); } return formatNumber(v, '0,0[.]0', '0[.]0[0]'); }, }, }) )); } const yAxisIndex = (extraSeriesOption.measuresRight && (extraSeriesOption.measuresRight.indexOf(measure) > -1)) ? 1 : 0; const seriesType = _.isArray(type) ? type[yAxisIndex] : type; let position; switch (option['data-chartType']) { case 'lineBarMix': if (seriesType === 'line') { position = 'top'; } if (seriesType === 'bar') { position = 'inside'; } break; case 'line': case 'groupBar': position = 'top'; break; case 'stackedBar': position = 'inside'; break; case 'horizontalGroupBar': position = 'right'; break; case 'horizontalStackedBar': position = 'inside'; break; default: position = 'inside'; } option.series.push(_.extend({ name: colInfo.comments, type: seriesType, data, label: { normal: { show: option['data-showNormalLabel'] || false, position, formatter: option['data-showNormalLabel'] ? ( ({ dataIndex }) => formatData[dataIndex] ) : null, } }, areaStyle: { normal: { opacity: option['data-showAreaStyle'] ? 1 : 0 } }, yAxisIndex, smooth: option['data-smooth'] || false, }, extraSeriesOption)); if (data.length === 1) { // 只有一条数据时,显示一个点 option.series.forEach(item => { item.symbol = 'circle'; }); } if (option['data-hideSymbol']) { // 关闭圆点 option.series.forEach(item => { // item.symbol = 'image://'; item.symbolSize = 1; }); } }); option.tooltip.formatter = (measures => params => { /* { color, data/value, dataIndex, name, seriesName, } */ if (!_.isArray(params)) { params = [params]; } const valueArray = option.series.map(item => item.data); const zipArgs = [].concat(valueArray, (...args) => { return args.reduce((a, b) => a + b, 0) }); const totalValueArray = _.zipWith(...zipArgs); const content = params.map(param => { const displayName = param.seriesName; const measure = measures[param.seriesIndex]; const formatData = this._getFormatData(dataSet, measure); // 针对 pie 类型的图表,需要显示百分比 // 其他类型的百分比,是当前列数据中的百分比,含义不一样 const showPercentage = measures.length > 1; const percent = param.seriesType === 'pie' ? `(${param.percent}%)` : (showPercentage ? `(${(param.value / totalValueArray[param.dataIndex] * 100).toFixed(2)}%)` : ''); return ` <div style="height:20px;"> <span style="display:inline-block;border-radius:8px;width:8px;height:8px;background:${param.color};vertical-align:middle;"></span> <span style="display:inline-block;padding:0 5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle;">${displayName}:</span> <span style="display:inline-block;vertical-align:middle;">${formatData[param.dataIndex] || '-'}</span> <span style="display:inline-block;vertical-align:middle;">${percent}</span> </div> `; }); const name = params[0].name; return ` <div style="padding:5px;border-radius:5px;font-size:12px;"> <div style="font-size:14px;font-weight:700;">${name}</div> <div> ${content.join('')} </div> </div> `; })(measures); return me; } configLegend(colNames, dataSet) { const me = this; me._option.legend = me._option.legend ? me._option.legend : {}; _.merge(me._option.legend, { data: _.map(colNames, colName => dataSet.colByName[colName].comments), type: 'scroll', }); return me; } configLegendByData(data) { const me = this; me._option.legend = me._option.legend ? me._option.legend : {}; _.merge(me._option.legend, { data, }); return me; } toOption() { return this._option; } _getFormatData = (dataSet, measure) => { if (!dataSet || !measure) { return []; } const data = this.originalData.map(v => v[measure]); const measureSchema = _.find(dataSet.schema, { name: measure }); const formatData = measureSchema.format === 'auto' || _.isNull(measureSchema.format) ? data.map(v => formatNumber(v)) : dataSet.colValuesByName[measure]; return formatData; } // _yFormatter = v => formatNumber(v, '0,0[.]0', '0') _configDataZoom() { // if (!_.isArray(this._option.dataZoom) || !this._option['data-showDataZoomInside']) { // return; // } // else { // this._option.dataZoom = _.map(this._option.dataZoom, opt => { // let newOpt = _.assign({}, opt); // if (opt.type !== 'inside') { // // 由于dataZoom 会自适应grid,所以这里可以稍微根据自己的需要偏移 // // switch (this._option.padding) { // // case '': // // break; // // default: // // break; // // } // } // return newOpt; // }); // } } _configLayout() { switch (this._option.padding) { case 'large': this._option.grid = { left: 100, right: 100, bottom: 30, }; break; case 'small': this._option.grid = { left: 50, right: 50, bottom: 30, // containLabel: true, }; break; case 'none': this._option.grid = { left: 30, right: 30, bottom: 45, containLabel: true, }; break; default: break; } if (this._option.grid) { if (this._option.legend.show === false) { // 隐藏图例 this._option.grid.top = 10; } else { this._option.grid.top = 60; } } } } module.exports = OptionGenerator;