Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

d3.js Checking/Count series chart

I am working on an application that uses foursquare data. enter image description here

//this is the series chart that has had some delving into - but there are some bugs still running around here.

So we have a batch of data - Health & Beauty, Restaurants, Cafe, Public Houses. -- there would be a COUNT of them -- and a SUMMATION of checkout information. So I want this chart to be able to show the NUMBER of venues, but also indicate how POPULAR they are.. so for example the number of pubs may be smaller, but the number of checkins higher as they are more popular. So in that instance want to reverse the colors of the circles.

There are some bugs with the current code attempts.

  • the swapping of the circles/circle spacing causes tears in black paths and odd behaviors
  • with the lines I would like to have a black line under the blue circle, but inside the blue circle show a cropped circle path orange line -- so a kind of masking ability.

_latest jsfiddle

phase1

using "V" instead of "L" but couldn't make it work properly for the time being.

phase 2 I think it works more consistently but there are some issues. Also, I am not sure about the data and the scaling of the circles. (I've added extra labels so that it is visible what the value of the circles are)

phase 3

changed the getCircleSize a bit even though I believe a more consistent thing to do would be something like this layerSet.push(parseInt(getPercentage(layerArray[i], meansPerGroup[0])*60, 10));

so here the first step draws the circles by size order first... so in this case by count.. but maybe there is a bug here reversing the color to indicate the checkin count instead - so maybe we need to sort by count,checkin order - that way the first circle to get painted follows correctly.

  // Create Circles
  function setCircles(items) {
    // sort elements in order to draw them by size
    items.sort(function(a, b) {
      return parseFloat(b.value) - parseFloat(a.value);
    });

    var circlelayer = svg.append("g")
      .attr("class", "circlelayer");

    var circle = circlelayer.selectAll("circle")
      .data(items);

    circle.enter().append("circle")
      .attr("class", function(d, i) {
        if (d.l == 0) {
          return "blue";
        }
        return "gold";
      })
      .attr("cy", 60)
      .attr("cx", function(d, i) {
        var distance = calculateDistance(d, items);
        if (d.l == 1) {
          distancesL1.push(distance);
        } else {
          distancesL0.push(distance);
        }
        return distance;
      })
      .attr("r", function(d, i) {
        return Math.sqrt(d.value);
      })
      .attr("filter", function(d) {
        return "url(#drop-shadow)";
      });

    circle.exit().remove();
  }

json structure to look something like this

[{
    "label": "Health and Beauty",
    "count": 30,
    "checkin": 100
}, {
    "label": "Restaurants",
    "count": 23,
    "checkin": 200
}, {
    "label": "Cafes",
    "count": 11,
    "checkin": 900
}, {
    "label": "Public Houses",
    "count": 5,
    "checkin": 1000
}]
like image 857
The Old County Avatar asked Sep 03 '16 11:09

The Old County


1 Answers

I'm not sure I understand what is your problem but I've decided to try to create that chart from you screenshot with sample data from your plunker. Here is my result: enter image description here

My script is making sure that smaller circle is always on top of the rage one so both circles are always visible. Here you can find my code:

<!DOCTYPE html>

<head>
  <meta charset="utf-8">
  <script src="https://d3js.org/d3.v5.min.js"></script>
  <style>
    html {
      font-family: sans-serif;
    }
  </style>
</head>

<body>
  <div id="container">

  </div>

  <script>
    const data = [{
      "name": "Twitter",
      "vists": "15 billion",
      "unit": "per day",
      "layer1value": 15000000,
      "layer2value": 450
    }, {
      "name": "Facebook",
      "vists": "5 billion",
      "unit": "per day",
      "layer1value": 4000000,
      "layer2value": 5000000
    }, {
      "name": "Google",
      "vists": "5 billion",
      "unit": "per day",
      "layer1value": 5000000,
      "layer2value": 25000
    }, {
      "name": "Netflix",
      "vists": "10 billion",
      "unit": "per day",
      "layer1value": 3000000,
      "layer2value": 2200
    }, {
      "name": "Ebay",
      "vists": "8 billion",
      "unit": "per day",
      "layer1value": 2500000,
      "layer2value": 4900000
    }, {
      "name": "Klout",
      "vists": "2 billion",
      "unit": "per day",
      "layer1value": 1000000,
      "layer2value": 45
    }];

    /*
     * Finding max and min layer size
     */
    const values = data.reduce((acumulator, datum) => [...acumulator, datum.layer1value, datum.layer2value], []);
    const maxValue = Math.max(...values);
    const minValue = Math.min(...values);

    /*
     * Creating scale based on the smallest and largest layer1value or layer2value
     */
    const radiusScale = d3.scaleLinear()
      .domain([minValue, maxValue])
      .range([10, 150]); // min and max value of the circle

    const width = 900;
    const height = 500;
    const orangeColour = '#ffb000';
    const blueColour = '#00a1ff';

    // Creating svg element
    const svg = d3.select('#container').append('svg').attr('width', width).attr('height', height);

    let xPos = 0; // position of circle
    /*
     * iterate over each datum and render all associated elements: two circles, and two labels with pointer lines
     */
    for (let i = 0; i < data.length; i++) {
      const d = data[i]; // current data point
      const currMaxRadius = radiusScale(Math.max(d.layer1value, d.layer2value)); // get largest radius within current group of two layers
      xPos += currMaxRadius; // add that radius to xPos

      // create group element containing all view elements for current datum
      const group = svg.append('g')
        .attr('transform', `translate(${xPos}, ${height / 2})`);

      group.append('circle')
        .attr('r', radiusScale(d.layer1value))
        .style('fill', blueColour);
      group.insert('circle', d.layer2value > d.layer1value ? ':first-child' : null) // if layer2value is larger than layer1value then insert this circle before the previous one
        .attr('r', radiusScale(d.layer2value))
        .style('fill', orangeColour);
      xPos += currMaxRadius * 0.9;

      /*
       * ADDING LABEL UNDERNEATH THE CIRCLES
       */
      group.append('text')
        .text(d.name)
        .attr('dy', radiusScale(maxValue) + 40) // add 40px of padding so label is not just bellow the circle
        .style('text-anchor', 'middle');

      group.append('line')
        .attr('y1', radiusScale(d.layer2value))
        .attr('y2', radiusScale(maxValue) + 20) // add 20px of padding so the pointer line is not overlapping with label
        .style('stroke', orangeColour);


      /*
       * ADDING LABEL AT THE ANGLE OF 45deg RELATIVE TO THE CIRCLES
       */
      // we are creating new group element so we can easily rotate both line and label by -45deg
      const rotatedLabelGroup = group.append('g').style('transform', 'rotate(-45deg)');
      rotatedLabelGroup.append('line')
        .attr('x1', radiusScale(d.layer2value))
        .attr('x2', radiusScale(maxValue) + 20)
        .style('stroke', orangeColour);
      rotatedLabelGroup.append('text')
        .text(d.vists)
        .attr('dx', radiusScale(maxValue))
        .attr('dy', -5); // this way label is slightly above the line
    }
  </script>
</body>
like image 155
Mateusz Avatar answered Nov 09 '22 14:11

Mateusz