Skip to content
This repository has been archived by the owner on Jan 17, 2023. It is now read-only.
Pavel Rodionov edited this page Jan 16, 2018 · 19 revisions

D3js

D3 is a JavaScript library for producing dynamic, interactive data visualizations in web browsers. It makes use of the widely implemented SVG, HTML5, and CSS standards.

To start working with D3js just add the following line:

<script src="https://d3js.org/d3.v4.js"></script>

D3 lingo:

SELECTIONS:

D3 allows you to bind arbitrary data to a Document Object Model (DOM), and then apply data-driven transformations to the document. For example, you can use D3 to generate an HTML table from an array of numbers. Or, use the same data to create an interactive SVG bar chart with smooth transitions and interaction.

D3 employs a declarative approach, operating on arbitrary sets of nodes called selections.

D3 provides numerous methods for mutating nodes: setting attributes or styles; registering event listeners; adding, removing or sorting nodes; and changing HTML or text content. These suffice for the vast majority of needs. Direct access to the underlying DOM is also possible, as each D3 selection is simply an array of nodes.

DYNAMIC Properties:

Styles, attributes, and other properties can be specified as functions of data in D3, not just simple constants.

D3 provides many built-in reusable functions and function factories, such as graphical primitives for area, line and pie charts.

Computed properties often refer to bound data. Data is specified as an array of values, and each value is passed as the first argument (d) to selection functions. With the default join-by-index, the first element in the data array is passed to the first node in the selection, the second element to the second node, and so on. For example, if you bind an array of numbers to paragraph elements, you can use these numbers to compute dynamic font sizes:

d3.selectAll("p")
  .data([4, 8, 15, 16, 23, 42])
    .style("font-size", function(d) { return d + "px"; });

Once the data has been bound to the document, you can omit the data operator; D3 will retrieve the previously-bound data. This allows you to recompute properties without rebinding.

Example

the example is heavily based on this YouTube video

Before we start, look at the circle element in svg

<svg>
  <circle cx="50" cy="50" r="40">
</svg>

There are other attributes

<html>
<head>
  <script src="https://d3js.org/d3.v4.js"></script>
</head>
<body>
  <svg width="100%" height="100%">
  </svg> 

  <script>
    var data = [32, 13, 25, 29, 36];
    var svg = d3.select('svg');

    //this is where the data binding happens
    svg.selectAll("circle")
       .data(data)
       .enter().append('circle')  // this where circle is created and appended to svg
         .attr('cx', function(d) {return 0;}) // x - coordinate
         .attr('cy', function(d) {return 0;}) // y - coordinate 
         .attr('r', function(d){return 10;}) // radius
         .attr('fill', function(d){return red;}) // color
  </script>
</body>
</html>

At this stage D3 selects the SVG element and appends circles. However, they are all stuck at the same coordinates (0, 0).

Lets make some changes to our code. D3 allows you to apply forces to svg elements. To do so we will use d3.forceSimulation() function. Two forces with would like to look at. gravitation for to move our bubbles to the centre of the screen and collide force to prevent them from overlapping with each other.

forceSimulation([nodes]) <>

Creates a new simulation with the specified array of nodes and no forces. If nodes is not specified, it defaults to the empty array. The simulator starts automatically; use simulation.on to listen for tick events as the simulation runs. If you wish to run the simulation manually instead, call simulation.stop, and then call simulation.tick as desired.

Lets see how it works. First, lets change our dataset slightly and move all our circles to the left up corner:

data = [
  {'fruit':'apple', 'freq':'32', 'color' : 'green'},
  {'fruit':'pear', 'freq':'15', 'color' : 'blue'},
  {'fruit':'banan', 'freq':'67', 'color' : 'red'},
  {'fruit':'kiwi', 'freq':'34', 'color' : 'grey'},
  {'fruit':'peach', 'freq':'22', 'color' : 'yellow'}
]

//and move all our to the top left corner
svg.selectAll("circle")
  .data(data)
  .enter().append('circle')
    .attr('cx', function(d) {return 0;})
    .attr('cy', function(d) {return 0;})
    .attr('r', function(d){return radiusScale(d.freq);})
    .attr('fill', function(d){return d.color;})

simulation force will work on nodes, lets assign all our circles to one variable

var nodes_circles = d3.selectAll("circle");

simulation is like a clock. it ticks and ticks and every time a second or(millisecond) goes by, it updates the position of all of our circles. But we need to write the code that fires on every tick of the simulation.

then we need to add the tick function (more on why we need to add tick function)

simulation.nodes(data)   //why do we have to pass data here?????
  .on("tick", ticked)

function ticked() {
  nodes_circles
    .attr("cx", function(d){return d.x;})
    .attr("cy", function(d){return d.y;})
}

We are almost ready to start moving our bubbles to the centre of our screen

var width = 640,
    height = 480;

var simulation = d3.forceSimulation()
  .force("x", d3.forceX(width/2).strength(0.05)) 
  .force("y", d3.forceY(height/2).strength(0.05))

What we do here is called positioning: Positioning The x- and y-positioning forces push nodes towards a desired position along the given dimension with a configurable strength. The strength of the force is proportional to the one-dimensional distance between the node’s position and the target position. While these forces can be used to position individual nodes, they are intended primarily for global forces that apply to all (or most) nodes.

d3.forceX([ ]) //Creates a new positioning force along the x-axis towards the given position x. If x is not 
               // specified, it defaults to 0.
d3.forceY([ ])
x.strength([strength]) // in plain English is force we apply to our element. keep it between [0,1]
y.strength([strength])

At this stage the bubbles are still in the middle. We need to apply collide force, which supposed to prevent bubbles from overlapping

//d3.forceCollide([radius]) Creates a new circle collision force with the specified radius. If radius is not  //specified, it defaults to the constant one for all nodes.

//update the simulation
var simulation = d3.forceSimulation()
  .force("x", d3.forceX(width/2).strength(0.05)) 
  .force("y", d3.forceY(height/2).strength(0.05))
  .force("collide", d3.forceCollide(20))

The problem is how to select the right radius for collision force based on our bubbles. D3 has a Scaling functionality for that purpose. There are various types of scaling available. Because we work with radius, we will use scaleSqrt

var radiusScale = d3.scaleSqrt().domain([15, 67]).range([10, 80]);

in our example the min freq is 15 and max is 67(called domain). The function above will scale domain between 15 ad 67 into range between 10 and 80. ( (more about why scaleSqrt)[https://stackoverflow.com/questions/20091434/d3-js-using-d3-scale-sqrt] ).

finally, the simulation will look like this

var radiusScale = d3.scaleSqrt().domain([15, 67]).range([10, 80]);

var simulation = d3.forceSimulation()
  .force("x", d3.forceX(width/2).strength(0.05))
  .force("y", d3.forceY(height/2).strength(0.05))
  .force("collide", d3.forceCollide(function(d){
    return radiusScale(d.freq);
  }))

Final version:

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <script src="https://d3js.org/d3.v4.js"></script>
  <style type="text/css" media="screen">
    svg{
      display: block;
      position: absolute;
      top: 5%; left:5%; width:90%; height:90%;
    }
  </style>
    <title></title>
</head>
<body>
  <h1>Hello World</h1>
  <svg width="100%" height="100%">
  </svg>
</body>
  <script>
    data = [
      {'fruit':'apple', 'freq':'32', 'color' : 'green'},
      {'fruit':'pear', 'freq':'15', 'color' : 'blue'},
      {'fruit':'banan', 'freq':'67', 'color' : 'red'},
      {'fruit':'kiwi', 'freq':'34', 'color' : 'grey'},
      {'fruit':'peach', 'freq':'22', 'color' : 'yellow'}
    ]

    var svg = d3.select('svg');

    var width = 640,
        height = 480;

    var radiusScale = d3.scaleSqrt().domain([15, 67]).range([10, 80]);

    svg.selectAll("circle")
      .data(data)
      .enter().append('circle')
        .attr('cx', function(d) {return 0;})
        .attr('cy', function(d) {return 0;})
        .attr('r', function(d){return radiusScale(d.freq);})
        .attr('fill', function(d){return d.color;})

    nodes_circles = d3.selectAll("circle");

    //simulation is a collection of forces
    //that going to affect our circles
    var simulation = d3.forceSimulation()
      .force("x", d3.forceX(width/2).strength(0.05))
      .force("y", d3.forceY(height/2).strength(0.05))
      .force("collide", d3.forceCollide(function(d){
        return radiusScale(d.freq);
      }))

    simulation.nodes(data)
      .on("tick", ticked)

    function ticked() {
      nodes_circles
        .attr("cx", function(d){return d.x;})
        .attr("cy", function(d){return d.y;})
    }

</script>
</html>
Clone this wiki locally