Skip to content

Instantly share code, notes, and snippets.

@timshadel
Forked from mbostock/.block
Last active December 22, 2015 09:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save timshadel/6454617 to your computer and use it in GitHub Desktop.
Save timshadel/6454617 to your computer and use it in GitHub Desktop.
node_modules

Say your dataset is an array of numbers, and includes both positive and negative values. Use two scales to construct the bar chart: a quantitative scale (such as a linear scale) to compute the bar positions along the x-axis, and an ordinal scale with rangeBands to compute the bar positions along the y-axis.

For the quantitative scale, compute the data domain (the minimum and maximum value) using d3.extent:

var x = d3.scale.linear()
    .domain(d3.extent(data, function(d) { return d.value; }))
    .range([0, width]);

Nicing the scale will extend the extent slightly to the nearest round numbers. If you want the zero-value to be centered in the middle of the canvas, take the greater of the minimum and maximum value by magnitude, or simply hard-code the desired domain.

For the y-axis, use rangeRoundBands to divide the vertical space into bands for each bar and specify the amount of padding between bars. The input (domain) to the ordinal scale is some identifying data—such as a name or a unique id. A simple such identifier is the data’s index:

var y = d3.scale.ordinal()
    .domain(data.map(function(d) { return d.name; }))
    .rangeRoundBands([0, height], .2);

Use both scales to position the bars. This is made slightly tricky in that SVG rects are positioned (the x and y attributes) by their top-left corner and cannot have a negative width or height. So, we must use the x- and y-scales to compute the position of the top-left corner, depending on whether the associated value is positive or negative: if the value is positive, then the data value determines the right edge of the bar, while if it’s negative, it determines the left edge of the bar. Hence the conditionals:

svg.selectAll(".bar")
    .data(data)
  .enter().append("rect")
    .attr("class", "bar")
    .attr("x", function(d) { return x(Math.min(0, d.value)); })
    .attr("y", function(d) { return y(d.name); })
    .attr("width", function(d) { return Math.abs(x(d.value) - x(0)); })
    .attr("height", y.rangeBand());

Lastly, you can add an axis to display tick marks on top. You might also compute a fill style (or even a gradient) to alter the differentiate the appearance of positive and negative values.

var express = require('express');
var app = express();
app.use('/', express.static(__dirname));
app.listen(3000);
name value
SIGNED UP 15
Created Canvas 20
C 22
D 18
E 2
F 6
G 26
H 18
[
{
"name": "SIGNED UP",
"type": "begin",
"value": 100
},
{
"name": "Created Canvas",
"time": "after less than a minute",
"value": 98.2
},
{
"name": "Opted In",
"time": "after 3 minutes",
"value": 5.4
},
{
"name": "Added 1 Item",
"time": "after 9 minutes",
"value": 98.2
},
{
"name": "Populated Problem",
"time": "after 12 minutes",
"value": 92.9
},
{
"name": "Populated Customer Segments",
"time": "after 14 minutes",
"value": 85.7
},
{
"name": "Populated Existing Alternatives",
"time": "after 17 minutes",
"value": 82.7
},
{
"name": "Updated Profile",
"time": "after 23 minutes",
"value": 1.8
},
{
"name": "Populated Solution",
"time": "after 23 minutes",
"value": 87.5
},
{
"name": "Populated Early Adopters",
"time": "after 35 minutes",
"value": 57.1
},
{
"name": "Populated Value Proposition",
"time": "after about 1 hour",
"value": 81.9
},
{
"name": "Populated Cost Structure",
"time": "after about 1 hour",
"value": 21.8
},
{
"name": "Populated Channels",
"time": "after about 1 hour",
"value": 46.4
},
{
"name": "Populated Key Activity",
"time": "after about 1 hour",
"value": 26.7
},
{
"name": "ACTIVATED",
"time": "after about 1 hour",
"type": "end",
"value": 100
},
{
"name": "Referred",
"time": "after abount 1 hour",
"value": 37.9
},
{
"name": "Populated Revenue Streams",
"time": "after about 1 hour",
"value": 30.4
},
{
"name": "Populated High Level",
"time": "after about 1 hour",
"value": 53.0
},
{
"name": "Populated Unfair Advantage",
"time": "after about 1 hour",
"value": 28.6
},
{
"name": "Viewed Faq",
"time": "after about 2 hours",
"value": 14.2
}
]
<!DOCTYPE html>
<meta charset="utf-8">
<title>Bar Chart with Negative Values</title>
<style>
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 8px;
}
.bar.key {
fill: steelblue;
}
.bar.event {
fill: lightsteelblue;
}
.axis text {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.percent {
fill: #777;
font-size: 80%;
}
.title {
font-weight: bold;
}
.time {
font-weight: normal;
font-style: italic;
fill: #999;
}
.line {
fill: none;
stroke: red;
stroke-width: 1px;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var margin = {top: 30, right: 10, bottom: 10, left: 10},
width = 400 - margin.left - margin.right,
height = 660 - margin.top - margin.bottom;
var x = d3.scale.linear()
.range([0, width])
var y = d3.scale.ordinal()
.rangeRoundBands([0, height], .2);
var xAxis = d3.svg.axis()
.scale(x)
.orient("top");
var line = d3.svg.line()
.x(function(d) { return x(Math.min(0, -1 * d.value)); })
.y(function(d) { return y(d.name) + y.rangeBand()/2; });
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 + ")");
d3.json("events.json", function(error, data) {
x.domain([-120,250]);
y.domain(data.map(function(d) { return d.name; }));
var bar = svg.selectAll(".bar")
.data(data)
.enter().append("g");
bar.append("rect")
.attr("class", function(d) { return d.type ? "bar key" : "bar event"; })
.attr("x", function(d) { return x(Math.min(0, -1 * d.value)); })
.attr("y", function(d) { return y(d.name); })
.attr("width", function(d) { return Math.abs(x(-1 * d.value) - x(0)); })
.attr("height", y.rangeBand());
var percent = bar.append("g")
.style("text-anchor", "end")
.attr("transform", function(d) { return "translate(" + x(-1 * d.value - 3) + "," + (y(d.name) + y.rangeBand() * 0.6) + ")"; } );
percent.append("text")
.attr("class", "percent")
.text(function(d) { return d.value + "%"; })
var title = bar.append("g")
.style("text-anchor", "begin")
.attr("transform", function(d) { return "translate(" + x(5) + "," + (y(d.name) + y.rangeBand() * 0.6) + ")"; } );
title.append("text")
.attr("class", "title")
.text(function(d) { return d.name; })
.append("tspan")
.attr("class", "time")
.attr("dx", "0.5em")
.text(function(d) { return d.time; });
svg.append("path")
.datum(data.filter(function(d) { return d.value >= 80; }))
.attr("class", "line")
.attr("d", line);
svg.append("g")
.attr("class", "y axis")
.append("line")
.attr("x1", x(0))
.attr("x2", x(0))
.attr("y2", height);
});
function type(d) {
d.value = +d.value;
return d;
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment