Skip to content

Instantly share code, notes, and snippets.

@tlfrd
Last active September 13, 2019 10:45
Show Gist options
  • Save tlfrd/9ee6ea68c7c46b9d3d5eb1e7f4858fc7 to your computer and use it in GitHub Desktop.
Save tlfrd/9ee6ea68c7c46b9d3d5eb1e7f4858fc7 to your computer and use it in GitHub Desktop.

Revisions

  1. Cale Tilford revised this gist Dec 14, 2017. No changes.
  2. Cale Tilford revised this gist Dec 14, 2017. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,5 @@
    An example of a [Slopegraph](http://charliepark.org/slopegraphs/). Uses [constraint relaxing](https://www.safaribooksonline.com/blog/2014/03/11/solving-d3-label-placement-constraint-relaxing/) to programmatically reposition labels to stop them from overlapping.
    Uses a voronoi to make line selection easier (this still isn't ideal and some tweaking may be nessecary).
    Uses a voronoi to make line selection easier (this still isn't ideal and some tweaking may be necessary).

    This is part of a series of visualisations called [My Visual Vocabulary](https://github.com/tlfrd/my-visual-vocabulary) which aims to recreate every visualisation in the FT's [Visual Vocabulary](https://github.com/ft-interactive/visual-vocabulary) from scratch using D3.

  3. Building blocks revised this gist Dec 13, 2017. 1 changed file with 0 additions and 0 deletions.
    Binary file added thumbnail.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
  4. Cale Tilford revised this gist Sep 25, 2017. No changes.
  5. Cale Tilford created this gist Sep 25, 2017.
    2 changes: 2 additions & 0 deletions .block
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,2 @@
    license: mit
    height: 760
    12 changes: 12 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,12 @@
    An example of a [Slopegraph](http://charliepark.org/slopegraphs/). Uses [constraint relaxing](https://www.safaribooksonline.com/blog/2014/03/11/solving-d3-label-placement-constraint-relaxing/) to programmatically reposition labels to stop them from overlapping.
    Uses a voronoi to make line selection easier (this still isn't ideal and some tweaking may be nessecary).

    This is part of a series of visualisations called [My Visual Vocabulary](https://github.com/tlfrd/my-visual-vocabulary) which aims to recreate every visualisation in the FT's [Visual Vocabulary](https://github.com/ft-interactive/visual-vocabulary) from scratch using D3.

    TODO:

    - Refactor into a reusable function
    - Add label positions to voronoi


    forked from <a href='http://bl.ocks.org/tlfrd/'>tlfrd</a>'s block: <a href='http://bl.ocks.org/tlfrd/042b2318c8767bad7a485098fbf760fc'>Slopegraph</a>
    307 changes: 307 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,307 @@
    <!DOCTYPE html>
    <head>
    <meta charset="utf-8">
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <link href="https://fonts.googleapis.com/css?family=Open+Sans:400, 700" rel="stylesheet">
    <style>
    body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }

    body {
    font-family: 'Open Sans', sans-serif;
    }

    .title {
    font-size: 18px;
    font-weight: 700;
    }

    .slope-line {
    stroke: #333;
    stroke-width: 2px;
    stroke-linecap: round;
    }

    .slope-label-left, .slope-label-right {
    font-size: 16px;
    cursor: default;
    font-weight: 400;
    }

    .label-figure {
    font-weight: 700;
    }

    .border-lines {
    stroke: #999;
    stroke-width: 1px;
    }

    .voronoi path {
    fill: none;
    pointer-events: all;
    }

    circle {
    fill: white;
    stroke: black;
    stroke-width: 2px;
    }
    </style>
    </head>

    <body>
    <script>
    var margin = {top: 100, right: 275, bottom: 40, left: 275};

    var width = 960 - margin.left - margin.right,
    height = 760 - margin.top - margin.bottom;

    var svg = d3.select("body").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 url = "https://raw.githubusercontent.com/tlfrd/pay-ratios/master/data/payratio.json";

    var y1 = d3.scaleLinear()
    .range([height, 0]);

    var config = {
    xOffset: 0,
    yOffset: 0,
    width: width,
    height: height,
    labelPositioning: {
    alpha: 0.5,
    spacing: 18
    },
    leftTitle: "2013",
    rightTitle: "2016",
    labelGroupOffset: 5,
    labelKeyOffset: 50,
    radius: 6,
    // Reduce this to turn on detail-on-hover version
    unfocusOpacity: 0.3
    }

    function drawSlopeGraph(cfg, data, yScale, leftYAccessor, rightYAccessor) {
    var slopeGraph = svg.append("g")
    .attr("class", "slope-graph")
    .attr("transform", "translate(" + [cfg.xOffset, cfg.yOffset] + ")");
    }

    d3.json(url, function(error, data) {
    if (error) return error;

    // Combine ratios into a single array
    var ratios = [];
    data.pay_ratios_2012_13.forEach(function(d) {
    d.year = "2012-2013";
    ratios.push(d);
    });
    data.pay_ratios_2015_16.forEach(function(d) {
    d.year = "2015-2016";
    ratios.push(d);
    });

    // Nest by university
    var nestedByName = d3.nest()
    .key(function(d) { return d.name })
    .entries(ratios);

    // Filter out those that only have data for a single year
    nestedByName = nestedByName.filter(function(d) {
    return d.values.length > 1;
    });

    var y1Min = d3.min(nestedByName, function(d) {
    var ratio1 = d.values[0].max / d.values[0].min;
    var ratio2 = d.values[1].max / d.values[1].min;

    return Math.min(ratio1, ratio2);
    });

    var y1Max = d3.max(nestedByName, function(d) {
    var ratio1 = d.values[0].max / d.values[0].min;
    var ratio2 = d.values[1].max / d.values[1].min;

    return Math.max(ratio1, ratio2);
    });

    // Calculate y domain for ratios
    y1.domain([y1Min, y1Max]);

    var yScale = y1;

    var voronoi = d3.voronoi()
    .x(d => d.year == "2012-2013" ? 0 : width)
    .y(d => yScale(d.max / d.min))
    .extent([[-margin.left, -margin.top], [width + margin.right, height + margin.bottom]]);

    var borderLines = svg.append("g")
    .attr("class", "border-lines")
    borderLines.append("line")
    .attr("x1", 0).attr("y1", 0)
    .attr("x2", 0).attr("y2", config.height);
    borderLines.append("line")
    .attr("x1", width).attr("y1", 0)
    .attr("x2", width).attr("y2", config.height);

    var slopeGroups = svg.append("g")
    .selectAll("g")
    .data(nestedByName)
    .enter().append("g")
    .attr("class", "slope-group")
    .attr("id", function(d, i) {
    d.id = "group" + i;
    d.values[0].group = this;
    d.values[1].group = this;
    });

    var slopeLines = slopeGroups.append("line")
    .attr("class", "slope-line")
    .attr("x1", 0)
    .attr("y1", function(d) {
    return y1(d.values[0].max / d.values[0].min);
    })
    .attr("x2", config.width)
    .attr("y2", function(d) {
    return y1(d.values[1].max / d.values[1].min);
    });

    var leftSlopeCircle = slopeGroups.append("circle")
    .attr("r", config.radius)
    .attr("cy", d => y1(d.values[0].max / d.values[0].min));

    var leftSlopeLabels = slopeGroups.append("g")
    .attr("class", "slope-label-left")
    .each(function(d) {
    d.xLeftPosition = -config.labelGroupOffset;
    d.yLeftPosition = y1(d.values[0].max / d.values[0].min);
    });

    leftSlopeLabels.append("text")
    .attr("class", "label-figure")
    .attr("x", d => d.xLeftPosition)
    .attr("y", d => d.yLeftPosition)
    .attr("dx", -10)
    .attr("dy", 3)
    .attr("text-anchor", "end")
    .text(d => (d.values[0].max / d.values[0].min).toPrecision(3));

    leftSlopeLabels.append("text")
    .attr("x", d => d.xLeftPosition)
    .attr("y", d => d.yLeftPosition)
    .attr("dx", -config.labelKeyOffset)
    .attr("dy", 3)
    .attr("text-anchor", "end")
    .text(d => d.key);

    var rightSlopeCircle = slopeGroups.append("circle")
    .attr("r", config.radius)
    .attr("cx", config.width)
    .attr("cy", d => y1(d.values[1].max / d.values[1].min));

    var rightSlopeLabels = slopeGroups.append("g")
    .attr("class", "slope-label-right")
    .each(function(d) {
    d.xRightPosition = width + config.labelGroupOffset;
    d.yRightPosition = y1(d.values[1].max / d.values[1].min);
    });

    rightSlopeLabels.append("text")
    .attr("class", "label-figure")
    .attr("x", d => d.xRightPosition)
    .attr("y", d => d.yRightPosition)
    .attr("dx", 10)
    .attr("dy", 3)
    .attr("text-anchor", "start")
    .text(d => (d.values[1].max / d.values[1].min).toPrecision(3));

    rightSlopeLabels.append("text")
    .attr("x", d => d.xRightPosition)
    .attr("y", d => d.yRightPosition)
    .attr("dx", config.labelKeyOffset)
    .attr("dy", 3)
    .attr("text-anchor", "start")
    .text(d => d.key);

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

    titles.append("text")
    .attr("text-anchor", "end")
    .attr("dx", -10)
    .attr("dy", -margin.top / 2)
    .text(config.leftTitle);

    titles.append("text")
    .attr("x", config.width)
    .attr("dx", 10)
    .attr("dy", -margin.top / 2)
    .text(config.rightTitle);

    relax(leftSlopeLabels, "yLeftPosition");
    leftSlopeLabels.selectAll("text")
    .attr("y", d => d.yLeftPosition);

    relax(rightSlopeLabels, "yRightPosition");
    rightSlopeLabels.selectAll("text")
    .attr("y", d => d.yRightPosition);

    d3.selectAll(".slope-group")
    .attr("opacity", config.unfocusOpacity);

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

    voronoiGroup.selectAll("path")
    .data(voronoi.polygons(d3.merge(nestedByName.map(d => d.values))))
    .enter().append("path")
    .attr("d", function(d) { return d ? "M" + d.join("L") + "Z" : null; })
    .on("mouseover", mouseover)
    .on("mouseout", mouseout);
    });

    function mouseover(d) {
    d3.select(d.data.group).attr("opacity", 1);
    }

    function mouseout(d) {
    d3.selectAll(".slope-group")
    .attr("opacity", config.unfocusOpacity);
    }

    // Function to reposition an array selection of labels (in the y-axis)
    function relax(labels, position) {
    again = false;
    labels.each(function (d, i) {
    a = this;
    da = d3.select(a).datum();
    y1 = da[position];
    labels.each(function (d, j) {
    b = this;
    if (a == b) return;
    db = d3.select(b).datum();
    y2 = db[position];
    deltaY = y1 - y2;

    if (Math.abs(deltaY) > config.labelPositioning.spacing) return;

    again = true;
    sign = deltaY > 0 ? 1 : -1;
    adjust = sign * config.labelPositioning.alpha;
    da[position] = +y1 + adjust;
    db[position] = +y2 - adjust;

    if (again) {
    relax(labels, position);
    }
    })
    })
    }


    </script>
    </body>