d2recharts
Version:
data driven react components of echarts
370 lines (352 loc) • 11.2 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: 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\('(\S+)'\)/.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;