Skip to content

Instantly share code, notes, and snippets.

@ckinmind
Last active January 3, 2018 02:19
Show Gist options
  • Save ckinmind/9b2fdee3c77d032d18f02eb8b4ea100a to your computer and use it in GitHub Desktop.
Save ckinmind/9b2fdee3c77d032d18f02eb8b4ea100a to your computer and use it in GitHub Desktop.
histogram——以日期为时间维度

说明

  • 图表类型:直方图(混合散点图)
  • 原图地址:Date slider - histogram legend
  • 修改了原图的部分实现,将滑动条部分的实现从line改为rect
  • 将原图以时间为间隔改为了以天为间隔

知识点

  • d3.histogram - 直方图生成器
  • selections.exit() - 选取失去数据的节点
  • d3.timeParse - 时间格式化
  • d3.timeYears - 生成以年为间隔的时间范围数组
  • d3.drag - 创建一个拖曳行为
const typeArr = [
'feature', // 功能需求
'bug', // bug fix
'meeting', // 开会
'other' // 其他
]
const generate = (date, key, count) => {
return d3.range(0, count).map((item, index) => {
const num = (Math.random() * 23).toFixed(0)
return {
id: key + index,
date: `${date}-${num}`,
}
})
}
const arr1 = generate('2017-12-18', 'a', 65)
const arr2 = generate('2017-12-19', 'b', 100)
const arr3 = generate('2017-12-20', 'c', 25)
const arr4 = generate('2017-12-21', 'd', 55)
const arr5 = generate('2017-12-22', 'e', 45)
const arr6 = generate('2017-12-23', 'f', 43)
const arr7 = generate('2017-12-24', 'g', 22)
const data = [
...arr1,
...arr2,
...arr3,
...arr4,
...arr5,
...arr6,
...arr7,
]
<!DOCTYPE html>
<head>
<meta charset='utf-8'>
<style>
body {
font-family:'avenir next', Arial, sans-serif;
font-size: 12px;
color: #696969;
}
#vis{
margin: 0 auto;
width: 900px;
height: 500px;
}
.ticks {
font-size: 10px;
}
.track-overlay {
pointer-events: stroke;
stroke-width: 50px;
stroke: transparent;
cursor: crosshair;
}
.drag-layer {
cursor: crosshair
}
.handle {
fill: #fff;
stroke: #000;
stroke-opacity: 0.5;
stroke-width: 1.25px;
}
</style>
</head>
<body>
<div id='vis'></div>
<script src='https://d3js.org/d3.v4.min.js'></script>
<script src='./data.js'></script>
<script>
const margin = {top:50, right:50, bottom:50, left:50},
width = 900 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom
const histHeight = 100
const parseDate = d3.timeParse('%Y-%m-%d-%H')
const formatDateIntoDay = d3.timeFormat('%m月%d')
const startDate = new Date('2017-12-18 00:00:00')
const endDate = new Date('2017-12-25 00::00:00')
const dateArray = d3.timeDays(startDate, endDate)
data.forEach(d => {
d.date = parseDate(d.date)
})
// 颜色比例尺
const colors = d3.scaleOrdinal()
.domain(dateArray)
.range(['#409ffb', '#85d1ea', '#65cccb', '#77debd', '#6ccb74', '#abdf81', '#fbd340']);
// x轴比例尺
const x = d3.scaleTime()
.domain([startDate, endDate])
.range([0, width])
.clamp(true) // time.clamp - 启用闭合, 意思是如果入参超出定义域,则返回值会直接显示为边界值,查看https://github.com/d3/d3-scale#continuous_clamp
// 直方图生成器
const histogram = d3.histogram() // histogram 将离散样本分成连续的无重叠的间隔
.value(d => d.date) // histogram.value - 为每个样本指定一个值访问器
.domain(x.domain()) // histogram.domain - 指定可观测值的间隔
// .thresholds(x.ticks(d3.timeDay)) // histogram.thresholds - 指定值划分成不同箱的方法
.thresholds(dateArray) // histogram.thresholds - 指定值划分成不同箱的方法
const bins = histogram(data)
// y轴比例尺
const y = d3.scaleLinear()
.domain([0, d3.max(bins, d => d.length)])
.range([0, histHeight])
const svg = d3.select('#vis')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
// 直方图容器g
const hist = svg.append('g')
.attr('class', 'histogram')
.attr('transform', `translate(${margin.left}, ${margin.top})`)
// 滑动条容器g
const slider = svg.append('g')
.attr('class', 'slider')
.attr('transform', `translate(${margin.left}, ${margin.top + histHeight})`)
// 散点图容器g
const plot = svg.append('g')
.attr('class', 'plot')
.attr('transform', `translate(${margin.left}, ${margin.top + histHeight + 50})`)
drawHistogram() // 绘制直方部分
drawPlot(data) // 绘制散点部分
drawSlider() // 绘制滑动条部分
function drawHistogram() {
const bar = hist.selectAll('.bar')
.data(bins)
.enter()
.append('g')
.attr('transform', d => `translate(${x(d.x0)}, ${histHeight - y(d.length)})`)
bar.append('rect')
.attr('class', 'bar')
.attr('width', d => x(d.x1) - x(d.x0) - 1) // 1是柱子间的间隔
.attr('height', d => y(d.length))
.attr('fill', d => colors(d.x0))
bar.append('text')
.attr('dy', '.75em')
.attr('y', '6')
.attr('x', d => (x(d.x1) - x(d.x0))/2)
.attr('text-anchor', 'middle')
.text(d => d.length)
.attr('fill', 'white')
}
function drawSlider() {
// 轨迹背景条
slider.append('rect')
.attr('class', 'drag-bar')
.attr('x', 0)
.attr('y', 0)
.attr('width', width)
.attr('height', 10)
.attr('fill', '#dcdcdc')
.attr('rx', 4)
.attr('ry', 4)
slider.append('g', '.track-overlay')
.attr('class', 'ticks')
.attr('transform', 'translate(0, 18)')
.selectAll('text')
.data(x.ticks(7))
.enter()
.append('text')
.attr('x', x)
.attr('y', 10)
.attr('text-anchor', 'middle')
.text(d => formatDateIntoDay(d))
handle = slider.append('circle', '.track-overlay')
.attr('class', 'handle')
.attr('r', 9)
.attr('cy', 5)
// 可拖拽区域(时间绑定区域)
slider.append('rect')
.attr('class', 'drag-layer')
.attr('x', 0)
.attr('y', -35) // 起始位置是0轴的位置
.attr('width', width)
.attr('height', 70)
.attr('fill', 'transparent')
.call(
d3.drag().on('start drag', update)
)
}
function drawPlot(data) {
const locations = plot.selectAll('.location')
.data(data, d => d.id)
locations.exit().remove()
// 对新生成的节点添加动画
locations.enter()
.append('circle')
.attr('class', 'location')
.attr('cx', d => x(d.date))
.attr('cy', d => Math.random() * 100)
.attr('fill', d => colors(d3.timeDay(d.date)))
.attr('stroke', d => colors(d3.timeDay(d.date)))
.attr('opacity', 0.7)
.attr('r', 5)
.transition()
.duration(400)
.attr('r', 15)
.transition()
.attr('r', 5)
// 删除失去数据的节点
locations.exit().remove()
}
function update() {
const h = x.invert(d3.event.x)
// 改变拖动条条的位置
handle.attr('cx', x(h))
// 删除出当前位置以下的数据
const newData = data.filter(d => d.date < h)
// 重汇散点图
drawPlot(newData)
// 重新设置颜色, 小于h的还是原来的颜色,大于h的则置灰
d3.selectAll('.bar')
.attr('fill', d => d.x0 < h ? colors(d.x0) : '#eaeaea')
}
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment