Instantly share code, notes, and snippets.
Created
March 23, 2019 20:15
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save john-guerra/db5937cf91d76ce14368ed380ad96d76 to your computer and use it in GitHub Desktop.
navio.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// https://github.com/john-guerra/Navio#readme v0.0.36 Copyright 2019 John Alexis Guerra Gómez | |
(function (global, factory) { | |
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('d3'), require('d3-scale-chromatic'), require('popper.js')) : | |
typeof define === 'function' && define.amd ? define(['d3', 'd3-scale-chromatic', 'popper.js'], factory) : | |
(global.navio = factory(global.d3,global.d3ScaleChromatic,global.Popper)); | |
}(this, (function (d3,d3ScaleChromatic,Popper) { 'use strict'; | |
Popper = Popper && Popper.hasOwnProperty('default') ? Popper['default'] : Popper; | |
class FilterByRange { | |
constructor(opts ) { | |
this.first = opts.first; | |
this.last = opts.last; | |
this.level = opts.level; | |
} | |
filter(d) { | |
return d.__i[this.level] >= this.first.__i[this.level] && d.__i[this.level] <= this.last.__i[this.level]; | |
} | |
} | |
class FilterByValue { | |
constructor(opts ) { | |
this.itemAttr = opts.itemAttr; | |
this.sel = opts.sel; | |
} | |
filter(d) { | |
return d[this.itemAttr] === this.sel[this.itemAttr]; | |
} | |
} | |
// A fake scale that uses only the first digits of a text to compute the color. | |
// Creates a list of all the possible first digits and uses a sequential scale to color based on such index | |
function scaleText(digits = 1) { | |
const defaultColorInterpolator = "interpolateGreys" in d3 ? d3.interpolateGreys : d3ScaleChromatic.interpolateGreys; // Hack to keep it working with d3.v4 | |
let scale = d3.scaleSequential(defaultColorInterpolator).domain([32, 90]), // initialize with ascii | |
dRepresentativesCounts = d3.map(), // Contains the counts for each letter/substrg | |
dRepresentativesIndexes = d3.map(); | |
// Computes the actual value, based on the index of the first digits in the domain | |
function compute(d) { | |
if (typeof(d)!==typeof("")) return "white"; | |
let ci = dRepresentativesIndexes.get( | |
d.slice(0, digits) | |
); | |
if (ci === undefined) { | |
console.log( | |
`scaleText Couldn't find index for ${d} did you call domain? Using ascii of first letter` | |
); | |
ci = d | |
.slice(0, digits) | |
.charCodeAt(0); | |
} | |
return scale(ci) || "white"; | |
} | |
function computeRepresentatives(data, doIndex = true) { | |
dRepresentativesCounts = d3.map(); | |
for (let v of data) { | |
//Initialize | |
if (!dRepresentativesCounts.has(v)) dRepresentativesCounts.set(v, 0); | |
//count+=1 | |
dRepresentativesCounts.set(v, dRepresentativesCounts.get(v) + 1); | |
} | |
const ret = { | |
counts: dRepresentativesCounts | |
}; | |
if (doIndex) { | |
// Compute the indexes of each representative | |
dRepresentativesIndexes = d3.map(); | |
let i = 0; | |
for (let r of dRepresentativesCounts.keys().sort()) { | |
dRepresentativesIndexes.set(r, i++); | |
} | |
ret.indexes = dRepresentativesIndexes; | |
} | |
return ret; | |
} | |
compute.digits = function(_) { | |
return arguments.length ? ((digits = _), compute) : digits; | |
}; | |
compute.scale = function(_) { | |
return arguments.length ? ((scale = _), compute) : scale; | |
}; | |
compute.domain = function(data) { | |
if (arguments.length) { | |
// Compute representatives for letters/substrings | |
computeRepresentatives( | |
data | |
.filter(d => typeof(d) === typeof("")) | |
.map(d => d.slice(0, digits)) | |
); | |
scale.domain([0, dRepresentativesCounts.keys().length]); | |
return compute; | |
} else { | |
return scale.domain(); | |
} | |
}; | |
compute.computeRepresentatives = computeRepresentatives; | |
compute.__type = "text"; | |
return compute; | |
} | |
// import * as d3 from "../node_modules/d3/build/d3.js"; // Force react to use the es6 module | |
//eleId must be the ID of a context element where everything is going to be drawn | |
function navio(selection, _h) { | |
let nv = this || {}, | |
data = [], //Contains the original data attributes | |
dataIs = [], //Contains only the indices to the data, is an array of arrays, one for each level | |
links = [], | |
visibleLinks = [], | |
dData = d3.map(), // A hash for the data | |
dDimensions = d3.map(), | |
dimensionsOrder = [], | |
dSortBy = [], //contains which attribute to sort by on each column | |
dBrushes = [], | |
filtersByLevel = [], // The filters applied to each level | |
yScales =[], | |
xScale, | |
x, | |
height = _h!==undefined ? _h : 600, | |
colScales = d3.map(), | |
levelScale, | |
svg, | |
canvas, | |
context, | |
tooltip, | |
tooltipElement, | |
tooltipCoords = { x: -50, y: -50}, | |
defaultColorInterpolator = "interpolateBlues" in d3 ? d3.interpolateBlues : d3ScaleChromatic.interpolateBlues, // necessary for supporting d3v4 and d3v5 | |
defaultColorInterpolatorDate = "interpolatePurples" in d3 ? d3.interpolatePurples : d3ScaleChromatic.interpolatePurples, | |
defaultColorInterpolatorDiverging = "interpolateBrBG" in d3 ? d3.interpolateBrBG : d3ScaleChromatic.interpolateBrBG, | |
defaultBooleanColorRange = ["#a1d76a", "#e9a3c9", "white"], //true false null | |
visibleColorRange = ["white", "#b5cf6b"], | |
fmt = d3.format(",.0d"), | |
id = "__seqId", | |
updateCallback = function () {}; | |
// Default parameters | |
nv.x0=0; | |
nv.y0=100; | |
nv.maxNumDistictForCategorical = 10; | |
nv.howManyItemsShouldSearchForNotNull = 100; | |
nv.margin = 10; | |
nv.showAttribTitles = true; | |
nv.attribWidth = 15; | |
nv.attribRotation = -45; | |
nv.attribFontSize = 13; | |
nv.attribFontSizeSelected = 32; | |
nv.levelsSeparation = 40; | |
nv.divisionsColor = "white"; | |
nv.levelConnectionsColor = "rgba(205, 220, 163, 0.5)"; | |
nv.divisionsThreshold = 4; // What's the minimum row width needed to draw divisions | |
nv.legendFont = "14px sans-serif"; | |
nv.linkColor = "#ccc"; | |
nv.tooltipFontSize = 12; | |
nv.tooltipBgColor = "#b2ddf1"; | |
nv.tooltipMargin = 50; | |
nv.tooltipArrowSize = 10; | |
function nozoom() { | |
console.log("nozoom"); | |
d3.event.preventDefault(); | |
} | |
function initTooltipPopper() { | |
if (tooltipElement) tooltipElement.remove(); | |
tooltipElement = selection | |
.append("div") | |
.attr("class", "_nv_popover") | |
// .style("text-shadow", "0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff") | |
.style("pointer-events", "none") | |
.style("font-family", "sans-serif") | |
.style("font-size", nv.tooltipFontSize) | |
.style("text-align", "center") | |
.style("background", nv.tooltipBgColor) | |
.style("position", "relative") | |
.style("color", "black") | |
.style("z-index", 4) | |
.style("border-radius", "4px") | |
.style("box-shadow", "0 0 2px rgba(0,0,0,0.5)") | |
.style("padding", "10px") | |
.style("text-align", "center") | |
.style("display", "none"); | |
tooltipElement | |
.append("style") | |
.attr("scoped", "") | |
.text(` | |
[x-arrow] { | |
width: 0; | |
height: 0; | |
border-style: solid; | |
position: absolute; | |
margin: ${nv.tooltipArrowSize}px; | |
border-color: ${nv.tooltipBgColor} | |
} | |
._nv_popover[x-placement="left"] { | |
margin-right: ${nv.tooltipArrowSize + nv.tooltipMargin}px; | |
} | |
._nv_popover[x-placement="left"] [x-arrow] { | |
border-width: ${nv.tooltipArrowSize}px 0 ${nv.tooltipArrowSize}px ${nv.tooltipArrowSize}px; | |
border-top-color: transparent; | |
border-right-color: transparent; | |
border-bottom-color: transparent; | |
right: -${nv.tooltipArrowSize}px; | |
top: calc(50% - ${nv.tooltipArrowSize}px); | |
margin-left: 0; | |
margin-right: 0; | |
} | |
._nv_popover[x-placement="right"] { | |
margin-left: ${nv.tooltipArrowSize + nv.tooltipMargin}px; | |
} | |
._nv_popover[x-placement="right"] [x-arrow] { | |
border-width: ${nv.tooltipArrowSize}px ${nv.tooltipArrowSize}px ${nv.tooltipArrowSize}px 0; | |
border-left-color: transparent; | |
border-top-color: transparent; | |
border-bottom-color: transparent; | |
left: -${nv.tooltipArrowSize}px; | |
top: calc(50% - ${nv.tooltipArrowSize}px); | |
margin-left: 0; | |
margin-right: 0; | |
} | |
._nv_popover[x-placement="bottom"] { | |
margin-top: ${nv.tooltipArrowSize + nv.tooltipMargin}px; | |
} | |
._nv_popover[x-placement="bottom"] [x-arrow] { | |
border-width: 0 ${nv.tooltipArrowSize}px ${nv.tooltipArrowSize}px ${nv.tooltipArrowSize}px; | |
border-left-color: transparent; | |
border-right-color: transparent; | |
border-top-color: transparent; | |
top: -${nv.tooltipArrowSize}px; | |
left: calc(50% - ${nv.tooltipArrowSize}px); | |
margin-top: 0; | |
margin-bottom: 0; | |
} | |
._nv_popover[x-placement="top"] { | |
margin-bottom: ${nv.tooltipArrowSize + nv.tooltipMargin}px; | |
} | |
._nv_popover[x-placement="top"] [x-arrow] { | |
border-width: ${nv.tooltipArrowSize}px ${nv.tooltipArrowSize}px 0 ${nv.tooltipArrowSize}px; | |
border-left-color: transparent; | |
border-right-color: transparent; | |
border-bottom-color: transparent; | |
bottom: -${nv.tooltipArrowSize}px; | |
left: calc(50% - ${nv.tooltipArrowSize}px); | |
margin-top: 0; | |
margin-bottom: 0; | |
} | |
`); | |
tooltipElement | |
.append("div") | |
.attr("class", "tool_id"); | |
tooltipElement | |
.append("div") | |
.attr("class", "tool_value_name") | |
.style("font-weight", "bold") | |
.style("font-size", "120%"); | |
tooltipElement | |
.append("div") | |
.attr("class", "tool_value_val") | |
.style("max-width", "400px") | |
.style("max-height", "5.5em") | |
.style("text-align", "left") | |
.style("overflow", "hidden") | |
.style("font-size", "90%"); | |
tooltipElement | |
.append("div") | |
.attr("x-arrow", ""); | |
const ref= { | |
getBoundingClientRect: () => { | |
const svgBR = svg.node().getBoundingClientRect(); | |
return { | |
top: tooltipCoords.y + svgBR.top, | |
right: tooltipCoords.x + svgBR.left, | |
bottom: tooltipCoords.y + svgBR.top, | |
left: tooltipCoords.x + svgBR.left, | |
width: 0, | |
height: 0, | |
}; | |
}, | |
clientWidth: 0, | |
clientHeight: 0, | |
}; | |
// const ref= { | |
// getBoundingClientRect: () => { | |
// return { | |
// top: tooltipCoords.y, | |
// right: tooltipCoords.x, | |
// bottom: tooltipCoords.y, | |
// left: tooltipCoords.x, | |
// width: 0, | |
// height: 0, | |
// }; | |
// }, | |
// clientWidth: 0, | |
// clientHeight: 0, | |
// }; | |
tooltip = new Popper(ref, | |
tooltipElement.node(), | |
{ | |
placement: "auto", | |
// modifiers: { | |
// preventOverflow: { | |
// boundariesElement: selection.node(), | |
// }, | |
// }, | |
}); | |
} | |
function init() { | |
// Try to support strings and elements | |
selection = typeof(selection) === typeof("") ? d3.select(selection) : selection; | |
selection = selection.selectAll === undefined ? d3.select(selection) : selection; | |
selection.selectAll("*").remove(); | |
const divNavio = selection | |
.on("touchstart", nozoom) | |
.on("touchmove", nozoom) | |
.style("height", height + "px") | |
.attr("class", "navio") | |
.append("div") | |
.style("position", "relative"); | |
divNavio | |
.append("canvas"); | |
svg = divNavio | |
.append("svg") | |
.style("overflow", "visible") | |
.style("position", "absolute") | |
.style("z-index", 3) | |
.style("top", 0) | |
.style("left", 0); | |
svg.append("g") | |
.attr("class", "attribs"); | |
initTooltipPopper(); | |
svg.append("g") | |
.attr("id", "closeButton") | |
.style("fill", "white") | |
.style("stroke", "black") | |
.style("display", "none") | |
.append("path") | |
.call(function (sel) { | |
var crossSize = 7, | |
path = d3.path(); // Draw a cross and a circle | |
path.moveTo(0, 0); | |
path.lineTo(crossSize, crossSize); | |
path.moveTo(crossSize, 0); | |
path.lineTo(0, crossSize); | |
path.moveTo(crossSize*1.2 + crossSize/2, crossSize/2); | |
path.arc(crossSize/2, crossSize/2, crossSize*1.2, 0, Math.PI*2); | |
sel.attr("d", path.toString()); | |
}) | |
.on("click", deleteOneLevel); | |
xScale = d3.scaleBand() | |
// .rangeBands([0, nv.attribWidth], 0.1, 0); | |
.range([0, nv.attribWidth]) | |
.round(true) | |
.paddingInner(0.1) | |
.paddingOuter(0); | |
levelScale = d3.scaleBand() | |
.round(true); | |
colScales = d3.map(); | |
x = function (val, level) { | |
return levelScale(level) + xScale(val); | |
}; | |
canvas = selection.select("canvas").node(); | |
// canvas.style.position = "absolute"; | |
canvas.style.top = canvas.offsetTop + "px"; | |
canvas.style.left = canvas.offsetLeft + "px"; | |
// canvas.style.width = "150px"; | |
canvas.style.height = height + "px"; | |
const scale = window.devicePixelRatio; | |
// canvas.width = width * scale; | |
canvas.height = height * scale; | |
context = canvas.getContext("2d"); | |
context.scale(scale,scale); | |
context.imageSmoothingEnabled = context.mozImageSmoothingEnabled = context.webkitImageSmoothingEnabled = false; | |
context.globalCompositeOperation = "source-over"; | |
} | |
function showLoading(ele) { | |
d3.select(ele).style("cursor", "progress"); | |
svg.style("cursor", "progress"); | |
} | |
function hideLoading(ele) { | |
d3.select(ele).style("cursor", null); | |
svg.style("cursor", null); | |
} | |
function deferEvent(cbk) { | |
return function(d, i, all) { | |
showLoading(this); | |
setTimeout(() => { | |
cbk(d, i, all); | |
hideLoading(this); | |
},100); | |
}; | |
} | |
function invertOrdinalScale(scale, x) { | |
// Taken from https://bl.ocks.org/shimizu/808e0f5cadb6a63f28bb00082dc8fe3f | |
// custom invert function | |
var domain = scale.domain(); | |
var range = scale.range(); | |
var qScale = d3.scaleQuantize().domain(range).range(domain); | |
return qScale(x); | |
} | |
// Like d3.ascending but supporting null | |
function d3AscendingNull(a, b) { | |
if (b === null || b === undefined) { | |
if (a === null || a === undefined) return 0; // a == b == null | |
else return 1; // b==null a!=null | |
} else { // b!=null | |
if (a === null || a === undefined) return -1; | |
else if (a < b) return -1; | |
else if (a > b) return 1; | |
else if (a >= b) return 0; | |
else return NaN; | |
} | |
} | |
function d3DescendingNull(a, b) { | |
if (b === null || b === undefined) { | |
if (a === null || a === undefined) return 0; // a == b == null | |
else return -1; // b==null a!=null | |
} else { // b!=null | |
if (a === null || a === undefined) return 1; | |
else if (a < b) return 1; | |
else if (a > b) return -1; | |
else if (a >= b) return 0; | |
else return NaN; | |
} | |
} | |
function updateSorting(levelToUpdate) { | |
if (!dSortBy.hasOwnProperty(levelToUpdate)) { | |
console.log("UpdateSorting called without attrib in dSortBy", levelToUpdate, dSortBy); | |
return; | |
} | |
var before = performance.now(); | |
const sort = dSortBy[levelToUpdate]; | |
dataIs[levelToUpdate].sort(function (a, b) { | |
return sort.desc ? | |
d3DescendingNull(data[a][sort.attrib], data[b][sort.attrib]) : | |
d3AscendingNull(data[a][sort.attrib], data[b][sort.attrib]); | |
}); | |
// dataIs[levelToUpdate].forEach(function (row,i) { | |
// data[row].__i[levelToUpdate] = i; | |
// }); | |
assignIndexes(dataIs[levelToUpdate], levelToUpdate); | |
var after = performance.now(); | |
console.log("Sorting level " + levelToUpdate + " " + (after-before) + "ms"); | |
} | |
function onSortLevel(d) { | |
if (d3.event && d3.event.defaultPrevented) return; // dragged | |
console.log("click " + d); | |
dSortBy[d.level] = { | |
attrib:d.attrib, | |
desc:dSortBy[d.level]!==undefined && dSortBy[d.level].attrib === d.attrib ? | |
!dSortBy[d.level].desc : | |
false | |
}; | |
updateSorting(d.level); | |
removeBrushOnLevel(d.level); | |
nv.updateData(dataIs, colScales, { | |
levelToUpdate: d.level | |
}); | |
updateCallback(nv.getVisible()); | |
} | |
function getAttribs(obj) { | |
var attr, res = []; | |
for (attr in obj) { | |
if (obj.hasOwnProperty(attr)) { | |
res.push(attr); | |
} | |
} | |
return res; | |
} | |
function drawItem(item, level) { | |
var attrib, i, y ; | |
context.save(); | |
for (i = 0; i < dimensionsOrder.length; i++) { | |
attrib = dimensionsOrder[i]; | |
y = Math.round(yScales[level](item[id]) + yScales[level].bandwidth()/2); | |
// y = yScales[level](item[id]) + yScales[level].bandwidth()/2; | |
context.beginPath(); | |
context.moveTo(Math.round(x(attrib, level)), y); | |
context.lineTo(Math.round(x(attrib, level) + xScale.bandwidth()), y); | |
context.lineWidth = Math.ceil(yScales[level].bandwidth()); | |
// context.lineWidth = 1; | |
context.strokeStyle = item[attrib] === undefined || | |
item[attrib] === null || | |
item[attrib] === "" || | |
item[attrib] === "none" ? | |
"white" : | |
colScales.get(attrib)(item[attrib]); | |
context.stroke(); | |
//If the range bands are tick enough draw divisions | |
if (yScales[level].bandwidth() > nv.divisionsThreshold*2) { | |
var yLine = Math.round(yScales[level](item[id])) ; | |
// y = yScales[level](item[id])+yScales[level].bandwidth()/2; | |
context.beginPath(); | |
context.moveTo(x(attrib, level), yLine); | |
context.lineTo(x(attrib, level) + xScale.bandwidth(), yLine); | |
context.lineWidth = 1; | |
// context.lineWidth = 1; | |
context.strokeStyle = nv.divisionsColor; | |
context.stroke(); | |
} | |
} | |
context.restore(); | |
} // drawItem | |
function drawLevelBorder(i) { | |
context.save(); | |
context.beginPath(); | |
context.rect(levelScale(i), | |
yScales[i].range()[0]-1, | |
xScale.range()[1]+1, | |
yScales[i].range()[1]+2 - yScales[i].range()[0]); | |
context.strokeStyle = "black"; | |
context.lineWidth = 1; | |
context.stroke(); | |
context.restore(); | |
} | |
function removeBrushOnLevel(lev) { | |
d3.select("#level"+lev) | |
.selectAll(".brush") | |
.call(dBrushes[lev].move, null); | |
} | |
function removeAllBrushesBut(but) { | |
for (var lev=0; lev< dataIs.length ; lev+=1) { | |
if (lev===but) continue; | |
removeBrushOnLevel(lev); | |
} | |
} | |
// Assigns the indexes on the new level data | |
function assignIndexes(dataIsToUpdate, level) { | |
console.log("Assiging indexes ", level); | |
for (var j = 0; j < dataIsToUpdate.length; j++) { | |
data[dataIsToUpdate[j]].__i[level] = j; | |
} | |
} | |
function addBrush(d, level) { | |
dBrushes[level] = d3.brushY() | |
.extent([ | |
[x(xScale.domain()[0], level), yScales[level].range()[0]], | |
[ | |
x(xScale.domain()[xScale.domain().length - 1], level) + | |
xScale.bandwidth() * 1.1, | |
yScales[level].range()[1] | |
] | |
]) | |
.on("brush", brushed) | |
.on("end", onSelectByRange); | |
var _brush = d3.select(this) | |
.selectAll(".brush") | |
.data([{ | |
data : data[d], | |
level : level | |
}]); | |
_brush.enter() | |
.merge(_brush) | |
.append("g") | |
.on("mousemove", onMouseOver) | |
.on("click", onSelectByValue) | |
.on("mouseout", onMouseOut) | |
.attr("class", "brush") | |
.call(dBrushes[level]) | |
.selectAll("rect") | |
// .attr("x", -8) | |
.attr("width", x(xScale.domain()[xScale.domain().length-1], level) + xScale.bandwidth()*1.1); | |
_brush.exit().remove(); | |
// Applies the filters for the current level | |
function applyFilters() { | |
let before, after; | |
console.log("applyFilters ", filtersByLevel); | |
before = performance.now(); | |
// Check if each item fits on any filter | |
var filteredData = dataIs[level].filter(d => { | |
data[d].visible = false; | |
for (let filter of filtersByLevel[level]) { | |
if (filter.filter(data[d])) { | |
data[d].visible = true; | |
break; | |
} | |
} | |
return data[d].visible; | |
}); | |
// var filteredData = filtersByLevel[level].reduce(reduceFilters, dataIs[level]); | |
after = performance.now(); | |
console.log("Applying filters " + (after-before) + "ms"); | |
return filteredData; | |
} | |
function brushed() { | |
if (!d3.event.sourceEvent) return; // Only transition after input. | |
if (!d3.event.selection){ | |
console.log("Empty selection", d3.event.selection,d3.event.type, d3.event.sourceEvent); | |
// return; | |
// d3.event.preventDefault(); | |
// onSelectByValueFromCoords(d3.event.sourceEvent.clientX, d3.event.sourceEvent.clientY); | |
return; // Ignore empty selections. | |
} | |
const clientX = d3.event.sourceEvent.clientX, | |
clientY = d3.event.sourceEvent.clientY, | |
xOnWidget = d3.event.sourceEvent.offsetX, | |
yOnWidget = d3.event.sourceEvent.offsetY; | |
showTooptip(xOnWidget, yOnWidget, clientX, clientY, level); | |
} | |
function onSelectByRange() { | |
showLoading(this); | |
if (!d3.event.sourceEvent) return; // Only transition after input. | |
if (!d3.event.selection){ | |
console.log("Empty selection", d3.event.selection,d3.event.type, d3.event.sourceEvent); | |
// return; | |
// d3.event.preventDefault(); | |
// onSelectByValueFromCoords(d3.event.sourceEvent.clientX, d3.event.sourceEvent.clientY); | |
return; // Ignore empty selections. | |
} | |
removeAllBrushesBut(level); | |
var before = performance.now(); | |
var brushed = d3.event.selection; | |
var | |
// first = dData.get(invertOrdinalScale(yScales[level], brushed[0] -yScales[level].bandwidth())), | |
first = dData.get(invertOrdinalScale(yScales[level], brushed[0])), | |
// last = dData.get(invertOrdinalScale(yScales[level], brushed[1] -yScales[level].bandwidth())) | |
last = dData.get(invertOrdinalScale(yScales[level], brushed[1])); | |
const newFilter = new FilterByRange({first, last, level:level}); | |
if (d3.event.sourceEvent.shiftKey) { | |
// Append the filter | |
filtersByLevel[level].push(newFilter); | |
} else { | |
// Remove previous filters | |
filtersByLevel[level]= [ newFilter ]; | |
} | |
var filteredData = applyFilters(); | |
//Assign the index | |
assignIndexes(filteredData, level+1); | |
var after = performance.now(); | |
console.log("selectByRange filtering " + (after-before) + "ms", first, last); | |
console.log("Computing new data"); | |
var newData = dataIs; | |
if (filteredData.length===0) { | |
console.log("Empty selection!"); | |
return; | |
} else { | |
newData = dataIs.slice(0,level+1); | |
newData.push(filteredData); | |
} | |
nv.updateData( | |
newData, | |
colScales | |
); | |
console.log("out of updateData"); | |
console.log("Selected " + filteredData.length + " calling updateCallback"); | |
updateCallback(nv.getVisible()); | |
hideLoading(this); | |
}// onSelectByRange | |
function onSelectByValue() { | |
console.log("click"); | |
showLoading(this); | |
var clientY = d3.mouse(d3.event.target)[1], | |
clientX = d3.mouse(d3.event.target)[0]; | |
onSelectByValueFromCoords(clientX, clientY); | |
hideLoading(this); | |
} | |
function onSelectByValueFromCoords(clientX, clientY) { | |
console.log("onSelectByValueFromCoords", clientX, clientY); | |
removeAllBrushesBut(-1); // Remove all brushes | |
var before = performance.now(); | |
var itemId = invertOrdinalScale(yScales[level], clientY); | |
var after = performance.now(); | |
console.log("invertOrdinalScale " + (after-before) + "ms"); | |
var itemAttr = invertOrdinalScale(xScale, clientX - levelScale(level)); | |
if (itemAttr === undefined) return; | |
var sel = dData.get(itemId); | |
const newFilter = new FilterByValue({sel, itemAttr}); | |
if (d3.event.shiftKey) { | |
// Append the filter | |
filtersByLevel[level].push(newFilter); | |
} else { | |
// Remove previous filters | |
filtersByLevel[level]= [ newFilter ]; | |
} | |
var filteredData = applyFilters(); | |
assignIndexes(filteredData, level+1); | |
var newData = dataIs.slice(0,level+1); | |
newData.push(filteredData); | |
nv.updateData( | |
newData, | |
colScales | |
); | |
console.log("Selected " + nv.getVisible().length + " calling updateCallback"); | |
updateCallback(nv.getVisible()); | |
} | |
} // addBrush | |
function showTooptip(xOnWidget, yOnWidget, clientX, clientY, level) { | |
var itemId = invertOrdinalScale(yScales[level], yOnWidget); | |
var itemAttr = invertOrdinalScale(xScale, xOnWidget - levelScale(level)); | |
var d = dData.get(itemId); | |
if (!d || d=== undefined) { | |
console.log("Couldn't find datum for tooltip y", yOnWidget, d); | |
return; | |
} | |
tooltipCoords.x = xOnWidget; | |
tooltipCoords.y = yOnWidget; | |
tooltipElement.select(".tool_id").text(itemId); | |
tooltipElement.select(".tool_value_name").text(itemAttr); | |
tooltipElement.select(".tool_value_val").text(d[itemAttr]); | |
tooltipElement.style("display", "initial"); | |
tooltip.scheduleUpdate(); | |
console.log("Mouse over", d); | |
} | |
function onMouseOver(overData) { | |
const xOnWidget = d3.mouse(d3.event.target)[0], | |
yOnWidget = d3.mouse(d3.event.target)[1], | |
clientX = d3.event.clientX, | |
clientY = d3.event.clientY; | |
console.log("onMouseOver", yOnWidget, clientY, d3.event.offsetY, d3.event.clientY, d3.event); | |
showTooptip(xOnWidget, yOnWidget, clientX, clientY, overData.level); | |
} | |
function onMouseOut() { | |
tooltipCoords.x = -200; | |
tooltipCoords.y = -200; | |
tooltipElement.style("display", "none"); | |
tooltip.scheduleUpdate(); | |
// svg.select(".nvTooltip") | |
// .attr("transform", "translate(" + (-200) + "," + (-200) + ")") | |
// .call(function (tool) { | |
// tool.select(".tool_id") | |
// .text(""); | |
// tool.select(".tool_value_name") | |
// .text(""); | |
// tool.select(".tool_value_val") | |
// .text(""); | |
// }); | |
} | |
function drawBrushes(updateBrushes) { | |
var attribs = xScale.domain(); | |
var levelOverlay = svg.select(".attribs") | |
.selectAll(".levelOverlay") | |
.data(dataIs); | |
var levelOverlayEnter = levelOverlay.enter() | |
.append("g"); | |
levelOverlayEnter | |
.attr("class", "levelOverlay") | |
.attr("id", function (d,i) { return "level" +i; }); | |
// Bugfix: when adding all attribs we need to update the brush | |
if (updateBrushes) { | |
levelOverlayEnter | |
.merge(levelOverlay) | |
.each(addBrush); | |
} else { | |
levelOverlayEnter | |
.each(addBrush); | |
} | |
var attribOverlay = levelOverlayEnter.merge(levelOverlay) | |
.selectAll(".attribOverlay") | |
.data(function (_, i) { | |
return attribs.map(function (a) { | |
return {attrib:a, level:i}; | |
}); | |
}); | |
var attribOverlayEnter = attribOverlay | |
.enter() | |
.append("g") | |
.attr("class", "attribOverlay") | |
.style("cursor", "pointer"); | |
attribOverlayEnter | |
.merge(attribOverlay) | |
.attr("transform", function (d) { | |
return "translate(" + | |
x(d.attrib, d.level) + | |
"," + | |
yScales[d.level].range()[0] + | |
")"; | |
}); | |
attribOverlayEnter | |
.append("rect") | |
.merge(attribOverlay.select("rect")) | |
.attr("fill", "none") | |
// .style("opacity", "0.1") | |
.attr("x", 0) | |
.attr("y", 0) | |
.attr("width", function () { | |
return xScale.bandwidth()*1.1; | |
}) | |
.attr("height", function (d) { return yScales[d.level].range()[1] - yScales[d.level].range()[0]; }); | |
if (nv.showAttribTitles) { | |
attribOverlayEnter | |
.append("text") | |
.merge(attribOverlay.select("text")) | |
.style("cursor", "point") | |
.style("-webkit-user-select", "none") | |
.style("-moz-user-select", "none") | |
.style("-ms-user-select", "none") | |
.style("user-select", "none") | |
.text(function (d) { | |
return d.attrib === "__seqId" ? | |
"sequential Index" : | |
d.attrib + | |
(dSortBy[d.level]!==undefined && | |
dSortBy[d.level].attrib === d.attrib ? | |
dSortBy[d.level].desc ? | |
" \u2193" : | |
" \u2191" : | |
""); | |
}) | |
.attr("x", xScale.bandwidth()/2) | |
.attr("y", 0) | |
.style("font-weight", function (d) { | |
return (dSortBy[d.level]!==undefined && | |
dSortBy[d.level].attrib === d.attrib ? | |
"bolder" : | |
"normal"); | |
}) | |
.style("font-family", "sans-serif") | |
.style("font-size", function (d) { | |
// make it grow ? | |
// if (dSortBy[d.level]!==undefined && | |
// dSortBy[d.level].attrib === d.attrib ) | |
// d3.select(this).dispatch("mousemove"); | |
return Math.min(nv.attribFontSize, nv.attribWidth) + "px"; | |
}) | |
.on("click", deferEvent(onSortLevel)) | |
.call(d3.drag() | |
.container(attribOverlayEnter.merge(attribOverlay).node()) | |
.on("start", attribDragstarted) | |
.on("drag", attribDragged) | |
.on("end", attribDragended)) | |
.on("mousemove", function () { | |
var sel = d3.select(this); | |
sel = sel.transition!==undefined? sel.transition().duration(150) : sel; | |
sel | |
.style("font-size", nv.attribFontSizeSelected+"px"); | |
}) | |
.on("mouseout", function () { | |
var sel = d3.select(this); | |
sel = sel.transition!==undefined ? sel.transition().duration(150) : sel; | |
sel | |
.style("font-size", Math.min(nv.attribFontSize, nv.attribWidth) +"px"); | |
}) | |
.attr("transform", `rotate(${nv.attribRotation})`); | |
} // if (nv.showAttribTitles) { | |
levelOverlayEnter | |
.append("text") | |
.merge(levelOverlay.select("text.numNodesLabel")) | |
.attr("class", "numNodesLabel") | |
.style("font-family", "sans-serif") | |
.style("pointer-events", "none") | |
.merge(levelOverlay.select(".numNodesLabel")) | |
.attr("y", function (_, i) { | |
return yScales[i].range()[1] + 15; | |
}) | |
.attr("x", function (_, i) { | |
return levelScale(i); | |
}) | |
.text(function (d) { | |
return fmt(d.length); | |
}); | |
attribOverlay.exit().remove(); | |
levelOverlay.exit().remove(); | |
} // drawBrushes | |
function attribDragstarted(d) { | |
if (!d3.event.sourceEvent.shiftKey) | |
return; | |
console.log("start", d); | |
d3.select(this.parentNode) | |
.attr("transform", function (d) { | |
return "translate(" + | |
(d3.event.x + nv.attribFontSize/2) + | |
"," + | |
yScales[d.level].range()[0] + | |
")"; | |
}); | |
} | |
function attribDragged(d) { | |
// var attribInto = invertOrdinalScale(xScale, d3.everythingnt.x + nv.attribFontSize/2 - levelScale(d.level)); | |
// if (DEBUG) console.log(d3.event.x, d3.event.y, attribInto); | |
if (!d3.event.sourceEvent.shiftKey) | |
return; | |
d3.select(this.parentNode) | |
.attr("transform", function (d) { | |
return "translate(" + | |
(d3.event.x + nv.attribFontSize/2) + | |
"," + | |
yScales[d.level].range()[0] + | |
")"; | |
}); | |
} | |
function attribDragended(d) { | |
if (!d3.event.sourceEvent.shiftKey) | |
return; | |
console.log("end", d); | |
var attrDraggedInto = invertOrdinalScale(xScale, d3.event.x + nv.attribFontSize/2 - levelScale(d.level)); | |
var pos; | |
d3.select(this.parentNode) | |
.attr("transform", function (d) { | |
return "translate(" + | |
x(d.attrib, d.level) + | |
"," + | |
yScales[d.level].range()[0] + | |
")"; | |
}); | |
if (attrDraggedInto!== d.attrib) { | |
pos = dimensionsOrder.indexOf(attrDraggedInto); | |
moveAttrToPos(d.attrib, pos); | |
nv.updateData(dataIs); | |
} | |
} | |
function drawCloseButton() { | |
var maxLevel = dataIs.length-1; | |
svg.select("#closeButton") | |
.style("display", dataIs.length === 1 ? "none":"block") | |
.attr("transform", "translate(" + (levelScale(maxLevel) + levelScale.bandwidth() - nv.levelsSeparation +15) + "," + yScales[maxLevel].range()[0] + ")"); | |
} | |
// Links between nodes | |
function drawLink(link) { | |
var | |
lastAttrib = xScale.domain()[xScale.domain().length-1], | |
rightBorder = x(lastAttrib, dataIs.length-1)+ xScale.bandwidth()+2, | |
ys = yScales[dataIs.length-1](link.source[id]) + yScales[dataIs.length-1].bandwidth()/2, | |
yt = yScales[dataIs.length-1](link.target[id]) + yScales[dataIs.length-1].bandwidth()/2, | |
miny = Math.min(ys, yt), | |
maxy = Math.max(ys, yt), | |
midy = maxy-miny; | |
context.moveTo(rightBorder, miny); //starting point | |
context.quadraticCurveTo( | |
rightBorder + midy/6, miny + midy/2, // mid point | |
rightBorder, maxy // end point | |
); | |
} | |
function drawLinks() { | |
if (!links.length) return; | |
console.log("Draw links ", links[links.length-1].length , links); | |
context.save(); | |
context.beginPath(); | |
context.strokeStyle = nv.linkColor; | |
context.globalAlpha = Math.min(1, | |
Math.max(0.1,1000 / links[links.length-1].length ) | |
); // More links more transparency | |
// context.lineWidth = 0.5; | |
visibleLinks.forEach(drawLink); | |
context.stroke(); | |
context.restore(); | |
} | |
function drawLine(points, width, color, close) { | |
context.beginPath(); | |
points.forEach(function (p, i) { | |
if (i === 0) { | |
context.moveTo(p.x, p.y); | |
} else { | |
context.lineTo(p.x, p.y); | |
} | |
}); | |
context.lineWidth = width; | |
if (close) { | |
context.fillStyle = color; | |
context.closePath(); | |
context.fill(); | |
} else { | |
context.strokeStyle = color; | |
context.stroke(); | |
} | |
} | |
function drawLevelConnections(level) { | |
if (level <= 0) { | |
return; | |
} | |
dataIs[level].representatives.forEach(function (item) { | |
// Compute the yPrev by calculating the index of the corresponding representative | |
var iOnPrev = dData.get(data[item][id]).__i[level-1]; | |
var iRep = Math.floor(iOnPrev - iOnPrev%dataIs[level-1].itemsPerpixel); | |
// if (DEBUG) console.log("i rep = "+ iRep); | |
// if (DEBUG) console.log(data[level-1][iRep]); | |
// if (DEBUG) console.log(yScales[level-1](data[level-1][iRep][id])); | |
var locPrevLevel = { | |
x: levelScale(level-1) + xScale.range()[1], | |
y: yScales[level-1]( data[dataIs[level-1][iRep]] [id]) | |
}; | |
var locLevel = { | |
x: levelScale(level), | |
y: yScales[level](data[item][id]) }; | |
var points = [ locPrevLevel, | |
{x: locPrevLevel.x + nv.levelsSeparation * 0.3, y: locPrevLevel.y}, | |
{x: locLevel.x - nv.levelsSeparation * 0.3, y: locLevel.y}, | |
locLevel, | |
{x: locLevel.x, y: locLevel.y + yScales[level].bandwidth()}, | |
{x: locLevel.x - nv.levelsSeparation * 0.3, y: locLevel.y + yScales[level].bandwidth()}, | |
{x: locPrevLevel.x + nv.levelsSeparation * 0.3, y: locPrevLevel.y + yScales[level - 1].bandwidth()}, | |
{x: locPrevLevel.x, y: locPrevLevel.y + yScales[level -1 ].bandwidth()}, | |
locPrevLevel | |
]; | |
drawLine(points, 1, nv.levelConnectionsColor); | |
drawLine(points, 1, nv.levelConnectionsColor, true); | |
}); | |
} | |
function updateScales(opts) { | |
let {levelToUpdate, updateColorDomains} = opts || {}; | |
console.log("Update scales"); | |
const before = performance.now(); | |
const lastLevel = dataIs.length-1; | |
levelToUpdate = levelToUpdate!==undefined ? levelToUpdate : lastLevel; | |
updateColorDomains = updateColorDomains!==undefined ? updateColorDomains : false; | |
// Delete unvecessary scales | |
console.log("Delete unvecessary scales"); | |
yScales.splice(lastLevel+1, yScales.length); | |
yScales[levelToUpdate] = d3.scaleBand() | |
.range([nv.y0, height-nv.margin - 30]) | |
.paddingInner(0.0) | |
.paddingOuter(0); | |
// Compute Representatives | |
console.log("Compute representatives"); | |
let representatives = []; | |
if (dataIs[levelToUpdate].length>height) { | |
const itemsPerpixel = Math.max(Math.floor(dataIs[levelToUpdate].length / (height*2)), 1); | |
console.log("itemsPerpixel", itemsPerpixel); | |
dataIs[levelToUpdate].itemsPerpixel = itemsPerpixel; | |
for (let i = 0; i< dataIs[levelToUpdate].length; i+=itemsPerpixel ) { | |
representatives.push(dataIs[levelToUpdate][i]); | |
} | |
} else { | |
dataIs[levelToUpdate].itemsPerpixel=1; | |
representatives = dataIs[levelToUpdate]; | |
} | |
dataIs[levelToUpdate].representatives = representatives; | |
// Update x and y scales | |
yScales[levelToUpdate].domain(representatives.map(function (rep) { return data[rep][id];})); | |
xScale | |
.domain(dimensionsOrder) | |
.range([0, nv.attribWidth * (dDimensions.keys().length)]) | |
.paddingInner(0.1) | |
.paddingOuter(0); | |
levelScale.domain(dataIs.map(function (d,i) { return i; })) | |
.range([nv.x0+nv.margin, ((xScale.range()[1] + nv.levelsSeparation) * dataIs.length) + nv.x0]) | |
.paddingInner(0) | |
.paddingOuter(0); | |
// Update color scales domains | |
if (updateColorDomains) { | |
console.log("Update color scale domains", levelToUpdate); | |
// colScales = d3.map(); | |
dDimensions.keys().forEach( | |
function (attrib) { | |
if (attrib === "visible") return; | |
var scale = colScales.get(attrib); | |
if (scale.__type==="seq" || scale.__type==="date") { | |
scale.domain(d3.extent( | |
dataIs[0].map(function (i) { | |
return data[i][attrib]; | |
}) | |
)); //TODO: make it compute it based on the local range | |
} else if (scale.__type==="div") { | |
const [min, max] = d3.extent(dataIs[0].map(function (i) { | |
return data[i][attrib]; | |
})); | |
const absMax = Math.max(-min, max); // Assumes diverging point on 0 | |
scale.domain([-absMax, absMax]); | |
} else if (scale.__type==="text") { | |
scale.domain(dataIs[0].map((i) => data[i][attrib])); | |
} | |
colScales.set(attrib, scale); | |
} | |
); | |
} | |
const after = performance.now(); | |
console.log("Updating Scales " + (after-before) + "ms"); | |
} | |
function deleteOneLevel() { | |
if (dataIs.length<=1) return; | |
showLoading(this); | |
console.log("Delete one level"); | |
removeBrushOnLevel(dataIs.length-2); | |
dataIs[dataIs.length-2].forEach(function (d) { data[d].visible=true; }); | |
dataIs = dataIs.slice(0, dataIs.length-1); | |
nv.updateData(dataIs, colScales); | |
updateCallback(nv.getVisible()); | |
hideLoading(this); | |
} | |
function moveAttrToPos(attr, pos) { | |
var i = dimensionsOrder.indexOf(attr); | |
if ( i === -1) { console.err("moveAttrToPos attr not found", attr); return; } | |
if ( pos > dimensionsOrder.length || pos < 0) { console.err("moveAttrToPos pos out of bounds", pos, dimensionsOrder.length); return; } | |
dimensionsOrder.splice(i, 1); | |
dimensionsOrder.splice(pos, 0, attr); | |
} | |
function findNotNull(data, attr) { | |
let i, | |
val; | |
for ( i = 0; i<nv.howManyItemsShouldSearchForNotNull && i< data.length; i++ ) { | |
val = data[i][attr]; | |
if (val !== null && | |
val !== undefined && | |
val !== "") { | |
return val; | |
} | |
} | |
return val; | |
} | |
function recomputeVisibleLinks() { | |
if (links.length>0) { | |
visibleLinks = links.filter(function (d) { | |
return d.source.visible && d.target.visible; | |
}); | |
} | |
} | |
nv.initData = function (mData, mColScales) { | |
var before = performance.now(); | |
// getAttribs(mData[0][0]); | |
colScales = mColScales; | |
colScales.keys().forEach(function (d) { | |
dDimensions.set(d, true); | |
}); | |
dData = d3.map(); | |
for (var i = 0; i < data.length ; i++) { | |
var d = data[i]; | |
d.__seqId = i; //create a default id with the sequential number | |
dData.set(d[id], d); | |
d.__i=[]; | |
d.__i[0] = i; | |
} | |
filtersByLevel = []; | |
filtersByLevel[0] = []; // Initialice filters as empty for lev 0 | |
// nv.updateData(mData, mColScales, mSortByAttr); | |
var after = performance.now(); | |
console.log("Init data " + (after-before) + "ms"); | |
}; | |
nv.updateData = function (mDataIs, mColScales, opts) { | |
const {levelToUpdate, updateColorDomains} = opts || {}; | |
console.log("updateData"); | |
var before = performance.now(); | |
var ctxWidth; | |
if (typeof mDataIs !== typeof []) { | |
console.error("navio updateData didn't receive an array"); | |
return; | |
} | |
colScales = mColScales !== undefined ? mColScales: colScales; | |
dataIs = mDataIs; | |
// Delete filters on unused levels | |
filtersByLevel.splice(mDataIs.length); | |
// Initialize new filter level | |
filtersByLevel[mDataIs.length] = []; | |
recomputeVisibleLinks(); | |
// Delete unnecesary brushes | |
dBrushes.splice(mDataIs.length); | |
// Update the sorting of the last level | |
updateSorting(mDataIs.length-1); | |
updateScales({ | |
levelToUpdate, | |
updateColorDomains | |
}); | |
ctxWidth = levelScale.range()[1] + nv.margin + nv.x0; | |
d3.select(canvas) | |
.attr("width", ctxWidth) | |
.attr("height", height) | |
.style("width", ctxWidth) | |
.style("height", height+"px"); | |
canvas.style.width = ctxWidth+"px"; | |
canvas.style.height = height+"px"; | |
svg | |
.attr("width", ctxWidth) | |
.attr("height", height); | |
nv.update(); | |
var after = performance.now(); | |
console.log("Updating data " + (after-before) + "ms"); | |
}; // updateData | |
nv.update = function(_updateBrushes) { | |
if (!dataIs.length) return nv; | |
var updateBrushes = _updateBrushes !== undefined ? _updateBrushes : false; | |
var before = performance.now(); | |
var w = levelScale.range()[1] + nv.margin + nv.x0; | |
context.clearRect(0,0,w+1,height+1); | |
drawLinks(); | |
dataIs.forEach(function (levelData, i) { | |
drawLevelBorder(i); | |
levelData.representatives.forEach(function (rep) { | |
drawItem(data[rep], i); | |
}); | |
drawLevelConnections(i); | |
}); | |
drawBrushes(updateBrushes); | |
drawCloseButton(); | |
var after = performance.now(); | |
console.log("Redrawing " + (after-before) + "ms"); | |
return nv; | |
}; | |
nv.addAttrib = function (attr, scale) { | |
if (dimensionsOrder.indexOf(attr)!== -1) return; | |
dimensionsOrder.push(attr); | |
colScales.set(attr, scale); | |
return nv; | |
}; | |
nv.addSequentialAttrib = function (attr, _scale ) { | |
const domain = data!==undefined && data.length>0 ? | |
d3.extent(data, function (d) { return d[attr]; }) : | |
[0, 1]; //if we don"t have data, set the default domain | |
const scale = _scale || | |
d3.scaleSequential(defaultColorInterpolator) | |
.domain(domain); | |
scale.__type = "seq"; | |
nv.addAttrib(attr, scale); | |
return nv; | |
}; | |
// Same as addSequentialAttrib but with a different color | |
nv.addDateAttrib = function (attr, _scale ) { | |
const domain = data!==undefined && data.length>0 ? | |
d3.extent(data, function (d) { return d[attr]; }) : | |
[0, 1]; | |
const scale = _scale || | |
d3.scaleSequential(defaultColorInterpolatorDate) | |
.domain(domain); //if we don"t have data, set the default domain | |
nv.addAttrib(attr,scale); | |
scale.__type = "date"; | |
return nv; | |
}; | |
// Adds a diverging scale | |
nv.addDivergingAttrib = function (attr, _scale ) { | |
const domain = data!==undefined && data.length>0 ? | |
d3.extent(data, function (d) { return d[attr]; }) : | |
[-1, 1]; | |
const scale = _scale || | |
d3.scaleSequential(defaultColorInterpolatorDiverging) | |
.domain([domain[0], domain[1]]); //if we don"t have data, set the default domain | |
scale.__type = "div"; | |
nv.addAttrib(attr, scale); | |
return nv; | |
}; | |
nv.addCategoricalAttrib = function (attr, _scale ) { | |
const scale = _scale || | |
d3.scaleOrdinal(d3.schemeCategory10); | |
scale.__type = "cat"; | |
nv.addAttrib(attr, scale); | |
return nv; | |
}; | |
nv.addTextAttrib = function (attr, _scale ) { | |
const scale = _scale || | |
scaleText(); | |
nv.addAttrib(attr, scale); | |
return nv; | |
}; | |
nv.addBooleanAttrib = function (attr, _scale ) { | |
const scale = _scale || | |
d3.scaleOrdinal() | |
.domain([true, false, null]) | |
.range(defaultBooleanColorRange); | |
scale.__type = "bool"; | |
nv.addAttrib(attr, scale); | |
return nv; | |
}; | |
// Adds all the attributes on the data, or all the attributes provided on the list based on their types | |
nv.addAllAttribs = function (_attribs) { | |
if (!data || !data.length) throw Error("addAllAttribs called without data to guess the attribs. Make sure to call it after setting the data"); | |
var attribs = _attribs!==undefined ? _attribs : getAttribs(data[0]); | |
attribs.forEach(function (attr) { | |
if (attr === "__seqId" || | |
attr === "__i" || | |
attr === "visible") | |
return; | |
const firstNotNull = findNotNull(data, attr); | |
if (firstNotNull === null || | |
firstNotNull === undefined || | |
typeof(firstNotNull) === typeof("")) { | |
const counts = scaleText() | |
.computeRepresentatives(data.slice(0, nv.howManyItemsShouldSearchForNotNull) | |
.map(d => d[attr])) | |
.counts; | |
// How many different elements are there | |
if (counts.keys().length < nv.maxNumDistictForCategorical) { | |
console.log(`Navio: Adding attr ${attr} as categorical`); | |
nv.addCategoricalAttrib(attr); | |
} else { | |
console.log(`Navio: Attr ${attr} has too many distinct elements (${counts.keys().length}) using textAttrib`); | |
nv.addTextAttrib(attr); | |
} | |
} else if (typeof(firstNotNull) === typeof(0)) { | |
// Numbers | |
if (d3.min(data, d=> d[attr]) < 0) { | |
console.log(`Navio: Adding attr ${attr} as diverging`); | |
nv.addDivergingAttrib(attr); | |
} else { | |
console.log(`Navio: Adding attr ${attr} as sequential`); | |
nv.addSequentialAttrib(attr); | |
} | |
} else if (typeof(firstNotNull) === typeof(new Date())) { | |
console.log(`Navio: Adding attr ${attr} as date`); | |
nv.addDateAttrib(attr); | |
} else if (typeof(firstNotNull) === typeof(true)) { | |
console.log(`Navio: Adding attr ${attr} as boolean`); | |
nv.addBooleanAttrib(attr); | |
} else { | |
// Default categories | |
console.log(`Navio: Don't know what to do with attr ${attr} adding as categorical (type=${typeof(firstNotNull)})`); | |
nv.addCategoricalAttrib(attr); | |
} | |
}); | |
nv.data(data); | |
drawBrushes(true); // updates brushes width | |
return nv; | |
}; | |
nv.data = function(_) { | |
initTooltipPopper(); | |
if (!colScales.has("visible")) { | |
nv.addAttrib("visible", | |
d3.scaleOrdinal() | |
.domain([false,true]) | |
.range(visibleColorRange) | |
//, "#cddca3", "#8c6d31", "#bd9e39"] | |
); | |
moveAttrToPos("visible", 0); | |
} | |
if (!colScales.has("__seqId")) { | |
nv.addSequentialAttrib( | |
"__seqId" | |
); | |
moveAttrToPos("__seqId", 1); | |
} | |
if (arguments.length) { | |
data = _.slice(0); | |
data.forEach(function (d) { | |
d.visible = true; | |
}); | |
dataIs = [data.map(function (_, i) { return i; })]; | |
nv.initData( | |
dataIs, | |
colScales | |
); | |
nv.updateData( | |
dataIs, | |
colScales, | |
{ updateColorDomains:true } | |
); | |
return nv; | |
} else { | |
return data[0]; | |
} | |
}; | |
nv.getVisible = function() { | |
return dataIs[dataIs.length-1] | |
.filter(function (d) { return data[d].visible; }) | |
.map(function (d) { return data[d]; }); | |
}; | |
nv.sortBy = function (_attrib, _desc = false, _level = undefined) { | |
// The default level is the last one | |
let level = Math.max(0, _level !== undefined && _level < dataIs.length ? _level : dataIs.length-1); | |
if (_attrib !== undefined) { | |
// if (dimensionsOrder.indexOf(_attrib)===-1) { | |
// throw `sortBy: ${_attrib} is not in the list of attributes` | |
// } | |
dSortBy[level] = { | |
attrib:_attrib, | |
desc:_desc | |
}; | |
return nv.update(); | |
} else { | |
return dSortBy[level]; | |
} | |
}; | |
nv.updateCallback = function(_) { | |
return arguments.length ? (updateCallback = _, nv) : updateCallback; | |
}; | |
nv.visibleColorRange = function(_) { | |
return arguments.length ? (visibleColorRange = _, nv) : visibleColorRange; | |
}; | |
nv.defaultColorInterpolator = function(_) { | |
return arguments.length ? (defaultColorInterpolator = _, nv) : defaultColorInterpolator; | |
}; | |
nv.id = function(_) { | |
return arguments.length ? (id = _, nv) : id; | |
}; | |
nv.links = function(_) { | |
if (arguments.length) { | |
links = _; | |
recomputeVisibleLinks(); | |
return nv; | |
} else { | |
return links; | |
} | |
}; | |
init(); | |
return nv; | |
} | |
return navio; | |
}))); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment