Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active December 26, 2015 11:08
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 nitaku/7141176 to your computer and use it in GitHub Desktop.
Save nitaku/7141176 to your computer and use it in GitHub Desktop.
Gosper regions (quantitative)

An experiment with Gosper regions and quantitative data. A sequence of elements, each with its own quantitative variable, is sorted, then placed on the map following the Gosper curve. Quantization can then be changed without changing the elements' position, yet still producing adjacent regions with a good aspect ratio.

If such a solution would fit a certain problem is anyone's guess, but I think it could be of help if a ZUI is desirable (e.g. if users want to request details about a certain element or its neighborhood).

global = {}
### compute a Lindenmayer system given an axiom, a number of steps and rules ###
fractalize = (config) ->
input = config.axiom
for i in [0...config.steps]
output = ''
for char in input
if char of config.rules
output += config.rules[char]
else
output += char
input = output
return output
### convert a Lindenmayer string into an array of hexagonal coordinates ###
hex_coords = (config) ->
directions = [
{x:+1, y:-1, z: 0},
{x:+1, y: 0, z:-1},
{x: 0, y:+1, z:-1},
{x:-1, y:+1, z: 0},
{x:-1, y: 0, z:+1},
{x: 0, y:-1, z:+1}
]
### start the walk from the origin cell, facing east ###
path = [{x:0,y:0,z:0}]
dir_i = 0
for char in config.fractal
if char == '+'
dir_i = (dir_i+1) % directions.length
else if char == '-'
dir_i = dir_i-1
if dir_i == -1
dir_i = 5
else if char == 'F'
dir = directions[dir_i]
current = path[path.length-1]
path.push {x:current.x+dir.x, y:current.y+dir.y, z:current.z+dir.z}
return path
window.main = () ->
width = 960
height = 500
svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
global.vis = svg.append('g')
.attr('transform', 'translate(540,10)')
### create the input sequence (random length between 1000 and 2401) ###
### give the element a random value (random choice between uniform and triangular distribution) ###
seq = ((if Math.random() > 0.5 then Math.random() else (Math.random()+Math.random())/2) for i in [0..1000+Math.floor(Math.random() * 1401)])
### sort the sequence by value ###
seq.sort()
### create the Gosper curve ###
gosper = fractalize
axiom: 'A'
steps: 4
rules:
A: 'A+BF++BF-FA--FAFA-BF+'
B: '-FA+BFBF++BF+FA--FA-B'
### convert the curve into coordinates of hex cells ###
coords = hex_coords
fractal: gosper
### create the GeoJSON hexes ###
hexes = {
type: 'FeatureCollection',
features: (new_hex(coords[i], e) for e, i in seq)
}
### custom projection to make hexagons appear regular (y axis is also flipped) ###
radius = 5
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)
})
### draw the cells ###
global.vis.selectAll('.hex')
.data(hexes.features)
.enter().append('path')
.attr('class', 'hex')
.attr('d', path_generator)
.append('title')
recolor(false)
document.getElementById('default').focus()
### color the cells according to the selected quantization ###
window.recolor = (quantization) ->
### define a quantize scale ###
if quantization isnt false
quantize = d3.scale.quantize()
.domain([0, 1])
.range(d3.range(0,1,1.0/quantization))
else
quantize = (x) -> x
### define a linear color scale ###
colorify = d3.scale.linear()
.domain([0, 1])
.range(['rgb(247,251,255)','rgb(8,48,107)'])
.interpolate(d3.interpolateHcl)
### define a percentage format ###
perc = d3.format('.4p')
global.vis.selectAll('.hex')
.attr('fill', (d) -> colorify quantize d.properties['value'])
.attr('stroke', (d) -> colorify quantize d.properties['value'])
.select('title')
.text((d) -> "#{perc d.properties['value']} > #{perc quantize d.properties['value']}")
### create a new hexagon ###
new_hex = (c, e) ->
### conversion from hex coordinates to rect ###
x = 2*(c.x + c.z/2.0)
y = 2*c.z
return {
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [[
[x, y+2],
[x+1, y+1],
[x+1, y],
[x, y-1],
[x-1, y],
[x-1, y+1],
[x, y+2]
]]
},
properties: {
'value': e
}
}
svg {
background: lightgrey;
}
.hex {
stroke-width: 1;
}
.hex:hover {
fill: yellow;
}
#buttons {
position: absolute;
top: 16px;
left: 16px;
}
button {
border: 0;
border-bottom: 2px solid #555555;
height: 32px;
border-radius: 3px;
font-weight: bold;
text-shadow: 0px -2px #555555;
background: gray;
color: #eeeeee;
padding: 8px;
box-shadow: 0 0 6px rgba(0, 0, 0, 0.4);
}
button:hover {
background: #707070;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Gosper Regions (quantitative data)</title>
<link type="text/css" href="index.css" rel="stylesheet"/>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="index.js"></script>
</head>
<body onload="main()">
<div id="buttons">
<button id="default" onclick="recolor(false)">No quantization</button>
<button onclick="recolor(9)">9 ranges</button>
<button onclick="recolor(5)">5 ranges</button>
<button onclick="recolor(3)">3 ranges</button>
</div>
</body>
</html>
(function() {
var fractalize, global, hex_coords, new_hex;
global = {};
/* compute a Lindenmayer system given an axiom, a number of steps and rules
*/
fractalize = function(config) {
var char, i, input, output, _i, _len, _ref;
input = config.axiom;
for (i = 0, _ref = config.steps; 0 <= _ref ? i < _ref : i > _ref; 0 <= _ref ? i++ : i--) {
output = '';
for (_i = 0, _len = input.length; _i < _len; _i++) {
char = input[_i];
if (char in config.rules) {
output += config.rules[char];
} else {
output += char;
}
}
input = output;
}
return output;
};
/* convert a Lindenmayer string into an array of hexagonal coordinates
*/
hex_coords = function(config) {
var char, current, dir, dir_i, directions, path, _i, _len, _ref;
directions = [
{
x: +1,
y: -1,
z: 0
}, {
x: +1,
y: 0,
z: -1
}, {
x: 0,
y: +1,
z: -1
}, {
x: -1,
y: +1,
z: 0
}, {
x: -1,
y: 0,
z: +1
}, {
x: 0,
y: -1,
z: +1
}
];
/* start the walk from the origin cell, facing east
*/
path = [
{
x: 0,
y: 0,
z: 0
}
];
dir_i = 0;
_ref = config.fractal;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
char = _ref[_i];
if (char === '+') {
dir_i = (dir_i + 1) % directions.length;
} else if (char === '-') {
dir_i = dir_i - 1;
if (dir_i === -1) dir_i = 5;
} else if (char === 'F') {
dir = directions[dir_i];
current = path[path.length - 1];
path.push({
x: current.x + dir.x,
y: current.y + dir.y,
z: current.z + dir.z
});
}
}
return path;
};
window.main = function() {
var coords, dx, dy, e, gosper, height, hexes, i, path_generator, radius, seq, svg, width;
width = 960;
height = 500;
svg = d3.select('body').append('svg').attr('width', width).attr('height', height);
global.vis = svg.append('g').attr('transform', 'translate(540,10)');
/* create the input sequence (random length between 1000 and 2401)
*/
/* give the element a random value (random choice between uniform and triangular distribution)
*/
seq = (function() {
var _ref, _results;
_results = [];
for (i = 0, _ref = 1000 + Math.floor(Math.random() * 1401); 0 <= _ref ? i <= _ref : i >= _ref; 0 <= _ref ? i++ : i--) {
_results.push(Math.random() > 0.5 ? Math.random() : (Math.random() + Math.random()) / 2);
}
return _results;
})();
/* sort the sequence by value
*/
seq.sort();
/* create the Gosper curve
*/
gosper = fractalize({
axiom: 'A',
steps: 4,
rules: {
A: 'A+BF++BF-FA--FAFA-BF+',
B: '-FA+BFBF++BF+FA--FA-B'
}
});
/* convert the curve into coordinates of hex cells
*/
coords = hex_coords({
fractal: gosper
});
/* create the GeoJSON hexes
*/
hexes = {
type: 'FeatureCollection',
features: (function() {
var _len, _results;
_results = [];
for (i = 0, _len = seq.length; i < _len; i++) {
e = seq[i];
_results.push(new_hex(coords[i], e));
}
return _results;
})()
};
/* custom projection to make hexagons appear regular (y axis is also flipped)
*/
radius = 5;
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);
}
}));
/* draw the cells
*/
global.vis.selectAll('.hex').data(hexes.features).enter().append('path').attr('class', 'hex').attr('d', path_generator).append('title');
recolor(false);
return document.getElementById('default').focus();
};
/* color the cells according to the selected quantization
*/
window.recolor = function(quantization) {
/* define a quantize scale
*/
var colorify, perc, quantize;
if (quantization !== false) {
quantize = d3.scale.quantize().domain([0, 1]).range(d3.range(0, 1, 1.0 / quantization));
} else {
quantize = function(x) {
return x;
};
}
/* define a linear color scale
*/
colorify = d3.scale.linear().domain([0, 1]).range(['rgb(247,251,255)', 'rgb(8,48,107)']).interpolate(d3.interpolateHcl);
/* define a percentage format
*/
perc = d3.format('.4p');
return global.vis.selectAll('.hex').attr('fill', function(d) {
return colorify(quantize(d.properties['value']));
}).attr('stroke', function(d) {
return colorify(quantize(d.properties['value']));
}).select('title').text(function(d) {
return "" + (perc(d.properties['value'])) + " > " + (perc(quantize(d.properties['value'])));
});
};
/* create a new hexagon
*/
new_hex = function(c, e) {
/* conversion from hex coordinates to rect
*/
var x, y;
x = 2 * (c.x + c.z / 2.0);
y = 2 * c.z;
return {
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [[[x, y + 2], [x + 1, y + 1], [x + 1, y], [x, y - 1], [x - 1, y], [x - 1, y + 1], [x, y + 2]]]
},
properties: {
'value': e
}
};
};
}).call(this);
svg
background: lightgray
.hex
stroke-width: 1
&:hover
fill: yellow
#buttons
position: absolute
top: 16px
left: 16px
button
border: 0
border-bottom: 2px solid #555
height: 32px
border-radius: 3px
font-weight: bold
text-shadow: 0px -2px #555
background: gray
color: #EEE
padding: 8px
box-shadow: 0 0 6px rgba(0,0,0,0.4)
&:hover
background: #707070
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment