'use strict'; //////////////////////////////////////////////////////////////////////////////// /// FIELDS //////////////////////////////////////////////////////////////////////////////// const EXTRACTED_TERMS_EVENT = 'extracted_terms_event'; const EXTRACTED_COUNT_EVENT = 'extracted_count_event'; const SELECTED_TERM_EVENT = 'selected_term_event'; const SELECTED_BRANCH_EVENT = 'selected_branch_event'; const SELECTED_SOURCE_EVENT = 'selected_source_event'; const DISPLAY_VIEW_EVENT = 'display_view_event'; const ISO_LINE_DOM_QUERY = '.phylo-isoline'; const LEFT_COLUMN_DOM_QUERY = '.phylo-grid__blueprint__left'; const CENTER_COLUMN_DOM_QUERY = '.phylo-grid__blueprint__center'; const SCAPE_DOM_QUERY = '.phylo-grid__content__scape'; // (?) Global thread dependencies: // * d3 <Object> (main D3 proxy)) // * window <Window> (cf. below function signature for window uses) // * document <HTMLDocument> let memoTickText = {}; // <Object> of <Int> => <TickText> /// <TickText> :: /// <Int> bId /// <Float> limit /// <String>text let panel = undefined; // <Object> instanceof d3.selection let svg = undefined; // <Object> instanceof d3.selection let label = undefined; // <Object> instanceof d3.selection let zoom = undefined; // <Function> see https://github.com/d3/d3-zoom#zoom let xScale0 = undefined; // <Function> see https://github.com/d3/d3-scale#_continuous let yScale0 = undefined; // <Function> see https://github.com/d3/d3-scale#_continuous let subscribers = {}; // <Object> dictionary for pubsub pattern //////////////////////////////////////////////////////////////////////////////// /// HELPERS //////////////////////////////////////////////////////////////////////////////// /** * @name contains * @param {String} str * @param {Array} arr * @returns {Boolean} */ function contains(str,arr) { return arr.indexOf(str) > -1; } /** * @name addDays * @param {String|Date} date * @param {Int} days * @returns {Date} */ function addDays(date, days) { var result = new Date(date); result.setDate(result.getDate() + days); return result; } /** * @name removeDays * @param {String|Date} date * @param {Int} days * @returns {Date} */ function removeDays(date, days) { var result = new Date(date); result.setDate(result.getDate() - days); return result; } /** * @name appendCSS * @param {String} cssText * @param {Element} element * @unpure {HTMLDocument} document */ function appendCSS( cssText, element ) { var styleElement = document.createElement("style"); styleElement.setAttribute("type","text/css"); styleElement.innerHTML = cssText; var refNode = element.hasChildNodes() ? element.children[0] : null; element.insertBefore( styleElement, refNode ); } /** * @name debounce * @param {Function<*>} fn * @param {Int} wait * @param {Boolean} immediate [default: undefined] * @returns {Function<*>} */ function debounce(fn, wait, immediate) { var timeout; return function() { var context = this , args = arguments , later = function() { timeout = null; if (immediate !== true) { fn.apply(context, args); } } , now = immediate === true && timeout === null; clearTimeout(timeout); timeout = setTimeout(later, wait); if (now === true) { fn.apply(context, args); } } } /** * @name rdm * @returns {Int} */ function rdm() { if (Math.random() > 0.5) { return 1; } else { return -1; } } /** * @name arraySum * @param {*} acc * @param {*} curr */ function arraySum(acc, curr) { return acc + curr } /** * (?) Use of a PubSub pattern has been empiracally implemented to provide a * behavorial interface linking PureScript and JavaScript processes * * The PubSub will be used for bridge communication with functions (such * as `drawWordCloud`, `termClick`, etc.) that can both perform as: * - JavaScript → PureScript * - PureScript → JavaScript → PureScript * * One ideal solution would have been to translate this very JavaScript * module in PureScript, but due to a time-consuming issue certain parts are * still in JavaScript * * For these reasons, we decided to use a PubSub with event from JavaScript * ↔ PureScript. It is a simpler version as Justin Woo made in its repo [1] * It however make an assumption that every `callback` provided from a new * subscription was provided in a PureScript `Effect` thunk * * * @name pubsub * @param {Object} subscribers of <Array> of <Object> dictionary * <String> id => <Function<*>> callback * @pattern module * @link https://github.com/justinwoo/call-ps-from-js [1] */ var pubsub = (function(subscribers) { /** * @name generateUUID * @access private * @returns {String} */ function generateUUID() { return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) ); } return { /** * @name subscribe * @access public * @param {String} eventLabel * @param {Function<*>} cbk * @returns {String} subscriptionId */ subscribe(eventLabel, cbk) { var id; if (subscribers.hasOwnProperty(eventLabel) === false) { subscribers[eventLabel] = {}; } id = generateUUID(); subscribers[eventLabel][id] = cbk; return id; }, /** * @name publish * @access public * @param {String} eventLabel * @param {*} data */ publish(eventLabel, data) { if (subscribers.hasOwnProperty(eventLabel) === false) { return; } Object.keys(subscribers[eventLabel]).forEach( function(subscriptionId) { // assuming it came from PureScript (ie. as an `Effect`) return subscribers[eventLabel][subscriptionId](data)(); } ); }, /** * @name unsubscribe * @access public * @param {String} eventLabel * @param {String} subscriptionId */ unsubscribe(eventLabel, subscriptionId) { if (subscribers.hasOwnProperty(eventLabel) === false) { return; } if (subscribers[eventLabel].hasOwnProperty(subscriptionId) === false) { return; } delete subscribers[eventLabel][subscriptionId]; } }; })(subscribers); //////////////////////////////////////////////////////////////////////////////// /// ACTIONS //////////////////////////////////////////////////////////////////////////////// /** * @name groupTermsBy * @param {HTMLCollection} elements * @param {String} attr * @returns {Array} * <Array> * <String> stringified float * <String> stringified float * <String> stringified int * <Array> * <String> stringified float * <String> stringified float * <String> stringified int */ function groupTermsBy(elements, attr) { let grouped = {}, curr = ""; for (var i = 0; i < elements.length; i++) { let from = elements[i].getAttribute(attr) if (curr != from) { grouped[from] = [[(elements[i]).getAttribute("gx"),(elements[i]).getAttribute("gy"),(elements[i]).getAttribute("bid")]]; curr = from } else { grouped[from].push([(elements[i]).getAttribute("gx"),(elements[i]).getAttribute("gy"),(elements[i]).getAttribute("bid")]); } } return Object.values(grouped); } /** * @name showLabel * @unpure {HTMLDocument} document */ function showLabel() { var ngrams = document.getElementsByClassName("ngrams"); var groups = document.getElementsByClassName("group-inner"); var headers = document.getElementsByClassName("header"); window.ldView = false; d3.selectAll(".group-path") .classed("path-heading", false); Array.from(groups).forEach(function(item) { item.style.fill = "#fff"; item.classList.remove("group-heading"); }) Array.from(headers).forEach(function(item) { item.style.visibility = "hidden"; }) Array.from(ngrams).forEach(function(item) { item.style.visibility = "visible"; item.style.fill = '#0d1824'; }) } /** * @name showHeading * @unpure {Window.<Boolean>} window.ldView * @unpure {Object} d3 * @unpure {HTMLDocument} document */ function showHeading() { var ngrams = document.getElementsByClassName("ngrams"); var groups = document.getElementsByClassName("group-inner"); var headers = document.getElementsByClassName("header"); window.ldView = true; d3.selectAll(".group-path") .classed("path-heading", true); Array.from(groups).forEach(function(item) { item.style.fill = "#f5eee6"; item.classList.add("group-heading"); }) Array.from(headers).forEach(function(item) { item.style.visibility = "visible"; }) Array.from(ngrams).forEach(function(item) { item.style.visibility = "hidden"; }) } /** * @name showLanding * @unpure {Window.<Boolean>} window.ldView * @unpure {Object} d3 * @unpure {HTMLDocument} document */ function showLanding() { var ngrams = document.getElementsByClassName("ngrams"); var groups = document.getElementsByClassName("group-inner"); var headers = document.getElementsByClassName("header") window.ldView = true; d3.selectAll(".group-path") .classed("path-heading", false); Array.from(groups).forEach(function(item) { item.style.fill = "#61a3a9"; item.classList.remove("group-heading"); }) Array.from(headers).forEach(function(item) { item.style.visibility = "hidden" }) Array.from(ngrams).forEach(function(item) { item.style.fill = "#61a3a9"; item.style.visibility = "hidden"; }) } /** * @name resetSelection * @unpure {Window.Array.<Int>} window.branchFocus * @unpure {Object} pubsub */ function resetSelection() { window.branchFocus = []; pubsub.publish(SELECTED_TERM_EVENT, ''); pubsub.publish(SELECTED_BRANCH_EVENT, ''); pubsub.publish(SELECTED_SOURCE_EVENT, ''); pubsub.publish(EXTRACTED_TERMS_EVENT, []); pubsub.publish(EXTRACTED_COUNT_EVENT, ''); } /** * @name doubleClick * @unpure {Window.<Boolean>} window.highlighted * @unpure {Object} d3 * @unpure {Object} pubsub */ function doubleClick() { window.highlighted = false; headerOut(); d3.selectAll(".group-inner") .classed("group-unfocus",false) .classed("group-focus",false); d3.selectAll(".group-path") .classed("path-unfocus",false) .classed("path-focus",false); d3.selectAll(".term-path").remove(); d3.selectAll(".peak").classed("peak-focus",false); d3.selectAll(".peak").classed("peak-focus-source",false); d3.selectAll(".x-mark").style("fill","#4A5C70"); resetSelection(); } /** * @name headerOut * @unpure {Object} d3 */ function headerOut() { d3.selectAll(".header").nodes().forEach(function(header){ header.style["font-size"] = header.getAttribute("mem-size") + "px"; header.style["opacity"] = header.getAttribute("mem-opac"); }) } /** * @name termClick * @param {String} txt * @param {String} idx stringified int * @param {Int} nodeId * @param {String} typeNode "group|head|search" * @unpure {Object} d3 * @unpure {HTMLDocument} document * @unpure {Window.Array.<Int>} window.branchFocus * @unpure {Object} panel instanceof d3.selection * @unpure {Object} pubsub */ function termClick (txt,idx,nodeId,typeNode) { // remove old focus initPath() resetSelection(); // catch the last transformations if (typeNode == "group") { var transform = d3.select("#group" + nodeId).node().getAttribute("transform"); } else if (typeNode == "head") { var transform = d3.select("#head" + nodeId).node().getAttribute("transform"); } else { var transform = (d3.selectAll(".header").nodes())[0].getAttribute("transform"); } // focus pubsub.publish(SELECTED_TERM_EVENT, txt); // highlight the groups var terms = document.getElementsByClassName("fdt-" + idx), periods = groupTermsBy(terms,"from"); var groups = []; for (var i = 0; i < terms.length; i++) { groups.push(d3.select("#group" + (terms[i]).getAttribute("gid"))); window.branchFocus.push( parseInt( (terms[i]).getAttribute("bid"), 10 ) ); } highlightGroups(groups.map(g => g.node())); drawWordCloud(groups.map(g => g.node())); // highlight the cross branches links var bids = []; for (var i = 0; i < periods.length; i++) { if (i != periods.length - 1) { for (var j = 0; j < periods[i].length; j++) { bids.push(periods[i][j][2]) var x1 = periods[i][j][0], y1 = periods[i][j][1]; for (var k = 0; k < periods[i + 1].length; k++) { var x2 = periods[i + 1][k][0], y2 = periods[i + 1][k][1]; if ((periods[i][j][2] != periods[i + 1][k][2]) && (!bids.includes(periods[i + 1][k][2]))) { // draw the links between branches panel .append("path") .attr("class","term-path") .attr("d", function(d) { return "M" + x1 + "," + y1 + "C" + x2 + "," + y1 + " " + x2 + "," + y2 + " " + x2 + "," + y2; }) .attr("transform",transform) .style("stroke-opacity", 0.4) .lower(); } bids.push(periods[i + 1][k][2]) } } } } d3.selectAll(".path-unfocus").lower(); } /** * @name initPath * @unpure {Object} d3 */ function initPath () { let groups = d3.selectAll(".group-inner"); (groups.nodes()).map(function(g){ if (!g.classList.contains("source-focus")) { g.classList.add("group-unfocus"); g.classList.remove("group-focus"); } }) d3.selectAll(".group-path") .classed("path-unfocus",true) .classed("path-focus",false); d3.selectAll(".term-path").remove(); d3.selectAll(".peak").classed("peak-focus",false); d3.selectAll(".peak").classed("peak-focus-source",false); d3.selectAll(".x-mark").style("fill","#4A5C70"); } /** * @name highlightGroups * @param {Array.<Element>} groups * @unpure {HTMLDocument} document * @unpure {Object} d3 * @unpure {Object} pubsub */ function highlightGroups(groups) { let paths = document.getElementsByClassName("group-path"), gids = []; for (var i = 0; i < groups.length; i++) { // highlight the groups groups[i] .classList.add("group-focus"); groups[i] .classList.remove("group-unfocus"); // .classed("group-unfocus", false) // .classed("group-focus", true); gids.push(groups[i].getAttribute("gid")) // highlight the branches peak let bid = groups[i].getAttribute("bId") d3.select("#peak-" + bid) .classed("peak-focus", true); d3.select("#xmark-" + bid) .style("fill", "#F0684D"); } // facets var count = { groupCount: groups.length, termCount: countTerms(groups), branchCount: countBranches(groups) }; pubsub.publish( EXTRACTED_COUNT_EVENT, JSON.stringify( count ) ); // highlight the links for (var i = 0; i < paths.length; i++) { if (gids.includes((paths[i]).getAttribute("source")) && (paths[i]).getAttribute("target")) { paths[i].classList.add("path-focus"); paths[i].classList.remove("path-unfocus"); } } } /** * @name countTerms * @param {Array.<SVGCircleElement>} groups * @unpure {Object} d3 * @returns {Int} */ function countTerms(groups) { var terms = []; for (var i = 0; i < groups.length; i++) { let gid = ((groups[i].getAttribute("id")).split("group"))[1] d3.selectAll(".g-" + gid).nodes().forEach(e => terms.push(e.getAttribute("fdt"))) } return (Array.from(new Set(terms))).length; } /** * @name countBranches * @param {Array.<SVGCircleElement>} groups * @returns {Int} */ function countBranches(groups) { var branches = []; for (var i = 0; i < groups.length; i++) { branches.push(groups[i].getAttribute("bId")); } return (Array.from(new Set(branches))).length; } /** * @name resetView * @unpure {Object} svg instanceof d3.selection */ function resetView() { svg.transition() .duration(750) .call(zoom.transform, d3.zoomIdentity); } /** * @name peakClick * @param {Object} b <Gargantext.Components.PhyloExplorer.Types.Branch> * @param {Object} coordinates * <Float> x * <Float> y * <Float> w * <Float> h * @unpure {Object} d3 * @unpure {Function} zoom * @unpure {Window.Array.<Int>} window.branchFocus */ function peakClick (b, coordinates) { let groups = d3.selectAll(".group-inner").filter(".branch-" + b.bId).nodes(); initPath(); resetSelection(); pubsub.publish(SELECTED_BRANCH_EVENT, b.label); window.branchFocus.push( parseInt(b.bId, 10) ); drawWordCloud(groups); highlightGroups(groups); /* rescale */ let tx = (groups[0]).getAttribute("cx") svg.transition() .duration(750) .call(zoom.transform, d3.zoomIdentity.translate(((coordinates.x + coordinates.w) / 2) - tx, 0).scale(1)); d3.selectAll(".path-unfocus").lower(); } /** * @name peakOver * @param {Object} b <Gargantext.Components.PhyloExplorer.Types.Branch> * @param {Int} i * @unpure {Object} d3 * @unpure {Object} label * @unpure {Function<Int>} yScale0 see https://github.com/d3/d3-scale#_continuous * @unpure {Function<Int>} xScale0 see https://github.com/d3/d3-scale#_continuous */ function peakOver (b,i) { d3.select("#peak-" + i).classed("peak-focus",false); d3.select("#peak-" + i).classed("peak-over",true); label.text(b.label.replace(/"/g,'')) .style("visibility", "visible") .style("top", yScale0(b.y) + "px") .style("left",xScale0(b.x1) + "px"); branchOver(b.bId); } /** * @name peakOut * @param {Object} b <Gargantext.Components.PhyloExplorer.Types.Branch> * @param {Int} i * @unpure {Object} d3 * @unpure {Window.Array<Int>} window.branchFocus */ function peakOut (b,i) { d3.select("#peak-" + i).classed("peak-over",false); if (window.branchFocus.includes(b.bId)) { d3.select("#peak-" + i).classed("peak-focus",true); } branchOut(); } /** * @name showPeak * @unpure {Object} d3 */ function showPeak() { var centerColumnCoordinates = getCenterColumnCoordinates(); var centerColumn = d3 .select(CENTER_COLUMN_DOM_QUERY) .node() .getBoundingClientRect(); var xAxis = d3 .select(".x-axis") .node() .getBoundingClientRect(); var xBounds = [ centerColumn.left, centerColumn.left // (?) as we cannot rely on the created "scape" SVG width (which results // may vary on height, due to coordinate matrix ratio preserving), // we have to find the real SVG width via a trick: here we'll relying // on the xAxis + xAxis.width // add to be added due to use of `xAxis.width` trim padding + centerColumnCoordinates.l + centerColumnCoordinates.r ]; var yBounds = [ centerColumn.top, centerColumn.top + centerColumn.height ]; d3 .selectAll(".peak") .style( "fill" , function(peak,i) { var isVisible = d3 .selectAll(".branch-" + i) .nodes() .map(function(g){ var rect = g.getBoundingClientRect(); var x = rect.x, y = rect.y; // Adjustment values empirically managing intersection var dx = // + centerColumnCoordinates.l // - centerColumnCoordinates.r - rect.width var dy = + centerColumnCoordinates.t - centerColumnCoordinates.b - rect.height // Enclosure + Intersection var xLower = x >= (xBounds[0] + dx); var xUpper = x <= xBounds[1]; var yLower = y >= (yBounds[0] + dy); var yUpper = y <= yBounds[1]; return xLower && xUpper && yLower && yUpper; }) .reduce((mem,cur) => {return mem || cur;}) if (isVisible) { d3.select("#peak-shadow" + i).attr("visibility","visible"); return "#0d1824"; } else { d3.select("#peak-shadow" + i).attr("visibility","hidden"); return "#A9A9A9"; } } ); } /** * @name branchOver * @param {Int} bId * @unpure {Window.<String>} window.displayView * @unpure {Object} d3 */ function branchOver(bId) { // headers if (window.displayView === "headingMode") { d3.selectAll(".header").nodes().forEach(function(header){ if (header.getAttribute("bid") == bId) { header.style["font-size"] = "10px"; header.style["opacity"] = 1; } else { header.style["opacity"] = 0.3; } }) } // branches d3.select("#xmark-" + bId).style("fill","#f3be54"); d3.select("#hover-" + bId).style("visibility","visible"); } /** * @name branchOut * @param {Int} bId * @unpure {Object} d3 * @unpure {Window.Array<Int>} window.branchFocus */ function branchOut() { d3.selectAll(".peak-label").style("visibility","hidden"); d3.selectAll(".branch-hover").style("visibility","hidden"); d3.selectAll(".x-mark").style("fill","#4A5C70"); for (var i = 0; i < window.branchFocus.length; i++) { d3.select("#xmark-" + window.branchFocus[i]).style("fill","#F24C3D"); } headerOut(); } /** * @name tickClick * @param {Array} branches of <Gargantext.Components.PhyloExplorer.Types.Branch> * @param {Element} tick * @unpure {Object} d3 * @unpure {Window.Array<Int>} window.branchFocus * @unpure {Object} pubsub */ function tickClick(branches, tick) { let bid = parseInt(tick.getAttribute("bId"), 10), groups = d3.selectAll(".group-inner").filter(".branch-" + bid).nodes(), branch = branches.find(function(item) { return item.bId === bid; }); initPath(); resetSelection(); pubsub.publish(SELECTED_BRANCH_EVENT, branch.label); window.branchFocus.push(bid); drawWordCloud(groups); highlightGroups(groups); d3.selectAll(".path-unfocus").lower(); } /** * @name tickOver * @param {Element} tick * @param {Array} branches of <Gargantext.Components.PhyloExplorer.Types.Branch> * @unpure {Object} d3 */ function tickOver(tick, branches) { var ego = tick.getAttribute("bId"), branch = branches.find(b => b.bId == ego); if (d3.select("#peak-" + ego).node().style.visibility != "hidden") { branchOver(ego); peakOver(branch,ego); } } /** * @name tickOut * @param {Element} tick * @param {Array} branches of <Gargantext.Components.PhyloExplorer.Types.Branch> */ function tickOut(tick, branches) { var ego = tick.getAttribute("bId"), branch = branches.find(b => b.bId == ego); branchOut(); peakOut(branch,ego) } /** * @name onZoom * @param {Event} event * @param {Function<Int>} xScale see https://github.com/d3/d3-scale#_continuous * @param {Function<Int>} yScale see https://github.com/d3/d3-scale#_continuous * @param {Array<Object>} xLabels * <Float> x * <String> label * <Float> inf * <Float> sup * <Int> bId * @param {Array<Object>} yLabels * <Date> from * <Int> label * <Date> to * <Float> y * @param {Array} branches of <Gargantext.Components.PhyloExplorer.Types.Branch> * @param {Object} xAxis instanceof d3.selection * @param {Object} yAxis instanceof d3.selection * @unpure {Object} panel */ function onZoom(event, coordinates, xScale, yScale, xLabels, yLabels, branches, xAxis, yAxis) { var xBounds = coordinatesToXRange(coordinates); var yBounds = coordinatesToYRange(coordinates); var zoomX = event.transform.rescaleX(xScale), zoomY = event.transform.rescaleY(yScale), zoomXLabels = xLabels.filter( function(b) { var lower = zoomX(b.x) >= xBounds[0]; var upper = zoomX(b.x) <= xBounds[1]; return lower && upper; } ), zoomYLabels = yLabels.filter( function(p) { var lower = zoomY(p.y) >= yBounds[0]; var upper = zoomY(p.y) <= yBounds[1]; return lower && upper; } ); setAxisX(zoomX, zoomXLabels, branches, xAxis); setAxisY(zoomY, zoomYLabels, yAxis); panel.selectAll("circle").attr("transform", event.transform); panel.selectAll("text").attr("transform", event.transform); panel.selectAll("path").attr("transform", event.transform); panel.selectAll(".branch-hover").attr("transform", event.transform); panel.selectAll(".y-highlight").attr("transform", event.transform); panel.selectAll(".ngrams").attr("transform", event.transform); panel.selectAll(".term-path").attr("transform", event.transform); panel.selectAll(".emergence").attr("transform", event.transform); panel.selectAll(".header").attr("transform", event.transform); panel.selectAll(".header-wrapper").attr("transform", event.transform); showPeak(); } // function groupOver() { // var from = this.getAttribute("from"); // d3.select("#y-highlight-" + from).style("visibility","visible"); // // d3.select("#y-mark-year-inner-" + from).node().setAttribute("class","y-mark-year-inner-highlight"); // // d3.select("#y-mark-year-outer-" + from).node().setAttribute("class","y-mark-year-outer-highlight"); // // d3.select("#y-label-" + from).node().setAttribute("class","y-label-bold"); // } // function groupOut() { // var from = this.getAttribute("from"); // d3.select("#y-highlight-" + from).style("visibility","hidden"); // // d3.select("#y-mark-year-inner-" + from).node().setAttribute("class","y-mark-year-inner"); // // d3.select("#y-mark-year-outer-" + from).node().setAttribute("class","y-mark-year-outer"); // // d3.select("#y-label-" + from).node().setAttribute("class","y-label"); // } //////////////////////////////////////////////////////////////////////////////// /// EXPORTS //////////////////////////////////////////////////////////////////////////////// /** * @name exportViz */ function exportViz() { var time = new Date(); serialize(svg.node(),"phylomemy-" + Date.parse(time.toString()) + ".svg") } /** * @name serialize * @param {SVGSVGElement} graph * @param {String} name * @unpure {Window} window * @unpure {HTMLDocument} document */ function serialize(graph,name) { const xmlns = "http://www.w3.org/2000/xmlns/"; const xlinkns = "http://www.w3.org/1999/xlink"; const svgns = "http://www.w3.org/2000/svg"; graph = graph.cloneNode(true); const fragment = window.location.href + "#"; const walker = document.createTreeWalker(graph, NodeFilter.SHOW_ELEMENT, null, false); while (walker.nextNode()) { for (const attr of walker.currentNode.attributes) { if (attr.value.includes(fragment)) { attr.value = attr.value.replace(fragment, "#"); } } } graph.setAttributeNS(xmlns, "xmlns", svgns); graph.setAttributeNS(xmlns, "xmlns:xlink", xlinkns); var cssStyleText = getCSSStyles( graph ); appendCSS( cssStyleText, graph ); const serializer = new window.XMLSerializer; const string = serializer.serializeToString(graph); var svgBlob = new Blob([string], {type: "image/svg+xml"}); var svgUrl = URL.createObjectURL(svgBlob); var downloadLink = document.createElement("a"); downloadLink.href = svgUrl; downloadLink.download = name; document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); } /** * @name getCSSStyles * @param {SVGSVGElement} parentElement * @unpure {HTMLDocument} document * @returns {String} */ function getCSSStyles( parentElement ) { var selectorTextArr = []; // Add Parent element Id and Classes to the list selectorTextArr.push( '#'+parentElement.id ); for (var c = 0; c < parentElement.classList.length; c++) if ( !contains('.'+parentElement.classList[c], selectorTextArr) ) selectorTextArr.push( '.'+parentElement.classList[c] ); // Add Children element Ids and Classes to the list var nodes = parentElement.getElementsByTagName("*"); for (var i = 0; i < nodes.length; i++) { var id = nodes[i].id; if ( !contains('#'+id, selectorTextArr) ) selectorTextArr.push( '#'+id ); var classes = nodes[i].classList; for (var c = 0; c < classes.length; c++) if ( !contains('.'+classes[c], selectorTextArr) ) selectorTextArr.push( '.'+classes[c] ); } // Extract CSS Rules var extractedCSSText = ""; for (var i = 0; i < document.styleSheets.length; i++) { var s = document.styleSheets[i]; try { if(!s.cssRules) continue; } catch( e ) { if(e.name !== 'SecurityError') throw e; // for Firefox continue; } var cssRules = s.cssRules; for (var r = 0; r < cssRules.length; r++) { if ( contains( cssRules[r].selectorText, selectorTextArr ) ) extractedCSSText += cssRules[r].cssText; } } return extractedCSSText; } //////////////////////////////////////////////////////////////////////////////// /// WORD CLOUD //////////////////////////////////////////////////////////////////////////////// /** * @name drawWordCloud * @param {Array.<SVGCircleElement>} groups * @unpure {Object} d3 * @unpure {Object} pubsub */ function drawWordCloud (groups) { let col = {}, arr = [], count = 0; groups.forEach(function(g){ let gid = (g.getAttribute("id")).replace("group",""); let terms = d3.selectAll(".term").filter(".g-" + gid).nodes(); terms.forEach(function(t){ count ++; if (col[t.getAttribute("fdt")] == undefined) { col[t.getAttribute("fdt")] = {"freq" : 1, "label" : t.getAttribute("label")} } else { col[t.getAttribute("fdt")].freq = col[t.getAttribute("fdt")].freq + 1 } }) }); arr = (Object.values(col)).map(function(l){ return {"freq":(l.freq / count),"label":l.label}; }).sort(function(l1,l2){ return l2.freq - l1.freq; }); if (arr.length === 0) { return; } var scaler = d3 .scaleLinear() .domain([ Math.log( (arr[ arr.length - 1 ]).freq ), Math.log( (arr[ 0 ]).freq ) ]) .range([ 0, 1 ]); arr.forEach(function(item, idx) { arr[idx].ratio = scaler( Math.log(item.freq) ); }); pubsub.publish(EXTRACTED_TERMS_EVENT, arr); } //////////////////////////////////////////////////////////////////////////////// /// ISO LINE //////////////////////////////////////////////////////////////////////////////// /** * @name drawIsoLine * @param {Array} branches of <Gargantext.Components.PhyloExplorer.Types.Branch> * @unpure {Object} d3 * @unpure {Object} label */ function drawIsoLine(branches) { var coordinates = getIsoLineCoordinates(); var svg = d3 .select(ISO_LINE_DOM_QUERY) .append("svg") .attr("width", coordinates.w) .attr("height", coordinates.h) .append("g"); var centerColumnCoordinates = getCenterColumnCoordinates(); // (?) Iso line width: full width of its div parent, will stretch the page // Iso line real content: same length and x-position as the main scape SVG var xRange = coordinatesToXRange(centerColumnCoordinates); var yRange = coordinatesToYRange(coordinates); xScale0 = d3 .scaleLinear() .domain([ 0, Math.max( ...branches.map(b => b.x1) ) ]) .range(xRange); yScale0 = d3 .scaleLinear() .domain( d3.extent(branches, b => b.y) ) .nice() .range(yRange); var density = d3 .contourDensity() .x(function(b) { return xScale0(b.x1); }) .y(function(b) { return yScale0(b.y); }) .size([ coordinates.w, coordinates.h ]) .thresholds( Math.round(branches.length / 2) ) (branches) /* shadows and lights */ svg .append("g") .selectAll("circle") .data(branches) .enter() .append("circle") .attr("cx", b => xScale0(b.x1)) .attr("cy", b => yScale0(b.y)) .attr("r","55") .attr("id",b => "peak-shadow" + b.bId) .attr("visibility","visible") .style("fill","#FFFFFF"); svg .selectAll("path") .data(density) .enter() .append("path") .attr("d", d3.geoPath()) .attr("fill", "none") .attr("stroke", "#74B5FF") .attr("stroke-width", (d, i) => i % 2 ? 0.25 : 1) .attr("stroke-linejoin", "round"); label = d3 .select(ISO_LINE_DOM_QUERY) .append("div") .attr("class","peak-label"); svg .append("g") .selectAll("text") .data(branches) .enter() .append("text") .attr("x", b => xScale0(b.x1)) .attr("y", b => yScale0(b.y) + 4) .attr("class","peak") .attr("id",b => "peak-" + b.bId) .style("fill","#0d1824") .attr("visibility","visible") .text("▲") .on("mouseover", function(e, b) { peakOver(b, b.bId); }) .on("mouseout", function(e, b) { peakOut(b, b.bId); }) .on("click", function(e, b) { peakClick(b, coordinates); }); } /** * @name getIsoLineCoordinates * @unpure {Object} d3 * @returns {Object} * <Float> x * <Float> y * <Float> w * <Float> h * <Float> t * <Float> r * <Float> b * <Float> l */ function getIsoLineCoordinates() { var el = d3 .select(ISO_LINE_DOM_QUERY) .node() .getBoundingClientRect(); return { x: 0, y: 0, w: el.width, h: el.height, t: 16, r: 12, b: 16, l: 12 }; } //////////////////////////////////////////////////////////////////////////////// /// PHYLO //////////////////////////////////////////////////////////////////////////////// /** * @name drawPhylo * @param {Array} branches of <Gargantext.Components.PhyloExplorer.Types.Branch> * @param {Array} periods of <Gargantext.Components.PhyloExplorer.Types.Period> * @param {Array} groups of <Gargantext.Components.PhyloExplorer.Types.Group> * @param {Array} links of <Gargantext.Components.PhyloExplorer.Types.Link> * @param {Array} aLinks of <Gargantext.Components.PhyloExplorer.Types.AncestorLink> * @param {Array} bLinks of <Gargantext.Components.PhyloExplorer.Types.BranchLink> * @param {Array<Number>} frame * @unpure {Window.<Boolean>} window.weighted * @unpure {Object} d3 * @unpure {Object} svg instanceof d3.selection * @unpure {Object} panel instanceof d3.selection * @unpure {Function} zoom see https://github.com/d3/d3-zoom#zoom */ function drawPhylo(branches, periods, groups, links, aLinks, bLinks, frame) { drawIsoLine(branches); /* *** draw the phylo *** */ var centerColumnCoordinates = getCenterColumnCoordinates(); var scapeCoordinates = getScapeCoordinates(); svg = d3 .select('.phylo-grid__content__scape') .append("svg") .attr("width", scapeCoordinates.w) .attr("height", scapeCoordinates.h) /* labels */ var firstDate = Math.min(...groups.map(g => (g.from).getFullYear())) var yLabels = (periods.map(p => ({y:p.y,from:p.from,to:p.to,label:(p.from).getFullYear()}))).filter(p => p.label >= firstDate); var xLabels = toXLabels(branches,groups,frame[2]); /* weight */ if (window.weighted == true) { // var wInf = Math.min(...groups.map(g => g.weight)) var wSup = Math.max(...groups.map(g => g.weight)) var wScale = d3.scaleLog().domain([1,wSup]).range([3,10]) } /* scales */ var xRange = coordinatesToXRange(centerColumnCoordinates); var yRange = coordinatesToYRange(centerColumnCoordinates); var xScale = d3 .scaleLinear() .domain([ 0, frame[2] ]) .range(xRange); var yScale = d3 .scaleTime() .domain( setYDomain(yLabels) ) .range(yRange); /* mask */ svg .append("defs") .append("svg:clipPath") .attr("id","mask") .append("svg:rect") .attr("width", centerColumnCoordinates.w) .attr("height", centerColumnCoordinates.h) .attr("x", centerColumnCoordinates.x) .attr("y", centerColumnCoordinates.y); /* panel */ panel = svg .append("g") .attr("id", "panel") .attr("clip-path", "url(#mask)") /* highlight */ xLabels.forEach(b => panel.append("rect") .attr("class","branch-hover") .attr("x", xScale(b.inf)) .attr("y", -10000) .attr("width", xScale(b.sup) - xScale(b.inf)) .attr("height", 20000) .attr("id","hover-" + b.bId) .style("visibility","hidden")) yLabels.forEach(l => panel.append("line") .attr("class","y-highlight") .attr("id","y-highlight-" + l.label) .attr("x1", -10000) .attr("y1", yScale(l.from)) .attr("x2", 10000) .attr("y2", yScale(l.from)) .style("visibility","hidden")) /* links */ var linkGen = d3.linkVertical(); var groupLinks = links.map(l => ({source: findGroup(groups, l.from, xScale, yScale), target: findGroup(groups, l.to, xScale, yScale),from: l.from, to: l.to, label: l.label})); var groupAncestors = aLinks.map(l => ({source: findGroup(groups, l.from, xScale, yScale), target: findGroup(groups, l.to, xScale, yScale),from: l.from, to: l.to, label: l.label})); panel .selectAll("path") .data(groupLinks.concat(groupAncestors)) .join("path") .attr("d", linkGen) .attr("fill", "none") .attr("stroke","#0d1824") .attr("class", "group-path") .attr("source",d => d.from) .attr("target",d => d.to) .attr("label", d => d.label) // .on("click", function(){ // // console.log(this) // }) // var colors = ["#F0684D","#aa8c58","#74b5ff","#0d1824"]; /* groups */ groups.forEach(g => setGroup(g, xScale, yScale, wScale)); /* axis */ var xAxis = svg .append("g") .attr("class","x-axis") .attr("transform", "translate(0," + centerColumnCoordinates.t + ")"); var yAxis = svg .append("g") .attr("class","y-axis") .attr("transform", "translate(" + centerColumnCoordinates.x + ",0)"); setAxisX(xScale,xLabels, branches, xAxis); setAxisY(yScale,yLabels, yAxis); /* zoom */ // (!) Debouncing the whole "zoom" + "drag and drop" computation // // Some browser engine are not yet optimized for managing this kind of // heavy computations (especially with no treshold debounce, which can // lead to performance worsen by a factor of 10) // Google engine is also prone to change leading to various effects on // heavy SVG or heavy SVG computation // // Hence the act of debouncing. 20ms seems a sweet spot: // - empirically does not treshold a lot of computation, minimising // potential heavy job freezing ("doherty treshold" UX law) // - imperceptible buffer job ("aesthetic-usability effect" UX law) var debouncedOnZoom = debounce( onZoom, 20 ); zoom = d3 .zoom() .scaleExtent([ 1, 50 ]) .on("zoom", function(e) { debouncedOnZoom( e, centerColumnCoordinates, xScale, yScale, xLabels, yLabels, branches, xAxis, yAxis, xScale, yScale ); }); svg.call(zoom).on("dblclick.zoom",null).on("dblclick",doubleClick); /* role & dynamic */ var emergences = getEmergences(groups, xScale, yScale); var branchByGroup = getBranchByGroup(groups); var keys = Object.keys(emergences); var freqs = (keys.map(k => window.freq[k])).filter(f => f != null); // var fontScale = d3.scaleLinear().domain([0,Math.max(...freqs)]).range([2,10]); var fontScale = d3.scaleLinear().domain([0,Math.sqrt(Math.max(...freqs))]).range([2,20]); var opacityScale = d3.scaleLinear().domain([0,1/Math.sqrt(Math.max(...freqs))]).range([0.1,1]); keys.forEach(function(k){ addEmergenceLabels( k, emergences, branchByGroup, fontScale, opacityScale ); }); /* groups */ d3.selectAll(".header").raise(); } /** * @name getLeftColumnCoordinates * @unpure {Object} d3 * @returns {Object} * <Float> x * <Float> y * <Float> w * <Float> h * <Float> t * <Float> r * <Float> b * <Float> l */ function getLeftColumnCoordinates() { var el = d3 .select(LEFT_COLUMN_DOM_QUERY) .node() .getBoundingClientRect(); return { x: 0, y: 0, w: el.width, h: el.height, t: 16, r: 0, b: 0, l: 0 }; } /** * @name getCenterColumnCoordinates * @unpure {Object} d3 * @returns {Object} * <Float> x * <Float> y * <Float> w * <Float> h * <Float> t * <Float> r * <Float> b * <Float> l */ function getCenterColumnCoordinates() { var leftColumn = getLeftColumnCoordinates(); var elCenterColumn = d3 .select(CENTER_COLUMN_DOM_QUERY) .node() .getBoundingClientRect(); return { x: leftColumn.x + leftColumn.w, y: 32, w: elCenterColumn.width, h: elCenterColumn.height, t: 32, r: 8, b: 0, l: 8 }; } /** * @name getScapeColumnCoordinates * @unpure {Object} d3 * @returns {Object} * <Float> x * <Float> y * <Float> w * <Float> h * <Float> t * <Float> r * <Float> b * <Float> l */ function getScapeCoordinates() { var el = d3 .select(SCAPE_DOM_QUERY) .node() .getBoundingClientRect(); return { x: 0, y: 0, w: el.width, h: el.height, t: 0, r: 0, b: 0, l: 0 }; } /** * @name toXLabels * @param {Array} branches of <Gargantext.Components.PhyloExplorer.Types.Branch> * @param {Array} groups of <Gargantext.Components.PhyloExplorer.Types.Group> * @param {Float} xMax * @returns {Object} * <Int> x * <String> label * <Int> inf * <Int> sup * <Int> bId */ function toXLabels(branches, groups, xMax) { var xLabels = branches.map(function(b) { var bId = b.bId, xs = groups.filter(g => g.bId == bId).map(g => g.x), inf = Math.min(...xs), sup = Math.max(...xs); return { x : b.x2, label : b.label.replace(/\"/g, '').replace(/\|/g, ''), inf : inf, sup : sup, bId : bId}; }) return xLabels.map(function(b,i){ var prec = 0, succ = xMax; if (i != 0) prec = xLabels[i -1].sup if (i != (xLabels.length - 1)) succ = xLabels[i + 1].inf var w = Math.min(...[(b.x - prec) / 2,(succ - b.x) / 2]), inf = b.x - w, sup = b.x + w; inf = (b.inf < inf) ? b.inf : inf + (w / 10); sup = (b.sup > sup) ? b.sup : sup - (w / 10); return { x : (sup + inf) / 2, label : b.label, inf : inf, sup : sup, bId : b.bId}; }) } /** * @name xOverFlow * @param {Object} ticks instanceof d3.selection * @param {Array} arr <Array> * <Float> * <Float> * @unpure {Object} memoTickText * <Int> => <TickText> */ function xOverFlow(ticks,arr) { var average = arr.reduce((a,b) => a + b[0], 0) / arr.length; var delta = 2; ticks.each(function(t,i){ var text = d3.select(this), str = d3.select(this).text(), count = str.length, // y = text.attr("y"), dy = parseFloat(text.attr("dy")), bId = arr[i][1], tspan = text .attr("bId", bId) .text(null) .append("tspan") .attr("x", 0) .attr("y", -14) .attr("dy", dy + "em"); // .attr("bId",""); var idx; var buffer = ''; var node = tspan.node(); var limit = arr[i][0] - delta; // Case a: Memoized pattern if (bId in memoTickText && memoTickText[ bId ].limit === limit) { return tspan.text( memoTickText[ bId ].text ); } // Case b.1: Substractive pattern computation if (limit > average) { idx = count; while (idx > 2) { buffer = str.slice(0, idx) tspan.text( buffer ); if (node.getComputedTextLength() < limit) { break; } idx = Math.floor(idx / 2) } } // Case b.2: Additive pattern computation if (limit <= average) { idx = 2; while (idx <= count) { buffer = str.slice(0, idx); tspan.text( buffer ); if (node.getComputedTextLength() > limit) { break; } idx = Math.ceil(idx * 1.5); } } if (buffer.length !== count) { buffer = buffer.slice(0, -1) + '…'; tspan.text( buffer ); } // Store new value memoTickText[ bId ] = { bId: bId, limit: limit, text: buffer }; }); } /** * @name addMarkX * @param {Object} ticks instanceof d3.selection * @param {Array} ws <Float> * @param {Array} ids <Int> * @unpure {Object} d3 * @unpure {Window.Array.<Int>} window.branchFocus */ function addMarkX(ticks,ws,ids) { ticks.each(function(t,i){ d3.select(this) .append("rect") .attr("x","-" + (ws[i]/2 + 1)) .attr("y","-4") .attr("height","8") .attr("width",ws[i] + 1) .attr("class","x-mark") .attr("id", "xmark-" + ids[i]) if (window.branchFocus.includes(ids[i])) { d3.select("#xmark-" + ids[i]).style("fill","#F0684D"); } }) } /** * @name setMarkYLabel * @param {Object} labels instanceof d3.selection * @unpure {Object} d3 */ function setMarkYLabel(labels) { labels.each(function(l,i){ d3.select(this).attr("dx","-5").attr("class","y-label").attr("id","y-label-" + d3.timeYear(l).getFullYear()); }) } /** * @name addMarkY * @param {Object} ticks instanceof d3.selection * @unpure {Object} d3 */ function addMarkY(ticks) { ticks.each(function(d,i){ if (d3.timeYear(d) < d) { // month d3.select(this) .append("circle").attr("cx",0).attr("cy",0).attr("r",3).attr("class","y-mark-month"); } else { var from = d3.timeYear(d).getFullYear(); // year d3.select(this) .append("circle").attr("cx",0).attr("cy",0).attr("r",6).attr("class","y-mark-year-outer").attr("id","y-mark-year-outer-" + from); d3.select(this) .append("circle").attr("cx",0).attr("cy",0).attr("r",3).attr("class","y-mark-year-inner").attr("id","y-mark-year-inner-" + from); } }) } /** * @name setYDomain * @param {Object} labels * <Date> from * <String> label * <Date> to * <Int> y * @returns {Array} * <Date> * <Date> */ function setYDomain(labels) { var ts = ["week","month","day","year","epoch"]; //console.log(labels) if (ts.includes(window.timeScale)) { labels = labels.sort(function(d1,d2){return d1.from - d2.from;}) } var inf = (labels[0]).from, sup = (labels[labels.length - 1]).to; if (window.timeScale == "week") { inf = addDays(inf,7) sup = addDays(sup,7) } else if (window.timeScale == "month") { inf = removeDays(inf,31) sup = addDays(sup,31) } else if (window.timeScale == "day") { inf = removeDays(inf,1) sup = addDays(sup,1) } else if (window.timeScale == "year") { inf = removeDays(inf,365) sup = addDays(sup,365) } else if (window.timeScale == "epoch") { inf = inf sup = sup } else { inf = new Date((inf.getFullYear() - 1),0,0); sup = new Date((sup.getFullYear() + 1),0,0); } // inf = new Date((inf - 1),6,0); // inf = new Date((1950 - 1),6,0); // sup = new Date((sup + 1),0,0); return [inf,sup]; } /** * @name findGroup * @param {Array} groups of <Gargantext.Components.PhyloExplorer.Types.Group> * @param {Int} id * @param {Function<Int>} xsc see https://github.com/d3/d3-scale#_continuous * @param {Function<Int>} ysc see https://github.com/d3/d3-scale#_continuous * @returns {Array} * <Float> * <Float> */ function findGroup (groups, id, xsc, ysc) { var group = groups.find(g => g.gId == id); var x = xsc(group.x); var y = ysc(group.to); return [x,y] } /** * @name coordinatesToBox * @param {Object} coordinates * <Float> x * <Float> y * <Float> w * <Float> h * @returns {Array<Float>} */ function coordinatesToBox(coordinates) { return [ coordinates.x, coordinates.y, coordinates.w, coordinates.h ]; } /** * @name coordinatesToXRange * @param {Object} coordinates * <Float> x * <Float> w * <Float> r * <Float> l * @returns {Array<Float>} */ function coordinatesToXRange(coordinates) { var lower = coordinates.x + coordinates.r + coordinates.l; var upper = coordinates.x - coordinates.r - coordinates.l + coordinates.w; return [lower, upper]; } /** * @name coordinatesToYRange * @param {Object} coordinates * <Float> y * <Float> h * <Float> t * <Float> b * @returns {Array<Float>} */ function coordinatesToYRange(coordinates) { var lower = coordinates.y + coordinates.t + coordinates.b; var upper = coordinates.y - coordinates.t - coordinates.b + coordinates.h; return [lower, upper]; } /** * @name textWidth * @param {String} text * @returns {CanvasRenderingContext2D} */ function textWidth(text) { const context = document.createElement("canvas").getContext("2d"); return context.measureText(text).width; } /** * @name toLines * @param {String} words * @param {Array<Int>} fdt * @param {Array<Int>} role * @param {Float} targetWidth * @returns {Array<Object>} * <Float> width * <Array<String>> text * <Array<Int>> fdt * <Array<Int>> role */ function toLines(words,fdt,role,targetWidth) { let line; let lineWidth0 = Infinity; const lines = []; for (let i = 0, n = words.length; i < n; ++i) { let lineText1 = (line ? line.text + " " : "") + words[i]; // let lineFdt1 = (line ? line.fdt + " " : "") + fdt[i]; // let lineRole1 = (line ? line.role + " " : "") + role[i]; let lineWidth1 = textWidth(lineText1); if ((lineWidth0 + lineWidth1) / 2 < targetWidth + 10) { line.width = lineWidth0 = lineWidth1; // line.text = lineText1; line.text.push(words[i]) line.fdt.push(fdt[i]) line.role.push(role[i]) } else { lineWidth0 = textWidth(words[i]); line = {width: lineWidth0, text: [words[i]], fdt: [fdt[i]], role: [role[i]]}; lines.push(line); } } return lines; } /** * @name toTextRadius * @param {Array<Object>} * <Float> width * <Array<String>> text * <Array<Int>> fdt * <Array<Int>> role * @param {Int} lineHeight * @returns {Float} */ function toTextRadius(lines,lineHeight) { let radius = 0; for (let i = 0, n = lines.length; i < n; ++i) { const dy = (Math.abs(i - n / 2 + 0.5) + 2) * lineHeight; const dx = lines[i].width / 2; const sdy = Math.pow(dy, 2); const sdx = Math.pow(dx, 2); radius = Math.max(radius, Math.sqrt(sdx + sdy)); } return radius; } /** * @name findFreq * @param {Int} fdt * @unpure {Window.Array<Int>} window.freq * @returns {Int} */ function findFreq(fdt) { let freq = 0; if (window.freq[fdt] != null) { freq = window.freq[fdt] } return freq; } /** * @name findRole * @param {Int} r * @returns {String} */ function findRole(r) { if (r == 0) { return " emerging"; } else if (r == 2) { return " decreasing"; } else { return ""; } } /** * @name mergeLists * @param {Array<String>} l1 * @param {Array<Int>} l2 * @param {Array<Int>} l3 * @returns {Array<Array>} * <String> * <Int> * <Int> */ function mergeLists(l1,l2,l3) { let merged = []; for (let i = 0; i < l1.length; i++) { merged.push([l1[i],l2[i],l3[i]]) } return merged; } /** * @name setAxisX * @param {Function<Int>} scale see https://github.com/d3/d3-scale#_continuous * @param {Array<Object>} labels * <Float> x * <String> label * <Float> inf * <Float> sup * <Int> bId * @param {Array} branches of <Gargantext.Components.PhyloExplorer.Types.Branch> * @param {Object} xAxis instanceof d3.selection * @unpure {Object} d3 */ function setAxisX(scale, labels, branches, xAxis) { xAxis.call(d3.axisTop(scale) .tickValues(labels.map(l => l.x)) .tickFormat((l, i) => labels[i].label) .tickSizeOuter(0)); xAxis.selectAll(".tick text") .call(xOverFlow, labels.map(l => [scale(l.sup) - scale(l.inf),l.bId])) .on("mouseover", function() { tickOver(this, branches); }) .on("click", function() { tickClick(branches, this); }) .on("mouseout" , function() { tickOut(this, branches); }); xAxis.selectAll(".tick line").remove(); xAxis.selectAll(".tick rect").remove(); xAxis.selectAll(".tick") .call(addMarkX, labels.map(l => scale(l.sup) - scale(l.inf)),labels.map(l => l.bId)); } /** * @name setAxisY * @param {Function<Int>} scale see https://github.com/d3/d3-scale#_continuous * @param {Array<Object>} labels * <Date> from * <Int> label * <Date> to * <Float> y * @param {Object} yAxis instanceof d3.selection * @unpure {Object} d3 */ function setAxisY(scale,labels, yAxis) { yAxis.call(d3.axisLeft(scale) .tickFormat(function(d){ if (d3.timeYear(d) < d) { // '%B' return d3.timeFormat('%d %B')(d); } else { return d3.timeFormat('%Y')(d); } }) .tickSizeOuter(0)); yAxis.selectAll(".tick line").remove(); yAxis.selectAll(".tick circle").remove(); yAxis.selectAll(".tick") .call(addMarkY) yAxis.selectAll(".tick text") .call(setMarkYLabel) } /** * @name setGroupClass * @param {Object} g <Gargantext.Components.PhyloExplorer.Types.Group> * @returns {String} */ function setGroupClass(g) { var str = "group-inner" + " " + "branch-" + g.bId; for (var i = 0; i < g.source.length; i++) { str += " source-" + g.source[i]; } return str; } /** * @name setGroup * @param {Object} g <Gargantext.Components.PhyloExplorer.Types.Group> * @param {Function<Int>} xScale see https://github.com/d3/d3-scale#_continuous * @param {Function<Int>} yScale see https://github.com/d3/d3-scale#_continuous * @param {Function<Int>} wScale see https://github.com/d3/d3-scale#_continuous * @unpure {Window.<Boolean>} window.weighted * @unpure {Object} d3 */ function setGroup(g, xScale, yScale, wScale) { // console.log(window.weighted) if(window.weighted == true) { var radius = wScale(g.weight) } else { var radius = 5; } // var radius = 5; // var col = Math.round(Math.random() * 3) - 1 panel .append("circle") .attr("class","group-outer") .attr("cx", xScale(g.x)) .attr("cy", yScale(g.to)) .attr("r" , radius + 0.5); panel .append("circle") .attr("class", setGroupClass(g)) .attr("cx", xScale(g.x)) .attr("cy", yScale(g.to)) .attr("bId", g.bId) .attr("id" , "group" + g.gId) .attr("gid" , g.gId) .attr("r" ,radius) // .attr("stroke",colors[col]) .attr("stroke","#0d1824") .style("fill", "#61a3a9") .attr("from",(g.to).getFullYear()) // .on("mouseover",groupOver) // .on("mouseout" ,groupOut) /* group label */ var lineHeight = 12, targetWidth = Math.sqrt(textWidth(g.label.join('').trim()) * radius), lines = toLines(g.label,g.foundation,g.role,targetWidth), textRadius = toTextRadius(lines,lineHeight), textRatio = (radius - 0.5) / textRadius; for (let i = 0; i < lines.length; i++) { let words = lines[i].text, fdt = lines[i].fdt, roles = lines[i].role, terms = mergeLists(words,fdt,roles), toSpan = (acc, w) => acc + "<tspan fdt=" + w[1] + " class='term fdt-" + w[1] + " " + "g-" + g.gId + findRole(w[2]) + "'" + " gy=" + yScale(g.to) + " gx=" + xScale(g.x) + " freq=" + findFreq(w[1]) + " label='" + w[0] + "'" + " gid=" + g.gId + " bid=" + g.bId + " from=" + (g.to).getFullYear() + ">" + w[0] + "</tspan>"; panel .append("text") .attr("class","ngrams") .attr("text-anchor", "middle") .style("font-size", 12 * textRatio + "px") .style("visibility", "hidden") .append("tspan") .attr("x", xScale(g.x)) .attr("y", yScale(g.to) + (i - lines.length / 2.8) * (lineHeight * textRatio)) .html(terms.reduce(toSpan,"")); d3.selectAll(".term") .on("click",function(){ termClick(this.textContent,this.getAttribute("fdt"),this.getAttribute("gid"),"group"); }) // .on("mouseover",function(){ // d3.selectAll(".term").classed("term-unfocus",true); // d3.selectAll(".term").filter(".g-" + this.getAttribute("gid")).classed("term-focus",true); // }) // .on("mouseout",function(){ // d3.selectAll(".term").classed("term-unfocus",false); // d3.selectAll(".term").classed("term-focus",false); // }); } } /** * @name getBranchByGroup * @param {Array} groups of <Gargantext.Components.PhyloExplorer.Types.Group> * @returns {Array<Array<Int>>} */ function getBranchByGroup(groups) { var branchByGroup = {}; groups.forEach(function(g) { // is a term in many branches ? for (var i = 0; i < (g.foundation).length; i++) { var fdt = (g.foundation)[i]; if (fdt in branchByGroup) { (branchByGroup[fdt]).push(g.bId); } else { branchByGroup[fdt] = [g.bId]; } } }); return branchByGroup; } /** * @name getEmergences * @param {Array} groups of <Gargantext.Components.PhyloExplorer.Types.Group> * @param {Function<Int>} xScale see https://github.com/d3/d3-scale#_continuous * @param {Function<Int>} yScale see https://github.com/d3/d3-scale#_continuous * @returns {Object} * <Int> => <Object> * <Int> bId * <String> label * <Array<Float>> x * <Array<Float>> y */ function getEmergences(groups, xScale, yScale) { var emergences = {}; groups.forEach(function(g) { // is emerging ? if ((g.role).includes(0)) { for (var i = 0; i < (g.role).length; i++) { if ((g.role)[i] == 0) { var gf = (g.foundation)[i]; if (gf in emergences) { (emergences[gf].x).push(xScale(g.x)); (emergences[gf].y).push(yScale(g.to)); } else { emergences[gf] = {"label":g.label[i],"x":[xScale(g.x)],"y":[yScale(g.to)],"bid":g.bId} } } } } }); return emergences; } /** * @name addEmergenceLabels * @param {String} k * @param {Object} emergences * <Int> => <Object> * <Int> bId * <String> label * <Array<Float>> x * <Array<Float>> y * @param {Array<Array<Int>>} branchByGroup * @param {Function<Int>} fontScale see https://github.com/d3/d3-scale#_continuous * @param {Function<Int>} opacityScale see https://github.com/d3/d3-scale#_continuous * @unpure {Window.<Array<Int>>} window.freq * @unpure {Object} panel instanceof d3.selection * @unpure {Object} pubsub */ function addEmergenceLabels(k, emergences, branchByGroup, fontScale, opacityScale){ let x = ((emergences[k]).x).reduce(arraySum) / ((emergences[k]).x).length; let y = ((emergences[k]).y).reduce(arraySum) / ((emergences[k]).y).length; var freq = 0; if (k in window.freq) { freq = window.freq[k]; } var xr = x + (rdm() * Math.random() * 10); var yr = y + (rdm() * Math.random() * 10); // add header label text panel .append("text") .attr("x", xr) .attr("y", yr) .attr("fdt",k) .attr("id","head" + k) .attr("mem-size", fontScale(Math.sqrt(freq))) .attr("mem-opac", opacityScale(Math.sqrt(freq))) .attr("bid",(emergences[k]).bid) .style("font-size", fontScale(Math.sqrt(freq)) + "px") .style("opacity", opacityScale(1/Math.sqrt(freq))) .attr("class","header") .style("text-anchor", "middle") // .style("fill",(bid.length > 1) ? "#012840" : "#CC382F") .text((emergences[k]).label) .on("mouseover", function() { var wrapper = d3.select("#wrapper-" + this.id); // show header wrapper wrapper.classed("header-wrapper--hover", true); // little tweak to put elements into the foreground (~zIndex top) panel.node().appendChild( wrapper.node() ); panel.node().appendChild( this ); }) .on("mouseout", function() { // hide header wrapper d3 .select("#wrapper-" + this.id) .classed("header-wrapper--hover", false); }) .on("click",function(){ showLabel(); termClick((emergences[k]).label,k,k,"head"); pubsub.publish(DISPLAY_VIEW_EVENT, "labelMode"); // remove wrapper header (preventing little issue where wrapper is still // displayed because "mouseout" event has been disrupted by "click" event) d3 .select("#wrapper-" + this.id) .classed("header-wrapper--hover", false); }); // add header wrapper surrounding text // (based on its text width and height) var bbox = d3 .select("#head" + k) .node() .getBoundingClientRect(); // (?) Empirical proportional padding value (due to text size diffences) var padding = { t: 0, r: bbox.width * 0.05, b: bbox.height * 0.4, l: bbox.width * 0.05 }; var w3 = + bbox.width + padding.l + padding.r; var h3 = + bbox.height + padding.t + padding.b; var x3 = + xr - (bbox.width / 2) - padding.l; var y3 = + yr - bbox.height - padding.t; panel .append("rect", "text") .attr("x", x3) .attr("y", y3) .attr("width", w3) .attr("height", h3) .attr("class", "header-wrapper") .attr("id", "wrapper-head" + k) } //////////////////////////////////////////////////////////////////////////////// /// EXPORTS //////////////////////////////////////////////////////////////////////////////// export const _extractedTermsEvent = EXTRACTED_TERMS_EVENT; export const _extractedCountEvent = EXTRACTED_COUNT_EVENT; export const _selectedTermEvent = SELECTED_TERM_EVENT; export const _selectedBranchEvent = SELECTED_BRANCH_EVENT; export const _selectedSourceEvent = SELECTED_SOURCE_EVENT; export const _displayViewEvent = DISPLAY_VIEW_EVENT; let pubsubPublish = pubsub.publish; let pubsubSubscribe = pubsub.subscribe; let pubsubUnsubscribe = pubsub.unsubscribe; export { drawPhylo as _drawPhylo, drawWordCloud as _drawWordCloud, showLabel as _showLabel, termClick as _termClick, resetView as _resetView, showHeading as _showHeading, showLanding as _showLanding, exportViz as _exportViz, doubleClick as _doubleClick, highlightGroups as _highlightGroups, initPath as _initPath, pubsubPublish as _publish, pubsubSubscribe as _subscribe, pubsubUnsubscribe as _unsubscribe };