datavizstocklib
Version:
测试一下呗
1,241 lines (1,118 loc) • 51.1 kB
JavaScript
import * as d3 from "d3";
let config = {
/** 控件名 动态折线图 */
name: "DynamicMultiLine",
/** svg 对象 */
svg: {},
/** 展示类型 Dynamic Fixed Custom */
scaleType: "Dynamic",
/** 数据 */
data: [],
/** 内部使用 */
jsonData: {},
/** 内部使用 当前展示的数据 */
currentData: [],
/** 表头名集合 */
columnName: [],
/** 图表默认高度 */
clientHeight: 500,
/** 图表默认宽度 */
clientWidth: 0,
/** 升序或降序 asc desc */
sort: "desc",
/** Y轴比例尺 */
yScale: {},
/** X轴比例尺 */
xScale: {},
/** 边距设置 上,右,下,左 */
margin: [60, 100, 60, 100],
/** colors */
colors: ["#5E4FA2", "#3288BD", "#66C2A5", "#ABDDA4", "#E6F598", "#FFFFBF", "#FEE08B", "#FDAE61", "#F46D43", "#D53E4F", "#9E0142"],
/** 最大文字的字号 */
fontsize: 16, //字号
/**Y轴文字字号 */
YfontSize: 14,
/**X轴文字字号 */
XfontSize: 14,
/** 字体 */
fontfamilyBase: "SourceHanSansK-Regular-plotly,Helvetica Neue,PingFang SC,Microsoft YaHei,Helvetica,Hiragino Sans GB,Arial,sans-serif",
/** 文字颜色 */
rectTextColor: "#A3A3A3",
/** 柱高度自适应 */
rectHeightFixed: false,
/** 柱高度和 柱间距的比例 */
rectRate: 5,
/** 动画时长 */
durationTime: 200,
/** 最大值 */
maxValue: 0,
height: 35,
replay: false,
/** 矩形圆角 */
rx: 2,
index: 0,
dataIndex: 0,
timeout: null,
rankingFontsize: 16,
yTicks: 4,
xSicks: 0,
inn_line_path: [],
inn_line_length: [],
showThousands: true,
pointFormat: 'auto',
delayRate: 0.75,
minValue: 0,
maxValue: -999999,
/**初始刻度个数 */
initNumber: 5,
//当前进度
process: 0,
t: 0,
concentrations: '',
clipPathGridDef: '',
legendFollow: true,
dynamicImages: [],
radius: 25,
el: '_dataviz',
d3Ease: ''
}
String.prototype.replaceAll = function (s1, s2) {
return this.replace(new RegExp(s1, "gm"), s2);
}
export class DynamicMultiLine {
/** 构造器 */
constructor() {
let newconfig = JSON.parse(JSON.stringify(config));
Object.assign(this, newconfig)
}
/** 更新图表 */
update(obj) { // 复制新配置
Object.assign(this, obj)
}
/** 重新播放 */
replay() { }
/** 播放 */
play() { }
/** 暂停 */
pause() { }
/** 初始化 */
init(obj, callback) {
let that = this;
Object.assign(this, obj);
this.index = 2;
this.getEase();
that.arrayToJson();
that.svg = d3.select("#" + this.el).append("svg");
that.svg.append("g").attr("class", "bcrlegend");
//添加竖线
that.svg.append("g")
.attr("class", "grid")
.style("color", "#A3A3A3")
.style("font-size", "14")
that.clipPathGridDef = that.svg.append('defs').append("clipPath").attr("id", "clipPathGrid")
if (!that.showAxisY) that.margin[3] = that.margin[3] - 30
if (this.clientWidth > this.clientHeight) that.margin[1] = that.margin[1] + 50
that.svg.append("g")
.attr('clip-path', 'url(#clipPathGrid)')
.append('g')
.attr("class", "xaxis")
.style("color", "#A3A3A3")
.style("font-size", "14")
.attr('transform', 'translate(' + that.margin[3] + ',0)')
// let addtransX = 30
// if (that.branchType == 'rank') addtransX = 0
that.clipPathDef = that.svg.append('defs').append("clipPath").attr("id", "clipPath").append('rect')
.attr('x', that.margin[3])
.attr('y', 0)
.attr('width', 0)
.attr('height', that.clientHeight)
if (that.clientWidth == 0) {
that.clientWidth = document.getElementById(this.el).clientWidth;
}
that.svg.attr("width", that.clientWidth).attr("height", that.clientHeight);
that.concentrations = that.columnName.map(function (category) {
return {
category: category,
datapoints: that.jsonData.map(function (d) {
return { date: d[that.data[0][0]], concentration: d[category] };
})
};
});
if (that.branchType == 'rank') that.yTicks = that.concentrations.length - 1
if (that.branchType == 'rank') that.minValue = 1
// 获取数据初始initNumber 与 传入数据的长度最小的那个,作为截取初始数据使用。
// 更新process的进度
that.process = Math.min(that.initNumber, that.jsonData.length)
// if (that.initNumber > that.jsonData.length) that.initNumber = that.jsonData.length
let minData = 0
for (let j = 0; j < that.concentrations.length; j++) {
// that.concentrations[j]['datapoints'] 内容为:{concentration: 467, date: "2012"} 组成的数组
let maxTemp = d3.max(that.concentrations[j]['datapoints'].slice(0, that.process), function (d) { return d['concentration'] });
let minTemp = d3.min(that.concentrations[j]['datapoints'].slice(0, that.process), function (d) { return d['concentration'] });
if (!that.showAxisYZero) {
if (j == 0 || minData > minTemp) minData = minTemp
that.minValue = that.minValue > minData ? that.minValue : minData * 0.8;
} else {
that.minValue = that.minValue > minTemp ? minTemp : that.minValue;
}
that.maxValue = that.maxValue > maxTemp ? that.maxValue : maxTemp;
}
that.width = that.clientWidth - this.margin[1] - this.margin[3];
that.yScale = d3.scaleLinear().range([that.width, that.margin[3]]); // 界面像素范围
that.yScale.domain([that.minValue, that.maxValue]); //数据范围
that.bar = that.svg.append("g");
this.dataIndex = this.jsonData.length + 1
that.tmpData = [];
//that.updateChart(callback);
that.svg.append('defs').append("clipPath").attr("id", 'clipImage043').attr('r', that.radius).append("circle").attr('r', that.radius - 2)
// 获取x轴中的最大值,和2判断找到最大值是因为最后会添加“累计”的信息
that.maxXSize = Math.max(2, d3.max(that.xNames, (d) => {
return d.length
}))
setTimeout(function () {
that.updateChart(callback);
}, 1000)
}
getEase() {
this.d3Ease = d3;
this.animateType.split(".").forEach(item => {
if (item.indexOf("(") > -1) {
let arr = item.split("(")
this.d3Ease = this.d3Ease[arr[0]](arr[1].replace(")", ""))
} else {
this.d3Ease = this.d3Ease[item]
}
})
}
// 计算偏移量
// 防止遮挡和溢出顶部
collid(nodes) {
function ascendingDepth(a, b) {
return a.y - b.y;
}
let that = this
let height = that.clientHeight - this.margin[0] - this.margin[2];
var bottom = height + that.rankingFontsize / 4, // height微调,可以不加
nodePadding = 0,
node,
dy,
y0 = 0,
n = nodes.length,
i;
// Push any overlapping nodes down.
nodes.sort(ascendingDepth);
for (i = 0; i < n; ++i) {
node = nodes[i];
dy = y0 - node.y;
node.textOffset = 0;
if (dy > 0) {
node.y += dy;
node.textOffset = dy
}
y0 = node.y + node.dy + nodePadding;
}
// If the bottommost node goes outside the bounds, push it back up.
dy = y0 - nodePadding - bottom;
if (dy > 0) {
y0 = node.y -= dy;
node.textOffset -= dy
// Push any overlapping nodes back up.
for (i = n - 2; i >= 0; --i) {
node = nodes[i];
dy = node.y + node.dy + nodePadding - y0;
if (dy > 0) {
node.textOffset -= dy
node.y -= dy;
}
y0 = node.y;
}
}
}
updateChart(callback, lastflag) {
let that = this;
this.getEase();
that.arrayToJson();
that.columnMap = that.firstcolumnMap();
if (that.initNumber > that.jsonData.length) that.initNumber = that.jsonData.length
let lineChartWidth = that.width * (that.jsonData.length - 1) / (that.initNumber - 1)
let singleDataWidth = lineChartWidth / that.jsonData.length;
let currentData = that.jsonData;
// console.log("数据长度:" + currentData.length + " 索引:" + that.index + " 时间跨度:" + that.barNumber);
if (currentData.length == 0) {
return;
}
if (lastflag) { //最后一帧
currentData = that.jsonData;
}
that.svg.attr("width", that.clientWidth).attr("height", that.clientHeight);
let uirange = [];
uirange.push(that.margin[3]);
uirange.push(that.clientWidth - that.margin[1]);
that.xScale = d3.scaleLinear().range([0, lineChartWidth]).nice(); // 界面像素范围
that.xScale.domain([0, that.jsonData.length - 1]); //数据范围
let height = that.clientHeight - this.margin[0] - this.margin[2];
that.yScale = d3.scaleLinear().range([height, that.margin[2]]); // 界面像素范围
if (that.branchType == 'rank') that.yScale = d3.scaleLinear().range([that.margin[2], height]);
that.yScale.domain([that.minValue, that.maxValue * 1.1]); //数据范围
let yScale = that.yScale;
let xScale = that.xScale;
let zoomTimes = 1
while (that.maxXSize * that.XfontSize > (xScale(1) - xScale(0)) * zoomTimes * 1.5) {
zoomTimes *= 2
}
//设置遮罩的宽度和位置
that.clipPathDef.attr('x', that.margin[3])
.attr('y', 0)
.attr('width', 0) //lineChartWidth * 2)
.attr('height', that.clientHeight)
this.jsonData.slice(0, this.initNumber + 1).forEach(function (d) {
var fontsize = that.rankingFontsize;
var nodes = that.columnName.map(function (c) {
return {
c: c,
dy: fontsize,
originY: yScale(d[c]),
y: yScale(d[c]) - fontsize / 2
};
});
that.collid(nodes);
nodes.forEach(function (node) {
d[node.c + 'L'] = node.y + fontsize / 2;
d[node.c + 'Offset'] = node.textOffset
});
});
// 添加 x 轴坐标 // 获取刻度文本
let title = that.getTextData();
that.xSicks = that.jsonData.length
let lineLegendoffsetX = that.margin[3] + 20;
let lineLegendoffsetY = 8
let lineLegendoffsetY_phone = 0
if (that.clientWidth <= that.clientHeight) {
lineLegendoffsetX = lineLegendoffsetX - 70
lineLegendoffsetY = -15
that.xSicks = that.xSicks / 2
lineLegendoffsetY_phone = -30
}
that.xtickScale = d3.axisBottom()
.scale(that.xScale)
.tickSize(that.clientHeight - that.margin[0] - that.margin[2], 0, 0)
.ticks(that.xNames.length < 4 ? that.xNames.length - 1 : that.xNames.length)
.tickPadding(12 + that.xLabelOffsetY)
.tickFormat((d, i) => {
// 根据字体大小和文本长度,计算x轴显示与否,第一个最后一个永远都会显示出来
// 如果在zoomTime不是1的情况下与最后一个发生冲突的节点不会显示x轴文字
if (zoomTimes > 1) {
// 向左查找可能会重叠的节点文字,设置为不显示
let tempZoomTimes = zoomTimes
let tempIndex = that.xNames.length - 1
// 从最后一个往左查找,每一个不符合要求的返回false,即不绘制
while (tempIndex - 1 >= 0 && tempZoomTimes > 1) {
if (i == tempIndex - 1) {
return ''
}
tempZoomTimes--
tempIndex--
}
}
// 常规情况下每隔zoomTimes个取一个,或者若为最后一个则一定显示
if (!(i % zoomTimes) || i == that.xNames.length - 1) {
return that.xNames[i]
} else {
return ''
}
});
// 更新yAxis
let gridHeight = yScale(that.maxValue) - yScale(0) - that.clientHeight;
let yTickSize = that.width + that.margin[1] * 2 / 3
// if (that.showAxisY) {
// let x1 = that.xScale(0);
// let y1 = that.yScale(0) - (height - that.margin[2]);
// let x2 = x1;
// let y2 = that.yScale(0);
// console.log("x1=" + x1 + " y1=" + y1 + " x2=" + x2 + " y2=" + y2)
// that.svg.select(".grid").append("line")
// .attr("x1", x1)
// .attr("y1", y1)
// .attr("x2", x2)
// .attr("y2", y2)
// .attr("stroke", "currentColor")
// .attr("opacity", 0.2)
// }
if (that.branchType == 'rank') gridHeight = -1150
that.gridscale = d3.axisLeft()
.scale(that.yScale)
.ticks(that.yTicks)
.tickSize(-yTickSize)
.tickFormat(d => {
if (that.branchType == 'rank' && d > that.concentrations.length) {
return ''
}
return that.format(d);
});
// 更新绘制曲线的方法
var line = d3.line()
.x(function (d, i) {
return xScale(that.columnMap[d['date']]);
})
.y(function (d) {
return yScale(parseFloat(d["concentration"]));
})
// 是否添加插值方式
if (that.curve) line.curve(d3.curveMonotoneX)
// 设置获取颜色的方法
var color = d3.scaleOrdinal().range(that.colors);
color.domain(that.columnName);
// 绑定数据
var binding = that.bar.selectAll('g')
.data(that.concentrations, function (d) {
return JSON.stringify(d);
});
//补充新的元素
let enterG = binding.enter();
that.clipPathGridDef.append('rect')
.attr('x', that.margin[3] - 60)
.attr('y', 130)
.attr('width', that.width + that.margin[3] + 60) //lineChartWidth * 2)
.attr('height', that.clientHeight)
let linesContainer = enterG.append('g')
.attr('clip-path', 'url(#clipPath)');
// 添加曲线
let lines = linesContainer.append("path")
.attr("class", "line path-original")
.attr("id", function (d, i) {
return 'path-original' + i
})
.attr("fill", "none")
.attr("stroke-width", that.lineWidth)
.attr("stroke", function (d) {
return color(d.category);
})
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr('transform', 'translate(' + that.margin[3] + ',0)')
.attr("d", function (d, i) {
return line(d.datapoints);
})
// 添加图标
let circleR = that.lineWidth * 1.5
let circles = enterG.append("circle")
.attr("class", function (d, i) { return 'circle'; })
.attr("id", function (d, i) {
return 'circle' + i
})
.attr('cx', function (d) { return 0; })
.attr('cy', function (d) { return 0 }) //Math.random() * h;
.attr('r', function (d, i) {
if (that.dynamicImages[i]) return 0
return circleR;
})
.attr('transform', function (d) { return 'translate(' + that.margin[3] + ', ' + 0 + ')'; })
.attr("fill", function (d) {
return color(d.category);
})
let size = that.radius * 2
let offsetX = that.margin[3]
let images = enterG.append("g")
.attr("id", function (d, i) {
return 'images' + i
})
.attr('transform', function (d) { return 'translate(' + (that.margin[3] + offsetX) + ', ' + 0 + ')' })
images.append("circle").attr('r', function (d, i) {
if (!that.dynamicImages[i] || that.iconShape == 'rect') return 0
return that.radius;
}).attr("fill", function (d) {
return color(d.category);
})
let imageG = images.append('g').attr('class', 'images-g')
imageG.append("image")
.attr("preserveAspectRatio", "xMidYMid meet")
.attr("x", function (d, i) {
return -size / 2 //-(that.dynamicImages[i]['width'] / 2.6) / 2
})
.attr("y", function (d, i) {
return -size / 2 //-(that.dynamicImages[i]['height'] / 2.6) / 2
})
.attr("width", function (d, i) {
if (!that.dynamicImages[i]) return 0
return size //that.dynamicImages[i]['width'] / 2.6
})
.attr("height", function (d, i) {
if (!that.dynamicImages[i]) return 0
return size //that.dynamicImages[i]['height'] / 2.6
})
.attr('xlink:href', function (d, i) {
if (that.dynamicImages[i]) {
return that.dynamicImages[i]
} else {
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFgAAABVCAYAAADNCyPqAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAFiUAABYlAUlSJPAAAADGSURBVHhe7dAxAQAwDMCg+ffctSr4cmCA92c2TsFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjBWMFYwVjB1OwBeFHfxoh6bUcAAAAASUVORK5CYII="
}
})
.attr('clip-path', function (d, i) {
if (that.iconShape == 'rect') return ''
return 'url(#clipImage043)';
})
let lineLegend = ''
let lineLegendContainer = enterG.append('g')
lineLegend = lineLegendContainer.append("text")
.attr("id", function (d, i) {
return "lineLegend" + i
})
.attr('x', function (d) { return 0; })
.attr('y', function (d) { return 0 }) //Math.random() * h;
.attr('transform', function (d) { return 'translate(' + 0 + ', ' + -15 + ')'; })
.attr("fill", function (d) {
return color(d.category);
})
.attr('font-size', that.fontsize + 'px')
binding.exit().transition().duration(0).attr("opacity", 0.0001).remove();
// grid -> y轴
that.svg.select(".grid")
.attr('transform', 'translate(' + that.margin[3] + ',0)')
.transition()
.duration(0)
.call(that.gridscale)
.ease(that.d3Ease)
let xaxis = that.svg.select(".xaxis")
.attr('transform', 'translate(' + that.margin[3] + ',0)')
.call(that.xtickScale)
that.svg.select(".grid").selectAll("Text")
.attr("fill", that.fontColor).attr("fill-opacity", that.showAxisY ? 1 : 0.0001).attr("font-size", that.YfontSize);;
that.svg.select(".xaxis").selectAll("Text")
.attr("fill", that.fontColor).attr("fill-opacity", 1).attr("font-size", that.XfontSize);;
that.svg.select(".grid").selectAll("line").attr("opacity", 0.2);
that.svg.select(".xaxis").selectAll("line").attr("opacity", 0.0001);
that.svg.select(".grid").selectAll("path").remove();
that.svg.select(".xaxis").selectAll("path").remove();
// 如果是曲线,执行如下的动画更新点和图片位置
if (that.curve) {
// 曲线的话就更新曲线数据,防止曲线过长印象效果
lines
// .selectAll(".path-original")
.attr("d", function (d, i) {
let data = d.datapoints.slice(0, that.initNumber + 2)
return line(data);
})
lines
.transition()
.duration(that.initNumber * that.durationTime)
.ease(that.d3Ease)
.tween('transMarker', function (d, i) {
let node = linesContainer.select(`#path-original${i}`).node()
let length = lineChartWidth
const l = node.getTotalLength()
return function (t) {
t = t * (that.initNumber - 1) / (that.jsonData.length - 1)
// const p = node.getPointAtLength(t * l)
let headY = 0
let y = findY(node, l, t * length, length)
headY = y
that.svg.select('#circle' + i)
.attr('cy', function () {
return headY
})
.attr('cx', t * lineChartWidth)
that.svg.select('#images' + i)
.attr('transform', function () {
let x = t * lineChartWidth + offsetX
return "translate(" + x + "," + headY + ")"
});
}
})
}
that.clipPathDef.transition()
.duration(that.initNumber * that.durationTime)
.ease(that.d3Ease)
.tween('getT', function () {
return function (t) {
that.t = t * (that.initNumber - 1) / (that.jsonData.length - 1)
if (!that.curve) {
circles
.attr('cx', that.t * lineChartWidth)
.attr('cy', function (d) {
let y = getValue(that.t, d)
return yScale(y)
});
images.attr('transform', function (d, i) {
let x = that.t * lineChartWidth + offsetX
let y = getValue(that.t, d)
return "translate(" + x + "," + yScale(y) + ")"
});
}
lineLegend.attr('transform', function (d, i) {
let y = getValue(that.t, d, 'legend');
let addX = 0
if (that.dynamicImages[i]) {
addX = 40
lineLegendoffsetY = lineLegendoffsetY_phone
}
if (that.branchType == 'rank') addX = 36
return 'translate(' + (that.t * lineChartWidth + lineLegendoffsetX + addX) + ', ' +
(y + lineLegendoffsetY) + ')';
}).text(function (d) {
let index = parseInt(t * (that.initNumber - 1))
let v = ''
if (that.interpolate) {
let nextIndex = index + 1
if (index >= that.jsonData.length) index = that.jsonData.length
if (nextIndex >= that.jsonData.length) nextIndex = index
let i = d3.interpolate(d.datapoints[index].concentration, d.datapoints[nextIndex].concentration);
// console.log(that.format(i(t), d.datapoints[nextIndex].concentration))
if (that.legendFollow) v = ' : ' + that.format(i(t), d.datapoints[nextIndex].concentration)
} else {
if (that.legendFollow) v = ' : ' + d.datapoints[index].concentration
}
return d['category'] + v;
})
.attr("text-anchor", "start")
};
})
.attr('width', that.width)
.on("end", () => {
if (that.initNumber < that.jsonData.length) {
// 遮罩层完全隐藏后,跑剩余的数据
runAnimate()
}
})
// 初屏出现之后,改为根据一条数据进行更新视图
function runAnimate(type) {
// 更新最大值与最小值,同时更新yScale
let localMaxTemp
for (let j = 0; j < that.concentrations.length; j++) {
// that.concentrations[j]['datapoints'] 内容为:{concentration: 467, date: "2012"} 组成的数组
let maxTemp = d3.max(that.concentrations[j]['datapoints'].slice(that.process - that.initNumber, that.process + 1), function (d) { return d['concentration'] });
let minTemp = d3.min(that.concentrations[j]['datapoints'].slice(that.process - that.initNumber, that.process + 1), function (d) { return d['concentration'] });
that.minValue = that.minValue > minTemp ? minTemp : that.minValue;
that.maxValue = that.maxValue > maxTemp ? that.maxValue : maxTemp;
localMaxTemp = localMaxTemp > maxTemp ? localMaxTemp : maxTemp;
}
updateLineAndMarker(localMaxTemp, type)
}
//iteratively search a path to get a point close to a desired x coordinate
function findY(path, pathLength, x, width) {
x = x + path.getPointAtLength(0).x
const accuracy = 0.1 //px
const iterations = Math.ceil(Math.log10(accuracy / width) / Math.log10(0.5)); //for width (w), get the # iterations to get to the desired accuracy, generally 1px
let i = 0;
let nextLengthChange = pathLength / 2;
let nextLength = pathLength / 2;
let y = 0;
for (i; i < iterations; i++) {
let pos = path.getPointAtLength(nextLength)
y = pos.y
nextLength = x < pos.x ? nextLength - nextLengthChange : nextLength + nextLengthChange
nextLengthChange = nextLengthChange / 2
}
return y
}
// 获取曲线curve
function getCurevPath(path) {
// 如果是结尾处就直接返回
let pathArr = path.split('C')
if (pathArr.length < 3) {
return path
}
let startPoints = pathArr[1].split(",");
return `M${startPoints[startPoints.length - 2]},${startPoints[startPoints.length - 1]}C${pathArr[2]}`
}
// 更新节点和曲线,采用icon沿着曲线运动的方法。
function updateLineAndMarker(localMaxTemp, type) {
// 修改y scale更新数据
let oldYScale = d3.scaleLinear()
.range(that.yScale.range())
.domain(that.yScale.domain())
if (type == "ending") {
oldYScale = d3.scaleLinear()
.range(that.lastYScale.range())
.domain(that.lastYScale.domain())
}
// 更新yScale的坐标轴
if (type !== "ending") {
that.yScale.domain([that.minValue, localMaxTemp * 1.1]);
} else {
that.yScale.domain([that.minValue, that.maxValue * 1.1]);
}
// 更新绘制曲线的方法
var lineSeg = d3.line()
.x(function (d, i) {
return that.xScale(that.columnMap[d['date']]);
})
.y(function (d) {
return oldYScale(parseFloat(d["concentration"]));
})
.defined((d, i, data) => {
if (type !== "ending") {
// 只是绘制当前点和下一个点,弧线前一个点和后一个点都需要截取出来
let preNum = that.curve && i >= 2 ? 2 : 1
let afterNum = that.curve ? 1 : 0
if (i >= that.process - preNum && i <= that.process + afterNum) {
return true
} else {
return false
}
} else {
if (i === data.length - 1) {
return true
} else {
return false
}
}
})
// 是否添加插值方式
if (that.curve) {
lineSeg.curve(d3.curveMonotoneX)
}
// 添加轨迹曲线
if (that.process == that.initNumber || that.pathLines == undefined) {
// 如果没有轨迹曲线就添加
that.pathLines = linesContainer.append("path")
.attr("id", (d, i) => `path-line-${i}`)
.attr("class", "line")
.attr("fill", "none")
// .attr("stroke-width", that.lineWidth)
// .attr("stroke", function (d) {
// return "black";
// })
.attr('transform', 'translate(' + that.margin[3] + ',0)')
.attr("d", function (d, i) {
if (that.curve) {
return getCurevPath(lineSeg(d.datapoints));
} else {
return lineSeg(d.datapoints);
}
})
} else {
// 如果已有轨迹曲线就更新位置
that.pathLines.attr("d", function (d, i) {
if (that.curve) {
return getCurevPath(lineSeg(d.datapoints));
} else {
return lineSeg(d.datapoints);
}
})
}
// 更新Y坐标轴
that.gridscale.scale(that.yScale)
// 更新数据的遮挡问题
updateCol()
// 更新y轴
that.svg.select(".grid")
.attr('transform', 'translate(' + that.margin[3] + ',0)')
.transition()
.duration(that.durationTime)
.call(that.gridscale)
.ease(that.d3Ease)
// 需要删除坐标轴往下 往右的两条path
that.svg.select(".grid").selectAll("Text")
.attr("fill", that.fontColor)
.attr("fill-opacity", that.showAxisY ? 1 : 0.0001)
.attr("font-size", that.YfontSize);
that.svg.select(".grid").selectAll("line").attr("opacity", 0.2);
that.svg.select(".grid").selectAll("path").remove();
// 更新坐标轴和曲线的水平移动
let transX = 0
// 计算lineChartWidth 的方法为 that.width * that.jsonData.length / that.initNumber
// width = 图表宽度 - 左边距 - 右边距
// 现在修改为按照进度每次移动一段距离,原始为一次性全部移动过去
if ("ending" !== type) {
transX = -(that.process - that.initNumber + 1) / (that.jsonData.length - 1) * lineChartWidth + that.margin[3]
} else {
transX = -(that.process - that.initNumber) / (that.jsonData.length - 1) * lineChartWidth + that.margin[3]
}
// 更新y坐标轴、线段的压缩尺度、icon的位置等信息
// 更新片段的y尺度
lineSeg.y(function (d) {
return that.yScale(parseFloat(d["concentration"]));
})
// 更新轨迹曲线,使其更加贴合原始的运动曲线
that.pathLines.transition()
.duration(that.durationTime)
.ease(that.d3Ease)
.attr("transform", function () {
return "translate( " + transX + ",0)";
})
.attr("d", d => {
// 更新轨迹曲线
if (that.curve) {
return getCurevPath(lineSeg(d.datapoints));
} else {
return lineSeg(d.datapoints);
}
})
.tween('changeLegend', function (d, i) {
// 根据轨迹曲线的坐标,来更新icon的位置
let node = linesContainer.select(`#path-line-${i}`).node()
let length = 1 / (that.jsonData.length - 1) * lineChartWidth
return function (t) {
const l = node.getTotalLength()
const p = node.getPointAtLength(t * l)
let headY = 0
if (that.curve) {
let y = findY(node, l, t * length, length)
headY = y
} else {
headY = p.y
}
that.svg.select('#circle' + i)
.attr('cy', function () {
return headY
})
that.svg.select('#images' + i).attr('transform', function () {
let x = that.t * lineChartWidth + offsetX
return "translate(" + x + "," + headY + ")"
});
that.svg.select("#lineLegend" + i)
.attr('transform', function () {
let { v1, v2 } = getOffsetValue(d, type);
var interOffsetY = d3.interpolate(v1, v2);
let addX = 0
if (that.dynamicImages[i]) {
addX = 40
lineLegendoffsetY = lineLegendoffsetY_phone
}
if (that.branchType == 'rank') addX = 36
return 'translate(' + (that.t * lineChartWidth + lineLegendoffsetX + addX) + ', ' +
(interOffsetY(t) + lineLegendoffsetY + headY) + ')';
})
.text(function () {
let { v1, v2 } = getSingleValue(d, type);
var interY = d3.interpolate(v1, v2); // yScale
let v = ''
if (that.interpolate) {
let i = d3.interpolate(v1, v2);
if (that.legendFollow) v = ' : ' + that.format(i(t), v2)
} else {
if (that.legendFollow) v = ' : ' + v2
}
return d['category'] + v;
})
.attr("text-anchor", "start")
};
})
if (type !== "ending") {
// 原始曲线更新,包括yScale更新和x坐标位移更新
lines.transition()
.duration(that.durationTime)
.ease(that.d3Ease)
.attr("transform", function () {
return "translate( " + transX + ",0)";
})
.attr("d", function (d, i) {
return line(d.datapoints);
})
// 更新x轴
xaxis.transition()
.duration(that.durationTime)
.ease(that.d3Ease)
.attr("transform", function () {
return "translate( " + transX + ",0)";
})
.on('end', function () {
that.process++ //进度加一
if (that.process == that.jsonData.length) {
setTimeout(function () {
// 最终更新整体
enddingChart();
}, 500)
} else {
runAnimate()
}
});
}
that.lastYScale = d3.scaleLinear()
.range(that.yScale.range())
.domain(that.yScale.domain())
}
var updateCol = function () {
let sliceIndex = that.process >= that.jsonData.length ? that.process - 1 : that.process
that.jsonData.slice(sliceIndex, sliceIndex + 1).forEach(function (d) {
var fontsize = that.rankingFontsize;
var nodes = that.columnName.map(function (c) {
return {
c: c,
dy: fontsize,
originY: that.yScale(d[c]),
y: that.yScale(d[c]) - fontsize / 2
};
});
that.collid(nodes);
nodes.forEach(function (node) {
d[node.c + 'L'] = node.y + fontsize / 2;
d[node.c + 'Offset'] = node.textOffset
});
});
}
var getValue = function (t, comp, isLegend) {
let key = isLegend ? comp.category + 'L' : comp.category;
if (t >= 1) {
return that.jsonData[that.jsonData.length - 1][key];
}
var startIndex = Math.floor(t * (that.jsonData.length - 1));
var endIndex = (t === 1) ? startIndex : (startIndex + 1);
var r = t % (1 / (that.jsonData.length - 1)) * (that.jsonData.length - 1);
var v1 = that.jsonData[startIndex][key];
var v2 = that.jsonData[endIndex][key];
var v = v1 + (v2 - v1) * r;
// var v = v1 + (v2 - v1) * t;
return v;
}
// 单步执行获取前后两个值
var getSingleValue = function (comp, type) {
//通过that.process进度获取插值数值
let key = comp.category;
var startIndex = that.process - 1 > that.jsonData.length - 1 ? that.jsonData.length - 1 : that.process - 1;
var endIndex = type === "ending" ? startIndex : that.process;
var v1 = that.jsonData[startIndex][key];
var v2 = that.jsonData[endIndex][key];
return { v1, v2 }
}
// 单步执行中获取文字legend的偏移量
var getOffsetValue = function (comp, type) {
let key = comp.category + 'Offset';
var startIndex = that.process - 1 > that.jsonData.length - 1 ? that.jsonData.length - 1 : that.process - 1;
var endIndex = type == "ending" ? startIndex : that.process;
var v1 = that.jsonData[startIndex][key] ? that.jsonData[startIndex][key] : 0;
var v2 = that.jsonData[endIndex][key] ? that.jsonData[endIndex][key] : 0;
return { v1, v2 }
}
let enddingChart = function () {
let xScale = d3.scaleLinear().range([0, that.width]); // 界面像素范围
xScale.domain([0, that.jsonData.length - 1]);
let height = that.clientHeight - that.margin[0] - that.margin[2];
let yScale = d3.scaleLinear().range([height, that.margin[2]]);
if (that.branchType == 'rank') yScale = d3.scaleLinear().range([that.margin[2], height]);
// 存储之前Y Scale
// that.preYScale = yScale
yScale.domain([that.minValue, that.maxValue * 1.1]);
that.yScale = yScale
that.xScale = xScale
updateCol()
// 更新legend位置
runAnimate("ending")
var line2 = d3.line()
.x(function (d, i) {
return xScale(that.columnMap[d['date']]);
})
.y(function (d) {
return yScale(parseFloat(d["concentration"]));
});
if (that.curve) line2.curve(d3.curveMonotoneX)
that.clipPathDef.attr('opacity', 0)
let zoomTimes = 1
while (that.maxXSize * that.XfontSize > (xScale(1) - xScale(0)) * zoomTimes * 1.5) {
zoomTimes *= 2
}
let xtickScale2 = d3.axisBottom()
.scale(xScale).tickSize(that.clientHeight - that.margin[0] - that.margin[2], 0, 0)
.ticks(that.xNames.length < 4 ? that.xNames.length - 1 : that.xNames.length)
.tickPadding(12 + that.xLabelOffsetY)
.tickFormat((d, i) => {
// 根据字体大小和文本长度,计算x轴显示与否,第一个最后一个永远都会显示出来
// 如果在zoomTime不是1的情况下与最后一个发生冲突的节点不会显示x轴文字
if (zoomTimes > 1) {
// 向左查找可能会重叠的节点文字,设置为不显示
let tempZoomTimes = zoomTimes
let tempIndex = that.xNames.length - 1
// 从最后一个往左查找,每一个不符合要求的返回false,即不绘制
while (tempIndex - 1 >= 0 && tempZoomTimes > 1) {
if (i == tempIndex - 1) {
return ''
}
tempZoomTimes--
tempIndex--
}
}
// 常规情况下每隔zoomTimes个取一个,或者若为最后一个则一定显示
if (!(i % zoomTimes) || i == that.xNames.length - 1) {
return that.xNames[i]
} else {
return ''
}
});
lines.transition()
.duration(that.durationTime)
.ease(that.d3Ease)
.attr("d", function (d, i) {
return line2(d.datapoints);
}).attr("transform", function () {
return "translate( " + that.margin[3] + ",0)";
})
xaxis.transition()
.duration(that.durationTime)
.ease(that.d3Ease)
.attr('transform', 'translate(' + that.margin[3] + ',0)')
.call(xtickScale2)
that.svg.select(".xaxis").selectAll("line").attr("opacity", 0.0001);
that.svg.select(".xaxis").selectAll("path").remove();
}
if (that.showLegend) {
that.addLegend(that.concentrations, color)
}
}
/**
* 显示图例
* @param {Object} m
*/
addLegend(concentrations, color) {
let that = this;
//清空原图例下所有数据
that.svg.select(".bcrlegend").selectAll("g").remove();
var binding = that.svg.select(".bcrlegend").selectAll("g").data(concentrations, function (d) { return d.key; });
binding.exit().remove();
that.svg.select(".bcrlegend").attr('transform', 'translate(' + that.margin[3] + ',' + 100 + ' )');
let enterG = binding.enter().append("g");
let offset = 0
enterG.append("rect")
.attr("width", 20).attr("height", 5)
.attr('transform', function (d, i) {
let beforeOffset = offset
offset = parseInt(d.category.length * 8) + parseInt(beforeOffset) + 50 + that.legendFontSize * 4
return 'translate(' + beforeOffset + ',' + 0 + ' )';
}).attr("fill", function (d, i) {
return color(d.category);
});
let offsetText = 0
enterG.append("text").attr("font-size", that.legendFontSize)
.attr("fill", that.fontColor)
.attr('transform', function (d, i) {
let beforeOffset = offsetText || 25
offsetText = parseInt(d.category.length * 8) + parseInt(beforeOffset) + 50 + that.legendFontSize * 4
return 'translate(' + beforeOffset + ',' + 7 + ' )';
}).text(function (d, i) {
return d['category'];
})
}
getRecentDataLength(isRun) {
if (isRun) {
return this.jsonData.length - this.process
} else {
return this.process
}
}
updateObj(obj, callback) {
Object.assign(this, obj)
d3.select("#" + this.el).html('')
this.init(obj, callback)
}
destroy() {
if (this.timeout != null) {
clearTimeout(this.timeout);
}
Object.assign(this, null)
}
getData() {
let that = this;
let currentData = [];
if (that.index <= that.barNumber) {
currentData = that.jsonData.slice(0, that.index);
} else {
currentData = that.jsonData.slice(that.index - that.barNumber, that.index);
}
return currentData;
}
getTextData() {
let that = this;
let tmpcurrentData = [];
let currentData = [];
if (that.jsonData.length > that.barNumber) {
if (that.index <= that.barNumber) {
tmpcurrentData = that.jsonData.slice(0, that.barNumber);
} else {
tmpcurrentData = that.jsonData.slice(that.index - that.barNumber, that.index);
}
} else {
tmpcurrentData = that.jsonData
}
for (let i = 0; i < tmpcurrentData.length; i++) {
currentData.push(tmpcurrentData[i][that.data[0][0]]);
}
return currentData;
}
sortJsonData() {
let that = this;
that.jsonData.sort(function (a, b) {
let aValue = Number(a[that.key]);
let bValue = Number(b[that.key]);
return d3.descending(bValue, aValue);
});
}
arrayToJson() { //添加配色
let that = this;
that.data = that.removeNull(that.data)
that.columnName = that.data[0];
let json = [];
that.key = that.columnName[1];
for (let i = 1; i < that.data.length; i++) {
let res = {};
for (let j = 0; j < that.columnName.length; j++) {
if (j == 0) {
res[that.columnName[j]] = that.data[i][j];
} else {
res[that.columnName[j]] = parseFloat(that.data[i][j]);
}
}
res.index = i
res['_inner_color'] = that.colors[i % that.colors.length];
json.push(res);
}
that.columnName = that.data[0].slice(1); //不删除此代码
that.xNames = that.data.slice(1).map(d => d[0])
that.jsonData = json;
}
firstcolumnMap() {
let columnMap = {};
for (let i = 0; i < this.data.length; i++) {
columnMap[this.data[i][0]] = i - 1;
}
return columnMap;
}
removeNull(data) {
let array = []
for (let i = 0; i < data.length; i++) {
let a = [];
for (let j = 0; j < data[i].length; j++) {
if (data[i][j] != null && data[i][j] != "" && data[i][j] != " ") {
data[i][j] = (data[i][j] + '').replace(/,/g, '');
a.push(data[i][j]);
}
}
if (a.length > 0) array.push(a)
}
return array
}
/** 千分位格式化 */
format(num, num2) {
if (this.pointFormat == 0) {
num = parseInt(num)
} else if (this.pointFormat == 1) {
num = parseFloat(num).toFixed(1)
} else if (this.pointFormat == 2) {
num = parseFloat(num).toFixed(2)
} else {
if (num2 || num2 === 0) {
let f = 0
let p = num2.toString().split('.')[1]
if (p) f = p.length
num = Number(num).toFixed(f)
} else {
num = parseFloat(num)
}
}
let result = '';
if (this.showThousands) {
num = (num || 0).toString();
let a = num.split('.')[0] || num
let b = num.split('.')[1] || ""
while (a.length > 3) {
result = ',' + a.slice(-3) + result;
a = a.slice(0, a.length - 3);
}
if (a) { result = a + result; }
if (b) { result = result + "." + b }
} else {
result = num
}
if (this.showPercent) result = result + '%'
return result
}
}
export default DynamicMultiLine;