深入浅出d3.js数据可视化之道(5)

在经过了一个阶段的学习之后,我们对基本的坐标轴和比例尺都有了很好的了解,今天我们结合之前的内容,配合节流函数来制作一款精美的可交互折线图。

准备的数据


const  line_data = [
    {
        country: "china",
        gdp: [
                [2008, 2033],
                [2009, 2400],
                [2010, 4333],
                [2011, 5600],
                [2012, 6500],
                [2013, 6700],
                [2014, 6933],
                [2015, 7400],
                [2016, 7733],
                [2017, 8200]
             ]
    },
    {
        country: "japan",
        gdp: [
                [2008, 3333],
                [2009, 4400],
                [2010, 5233],
                [2011, 5800],
                [2012, 6333],
                [2013, 6400],
                [2014, 6533],
                [2015, 6700],
                [2016, 7033],
                [2017, 7200]
             ]
    }

]

添加坐标轴

坐标轴的建立在前几节已多次介绍,这里就不再赘述。

    const data = line_data;

    var initWidth = 340
    var initHeight = 500

    var padding = { left:40, top:10, right:20, bottom: 20}

    var height = initWidth - padding.top - padding.bottom
    var width  = initHeight - padding.left - padding.right


    var svg = d3.select("body")
                .append("svg")
                .attr("id", "chart")
                .attr("width", width)
                .attr("height", height)
                .style("padding-left", padding.left)
                .style("padding-right", padding.right)
                .style("padding-top", padding.top)
                .style("padding-bottom", padding.bottom)


     //添加y轴坐标轴

        //y轴比例尺
        let nums = [...data[0]["gdp"], ...data[1]["gdp"]].map(function(e){
            return e[1]
        })
        let yScale = d3.scaleLinear()
         .domain([0, d3.max(nums)])
         .range([height , 0]);

         let _yScale = d3.scaleLinear()
         .domain([0, d3.max(nums)])
         .range([0, height]);

        //定义y轴
        let yAxis = d3.axisLeft(yScale)
                      .tickFormat(d3.format("d")); //把x,xxx 的数据计数方式格式化,转化为不带逗号的格式

        //添加y轴
        svg.append("g")
        .attr("class","axis")
        .attr("transform","translate(" + 0 + "," + 0 + ")")
        .call(yAxis);

     //添加x轴坐标轴

         //x轴比例尺
         let years = data[0]["gdp"].map(function(e){
                    return e[0]
          })

         let xScale =  d3.scaleLinear()
                        .domain([2008,2017])
                        .rangeRound([0, width])

        let _xScale =  d3.scaleLinear()
                       .domain([0,width])
                       .rangeRound([2008, 2017])

        //定义x轴
        let xAxis = d3.axisBottom(xScale)
                            .tickFormat(d3.format("d"))

        //添加x轴
         svg.append("g")
            .attr("class","axis")
            .attr("transform","translate(" + "0 ," + height + ")")
            .call(xAxis);


坐标轴的样式

    .axis path {
        stroke: steelblue;
        stroke-width: 1
    }
    .axis .tick line{
      stroke: steelblue;
      stroke-width: 3
    }
生成的坐标轴

添加背景间隔线

添加网线的内容上一节已经介绍过了,这次只使用了y轴方向的网线,来帮助使用者确立数据位置

        //添加x轴
         svg.append("g")
            .attr("class","axis")
            .attr("transform","translate(" + "0 ," + height + ")")
            .call(xAxis);


        //添加
        // gridlines in x axis function
        function make_x_gridlines() {
            return d3.axisBottom(xScale)
                .ticks(years.length)

        }

        // add the X gridlines
      var grid =  svg.append("g")
            .attr("id", "grid")
            .attr("transform", "translate(0," + height + ")")
            .call(make_x_gridlines()
                .tickSize(-height)
                .tickFormat("")
            )

样式

    #grid .tick:nth-child(2) {
      display: none
    }
    #grid path {
      display: none
    }

效果展示


预览

绘制图形

绘制网线的时候我们使用了直线生成器,这里简要介绍一下直线生成器。

首先,在svg中,线段元素放的写法是

<line x1="20" y1="20" x2="100" y2="100" />

或者是

<path d="M20,20L100,100>

两个点形成的一条线段很容易手写生成出来,但是如果有成千上万个点就很不方便了,因此d3 引入了 路径生成器 这个概念,能够自动根据数据生成路径。用于生成线段的生成器也就叫做 线段生成器

例如:

       var  lines = [  [80, 80], [200, 100],  [200, 200], [100, 200]  ]
       var linePath = d3.line()

        svg.append("path")
             .attr("d", linePath(lines))
             .attr("stroke-width", "2px")
             .attr("stroke",  "red")
             .attr("fill", "none")

生成效果


生成的路径

了解了这个知识点之后,继续绘制折线图

        //创建一个直线生成器
        var linePath = d3.line()
                          .curve(d3.curveCardinal.tension(0.5))
                         .x( function(d){ return xScale(d[0]) })
                         .y( function(d){ return yScale(d[1])})


        var colors = ["rgb(0, 188, 212)", "rgb(255, 64, 129)"]


        //添加路径
        svg.append("g").selectAll("path")
            .data(data)
            .enter()
            .append("path")
            .attr("transform","translate(0, 0)")
            .attr("d", function(d){
                return linePath(d.gdp)
            })
            .attr("fill", "none")
            .attr("stroke-width", "2px")
            .attr("stroke", function(d, i){
              return colors[i]
            })

添加路径之后的效果


预览

添加左侧指示栏

类别指示栏在上一节已经介绍过了,这一节也就不过多介绍,代码示下

      
        var cover =svg.append("g")

            cover.selectAll("rect")
                .data(data)
                .enter()
                .append("rect")
                .attr("width", 10)
                .attr("height", 10)
                .attr("fill", function(d, i){
                  return i%2 == 0 ? colors[0] : colors[1]
                })
                .attr("transform", function(d, i){
                    return `translate(10, ${(i)*20})`
                })


            cover.selectAll("text")
                  .data(data)
                  .enter()
                  .append("text")
                  .text(function(d, i){
                    return d.country
                  })
                  .attr("transform", function(d, i){
                      return `translate(27, ${(i)*20})`
                  })
                  .attr("font-size", '12px')
                  .attr("dy",function(){
                    return '0.75em'
                  })
                  .attr("fill", function(){
                    return '#333'
                  })

此时的效果


预览

添加提示栏和准线

截至到这里,已经完成了折线图的基本制作。接下来添加一些交互效果。

  • 滑动准线
  • 内容提示框

最后要达到的效果,鼠标在图表移动时,准线吸附到最近的参考线,并且提示栏内显示该参考线位置上折线的数据

其实思路很简单,只要计算出每两个参考线之间的距离singleStep,就可以根据鼠标位置找到当前鼠标距离哪两个参考线之间,并且距离哪个参考线更近,判断出来之后准线就吸到相应的参考线。

这里采用了mousemove事件来实时判断当前鼠标位置并进行运算,事实上,并不需要实时触发这个函数,准线吸附速度只要流畅就可以,大量触发会极大浪费计算机性能。这里采用了一个高级函数,通过控制函数在多少毫秒内只执行一次,来帮助解决这个问题。节流函数详细讲解在这里

节流函数

//节流函数
var throttle = function (fn, interval) {
        var  timer, firstTime = true;

        return function () {
          var args = arguments;
          var _me = this;

           if ( firstTime ) {
             fn.apply(_me, args)
             return firstTime = false;
           }

           if ( timer ) {
             return false
           }

           timer = setTimeout(function () {
             clearTimeout(timer);
             timer=null;
             fn.apply(_me, args)
           }, interval || 500)
        }

    }

绘制提示栏和准线(无刻度的y轴)



        var detailLine = svg.append("g")
                            .attr("class","line_y")
                            .attr("transform","translate(" + width + "," + 0 + ")")
                            .call(yAxis.ticks(0).tickSize(0).tickFormat(""));

        //添加提示栏
        var tooltip = d3.select("body")
                        .append("div")
                        .attr("class", "tooltip")
                        .style("opacity", 0)

        //计算位置 便于吸附
        let singleStep = width / (years.length-1)

        //这里使用节流函数,避免过多运算导致浏览器卡顿
        document.getElementById('chart').onmousemove =throttle(function(e){
              console.log(e.offsetX)
              e.stopPropagation();
              let t = Math.round((e.offsetX - padding.left) / singleStep)*singleStep
              detailLine.attr("transform","translate(" + t + "," + 0 + ")")
              let year = _xScale(t)
              let currentHtml = []
              data.forEach( (e) => {
                  e.gdp.forEach( (ev, i) =>{
                    if(ev[0]==year){
                      currentHtml.push(`<div>${e.country}: ${ev[1]}</div>`);
                    }
                  })
              })
            currentHtml.unshift(`<div>${year}</div>`)
            tooltip.html(currentHtml.join(""))
                   .style("left", e.pageX + 20+ "px")
                   .style("top", e.pageY + 20 + "px")
                   .style("opacity", 1)
        },50)


        //隐藏显示栏
        document.onclick= function(){
          tooltip.style("opacity", 0)
        }

提示栏样式

    .tooltip {
        position: absolute;
        min-width: 100px;
        height: auto;
        font-size: 14px;
        text-align: center;
        border: 1px solid #666;
        border-radius: 5px;
        color: #fff;
        background:rgba(0, 0, 0, 0.8);
        padding-bottom: 5px;
        transition: transform 0.2s
    }

最终效果

效果

源码地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容