block by syntagmatic 1ced118a51b49dadd4ea

IPv4 Axis

Full Screen

Uses the Integer IPv4 Representation from MaxMind to scale IP address values and label the axis.

This product includes GeoLite data created by MaxMind, available from http://www.maxmind.com. A random selection of 10,000 entries are shown.

index.html

<!DOCTYPE html>
<meta charset="utf-8">
<title>IP Address Axis</title>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="//bl.ocks.org/syntagmatic/raw/3341641/render-queue.js"></script>
<link rel="stylesheet" href="style.css"></link>
<script>
var margin = {top: 50, right: 60, bottom: 20, left: 220},
    width = 720 - margin.left - margin.right,
    height = 340 - margin.top - margin.bottom;

var color = d3.scale.ordinal()
  .range(["#5DA5B3","#D58323","#DD6CA7","#54AF52","#8C92E8","#E15E5A","#725D82","#776327","#50AB84","#954D56","#AB9C27","#517C3F","#9D5130","#357468","#5E9ACF","#C47DCB","#7D9E33","#DB7F85","#BA89AD","#4C6C86","#B59248","#D8597D","#944F7E","#D67D4B","#8F86C2"]);

var types = {
  "Number": {
    key: "Number",
    coerce: function(d) { return +d; },
    extent: d3.extent,
    within: function(d, extent) { return extent[0] <= d && d <= extent[1]; },
    defaultScale: d3.scale.linear().range([height, 0])
  },
  "String": {
    key: "String",
    coerce: String,
    extent: function (data) { return data.sort(); },
    within: function(d, extent, dim) { return extent[0] <= dim.scale(d) && dim.scale(d) <= extent[1]; },
    defaultScale: d3.scale.ordinal().rangePoints([0, height])
  },
  "Date": {
    key: "Date",
    coerce: function(d) { return new Date(d); },
    extent: d3.extent,
    within: function(d, extent) { return extent[0] <= d && d <= extent[1]; },
    defaultScale: d3.time.scale().range([0, height])
  },
  "IPv4": {
    key: "IPv4",
    coerce: function(d) { return +d; },
    extent: d3.extent,
    within: function(d, extent) { return extent[0] <= d && d <= extent[1]; },
    defaultScale: d3.scale.linear().range([height, 0])
  }
};

var dimensions = [
  {
    key: "start_num",
    description: "IPv4 Address",
    type: types["IPv4"],
    domain: ["0.0.0.0", "255.255.255.255"].map(ipv4ToNum),
    axis: d3.svg.axis().orient("left")
      .tickFormat(numToIpv4)
      .tickValues([
      "0.0.0.0",
      "20.0.0.0",
      "40.0.0.0",
      "60.0.0.0",
      "80.0.0.0",
      "100.0.0.0",
      "120.0.0.0",
      "140.0.0.0",
      "160.0.0.0",
      "180.0.0.0",
      "200.0.0.0",
      "220.0.0.0",
      "240.0.0.0",
      "255.255.255.255",
      ].map(ipv4ToNum))
  },
  {
    key: "country",
    type: types["String"],
    axis: d3.svg.axis().orient("left")
      .tickFormat(function(d,i) {
        if (d == null) return "(null)";
        return i % 7 == 0 ? d : "";
      })
  }
];

var xscale = d3.scale.ordinal()
    .domain(d3.range(dimensions.length))
    .rangePoints([0, width]);

var yAxis = d3.svg.axis()
    .orient("left");

var container = d3.select("body").append("div")
    .attr("class", "parcoords")
    .style("width", width + margin.left + margin.right + "px")
    .style("height", height + margin.top + margin.bottom + "px");

var svg = container.append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

var canvas = container.append("canvas")
    .attr("width", width + 1)
    .attr("height", height + 1)
    .style("margin-top", margin.top + "px")
    .style("margin-left", margin.left + "px");

var ctx = canvas.node().getContext("2d");
ctx.globalCompositeOperation = 'darken';
ctx.globalAlpha = 0.15;
ctx.lineWidth = 1.5;

var output = d3.select("body").append("pre");

var axes = svg.selectAll(".axis")
    .data(dimensions)
  .enter().append("g")
    .attr("class", function(d) { return "axis " + d.key; })
    .attr("transform", function(d,i) { return "translate(" + xscale(i) + ")"; });

d3.csv("subset.csv", function(error, data) {
  if (error) throw error;

  data.forEach(function(d) {
    dimensions.forEach(function(p) {
      d[p.key] = !d[p.key] ? null : p.type.coerce(d[p.key]);
    });
  });

  // type/dimension default setting happens here
  dimensions.forEach(function(dim) {
    if (!("domain" in dim)) {
      // detect domain using dimension type's extent function
      dim.domain = d3.functor(dim.type.extent)(data.map(function(d) { return d[dim.key]; }));

      // TODO - this line only works because the data encodes data with integers
      // Sorting/comparing should be defined at the type/dimension level
      dim.domain.sort(function(a,b) {
        return a - b;
      });
    }
    if (!("scale" in dim)) {
      // use type's default scale for dimension
      dim.scale = dim.type.defaultScale.copy();
    }
    dim.scale.domain(dim.domain);
  });

  var render = renderQueue(draw).rate(30);

  ctx.clearRect(0,0,width+1,height+1);
  ctx.globalAlpha = d3.min([0.75/Math.pow(data.length,0.3),1]);
  render(data);

  axes.append("g")
      .each(function(d) {
        var renderAxis = "axis" in d
          ? d.axis.scale(d.scale)  // custom axis
          : yAxis.scale(d.scale);  // default axis
        d3.select(this).call(renderAxis);
      })
    .append("text")
      .attr("class", "title")
      .attr("text-anchor", "start")
      .text(function(d) { return "description" in d ? d.description : d.key; });

  // Add and store a brush for each axis.
  axes.append("g")
      .attr("class", "brush")
      .each(function(d) {
        d3.select(this).call(d.brush = d3.svg.brush()
          .y(d.scale)
          .on("brushstart", brushstart)
          .on("brush", brush));
      })
    .selectAll("rect")
      .attr("x", -8)
      .attr("width", 16);

  output.text(d3.tsv.format(data.slice(0,20)));

  function project(d) {
    return dimensions.map(function(p,i) {
      if (d[p.key] === null) return null;
      return [xscale(i),p.scale(d[p.key])];
    });
  };

  function draw(d) {
    ctx.strokeStyle = color(d.country);
    ctx.beginPath();
    var coords = project(d);
    coords.forEach(function(p,i) {
      // this tricky bit avoids rendering null values as 0
      if (p === null) {
        // this bit renders horizontal lines on the previous/next
        // dimensions, so that sandwiched null values are visible
        if (i > 0) {
          var prev = coords[i-1];
          if (prev !== null) {
            ctx.moveTo(prev[0],prev[1]);
            ctx.lineTo(prev[0]+6,prev[1]);
          }
        }
        if (i < coords.length-1) {
          var next = coords[i+1];
          if (next !== null) {
            ctx.moveTo(next[0]-6,next[1]);
          }
        }
        return;
      }
      
      if (i == 0) {
        ctx.moveTo(p[0],p[1]);
        return;
      }

      ctx.lineTo(p[0],p[1]);
    });
    ctx.stroke();
  }

  function brushstart() {
    d3.event.sourceEvent.stopPropagation();
  }

  // Handles a brush event, toggling the display of foreground lines.
  function brush() {
    var actives = dimensions.filter(function(p) { return !p.brush.empty(); }),
        extents = actives.map(function(p) { return p.brush.extent(); });

    var selected = data.filter(function(d) {
      if (actives.every(function(dim, i) {
          // test if point is within extents for each active brush
          return dim.type.within(d[dim.key], extents[i], dim);
        })) {
        return true;
      }
    });

    ctx.clearRect(0,0,width+1,height+1);
    ctx.globalAlpha = d3.min([0.75/Math.pow(selected.length,0.3),1]);
    render(selected);

    output.text(d3.tsv.format(selected.slice(0,20)));
  }
});

/* Utility functions */
function ipv4ToNum(d) {
  // based on //dev.maxmind.com/geoip/legacy/csv/
  var o = d.split(".").map(Number);
  return (16777216 * o[0])
       + (   65536 * o[1])
       + (     256 * o[2])
       +             o[3];
};

function numToIpv4(d) {
  // based on //dev.maxmind.com/geoip/legacy/csv/
  o1 = Math.floor(d / 16777216) % 256;
  o2 = Math.floor(d / 65536   ) % 256;
  o3 = Math.floor(d / 256     ) % 256;
  o4 = Math.floor(d           ) % 256;
  return [o1, o2, o3, o4].join('.')
};
</script>

style.css

body {
  min-width: 760px;
}

.parcoords {
  display: block;
}

.parcoords svg,
.parcoords canvas {
  font: 9px sans-serif;
  position: absolute;
}

.parcoords canvas {
  opacity: 0.9;
  pointer-events: none;
}

.axis .title {
  font-size: 11px;
  transform: rotate(-20deg) translate(-5px,-6px);
}

.axis line,
.axis path {
  fill: none;
  stroke: #000;
  stroke-width: 1px;
}

.axis.manufac_name {
  font-size: 7px;
}

.brush .extent {
  fill-opacity: .3;
  stroke: #fff;
  stroke-width: 1px;
}

pre {
  width: 100%;
  height: 160px;
  margin: 6px 12px;
  tab-size: 25;
  font-size: 9px;
  overflow: auto;
}