datavizstocklib
Version:
测试一下呗
552 lines (518 loc) • 19.8 kB
JavaScript
import * as d3 from "d3";
let config = {
/** 控件名 */
name: "DynamicPackedCircle",
/** 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
}
var colorRgb = function(sColor){
sColor = sColor.toLowerCase();
//十六进制颜色值的正则表达式
var reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;
// 如果是16进制颜色
if (sColor && reg.test(sColor)) {
if (sColor.length === 4) {
var sColorNew = "#";
for (var i=1; i<4; i+=1) {
sColorNew += sColor.slice(i, i+1).concat(sColor.slice(i, i+1));
}
sColor = sColorNew;
}
//处理六位的颜色值
var sColorChange = [];
for (var i=1; i<7; i+=2) {
sColorChange.push(parseInt("0x"+sColor.slice(i, i+2)));
}
return "rgb(" + sColorChange.join(",") + ")";
}
return sColor;
};
export class DynamicPackedCircle {
/** 构造器 */
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;
}
if (this.clientHeight > this.clientWidth) {
this.margin[1] = this.margin[3] = 40
}
that.svg.attr("width", that.clientWidth)
.attr("height", that.clientHeight);
let width = this.clientWidth - this.margin[1] - this.margin[3];
let height = this.clientHeight - this.margin[0] - this.margin[2];
that.svg.append("circle")
.attr("class", "circle-bg")
.attr("r", width > height ? height / 2 : width / 2)
.attr("fill", colorRgb(this.backgroundColor))
.attr("fill-opacity",.2)
.attr('transform', `translate(${width/2+this.margin[3]},${height/2+this.margin[0]})`);
//创建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.clientHeight > this.clientWidth) {
this.margin[1] = this.margin[3] = 40
}
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 that = this
let width = this.clientWidth - this.margin[1] - this.margin[3];
let height = this.clientHeight - this.margin[0] - this.margin[2];
this.treemap = data => d3.pack()
// .tile(d3.treemapResquarify)
.size([width, height])
.padding(this.treemapBorder)
(d3.hierarchy(data, (d) => d.children)
.sum(d => d.value)
.sort((a, b) => {
return b.value - a.value;
}))
this.chart.attr("transform", `translate(${this.margin[3]},${this.margin[0]})`)
this.svg.select(".circle-bg")
// .attr("fill",this.colors[0])
// .attr("opacity",.2)
.transition()
.duration(this.durationTime)
.ease(this.d3Ease)
.attr("opacity", .2)
.attr("r", width > height ? height / 2 : width / 2)
.attr('transform', `translate(${width/2+this.margin[3]},${height/2+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.isDark ? "#808080" : this.fontColor
})
.transition()
.duration(this.durationTime)
.ease(this.d3Ease)
.attr("font-size", this.rankingFontsize)
.on("end", () => {
if (!ifEnd) {
if (this.isPlay) {
setTimeout(function() {
callback(that.index, false)
that.updateChart(callback);
that.index++;
}, 300)
}
} else {
callback(this.index, true);
this.index = 0;
}
})
const root = this.treemap({ name: "", children: showData });
this.chart.selectAll("g")
.data(root.leaves(), function(d) { //root.descendants()
return d.data.name
})
.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")
.transition()
.duration(this.durationTime)
.ease(this.d3Ease)
.attr("fill", (d, i) => { return this.colors[i]; })
.attr("r", 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.r
d.fontSize = Math.min(width / maxLength, 30) * self.circlefontsize
}
return `translate(${d.x},${d.y})`
});
leaf.select("circle")
.transition()
.duration(this.durationTime)
.ease(this.d3Ease)
.attr("r", d => d.r)
.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 + 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)
}
});
leaf.select("title").text(d => `${d.ancestors().reverse().map(d => d.data.name).join("")}\n${this.format(d.value)}`);
}
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.r
d.fontSize = Math.min(width / maxLength, 30) * self.circlefontsize
}
return `translate(${d.x},${d.y})`
});
leaf.append("title")
.text(d => `${d.ancestors().reverse().map(d => d.data.name).join("")}\n${this.format(d.value)}`);
leaf.append("circle")
.attr("id", (d, i) => {
d.leafUid = "leaf" + i
return d.leafUid
})
.attr("r", 0)
.attr("fill", (d, i) => { return this.colors[i]; })
.attr("fill-opacity", 0.8)
.transition()
.duration(this.durationTime)
.ease(this.d3Ease)
.attr("r", d => d.r)
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 + 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 DynamicPackedCircle;