// import * as d3 from "d3"; /* import { selection, select, selectAll } from "d3-selection"; //event import { drag } from "d3-drag" import { scaleLinear } from "d3-scale"; import { transition } from "d3-transition"; import { partition, hierarchy } from "d3-hierarchy"; import { json } from "d3-fetch"; import { easeLinear } from "d3-ease"; TODO - Why text not as childs of rectangles? - width height as arguments for init - Vertical - import d3 modules - // Via http://stackoverflow.com/questions/14167863/how-can-i-bring-a-circle-to-the-front-with-d3 // Necessary for highlighting time intervals properly d3.selection.prototype.moveToFront = function() { return this.each(function(){ this.parentNode.appendChild(this); }); }; */ // export default const timescale = (function(width = 960, height = 130) { // Via https://stackoverflow.com/questions/38224875/replacing-d3-transform-in-d3-v4 function getTranslation(transform) { // Create a dummy g for calculation purposes only. This will never // be appended to the DOM and will be discarded once this function // returns. const g = document.createElementNS("http://www.w3.org/2000/svg", "g"); // Set the transform attribute to the provided string value. g.setAttributeNS(null, "transform", transform); // consolidate the SVGTransformList containing all transformations // to a single SVGTransform of type SVG_TRANSFORM_MATRIX and get // its SVGMatrix. const matrix = g.transform.baseVal.consolidate().matrix; // As per definition values e and f are the ones for the translation. return [matrix.e, matrix.f]; } // Via https://stackoverflow.com/questions/9133500/how-to-find-a-node-in-a-tree-with-javascript function searchTree(node, property, match){ if (property === "nam" || "id" || "mid" || "end" || "") { if (node.data[property] === match){ return node; } else if (node.children != null){ let result = null; for (let i=0; result === null && i < node.children.length; i++) { result = searchTree(node.children[i], property, match); } return result; } return null; } else { console.warn("Property can't be used to search") } } // Initialize data let currentInterval; let root; let x; let dragStart; let transformStart; return { "init": function(divId) { let newX = 0.01; const drag = d3.drag() .subject(() => {return {x: newX, y: 0}} ) .on("start", function() { dragStart = event.pageX; transformStart = getTranslation(d3.select(".timescale").select("g").attr("transform")); d3.event.sourceEvent.stopPropagation(); }) .on("drag", function() { currentDrag = event.pageX; newX = (dragStart - currentDrag); d3.select(".timescale").select("g") .attr("transform", () => `translate(${[ parseInt(transformStart[0] + -newX), 0 ]}) scale(${parseInt(d3.select(".timescale").style("width"))/960})`); }); // Add class timescale to whatever divId was supplied d3.select(`#${divId}`).attr("class", "timescale"); // Create the SVG for the chart const time = d3.select(`#${divId}`).append("svg") .attr("width", width) .attr("height", height) .append("g"); // Move whole tick SVG group down 125px const scale = time.append("g") .attr("id", "tickBar") .attr("transform", "translate(0,125)"); x = d3.scaleLinear() .range([5, width]) .domain([5, width]); // Create a new d3 partition layout const partition = d3.partition() .size([width, height]) .padding(0); // Load the time scale data d3.json("intervals.json").then((result) => { // debugger root = d3.stratify() .id(d => d.id) .parentId(d => d.parentId )(result.records); //? add time for Holocene // Only sum lowest level timespans // .count partition(root.sum(d => (d.level === 5) ? d.start - d.end : 0 )) // timescale.initForm(root.descendants()) /* Draw timescale */ const rectGroup = time.append("g") .attr("id", "rectGroup"); const cell = rectGroup .selectAll("rect") .data(root.descendants()) .join("rect") .attr("x", d => d.x0) .attr("y", d => d.y0) .attr("width", d => (d.x1 - d.x0)) .attr("height", d => (d.y1 - d.y0)) .attr("fill", d => d.data.color) .attr("id", d => `t${d.data.id}`) .style("opacity", 0.83) .call(drag) .on("click", d => timescale.goTo(d) ); cell.append("title") .text(d => `${d.ancestors().map(d => d.data.name).reverse().join(" > ")}`); const uniqueAgesSet = new Set(root.descendants().map(node => node.data.start)) const uniqueAgesArray = Array.from(uniqueAgesSet) .map(start => (root.descendants()).find(node => node.data.start === start)) // Scale bar for the bottom of the graph const scaleBar = scale.selectAll("rect") .data(uniqueAgesArray); const hash = scaleBar.enter().append("g") .attr("class", d => `tickGroup s${d.depth}`) .attr("transform", d => `translate(${d.x0}, 0)`); hash.append("line") .attr("x1", 0) .attr("y1", 7.5) .attr("x2", 0) .attr("y2", d => 52 - d.depth * 8) .style("stroke-width", d => "0.05em"); hash.append("text") // .attr("transform", "rotate(45)") .attr("x", 0) .attr("y", d => 60 - d.depth * 8) .style("text-anchor", d => ((d.data.start !== 0.0117) ? "middle" : "end")) .style("font-size", d => `${0.9 - 0.08 * d.depth}em`) .attr("paint-order", "stroke") .attr("stroke-width", "1.5px") .attr("stroke", "#fff") .attr("stroke-linecap", "butt") .attr("stroke-linejoin", "miter") .text(d => d.data.start ); const textGroup = time.append("g") .attr("id", "textGroup"); // Add the full labels textGroup.selectAll("fullName") .data(root.descendants()) .enter().append("text") .text(d => d.data.name ) .attr("x", 1) .attr("y", d => d.y0 + 15) .attr("width", function() {return this.getComputedTextLength();}) .attr("height", d => d.y1 - d.y0 ) .attr("class", d => `fullName level${d.depth}`) .attr("id", d => `l${d.data.id}`) .attr("x", d => timescale.labelX(d)) .on("click", d => timescale.goTo(d)); // Add the abbreviations textGroup.selectAll("abbrevs") .data(root.descendants() ) .enter().append("text") .text(d => d.data.abr || d.data.name.charAt(0)) .attr("x", 1) .attr("y", d => d.y0 + 15) .attr("width", 30) .attr("height", d => d.y1 - d.y0) .attr("class", d => `abbr level${d.depth}`) .attr("id", d => `a${d.data.id}`) .attr("x", d => timescale.labelAbbrX(d)) .on("click", d => timescale.goTo(d) ); // Position the labels for the first time timescale.goTo(root); // Remove the Geologic time abbreviation d3.select(".abbr.levelundefined").remove(); // Open to Phanerozoic // timescale.goToName("Phanerozoic"); }); // Attach window resize listener to the window d3.select(window).on("resize", timescale.resize); // Size time scale to window timescale.resize(); }, // Calculates x-position for label abbreviations "labelAbbrX": function(d) { const rectWidth = x(d.x1) - x(d.x0), rectX = x(d.x0); const abbrevWidth = d3.select(`#a${d.data.id}`).node().getComputedTextLength(); if (rectWidth - 8 < abbrevWidth) { d3.select(`#a${d.data.id}`).style("display", "none"); } return rectX + (rectWidth - abbrevWidth) / 2; }, "labelX": function(d) { const rectWidth = x(d.x1) - x(d.x0), rectX = x(d.x0); let labelWidth; try { labelWidth = d3.select(`#l${d.data.id}`).node().getComputedTextLength(); //this? } catch { labelWidth = 25; } if (rectWidth - 8 < labelWidth) { d3.select(`#l${d.data.id}`).style("display", "none"); } else { d3.select(`#a${d.data.id}`).style("display", "none"); } return rectX + (rectWidth - labelWidth) / 2; }, // Zooms the graph to a given time interval // Accepts a data point or a named interval // split into gotoName "goToName": function(d) { d = searchTree(root, "name", d) timescale.goTo(d) }, "goTo": function(d) { // Stores the currently focused time interval for state restoration purposes timescale.currentInterval = d; // Reset all abbrevs and fullNames d3.selectAll(".fullName, .abbr") .style("display", "block"); x = d3.scaleLinear() .range([5, width]) .domain([d.x0, d.x1]); // Define transition for concurrent animation const t = d3.transition() .duration(300) .ease(d3.easeLinear); // Hide lowest two time labels if (d.depth === 0 || d.depth === 1) { d3.selectAll(`.s5, .s4`).transition(t).style("display", "none"); } else { d3.selectAll(`.s5, .s4`).transition(t).style("display", "block"); } // Transition the rectangles d3.selectAll("rect").transition(t) .attr("x", d => x(d.x0)) .attr("width", d => x(d.x1) - x(d.x0)) // Transition tick groups d3.selectAll(".tickGroup").transition(t) .attr("transform", function(d) { d3.select(this).selectAll("text") .style("text-anchor", "middle"); if (x(d.x0) === 5) { d3.select(this).select("text") .style("text-anchor", "start"); } else if (x(d.x0) === width) { d3.select(this).select("text") .style("text-anchor", "end"); } return `translate(${x(d.x0)}, 0)` }); // Move the full names, d3.selectAll(".fullName").transition(t) .attr("x", d => this.labelX(d)) .attr("height", d => d.y1 - d.y0) //Move the abbreviations d3.selectAll(".abbr").transition(t) .attr("x", d => this.labelAbbrX(d)) .attr("height", d => d.y1 - d.y0) // Center whichever interval was clicked d3.select(`#l${d.data.id}`).transition(t) .attr("x", width/2); // Position all the ancestors labels in the middle of the scale if (d.parent) { const ancestors = d.ancestors() ancestors.forEach(ancestor => { d3.select(`#l${ancestor.id}, #a${ancestor.id}`).transition(t) .attr("x", width/2); }) } timescale.resize(); }, "initForm": function(nodes) { const handleSubmit = (e) => { e.preventDefault() const selectedPeriod = e.target.querySelector("input[name=select-period]").value timescale.goToName(selectedPeriod) } const form = document.getElementById("go-to-period") form.onsubmit = handleSubmit; const options = nodes.map(node => ``) const datalist = document.getElementById("time-periods") datalist.innerHTML = options; const nameInput = document.getElementById('select-period'); nameInput.addEventListener('invalid', () => { if(nameInput.value === '') { nameInput.setCustomValidity('Select a time period'); } else { nameInput.setCustomValidity('Select a valid timeperiod. Try again!'); } }); }, "resize": function() { d3.select(".timescale g") .attr("transform", () => `scale(${parseInt(d3.select(".timescale").style("width"))/961})`); d3.select(".timescale svg") .style("width", () => d3.select(".timescale").style("width")) .style("height", () => parseInt(d3.select(".timescale").style("width")) * 0.25 + "px"); }, // Method for getting the currently zoomed-to interval - useful for preserving states "currentInterval": currentInterval } })();