Skip to content

Instantly share code, notes, and snippets.

@austinhyde
Last active June 5, 2020 12:23
Show Gist options
  • Save austinhyde/02a038074dbd9a7b1eb3 to your computer and use it in GitHub Desktop.
Save austinhyde/02a038074dbd9a7b1eb3 to your computer and use it in GitHub Desktop.
3D Bar Chart
function bar3d() {
var config = {
x: prop('x'),
y: prop('y'),
z: prop('z'),
width: prop('width'),
height: prop('height'),
depth: prop('depth'),
camera: [0, 0, 1]
};
function bar3d(g) {
g.each(function(d){
var g = d3.select(this);
var faces = getFaces.apply(this, arguments);
_.each(faces, function(points, name) {
var path = g.selectAll('.face.'+name).data([name]);
path.exit().remove();
path.enter().append('path')
.attr('class', 'face ' + name);
d3.transition(path)
.attr('d', svgHelp.polygon(points));
});
});
}
function getFaces() {
// resolve absolute positions
// x,y,z is left top front corner of the bar
var left = config.x.apply(this, arguments);
var right = left + config.width.apply(this, arguments);
var top = config.y.apply(this, arguments);
var bottom = top + config.height.apply(this, arguments);
var front = config.z.apply(this, arguments);
var back = front + config.depth.apply(this, arguments);
var cx = config.camera[0];
var cy = config.camera[1];
var cz = config.camera[2];
var projection = perspective(config.camera);
var faces = {};
if (front > cz) {
faces.front = projection([
[left, top, front],
[left, bottom, front],
[right, bottom, front],
[right, top, front]
]);
}
if (top > cy) {
faces.top = projection([
[left, top, front],
[left, top, back],
[right, top, back],
[right, top, front]
]);
}
if (left > cx) {
faces.left = projection([
[left, top, front],
[left, top, back],
[left, bottom, back],
[left, bottom, front]
]);
}
if (right < cx) {
faces.right = projection([
[right, top, front],
[right, top, back],
[right, bottom, back],
[right, bottom, front]
]);
}
return faces;
}
accessor(bar3d, config, 'camera', true);
accessor(bar3d, config, 'x');
accessor(bar3d, config, 'y');
accessor(bar3d, config, 'z');
accessor(bar3d, config, 'width');
accessor(bar3d, config, 'height');
accessor(bar3d, config, 'depth');
return bar3d;
}
letter frequency
A .08167
B .01492
C .02782
D .04253
E .12702
F .02288
G .02015
H .06094
I .06966
J .00153
K .00772
L .04025
M .02406
N .06749
O .07507
P .01929
Q .00095
R .05987
S .06327
T .09056
U .02758
V .00978
W .02360
X .00150
Y .01974
Z .00074
<!DOCTYPE html>
<html lang="en">
<head>
<title>3d Bar Chart</title>
<style>
.bar .face {
shape-rendering: geometricPrecision;
stroke: #4286b4;
stroke-width: .7px;
}
.bar .face.front {
fill: #4286b4;
}
.bar .face.top {
fill: #3b82bd;
}
.bar .face.left,
.bar .face.right {
fill: #3675a9;
}
.bar:hover .face {
stroke: #A52A2A;
}
.bar:hover .face.front {
fill: #A52A2A;
}
.bar:hover .face.top {
fill: #991920;
}
.bar:hover .face.left,
.bar:hover .face.right {
fill: #8e181e;
}
.axis {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.x.axis path {
display: none;
}
</style>
</head>
<body>
<div id="chart"></div>
<script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
<script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/d3/3.4.8/d3.min.js"></script>
<script type="text/javascript" src="utils.js"></script>
<script type="text/javascript" src="bar3d.js"></script>
<script type="text/javascript">
var margin = {
top: 20,
right: 20,
bottom: 30,
left: 40,
front: 0,
back: 0
};
var width = 960 - margin.left - margin.right;
var height = 500 - margin.top - margin.bottom;
var depth = 100 - margin.front - margin.back;
var xScale = d3.scale.ordinal()
.rangeRoundBands([0, width], .2);
var yScale = d3.scale.linear()
.range([height, 0]);
var zScale = d3.scale.ordinal()
.domain([0, 1, 2])
.rangeRoundBands([0, depth], .4);
var xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom');
var yAxis = d3.svg.axis()
.scale(yScale)
.orient('left')
.ticks(10, '%');
var chart = d3.select('#chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', svgHelp.translate(margin.left, margin.right));
d3.tsv('data.tsv', type, function(err, data) {
if (err) return;
xScale.domain(_.sortBy(_.uniq(_.map(data, 'letter'))));
yScale.domain([0, _.max(data, 'frequency').frequency]);
function x(d) { return xScale(d.letter); }
function y(d) { return yScale(d.frequency); }
var camera = [width / 2, height / 2, -1000];
var barGen = bar3d()
.camera(camera)
.x(x)
.y(y)
.z(zScale(0))
.width(xScale.rangeBand())
.height(function(d) { return height - y(d); })
.depth(xScale.rangeBand());
chart.append('g')
.attr('class', 'x axis')
.attr('transform', svgHelp.translate(0, height))
.call(xAxis);
chart.append('g')
.attr('class', 'y axis')
.call(yAxis)
.append('text')
.attr('transform', svgHelp.rotate(-90))
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end')
.text('Frequency');
var extent = xScale.rangeExtent();
var middle = (extent[1] - extent[0]) / 2;
chart.selectAll('g.bar').data(data)
.enter().append('g')
.attr('class', 'bar')
// sort based on distance from center, so we draw outermost
// bars first. otherwise, bars drawn later might overlap bars drawn first
.sort(function(a, b) {
return Math.abs(x(b) - middle) - Math.abs(x(a) - middle);
})
.call(barGen);
});
function type(d) {
d.frequency = +d.frequency;
return d;
}
</script>
</body>
</html>
Copyright 2020 Austin Hyde
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// some helpers for working with svg
var svgHelp = {
translate: function(x, y) {
return 'translate(' + x.toFixed(2) + ',' + y.toFixed(2) + ')';
},
rotate: function(d) {
return 'rotate(' + d.toFixed(2) + ')';
},
polygon: function (points) {
points = points.map(function(p) {
return p.map(prop('toFixed', 2)).join(',');
});
return 'M' + points.join('L') + 'Z';
}
};
// generate a projection transformation from 3d to 2d
function perspective(camera) {
var cx = camera[0],
cy = camera[1],
cz = camera[2];
return function (points) {
return points.map(function(point) {
var px = point[0],
py = point[1],
pz = point[2];
return [ ((cx - px) * cz) / (pz - cz) + cx,
((cy - py) * cz) / (pz - cz) + cy ];
})
};
}
function functor(x) {
return _.isFunction(x) ? x : function() { return x };
}
// prop('x')({ x: 3 }) -> 3
// prop('slice', 1, 1)([0, 1, 2, 3]) -> [1]
function prop(p) {
var args = _.rest(arguments);
return function(o) {
return functor(o[p]).apply(o, args);
};
}
function accessor(target, config, name, isNotFunctor) {
target[name] = function (value) {
if (!arguments.length) return config[name];
config[name] = isNotFunctor ? value : functor(value);
return target;
};
}
@minniekp
Copy link

minniekp commented Jun 5, 2020

var camera = [width / 2, height / 2, -1000];
var barGen = bar3d()
.camera(camera)
.x(x)
.y(y)
.z(zScale(0))
.width(xScale.rangeBand())
.height(function(d) { return height - y(d); })
.depth(xScale.rangeBand());

I am getting an error in the above code saying "property camera does not exist on type (g: any) => void" when I try to call on bar3d() function. Please let me know what I am doing wrong here.

Also I need to make a cube on the left side of the bars for all bars in a grouped bar chart and stacked bar chart. Can you please tell me how to achieve this in 3d?

@minniekp
Copy link

minniekp commented Jun 5, 2020 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment