使用d3.js实现x轴固定,y轴自由定位的简单知识图谱

741 阅读8分钟

为什么使用D3.js

  在前端可视化的项目中,一般常用的echarts,height.js等组件库,就可以满足需求,对于更加复杂的图形,需要图形库支持更加底层的api,这样就更加灵活。D3.js对svg实现提供了更加底层的api支持,不太了解svg的同学可以这篇文章结尾的svg系列链接。可以这样说,不管设计师如何发挥想象力,做出多么奇奇怪怪的2D图形,使用D3.js都是可以实现的。下面以这个知识图谱为例(x轴固定,y轴坐标自由,尽量使坐标点分散), 1732709158732.png 介绍D3.js的使用,当然抛砖引玉,时间有限,不对之处,敬请指正。

D3.js使用介绍

加载数据

  首先我们要使用数据就要加载数据,我们在项目的页面中使用服务器提供的数据,通过接口获取,如果要自己写一些dom,要自己造点数据,D3为我们提供了,多种加载本地数据的方式。 参见d3.fetch模块,这个模块提供了对图片,json,csv,等数据的加载。 在我们的代码里面使用了:

    const nodes = await d3.csv("../../dataBase/export.csv");
    const linkDatas = await d3.json("../../dataBase/link.json");

node作为节点数据,linkDatas作为连线数据。

比例尺

  有了数据我们就需要作图了,这里我们首先需要确定的就是坐标轴。要了解坐标轴,需要了解D3里面scale的概念,从名字就可以感觉到做的就是缩放和映射,我们知道坐标轴不一定是数字,最常见的年份,颜色值,甚至你想使用指数数列作为x轴坐标,D3都可以满足你。scale就是指将你各种各样X轴的范围映射到你要作图的宽度上。 例如你想将 0 -100的数字,映射到颜色空间的红到蓝上,你可以这样写:

d3.scaleLinear([0, 100], ["red", "blue"])

在这个例子中我们获取数据中年份的最小值和最大值映射到0-1920的横坐标上。

        const xScale = d3.scaleUtc()
        .domain([new Date(endShowTime),new Date(startShowTime)])
        .range([marginLeft, width - marginRight]);

具体可以参考d3js.org/d3-scale/ti… 。也可以在d3-scale里面找到其更加丰富的映射关系。通过这种映射关系,我们可以获取到个点的x轴坐标

   nodes.map(item=>{
      item.x = xScale(new Date(item.year));
    })

由于y轴不固定,我们可以通过简单的斥力模型,轻松的计算出来每个点的y坐标。至此坐标轴,x,y坐标都有了。

坐标轴

首先定义好svg元素定义好宽度和高度。

    const svg = d3.create("svg")
        .attr("width", width)
        .attr("height", height);

然后向svg中添加坐标轴

    const axis_x = svg.append("g")
        .attr("transform", `translate(0,${height - marginBottom})`)
        .call(d3.axisBottom(xScale).ticks(50))

注意在D3中,没有坐标轴的图形逻辑对象,你可以反复添加坐标轴,像是这样:

 const axis_ticks = svg.append("g")
          .attr("transform", `translate(0,${height - marginBottom})`)
          .call(d3.axisTop(xScale).tickValues([new Date('1921-07-01'),new Date("1949-10-01"),new Date('1992-08-07')])

他们之间并不冲突,我们可以做出各种各样的坐标轴。这时候使用更底层的控制,我们可以更改坐标轴的颜色,断续线样式等。

          axis_ticks.call(g => g.selectAll(".tick line")
              .attr("stroke-opacity", 0.5)
              .attr("stroke-dasharray", "2,2"))

坐标刻度

  D3提供了多种对坐标刻度控制的api。d3js.org/d3-axis#axi… ,添加好坐标刻度后,我们还可使用选择器对各个元素进行更加精细的操作。例如想实现如下的坐标刻度:

1732754896980.png 代码如下:

    const axis_x = svg.append("g")
        .attr("transform", `translate(0,${height - marginBottom})`)
        .call(d3.axisTop(xScale).ticks(d3.timeYear.every(10)).tickSize(20).tickFormat(d=>d.getFullYear()))
        .call(g => g.selectAll(".tick text").attr("dy", 45))
        .call(g => g.selectAll(".tick line").attr("stroke",'#000000'))

    const axis_ticks_small = svg.append("g")
        .attr("transform", `translate(0,${height - marginBottom})`)
        .call(d3.axisTop(xScale).ticks(d3.timeYear.filter((d) => d.getFullYear() % 10 !== 0)).tickSize(10).tickFormat(d=>''))

通过axisTop,axisBottom,等添加坐标轴,如果坐标轴需要修改,我们可以使用选择器,直接选择对应的元素,然后对这些svg元素做更加底层的操作。

选择器

  选择器和css选择器是相通的,并不难理解。用于如果我们使用D3的api添加过元素后,想再修改对应的元素。
例如我们现在需要在图形上加上点,代码如下:

    const nodeEvent =  svg.append("g").selectAll("g")
      .data(showNodes)
      .enter()
      .append('g');

      nodeEvent.append('circle')
      .attr('cx', d => xScale(new Date(d.year+'-01-01')))
      .attr('cy', d => yScale(d.y))
      .attr('r', d => d.important == '1' || d.important == '3' ? 40 : 30)
      .attr('fill',d => d.important == '3' ? "#C20921" : d.important == '1' ? '#F7B35E' : '#F3CC9C')
      .attr('fill-opacity',0.9)
      .on('click',function(e){
        console.log(e.target.__data__)
      });
      
           svg.append("g").selectAll('line')
        .data(links)
        .enter()
        .append('line')
        .attr('x1',d => xScale(new Date(d[0].year+'-01-01')))
        .attr('y1',d => yScale(d[0].y))
        .attr('x2',d => xScale(new Date(d[1].year+'-01-01')))
        .attr('y2',d => yScale(d[1].y))
        .attr('stroke', d => (d[0].id == '7' || d[1].id == '7') ? '#C20921' : '#5A1D05' )
        .attr('stroke-opacity',d => (d[0].id == '7' || d[1].id == '7') ? 0.5 : 0.1)
        .attr('stroke-width', 1)

首先创建一个分组,该分组下是所有点的集合,然后再选择该分组先所有的g元素,即使该元素现在还没有,在这些g元素上绑定上数据,通过enter方法,创建对应的g元素,再通过append方法,将这些缺失的g元素添加进dom。然后对每一个g元素添加圆和文字。到一步我们的图形已经基本画完了。

时间间隔处理

  注意到:我们在坐标刻度里面使用:d3.timeYear.every(10)方法,意思就是说以10年为间隔展示,D3提供了丰富的时间处理方法:d3js.org/d3-time ,在这些time的api中,interval表示时间间隔,可以轻松解决,每隔一定时间间隔显示的问题,和在某个时间段内的时间时间间隔,可以是日月年时分秒等。具体我们可以查询api,我们这里只需要知道D3提供了丰富的间隔处理方法,根据不同的需求,我们可以查找对应的api。

拖拽

D3.js 实现了非常简单的拖拽控制,通过以下代码就可以实现:

.call(d3.drag()
            .on("start", dragstarted)
            .on("drag", dragged)
            .on("end", dragended));
            
            function dragged(event, d) {
            d3.select(this).attr("cy", event.y);
        }

可以在dragged函数里面通过获取当前元素,控制元素的坐标位置。从而实现拖动。这里面有个比较坑的地方,如果你的y坐标经过缩放了,注意拖动时y坐标是需要重新计算的。

缩放

  D3里面提供了复杂的缩放功能,这里不再铺开讲解(因为这一块我也没有好好研究):主要有x/y轴缩放,缩放到指定区域,例如:zoom.extent([[x0, y0], [x1, y1]],)缩放到指定区域,下面是一个简单的例子:

  const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);
  const g = svg.append("g")
  g.selectAll("circle")
    .data(data)
    .join("circle")
      .attr("cx", ([x]) => x)
      .attr("cy", ([, y]) => y)
      .attr("r", 1.5);

  svg.call(d3.zoom()
      .extent([[0, 0], [width, height]])
      .scaleExtent([1, 8])
      .on("zoom", zoomed));

  function zoomed({transform}) {
    g.attr("transform", transform);
  }

  return svg.node();

渐变

  渐变色和svg的渐变是相同的需要自己定义,然后使用就行了。举个例子:

    const lineGradient = svg.append('defs').append('linearGradient').attr('id','ticks_line').attr('x1','0%').attr('y1','0%').attr('x2','0%').attr('y2','100%')
    lineGradient.append("stop").attr("offset",'0%').attr('stop-color','#A78574');
    lineGradient.append("stop").attr("offset",'100%').attr('stop-color','#B80E15');

然后再对应的需要渐变的西方使用就行了,注意:line的stroke属性不支持使用渐变色,需要使用path代替

动画

  D3提供了丰富的动画时间线控制方法,在 d3js.org/d3-ease 。例如我们css常用的动画,liner,ease-in,ease-in-out等,下面是一个简单的例子:

const t = d3.transition()
    .duration(750)
    .ease(d3.easeLinear);

d3.selectAll(".apple").transition(t)
    .style("fill", "red");

d3.selectAll(".orange").transition(t)
    .style("fill", "orange");

除此之外还有分步动画stop方法等,更高级的D3提供了时间线控制,有兴趣的同学可以深入研究。

数据处理

  D3本身提供了数据处理的功能在:d3js.org/d3-array 例如数组的分组(类似于loadsh的分组),求和,排序,交集,并集,例如:


d3.cross([1, 2], ["x", "y"]) // [[1, "x"], [1, "y"], [2, "x"], [2, "y"]];
d3.merge(new Set([new Set([1]), new Set([2, 3])])) // [1, 2, 3]
d3.transpose([["Alice", "Bob", "Carol"], [32, 13, 14]]) // [["Alice", 32], ["Bob", 13], ["Carol", 14]]
d3.cross([1, 2], ["x", "y"]) // [[1, "x"], [1, "y"], [2, "x"], [2, "y"]]
d3.range(0, 1, 1 / 49) // 👎 returns 50 elements!

同时D3还提供了获取随机分布数的方法,例如想获取正态分布的点:

d3.randomNormal(0, 1) // mean of 0, and standard deviation of 1

除此之外还有,平均分布,伽马分布等,可以构造需要的数据点集合。

总结

  总的来说我们可以使用先添加后修改的方式对图形进行各种必要的操作。以上只是D3js的一部分,除此之外还有颜色控制器,chord图,d3-force(模拟节点和力的效果),插值算法,高程图,地图,树型图,quadtree,等,内容还是非常精深的。这篇文章算是总览,后续会出一系列文章,介绍D3的各个模块。如果有对svg不太熟悉的同学可以参考一下文章:

svg之viewBox以及图像裁剪
svg基本图形,路径,分组
stroke-dasharray的应用实战
svg坐标系统与变换
svg变换实践指南
svg动画详解
svg背景,渐变,蒙层,剪切详解以及与css的对比
svg剪切,蒙层的应用指南
svg滤镜详解(一)
svg滤镜详解(二)
svg滤镜详解(三)
svg滤镜详解(四)
svg滤镜详解之feDisplacementMap滤镜
# svg滤镜详解之feTurbulence噪声滤镜