@huangjs888/d3-chart
Version:
Implement some charts based on d3 library.
345 lines (343 loc) • 12.2 kB
JavaScript
const _excluded = ["smooth", "data", "tooltip"],
_excluded2 = ["line"],
_excluded3 = ["data"],
_excluded4 = ["line"],
_excluded5 = ["data"];
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
// @ts-nocheck
/*
* @Author: Huangjs
* @Date: 2021-03-17 16:23:00
* @LastEditors: Huangjs
* @LastEditTime: 2023-08-21 13:34:05
* @Description: 默认LineGraph构造器
*/
import * as d3 from 'd3';
import BaseChart from '../BaseChart';
import * as util from '../util';
function lineLabel() {
const {
onlyOneMerge
} = this.tooltip;
const pWidth = onlyOneMerge ? 0 : 20;
const color = (v, c) => this.filter$.findIndex(f => f.key === v.key) !== -1 ? '#aaa' : c;
const width = v => util.measureSvgText(v.label, this.fontSize) + pWidth;
let tWidth = 0;
const zlabel = this.rootSelection$.select('.zLabel');
zlabel.selectAll('g').remove();
zlabel.selectAll('g').data(this.data.line).join(enter => {
const legend = enter.append('g').attr('class', 'line-legend').attr('transform', v => {
let moveX = 0;
const w = width(v) + 5;
if (onlyOneMerge) {
moveX = this.scale.y ? -((this.width$ + w) / 2) : (this.width$ - w) / 2;
} else {
moveX = this.scale.y ? 5 - tWidth - w : tWidth;
tWidth += w;
}
return `translate(${moveX},0)`;
});
if (!onlyOneMerge) {
legend.insert('path').attr('fill', 'none').attr('stroke-width', 4).attr('transform', `translate(0,${-this.fontSize / 2}),scale(0.5)`).attr('d', this.smooth === 1 ? 'M0 6 C4 0,8 0,12 6 S20 12,24 6' : 'M0 6,28 6').attr('stroke', v => color(v, v.color || 'inherit'));
}
legend.insert('text').attr('dx', pWidth).attr('text-anchor', 'start').attr('stroke-width', 0.5).text(v => v.label).attr('fill', v => color(v, 'currentColor'));
return legend;
}, update => update, exit => exit.remove());
const showData = util.differenceWith(this.data.line, this.filter$, (a, b) => a.key === b.key);
this.rootSelection$.select('.zAxis').selectAll('path').data(showData).join('path').attr('stroke', d => d.color || 'inherit').attr('stroke-width', 1.2);
}
function tipCompute(prevRes, point, scaleAxis) {
const ok = a => a === 0 || !!a;
const scaleOpt = this.scale;
const {
cross,
average,
onlyOneMerge
} = this.tooltip;
const crossX = cross.indexOf('x') !== -1;
const crossY = cross.indexOf('y') !== -1;
const xScale = scaleAxis.x && scaleAxis.x.scale;
const x2Scale = scaleAxis.x2 && scaleAxis.x2.scale;
const yScale = scaleAxis.y && scaleAxis.y.scale;
const y2Scale = scaleAxis.y2 && scaleAxis.y2.scale;
let [x0, y0] = point;
const xyValue = [];
const fData = util.differenceWith(this.data.line, this.filter$, (a, b) => a.key === b.key);
if (crossX) {
fData.forEach(({
data,
color,
label
}) => {
let xxKey = 'x';
let xxScale = xScale;
if (x2Scale && data.length > 0) {
if (ok(data[0].x2)) {
xxKey = 'x2';
xxScale = x2Scale;
} else if (!xScale) {
throw new Error('scale只配置了x2坐标,但数据中没有x2值');
}
}
let yyKey = 'y';
if (y2Scale && data.length > 0) {
if (ok(data[0].y2)) {
yyKey = 'y2';
} else if (!yScale) {
throw new Error('scale只配置了y2坐标,但数据中没有y2值');
}
}
let xval = xxScale.invert(x0);
let yval = null;
const [xi0, xi1] = util.findNearIndex(+xval, data.map(d => d[xxKey]));
if (xi0 >= 0 && xi1 >= 0) {
const xval0 = !data[xi0] ? 0 : data[xi0][xxKey];
const xval1 = !data[xi1] ? 0 : data[xi1][xxKey];
if (xval0 === xval1) {
// 正好移动到了数据点上
yval = !data[xi0] ? 0 : data[xi0][yyKey];
} else if (average) {
// 平均值显示
const valRate = (xval - xval0) / (xval1 - xval0);
yval = valRate * ((!data[xi1] ? 0 : data[xi1][yyKey]) - (!data[xi0] ? 0 : data[xi0][yyKey])) + (!data[xi0] ? 0 : data[xi0][yyKey]);
} else {
// 最近值显示
const xi = Math.abs(xval - xval0) > Math.abs(xval - xval1) ? xi1 : xi0;
xval = !data[xi] ? 0 : data[xi][xxKey];
yval = !data[xi] ? 0 : data[xi][yyKey];
x0 = xxScale(xval);
}
xyValue[0] = _extends({}, scaleOpt[xxKey], {
value: xval
});
xyValue.push(_extends({}, scaleOpt[yyKey], {
value: yval,
color
}, onlyOneMerge ? {} : {
label
}));
}
});
} else if (crossY) {
fData.forEach(({
data,
color,
label
}) => {
let yyKey = 'y';
let yyScale = yScale;
if (y2Scale && data.length > 0) {
if (ok(data[0].y2)) {
yyKey = 'y2';
yyScale = y2Scale;
} else if (!yScale) {
throw new Error('scale只配置了y2坐标,但数据中没有y2值');
}
}
let xxKey = 'x';
if (x2Scale && data.length > 0) {
if (ok(data[0].x2)) {
xxKey = 'x2';
} else if (!xScale) {
throw new Error('scale只配置了x2坐标,但数据中没有x2值');
}
}
let yval = yyScale.invert(y0);
let xval = null;
const [yi0, yi1] = util.findNearIndex(+yval, data.map(d => d[yyKey]));
if (yi0 >= 0 && yi1 >= 0) {
const yval0 = !data[yi0] ? 0 : data[yi0][yyKey];
const yval1 = !data[yi1] ? 0 : data[yi1][yyKey];
if (yval0 === yval1) {
// 正好移动到了数据点上
xval = !data[yi0] ? 0 : data[yi0][xxKey];
} else if (average) {
// 平均值显示
const valRate = (yval - yval0) / (yval1 - yval0);
xval = valRate * ((!data[yi1] ? 0 : data[yi1][xxKey]) - (!data[yi0] ? 0 : data[yi0][xxKey])) + (!data[yi0] ? 0 : data[yi0][xxKey]);
} else {
// 最近值显示
const yi = Math.abs(yval - yval0) > Math.abs(yval - yval1) ? yi1 : yi0;
yval = !data[yi] ? 0 : data[yi][yyKey];
xval = !data[yi] ? 0 : data[yi][xxKey];
y0 = yyScale(yval);
}
xyValue[0] = _extends({}, scaleOpt[yyKey], {
value: yval
});
xyValue.push(_extends({}, scaleOpt[xxKey], {
value: xval,
color
}, onlyOneMerge ? {} : {
label
}));
}
});
}
if (xyValue.length > 0) {
// 去掉第一个因为第一个是标题
const prevValue = (prevRes.data || []).slice(1);
const data = [...xyValue, ...prevValue];
const result = selection => {
selection.selectAll('div').data(data).join('div').attr('style', 'white-space: nowrap;').html((d, i) => i === 0 ? `${d.label ? `${d.label}: ` : ''}${d.format(d.value)}${d.unit || ''}` : `${onlyOneMerge || !d.color ? '' : `<span style="background: ${d.color}; width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 8px;"></span>`}<span>${d.label ? `${d.label}: ` : ''}</span><span style="display: inline-block; ${onlyOneMerge || !d.color ? '' : 'margin-left: 30px;'}">${d.format(d.value)}${d.unit || ''}</span>`);
};
return {
x0,
y0,
data,
result
};
}
return _extends({}, prevRes);
}
function updateLine() {
let curve = d3.curveLinear; // 折线
if (this.smooth >= 1) {
curve = d3.curveBasis; // 平滑曲线
} else if (this.smooth < 1 && this.smooth > 0) {
curve = d3.curveBundle.beta(this.smooth); // 平滑度为0.8曲线
}
this.line$.curve(curve);
}
class LineGraph extends BaseChart {
constructor(...params) {
const _ref = params[0] || {},
{
smooth,
data,
tooltip
} = _ref,
restOptions = _objectWithoutPropertiesLoose(_ref, _excluded);
const _ref2 = data || {},
{
line
} = _ref2,
restData = _objectWithoutPropertiesLoose(_ref2, _excluded2);
super(_extends({}, restOptions, {
data: _extends({
line: (line || []).map(_ref3 => {
let {
data: xy
} = _ref3,
restLine = _objectWithoutPropertiesLoose(_ref3, _excluded3);
return _extends({
data: xy || []
}, restLine);
})
}, restData),
tooltip: !tooltip ? false : _extends({
cross: 'x'
}, tooltip, {
compute: (res, ...args) => {
let result = tipCompute.call(this, res, ...args);
if (typeof tooltip.compute === 'function') {
result = tooltip.compute.call(this, result, ...args);
}
return result;
}
})
}));
this.smooth = smooth || 0;
this.line$ = d3.line();
updateLine.call(this);
this.filter$ = [];
const zooming$$ = this.zooming$;
this.zooming$ = (e, ...args) => {
zooming$$.call(null, e, ...args);
const {
scaleAxis
} = e;
const xScale = (scaleAxis.x || scaleAxis.x2).scale;
const x2Scale = (scaleAxis.x2 || scaleAxis.x).scale;
const yScale = (scaleAxis.y || scaleAxis.y2).scale;
const y2Scale = (scaleAxis.y2 || scaleAxis.y).scale;
this.rootSelection$.select('.zAxis')
// .selectChildren() // ie11及以下不支持SVGSVGElement.children(其实可以SVGSVGElement.childNodes,但是d3未做兼容)
.selectAll('path').attr('d', d => this.line$.x(({
x,
x2
}) => typeof x === 'undefined' ? x2Scale(x2 || 0) : xScale(x || 0)).y(({
y,
y2
}) => typeof y === 'undefined' ? y2Scale(y2 || 0) : yScale(y || 0))(d.data));
};
this.rootSelection$.select('.zLabel').on('click', e => {
if (this.destroyed) return;
const datum = d3.select(e.target).datum();
const index = this.filter$.findIndex(f => f.key === datum.key);
if (index === -1) {
this.filter$.push(datum);
} else {
this.filter$.splice(index, 1);
}
lineLabel.call(this);
if (this.rendered) {
this.render();
}
});
lineLabel.call(this);
}
setData(data, render, computeDomain) {
if (!data) return this;
const {
line
} = data,
restData = _objectWithoutPropertiesLoose(data, _excluded4);
super.setData(_extends({
line: (line || []).map(_ref4 => {
let {
data: xy
} = _ref4,
restLine = _objectWithoutPropertiesLoose(_ref4, _excluded5);
return _extends({
data: xy || []
}, restLine);
})
}, restData), false, !line ? computeDomain : ({
line: lineData
}, needDomain) => {
const domains = {};
lineData.forEach(rd => {
const xy = {};
rd.data.forEach(dc => {
Object.keys(dc).forEach(k => {
if (!xy[k]) xy[k] = [];
xy[k].push(dc[k]);
});
});
needDomain.forEach(key => {
if (xy[key] && xy[key].length > 0) {
domains[key] = d3.extent([...xy[key], ...(domains[key] || [])]);
}
});
});
return domains;
});
if (this.data.line.length !== 1) {
this.tooltip.onlyOneMerge = false;
}
lineLabel.call(this);
if (render) {
this.render();
}
return this;
}
setSmooth(smooth, render) {
this.smooth = smooth;
this.rendered = false;
updateLine.call(this);
if (render) {
this.render();
}
return this;
}
destroy() {
this.line$ = null;
this.filter$ = null;
this.zooming$ = null;
super.destroy();
return this;
}
}
export default LineGraph;