Public
Edited
Oct 11, 2023
Insert cell
md`# Stressed Trout 3`
Insert cell
viewof v1 = html`<input type=checkbox>`
Insert cell
chart = {

var svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);
var defs = svg.append("defs");
let data = generateData();
data = redSpots;

gradientDef(svg, defs);
// spotBlur(defs);
// blur(defs);

svg.append("rect")
.attr("width",width)
.attr("height",height)
.attr("fill", "url(#gradient)")

const simulation = d3.forceSimulation(data)
// .alphaTarget(0.01)
.alphaMin(0.1)
//Q: What does this force do? Does this measure how deformed the polygon is?
.force('surface', function(alpha){
cells.each(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);
})
})
.force('colidecell', function(){
cells.each(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)];
});
});
//Q: Why is quadtree used? Does this speed up identification of overlapping cells?
//Or is it because quadtree functionality allow us to loop through the cells, while accessing
//co-ordinates of all other cells in a single iteration?
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
cells.each(function(d,i){
quadtree.visit(function(node, x0, y0, x1, y1){

var p = node.data;
//Return quadtree nodes that contain any data.
//Q: This confused me a little bit.
//I thought the "!" would return nodes that DIDN'T 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;
//Q: Could you please provide an overview of what this bit does?
//I'm guessing this makes the overlapping polygons jiggle a bit more when
//they overlap?
var tens = d.surface / p.surface,
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'
}
})
})
})
})
// .force('x', d3.forceX().strength(0.015).x(50))
// .force('y', function(){
// cells.each(function(d){
// d3.forceY().strength(0.01).y(10)
// })
// })
// .force('x', d3.forceX().strength(function(d,i){return i < 5? 0.1 : 0.01}).x(100))
.force("tension", function () {
cells.each(function (d) {
var l = d.children.length;
d.children.forEach(function (t, i) {
// console.log([(l-1) % l,(l+1) % l])
// var m = d.children[(l-1) % l].length + d.children[(l+1) % l].length
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 = (1 - f) * t.length + f * (m / 2);
// t.length = t.length < m/2 ? t.length * 1.02 : t.length;
t.length = t.length < d.surface && d.overlap == "Y" ? t.length * 1.02 : t.length;
// t.length = t.length < d.surface ? t.length * 1.01 : t.length;
// t.length = Math.min(t.length, 1.1);
// t.length = t.length * 1.02;
});
})
})
// .force("internal", function (alpha) {
// var f = 1 / 10;
// cells.each(function (d) {
// d.vx += f * d3.sum(d.children, function (t) {
// return t.length * t.sin;
// });
// d.vy += f * d3.sum(d.children, function (t) {
// return -t.length * t.cos;
// });
// })
// })
.force("expand", function () {
cells.each(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);
})
})
})
// .force('x', d3.forceX().strength(0.01).x(50))
// .force('y', d3.forceY().strength(function(d,i){return i < 40 && d.color !== "red" ? 0.03 : 0.005 + Math.random()/25}).y(20))
.force('y', d3.forceY().strength(d => d.band == 3 ? Math.sqrt(Math.random())/30 : d.band == 2 ? Math.log(Math.random()+1)/30: 0.03).y(5))
.force('collide', d3.forceCollide().radius(5).strength(0.2))
.force('move', function(){
cells.each(function(d){
d.x += Math.random()-0.5;
d.y += Math.random()-0.5;
})
})


//Plot elements--------------------------------------------------
let halos = svg.append('g')
.selectAll("path")
.data(simulation.nodes())
.enter()
.append("path")
.attr("class","polygons")
.attr('transform', function (d) {
return 'translate(' + [d.x, d.y] + ')'
})
.attr('fill',"#f6f6f6")
.attr('d', function (d) {
var arc = 2 * Math.PI / d.children.length,//Point spacing along circle
data = d.children
.map(function (t, i) {
return [i * arc, d.r * t.length + 3];
})
return line(data);
})
// .style("filter","url(#blur)")
// .style("filter","url(#glow)")
let cells = svg.append("g")
// .attr('transform', 'translate(0,0)')
.selectAll('g.cell')
.data(simulation.nodes())
.enter()
.append('g')
.classed('cell', true)
.attr('transform', function (d) {
// return 'translate(' + [d.x, d.y] + ')'
return 'translate(' + [d.x, d.y] + ')'
});
let paths = cells
.append('path')
.attr("class","polygons")
.attr("fill",d => d.color)
// .attr('fill', function (d,i){
// return color(i)
// })
.attr("opacity",1)
.attr('d', function (d) {
var arc = 2 * Math.PI / d.children.length,//Point spacing along circle
data = d.children
.map(function (t, i) {
return [i * arc, d.r * t.length];
})
return line(data);
})
// .style("filter","url(#blur)")
// let text = cells.append("text")
// .text(d => Math.round(d.surface * 10)/10)
// .attr("fill","white")
// .attr("font-size",6)


cells.each(function(d,i){
let currentCell = d3.select(this);
let data = d.children;
currentCell.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx",function(t){return (((d.r * t.length) + d.buffer) * t.sin)})
.attr("cy",function(t){return (((d.r * t.length) + d.buffer) * t.cos)})
.attr("r",1)
.attr('fill', color(i))
.attr("opacity",v1 ? 1 : 0)
});



//--------------------------------------------------------------------

//Simulation iteration updates------------------------------------------
let tickcount = 0
simulation.on("tick",function(d) {
tickcount++
cells.attr('transform', function (d) {
return 'translate(' + [d.x, d.y] + ')'
});
halos
.attr('transform', function (d) {
return 'translate(' + [d.x, d.y] + ')'
})
.attr('d', function (d) {
var arc = 2 * Math.PI / d.children.length,//Point spacing along circle
data = d.children.map(function (t, i) {
return [i * arc, d.r * t.length + 3];
});
return line(data);
});

paths.attr('d', function (d) {
var arc = 2 * Math.PI / d.children.length,//Point spacing along circle
data = d.children.map(function (t, i) {
return [i * arc, d.r * t.length];
});
return line(data);
});
// text.text(d => d.surface)

cells.each(function(d){
d3.select(this)
.selectAll("circle")
.attr("cx",function(t){return (((d.r * t.length) + d.buffer) * t.sin)})
.attr("cy",function(t){return (((d.r * t.length) + d.buffer) * t.cos)})
})




})




// })

// })
//--------------------------------------------------
return svg.node()
}
Insert cell
expand = function(cells){
cells.each(function (d) {
var u = 1 / Math.sqrt(d.surface);
d.children.forEach(function (t) {
t.length *= u;
t.length = Math.min(t.length, 1.1);
})
})
}
Insert cell
redScale = d3.scaleSequentialPow(["#534542","red"]).exponent(4)
Insert cell
import {ramp} from "@d3/sequential-scales"
Insert cell
ramp(redScale)
Insert cell
redSpots = {
const min = ratios[1]
const max = ratios[2]
let number = max - min
for (var i=0; i<number; i++) {
var index = Math.floor(Math.random() * (max - min) + min) ;
data[index].color = redScale(Math.random())
}
return data
}
Insert cell
data = generateData()
Insert cell
spotBlur = function(defs){
var filter = defs.append("filter")
.attr("id","blur")
.attr("width","400%")
.attr("height","400%")
.attr("x","-100%")
.attr("y","-100%");
filter.append("feGaussianBlur")
.attr("stdDeviation","0.3")
.attr("result","coloredBlur");
var feMerge = filter.append("feMerge");
feMerge.append("feMergeNode")
.attr("in","coloredBlur");
// feMerge.append("feMergeNode")
// .attr("in","SourceGraphic");
}

Insert cell
blur = function(defs) {
var filter = defs.append("filter")
.attr("id","glow")
.attr("width","400%")
.attr("height","400%")
.attr("x","-100%")
.attr("y","-100%")
filter.append("feMorphology")
.attr("in", "SourceGraphic")
.attr("result", "upperLayer")
.attr("operator", "dilate")
.attr("radius", "3 3");
filter.append("feGaussianBlur")
.attr("in", "SoureGraphic")
.attr("stdDeviation","1 1")
.attr("result","LowerBlur");
filter.append("feMorphology")
.attr("in", "SourceGraphic")
.attr("result", "englargedAlpha")
.attr("operator", "dilate")
.attr("radius", "3 3");
filter.append("feGaussianBlur")
.attr("in", "enlargedAlpha")
.attr("stdDeviation","4 4")
.attr("result","coloredBlur");
// Control opacity of shadow filter
var feMerge = filter.append("feMerge");
// feMerge.append("feMergeNode")
// .attr("in","coloredBlur");
feMerge.append("feMergeNode")
.attr("in","lowerBlur");
feMerge.append("feMergeNode")
.attr("in","SourceGraphic");
}
Insert cell
gradientDef = function(container,defs) {
var gradient = defs
.append('linearGradient')
.attr('x1', '0%')
.attr('x2', '0%')
.attr('y1', '0%')
.attr('y2', '100%')
.attr("id","gradient");
gradient.append("stop")
.attr("offset",0)
.style("stop-color", "#2d2319");
gradient.append("stop")
.attr("offset",0.07)
.style("stop-color", "#534542");
gradient.append("stop")
.attr("offset",0.23)
.style("stop-color", "#776554");

gradient.append("stop")
.attr("offset",0.72)
.style("stop-color", "#dbbf75");

gradient.append("stop")
.attr("offset",1)
.style("stop-color", "#f6f6f6");
}
Insert cell
color = function(i) { return d3.cubehelix((i%100)*3.60, 1.2, 0.6); }
Insert cell
function nodes(quadtree){
var nodes = [];
quadtree.visit(function(node,x0,y0,x1,y1){
node.x0 = x0, node.y0 = y0;
node.x1 = x1, node.y1 = y1;
nodes.push(node);
});
return nodes;
}
Insert cell
width = 500;
Insert cell
height = 150;
Insert cell
radiusSorted = {let radiusArray = [];
let data = generateData();

data.map(function(d,i){
radiusArray.push([i,d.r])
})

radiusArray.sort(function(a,b){return a[1]-b[1]})
return radiusArray
}

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
dataSize = 200;
Insert cell
ratios = bandSizing(dataSize,[6,3,3]);
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 >= ratios[1] ? 3 : (i > ratios[0] ? 2 : 1),
id: i,
// r: 4 * (1 + 5 * Math.random() * Math.random()),
// x: width * Math.cos(a) * d,
x: width * Math.random(),
// y: 200 * Math.sin(a) * d,
y: height * Math.sqrt(Math.random()),
// y: d.r < 7 ? height * 0.3 : height,
color: '#534542',
};
})
//Add n feelers
.map(function (d) {
d.y = d.band == 2 ? height * 0.5 :
d.band == 1 ? height * 0.3 :
height * 0.7
// d.y = d.band == 2 ? height*0.5 + (height - height*0.3) * Math.random() :
// d.band == 1 ? height * 0.3 :
// height * 0.7
// d.r = d.band == 2 ? 2 * (Math.random() * 3 + 1.5) :
// (Math.random() * 2 + 1.5);
d.r = d.band == 2 ? 7 : 4;
d.buffer = (d.r * 0.2) + 1;
var n = Math.floor(20 + 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
line = d3.radialLine()
.curve(d3.curveCatmullRomClosed);
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