上一篇: 基础散点图 https://blog.csdn.net/zjw_python/article/details/98483109
下一篇: 气泡图 https://blog.csdn.net/zjw_python/article/details/98485368
代码结构和初始化画布的Chart对象介绍,请先看 https://blog.csdn.net/zjw_python/article/details/98182540
本图完整的源码地址: https://github.com/zjw666/D3_demo/tree/master/src/scatterChart/multiSymbolChart
1 图表效果
2 数据
x,y,x1,y1,x2,y2,x3,y3
24,12,95,45,35,75,32,85
39,20,74,62,82,24,34,46
78,15,14,64,3,75,64,19
15,80,32,67,45,62,84,39
36,70,10,68,36,10,38,49
7,11,67,2,67,14,34,20
36,13,31,47,95,9,74,73
34,89,34,75,5,98,3,2
9,95,74,69,31,47,32,20
72,35,26,65,16,36,99,65
46,78,85,42,32,14,43,12
73,95,8,6,81,62,10,95
3 关键代码
导入数据
d3.csv('./data.csv', function(d){
return {
x: +d.x,
y: +d.y,
x1: +d.x1,
y1: +d.y1,
x2: +d.x2,
y2: +d.y2,
x3: +d.x3,
y3: +d.y3,
};
}).then(function(data){
......
样式参数配置
const config = {
margins: {top: 80, left: 80, bottom: 50, right: 80},
textColor: 'black',
gridColor: 'gray',
ShowGridX: [10, 20, 30, 40, 50, 60, 70 ,80, 90, 100],
ShowGridY: [10, 20, 30, 40, 50, 60, 70 ,80, 90, 100],
title: '多符号散点图',
pointSize: 100,
hoverColor: 'white',
animateDuration: 1000
}
尺度转换,这里要注意,X和Y轴的最大值的计算
/* ----------------------------尺度转换------------------------ */
chart.scaleX = d3.scaleLinear()
.domain([0, Math.ceil(d3.max(data, (d) => getMaxNum(d,'x'))/10)*10])
.range([0, chart.getBodyWidth()]);
chart.scaleY = d3.scaleLinear()
.domain([0, Math.ceil(d3.max(data, (d) => getMaxNum(d,'y'))/10)*10])
.range([chart.getBodyHeight(), 0]);
//获取数据行最大x值或y值
function getMaxNum(d, type){
const nums = [];
Object.keys(d).forEach((key) => {
if (key.indexOf(type) > -1) nums.push(d[key]);
})
return d3.max(nums);
}
渲染数据点,为方便数据绑定,我们先转换了一下数据的结构,渲染步骤跟基础散点图类似,区别在于我们使用path
元素,通过d3.symbol()
指定不同的数据点形状
/* ----------------------------渲染数据点------------------------ */
chart.renderPoints = function(){
/*
改变数据结构,方便渲染
[
[[x,y],[x,y],...],
[[x1,y1],[x1,y1],...],
[[x2,y2],[x2,y2],...],
[[x3,y3],[x3,y3],...],
]
*/
const tempData = data.map((d) => {
const items = [];
d3.permute(d, Object.keys(d)).forEach((item,i,array) => {
if (i % 2 === 0){
items.push([array[i],array[i+1],i/2]);
}
});
return items;
});
const multiData = d3.zip.apply(this,tempData);
let groups = chart.body().selectAll('.g')
.data(multiData);
let points = groups.enter()
.append('g')
.merge(groups)
.attr('class', (d,i) => 'g points-' + i)
.attr('fill', (d,i) => chart._colors(i))
.selectAll('.point')
.data((d) => d);
groups.exit()
.remove();
points.enter()
.append('path')
.classed('point', true)
.merge(points)
.attr('transform', (d) => 'translate(' + chart.scaleX(d[0]) + ',' + chart.scaleY(d[1]) + ')')
.attr('d', d3.symbol().type(function (d) {
return d3.symbols[d[2]];
}).size(1))
.transition().duration(config.animateDuration)
.attr('d', d3.symbol().type(function (d) {
return d3.symbols[d[2]];
}).size(config.pointSize));
points.exit()
.remove();
}
接着就是渲染坐标轴、文本标签和网格线等
/* ----------------------------渲染坐标轴------------------------ */
chart.renderX = function(){
chart.svg().insert('g','.body')
.attr('transform', 'translate(' + chart.bodyX() + ',' + (chart.bodyY() + chart.getBodyHeight()) + ')')
.attr('class', 'xAxis')
.call(d3.axisBottom(chart.scaleX));
}
chart.renderY = function(){
chart.svg().insert('g','.body')
.attr('transform', 'translate(' + chart.bodyX() + ',' + chart.bodyY() + ')')
.attr('class', 'yAxis')
.call(d3.axisLeft(chart.scaleY));
}
chart.renderAxis = function(){
chart.renderX();
chart.renderY();
}
/* ----------------------------渲染文本标签------------------------ */
chart.renderText = function(){
d3.select('.xAxis').append('text')
.attr('class', 'axisText')
.attr('x', chart.getBodyWidth())
.attr('y', 0)
.attr('fill', config.textColor)
.attr('dy', 30)
.text('X');
d3.select('.yAxis').append('text')
.attr('class', 'axisText')
.attr('x', 0)
.attr('y', 0)
.attr('fill', config.textColor)
.attr('dx', '-30')
.attr('dy', '10')
.text('Y');
}
/* ----------------------------渲染网格线------------------------ */
chart.renderGrid = function(){
d3.selectAll('.yAxis .tick')
.each(function(d, i){
if (config.ShowGridY.indexOf(d) > -1){
d3.select(this).append('line')
.attr('class','grid')
.attr('stroke', config.gridColor)
.attr('x1', 0)
.attr('y1', 0)
.attr('x2', chart.getBodyWidth())
.attr('y2', 0);
}
});
d3.selectAll('.xAxis .tick')
.each(function(d, i){
if (config.ShowGridX.indexOf(d) > -1){
d3.select(this).append('line')
.attr('class','grid')
.attr('stroke', config.gridColor)
.attr('x1', 0)
.attr('y1', 0)
.attr('x2', 0)
.attr('y2', -chart.getBodyHeight());
}
});
}
添加鼠标交互事件,与基础散点图相似
/* ----------------------------绑定鼠标交互事件------------------------ */
chart.addMouseOn = function(){
//防抖函数
function debounce(fn, time){
let timeId = null;
return function(){
const context = this;
const event = d3.event;
timeId && clearTimeout(timeId)
timeId = setTimeout(function(){
d3.event = event;
fn.apply(context, arguments);
}, time);
}
}
d3.selectAll('.point')
.on('mouseover', function(d){
const e = d3.event;
const position = d3.mouse(chart.svg().node());
e.target.style.cursor = 'hand'
d3.select(e.target)
.attr('fill', config.hoverColor);
chart.svg()
.append('text')
.classed('tip', true)
.attr('x', position[0]+5)
.attr('y', position[1])
.attr('fill', config.textColor)
.text('x: ' + d[0] + ', y: ' + d[1]);
})
.on('mouseleave', function(d){
const e = d3.event;
d3.select(e.target)
.attr('fill', chart._colors(d[2]));
d3.select('.tip').remove();
})
.on('mousemove', debounce(function(){
const position = d3.mouse(chart.svg().node());
d3.select('.tip')
.attr('x', position[0]+5)
.attr('y', position[1]-5);
}, 6)
);
}