D3
OG
Old school D3 from simpler times
All examples
By author
By category
About
bmershon
Full window
Github gist
An Interesting Lattice
<!DOCTYPE html> <meta charset="utf-8"> <title>Reflections</title> <style> body { cursor: crosshair; } .vertex { pointer-events: none; stroke: none; } .vertex.coprime { stroke: #000; stroke-width: 1px; } .vertex.invalid { fill-opacity: 0.1; stroke-opacity: 0.3; } .node { fill: #fff; fill-opacity: 0; stroke-width: 0; } .node.active { stroke-width: 1px; stroke: #ccc; stroke-dasharray: 5, 5; } .node.terminal { stroke-width: 1px; stroke: #000; } .edge { stroke: #ccc; stroke-width: 1px; pointer-events: none; } .edge.reflection { stroke: #333; stroke-width: 1px; } .beam { stroke-width: 1.5px; stroke: rgba(240, 0, 0, 0.8); } .extension { stroke-width: 1px; stroke: rgba(240, 0, 0, 0.8); stroke-dasharray: 5, 5; } text { fill: #aaa; font-size: 16px; font-family: monospace; pointer-events: none; } text.valid { fill: #000; } </style> <svg width="960" height="960"></svg> <script src="https://d3js.org/d3.v4.min.js"></script> <script src="intersection.debug.js"></script> <script> // 2017, Brooks Mershon var svg = d3.select("svg"), width = +svg.attr("width"), height = +svg.attr("height"), margin = { top: 20, left: 20, right: 20, bottom: 20}; // skewX(30) rotate(240) var g = svg.append("g") .attr("transform", "translate(" + width / 2 + "," + margin.top + ")"); // The number of dots in the last row. var N = 25; var side = (width - margin.left - margin.right) / N; var height = side * Math.sqrt(3) / 2; var emitter = new Vertex(0, 0, 0, 0, 0); var vertices = [ emitter ]; // Hash table for looking up vertices. var table = { 0: { 0: vertices[0] }}; var edges = []; var beam = { source: vertices[0], target: vertices[0] }; var extendedBeam = { source: vertices[0], target: vertices[0] }; // Create layout of vertices and edges. previousRow = vertices.slice(0, 1); for (let i = 1; i < N; i++) { let row = new Array(i + 1); for (let j = 0; j < i + 1; j++) { let label, newVertex, newEdge; // Compute position. let x = -side * i / 2 + j * side; let y = i * height; // Infer label. if (j === 0) { // First vertex in row. let t = previousRow[0]; label = (t.label + 1) % 3; newVertex = new Vertex(x, y, label, i, j); edges.push(new Edge(t, newVertex, 1)); } else { // Insect two other vertices of the triangle to infer vertex label. let p = row[j - 1]; let q = previousRow[j - 1]; let r = previousRow[j]; label = inferLabel(p.label, q.label); newVertex = new Vertex(x, y, label, i, j); edges.push( new Edge(p, newVertex, 1), new Edge(q, newVertex, 1)); if (j < i) { edges.push(new Edge(r, newVertex, 1)); } } if (!table[i]) { table[i] = {}; } table[i][j] = newVertex; row[j] = newVertex; } // Keep auxiliary information for the next row to infer colorings using the // previously created row of vertices. previousRow = row; vertices = vertices.concat(row); } // Create a vertex. function Vertex(x, y, label, row, index) { this.x = x; this.y = y; this.label = label; this.row = row; this.index = index; } // Create an edge. function Edge(a, b, label) { this.a = a; this.b = b; this.label = label; } // Returns the implied coloring if the other two vertices of a triangle are // colored x and y. The colors are 0, 1, or 2. function inferLabel(x, y) { return ((3 - (x + y) % 3) % 3); } var vertexColor = d3.scaleOrdinal() .domain([0, 2]) .range(["#EB9394", "#96D096", "#8FBBDA"]); var laser = g.append("line") .datum(beam) .classed("beam", true) .attr("x1", (d) => d.source.x) .attr("y1", (d) => d.source.y) .attr("x2", (d) => d.target.x) .attr("y2", (d) => d.target.y); var extension = g.append("line") .datum(extendedBeam) .classed("extension", true) .attr("x1", (d) => d.source.x) .attr("y1", (d) => d.source.y) .attr("x2", (d) => d.target.x) .attr("y2", (d) => d.target.y); var line = g.selectAll(".edge") .data(edges) .enter().append("line") .attr("x1", (d) => d.a.x) .attr("y1", (d) => d.a.y) .attr("x2", (d) => d.b.x) .attr("y2", (d) => d.b.y) .classed("edge", true); var circle = g.selectAll(".vertex") .data(vertices); var circleGroup = circle.enter().append("g"); var node = circleGroup.append("circle") .attr("cx", (d) => d.x) .attr("cy", (d) => d.y) .classed("node", true) .attr("r", side * 0.49) .on("mouseover", inspect) .on("mouseleave", function(d, i) { d3.select(this).classed("active", false); d3.select(this).classed("terminal", false); }); var vertex = circleGroup.append("circle") .attr("cx", (d) => d.x) .attr("cy", (d) => d.y) .attr("r", Math.min(0.1 * side, 5)) .classed("vertex", true) .classed("coprime", (d) => coprime(d.row, d.index)) .attr("fill", (d, i) => vertexColor(d.label)); function inspect(d, i) { let reflections = 0; let terminal = d; let factor = gcd(terminal.row, terminal.index); if (factor > 1) { terminal = table[terminal.row / factor][terminal.index / factor]; } beam.target = terminal; extendedBeam.source = beam.target; extendedBeam.target = d; d3.select(this).classed((d === terminal && d !== emitter) ? "terminal" : "active", true); vertex .classed("invalid", (vertex) => { return d.label === 0 && d === terminal && ((!coprime(vertex.row, vertex.index) || vertex.row !== d.row || vertex.label > 0)); }); // Horrible intersection test against all edges. line.each(function(edge, i) { let include = false; if (edge.a.row < terminal.row && edge.b.row < terminal.row && emitter !== edge.a && emitter !== edge.b && terminal !== edge.a && terminal !== edge.b) { include = intersection(0, 0, terminal.x, terminal.y, edge.a.x, edge.a.y, edge.b.x, edge.b.y); if (include) reflections++; } d3.select(this).classed("reflection", include); }); laser .datum(beam) .attr("x1", (d) => d.source.x) .attr("y1", (d) => d.source.y) .attr("x2", (d) => d.target.x) .attr("y2", (d) => d.target.y); extension .datum(extendedBeam) .attr("x1", (d) => d.source.x) .attr("y1", (d) => d.source.y) .attr("x2", (d) => d.target.x) .attr("y2", (d) => d.target.y); } // Returns the greatest common denominator. function gcd(a, b) { if (!b) { return a; } return gcd(b, a % b); }; function coprime(a, b) { return gcd(a, b) === 1; } </script>
https://d3js.org/d3.v4.min.js