Last active
September 13, 2019 10:45
-
-
Save tlfrd/9ee6ea68c7c46b9d3d5eb1e7f4858fc7 to your computer and use it in GitHub Desktop.
Revisions
-
Cale Tilford revised this gist
Dec 14, 2017 . No changes.There are no files selected for viewing
-
Cale Tilford revised this gist
Dec 14, 2017 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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 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. -
Building blocks revised this gist
Dec 13, 2017 . 1 changed file with 0 additions and 0 deletions.There are no files selected for viewing
LoadingSorry, something went wrong. Reload?Sorry, we cannot display this file.Sorry, this file is invalid so it cannot be displayed. -
Cale Tilford revised this gist
Sep 25, 2017 . No changes.There are no files selected for viewing
-
Cale Tilford created this gist
Sep 25, 2017 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,2 @@ license: mit height: 760 This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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> This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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>