UNPKG

datavizstocklib

Version:

测试一下呗

1,241 lines (1,118 loc) 51.1 kB
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;