UNPKG

datavizstocklib

Version:

测试一下呗

525 lines (502 loc) 22.5 kB
import * as d3 from "d3"; import { data } from "jquery"; let icon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyOC44OSAyOC44OSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOm5vbmU7c3Ryb2tlOiMwMDFlNzI7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjVweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPui1hOa6kCAzPC90aXRsZT48ZyBpZD0i5Zu+5bGCXzIiIGRhdGEtbmFtZT0i5Zu+5bGCIDIiPjxnIGlkPSLlm77lsYJfMS0yIiBkYXRhLW5hbWU9IuWbvuWxgiAxIj48Y2lyY2xlIGNsYXNzPSJjbHMtMSIgY3g9IjE0LjQ0IiBjeT0iMTQuNDQiIHI9IjExLjk0Ii8+PHBvbHlsaW5lIGNsYXNzPSJjbHMtMSIgcG9pbnRzPSIxNC40NCA3LjYxIDE0LjQ0IDE0LjQ0IDE3Ljk2IDE5LjkzIi8+PC9nPjwvZz48L3N2Zz4=' let config = { /** 控件名 */ name: "DynamicTimeline", /** svg 对象 */ svg: {}, /** 展示类型 Dynamic Fixed Custom */ scaleType: "Dynamic", /** 数据 */ data: [], /** 内部使用 */ jsonData: {}, /** 表头名集合 */ columnName: [], /** colors */ colors: [], // 柱子圆角 rx: 2, /** 内部使用 */ index: 0, el: '_dataviz', d3Ease:'', clientHeight: 1280, clientWidth: 720, // 1280*720 isTemplate: false, margin: { left: 40, top: 300, bottom: 130, right: 80 } } export class DynamicTimeline { /** 构造器 */ constructor() { let newconfig = JSON.parse(JSON.stringify(config)); Object.assign(this, newconfig) } /** 更新图表 */ update(obj) { // 复制新配置 Object.assign(this, obj) } /** 播放 */ play() { } /** 暂停 */ pause() { } destroy() { } /** 初始化 */ init(obj, callback) { let that=this Object.assign(this, obj); this.index = 1 // 设定缓动函数 this.getEase() that.arrayToJson(); that.svg = d3.select("#" + this.el).append("svg"); this.chartG = this.svg.append("g") .attr("transform",`translate(${this.margin.left},${this.margin.top})`) //添加剪切面板 this.chartG.append("defs") .append("clipPath") .attr("id","timeline-clip-path") .append("rect") .attr("x",0) .attr("y",-90) .attr("width", this.clientWidth - this.margin.left - this.margin.right) .attr("height", this.clientHeight - this.margin.top - this.margin.bottom) this.timelineG = this.chartG .append("g") .attr("clip-path","url(#timeline-clip-path)") this.svg.append("defs") .html( '<filter id="f3" x="0" y="0" width="200%" height="200%">'+ '<feOffset result="offOut" in="SourceGraphic" dx="2" dy="2" />'+ '<feColorMatrix result="matrixOut" in="offOut" type="matrix"'+ 'values="0.1 0 0 0 0 0 0.1 0 0 0 0 0 0.1 0 0 0 0 0 1 0" />'+ '<feGaussianBlur result="blurOut" in="matrixOut" stdDeviation="2" />'+ '<feBlend in="SourceGraphic" in2="blurOut" mode="normal" />'+ '</filter>' ) that.updateChart(obj, callback) } 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] } }) } updataImages() { let that = this; for (var i = 0; i < that.jsonData.length; i++) { that.jsonData[i]["image"] = that.dynamicImages[i] } } /** * 页面渲染 一次性渲染完毕 * @param {*} */ updateChart(obj, callback) { Object.assign(this, obj); this.getEase() this.chartG .attr("transform",`translate(${this.margin.left},${this.margin.top})`) // 如果宽度大于高度,让图表居中。 if(this.clientWidth > this.clientHeight){ // chart width 520 let offset = (this.clientWidth - this.margin.left - this.margin.right)/2 + this.margin.left -520/2 this.chartG .attr("transform",`translate(${offset},${this.margin.top})`) } this.chartG.select("#timeline-clip-path") .select("rect") .attr("width", this.clientWidth - this.margin.left - this.margin.right) .attr("height", this.clientHeight - this.margin.top - this.margin.bottom) this.svg.attr("width", this.clientWidth) .attr("height", this.clientHeight); // 删除所有节点 this.timelineG.selectAll("*").interrupt() this.timelineG.selectAll("*").remove() let time1G = this.timelineG.append("g") let showData = this.jsonData let offsetY = 0 let fontSize = 28, eachLineHeight = 35, eachLineTextNum = this.isTemplate ? 24 : 30 // 存储每个一级章节的偏移量 let transformArr = [] showData.forEach((time1Item, time1index)=>{ let offset2Y = 0 time1Item.data.forEach((time2Item,time2index)=>{ let offset3Y = 0 time2Item.data.forEach((item,i)=>{ let time2G = time1G.append("g") .attr("class", "_timeline_"+time1Item.time1.split("_special_xxxxx_")[1] + time2Item.time2.split("_special_xxxxx_")[1] + i) .attr("transform",`translate(${150}, ${1000})`) .attr("opacity",0.0001) let time2Line = time2G.append("line") // 添加年份文本标题 if(time2index === 0 && i === 0){ time2G.append("text") .attr("class","year-title") .attr("transform", 'translate(-85,-70)') .text(time1Item.time1.split("_special_xxxxx_")[0]) .attr("text-anchor",'middle') .attr("font-size", fontSize*0.8) .attr("fill", this.fontColor) } // 添加日期文本标题 if(i === 0){ time2G.append("text") .attr("class","date-title") .attr("transform",'translate(-85,-40)') .text(time2Item.time2.split("_special_xxxxx_")[0]) .attr("text-anchor",'middle') .attr("font-size",fontSize*0.8) .attr("fill",this.fontColor) } let result = this.getTextHeight(item[this.columnName[4]], eachLineTextNum) // 获取文本的高度 let height = (result.arr.length + .2) * eachLineHeight item.text = result.arr let eachItemG = time2G.append("g") .attr("transform",`translate(${0},${0})`) let rectWidth = this.clientWidth - this.margin.left - this.margin.right-170 // 添加背景颜色 eachItemG.append('rect') .attr("transform",`translate(${- fontSize*0.5},${ - 1.15*fontSize})`) .attr("width", rectWidth + 20) .attr("height", height) .attr("rx", this.rx) .attr("class","fill-color-by-index") .attr("fill", this.colors.length>1?this.colors[i%2]:this.colors[0]) .attr("filter","url(#f3)") // 添加三角形 eachItemG.append('path') .attr("transform",`translate(${- fontSize*0.5-10},${ - 1.15*fontSize + (result.arr.length + .2) * eachLineHeight/2})`) .attr("d",function(){ return "M0,0 L11,-10 L 11,10" }) .attr("class","fill-color-by-index") .attr("fill", ()=>{ return this.colors.length>1?this.colors[i%2]:this.colors[0] }) // 添加文本 eachItemG .append("text") .attr("font-size", fontSize) .attr("fill", ()=>{ return this.fontColor }) .selectAll("tspan") .data(item.text) .enter() .append("tspan") .text(d=>d) .attr("transform","translate(0,0)") .attr("x",0) .attr("y",0) .attr("dy",(d,i)=>{ return i * eachLineHeight }) // 添加右下角的时间 eachItemG.append("text") .attr("class","time3") .attr("id","time3-id") .text(item[this.columnName[3]]) .attr("transform",()=>{ return `translate(${rectWidth},${(result.arr.length + .2) * eachLineHeight})` }) .attr("text-anchor","end") .attr("font-size",fontSize*0.7) .attr("fill", ()=>{ return this.fontColor }) .each(function(){ var width = this.getComputedTextLength(); if(width>0){ eachItemG.append("svg:image") .attr("class","time3-image") .attr("transform",()=>{ return `translate(${rectWidth-width-28},${(result.arr.length + .2) * eachLineHeight-18})` }) .attr("xlink:href",icon) .attr("width",20) .attr("height",20) } }) // 添加左边的文本 let eachLineTextNum2 = 4 let result2 = this.getTextHeight(item[this.columnName[2]], eachLineTextNum2) // 获取文本的高度 let height2 = (result2.arr.length+1) * eachLineHeight * 0.8 - 10 item.text2 = result2.arr let eachItemG2 = time2G.append("g") .attr("transform",`translate(${-120},${offset3Y})`) // 添加背景颜色 eachItemG2.append('rect') .attr("transform",`translate(${- fontSize*0.5},${ - 1.15*fontSize})`) .attr("width",95) .attr("height", height2 ) .attr("rx", this.rx) .attr("class","fill-color-by-index") .attr("fill", this.colors.length>1?this.colors[i%2]:this.colors[0]) .attr("stroke",this.fontColor) // 添加文本 eachItemG2 .append("text") .attr("font-size", fontSize*.8) .attr("fill", ()=>{ return this.fontColor }) .selectAll("tspan") .data(item.text2) .enter() .append("tspan") .text(d=>d) .attr("transform","translate(0,0)") .attr("x",0) .attr("y",0) .attr("dy",(d,i)=>{ return i * eachLineHeight*0.8 }) // 修复前后两部分的偏移量差距 let offsetInner = height - height2 eachItemG .attr("transform",`translate(${0},${offsetInner > 0 ? 0: -offsetInner/2 })`) eachItemG2 .attr("transform",`translate(${-120},${offsetInner > 0 ? offsetInner/2: 0 })`) // 更新文本位置 time2G.select(".year-title") .attr("transform", `translate(-85,${-70 + ( offsetInner > 0 ? offsetInner/2 : 0)})`) time2G.select(".date-title") .attr("transform",`translate(-85,${-40 + ( offsetInner > 0 ? offsetInner/2 : 0)})`) // 更新线段的长度 let time3OffsetHeight = height > height2 ? height : height2 transformArr.push({ offset: offsetY + offset2Y + offset3Y, name: "_timeline_"+time1Item.time1.split("_special_xxxxx_")[1] + time2Item.time2.split("_special_xxxxx_")[1] + i, height: time3OffsetHeight + eachLineHeight*2, //标识一下是否是一级时间的开始部分 offsetY: (i === 0 && time2index===0) ? 30 : 30 }) // 更新每一行的offset offset3Y = offset3Y + time3OffsetHeight + eachLineHeight*2 // 更新背景线段的高度,是否为最后一条,后面为正常的 // 下部分的线段 if(time2index === time1Item.data.length-1 && i === time2Item.data.length-1 && time1index === showData.length-1){ // do nothing }else if(time2index === time1Item.data.length-1 && i === time2Item.data.length-1 ){ // 每个大段的第一条 time2Line.attr("x1",-85) .attr("y1", -20 + ( offsetInner > 0 ? offsetInner/2 : 0)) .attr("x2",-85) .attr("y2", time3OffsetHeight + eachLineHeight*2-60) .attr("stroke",this.fontColor) }else{ // 最后一条不添加,如果不是最后一条就添加 time2Line.attr("x1",-85) .attr("y1", -20 + ( offsetInner > 0 ? offsetInner/2 : 0)) .attr("x2",-85) .attr("y2", time3OffsetHeight + eachLineHeight*2-30) .attr("stroke", this.fontColor) } // if right height > left height if(offsetInner > 0 && transformArr.length>1){ this.timelineG.select("g") .select(`.${transformArr[transformArr.length-2].name}`) .select("line") .clone() .attr("y1", function(){ return +d3.select(this).attr("y2") + offsetInner/2 }) } }) offset2Y = offset2Y + offset3Y + eachLineHeight }) offsetY = offsetY + offset2Y }) this.updateEachPart(transformArr,0,callback) } updateEachPart(transformArr, index, callback){ let totalHeight = transformArr[index].offset + transformArr[index].height + 30 let moveLength = totalHeight - (this.clientHeight - this.margin.top - this.margin.bottom) if(moveLength > 0){ let topEle={ height: 0, offset: 0 } transformArr.forEach((d,i)=>{ let offset = d.offset //i === 0 ? 0 : transformArr[i-1].offset let height = d.height + d.offsetY // 如果顶部移出去且底部没有移出去,那么就要把底部也移走 if(offset - moveLength < 0 && height + offset - moveLength > 0){ topEle.height=height topEle.offset=offset } }) if(topEle.height!==0||topEle.offset!==0){ moveLength = topEle.height + topEle.offset } this.timelineG .select("g") .transition() .duration(1000) .ease(this.d3Ease) .attr("transform",`translate(0,${ -moveLength})`) } this.timelineG.select("g") .select(`.${transformArr[index].name}`) .selectAll("line") .attr("opacity",0.0001) this.timelineG.select("g") .select(`.${transformArr[index].name}`) .selectAll('.fill-color-by-index') .attr("fill",this.colors.length > 1 ? this.colors[index%2] : this.colors[0]) this.timelineG.select("g") .select(`.${transformArr[index].name}`) .attr("transform",()=>{ if(moveLength<0){ return `translate(${150},1000)` }else{ return `translate(${150},${totalHeight + transformArr[index].height})` } }) .transition() .duration(1000) .ease(this.d3Ease) .attr("transform",`translate(${150},${transformArr[index].offset})`) .attr("opacity",1) .on("end",()=>{ if(index+1 < transformArr.length){ this.updateEachPart(transformArr, index+1, callback) }else{ callback(true) } this.timelineG.select("g") .select(`.${transformArr[index].name}`) .selectAll("line") .transition() .duration(1000) .attr("opacity",1) }) } updateObj(obj, callback) { this.updateChart(obj, callback) } getTextHeight(text, num){ let arr=this.gblen(text, num) return arr } gblen(str, num) { if(str===null) return {arr:[],total:0}; str+=""; var len = 0; var total = 0; var arr = []; let subStr='' for (var i = 0; i < str.length; i++) { if (str.charCodeAt(i) > 127 || str.charCodeAt(i) == 94) { len += 2; total += 2; } else { len++; total++ } subStr = subStr + str[i] if (len > num) { arr.push(subStr); subStr='' len = 0; } } if (len <= num&& len!==0){ arr.push(subStr) } return { arr, total }; } removeNull(arr){ let result = arr.filter(function(item,index,data){ return item !== '' && item !== null }).filter(function(item,index,data){ return data.indexOf(item)===index }) if(result.length>0){ return result }else{ return [''] } } arrayToJson() { let that = this; that.columnName = that.data[0]; let json = []; for (let i = 1; i < that.data.length; i++) { let res = {}; for (let j = 0; j < that.columnName.length; j++) { res[that.columnName[j]] = that.data[i][j]; } json.push(res); } json.forEach((d,i)=>{ if(d[that.columnName[0]]!==''&&d[that.columnName[0]]!==null){ d[that.columnName[0]] = d[that.columnName[0]] +"_special_xxxxx_"+i // d.class1='_timeline_'+i } if(d[that.columnName[1]]!==''&&d[that.columnName[1]]!==null){ d[that.columnName[1]] = d[that.columnName[1]] +"_special_xxxxx_"+i // d.class2='_timeline_'+i } }) json.forEach((d,i)=>{ if((d[that.columnName[0]]===''||d[that.columnName[0]]===null)&&i!==0){ d[that.columnName[0]]=json[i-1][that.columnName[0]] } if((d[that.columnName[1]]===''||d[that.columnName[1]]===null)&&i!==0){ d[that.columnName[1]]=json[i-1][that.columnName[1]] } }) json = json.filter(d=>{ return d[that.columnName[4]]!==''&&d[that.columnName[4]]!==null && d[that.columnName[2]]!==''&&d[that.columnName[2]]!==null }) // 一级时间 var time1 = json.map(d => { return d[that.columnName[0]] }) time1 = this.removeNull(time1) // 二级时间 var time2 = json.map(d => { return d[that.columnName[1]] }) time2 = this.removeNull(time2) // 封装数据 let showData = [] time1.forEach((t1m,i)=>{ let time1Item={time1:t1m, data:[]} time2.forEach(t2m=>{ let time2Item={time2:t2m,data:[]} json.forEach(d=>{ if(d[that.columnName[0]]===t1m && d[that.columnName[1]]===t2m){ time2Item.data.push(d) } }) if(time2Item.data.length>0){ time1Item.data.push(time2Item) } }) showData.push(time1Item) }) that.jsonData = showData; } } export default DynamicTimeline;