UNPKG

d2recharts

Version:

data driven react components of echarts

370 lines (352 loc) 11.2 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: constant.DEFAULT_THEME_NAME, 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; } 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.xAxis, this._configXLabel()); _.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['data-showDataZoomInside']) { option.dataZoom = [{ show: true, type: 'slider', start: _.isPlainObject(option['data-dataZoomInside']) ? option['data-dataZoomInside'].start : 0, end: _.isPlainObject(option['data-dataZoomInside']) ? option['data-dataZoomInside'].end : 100 }]; } 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.yAxis) { if (!_.isArray(option.yAxis)) { option.yAxis = [option.yAxis] } option.yAxis = option.yAxis.map((item, index) => ( _.merge(item, { axisLabel: { formatter: v => { if (option['data-yAxisPercentageFormat'] && index === 0) { // Y 轴特殊百分比 return numeral(v).format('0.00%'); } if (option['data-yAxisPercentageFormatRight'] && index === 1) { // Y 轴特殊百分比 return numeral(v).format('0.00%'); } // const label = formatNumber(v, '0,0[.]0', '0[.]0[0]'); // let suffix = ''; // if (colInfo.format && _.includes(colInfo.format, 'suffix')) { // // formatter添加后缀 // const result = /suffix\(&apos;(\S+)&apos;\)/.exec(colInfo.format); // if (result) { // suffix = result[1]; // } // } return formatNumber(v, '0,0[.]0', '0[.]0[0]'); }, }, }) )); } const yAxisIndex = (extraSeriesOption.measuresRight && (extraSeriesOption.measuresRight.indexOf(measure) > -1)) ? 1 : 0; option.series.push(_.extend({ name: colInfo.comments, type: _.isArray(type) ? type[yAxisIndex] : type, data, label: { normal: { show: option['data-showNormalLabel'] || false, 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://'; }); } }); option.tooltip.formatter = (measures => params => { /* { color, data/value, dataIndex, name, seriesName, } */ if (!_.isArray(params)) { params = [params]; } const content = params.map(param => { const displayName = param.seriesName; const measure = measures[param.seriesIndex]; const formatData = this._getFormatData(dataSet, measure); 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> </div> `; }); return ` <div style="padding:5px;border-radius:5px;font-size:12px;"> <div style="font-size:14px;font-weight:700;">${params[0].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) }); 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) => { 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') _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': if (this._option['data-yAxisName']) { this._option.grid = { left: NAME_GAP + 12, // 52 + 12 right: 5, bottom: 42, }; } else { this._option.grid = { left: 10, // 露出最后一个刻度 right: 5, bottom: 30, containLabel: true, }; } break; default: break; } if (this._option.legend.show === false) { // 隐藏图例 this._option.grid.top = 10; } else { this._option.grid.top = 60; } } _configXLabel() { const lineWidth = 78; return { axisLabel: { formatter: (v) => { const defaultWidth = util.guessTextWidth(v); if (defaultWidth > lineWidth) { const guessLen = Math.floor(lineWidth / 12); let start = guessLen; let first = v.toString().substr(0, start); while (util.guessTextWidth(`${first}...`) <= lineWidth) { first += v[start]; start++; } return `${first}...`; // while (util.guessTextWidth(first) <= lineWidth) { // first += v[start]; // start++; // } // let second = v.slice(start); // if (defaultWidth > lineWidth * 2) { // second = v.substr(start, guessLen); // while (util.guessTextWidth(`${second}...`) <= lineWidth) { // second += v[start]; // start++; // } // second += '...'; // } // return `${first}\n${second}`; } return v; }, }, }; } } module.exports = OptionGenerator;