Skip to content

Instantly share code, notes, and snippets.

@GerHobbelt
Created February 22, 2012 09:56
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save GerHobbelt/1883717 to your computer and use it in GitHub Desktop.
Save GerHobbelt/1883717 to your computer and use it in GitHub Desktop.
d3.js: high dynamic range in the data where you want it by picking a suitable log() offset
# Editor backup files
*.bak
*~
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>Multiple Bar Charts showcasing various scale methods and data offset-ing technique</title>
<script type="text/javascript" src="https://raw.github.com/GerHobbelt/d3/master/d3.latest.js"></script>
<!--<script type="text/javascript" src="../../../../js/d3/d3.latest.js"></script>-->
<style>
circle {
stroke: #fff;
stroke-width: 1.5px;
}
body, p {
font: 12px sans-serif;
}
text {
font: 10px sans-serif;
}
#chart
{
/* bl.ocks.org sizes: */
width: 950px;
height: 1400px;
overflow: auto;
}
.chart rect
{
stroke: white;
fill: #4682B4;
}
.chart text.bar
{
/* stroke: #4682B4; */
fill: #89b6db;
}
</style>
</head>
<body>
<p>Select your offset by dragging this slider:</p>
<input type="range" id="graph_offset" name="offset" min="0" max="1000" value="0" onchange="redraw_on_offset_change();" style="width: 900px;">
<p>Offset: <span id="graph_offset_perc"></span>% <span style="color: #888;">(0% means no offset is applied to the data; 100% means the minimum value of the data range is mapped to an almost-zero value)</span></p>
<div id="chart"></div>
<script type="text/javascript" src="http://gerhobbelt.github.com/bl.ocks.org-hack/fixit.js" ></script>
<script type="text/javascript">
/*
Correct bl.ocks.org IFRAME, Iff we're shown in an iframe!
*/
d3.select(window.frameElement)
.style("height", "1500px");
var data_series = [
{name: 'basic', power: 1, values: [4, 8, 12, 16, 20, 24, 28, 32] },
{name: 'small', power: 3, values: [2,5,20,50,100,500,2000,2900] },
{name: 'huge', power: 8, values: [10000,20000,100000,1000000,10000000,100000000,250000000] }
];
var x_scales = [
{name: 'linear', scale: d3.scale.linear(), x_min: 0, tick_mult: 1},
{name: 'pow(2)', scale: d3.scale.pow().exponent(2), x_min: 0, tick_mult: 1},
{name: 'sqrt', scale: d3.scale.sqrt(), x_min: 0, tick_mult: 1},
{name: 'pow(1/4)', scale: d3.scale.pow().exponent(1/4), x_min: 0, tick_mult: 1},
{name: 'log10', scale: d3.scale.log(), x_min: 1, tick_mult: 0.2},
{name: 'log10', scale: d3.scale.log(), x_min: 1e4, tick_mult: 0.2}
];
var graph_dims = {
width: 250,
height: 100,
x_offset: 5,
y_offset: 15,
x_gutter: 50,
y_gutter: 30,
x_left_margin: 30
};
// create the SVG node so we can select it in the draw_bar_chart() function:
d3.select("#chart")
.append("svg")
.attr("class", "chart")
.attr("width", 930)
.attr("height", 1050);
plot_all_sample_graphs(0);
function plot_all_sample_graphs(offset_perunage)
{
var graph_x_px_consumption = (graph_dims.width + graph_dims.x_offset + graph_dims.x_gutter);
var graph_y_px_consumption = (graph_dims.height + graph_dims.y_offset + graph_dims.y_gutter);
var xcnt = Math.floor(950 / graph_x_px_consumption);
var i, j;
var l = data_series.length;
var s = x_scales.length;
// print the offset as a percentage:
d3.select('#graph_offset_perc').text(d3.format('.1f')(offset_perunage * 100));
for (j = 0; j < s; j++)
{
for (i = 0; i < l; i++)
{
var pos = {
x: (i % xcnt) * graph_x_px_consumption + graph_dims.x_left_margin,
y: Math.floor(i / xcnt + j * l / xcnt) * graph_y_px_consumption
};
var offset = Math.max(0, offset_perunage * d3.min(data_series[i].values) - 1e-9);
draw_bar_chart(data_series[i], x_scales[j], pos, offset, i + j * l);
}
}
}
function draw_bar_chart(data_series, x_scale, position, graphing_offset, graph_num)
{
// nuke existing graph if it exists; turning this into an animating is left as an exercise for the reader ;-)
d3.select('#chart #graph-' + graph_num).remove();
var offset = {
do_it: function(d) {
return d - graphing_offset;
},
undo: function(d) {
return d + graphing_offset;
}
};
var data = data_series.values.map(offset.do_it);
var fixed_format = d3.format(".0f");
var scientific_format = d3.format(".1e");
var offset_format = d3.format("f");
/*
as the log scale (and possibly a few others) can produce negative 'x' numbers for
our (shifted) domain, while we are only interested to see the original input values
as 'all positive', we first create a 'intermediate' x scale function for our domain, just
so that we can see where we would land range-wise, and then we map that output
onto our graph pixel range in a second scale transform.
*/
var inter_x = x_scale.scale
.domain([d3.min(data), d3.max(data)])
.range([0, 1]);
var x_0 = inter_x.invert(-9); // --> total span 10x size of domain dynamic in graph
// we play a bit nasty when it comes to scale.log when determining the minimum value:
// as we like to see a thickness for the smallest data value as long as possible, we
// check here what's the lower limit here if we don't want NaN's from Math.log():
var x_lower_bound = Math.max(1e-308 /* log NaN protection */, x_0, Math.min(offset.do_it(x_scale.x_min), d3.min(data)));
// use the new lower bound / smallest in-data-range non-zero double value instead of zero itself so log scales don't b0rk with NaN all over the place as log(0) == NaN
var x = x_scale.scale
.domain([x_lower_bound, d3.max(data)])
.range([0, graph_dims.width]);
if (graph_num >= 12) console.log('G'+ graph_num + ': domain / range = ', x.domain(), x.range(), x_lower_bound, x(x.domain()[0]), x(x.domain()[1]), data, [offset.do_it(x_scale.x_min) /* d3.min(data) */, d3.max(data)], data.map(x));
/*
WARNING:
as 'x.ticks()' does all sorts of smart stuff with the domain data
to determine where the 'good looking' ticks should end up, we MUST
feed .ticks() the ORIGINAL data series (we can't easily patch
offset.undo() in them there internals ;-) ), so this complicates
thing a bit: where you would otherwise have used 'x.ticks()',
we now also carry a 'orig_x' which has the unadulterated,
original data domain in mint condition.
It is used ONLY for the ticks placement calculus; everybody else
is forced to rely on the offset back&forth via offset.do_it/undo.
NB: as 'ticks placement' is about calculating the SET OF TICKS,
which must be done in 'original data' space, but the plotting
of all that data is based on the offsetted data, we next
need to offset those tick positions too: hence the
.map(offset.do_it) over there.
The code does NOT take care of ticks at negative/out-of-graph positions.
An extra .filter() would be used for that: pass_only_sensible_ticks()
WARNING #2: Took me a couple of hours pulling hair to find out that
the domain/range of the scale are nuked due to them being
'overwritten' in the next bit, so it is mandatory to CLONE
THE SCALE FIRST: .copy() ! (I should've guessed but
sometimes I get stuck like that. :-(( )
When you remove the .copy() you'll note that the whole
offset business around here is completely b0rked graphically.
*/
var orig_x = x_scale.scale.copy()
.domain([offset.undo(x_lower_bound) /* d3.min(data_series.values) */, d3.max(data_series.values)])
.range([0, graph_dims.width]);
if (graph_num >= 12) console.log('G'+ graph_num + ': original domain / range = ', orig_x.domain(), orig_x.range());
var y = d3.scale.ordinal()
.domain(data)
.rangeBands([0, graph_dims.height]);
// position the new graph in the encompassing <svg> area:
var chart = d3.select("#chart svg")
//.attr("width", graph_dims.width + graph_dims.x_offset)
//.attr("height", graph_dims.height + graph_dims.y_offset)
.append("g")
.attr('id', 'graph-' + graph_num)
.attr("transform", "translate(" + (graph_dims.x_offset + position.x) + "," + (graph_dims.y_offset + position.y) + ")");
var tickcount_advise = 6 * x_scale.tick_mult;
var pass_only_sensible_ticks = function(d, i) {
// only allow x-coordinates >= 0 to make it through:
var c = x(d);
return (c >= 0);
};
var tick_collection = orig_x.ticks(tickcount_advise).map(offset.do_it).filter(pass_only_sensible_ticks);
if (graph_num >= 12) console.log('G'+ graph_num + ': tick collection = ', tick_collection, x(d3.min(tick_collection)));
chart.selectAll("line")
.data(tick_collection /* x.ticks(...) */ )
.enter().append("line")
.attr("x1", x)
.attr("x2", x)
.attr("y1", 0)
.attr("y2", graph_dims.height)
.style("stroke", "#ccc");
var last_tick_x = -666;
chart.selectAll(".rule")
.data(tick_collection /* x.ticks(...) */ )
.enter().append("text")
.attr("x", x)
.attr("y", 0)
.attr("dy", -3)
.attr("text-anchor", "middle")
.text(function(d, i) {
d = offset.undo(d);
/*
heuristic for log scale et al: only draw text when it's far enough apart.
We track the x coordinate for that.
*/
var tx = x(d);
// always 'print' the first tick and every power of ten! UNLESS the previous one was a power-of-10 too, then we tolerate a skip...
var pow_ten_check = Math.log(d) / Math.LN10 + 1e-6 /* epsilon FP fix */;
pow_ten_check = (pow_ten_check - Math.floor(pow_ten_check) < 0.02);
if (tx - last_tick_x < 40 /* pixels */ && i != 0)
{
if (!pow_ten_check)
{
return '';
}
}
last_tick_x = tx;
if (d < 1000)
{
return fixed_format(d);
}
return scientific_format(d);
});
chart.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("y", y)
.attr("width", function(d, i) {
var v = x(d);
if (v < 0) console.log('negative width for d = ', d, i, v, x.domain(), x.range(), x(x.domain()[0]), x(x.domain()[1]));
return v;
})
.attr("height", y.rangeBand());
chart.selectAll(".bar")
.data(data)
.enter().append("text")
.attr("class", "bar")
.attr("x", x)
.attr("y", function(d) { return y(d) + y.rangeBand() / 2; })
.attr("dx", -3)
.attr("dy", ".35em")
.attr("text-anchor", "end")
.text(function(d) {
d = offset.undo(d);
return fixed_format(d);
});
chart.append("line")
.attr("y1", 0)
.attr("y2", graph_dims.height)
.style("stroke", "#000");
chart.append("text")
.attr("x", graph_dims.width / 2)
.attr("y", graph_dims.height + graph_dims.y_offset)
.attr("dy", -3)
.attr("text-anchor", "middle")
.text(data_series.name + ' @ scale: ' + x_scale.name + ', offset: ' + offset_format(graphing_offset) + ', min: ' + offset_format(x_lower_bound));
}
function redraw_on_offset_change()
{
var input = d3.select('#graph_offset');
//var v = input.attr('value');
var v = input.node().value;
v = +v;
plot_all_sample_graphs(v / 1000);
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment