Another unambiguous fractal treemap, this time a bigger one.
(function() {
var concat;
concat = function(a) {
return a.reduce((function(a, o) {
return a.concat(o);
}), []);
};
window.main = function() {
/* "globals"
*/
var categories, colorify, container, dx, dy, height, legend, path_generator, radius, scale, svg, vis, whiten, whiteness, width, _i, _ref, _ref2, _results;
vis = null;
width = 960;
height = 500;
svg = d3.select('body').append('svg').attr('width', width).attr('height', height);
/* ZOOM and PAN
*/
/* create container elements
*/
container = svg.append('g').attr('transform', 'translate(1370, -136)');
container.call(d3.behavior.zoom().scaleExtent([0.3, 49]).on('zoom', (function() {
return vis.attr('transform', "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
})));
vis = container.append('g');
/* create a rectangular overlay to catch events
*/
/* WARNING rect size is huge but not infinite. this is a dirty hack
*/
vis.append('rect').attr('class', 'overlay').attr('x', -500000).attr('y', -500000).attr('width', 1000000).attr('height', 1000000);
/* END ZOOM and PAN
*/
/* custom projection to make hexagons appear regular (y axis is also flipped)
*/
radius = 6;
dx = radius * 2 * Math.sin(Math.PI / 3);
dy = radius * 1.5;
path_generator = d3.geo.path().projection(d3.geo.transform({
point: function(x, y) {
return this.stream.point(x * dx / 2, -(y - (2 - (y & 1)) / 3) * dy / 2);
}
}));
/* depth scale
*/
whiteness = 0.6;
whiten = function(color) {
return d3.interpolateHcl(color, d3.hcl(void 0, 0, 100))(whiteness);
};
colorify = d3.scale.quantize().domain([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]).range(['#396353', '#0DB14B', '#6DC067', '#ABD69B', '#DAEAC1', '#DFCCE4', '#C7B2D6', '#9474B4', '#754098', '#504971'].map(whiten));
/* legend
*/
legend = svg.append('g').attr('id', 'legend').attr('transform', 'translate(30,20)');
scale = (function() {
_results = [];
for (var _i = _ref = colorify.domain()[0], _ref2 = colorify.domain()[1]; _ref <= _ref2 ? _i <= _ref2 : _i >= _ref2; _ref <= _ref2 ? _i++ : _i--){ _results.push(_i); }
return _results;
}).apply(this);
categories = legend.selectAll('.category').data(scale).enter().append('g').attr('class', 'category').attr('transform', function(d, i) {
return "translate(0," + (i * 22) + ")";
});
categories.append('rect').attr('x', 0).attr('y', 0).attr('width', 22).attr('height', 22).attr('fill', function(d) {
return colorify(d);
});
categories.append('text').attr('y', 11).attr('dx', -4).attr('dy', '0.35em').text(function(d) {
return d;
});
/* load topoJSON data
*/
return d3.json('regions_depth.topo.json', function(error, data) {
/* write the name of the land
*/
var defs, depth, title;
title = svg.append('text').attr('class', 'title').text(topojson.feature(data, data.objects['0']).features[0].properties.label).attr('transform', 'translate(80,40)');
/* define the level zero region (the land)
*/
defs = svg.append('defs');
defs.append('path').datum(topojson.feature(data, data.objects['0']).features[0]).attr('id', 'land').attr('d', path_generator);
/* faux land glow (using filters takes too much resources)
*/
vis.append('use').attr('class', 'land-glow-outer').attr('xlink:href', '#land');
vis.append('use').attr('class', 'land-glow-inner').attr('xlink:href', '#land');
/* draw the depth levels
*/
vis.selectAll('.contour').data(topojson.feature(data, data.objects.depth).features).enter().append('path').attr('class', 'contour').attr('d', path_generator).attr('fill', function(d) {
return colorify(d.properties.depth);
});
/* draw the land border
*/
vis.append('use').attr('class', 'land-fill').attr('xlink:href', '#land');
/* draw the political boundaries
*/
for (depth = 1; depth <= 2; depth++) {
vis.append('path').datum(topojson.mesh(data, data.objects[depth], (function(a, b) {
return a !== b;
}))).attr('d', path_generator).attr('class', 'boundary').style('stroke-width', "" + (1.8 / depth) + "px");
}
/* draw the other labels
*/
return vis.selectAll('.label').data(concat((function() {
var _results2;
_results2 = [];
for (depth = 2; depth >= 1; depth--) {
_results2.push(topojson.feature(data, data.objects[depth]).features.map(function(d) {
return {
feature: d,
depth: depth
};
}));
}
return _results2;
})())).enter().append('text').attr('class', 'label').attr('dy', '0.35em').attr('transform', (function(d) {
var area, centroid;
centroid = path_generator.centroid(d.feature);
area = path_generator.area(d.feature);
return "translate(" + centroid[0] + "," + centroid[1] + ") scale(" + (10 / d.depth) + ")";
})).text(function(d) {
return d.feature.properties.label;
});
});
};
}).call(this);
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Fractal treemap - Panebukyaar (13,308)</title>
<link type="text/css" href="index.css" rel="stylesheet"/>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script src="index.js"></script>
</head>
<body onload="main()">
</body>
</html>
from __future__ import print_function
from itertools import izip
import csv
import json
from shapely.geometry.polygon import Polygon
import shapely.wkt
from fiona import collection
from shapely.geometry import mapping
import re
import os
# yield the leaves of the tree, also computing each leaf path
def leafify(node, path=()):
if 'c' not in node:
node['path'] = path + (node,)
yield node
else:
for c in node['c']:
for l in leafify(c, path + (node,)):
yield l
def gosperify(tree_path, hexes_path, output_regions_dir_path, depth_filename):
leaves_done = 0
layers = {}
depth_levels = {}
print('Reading the tree...')
with open(tree_path, 'rb') as tree_file:
tree = json.loads(tree_file.read())
print('Reading hexes...')
# iterate over the hexes taken from the file
with open(hexes_path, 'rb') as hexes_file:
hexes_reader = csv.reader(hexes_file, delimiter=';', quotechar='#')
for leaf, hexes_row in izip(leafify(tree), hexes_reader):
path = leaf['path']
hex = shapely.wkt.loads(hexes_row[0])
for depth in xrange(len(path)):
# add the hex to its political regions
ancestor_or_self = path[depth]
if depth not in layers:
layers[depth] = {}
if id(ancestor_or_self) not in layers[depth]:
layers[depth][id(ancestor_or_self)] = {
'geometry': hex,
'node': ancestor_or_self
}
else:
layers[depth][id(ancestor_or_self)]['geometry'] = layers[depth][id(ancestor_or_self)]['geometry'].union(hex)
# add the hex to all the depth levels with d <= depth
if depth not in depth_levels:
depth_levels[depth] = hex
else:
depth_levels[depth] = depth_levels[depth].union(hex)
# logging
leaves_done += 1
print('%d leaves done' % leaves_done, end='\r')
print('Writing political regions...')
schema = {'geometry': 'Polygon', 'properties': {'label': 'str'}}
if not os.path.exists(output_regions_dir_path):
os.makedirs(output_regions_dir_path)
for depth, regions in layers.items():
with collection(output_regions_dir_path+'/'+str(depth)+'.json', 'w', 'GeoJSON', schema) as output:
for _, region_obj in regions.items():
output.write({
'properties': {
'label': region_obj['node']['l']
},
'geometry': mapping(region_obj['geometry'])
})
print('Writing depth_levels...')
schema = {'geometry': 'Polygon', 'properties': {'depth': 'int'}}
with collection(depth_filename, 'w', 'GeoJSON', schema) as output:
for depth, region in depth_levels.items():
output.write({
'properties': {
'depth': depth
},
'geometry': mapping(region)
})
concat = (a) -> a.reduce ((a,o) -> a.concat(o)), []
window.main = () ->
### "globals" ###
vis = null
width = 960
height = 500
svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
### ZOOM and PAN ###
### create container elements ###
container = svg.append('g')
.attr('transform','translate(1370, -136)')
container.call(d3.behavior.zoom().scaleExtent([0.3, 49]).on('zoom', (() -> vis.attr('transform', "translate(#{d3.event.translate})scale(#{d3.event.scale})"))))
vis = container.append('g')
### create a rectangular overlay to catch events ###
### WARNING rect size is huge but not infinite. this is a dirty hack ###
vis.append('rect')
.attr('class', 'overlay')
.attr('x', -500000)
.attr('y', -500000)
.attr('width', 1000000)
.attr('height', 1000000)
### END ZOOM and PAN ###
### custom projection to make hexagons appear regular (y axis is also flipped) ###
radius = 6
dx = radius * 2 * Math.sin(Math.PI / 3)
dy = radius * 1.5
path_generator = d3.geo.path()
.projection d3.geo.transform({
point: (x,y) ->
this.stream.point(x * dx / 2, -(y - (2 - (y & 1)) / 3) * dy / 2)
})
### depth scale ###
whiteness = 0.6
whiten = (color) -> d3.interpolateHcl(color, d3.hcl(undefined,0,100))(whiteness)
colorify = d3.scale.quantize()
.domain([0..9])
.range(['#396353','#0DB14B','#6DC067','#ABD69B','#DAEAC1','#DFCCE4','#C7B2D6','#9474B4','#754098','#504971'].map(whiten))
# .range(['#59A80F','#9ED54C','#C4ED68','#E2FF9E','#F0F2DD','#F8CA8C','#E9A189','#D47384','#AC437B','#8C286E'])
# .range(['#FFFCF6','#FFF7DB','#FFF4C2','#FEECAE','#F8CA8C','#F0A848','#C07860','#A86060','#784860','#604860'])
### legend ###
legend = svg.append('g')
.attr('id','legend')
.attr('transform', 'translate(30,20)')
scale = [colorify.domain()[0]..colorify.domain()[1]]
categories = legend.selectAll('.category')
.data(scale)
.enter().append('g')
.attr('class', 'category')
.attr('transform', (d,i) -> "translate(0,#{i*22})")
categories.append('rect')
.attr('x', 0).attr('y', 0)
.attr('width', 22).attr('height', 22)
.attr('fill', (d) -> colorify(d))
categories.append('text')
.attr('y', 11)
.attr('dx', -4)
.attr('dy', '0.35em')
.text((d) -> d)
### load topoJSON data ###
d3.json 'regions_depth.topo.json', (error, data) ->
### write the name of the land ###
title = svg.append('text')
.attr('class', 'title')
.text(topojson.feature(data, data.objects['0']).features[0].properties.label)
.attr('transform', 'translate(80,40)')
### define the level zero region (the land) ###
defs = svg.append('defs')
defs.append('path')
.datum(topojson.feature(data, data.objects['0']).features[0])
.attr('id', 'land')
.attr('d', path_generator)
### faux land glow (using filters takes too much resources) ###
vis.append('use')
.attr('class', 'land-glow-outer')
.attr('xlink:href', '#land')
vis.append('use')
.attr('class', 'land-glow-inner')
.attr('xlink:href', '#land')
### draw the depth levels ###
vis.selectAll('.contour')
.data(topojson.feature(data, data.objects.depth).features)
.enter().append('path')
.attr('class', 'contour')
.attr('d', path_generator)
.attr('fill', (d) -> colorify(d.properties.depth))
### draw the land border ###
vis.append('use')
.attr('class', 'land-fill')
.attr('xlink:href', '#land')
### draw the political boundaries ###
for depth in [1..2]
vis.append('path')
.datum(topojson.mesh(data, data.objects[depth], ((a,b) -> a isnt b)))
.attr('d', path_generator)
.attr('class', 'boundary')
.style('stroke-width', "#{1.8/depth}px")
### draw the other labels ###
vis.selectAll('.label')
.data(concat((topojson.feature(data, data.objects[depth]).features.map((d) -> {feature: d, depth: depth}) for depth in [2..1])))
.enter().append('text')
.attr('class', 'label')
.attr('dy','0.35em')
.attr('transform', ((d) ->
centroid = path_generator.centroid(d.feature)
area = path_generator.area(d.feature)
return "translate(#{centroid[0]},#{centroid[1]}) scale(#{10/d.depth})"
))
.text((d) -> d.feature.properties.label)
#land {
fill: none;
}
.land-glow-outer {
stroke: #eeeeee;
stroke-width: 16px;
}
.land-glow-inner {
stroke: #dddddd;
stroke-width: 8px;
}
.land-fill {
stroke: #444444;
stroke-width: 2px;
}
.contour {
stroke: none;
}
.category text {
text-anchor: end;
fill: #444444;
font-family: sans-serif;
font-weight: bold;
font-size: 14px;
pointer-events: none;
text-shadow: -1px 0 white, 0 1px white, 1px 0 white, 0 -1px white;
}
.boundary {
stroke: #444444;
fill: none;
pointer-events: none;
}
@font-face {
font-family: "Lacuna";
src: url("lacuna.ttf");
}
.label {
text-transform: capitalize;
text-anchor: middle;
font-size: 2.5px;
fill: #444444;
pointer-events: none;
font-family: "Lacuna";
text-shadow: -2px 0 white, 0 2px white, 2px 0 white, 0 -2px white, -1px -1px white, 1px -1px white, 1px 1px white, -1px 1px white;
}
.title {
text-transform: capitalize;
text-anchor: start;
font-size: 24pt;
fill: #444444;
pointer-events: none;
font-family: "Lacuna";
text-shadow: -1px 0 white, 0 1px white, 1px 0 white, 0 -1px white;
}
.overlay {
fill: transparent;
}
@mixin halo($color)
text-shadow: -1px 0 $color, 0 1px $color, 1px 0 $color, 0 -1px $color
@mixin halo_double($color)
text-shadow: -2px 0 $color, 0 2px $color, 2px 0 $color, 0 -2px $color, -1px -1px $color, 1px -1px $color, 1px 1px $color, -1px 1px $color
#land
fill: none
.land-glow-outer
stroke: #EEE
stroke-width: 16px
.land-glow-inner
stroke: #DDD
stroke-width: 8px
.land-fill
stroke: #444
stroke-width: 2px
.contour
stroke: none
.category text
text-anchor: end
fill: #444
font-family: sans-serif
font-weight: bold
font-size: 14px
pointer-events: none
@include halo(white)
.boundary
stroke: #444
fill: none
// avoid blinking when hovering regions
pointer-events: none
@font-face
font-family: 'Lacuna'
src: url('lacuna.ttf')
.label
text-transform: capitalize
text-anchor: middle
font-size: 2.5px
fill: #444
pointer-events: none
font-family: 'Lacuna'
@include halo_double(white)
.title
text-transform: capitalize
text-anchor: start
font-size: 24pt
fill: #444
pointer-events: none
font-family: 'Lacuna'
@include halo(white)
// zoom and pan
.overlay
fill: transparent