d2recharts
Version:
data driven react components of echarts
395 lines (378 loc) • 11.9 kB
JavaScript
'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;