Published
Edited
Mar 21, 2022
1 star
Insert cell
md`# Stressed Trout - Canvas`
Insert cell
parent_view = {
const parent = DOM.element('div')
d3.select(parent).attr("id","container")
return parent;
}
Insert cell
data = generateData();

Insert cell
html
`<style>

#canvas {
background:
linear-gradient(
rgba(45,35,25,1) 0%,
rgba(83,69,66,1) 7%,
rgba(119,101,84,1) 23%,
rgba(219,191,117,1) 72%,
rgba(246,246,246,1) 100%);
}

#container {
background-image: url(data:image/png;base64,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);
background-color: transparent;
}
</style>`
Insert cell
canvas = {
return d3.select(parent_view).append("canvas")
.attr("id","canvas")
.attr("width", width)
.attr("height", height)
}
Insert cell
canvasOverlay = {
return d3.select(parent_view).insert("div")
.attr("id", "canvasOverlay")
.attr("width", width)
.attr("height", height)
}
Insert cell
context = canvas.node().getContext('2d');
Insert cell
simulation = {
d3.forceSimulation(data)
.alphaMin(0.05)
.force('surface', surface)
.force('collideCell', collide)
.force('tension', tension)
.force('expand', expand)
.force('y', forceY)
.force('move', move)
.on("tick",tick)
}
Insert cell
forceY = d3.forceY().strength(
d => d.band == 3 ? Math.sqrt(Math.random())/50 :
d.band == 2 ? Math.log(Math.random()+1)/40 :
(Math.random()/40) + 0.01)
.y(5)
Insert cell
surface = function() {
data.forEach(function(d){
d.surface = Math.sqrt(
d.children.map(function(t){
return t.length * t.length;
})
.reduce(function(a,b){
return a + b;
}, 0) / d.children.length);
})
}
Insert cell
collide = function(){
data.forEach(function(d){
var p1 = d.r + d.buffer; //buffer for the current polygon
//Co-ordinates of current polygon points
d.polygon = d.children.map(function(t){
return [(d.x + ((d.r * t.length) + d.buffer) * t.sin) , (d.y + ((d.r * t.length) + d.buffer) * t.cos)];
});
});
var quadtree = d3.quadtree(data, d=>d.x, d=>d.y);
var collisions = 0;
//Loop through the cells, and shrink the polygon where it overlaps with neighbours
data.forEach(function(d,i){
quadtree.visit(function(node, x0, y0, x1, y1){

var p = node.data;
//Return quadtree nodes that contain any data.

if (!p) return;

//remove the current node
if (p.id == d.id) return;

//Calculate distance between current node and all others
var dx = p.x - d.x,
dy = p.y - d.y,
dist2 = dx*dx + dy*dy;

//Only keep nodes within a reasonable distance of current node
if (dist2 > 4 * (d.r + p.r) * (d.r + p.r)) return;

var stress = 0;


//Polygon co-ordinates for current cell polygon
d.children.forEach(function(t){
//Co-ordinates for a single feeler
var txy = [(d.x + ((d.r * t.length) + d.buffer) * t.sin), (d.y + ((d.r * t.length) + d.buffer) * t.cos)];

var collisions = 0;
//If the feeler overlaps neighbouring polygon
if (d3.polygonContains(p.polygon, txy)) {
d.overlap = 'Y'
collisions ++;
stress++;
//Bring feeler in a little bit
t.length /= 1.05;

// var tens = d.surface / p.surface,
var f = 0.1 * (stress > 2 ? 1 : 0.5);
d.vx += f * ((d.x - p.x) > 1? 1 : -1);
d.vy += f * ((d.y - p.y) > 1? 1 : -1);
p.vx += f * ((p.x - d.x) > 1? 1 : -1);
p.vy += f * ((p.y - d.y) > 1? 1 : -1);
} else {
d.overlap = 'N'
}
})
})
})
}
Insert cell
tension = function() {
data.forEach(function (d) {
var l = d.children.length;
d.children.forEach(function (t, i) {
var m = d.children[l-1].length + d.children[1].length //last feeler length + 1st feeler length
var f = 0.1; // spiky-ness
t.length = t.length < d.surface && d.overlap == "Y" ? t.length * 1.02 : t.length;
});
})
}
Insert cell
expand = function(){
data.forEach(function(d){
// var u = 1 / Math.sqrt(d.surface); //multiplication factor to edge back to a t.length of 1
var u = 1 / d.surface; //multiplication factor to edge back to a t.length of 1
d.children.forEach(function (t) {
t.length *= u * 1.05;
// t.length *= u;
t.length = Math.min(t.length, 1.1);
})
})

}
Insert cell
move = function() {
data.forEach(function(d){
d.x += (Math.random()-0.5)/3;
d.y += (Math.random()-0.5)/3;
})
}
Insert cell
pathCoords = function(d){
let arc = 2 * Math.PI / d.children.length,
data = d.children.map(function(t,i) {
return [i * arc, d.r * t.length];
});
return data;
}
Insert cell
haloCoords = function(d){
let arc = 2 * Math.PI / d.children.length,
data = d.children.map(function(t,i) {
return [i * arc, d.r * t.length + 5];
});
return data;
}
Insert cell
tick = function(){
drawHalos();
drawSpots();
shadowGradient();
}
Insert cell
gradient = function() {
var gradient = context.createLinearGradient(width/2,0, width/2,height);

// Add three color stops
gradient.addColorStop(0, "#2d2319");
gradient.addColorStop(0.07, "#534542");
gradient.addColorStop(0.23, "#776554");
gradient.addColorStop(0.72, "#dbbf75");
gradient.addColorStop(1, "#f6f6f6");

// Set the fill style and draw a rectangle
context.fillStyle = gradient;
context.fillRect(0, 0, width, height);
}
Insert cell
shadowGradient = function() {
var gradient = context.createLinearGradient(width/2,0, width/2,height);

// Add three color stops
gradient.addColorStop(0, "#2d2319");
// gradient.addColorStop(0.1, "#96918C");
gradient.addColorStop(0.3, "transparent");

// Set the fill style and draw a rectangle
context.fillStyle = gradient
context.fillRect(0, 0, width, height);
}
Insert cell
drawHalos = function() {
context.clearRect(0, 0, width, height);
// gradient();
data.forEach(function(d,i){
context.save();
context.translate(d.x,d.y)
context.beginPath();
d3.lineRadial().curve(d3.curveCatmullRomClosed).context(context)(haloCoords(d));
context.filter = 'blur(1px)'
context.fillStyle = "#e2dbcc";
context.fill()
context.restore();
});
}
Insert cell
drawSpots = function() {
// context.clearRect(0, 0, width, height);
data.forEach(function(d,i){
context.save();
context.translate(d.x,d.y)
context.beginPath();
d3.lineRadial().curve(d3.curveCatmullRomClosed).context(context)(pathCoords(d));
context.fillStyle = d.color;
context.fill();
context.restore();
});
}
Insert cell
custom = {
var customBase = document.createElement("custom")
return d3.select(customBase);
}
Insert cell
width = 800;
Insert cell
height = 250;
Insert cell
dataSize = 200;
Insert cell
bandSizing = function(numPoints, bandRatio) {

let unit = numPoints / d3.sum(bandRatio)
let sizes = bandRatio.map(d => d * unit)

let breakpoints = []

for (let i=0; i < sizes.length; i++){
breakpoints[i] = i == 0? sizes[i] : breakpoints[i-1] + sizes[i]
}
return breakpoints
}
Insert cell
breakPoints = bandSizing(dataSize,[6,3,3]);
Insert cell
redScale = d3.scaleSequentialPow(["#534542","red"]).exponent(4)
Insert cell
redSpots = {
const min = breakPoints[1]
const max = breakPoints[2]
let number = max - min
for (var i=0; i<number-1; i++) {
var index = Math.floor(Math.random() * (max - min) + min) ;
data[index].color = redScale(Math.random())
}
return data
}
Insert cell
Insert cell
function generateData(){
let data = d3.range(dataSize)
.map(function (i) {
var a = 2 * Math.PI * Math.random(), //A random point along a circle
d = Math.sqrt(Math.random()), //Distribution Factor
bandSize = dataSize / 3
return {
band: i >= breakPoints[1] ? 3 : (i > breakPoints[0] ? 2 : 1),
id: i,
x: width * Math.random(),
y: height * Math.sqrt(Math.random()),
color: '#534542',
};
})
//Add n feelers
.map(function (d) {
d.y = d.band == 2 ? height * 0.5 :
d.band == 1 ? height * 0.3 :
height * 0.8
d.r = d.band == 2 ? Math.random()*3 + 7: Math.random()*3 + 4;
d.buffer = (d.r * 0.2) + 1
var n = Math.floor(4 + d.r) //Start at a random point along circle
d.children = d3.range(n)
.map(function (i) {
//Plot points around the radius of a circle
var angle = Math.PI - (i * (Math.PI * 2 / n)),
// var angle = i * (Math.PI * 2 / n),
t = {
length: 1,
angle: angle,
sin: Math.sin(angle),
cos: Math.cos(angle),
parent: d,
};
return t;
});
return d;
});
return data;
}
Insert cell
d3 = require("d3@5", "d3-scale@^3.2.0")
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more