Skip to content

Instantly share code, notes, and snippets.

@mthh
Last active November 30, 2017 23:50
Show Gist options
  • Save mthh/19c11d69ef228d69f126611d4e9802ea to your computer and use it in GitHub Desktop.
Save mthh/19c11d69ef228d69f126611d4e9802ea to your computer and use it in GitHub Desktop.
Sketchy map II
license: gpl-3.0
border: no
/*global d3:false */
/*jshint unused:false*/
/**
* Initiate the sketchy library
* @constructor
*/
var d3sketchy = function(){
/**
* Default attributes for generating the shapes, doing this we don't need to check if all parameters are provided
* And if someone wants to build a lot of shapes with the same properties she just needs to use "setDefaults" to change them
* @type {object}
* @defaultvalue
*/
var defaults = {
x:0,
y:0,
width:20,
height:20,
sketch:1,
density:1,
radius:10,
angle:45,
count:2,
shape:"circle",
clip:"",
margin:2
};
/**
* Changing the default attributes
* @param {object} opts - object with default attributes see "var defaults"
* @return {object} defaults - the full default object
*/
function setDefaults(opts){
defaults = extend(defaults, opts);
return defaults;
}
/**
* merging two objects, source will replace duplicates in destination
* @param {object} destination
* @param {object} source
*/
function extend(destination, source) {
var returnObj = {}, attrname;
for (attrname in destination) { returnObj[attrname] = destination[attrname]; }
for (attrname in source) { returnObj[attrname] = source[attrname]; }
return returnObj;
}
/**
* Generate random number between min and max
* @param {float|int} min
* @param {float|int} max
* @return {float}
*/
function rand(min, max){
return Math.random()* (max-min) + min;
}
/**
* Create sketchy
* @constructor
*/
function sketchy(){
}
/**
* drawing a sketchy line
* this is kind of the heart of the whole tool.
* so if you want to make changes to the appearance of the lines, tweak the following lines
* @param {object} opts
* @param {d3.selection} opts.svg
* @param {float|int} opts.x1 - x point 1
* @param {float|int} opts.y1 - y point 1
* @param {float|int} opts.x2 - x point 2
* @param {float|int} opts.y2 - y point 2
* @param {object} opts.sketch
* @param {object} opts.sketch.x - sketchiness on the x-axis
* @param {object} opts.sketch.y - sketchiness on the y-axis
*/
sketchy.drawLine = function(opts){
//Each line is drawn twice the increase sketchiness
for(var i = 1; i<3; i++){
var or2 = rand(0.2, 0.8);
var cx2 = opts.x1+ (opts.x2-opts.x1)*or2+rand(-1,1);
var cy2 = opts.y1+ (opts.y2-opts.y1)*or2+rand(-1,1);
var or1 = or2 + rand(-0.3, -0.2);
var cx1 = opts.x1+ (opts.x2-opts.x1)*or1+rand(-1,1);
var cy1 = opts.y1+ (opts.y2-opts.y1)*or1+rand(-1,1);
opts.svg.append("path")
.attr("d", "M"+
(opts.x1 + rand(-1,0)*opts.sketch.x/i)+" "+
(opts.y1 + rand(-1,1)*opts.sketch.y/i)+" Q"+
cx1+" "+cy1+" "+
cx2+" "+cy2+" T"+
(opts.x2 + rand(0,1)*opts.sketch.x/i)+" "+
(opts.y2 + rand(-1,1)*opts.sketch.y/i));
}
};
/**
* drawing a circle shape
* no outline just the fill
* @param {object} opts - object containing the attributes
* @param {float|int} opts.x - x position
* @param {float|int} opts.y - y position
* @param {float|int} opts.r - radius
* @param {float|int} opts.angle - angle of the lines (0-360)
* @param {float|int} opts.density - distance between lines
* @param {float|int} opts.sketch - sketchiness factor
* @param {string} opts.shape - this is a development relic, default is "circle", alternatives "cut" and "star"
* @return {object} svg - d3.selection of a group object, containing the circle
*/
sketchy.circleFill = function(opts){
//merging default attributes with user attributes
var merged_opts = extend(defaults, opts);
//create a container, this is used to translate and rotate the circle, this container will be returned at the end of this function
var svg = merged_opts.svg.append("g").attr("transform", "translate("+merged_opts.x+" "+merged_opts.y+") rotate("+merged_opts.angle+")");
//Looping through the lines
var y_dist = 0;
while(y_dist > -2*opts.r){
var x;
//During the development i accidentaly generated those shapes and kept them :)
if(merged_opts.shape==="cut"){
x = Math.sqrt( ( Math.pow(merged_opts.r, 2) - Math.pow((merged_opts.r-Math.abs(y_dist)), 2) ) );
}else if(merged_opts.shape==="star"){
x = merged_opts.r - Math.sqrt( ( Math.pow(merged_opts.r, 2) - Math.pow((merged_opts.r-Math.abs(y_dist)), 2) ) );
}else{
x = Math.sqrt( ( Math.pow(merged_opts.r, 2) - Math.pow((merged_opts.r-Math.abs(y_dist)), 2) ) );
}
//Draw the sketchy lines
sketchy.drawLine({
svg:svg,
x1:-x,
y1:y_dist+merged_opts.r,
x2:x,
y2:y_dist+merged_opts.r,
sketch:{
x:merged_opts.density*merged_opts.sketch,
y:merged_opts.density*merged_opts.sketch
}
});
y_dist -= merged_opts.density;
}
return svg;
};
/**
* draws a rectangle
* no outline just the fill
* @param {object} opts - object containing the attributes
* @param {float|int} opts.x - x position
* @param {float|int} opts.y - y position
* @param {float|int} opts.width - width
* @param {float|int} opts.height - height
* @param {float|int} opts.angle - angle of the lines (0-360)
* @param {float|int} opts.density - distance between lines
* @param {float|int} opts.sketch - sketchiness factor
* @return {object} svg - d3.selection of a group object, containing the rectangle
*/
sketchy.rectFill = function(opts){
var svg = opts.svg.append("g").attr("transform", "translate("+opts.x+" "+opts.y+")");
opts.svg = svg;
return sketchy.drawPattern(opts);
};
/**
* draws a background pattern in the shape of a square according to x,y,with,height
* @param {object} opts - object containing the attributes
* @param {float|int} opts.x - x position
* @param {float|int} opts.y - y position
* @param {float|int} opts.width - width
* @param {float|int} opts.height - height
* @param {float|int} opts.angle - angle of the lines (0-360)
* @param {float|int} opts.density - distance between lines
* @param {float|int} opts.sketch - sketchiness factor
* @return {object} svg - d3.selection of a group object, containing the background
*/
sketchy.drawPattern = function(opts){
var svg = opts.svg;
//angle for strokes
var angle = opts.angle;
while(angle > 360){angle -= 360;}
if(angle > 180){angle -= 180;}
var radian = (Math.PI/180)*(90-angle);
var vector = {
y:1,
x:-1/Math.tan(radian)
};
//distance between strokes
var dist = opts.density;
var vy, tx, ty, vx, y1, x1, y_dist, x_dist;
var x = opts.x, y = opts.y;
if(Math.abs(angle) === 90){
while(y < opts.y+opts.height){
sketchy.drawLine({
svg:svg,
x1:x,
y1:y,
x2:x+opts.width,
y2:y,
sketch:{
x:dist*opts.sketch,
y:dist*opts.sketch
}
});
y += dist;
}
}else if((Math.abs(angle) === 180)||(angle === 0)){
while(x < opts.x+opts.width){
sketchy.drawLine({
svg:svg,
x1:x,
y1:y,
x2:x,
y2:y+opts.height,
sketch:{
x:dist*opts.sketch,
y:dist*opts.sketch
}
});
x += dist;
}
}else if(angle < 90){
y_dist = Math.abs(dist / Math.sin(Math.PI/180*angle));
x_dist = Math.abs(dist / Math.sin(Math.PI/180*(90-angle)));
y += y_dist;
y1 = opts.y;
x1 = opts.x;
while(y1 < opts.y+opts.height){
vx = opts.width / vector.x;
x1 = opts.width + x;
y1 = y + vector.y * vx;
ty = y;
tx = x;
if(y1<opts.y){
vy = (y-opts.y)/vector.y;
x1 = x + Math.abs(vector.x) * vy;
y1 = opts.y;
}else if(y > (opts.y+opts.height)){
ty = opts.y+opts.height;
vy = (ty-y1)/vector.y;
tx = x + opts.width - vy*Math.abs(vector.x);
}
sketchy.drawLine({
svg:svg,
x1:tx,
y1:ty,
x2:x1,
y2:y1,
sketch:{
x:x_dist*opts.sketch,
y:y_dist*opts.sketch
}
});
y += y_dist;
}
}else{
y_dist = Math.abs(dist / Math.sin(Math.PI/180*angle));
x_dist = Math.abs(dist / Math.sin(Math.PI/180*(180-angle)));
y = opts.y+opts.height;
y -= y_dist;
y1 = opts.y+opts.height;
x1 = opts.x;
while(y1 > opts.y){
vx = opts.width / vector.x;
x1 = opts.width + x;
y1 = y + vector.y * vx;
ty = y;
tx = x;
if(y1>(opts.y+opts.height)){
vy = (y-(opts.y+opts.height))/vector.y;
x1 = x + Math.abs(vector.x * vy);
y1 = opts.y+opts.height;
}else if(y < opts.y){
ty = opts.y;
vy = (ty-y1)/vector.y;
tx = x + opts.width - Math.abs(vy*vector.x);
}
sketchy.drawLine({
svg:svg,
x1:tx,
y1:ty,
x2:x1,
y2:y1,
sketch:{
x:x_dist*opts.sketch,
y:y_dist*opts.sketch
}
});
y -= y_dist;
}
}
return svg;
};
/**
* draws a background pattern in the shape of a square according to the position and size of the clip-path object
* @param {object} opts - object containing the attributes
* @param {string} opts.clip - id of the clip path
* @param {float|int} opts.angle - angle of the lines (0-360)
* @param {float|int} opts.density - distance between lines
* @param {float|int} opts.sketch - sketchiness factor
* @param {float|int} opts.margin - extra margin for the background
* @return {object} svg - d3.selection of a group object, containing the background
*/
sketchy.fill = function(opts){
var merged_opts = extend(defaults, opts);
var svg = merged_opts.svg.append("g")
.attr("clip-path", "url(#"+merged_opts.clip+")");
//Get the bounding box of the object that wants a background
var bb = d3.select("path."+merged_opts.clip).node().getBoundingClientRect();
//To make sure that the background covers the whole are we increase the background by a few pixels
merged_opts.x = bb.left-merged_opts.margin;
merged_opts.y = bb.top-merged_opts.margin;
merged_opts.width = bb.width + 2*merged_opts.margin;
merged_opts.height = bb.height + 2*merged_opts.margin;
merged_opts.svg = svg;
return sketchy.drawPattern(merged_opts);
};
/**
* draws a background pattern in the shape of a square according to the position and size of the clip-path object
* @param {object} opts - object containing the attributes
* @param {array} opts.path - array of points {x:float|integer, y:float|integer}
* @param {int} opts.count - how many altered paths should be generated
* @param {float|int} opts.sketch - sketchiness factor
* @return {array} paths - altered paths
*/
sketchy.alterPath = function(opts){
var merged_opts = extend(defaults, opts);
var paths = [];
for(var i = 0; i<merged_opts.count; i++){
var t_path = [];
for(var j = 0; j<merged_opts.path.length; j++){
t_path.push({
x:merged_opts.path[j].x + rand(-1,1)*merged_opts.sketch/(i+1),
y:merged_opts.path[j].y + rand(-1,1)*merged_opts.sketch/(i+1)
});
}
paths.push(t_path);
}
return paths;
};
/**
* Draws alterPath() paths
* only straight lines, use alterPath and your own drawing function to draw curves etc.
* @param {object} opts - object containing the attributes
* @param {array} opts.svg - d3.selection of an svg
* @param {array} opts.path - array of points {x:float|integer, y:float|integer}
* @param {int} opts.count - how many altered paths should be generated
* @param {float|int} opts.sketch - sketchiness factor
* @return {object} svg - svg with the strokes
*/
sketchy.pathStroke = function(opts){
var paths = sketchy.alterPath(opts);
var svg = opts.svg.append("g");
for(var i = 0; i<paths.length; i++){
for(var j = 0; j<paths[i].length; j++){
var x1 = paths[i][j].x;
var y1 = paths[i][j].y, x2, y2;
if(j<(paths[i].length-1)){
x2 = paths[i][j+1].x;
y2 = paths[i][j+1].y;
}else{
x2 = paths[i][0].x;
y2 = paths[i][0].y;
}
sketchy.drawLine({
svg:svg,
x1:x1,
y1:y1,
x2:x2,
y2:y2,
sketch:{
x:opts.sketch,
y:opts.sketch
}
});
}
}
return svg;
};
/**
* Helper function for circleStroke
* Adapted from http://codepen.io/spencerthayer/pen/nhjwu
* Generates an altered circle path
* @param {float|int} radius - radius of the circle
* @param {float|int} radius_min - alternating radius min
* @param {float|int} radius_max - alternating radius max
* @param {float|int} s_angle_min - alternating angle min
* @param {float|int} s_angle_max - alternating angle max
* @param {float|int} rotation_min - alternating rotation min
* @param {float|int} rotation_max - alternating rotation max
* @return {string} path - altered circle svg path
*/
function circlePath(radius, radius_min,radius_max, s_angle_min, s_angle_max, rotation_min,rotation_max) {
var c = 0.551915024494,
b = Math.atan(c),
d = Math.sqrt(c*c+1*1),
r = radius,
o = rand(s_angle_min, s_angle_max)*Math.PI/180,
path = 'M';
path += [r * Math.sin(o), r * Math.cos(o)];
path += ' C' + [d * r * Math.sin(o + b), d * r * Math.cos(o + b)];
for (var i=0; i<4; i++) {
o += Math.PI/2 * (1 + rand(rotation_min, rotation_max));
r *= (1 + rand(radius_min, radius_max));
path += ' ' + (i?'S':'') + [d * r * Math.sin(o - b), d * r * Math.cos(o - b)];
path += ' ' + [r * Math.sin(o), r * Math.cos(o)];
}
return path;
}
/**
* Helper function for circleStroke
* Adapted from http://codepen.io/spencerthayer/pen/nhjwu
* Generates the transform value for squashing and rotating
* @param {float|int} squash_min - squashing min
* @param {float|int} squash_max - squashing max
* @param {float|int} squash_rotation_min - squashing rotation min
* @param {float|int} squash_rotation_max - squashing rotation max
* @return {string} path - transform string
*/
function circleTransform(squash_min, squash_max, squash_rotation_min, squash_rotation_max) {
var o = rand(squash_rotation_min, squash_rotation_max);
return 'rotate('+o+')'+'scale(1,'+rand(squash_min, squash_max) + ')';
}
/**
* Draw a sketch circle stroke
* Adapted from http://codepen.io/spencerthayer/pen/nhjwu
* @param {object} opts - object containing the attributes
* @param {object} opts.svg - svg container
* @param {float|int} opts.x - center x of circle
* @param {float|int} opts.y - center y of circle
* @param {float|int} opts.r - radius of circle
* @param {int} count - number of strokes
* @param {float|int} sketch - sketchiness factor
* @return {object} svg - d3.selection of the svg containing the circles
*/
sketchy.circleStroke = function(opts){
var merged_opts = extend(defaults, opts);
var svg = merged_opts.svg.append("g").attr('transform', function() { return "translate("+merged_opts.x+" "+merged_opts.y+") "+circleTransform(1,1, 0,360); });
for(var i = 0; i<merged_opts.count; i++){
svg.append('path')
.attr('d', function() {
return circlePath(merged_opts.r, merged_opts.sketch/-50/(i+1),merged_opts.sketch/10/(i+1), 200,240, 0,merged_opts.sketch/5/(i+1));
});
}
return svg;
};
/**
* Draw a sketch rectangle stroke
* @param {object} opts - object containing the attributes
* @param {object} opts.svg - svg container
* @param {float|int} opts.x - x coordinate
* @param {float|int} opts.y - y coordinate
* @param {float|int} opts.width - width
* @param {float|int} opts.height - height
* @param {int} count - number of strokes
* @param {float|int} sketch - sketchiness factor
* @return {object} svg - d3.selection of the svg containing the rectangles
*/
sketchy.rectStroke = function(opts){
var merged_opts = extend(defaults, opts);
var svg = merged_opts.svg.append("g");
var path = [
{x:merged_opts.x, y:merged_opts.y},
{x:merged_opts.x+merged_opts.width, y:merged_opts.y},
{x:merged_opts.x+merged_opts.width, y:merged_opts.y+merged_opts.height},
{x:merged_opts.x, y:merged_opts.y+merged_opts.height}
];
return sketchy.pathStroke({svg:svg, path:path, count:merged_opts.count, sketch:merged_opts.sketch});
};
return sketchy;
};
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
<!DOCTYPE html>
<meta charset="utf-8">
<style type="text/css">
body,html{
margin:0;
padding:0;
}
path{
fill:none;
stroke:#000;
stroke-width:0.3;
stroke-linecap:round;
stroke-linejoin:round;
}
</style>
<body>
<script src="http://d3js.org/d3.v4.js" charset="utf-8"></script>
<script src="d3.sketchy.js"></script>
<script type="text/javascript">
const sqrt = Math.sqrt;
const abs = Math.abs;
const pi = Math.PI;
const width = 600;
const height = 500;
const svg = d3.select('body').append('svg')
.attr('width', width).attr('height', height);
const defs = svg.append('defs');
const g_layer = svg.append('g');
const g_symbol = svg.append('g');
const sketchlayer = svg.append('g');
const sketchy = d3sketchy();
const color_map = new Map();
const PropSizer = function (fixed_value, fixed_size, type_symbol) {
this.fixed_value = fixed_value;
if (type_symbol === 'circle') {
this.smax = fixed_size * fixed_size * pi;
this.scale = val => sqrt(abs(val) * this.smax / this.fixed_value) / pi;
this.get_value = size => ((size * pi) ** 2) / this.smax * this.fixed_value;
} else if (type_symbol === 'rect') {
this.smax = fixed_size * fixed_size;
this.scale = val => sqrt(abs(val) * this.smax / this.fixed_value);
this.get_value = size => (size ** 2) / this.smax * this.fixed_value;
}
};
let proj = d3.geoAzimuthalEqualArea()
.rotate([-6, -48, 0])
.precision(0.1);
let communes, path;
let line = d3.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; });
const req = new XMLHttpRequest();
req.open('GET', 'GrandParis.geojson', true);
req.onload = () => { prepare(JSON.parse(req.responseText)) };
req.send();
function prepare(communes) {
proj.fitExtent([[20, 20], [width, height]], communes);
path = d3.geoPath().projection(proj);
let values_stock = [];
communes.features.forEach((d) => {
values_stock.push(d.properties.INC);
if (!color_map.has(d.properties.EPT)) {
color_map.set(d.properties.EPT, [randomColor(), 1 + Math.random() * 359]);
}
});
let max_value = d3.max(values_stock);
let prop_size = new PropSizer(max_value, 30, 'circle');
makeFilter(defs.node(), 'filter1');
let elems = g_layer.attr('filter', 'url(#filter1)').selectAll('path')
.data(communes.features)
.enter()
.append('path')
.attr('d', path)
.style('stroke', 'darkgray')
.style('stroke-width', 1.75)
.style('fill', 'lightgray')
.style('fill-opacity', 0.9);
elems.each(function (d, i) {
const [cx, cy] = path.centroid(d.geometry);
const r = prop_size.scale(d.properties.INC);
const [color, angle] = color_map.get(d.properties.EPT);
g_symbol.append('circle')
.attr('cx', cx)
.attr('cy', cy)
.attr('r', r)
.style('fill', color)
.style('stroke', 'brown');
const s = sketchy.circleFill({
x: cx, y: cy, r: r, svg: sketchlayer, angle: angle,
});
});
}
const makeFilter = (defs_node, id = 'blur_filter', d = 5, stdDev = 10) => {
defs_node.innerHTML = `<filter id="${id}" width="200%" height="200%">
<feOffset result="offOut" in="SourceAlpha" dx="${d}" dy="${d}"></feOffset>
<feGaussianBlur result="blurOut" in="offOut" stdDeviation="${stdDev}"></feGaussianBlur>
<feBlend in="SourceGraphic" in2="blurOut" mode="normal"></feBlend>
</filter>`;
};
// From https://gist.github.com/jdarling/06019d16cb5fd6795edf
// itself adapted from http://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/
const randomColor = (function () {
const golden_ratio_conjugate = 0.618033988749895;
let _h = Math.random();
const hslToRgb = function (h, s, l) {
let r,g,b;
if (s === 0) r = g = b = l;
else {
function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
}
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return `#${Math.round(r * 255).toString(16)}${Math.round(g * 255).toString(16)}${Math.round(b * 255).toString(16)}`;
};
return function () {
_h += golden_ratio_conjugate;
_h %= 1;
return hslToRgb(_h, 0.5, 0.60);
};
}());
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment