图表分享

混合散点直方图

Shared by CK

讲解步骤

  1. 数据结构
  2. 图表结构
  3. 绘制前的准备: 配置, 数据预处理, 比例尺等
  4. 绘制直方图
  5. 绘制散点图
  6. 绘制滑动条

数据结构


const data = [
    {id: 1, date: '2017-12-18-09},
    {id: 2, date: '2017-12-18-11},
    {id: 3, date: '2017-12-18-15},
    {id: 4, date: '2017-12-19-22},
    {id: 5, date: '2017-12-20-12},
    {id: 6, date: '2017-12-21-13},
    {id: 7, date: '2017-12-22-14},
    {id: 8, date: '2017-12-23-15},
    {id: 9, date: '2017-12-24-16},
 ]
					

图表结构

总体结构


                    
                         

                         

                          
                    
                

直方图部分


                        
                            
                                
                                66
                            
                            
                        
                

滑动条部分


                    
                        
                        

                        
                        
                            2月18
                            
                        

                        
                        

                        
                        
                    
                

散点图部分


                        
                            
                             
                        
                    

绘制前的准备:

配置, 数据预处理, 比例尺等

1. 基础配置


                        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
                

2.1 数据预处理


                        const parseDate = d3.timeParse('%Y-%m-%d-%H')

                        data.forEach(d => {
                            d.date = parseDate(d.date)
                        })

                        // d3.timeParse 时间解析
                        //
                        // parseDate('2017-12-18-09')
                        //   =>
                        // Mon Dec 18 2017 09:00:00 GMT+0800 (CST)
                

2.2 数据预处理


                        const startDate = new Date('2017-12-18 00:00:00')
                        const endDate = new Date('2017-12-25 00:000:00')
                        const dateArray = d3.timeDays(startDate, endDate)

                        // d3.range(1, 5) => [1,2,3,4]
                        // d3.timeDays是d3.timeDay.range的别名
                        // 类似的还有 d3.timeYears,  d3.timeMonths, d3.timeSaturdays等
                
Down arrow

3.1 颜色比例尺


                        const colours = d3.scaleOrdinal()
                            .domain(dateArray)
                            .range([
                                '#409ffb', '#85d1ea', '#65cccb',
                                '#77debd', '#6ccb74', '#abdf81', '#fbd340'
                            ]);

                        // d3.scaleOrdinal - 创建一个序数比例尺
                

3.2 x轴时间比例尺


                        const x = d3.scaleTime()
                            .domain([startDate, endDate])
                            .range([0, width])
                            .clamp(true)

                    // 1. d3.scaleTime —— 创建时间线性比例尺
                    // 2. time.clamp —— 启用闭合
                

3.3 直方图生成器


                     const histogram = d3.histogram()
                            .value(d => d.date)
                            .domain(x.domain())
                            .thresholds(dateArray)

                    const bins = histogram(data)

                     // d3.histogram 创建一个新的直方图生成器 (本质是一个数据切分器)
                     // histogram.value 为每个样本指定一个值访问器 (指定你要切分的数据字段)
                     // histogram.domain 指定可观测值的间隔 (指定数据范围)
                     // histogram.thresholds 指定值划分成不同箱的方法 (指定切分的方法或者数组)
                

                    const bins = histogram(data)
                
Down arrow

3.4 Y轴线性比例尺


                    const y = d3.scaleLinear()
                            .domain([0, d3.max(bins, d => d.length)])
                            .range([0, histHeight])

                    // d3.scaleLinear - 创建定量线性比例尺
                    // d3.max d3.max - 计算数组中的最大值
                
Down arrow

3.5 绘制容器g


                      // 直方图容器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})`)
                

绘制直方图


                    // y轴比例尺
                    const y = d3.scaleLinear()
                       .domain([0, d3.max(bins, d => d.length)])
                       .range([0, histHeight])

                    const bar = hist.selectAll('.bar')
                      .data(bins).enter()
                      .append('g')
                      .attr('class', 'bar')
                      .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 => colours(d.x0))

                    bar.append('text')
                      .attr('y', '6')
                      .attr('x', d => (x(d.x1) - x(d.x0))/2)
                      .attr('text-anchor', 'middle')
                      .text(d => d.length)
                
Down arrow

绘制散点图


                    function drawPlot(data) {
                        const locations = plot.selectAll('.location')
                          .data(data, d => d.id)

                        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()
                    }
            

关于Update、Enter、Exit

  • 有数据,而没有足够图形元素的时候,使用enter方法可以添加足够的元素
  • 有元素与数据对应的部分称为 Update
  • 元素存在但是没有数据绑定的部分被称为 Exit
  • exit 部分的处理办法一般是:删除元素

绘制滚动条部分


                    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)
                          .attr('width', width)
                          .attr('height', 70)
                          .attr('fill', 'transparent')
                          .call(
                            d3.drag().on('start drag', update)
                          )
                    }
            

绑定拖拽事件


                        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)
                          )
                
  • d3.drag - 创建一个拖曳行为
  • drag.on - 监听拖曳事件
  • start - on mousedown or touchstart
  • drag - on mousemove or touchmove
  • end - on mouseup, touchend or touchcancel

更新数据


                        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')
                        }
                

What's more

谢谢观看