datavizstocklib
Version:
测试一下呗
723 lines (659 loc) • 27.5 kB
JavaScript
import * as d3 from "d3";
let config = {
/** 控件名 动态瀑布图 */
name: "DynamicWaterfall",
/** 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: 14, //字号
/**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",
/** 条数 */
barNumber: 5,
/** 柱高度自适应 */
rectHeightFixed: false,
/** 柱高度和 柱间距的比例 */
rectRate: 5,
/** 内部使用 */
rectSpace: 0,
/** X轴坐标位置 bottom top */
xAxisPos: "top",
/** 动画时长 */
durationTime: 200,
/** 最大值 */
maxValue: 0,
/** 延时播放 */
delay: 2000,
height: 35,
/** 内部使用 */
isRun: false,
replay: false,
showRanking: true,
/** 矩形圆角 */
rx: 2,
index: 0,
dataIndex: 0,
timeout: null,
rankingFontsize: 16,
xSicks: 5,
showThousands: true,
pointFormat: 'auto',
showPercent: false,
el: '_dataviz',
d3Ease:''
}
String.prototype.replaceAll = function(s1, s2) {
return this.replace(new RegExp(s1, "gm"), s2);
}
export class DynamicWaterfall {
/** 构造器 */
constructor() {
let newconfig = JSON.parse(JSON.stringify(config));
Object.assign(this, newconfig)
}
/** 更新图表 */
update(obj) { // 复制新配置
Object.assign(this, obj)
this.getEase();
this.durationTime = 200;
}
/** 重新播放 */
replay() {}
/** 播放 */
play() {}
/** 暂停 */
pause() {}
/** 初始化 */
init(obj, callback) {
let that = this;
Object.assign(this, obj);
this.getEase();
this.index = 1;
that.arrayToJson();
that.svg = d3.select("#" + this.el).append("svg");
//添加竖线
that.svg.append("g")
.attr("class", "grid")
.style("color", "#A3A3A3")
.style("font-size", "14")
that.svg.append("g")
.attr("class", "xaxis")
.attr("font-size", that.XfontSize)
.attr("font-family", that.fontfamilyBase)
.attr("fill", that.fontColor)
if (that.clientWidth == 0) {
that.clientWidth = document.getElementById(this.el).clientWidth;
}
that.svg.attr("width", that.clientWidth).attr("height", that.clientHeight);
let currentData = that.getData();
let maxValue = that.max(currentData) * 1.1;
let minValue = that.min(currentData);
let width = that.clientWidth - this.margin[1] - this.margin[3];
that.yScale = d3.scaleLinear().range([width, that.margin[3]]); // 界面像素范围
that.yScale.domain([minValue, maxValue]); //数据范围
that.bar = that.svg.append("g");
that.svg.append("text").attr("class", "topText")
.attr("x", that.svg.attr("width") - that.margin[1] + 80)
.attr("y", that.margin[0] - that.rectRate * 5)
.text('')
.attr("text-anchor", "end")
.attr("font-size", that.rankingFontsize)
.attr("fill", that.fontColor);
this.dataIndex = this.jsonData.length + 1
clearTimeout(this.timeout);
if (this.timeout != null) this.timeout.stop();
// 获取x轴中的最大值,和2判断找到最大值是因为最后会添加“累计”的信息
that.maxXSize = Math.max(2, d3.max(that.jsonData, (d)=>{
return ""+d[that.columnName[0]].length
}))
that.updateChart(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]
}
})
}
/**
* 页面渲染
* @param {*} year
*/
updateChart(callback, lastflag) {
let that = this;
that.arrayToJson();
let currentData = that.getData();
if (currentData.length == 0) {
return;
}
if (lastflag) {
currentData = that.jsonData;
let tmpObj = currentData.slice(currentData.length - 1);
var objString = JSON.stringify(tmpObj[0]);
var obj2 = JSON.parse(objString);
obj2['_inner_color'] = that.colors[0];
obj2[that.columnName[0]]='累计'
currentData.push(obj2);
that.barNumber = that.barNumber + 1;
}
that.maxValue = that.max(currentData);
that.svg.attr("width", that.clientWidth).attr("height", that.clientHeight);
let uirange = [];
uirange.push(that.margin[3]);
uirange.push(that.clientWidth - that.margin[1]);
let startDate = that.index > that.barNumber ? that.index - that.barNumber + 1 : 1;
// 如果是最后一次计算,会绘制所有柱子,此时开始日期应当是第二个数据(去除表头)
if(lastflag){
startDate=1
}
// that.xScale = d3.scaleLinear().range(uirange); // 界面像素范围
// that.xScale.domain([-0.5, that.barNumber]); //数据范围
let xScaleData = that.data
.slice(startDate, that.barNumber + startDate)
.map(d=>d[0])
if(lastflag){
xScaleData = currentData
.map(d=>d[that.columnName[0]])
}
that.xScale = d3.scaleBand()
.domain(xScaleData)
.range(uirange)
.padding(0.3); // 界面像素范围
let width = that.clientHeight - this.margin[0] - this.margin[2];
that.yScale = d3.scaleLinear().range([width, that.margin[2]]); // 界面像素范围
let minValue = 0; //that.min(currentData);
that.yScale.domain([minValue, that.maxValue * 1.1]); //数据范围
let yScale = that.yScale;
let xScale = that.xScale;
let zoomTimes=1
while( that.maxXSize * that.XfontSize > xScale.step() * zoomTimes){
zoomTimes *= 2
}
//添加 x 轴坐标,在tickValues中计算是否绘制
let gridXScale = d3.axisBottom(that.xScale)
.tickValues(xScaleData.filter(function(d,i,data){
// 根据字体大小和文本长度,计算x轴显示与否,第一个第二个永远都会显示出来
let index = that.index - 1
if(lastflag){
index = that.jsonData.length - 1
}
// 如果在zoomTime不是1的情况下与最后一个发生冲突的节点不会显示x轴文字
if(zoomTimes>1){
// 向左查找可能会重叠的节点文字,设置为不显示
let tempZoomTimes=zoomTimes
let tempIndex=index
while(tempIndex-1>=0 && tempZoomTimes > 1){
if(d == that.jsonData[tempIndex-1][that.columnName[0]]){
return false
}
tempZoomTimes--
tempIndex--
}
// 向右查找可能会重叠的节点文字,设置为不显示
tempZoomTimes=zoomTimes
tempIndex=index
while(tempIndex+1< that.jsonData.length-1 && tempZoomTimes > 1){
if(d == that.jsonData[tempIndex+1][that.columnName[0]]){
return false
}
tempZoomTimes--
tempIndex++
}
}
// 常规情况下每隔zoomTimes个取一个,或者若为最后一个则一定显示
return !(i%zoomTimes) || d == that.jsonData[index>that.jsonData.length-1?that.jsonData.length-1:index][that.columnName[0]]
}))
that.svg.select(".xaxis")
.attr("transform", function(){
return `translate(0,${-that.margin[2]+that.clientHeight})`
})
.html();
that.svg.select(".xaxis")
.transition()
.duration(that.durationTime)
.ease(that.d3Ease)
.call(gridXScale);
that.svg.select(".xaxis")
.selectAll("line").remove()
that.svg.select(".xaxis")
.selectAll("path").remove()
that.svg.select(".xaxis")
.selectAll("text")
.attr("font-size", that.XfontSize)
.attr("font-family", that.fontfamilyBase)
.attr("fill", that.fontColor)
that.height = (that.clientHeight - that.margin[0] - that.margin[2]) * 0.95 / that.barNumber * that.rectRate
let gridHeight = yScale(that.maxValue) - yScale(0) - that.clientHeight;
let svgWidth = that.clientWidth - this.margin[1] - this.margin[3];
let yTickSize = svgWidth + that.margin[1]*1/3
that.gridscale = d3.axisLeft()
.scale(that.yScale)
.ticks(that.xSicks)
.tickSize(-yTickSize, 0, 0)
.tickFormat(d => {
if (d < 0) {
return "";
}
return that.format(d);
})
that.svg.select(".grid")
.attr('transform', 'translate(' + that.margin[3] + ',0)')
.transition().duration(that.durationTime)
.call(that.gridscale).ease(that.d3Ease)
that.svg.select(".grid").selectAll("Text")
.attr("fill", that.fontColor).attr("fill-opacity", 1).attr("font-size", that.YfontSize);;
that.svg.select(".grid").selectAll("line").attr("opacity", 0.4);
that.svg.select(".grid").select("path").remove();
var binding = that.bar.selectAll('g').data(currentData, function(d) {
return d[that.columnName[0]] + d['_inner_color']
});
binding.attr("class", "binding");
binding.exit().transition()
.duration(that.durationTime)
.attr("transform", function(d, i) {
return "translate(" + (xScale(xScaleData[0]) - xScale.step()) + "," + (yScale(0)) + ")";
}).ease(that.d3Ease).attr("opacity",0.0001).remove();
let barWidthTmp = xScale.bandwidth() * that.rectRate;
//补充新的元素
let enterG = binding.enter().append("g").attr("opacity", 1);
// 新元素初始化
enterG.attr("opacity",0.0001)
.attr("transform", function(d, i) {
if (currentData.length == that.barNumber) {
return "translate(" + width + "," + (yScale(0)) + ")";
} else {
return "translate(" + width + "," + (yScale(0)) + ")";
}
})
.transition().duration(0)
.attr("transform", function(d, i) {
return "translate(" + (xScale(d[that.columnName[0]]) + xScale.bandwidth() * (1 - that.rectRate)/2 ) + "," + (yScale(0)) + ")";
}).attr("opacity", 1)
.ease(that.d3Ease);
// let barWidthTmp = xScale.bandwidth() * that.rectRate//(xScale(1) - xScale(0)) * that.rectRate;
enterG.append("rect")
.attr("width", barWidthTmp)
.attr("rx", that.rx)
.attr("height", 0)
.attr("y", function(d, i) {
if (i == 0) {
return 0;
}
if (lastflag && (i == that.barNumber - 1)) {
return -(yScale(0) - yScale(d[that.key]));
}
let preData = currentData[i - 1][that.key];
if (that.index > that.barNumber) {
if (parseFloat(preData) < parseFloat(d[that.key])) {
return -(yScale(0) - yScale(d[that.key])) + Math.abs((yScale(0) - yScale(preData - d[that.key])));
} else {
return -(yScale(0) - yScale(preData));
}
} else {
return -(yScale(0) - yScale(preData));
}
})
.attr("fill", function(d, i) {
return d['_inner_color'];
});
// enterG.append("text")
// .text(function(d, i) {
// if (lastflag) {
// if (i % 3 == 0) {
// return d[that.columnName[0]];
// } else {
// return "";
// }
// } else {
// return d[that.columnName[0]];
// }
// })
// .attr("font-size", that.XfontSize)
// .attr("font-family", that.fontfamilyBase)
// .attr("fill", that.fontColor)
// .attr("class", 'yText')
// .attr("text-anchor", "middle")
// .attr("transform", function(d, i) {
// return "translate(" + barWidthTmp / 2 + "," + 30 + ")";
// })
// .transition().duration(that.durationTime)
// .ease(that.d3Ease);
// binding.selectAll('.yText').attr("fill", that.fontColor).attr("font-size", that.XfontSize).attr("transform", function(d, i) {
// return "translate(" + barWidthTmp / 2 + "," + 30 + ")";
// })
// if (lastflag) {
// that.svg.selectAll(".yText").text(function(d, i) {
// if (lastflag) {
// if (i == that.barNumber - 1) {
// return "累计";
// }
// }
// if (that.jsonData.length > that.originBarNumber*1.8){
// if ((that.barNumber - 3 - i) % 3 == 0) {
// return d[that.columnName[0]];
// } else {
// return "";
// }
// }else{
// return d[that.columnName[0]];
// }
// });
// }
var storage = window.localStorage;
enterG.append("text").attr("class", "tips")
.attr("font-size", that.fontsize)
.attr("fill", function(d, i) {
return d['_inner_color'];
})
.text(function(d, i) {
return that.format(Math.abs(d.min));
})
.attr("transform", function(d, i) {
if (d.min < 0) {
return storage["tipsTempTransFormAppend"];
} else {
that.tipsTempTransFormAppend = "translate(" + barWidthTmp / 2 + "," + (yScale(d[that.key]) - yScale(minValue) - 20) + ")";
storage["tipsTempTransFormAppend"] = that.tipsTempTransFormAppend;
return that.tipsTempTransFormAppend;
}
})
.attr("text-anchor", "middle")
.transition()
.duration(that.durationTime)
.ease(that.d3Ease);
let tipsTempTransForm = "";
binding.selectAll(".tips")
.attr("text-anchor", "middle")
.attr("font-size", that.fontsize)
.text(function(d, i) {
return that.format(Math.abs(d.min));
})
.transition()
.duration(that.durationTime)
.attr("transform", function(d, i, data) {
if (d.min < 0) {
tipsTempTransForm = "translate(" + barWidthTmp / 2 + "," + (yScale(d[that.key]-d.min) - yScale(minValue) - 20) + ")";
return tipsTempTransForm;
} else {
tipsTempTransForm = "translate(" + barWidthTmp / 2 + "," + (yScale(d[that.key]) - yScale(minValue) - 20) + ")";
return tipsTempTransForm;
}
})
.ease(that.d3Ease);
if (lastflag) {
that.svg.selectAll(".tips").text(function(d, i) {
if (i == that.barNumber - 1) {
return that.format(d[that.key]);
} else {
return "";
}
});
}
that.bar.selectAll("rect")
.transition()
.duration(that.durationTime)
.attr("width", barWidthTmp)
.attr("height", function(d, i) {
let theight = 0;
if (lastflag && (i == that.barNumber - 1)) {
return Math.abs(-(yScale(0) - yScale(d[that.key])));
}
if (i == 0) {
theight = yScale(0) - yScale(d[that.key]);
} else {
let preData = 0;
if (that.index > that.barNumber) {
if (i == 1) {
preData = that.jsonData[that.index - 1 - that.barNumber][that.key];
} else {
if (currentData.length < (i - 1)) {
return;
} else {
preData = currentData[i - 2][that.key];
}
}
} else {
if (currentData.length < i) {
return;
} else {
preData = currentData[i - 1][that.key];
}
}
if (lastflag) {
preData = currentData[i - 1][that.key];
}
let min = parseFloat(preData) - parseFloat(d[that.key]);
theight = Math.abs(yScale(0) - yScale(min));
// console.log("that.index:" + that.index + " i:" + i + " min:" + min + ":" + parseFloat(preData) + ":" + parseFloat(d[key]) + " : " + theight);
}
if(theight<0)theight = 0
return theight;
})
.attr("y", function(d, i, data) { //向上增长 y height 发生变化 向下增长 y 为前值的y height 发生变化
if (i == 0) {
// console.log(startDate)
return -(yScale(0) - yScale(d[that.key]));
}
let preData = 0;
if (that.index > that.barNumber) {
if (i == 1) {
if(startDate > 1) {
preData = parseFloat(that.data[startDate - 1][1])
}
} else {
if (currentData.length < (i - 1)) {
return;
} else {
preData = currentData[i - 2][that.key];
}
}
} else {
if (currentData.length < i) {
return;
} else {
preData = currentData[i - 1][that.key];
}
}
if (lastflag) {
preData = currentData[i - 1][that.key];
}
let min = parseFloat(preData) - parseFloat(d[that.key]);
if (min > 0) { //递减
// 以前值的 y 为起点 减去 两值差的高度
return -yScale(0) + yScale(preData);
} else { // 递增 ok
// 以本值的高度
let y = -yScale(0) + yScale(d[that.key]);
return y;
}
})
.attr("rx", that.rx)
.attr("fill", function(d, i) {
return d['_inner_color'];
})
.ease(that.d3Ease);
binding.transition().duration(that.durationTime).attr("transform", function(d, i) {
return "translate(" + (xScale(d[that.columnName[0]])+ xScale.bandwidth() * (1 - that.rectRate)/2 ) + "," + yScale(0) + ")";
}).attr("fill", function(d, i) { return d['_inner_color']; }).ease(that.d3Ease);
that.svg.selectAll("g").attr('opacity', '1')
// 排名显示
// that.svg.select('.topText').attr('visibility', that.showRanking ? 'visible' : 'hidden').attr("x", that.svg.attr("width") - that.margin[1] + 80)
// .attr("y", that.margin[0] - that.rectRate * 5).attr("font-size", that.rankingFontsize).text(function (d, i) {
// if (that.index - 1 < 0) return ''
// return '第' + (that.jsonData.length - that.index + 1) + '名 ' + that.jsonData[that.index - 1][that.columnName[0]]
// })
if (lastflag) {
callback(that.index, true)
that.index = 0
return;
}
this.timeout = d3.timeout(function() {
if (that.index > that.jsonData.length) {
// console.log("do if" + that.index);
that.originBarNumber = that.barNumber
that.barNumber = that.jsonData.length; //return;
that.updateChart(callback, true);
} else {
if (that.isPlay) {
callback(that.index, false)
that.updateChart(callback);
that.index++;
that.dataIndex--
}
}
}, that.index == 1?that.durationTime/2:that.durationTime);
}
updateObj(obj, callback) {
clearTimeout(this.timeout);
this.timeout.stop();
Object.assign(this, obj)
if (this.replay) {
this.dataIndex = this.jsonData.length + 1
this.index = 0
d3.select("#" + this.el).html('')
this.init(obj, callback)
} else {
this.updateChart(callback)
}
}
destroy() {
clearTimeout(this.timeout);
if (this.timeout != null) this.timeout.stop();
Object.assign(this, null)
}
min(currentData) {
let that = this;
return d3.min(currentData, d => d[that.key]) * 0.9;
}
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;
}
/** 千分位格式化 */
format(num) {
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 {
num = num.toString()
}
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
}
/** 计算最大值 */
max(currentData) {
let that = this;
return d3.max(currentData, function(d) { return parseFloat(d[that.key]); });
}
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++) {
res[that.columnName[j]] = that.data[i][j];
}
if (i == 1) {
res['_inner_color'] = parseFloat(res[that.key]) > 0 ? that.colors[1] : that.colors[2];
res['min'] = res[that.key];
} else if (i == (that.data.length - 1)) {
res['_inner_color'] = (parseFloat(res[that.key]) - json[i - 2][that.key]) > 0 ? that.colors[1] : that.colors[2];
res['min'] = (res[that.key] - json[i - 2][that.key]).toFixed(2);
} else {
let min = (res[that.key] - json[i - 2][that.key]).toFixed(2);
res['min'] = min;
if (min > 0) {
res['_inner_color'] = that.colors[1];
} else {
res['_inner_color'] = that.colors[2];
}
}
json.push(res);
}
that.jsonData = json;
// that.sortJsonData();
}
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
}
}
export default DynamicWaterfall;