datavizstocklib
Version:
测试一下呗
504 lines (470 loc) • 17.8 kB
JavaScript
import * as d3 from "d3";
let config = {
/** 控件名 */
name: "DynamicTreemap",
/** 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: [],
/** 最大文字的字号 */
YfontSize: 25, //字号
/** 字体 */
fontfamilyBase: "SourceHanSansK-Regular-plotly,Helvetica Neue,PingFang SC,Microsoft YaHei,Helvetica,Hiragino Sans GB,Arial,sans-serif",
/** 文字颜色 */
rectTextColor: "#A3A3A3",
/** 条数 */
barNumber: 5,
/** 柱高度自适应 */
rectHeightFixed: false,
// 柱子高度
rectHeight: 100,
/** 柱高度和 柱间距的比例 */
rectRate: 5,
/** 内部使用 */
rectSpace: 0,
/** X轴坐标位置 bottom top */
xAxisPos: "top",
/** 动画时长 */
durationTime: 1000,
/** 最大值 */
maxValue: 0,
/** 延时播放 */
delay: 2000,
height: 35,
/** 内部使用 */
isRun: false,
replay: false,
/** 矩形圆角 */
rx: 2,
index: 0,
dataIndex: 0,
timeout: null,
xSicks: 5,
showThousands: true,
pointFormat: 'auto',
el: '_dataviz',
d3Ease: '',
// 是否填充渐变色
ifGradient: false,
// 是否填显示尖角
rectSharpCorner: false,
// 图片
useImages: true,
// 名称显示在左边,否则显示在柱子上
nameAtLeft: true,
showLabel: true,
showRanking: true,
rankingFontsize: 30,
showPercent: false
}
export class DynamicTreemap {
/** 构造器 */
constructor() {
let newconfig = JSON.parse(JSON.stringify(config));
Object.assign(this, newconfig)
}
/** 更新图表 */
update(obj) {
// 复制新配置
Object.assign(this, obj)
}
/** 初始化 */
init(obj, callback) {
let that = this;
Object.assign(this, obj);
this.index = 1
let c = that.colors.concat()
for (let i = 0; i <= that.data.length; i = i + c.length) {
that.colors = that.colors.concat(c)
}
this.getEase();
that.arrayToJson();
that.svg = d3.select("#" + this.el).append("svg");
if (that.clientWidth == 0) {
that.clientWidth = document.getElementById(this.el).clientWidth;
}
that.svg.attr("width", that.clientWidth)
.attr("height", that.clientHeight);
//创建y轴比例尺
that.chart = that.svg.append("g")
.attr("width", that.clientWidth)
.attr("height", that.clientHeight)
.attr("transform", `translate(${that.margin[3]},${that.margin[0]})`)
that.svg.append("g").attr("class", "timeTextG")
.append("text")
.attr("class", "topText")
.attr("x", that.clientWidth * 0.9)
.attr("y", that.margin[0] - that.rectRate - 10)
.text('')
.attr("text-anchor", "end")
.attr("font-size", this.rankingFontsize)
.attr("fill", () => {
return that.fontColor
});
that.updateChart(callback);
}
updateObj(obj, callback) {
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 {
let c = this.colors.concat()
for (let i = 0; i <= this.data.length; i = i + c.length) {
this.colors = this.colors.concat(c)
}
this.updateChart(callback)
}
}
// 页面渲染
updateChart(callback) {
let width = this.clientWidth - this.margin[1] - this.margin[3];
let height = this.clientHeight - this.margin[0] - this.margin[2]
this.treemap = data => d3.treemap()
.tile(d3.treemapResquarify)
.size([width, height])
.padding(this.treemapBorder)
.round(true)(d3.hierarchy(data, (d) => d.children)
.sum(d => d.value))
this.chart.attr("transform", `translate(${this.margin[3]},${this.margin[0]})`)
this.svg.attr("width", this.clientWidth)
.attr("height", this.clientHeight);
let { showData, showTime, ifEnd } = this.getData();
this.svg.select(".topText")
.attr("x", this.clientWidth * 0.9)
.attr("y", this.margin[0] - this.rectRate - 10)
.text(this.showRanking ? showTime : "")
.attr("fill", () => {
return this.fontColor
})
.transition()
.duration(this.durationTime)
.ease(this.d3Ease)
.attr("font-size", this.rankingFontsize)
.on("end", () => {
if (!ifEnd) {
if (this.isPlay) {
callback(this.index, false)
this.updateChart(callback);
this.index++;
}
} else {
callback(this.index, true)
this.index = 0
}
})
const root = this.treemap({ name: "", children: showData });
this.chart.selectAll("g")
.data(root.leaves())
.join(
enter => {
const leaf = enter.append("g");
this.enterRect(leaf)
},
update => { this.updateRect(update) },
exit => { this.exitRect(exit) }
)
// .attr("transform", d => `translate(${d.x0},${d.y0})`);
}
exitRect(leaf) {
leaf.select("rect")
.attr("rx", this.rx)
.transition()
.duration(this.durationTime)
.ease(this.d3Ease)
.attr("fill", (d, i) => { return this.colors[i]; })
.attr("width", 0)
.attr("height", 0);
leaf
.transition()
.duration(this.durationTime)
.ease(this.d3Ease)
.attr("opacity", 0.0001)
.remove();
}
updateRect(leaf) {
const self = this
leaf
.transition()
.duration(this.durationTime)
.ease(this.d3Ease)
.attr("transform", d => {
if (this.textAlign == "Center") {
let showText = this.showLabel ? [d.data.name].concat(d.value) : [d.data.name].concat('')
let maxLength = d3.max(showText, (c) => ("" + c).length)
let width = d.x1 - d.x0
let height = d.y1 - d.y0
d.fontSize = Math.min(Math.min(width / maxLength, height / 3), 30)
}
return `translate(${d.x0},${d.y0})`
});
leaf.select("rect")
.transition()
.duration(this.durationTime)
.ease(this.d3Ease)
.attr("width", d => d.x1 - d.x0)
.attr("height", d => d.y1 - d.y0)
.attr("fill", (d, i) => { return this.colors[i]; })
leaf.select("clipPath")
.select("use")
.attr("transform", (d) => {
let x = this.textAlign == "Center" ? (d.x1 - d.x0) / 2 : 0
let y = this.textAlign == "Center" ? (d.y1 - d.y0) / 2 - d.fontSize : 0
return `translate(${-x},${-y})`
})
leaf.select("text")
.transition()
.duration(this.durationTime)
.ease(this.d3Ease)
.attr("transform", (d) => {
let x = this.textAlign == "Center" ? (d.x1 - d.x0) / 2 : 0
let y = this.textAlign == "Center" ? (d.y1 - d.y0) / 2 - d.fontSize : 0
return `translate(${x},${y})`
})
.attr("text-anchor", this.textAlign == "Center" ? "middle" : "start")
.attr("font-size", (d) => {
if (this.textAlign !== "Center") {
return this.textFontSize
} else {
return d.fontSize
}
})
leaf.select("text")
.selectAll("tspan")
.data(d => {
//.split(/(?=[A-Z][a-z])|\s+/g)
return this.showLabel ? [d.data.name].concat(d.value) : [d.data.name].concat('')
})
.attr("x", this.textAlign === "Center" ? 0 : 3)
.attr("y", (d, i, nodes) => `${(i === nodes.length - 1) * 0.3 + 1.1 + i * 0.9}em`)
.attr("fill-opacity", (d, i, nodes) => i === nodes.length - 1 ? 0.7 : 1)
.style("font-family", "regular")
.transition()
.duration(this.durationTime)
.ease(this.d3Ease)
.attr("fill", this.fontColor)
.tween("text", function(d, i, data) {
let that = this
if (self.interpolate && i == data.length - 1 && self.showLabel) {
let preNum = d3.select(this).text().split("%")[0].split(",").join("")
let maxLength = Math.max(self.countDecimals(preNum), self.countDecimals(d))
let interNum = d3.interpolateNumber(Number(preNum), Number(d))
return function(t) {
d3.select(that).text(self.format(Number(interNum(t).toFixed(maxLength))))
}
} else {
if (i == data.length - 1)
d3.select(this).text(self.showLabel ? self.format(d) : "")
else
d3.select(this).text(d)
}
});
}
enterRect(leaf) {
let self = this
leaf
.transition()
.duration(this.durationTime)
.ease(this.d3Ease)
.attr("transform", d => {
if (this.textAlign == "Center") {
let showText = this.showLabel ? [d.data.name].concat(d.value) : [d.data.name].concat('')
let maxLength = d3.max(showText, (c) => ("" + c).length)
let width = d.x1 - d.x0
let height = d.y1 - d.y0
d.fontSize = Math.min(Math.min(width / maxLength, height / 3), 30)
}
return `translate(${d.x0},${d.y0})`
});
leaf.append("title")
.text(d => `${d.ancestors().reverse().map(d => d.data.name).join("")}\n${this.format(d.value)}`);
leaf.append("rect")
.attr("id", (d, i) => {
d.leafUid = "leaf" + i
return d.leafUid
})
.attr("rx", this.rx)
.attr("fill", (d, i) => { return this.colors[i]; })
.attr("fill-opacity", 0.8)
.transition()
.duration(this.durationTime)
.ease(this.d3Ease)
.attr("width", d => d.x1 - d.x0)
.attr("height", d => d.y1 - d.y0);
leaf.append("clipPath")
.attr("id", (d, i) => {
d.clipUid = "clip" + i
return d.clipUid
})
.append("use")
.attr("transform", (d) => {
let x = this.textAlign == "Center" ? (d.x1 - d.x0) / 2 : 0
let y = this.textAlign == "Center" ? (d.y1 - d.y0) / 2 - d.fontSize : 0
return `translate(${-x},${-y})`
})
.attr("xlink:href", d => "#" + d.leafUid);
leaf.append("text")
.transition()
.duration(this.durationTime)
.ease(this.d3Ease)
.attr("transform", (d) => {
let x = this.textAlign == "Center" ? (d.x1 - d.x0) / 2 : 0
let y = this.textAlign == "Center" ? (d.y1 - d.y0) / 2 - d.fontSize : 0
return `translate(${x},${y})`
})
.attr("text-anchor", this.textAlign == "Center" ? "middle" : "start")
.attr("font-size", (d) => {
if (this.textAlign !== "Center") {
return this.textFontSize
} else {
return d.fontSize
}
})
.attr("clip-path", d => `url(#${d.clipUid})`)
// .attr("font-size",this.textFontSize)
leaf.select("text")
.selectAll("tspan")
.data(d => {
//.split(/(?=[A-Z][a-z])|\s+/g)
return this.showLabel ? [d.data.name].concat(d.value) : [d.data.name].concat('')
})
.join("tspan")
.attr("x", this.textAlign === "Center" ? 0 : 3)
.attr("y", (d, i, nodes) => `${(i === nodes.length - 1) * 0.3 + 1.1 + i * 0.9}em`)
.attr("fill-opacity", (d, i, nodes) => i === nodes.length - 1 ? 0.7 : 1)
.style("font-family", "regular")
.attr("fill", this.fontColor)
.transition()
.duration(this.durationTime)
.ease(this.d3Ease)
.tween("text", function(d, i, data) {
let that = this
if (self.interpolate && i == data.length - 1 && self.showLabel) {
let maxLength = Math.max(0, self.countDecimals(d))
let interNum = d3.interpolateNumber(0, Number(d))
return function(t) {
d3.select(that).text(self.format(Number(interNum(t).toFixed(maxLength))))
}
} else {
if (i == data.length - 1) {
d3.select(this).text(self.showLabel ? self.format(d) : "")
} else {
d3.select(this).text(d)
}
}
});
}
countDecimals(value) {
if (!value) {
return 0
}
if (Math.floor(value) === Number(value)) return 0;
if (!String(value).split(".")[1]) {
return 0
}
return String(value).split(".")[1].length || 0;
}
getData() {
// 获取当前时间
let showTimeIndex = this.index > this.columnName.length - 1 ?
this.columnName.length - 1 : this.index
let showTime = this.columnName[showTimeIndex]
let showData = this.jsonData.map(d => {
return {
value: +d[showTime],
name: d[this.columnName[0]]
}
})
return { showData, showTime, ifEnd: this.index > this.columnName.length - 1 ? true : false }
}
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]
}
}
destroy() {
clearTimeout(this.timeout);
if (this.timeout != null) this.timeout.stop();
Object.assign(this, null)
}
/** 千分位格式化 */
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 = Number(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
}
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);
}
that.jsonData = json;
if (that.useImages) this.updataImages();
// that.sortJsonData();
}
}
export default DynamicTreemap;