From 56e65ff18429397bf0bf73c2957808e93c1e8dff Mon Sep 17 00:00:00 2001 From: Daniel Ellis Date: Mon, 2 Dec 2024 21:26:28 +0000 Subject: [PATCH] development --- static/circlepacked/bundle.js | 457 +++++++++++++++++++++++++++++++ static/circlepacked/index.html | 57 ++++ static/circlepacked/index.js | 200 ++++++++++++++ static/circlepacked/package.json | 14 + 4 files changed, 728 insertions(+) create mode 100644 static/circlepacked/bundle.js create mode 100644 static/circlepacked/index.html create mode 100644 static/circlepacked/index.js create mode 100644 static/circlepacked/package.json diff --git a/static/circlepacked/bundle.js b/static/circlepacked/bundle.js new file mode 100644 index 0000000..2ffd2a8 --- /dev/null +++ b/static/circlepacked/bundle.js @@ -0,0 +1,457 @@ +/* + FDEB algorithm implementation [www.win.tue.nl/~dholten/papers/forcebundles_eurovis.pdf]. + + Author: Corneliu S. (github.com/upphiminn) + 2013 + + */ +(function () { + d3.ForceEdgeBundling = function () { + var data_nodes = {}, // {'nodeid':{'x':,'y':},..} + data_edges = [], // [{'source':'nodeid1', 'target':'nodeid2'},..] + compatibility_list_for_edge = [], + subdivision_points_for_edge = [], + K = 0.1, // global bundling constant controlling edge stiffness + S_initial = 0.1, // init. distance to move points + P_initial = 1, // init. subdivision number + P_rate = 2, // subdivision rate increase + C = 6, // number of cycles to perform + I_initial = 90, // init. number of iterations for cycle + I_rate = 0.6666667, // rate at which iteration number decreases i.e. 2/3 + compatibility_threshold = 0.6, + eps = 1e-6; + + + /*** Geometry Helper Methods ***/ + function vector_dot_product(p, q) { + return p.x * q.x + p.y * q.y; + } + + function edge_as_vector(P) { + return { + 'x': data_nodes[P.target].x - data_nodes[P.source].x, + 'y': data_nodes[P.target].y - data_nodes[P.source].y + } + } + + function edge_length(e) { + // handling nodes that are on the same location, so that K/edge_length != Inf + if (Math.abs(data_nodes[e.source].x - data_nodes[e.target].x) < eps && + Math.abs(data_nodes[e.source].y - data_nodes[e.target].y) < eps) { + return eps; + } + + return Math.sqrt(Math.pow(data_nodes[e.source].x - data_nodes[e.target].x, 2) + + Math.pow(data_nodes[e.source].y - data_nodes[e.target].y, 2)); + } + + function custom_edge_length(e) { + return Math.sqrt(Math.pow(e.source.x - e.target.x, 2) + Math.pow(e.source.y - e.target.y, 2)); + } + + function edge_midpoint(e) { + var middle_x = (data_nodes[e.source].x + data_nodes[e.target].x) / 2.0; + var middle_y = (data_nodes[e.source].y + data_nodes[e.target].y) / 2.0; + + return { + 'x': middle_x, + 'y': middle_y + }; + } + + function compute_divided_edge_length(e_idx) { + var length = 0; + + for (var i = 1; i < subdivision_points_for_edge[e_idx].length; i++) { + var segment_length = euclidean_distance(subdivision_points_for_edge[e_idx][i], subdivision_points_for_edge[e_idx][i - 1]); + length += segment_length; + } + + return length; + } + + function euclidean_distance(p, q) { + return Math.sqrt(Math.pow(p.x - q.x, 2) + Math.pow(p.y - q.y, 2)); + } + + function project_point_on_line(p, Q) { + var L = Math.sqrt((Q.target.x - Q.source.x) * (Q.target.x - Q.source.x) + (Q.target.y - Q.source.y) * (Q.target.y - Q.source.y)); + var r = ((Q.source.y - p.y) * (Q.source.y - Q.target.y) - (Q.source.x - p.x) * (Q.target.x - Q.source.x)) / (L * L); + + return { + 'x': (Q.source.x + r * (Q.target.x - Q.source.x)), + 'y': (Q.source.y + r * (Q.target.y - Q.source.y)) + }; + } + + /*** ********************** ***/ + + /*** Initialization Methods ***/ + function initialize_edge_subdivisions() { + for (var i = 0; i < data_edges.length; i++) { + if (P_initial === 1) { + subdivision_points_for_edge[i] = []; //0 subdivisions + } else { + subdivision_points_for_edge[i] = []; + subdivision_points_for_edge[i].push(data_nodes[data_edges[i].source]); + subdivision_points_for_edge[i].push(data_nodes[data_edges[i].target]); + } + } + } + + function initialize_compatibility_lists() { + for (var i = 0; i < data_edges.length; i++) { + compatibility_list_for_edge[i] = []; //0 compatible edges. + } + } + + function filter_self_loops(edgelist) { + var filtered_edge_list = []; + + for (var e = 0; e < edgelist.length; e++) { + if (data_nodes[edgelist[e].source].x != data_nodes[edgelist[e].target].x || + data_nodes[edgelist[e].source].y != data_nodes[edgelist[e].target].y) { //or smaller than eps + filtered_edge_list.push(edgelist[e]); + } + } + + return filtered_edge_list; + } + + /*** ********************** ***/ + + /*** Force Calculation Methods ***/ + function apply_spring_force(e_idx, i, kP) { + var prev = subdivision_points_for_edge[e_idx][i - 1]; + var next = subdivision_points_for_edge[e_idx][i + 1]; + var crnt = subdivision_points_for_edge[e_idx][i]; + var x = prev.x - crnt.x + next.x - crnt.x; + var y = prev.y - crnt.y + next.y - crnt.y; + + x *= kP; + y *= kP; + + return { + 'x': x, + 'y': y + }; + } + + function apply_electrostatic_force(e_idx, i) { + var sum_of_forces = { + 'x': 0, + 'y': 0 + }; + var compatible_edges_list = compatibility_list_for_edge[e_idx]; + + for (var oe = 0; oe < compatible_edges_list.length; oe++) { + var force = { + 'x': subdivision_points_for_edge[compatible_edges_list[oe]][i].x - subdivision_points_for_edge[e_idx][i].x, + 'y': subdivision_points_for_edge[compatible_edges_list[oe]][i].y - subdivision_points_for_edge[e_idx][i].y + }; + + if ((Math.abs(force.x) > eps) || (Math.abs(force.y) > eps)) { + var diff = (1 / Math.pow(custom_edge_length({ + 'source': subdivision_points_for_edge[compatible_edges_list[oe]][i], + 'target': subdivision_points_for_edge[e_idx][i] + }), 1)); + + sum_of_forces.x += force.x * diff; + sum_of_forces.y += force.y * diff; + } + } + + return sum_of_forces; + } + + + function apply_resulting_forces_on_subdivision_points(e_idx, P, S) { + var kP = K / (edge_length(data_edges[e_idx]) * (P + 1)); // kP=K/|P|(number of segments), where |P| is the initial length of edge P. + // (length * (num of sub division pts - 1)) + var resulting_forces_for_subdivision_points = [{ + 'x': 0, + 'y': 0 + }]; + + for (var i = 1; i < P + 1; i++) { // exclude initial end points of the edge 0 and P+1 + var resulting_force = { + 'x': 0, + 'y': 0 + }; + + spring_force = apply_spring_force(e_idx, i, kP); + electrostatic_force = apply_electrostatic_force(e_idx, i, S); + + resulting_force.x = S * (spring_force.x + electrostatic_force.x); + resulting_force.y = S * (spring_force.y + electrostatic_force.y); + + resulting_forces_for_subdivision_points.push(resulting_force); + } + + resulting_forces_for_subdivision_points.push({ + 'x': 0, + 'y': 0 + }); + + return resulting_forces_for_subdivision_points; + } + + /*** ********************** ***/ + + /*** Edge Division Calculation Methods ***/ + function update_edge_divisions(P) { + for (var e_idx = 0; e_idx < data_edges.length; e_idx++) { + if (P === 1) { + subdivision_points_for_edge[e_idx].push(data_nodes[data_edges[e_idx].source]); // source + subdivision_points_for_edge[e_idx].push(edge_midpoint(data_edges[e_idx])); // mid point + subdivision_points_for_edge[e_idx].push(data_nodes[data_edges[e_idx].target]); // target + } else { + var divided_edge_length = compute_divided_edge_length(e_idx); + var segment_length = divided_edge_length / (P + 1); + var current_segment_length = segment_length; + var new_subdivision_points = []; + new_subdivision_points.push(data_nodes[data_edges[e_idx].source]); //source + + for (var i = 1; i < subdivision_points_for_edge[e_idx].length; i++) { + var old_segment_length = euclidean_distance(subdivision_points_for_edge[e_idx][i], subdivision_points_for_edge[e_idx][i - 1]); + + while (old_segment_length > current_segment_length) { + var percent_position = current_segment_length / old_segment_length; + var new_subdivision_point_x = subdivision_points_for_edge[e_idx][i - 1].x; + var new_subdivision_point_y = subdivision_points_for_edge[e_idx][i - 1].y; + + new_subdivision_point_x += percent_position * (subdivision_points_for_edge[e_idx][i].x - subdivision_points_for_edge[e_idx][i - 1].x); + new_subdivision_point_y += percent_position * (subdivision_points_for_edge[e_idx][i].y - subdivision_points_for_edge[e_idx][i - 1].y); + new_subdivision_points.push({ + 'x': new_subdivision_point_x, + 'y': new_subdivision_point_y + }); + + old_segment_length -= current_segment_length; + current_segment_length = segment_length; + } + current_segment_length -= old_segment_length; + } + new_subdivision_points.push(data_nodes[data_edges[e_idx].target]); //target + subdivision_points_for_edge[e_idx] = new_subdivision_points; + } + } + } + + /*** ********************** ***/ + + /*** Edge compatibility measures ***/ + function angle_compatibility(P, Q) { + return Math.abs(vector_dot_product(edge_as_vector(P), edge_as_vector(Q)) / (edge_length(P) * edge_length(Q))); + } + + function scale_compatibility(P, Q) { + var lavg = (edge_length(P) + edge_length(Q)) / 2.0; + return 2.0 / (lavg / Math.min(edge_length(P), edge_length(Q)) + Math.max(edge_length(P), edge_length(Q)) / lavg); + } + + function position_compatibility(P, Q) { + var lavg = (edge_length(P) + edge_length(Q)) / 2.0; + var midP = { + 'x': (data_nodes[P.source].x + data_nodes[P.target].x) / 2.0, + 'y': (data_nodes[P.source].y + data_nodes[P.target].y) / 2.0 + }; + var midQ = { + 'x': (data_nodes[Q.source].x + data_nodes[Q.target].x) / 2.0, + 'y': (data_nodes[Q.source].y + data_nodes[Q.target].y) / 2.0 + }; + + return lavg / (lavg + euclidean_distance(midP, midQ)); + } + + function edge_visibility(P, Q) { + var I0 = project_point_on_line(data_nodes[Q.source], { + 'source': data_nodes[P.source], + 'target': data_nodes[P.target] + }); + var I1 = project_point_on_line(data_nodes[Q.target], { + 'source': data_nodes[P.source], + 'target': data_nodes[P.target] + }); //send actual edge points positions + var midI = { + 'x': (I0.x + I1.x) / 2.0, + 'y': (I0.y + I1.y) / 2.0 + }; + var midP = { + 'x': (data_nodes[P.source].x + data_nodes[P.target].x) / 2.0, + 'y': (data_nodes[P.source].y + data_nodes[P.target].y) / 2.0 + }; + + return Math.max(0, 1 - 2 * euclidean_distance(midP, midI) / euclidean_distance(I0, I1)); + } + + function visibility_compatibility(P, Q) { + return Math.min(edge_visibility(P, Q), edge_visibility(Q, P)); + } + + function compatibility_score(P, Q) { + return (angle_compatibility(P, Q) * scale_compatibility(P, Q) * position_compatibility(P, Q) * visibility_compatibility(P, Q)); + } + + function are_compatible(P, Q) { + return (compatibility_score(P, Q) >= compatibility_threshold); + } + + function compute_compatibility_lists() { + for (var e = 0; e < data_edges.length - 1; e++) { + for (var oe = e + 1; oe < data_edges.length; oe++) { // don't want any duplicates + if (are_compatible(data_edges[e], data_edges[oe])) { + compatibility_list_for_edge[e].push(oe); + compatibility_list_for_edge[oe].push(e); + } + } + } + } + + /*** ************************ ***/ + + /*** Main Bundling Loop Methods ***/ + var forcebundle = function () { + var S = S_initial; + var I = I_initial; + var P = P_initial; + + initialize_edge_subdivisions(); + initialize_compatibility_lists(); + update_edge_divisions(P); + compute_compatibility_lists(); + + for (var cycle = 0; cycle < C; cycle++) { + for (var iteration = 0; iteration < I; iteration++) { + var forces = []; + for (var edge = 0; edge < data_edges.length; edge++) { + forces[edge] = apply_resulting_forces_on_subdivision_points(edge, P, S); + } + for (var e = 0; e < data_edges.length; e++) { + for (var i = 0; i < P + 1; i++) { + subdivision_points_for_edge[e][i].x += forces[e][i].x; + subdivision_points_for_edge[e][i].y += forces[e][i].y; + } + } + } + // prepare for next cycle + S = S / 2; + P = P * P_rate; + I = I_rate * I; + + update_edge_divisions(P); + //console.log('C' + cycle); + //console.log('P' + P); + //console.log('S' + S); + } + return subdivision_points_for_edge; + }; + /*** ************************ ***/ + + + /*** Getters/Setters Methods ***/ + forcebundle.nodes = function (nl) { + if (arguments.length === 0) { + return data_nodes; + } else { + data_nodes = nl; + } + + return forcebundle; + }; + + forcebundle.edges = function (ll) { + if (arguments.length === 0) { + return data_edges; + } else { + data_edges = filter_self_loops(ll); //remove edges to from to the same point + } + + return forcebundle; + }; + + forcebundle.bundling_stiffness = function (k) { + if (arguments.length === 0) { + return K; + } else { + K = k; + } + + return forcebundle; + }; + + forcebundle.step_size = function (step) { + if (arguments.length === 0) { + return S_initial; + } else { + S_initial = step; + } + + return forcebundle; + }; + + forcebundle.cycles = function (c) { + if (arguments.length === 0) { + return C; + } else { + C = c; + } + + return forcebundle; + }; + + forcebundle.iterations = function (i) { + if (arguments.length === 0) { + return I_initial; + } else { + I_initial = i; + } + + return forcebundle; + }; + + forcebundle.iterations_rate = function (i) { + if (arguments.length === 0) { + return I_rate; + } else { + I_rate = i; + } + + return forcebundle; + }; + + forcebundle.subdivision_points_seed = function (p) { + if (arguments.length == 0) { + return P; + } else { + P = p; + } + + return forcebundle; + }; + + forcebundle.subdivision_rate = function (r) { + if (arguments.length === 0) { + return P_rate; + } else { + P_rate = r; + } + + return forcebundle; + }; + + forcebundle.compatibility_threshold = function (t) { + if (arguments.length === 0) { + return compatibility_threshold; + } else { + compatibility_threshold = t; + } + + return forcebundle; + }; + + /*** ************************ ***/ + + return forcebundle; + } +})(); diff --git a/static/circlepacked/index.html b/static/circlepacked/index.html new file mode 100644 index 0000000..8b7833e --- /dev/null +++ b/static/circlepacked/index.html @@ -0,0 +1,57 @@ + + + + + + D3 Visualization + + + + + + + +
+
+ + + + + + + \ No newline at end of file diff --git a/static/circlepacked/index.js b/static/circlepacked/index.js new file mode 100644 index 0000000..845c8e0 --- /dev/null +++ b/static/circlepacked/index.js @@ -0,0 +1,200 @@ +function chart(data) { + // Clear any existing chart + const chartContainer = document.getElementById('chart'); + chartContainer.innerHTML = ''; + + // Specify the chart's dimensions. + const width = d3.min([window.innerWidth, window.innerHeight]); + const height = width; + + // Create the color scale. + // const color = d3.scaleOrdinal(d3.schemeCategory10); + // Color generation + const colours = d3.schemeCategory10.map((d) => + d3.scaleSequential([5, 0], (t) => d3.interpolate(d, "white")(t)) + ); + + const color = {}; + data.children.forEach((d, i) => (color[d.prefix] = colours[i])); + + + // Compute the layout. + const pack = (data) => + d3.pack().size([width, height]).padding(5)( + d3 + .hierarchy(data) + .sum((d) => d.size) + // .sort((a, b) => b.size - a.size) + ); + + // function d3_layout_packSort(a, b) { + // return -(a.value - b.value); + // } + const root = pack(data); + + // Create the SVG container. + const svg = d3 + .create("svg") + .attr("viewBox", `-${width / 2} -${height / 2} ${width} ${height}`) + .attr("width", width) + .attr("height", height) + .attr('fill', '#555') + .attr("text-anchor", "middle") + + .attr( + "style", + `max-width: 100%; height: auto; display: block; margin: 0 -14px; background: white; cursor: pointer;` + ); + + + window.nodes = root.descendants().slice(1) + + function sanitizeUrl(url) { + return url.toLowerCase().replace(/([^:]\/)\/+/g, '$1'); + } + + +function linkstroke(d) { + return d.depth==2?'none':color(d.data.prefix) +} + + +const head = d3.select("#title") + // Append the nodes. + const node = svg + .append("g") + .selectAll("circle") + .data(window.nodes) + .join("circle") + .attr("fill", "white") + .attr("fill-opacity", 0.1) + .attr("stroke", linkstroke) + .attr("stroke-width", (d) => (d.depth == 2 ? 2 : 1)) + .attr("stroke-opacity", (d) => (d.depth ===1? 0.3: d.depth < 3 === 0 ? 1 : 0.4)) + .attr("stroke-dasharray", (d) => (d.depth != 1 ? "0" : "8 6")) + .attr("pointer-events", (d) => (!d.children ? "none" : null)) + .on("mouseover", function () { + d3.select(this).attr("stroke", (d) => { + head.text(`${d.data.prefix} : ${d.data.name}`) + return color(d.data.prefix)} + ); + + + }) + .on("mouseout", function () { + d3.select(this) + .attr("stroke", linkstroke) + .attr("stroke-opacity", 1); + }) + .on( + "click", + + (event, d) => {if (d.depth<3) return focus !== d && (zoom(event, d), event.stopPropagation()) +}); + + console.log(links) + + // Append the text labels. + const label = svg + .append("g") + .style("font", "10px sans-serif") + .attr("pointer-events", "none") + .attr("text-anchor", "middle") + + .selectAll("text") + .data(root.descendants()) + .join("text") + .style("fill-opacity", (d) => (d.parent === root ? 1 : 0)) + .style("display", (d) => (d.parent === root ? "inline" : "none")) + .style("font-size", (d) => + d.depth < 2 ? "2.5em" : d.depth > 2 ? "0.01em" : "1em" + ) + .attr("text-align", "center") + .attr("backdrop-filter", "blur(10px)") + // .attr("color", "red") + .text((d) => d.data.name.toUpperCase()); + + // Create the zoom behavior and zoom immediately in to the initial focus node. + svg.on("click", (event) => zoom(event, root)); + let focus = root; + let view; + zoomTo([focus.x, focus.y, focus.r * 2]); + + function zoomTo(v) { + const k = width / v[2]; + + view = v; + + label.attr( + "transform", + (d) => `translate(${(d.x - v[0]) * k},${(d.y - v[1]) * k})` + ); + node.attr( + "transform", + (d) => `translate(${(d.x - v[0]) * k},${(d.y - v[1]) * k})` + ); + + console.log(k, v, width / 2) + + svglinks.attr( + "transform", + `translate(${-v[0]},${-v[1]})` + ); + + if (Math.floor(k) != 1) { + svglinks.attr("opacity", 0) + } else { + svglinks.attr("opacity", 1) + } + + node.attr("r", (d) => d.r * k); + } + + function zoom(event, d) { + const focus0 = focus; + + focus = d; + + const transition = svg + .transition() + .duration(event.altKey ? 7500 : 750) + .tween("zoom", (d) => { + const i = d3.interpolateZoom(view, [focus.x, focus.y, focus.r * 2]); + return (t) => zoomTo(i(t)); + }); + + label + .filter(function (d) { + return d.parent === focus || this.style.display === "inline"; + }) + .transition(transition) + .style("fill-opacity", (d) => (d.parent === focus ? 1 : 0)) + .attr("backdrop-filter", "blur(10px)") + .on("start", function (d) { + if (d.parent === focus) this.style.display = "inline"; + }) + .on("end", function (d) { + if (d.parent !== focus) this.style.display = "none"; + }); + } + + // Add the SVG to the chart container + document.getElementById('chart').appendChild(svg.node()); +} + +// When the data is loaded, call the chart function +document.addEventListener('DOMContentLoaded', () => { + // Check if the data is already loaded (via script tag) + if (window.data) { + chart(window.data); + } else { + // Fetch the data if not already loaded + fetch('https://wcrp-cmip.github.io/LD-Collection/universe_contents/universe_hierarchy.json') + .then(response => response.json()) + .then(data => chart(data)) + .catch(error => console.error('Error loading data:', error)); + } +}); + + +// npm install -g nodemon \ No newline at end of file diff --git a/static/circlepacked/package.json b/static/circlepacked/package.json new file mode 100644 index 0000000..500d8b0 --- /dev/null +++ b/static/circlepacked/package.json @@ -0,0 +1,14 @@ +{ + "name": "d3-visualization", + "version": "1.0.0", + "description": "D3 Visualization with Live Reload", + "scripts": { + "start": "nodemon --watch index.html --watch visualization.js --watch data.json --watch links.json --exec 'browser-sync start --server --files \"index.html, visualization.js, data.json, links.json\" --no-open'" + }, + "devDependencies": { + "browser-sync": "^2.29.3" + }, + "dependencies": { + "nodemon": "^3.1.7" + } +}