datavizstocklib
Version:
测试一下呗
525 lines (502 loc) • 22.5 kB
JavaScript
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;