简介
使用JavaScript创建图表的最大优点是能够响应用户的输入。在本章中,我们将了解用户可以与我们的图形交互,以及如何实现它们。
d3 events
d3 selection 对象有一个.on()方法来为我们选择的dom元素创建监听事件。
例一
onMounted(() => {
const dataset = [0, 1, 2, 3]
const colorsRect = ['yellowgreen', 'cornflowerblue', 'seagreen', 'slateblue']
const xAxis = d3.scaleLinear().domain([0, dataset.length]).range([0, 400]).nice()
const rectGroups = d3
.select('#event-1')
.append('svg')
.attr('width', '100%')
.attr('height', '100%')
.attr('viewBox', '0, 0, 400,400')
.selectAll('rect')
.data(colorsRect)
.join('rect')
.attr('x', (d, i) => xAxis(i))
.attr('y', 160)
.attr('width', 80)
.attr('height', 80)
.attr('fill', '#cccccc')
})
首先添加四个rect,我们绑定的数据是一个颜色数组。 rectGroups是当前rect的selection对象。
通过.on()添加事件:
rectGroups.on('mouseenter', function (e, d) {
d3.select(this).attr('fill', d)
})
.on('mouseout', function (e, d) {
d3.select(this).attr('fill', '#cccccc')
})
第一个参数是事件触发方式
第二个参数为相应事件方法 方法中的参数e 为event事件,d为节点绑定的参数 此方法必须用function(){}写法,不能用()=>{},大家打印下两种情况下的this就能发现()=>{}中this指向windows,function写法指向this是当前事件元素,那我们就可以直接用d3.select(this)来获取selection对象以此使用相关方法。
鼠标悬浮修改颜色,移出再改回去。
现在需求只在三秒内能触发,三秒后去掉事件,只需要将rectGroups的两个相关事件置为null,但是三秒过后我们的鼠标可能还在某一个rect上颜色改变后out事件没有了就无法再改回去了,我们可以使用dispatch(eventType)方法, dispatch()会自动触发参数事件。
setTimeout(() => {
rectGroups.dispatch('mouseout').on('mouseenter', null).on('mouseout', null)
}, 3000)
barchart
实现鼠标悬浮在柱状图上方显示柱状图信息
我们使用之前写好的柱状图文件,将svg设置为宽高百分百适应外层div,但是画图时要保持宽高比,使用 preserveAspectRatio 属性实现:
const svg = d3
.select('#deom-bar-chart-event')
.append('svg')
.attr('viewBox', dimensions.viewBox)
.attr('width', '100%')
.attr('height', '100%')
.attr('preserveAspectRatio', 'xMinYMin')
添加一个显示数据的div
<div id="deom-bar-chart-event">
<div class="tooltip">
<div>范围:{{ tooltip.x0 }}-{{ tooltip.x1 }}</div>
<div>天数:{{ tooltip.length }}</div>
</div>
</div>
修改css将tooltip设置为绝对定位:
#deom-bar-chart-event {
position: relative;
.bin rect {
fill: cornflowerblue;
transition: height 1s ease-out, y 1s ease-out;
&:hover {
fill: purple;
}
}
}
.tooltip {
opacity: 0;
top: 0;
left: 0;
position: absolute;
padding: 0.6em 1em;
background: #fff;
text-align: center;
border: 1px solid #ddd;
z-index: 10;
transition: all 0.2s ease-out;
pointer-events: none;
}
.tooltip:before {
content: '';
position: absolute;
bottom: 0;
left: 50%;
width: 12px;
height: 12px;
background: white;
border: 1px solid #ddd;
border-top-color: transparent;
border-left-color: transparent;
transform: translate(-50%, 50%) rotate(45deg);
transform-origin: center center;
z-index: 10;
}
定义绑定数据
const tooltip = reactive({
x0: 0,
x1: 0,
length: 0,
})
在鼠标移入事件中给tooltip赋值:
.on('mouseenter', function (e, d) {
tooltip.x0 = d.x0
tooltip.x1 = d.x1
tooltip.length = d.length
}
计算tooltip坐标
tooltip初始坐标0,0,计划通过translate来移动到最终位置。
x: x坐标即 bounds距离左侧的位置 + 当前bar距离bounds左侧的位置,如果不使用viewbox,使用固定宽高按如下:
xScale(d.x0) + (xScale(d.x1) - xScale(d.x0)) / 2 + dimensions.margin.left
如果用了比例尺最终x的位置出现偏差,所以我们要计算出当前视图x轴的缩放比例:
Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置。
const xRate = (d3.select('.bins').node() as Element).getBoundingClientRect().width / dimensions.boundsWidth
let x = (xScale(d.x0) + (xScale(d.x1) - xScale(d.x0)) / 2 + dimensions.margin.left) * xRate
我们之前设置了以x为准等比例缩放 所以y:
let y = (50 + yScale(d.length)) * xRate - 7
- 7是为了去除div箭头的高度 当鼠标悬浮时,为了让div x方向居中,需要div左移自身百分之50%,y方向上移百分之百高度则正好整体贴在bar上方,设置透明度为一显示tooltip
d3.select('.tooltip')
.style('transform', `translate(calc(-50% + ${x}px),calc(-100% + ${y}px))`)
.style('opacity', 1)
最后设置鼠标移出事件:
.on('mouseleave', function (e, d) {
d3.select('.tooltip').style('opacity', 0)
})
散点图
再写散点图时发现如果按照柱状图写法求xy坐标非常麻烦,最后发现通过getBoundingClientRect方法可以获取target的真实大小和相对于窗口的真实位置,于是我将tooltip设置了fixed,直接用getBoundingClientRect获取的数据来给tooltip设置位置:
.on('mouseenter', function (e, d) {
tooltip.date = d.date
tooltip.humidity = d.humidity
// target相对窗口的x坐标 在加上target半径
let x = e.target.getBoundingClientRect().x + e.target.getBoundingClientRect().width / 2
// target相对窗口的y坐标 在去掉tooltip突出的高度
let y = e.target.getBoundingClientRect().y - 8.5
d3.select('.scatter-tooltip')
.style('transform', `translate(calc(-50% + ${x}px),calc(-100% + ${y}px))`)
.style('opacity', 1)
})
d3-voronoi
让我们简单地谈谈沃罗诺伊图。对于散点图上的每个位置,都有一个距离最近的点。沃罗诺伊图根据最近的点将一个平面划分为区域。每个部分内的任何位置都同意最近的点。Voronoi在许多领域都很有用——从创造艺术到检测神经肌肉疾病,再到开发森林火灾的预测模型。让我们看看我们的散点图被沃罗诺伊图分割时会是什么样子
在主的d3包中内置了一个沃罗诺伊包:d3-voronoi。但是,该模块不推荐使用,而是建议使用更快的d3-delaunay。
d3.Delaunay
d3.Delaunay接收三个参数
- 我们的数据数组
- x访问器函数
- y访问器函数
const delaunay = d3.Delaunay.from(
dataset,
d => xScale(xAccessor(d)),
d => yScale(yAccessor(d)),
)
现在我们想把我们的德拉奥内三角剖分变成一个沃罗诺图——谢天谢地,我们的三角剖分有一个.voronoi()方法。
const voronoi = delaunay.voronoi()
然后我们绑定数据,将每个数据绑定到要给类名为voronoi的path路径:
bounds
.selectAll('.voronoi')
.data(dataset)
.join('path')
.attr('class', 'voronoi')
.attr('d', (d, i) => voronoi.renderCell(i))
.attr('stroke', 'salmon')
效果如下
沃罗诺图超出了bounds,因为我们没有给他设置宽高,所以它使用了默认的宽高,打印voronoi:
可以看到它的xmax和ymax属性,我们将bounds的宽高赋值给他:
const voronoi = delaunay.voronoi()
voronoi.xmax = dimensions.boundedWidth
voronoi.ymax = dimensions.boundedHeight
正常了:
现在我们去掉.attr('stroke', 'salmon')这样就不会看到连线了
去掉背景色:
.voronoi {
fill: transparent;
}
添加鼠标事件:
- 鼠标移入时 在区域点绘制一个更大尺寸的点(tooltipDot)
- tooltipDot上显示tooltip
- 鼠标移出时删除tooltipDot,tooltip透明度重设为0
voronois
.on('mouseenter', function (e, d) {
//添加tooltipDot覆盖选中的点
const tooltipDot = bounds
.append('circle')
.attr('r', 5)
.attr('class', 'tooltip-dot')
.attr('cx', xScale(xAccessor(d)))
.attr('cy', yScale(yAccessor(d)))
.style('fill', 'maroon')
tooltip.date = d.date
tooltip.humidity = d.humidity
const tooltipDotNode = tooltipDot.node() as Element
let x = tooltipDotNode.getBoundingClientRect().x + tooltipDotNode.getBoundingClientRect().width / 2
let y = tooltipDotNode.getBoundingClientRect().y - 8.5
d3.select('.scatter-tooltip')
.style('transform', `translate(calc(-50% + ${x}px),calc(-100% + ${y}px))`)
.style('opacity', 1)
})
.on('mouseleave', function (e, d) {
d3.selectAll('.tooltip-dot').remove()
d3.select('.scatter-tooltip').style('opacity', 0)
})
折线图
思路:
- 在折线图任意位置都会触发事件 添加一个覆盖bounds的rect然后给其添加事件
- 找到最近的点添加样式 当鼠标在rect上移动计算距离鼠标x坐标最近的点,找到相应数据元素即可进行绘制。 使用了leastIndex()方法 以前为scan()
// 计算出当前传入数据和悬浮x坐标的距离绝对值
const getDistanceFromHoveredDate = (d: weatherData) => Math.abs(xAccessor(d).getTime() - hoveredDate.getTime())
// leastIndex 方法传入两个参数,1 比较的数据数组,2.迭代比较方法(a:当前值b:最小值) 从0开始(0时自己与自己比较为最小值)最终得出一个最小值的index
const index = d3.leastIndex(
dataset,
(a, b) => getDistanceFromHoveredDate(a) - getDistanceFromHoveredDate(b)
) as number
// 最近的元素获取到了
let data = dataset[index]
按照之前的方法进行绘制即可。