Public
Edited
Nov 7, 2023
1 fork
Importers
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof cellStroke = Inputs.color({label: "Cell color", value: "#1f303d"})
Insert cell
viz.update(backgroundColor, lineColor, cellColor, cellStroke)
Insert cell
viz = {
const w = width
const h = 300
const svg = d3.select(DOM.svg(w, h));


const svgRect = svg.append("rect")
.attr("width", w)
.attr("height", h)
// .attr("fill", backgroundColor);


svg
.append("path")
.attr("id", "hello")
.attr("d", testPathAbs)
// .attr("stroke", lineColor)
.attr("fill", "none");

// const centroid = getPathCentroid("hello")
const centroid = d3.polygonCentroid(fishCoords)

svg.append("circle")
.attr("cx", centroid[0])
.attr("cy", centroid[1])
.attr("r", 4)
.attr("fill", "none")


const simulation = d3.forceSimulation(data)
.alphaMin(0.01)
.force('surface', function(){
// .force('surface', function(alpha){
data.map(function(d){
// 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(){colideCell()})
// .force('colidecell', function(){
// // cells.each(function(d){
// data.map(function(d){
// var p1 = d.r + 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) + buffer) * t.sin) , (d.y + ((d.r * t.length) + buffer) * t.cos)];
// // return [(d.x + d.r * t.length * t.sin), (d.y + d.r * t.length * 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
// // cells.each(function(d,i){
// data.map(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) + buffer) * t.sin), (d.y + ((d.r * t.length) + buffer) * t.cos)];

// var collisions = 0;
// //If the feeler overlaps neighbouring polygon
// if (d3.polygonContains(p.polygon, txy)) {
// collisions ++;
// stress++;
// //Bring feeler in a little bit
// // t.length /= 1.05;
// t.length /= 1.08;
// //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);
// // f = 0.1
// d.vx += f * Math.atan((d.x - p.x) * tens);
// d.vy += f * Math.atan((d.y - p.y) * tens);
// p.vx -= f * Math.atan((d.x - p.x) * tens);
// p.vy -= f * Math.atan((d.y - p.y) * tens);
// // console.log([d.vx,d.vy,p.vx,p.vy])
// }
// })
// })
// })
// })
.force("tension", function () {
cells.each(function (d) {
var l = d.children.length;
d.children.forEach(function (t, i) {
var m = d.children[(l - 1) % l].length + d.children[(l + 1) % l].length;
var f = 1 / 8; // spiky-ness
// var f = 1 / 10; // spiky-ness
t.length = (1 - f) * t.length + f * m / 2;
});
})
})
.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);
d.children.forEach(function (t) {
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(0.1).y(d => d.y - 10))
.force('shrink', function(){
const heights = data.map(d => d.y);
const upperQ = d3.quantile(heights.sort(d3.ascending), 0.65);
const lowerQ = d3.quantile(heights.sort(d3.ascending), 0.25);
cells.each(function(d){
if (d.y > upperQ | d.y < lowerQ) {d.color = 'green'} else {d.color = 'blue'};
d.children.forEach(function (t) {
if (d.y > upperQ | d.y < lowerQ) {t.length *= 0.75};
})

})

})
// .force('move', function(){
// cells.each(function(d){
// d.x += Math.random()-0.5;
// d.y += Math.random()-0.5;
// })
// })
.force('boundary', function(){
cells.each(function(d){
const pointCoords = [d.x, d.y]
if (!geometric.pointInPolygon(pointCoords, fishCoords)) {
d.color = 'red';

// Calculate the distance from the cell to the polygon's centroid
const distX = centroid[0] - d.x;
const distY = centroid[1] - d.y;
const distance = Math.sqrt(distX * distX + distY * distY);

// Calculate the force to pull the node back towards the centroid
const strength = 0.5; // Adjust the strength as needed
const forceX = (distX / distance) * strength;
const forceY = (distY / distance) * strength;

// Apply the force to the node
d.vx += forceX
d.vy += forceY
}
})
})

// Clip path-----------------------------------------------

const clipPath = svg
.append("defs")
.append("clipPath")
.attr("id", "clip-path-id");

clipPath
.append("path")
.attr("d", testPathAbs); // Replace with your SVG path data

// //Plot elements--------------------------------------------------
let cells = svg.append("g").attr("id","cellGroup")
// .attr('transform', 'translate(100,20)')
.selectAll('g.cell')
.data(simulation.nodes())
.enter()
.append('g')
.classed('cell', true)
.attr('transform', function (d) {
return 'translate(' + [d.x, d.y] + ')'
})
// .attr("opacity",0.5);

d3.select("#cellGroup").attr("clip-path", "url(#clip-path-id)");

let paths = cells
.append('path')
// .attr("fill", cellColor)
// .attr('fill', function (d,i){
// return d.color
// })
// .attr("opacity",0.6)
.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);
})
.attr("stroke","black")
.attr("stroke-width",strokeWidth);

//plot feelers
// 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) + buffer) * t.sin)})
// .attr("cy",function(t){return (((d.r * t.length) + buffer) * t.cos)})
// .attr("r",1)
// .attr('fill', d.color)
// .attr("opacity",1)
// });

//Simulation iteration updates------------------------------------------

let tickcount = 0
simulation.on("tick",function(d) {
tickcount++
cells.attr('transform', function (d) {
return 'translate(' + [d.x, d.y] + ')'
});

// paths.attr('fill', function (d) {
// return d.color
// });

d3.select("#cellGroup").attr("clip-path", "url(#clip-path-id)");

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);
});

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

});



// return svg.node();
return Object.assign(svg.node(), {
update(backgroundCol, lineCol, cellCol, cellStroke) {
svgRect.attr("fill", backgroundCol);
d3.select("#hello").attr("stroke", lineCol);
paths.attr("fill", cellCol);
paths.attr("stroke", cellStroke);
}
})
}




Insert cell
colideCell = function(){
data.map(function(d){
var p1 = d.r + 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) + buffer) * t.sin) , (d.y + ((d.r * t.length) + buffer) * t.cos)];
// return [(d.x + d.r * t.length * t.sin), (d.y + d.r * t.length * 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.map(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) + buffer) * t.sin), (d.y + ((d.r * t.length) + buffer) * t.cos)];

var collisions = 0;
//If the feeler overlaps neighbouring polygon
if (d3.polygonContains(p.polygon, txy)) {
collisions ++;
stress++;
//Bring feeler in a little bit
// t.length /= 1.05;
t.length /= 1.08;
//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);
// f = 0.1
d.vx += f * Math.atan((d.x - p.x) * tens);
d.vy += f * Math.atan((d.y - p.y) * tens);
p.vx -= f * Math.atan((d.x - p.x) * tens);
p.vy -= f * Math.atan((d.y - p.y) * tens);
// console.log([d.vx,d.vy,p.vx,p.vy])
}
})
})
})
};
Insert cell
Insert cell
// earcut = require("https://unpkg.com/earcut@2.1.1/dist/earcut.min.js")
Insert cell
fishCoords = {
const path = d3.select(starSvg).select("#myPath")
const pathData = path.attr("d");
const points = parsePathData(pathData);
return points
}
Insert cell
triangulatedTrout = {
const svg = d3.select(DOM.svg(500, 400))

const { coords, vertices } = polygon_to_triangles([fishCoords])
const triangles = [];

for (let i = 0; i < vertices.length; i += 3){
const a = coords.slice(vertices[i]*2, vertices[i]*2+2);
const b = coords.slice(vertices[i+1]*2, vertices[i+1]*2+2);
const c = coords.slice(vertices[i+2]*2, vertices[i+2]*2+2);
const area = Math.abs(
a[0] * (b[1] - c[1]) + b[0] * (c[1] - a[1]) + c[0] * (a[1] - b[1])
) / 2
triangles.push({a,b,c, area})
};

let totalArea = d3.sum(triangles.map(d => d.area))

// return triangles

const triangleArray =
triangles.map(function(d){
let triangleTriplet = [];
for (var key in d) {
var obj = d[key];
if (key == 'area') continue;
triangleTriplet.push({
"x": obj[0],
"y": obj[1]
})
}
return triangleTriplet
})



drawTriangles(triangleArray, svg, "test");

const pointCount = 230;

const randomPoints = _(triangles).flatMap(({a, b, c, area}) => {
const points = Math.round(pointCount * area/totalArea)
return d3.range(points).map(() => {
const point = random_point(a, b, c);
return {x: point[0], y: point[1]};})
// return {x: 0, y: 0};})
}).value()

drawPoints(randomPoints, svg, 3.2, 'points');


return {"chart":svg.node(), "data":randomPoints}


}
Insert cell
line = d3.radialLine().curve(d3.curveCatmullRomClosed)
Insert cell
strokeWidth = 1
Insert cell
buffer = 1 + strokeWidth
Insert cell
triangulatedTrout["data"]
Insert cell
data = triangulatedTrout["data"]
.map(function (p,i) {
var a = 2 * Math.PI * Math.random(), //A random point along a circle
d = Math.sqrt(Math.random()); //Distribution Factor
return {
id: i,
// r: 2 + (4 * Math.random()),
// r: Math.min(2.5 + (2 * Math.random()) + (2 * Math.random()),5),
r: 2.5 + (2 * Math.random()) + (2 * Math.random()),
x: p.x,
y: p.y,
color: 'blue',
};
})
//Add n feelers
.map(function (d) {
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;
});
Insert cell
const randomPoints = _(triangles).flatMap(({a, b, c, area}) => {
const points = Math.round(pointCount * area / totalArea);
return d3.range(points).map(() => {
// Generate a random point within the triangle and return it as an object
const point = random_point(a, b, c);
return { x: point[0], y: point[1] };
});
}).value();
Insert cell
function drawPoints(points, selector, r, className) {
selector.selectAll(`.${className}`)
.data(points)
.join(enter => enter
.append('circle')
.attr("class", className)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", r)
.attr("fill", d => d.color || "grey")
.attr("stroke", "white")
.attr("stroke-width", 1)
.style("opacity", d => d.opacity || 1)
)
}
Insert cell
function random_point([ax, ay], [bx, by], [cx, cy]) {
const a = [bx - ax, by - ay]
const b = [cx - ax, cy - ay]
let [u1, u2] = [Math.random(), Math.random()]
if (u1 + u2 > 1) {u1 = 1 - u1; u2 = 1 - u2}
const w = [u1 * a[0] + u2 * b[0], u1 * a[1] + u2 * b[1]]
return [w[0] + ax, w[1] + ay]
}
Insert cell
function randround(how_many_points_do_i_get) {
const leftover = how_many_points_do_i_get % 1;
// Random round to decide if you get a fractional point.
if (Math.random() > leftover) {
how_many_points_do_i_get -= leftover
} else {
how_many_points_do_i_get += (1 - leftover)
}
return how_many_points_do_i_get
}
Insert cell
0.1%1
Insert cell
import { earcut, polygon_to_triangles } from '@bmschmidt/a-binary-file-format-for-projected-triangulated-shapefile'
Insert cell
function drawTriangles(triangleCoords, selector, className) {
selector.selectAll(`.${className}`)
.data(triangleCoords)
.join(enter => enter
.append('path')
.attr("class", className)
.attr("d", function(d){
var coords = [d[0], d[1], d[2]];
console.log(coords)
return d3.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.curve(d3.curveLinear)(coords);
})
.attr("stroke", "black")
.attr("fill", function(d, i) {
return i % 2 === 0 ? "yellow" : "lightblue"; // alternate fill colors
})
)
}
Insert cell
Insert cell
fish ={
const width=1500, height=400;
const svg = d3.select(DOM.svg(width, height))

const initialPath = svg.append("path")
.attr("id", "hello")
.attr("d", testPath)
// .attr("transform", "translate(50, 50)")
.attr("stroke", "blue")

return svg.node()
}
Insert cell
{
const width=1500, height=400;
const svg = d3.select(DOM.svg(width, height))

const initialPath = svg.append("path")
.attr("d", testPathAbs)
// .attr("transform", "translate(50, 50)")
.attr("stroke", "blue");

return svg.node()
}
Insert cell
testPath
Insert cell
testPathAbs = "M72.098957,146.14026C72.098957,146.14026,83.65595065579298,144.7308414271207,91.83041132922082,148.81805983174377C100.00486603660323,152.90533789682115,120.1591399716424,166.4354340300091,129.0382920333736,169.53610716198008C137.9174440951048,172.6367802939511,158.28313081607587,171.7910933539509,158.28313081607587,171.7910933539509C158.28313081607587,171.7910933539509,161.10190233533473,171.93189202614846,158.6354854592957,170.24081644841974C156.16905665116582,168.5495022288737,152.78651770275525,170.24081644841974,151.9408904232094,168.69047988243423C151.09524524552725,167.14014331644873,151.37715879033027,163.05292491182567,156.80328904291608,161.50258834584017C162.22943719363818,159.95225177985466,172.658913889264,166.7877886732289,177.16895786575085,166.7173296766758C181.67899587619226,166.64693034057703,256.49231665395666,156.73703057558734,253.2534102493239,155.34181119083644C250.01450384469115,153.94659180608554,235.7134454905401,154.44487592057453,236.90933929740453,150.80731836016884C238.10523310426896,147.16976079976317,247.2738815545474,140.3929894538951,248.12100034545125,139.44617804382085C248.9681191363551,138.4994262942009,250.811806156237,135.7089875246991,251.90800734393105,135.4598454674546C253.00426819207942,135.21046476839282,254.44930405618837,135.55947842617067,254.44930405618837,135.55947842617067C254.44930405618837,135.55947842617067,253.6022449257388,133.81542436500487,254.6485699736205,132.37038850089596C255.69501434241084,130.925352636787,261.02674982416994,125.79288307246004,261.076566303528,125.09527338008459C261.12429466698484,124.39766368770914,265.3121005975931,118.21880941504615,266.3086688265711,117.86997473863127C267.30523705554907,117.52096108085343,269.54757523120384,117.67070882119913,269.54757523120384,117.67070882119913C269.54757523120384,117.67070882119913,268.30186494498133,113.43517452713407,270.29506106339164,111.99013866302514C272.2881975213476,110.5451027989162,278.9653359084996,114.93008653105537,281.3571831826828,116.47475535388037C283.74897079641164,118.0194838371597,292.3694888226159,124.84613132284011,295.01041849358927,127.13828597785292C297.651407825017,129.43044063286573,312.84937161920277,145.9738073324444,313.9456324673512,146.3226420088593C315.04183365504525,146.67165566663715,354.40711394603596,143.73146915678964,388.6898008125121,151.30554281420353C422.9724280185339,158.87961647161742,445.79434160994595,167.5499509771797,454.16565791845136,170.83873352162482C462.5370338874111,174.12745640561562,472.1188608140896,181.12825275745902,472.5064747858131,185.4973668087488C472.8942677388996,189.8664808600386,471.0970562129338,191.4520767545278,471.0970562129338,191.4520767545278C471.0970562129338,191.4520767545278,474.7967203062876,192.61479934878977,474.40916599501844,194.7288675476544C474.02137304193195,196.84299540697336,471.9779428209834,199.3798772456109,470.2866882618917,200.85975481504332C468.5954337028,202.3395727240214,458.80014167056095,207.1315004150841,451.4008731443076,209.35128693900555C444.0015449575999,211.571073462927,435.8623674768575,213.40330567557922,432.09224438695054,214.10801496201887C428.3221212970436,214.8126645880042,419.44297520135785,215.86972851766367,418.28019294664153,216.0811651677773C417.11747035237954,216.29236317607365,405.73664208610563,219.85122859722992,403.622573887241,220.48547888711647C401.5085056883764,221.11972917700302,393.29874989017225,221.19018817355612,391.46657733797434,222.6348064144848C389.63440478577644,224.0794246554135,375.1529033875315,245.85447625485497,369.23345277025624,246.84108118796135C363.31400215298095,247.8276264606134,365.92140264862593,231.30251586005696,364.26534775758364,229.68172029751835C362.6093525269956,228.06092473497975,336.32420922270387,233.5223023839784,327.4803224555218,233.73373903409203C318.63637602788543,233.94493704238837,309.61625227863914,234.43844832053168,306.44517981056936,234.47364798858106C303.27404768204525,234.50944426117366,297.9183286976491,233.66328003753893,296.7203467748834,234.01545569939577C295.52236485211773,234.36804898443287,290.3076235212821,246.27711091326495,286.396582438269,247.0170795282083C282.48554135525586,247.75698848269735,283.2254503097449,243.6697700780743,281.4989364221495,243.52879242451377C279.77248219500837,243.38799375231622,267.7574036388479,257.6931985080426,261.8379530215726,258.85592110230453C255.91856206475165,260.0187033570208,256.5175530261345,253.5707207749158,256.5175530261345,253.5707207749158C256.5175530261345,253.5707207749158,257.11654398751733,247.75698848269732,258.49070323189295,241.87279719392575C259.86480281581424,235.98860590515417,262.08458933973566,230.98524156397784,260.21715745903407,230.1748736129357C258.3497255783325,229.36444600143923,217.5831107060229,223.26881806255403,213.10833799013062,225.48860458647547C208.63350561378402,227.70845077085124,169.73437035053956,249.97677500661874,164.13204487820752,249.1311477270729C158.52973133796638,248.28552044752703,153.8435100398696,232.74695511962264,153.91397500246816,229.25872767638242C153.98437433856694,225.7705002331422,153.56162035924834,212.59269908271978,154.72437278373746,212.24034443949995C155.88711924218117,211.88775115446285,161.7008455683542,211.95874709510483,161.8770228899641,210.72526720201816C162.05320021157402,209.4920856112031,145.6338079360744,207.83603072016078,142.9559663420126,207.97694871326698C140.27813668004168,208.11774738546453,121.11043784788173,209.4920856112031,109.02492415934988,215.9048088648044C96.93941047081803,222.31753211840572,85.80523642084341,223.23361839450467,81.71800011808406,223.44499538416397C77.63077574741557,223.65619339246032,67.23651661797543,225.62958224003603,67.02512173017983,223.65619339246032C66.81368508006621,221.6830431867019,70.44287824477608,204.6294006213157,71.46470074406815,204.3123053065996C72.48650534522392,203.99491168961188,74.565344045812,200.08416890887034,74.49487908321346,198.81572798955156C74.42447974711469,197.54722740977846,74.91769272298639,195.25698188930392,75.34050636275931,194.44661393826178C75.76332000253224,193.6361863267653,77.06700831826386,189.97178156191518,76.78513653577886,189.40799026858173C76.50324088911213,188.84419897524828,76.00997421883154,183.41814031520767,75.34050636275931,182.2906173889951C74.67106237086881,181.1630944627825,72.41602248448909,176.0540714570037,72.34555752189056,175.0674665238973C72.27515818579178,174.08092125124526,71.85228488556453,170.66315280455814,71.85228488556453,169.78226619650857C71.85228488556453,168.901379588459,69.45631507398774,147.51394196074108,72.09892120372754,146.1397827163655Z"
Insert cell
import {testPath} from "7ade30630079ae0f"
Insert cell
testPath.match(/[a-df-z][^a-df-z]*/gi)[1].slice(1).trim().split(/\s*,\s*|\s+/).map(parseFloat)
Insert cell
geometric = require("geometric@2");
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