Commit 6ffe4344 authored by Romain Loth's avatar Romain Loth

facet legends 2/2

heatmap coloring, better facetOptions cases, better handling NaN in attributes and renamed coloring functions
parent 735605a4
......@@ -43,18 +43,7 @@ This will still evolve but the main steps for any graph initialization messily u
- `somenode.attributes`: the `attributes` property is always an object
- any attribute listed in the sourcenode.attributes will be indexed if the TW.scanClusters flag is true
- data type and style of processing (for heatmap, or for classes, etc.) should be stipulated in settings
- the mapping from attribute values to matching nodes is in TW.Clusters.aType.anAttr.aValue.map
- finally in GUI we can associate 3 types of coloration
- `"gradient"` coloration
- available for any attribute that looks like a continuous metric
- `"heatmap"` coloration
- colors from cold to hot centered on a white "neutral" color
- applied for attributes stipulated in settings: eg "`age`" "`growth_rate`"
- `"cluster"` coloration for str or num classes like modularity_class, affiliation, etc.
- we use contrasted values from colorList
- automatically applied for "`cluster_index`" or any name in `TW.conf.nodeClusAtt`
- data type and style of processing (for heatmap, or for classes, etc.) should be stipulated in settings (cf. **data facets** below)
## User interaction mecanisms
......@@ -87,10 +76,28 @@ For any node `n` the relevant flags at selection are:
#### Facets: node attributes as colors/clusters
At parsing time, every node attributes are indexed by values.
At parsing time, every node attributes are indexed by values (allows to highlight them from legend, other uses are possible).
The values can be binned or not and can be linked to different color schemes:
- we can associate 3 types of coloration
- `"gradient"` coloration
- available for any attribute that looks like a continuous metric
- `"heatmap"` coloration
- colors from cold to hot centered on a white "neutral" color
- applied for attributes stipulated in settings: eg "`growth_rate`"
- `"cluster"` coloration for str or num classes like modularity_class, affiliation, etc.
- we use contrasted values from colorList
- automatically applied for "`cluster_index`" or any name in `TW.conf.nodeClusAtt`
- and also 3 possible binning modes:
- 'samerange': constant intervals between each bin
- 'samepop': constant cardinality inside each class (~ quantiles)
- 'off' : no binning (each distinct value will be a legend item)
These choices can be specified in the conf `facetOptions` entry.
If an attribute is **not** described in `facetOptions`, it will get `"gradient"` coloration and will be binned iff it has more disctinct values than `maxDiscreteValues`, into `legendBins` intervals.
This indexes are stored in TW.Clusters and provide an access to sets of nodes that have a given value or range of values.
- if discrete attrvalues with <= 30 classes (colorsBy, clustersBy), the storage structure is: `TW.Clusters[nodeType][clusterType].classes.[possibleValue]`
(the content is a list of ids with the value `possibleValue`)
- if continuous or many possible values (>30) (clustersBy, colorsRelByBins), the storage uses ordered ranges ("bins"):
`TW.Clusters[nodeType][clusterType].ranges.[interval]`
These indexes are stored in TW.Clusters and provide an access to sets of nodes that have a given value or range of values
- the mapping from attribute values to matching nodes is always in TW.Clusters.aType.anAttr.aClass.map
(where aClass is the chosen interval or distinct value)
......@@ -639,11 +639,11 @@
<!-- <script src="tinawebJS/sigma.v1.customized.js" type="text/javascript" language="javascript"></script> -->
<!-- <script src="tinawebJS/sigma.forceatlas2.js" type="text/javascript" language="javascript"></script> -->
<script src="settings_explorerjs.js" type="text/javascript" language="javascript"></script>
<script src="tinawebJS/enviroment.js" type="text/javascript" language="javascript"></script>
<script src="tinawebJS/sigma.parseCustom.js" type="text/javascript" language="javascript"></script>
<script src="extras_explorerjs.js" type="text/javascript" language="javascript"></script>
<script src="tinawebJS/sigmaUtils.js" type="text/javascript" language="javascript"></script>
<script src="tinawebJS/methods.js" type="text/javascript" language="javascript"></script>
<script src="tinawebJS/enviroment.js" type="text/javascript" language="javascript"></script>
<!-- <script src="tinawebJS/asyncFA2.js" type="text/javascript" language="javascript"></script> -->
<script src="tinawebJS/Tinaweb.js" type="text/javascript" language="javascript"></script>
<script src="tinawebJS/main.js" type="text/javascript" language="javascript"></script>
......
......@@ -11,7 +11,7 @@ function newPopup(url) {
// = = = = = = = = = = = [ Clusters Plugin ] = = = = = = = = = = = //
// Execution: changeGraphAppearanceByFacets( true )
// It reads scanned node-attributes and prepared legends in TW.Clusters
// to add the button in the html with the sigmaUtils.clustersBy(x) listener.
// to add the button in the html with the sigmaUtils.gradientColoring(x) listener.
function changeGraphAppearanceByFacets( manualflag ) {
if ( !isUndef(manualflag) && !TW.conf.colorByAtt ) TW.conf.colorByAtt = manualflag;
......@@ -20,16 +20,21 @@ function changeGraphAppearanceByFacets( manualflag ) {
// for GUI html: if present, rename raw attribute key by a proper label
var AttsTranslations = {
'clust_louvain': 'Groupes de voisins, méthode de Louvain',
'pageranks': 'Importance dans le réseau, méthode Google',
'PageRank': 'Importance dans le réseau, méthode Google',
'age': 'Date initiale d\'apparition du terme dans le corpus',
'growth_rate': 'Tendances et oubliés de la semaine',
'modularity_class': 'Groupes de voisins, méthode des classes de modularité'
'Modularity Class': 'Groupes de voisins, méthode des classes de modularité'
}
// create colormenu
// settings to function name
var colorFuns = {
'heatmap': "heatmapColoring",
'gradient': "gradientColoring",
'cluster': "clusterColoring"
}
// create colormenu
var color_menu_info = '<li><a href="#" onclick="graphResetColor()">By Default</a></li>';
if( $( "#colorgraph-menu" ).length>0 ) {
......@@ -43,15 +48,26 @@ function changeGraphAppearanceByFacets( manualflag ) {
// POSS here distinguish [ty][att_s].classes.length and ranges.length
var att_c = TW.Clusters[ty][att_s].length
var the_method = "clustersBy"
// variants
if(att_s.indexOf("clust")>-1||att_s.indexOf("class")>-1) {
// for classes and clusters
the_method = "colorsBy"
// coloringFunction
var the_method
// read from user settings
if (TW.conf.facetOptions[att_s] && TW.conf.facetOptions[att_s]['col']) {
the_method = colorFuns[TW.conf.facetOptions[att_s]['col']]
}
// default values
if (! the_method) {
if(att_s.indexOf("clust")>-1||att_s.indexOf("class")>-1) {
// for classes and clusters
the_method = "clusterColoring"
}
else {
the_method = "gradientColoring"
}
}
if(att_s == "growth_rate") the_method = "colorsRelByBins"
if(att_s == "age") the_method = "colorsRelByBins"
// family label :)
var lab_att_s ;
......@@ -65,30 +81,15 @@ function changeGraphAppearanceByFacets( manualflag ) {
}
// we also add clust_louvain in all cases
color_menu_info += `<li><a href="#" onclick='colorsBy("clust_louvain")'>By Louvain clustering (${TW.partialGraph.graph.nNodes()})</a></li>`
color_menu_info += `<li><a href="#" onclick='clusterColoring("clust_louvain")'>By Louvain clustering (${TW.partialGraph.graph.nNodes()})</a></li>`
// for debug
console.warn('color_menu_info', color_menu_info)
$("#colorgraph-menu").html(color_menu_info)
}
// // 2) prepare legend slots
// console.warn ("classes_per_Att:", classes_per_Att)
// let nodeType = getCurrentType()
// for (var attr in classes_per_Att) {
// let distinctVals = Object.keys(classes_per_Att[attr])
//
// // ------------------------------------------------
// if (distinctVals.length > TW.maxDiscreteValues) {
// TW.Clusters[nodeType][attr] = {'ranges': {}}
// // will be computed at changeColor FIXME could be now...
// }
// else {
// TW.Clusters[nodeType][attr] = {'classes': {}}
// for (var k_cls in distinctVals) {
// TW.Clusters[nodeType][attr].classes[distinctVals[k_cls]] = []
// // will become array of ids per subclass
// }
// }
// }
// Legend slots were prepared in TW.Clusters
}
......
......@@ -31,8 +31,8 @@
.my-legend {
position:fixed;
/* width: we set it equal or smaller than #lefttopbox width */
max-width:18%;
max-height: 25%;
max-width:20%;
max-height: 30%;
padding: 0 5px;
overflow-y:scroll;
bottom:0;
......@@ -42,7 +42,7 @@
opacity: 0.8;
color:#250587;
margin: 7px;
font-size:small;
font-size:120%;
}
.my-legend .legend-title {
......
......@@ -51,39 +51,43 @@ TW.conf = (function(TW){
// create facets ?
TWConf.scanClusters = true
// for continuous attrvalues/colors (cf. clustersBy), how many levels in legend?
TWConf.legendsBins = 7 ;
// max discrete levels in facet legend (if attribute has more distinct values then binning)
TWConf.maxDiscreteValues = 40
// £TODO transform for new specifications
// some specific attributes may have other number of levels
TWConf.customLegendsBins = {
'age': 8,
'growth_rate': 12
}
// facetOptions: choose here the visual result of your node attributes
// -------------------------------------------------------------------
// 3 possible coloring functions
// - cluster (contrasted colors for attributes describing *classes*)
// - gradient (uniform map from a numeric attribute to red/yellow gradient)
// - heatmap (from blue to red/brown, centered on a white "neutral" color)
// 2 possible binning modes
// - samerange: constant intervals between each bin
// - samepop: constant cardinality inside each class (~ quantiles)
// Cases with no binning: if type is not numeric or if there is less than n vdistinct values
// - heatmap (from blue to red, centered on a white "neutral" color)
// 3 possible binning modes
// - 'samerange': constant intervals between each bin
// - 'samepop': constant cardinality inside each class (~ quantiles)
// - 'off' : no binning (each distinct value will be a legend item)
TWConf.facetOptions = {
// attribute | coloring | number | binning
// name | function | of bins | mode
// gexf | | custom |
// attribute | coloring | number | binning
// title | function | of bins | mode
// --------------------------------------------------------------------
'numuniform' : {'col': "gradient", 'n': 3, 'binmode': 'samerange'},
'numpareto' : {'col': "gradient", 'n': 8, 'binmode': 'samepop' },
'intfewvalues' : {'col': "heatmap" , 'n': 4, 'binmode': 'samerange'},
'countryuniform':{'col': "cluster" },
'age' : {'col': "gradient", 'n': 2, 'binmode': 'samerange'},
'growth_rate' : {'col': "heatmap", 'n': 11, 'binmode': 'samepop' },
'PageRank' : {'col': "gradient", 'n': 8, 'binmode': 'samepop' },
'numuniform' : {'col': "heatmap", 'n': 7, 'binmode': 'samepop' },
'numpareto' : {'col': "gradient", 'n': 5, 'binmode': 'samerange'},
'intfewvalues' : {'col': "cluster" , 'n': 4, 'binmode': 'samerange'},
'Modularity Class': {'col': "cluster", 'binmode': 'off'}, // <== exemple with no binning
'countryuniform' : {'col': "cluster" , 'binmode': 'off'},
'countrypareto' : {'col': "cluster" , 'binmode': 'off'},
}
// NB other cases with no binning:
// - if data type is not numeric
// - if there is less than distinct values that facetOptions[attr][n]
// NB for heatmapColoring:
// - you should prefer odd number of bins
// - if the number of bins is even, the 2 classes in the middle get white
// - the maximum number of bins is 24
// other POSS option: display attribute value in label or not ?
......@@ -125,7 +129,7 @@ TW.conf = (function(TW){
// -----------------------------
TWConf.filterSliders = true // show sliders for nodes/edges subsets
TWConf.colorsByAtt = true; // show "Set colors" menu
TWConf.clusterColorsAtt = true; // show "Set colors" menu
TWConf.deselectOnclickStage = true // click on background remove selection ?
// (except when dragging)
......@@ -245,7 +249,7 @@ TW.conf = (function(TW){
// show verbose console logs...
logFetchers: false, // ...about ajax/fetching of graph data
logParsers: false, // ...about parsing said data
logFacets: true, // ...about parsing node attribute:value facets
logFacets: false, // ...about parsing node attribute:value facets
logSettings: false, // ...about settings at Tina and Sigma init time
logSelections: false
}
......
......@@ -756,7 +756,7 @@ var TinaWebJS = function ( sigmacanvas ) {
TW.partialGraph.camera.goTo({x:0, y:0, ratio:1.2})
});
if (!TW.conf.colorsByAtt) {
if (!TW.conf.clusterColorsAtt) {
$("#setcolorsMenu").hide()
}
......
......@@ -3,6 +3,118 @@
// always useful
var theHtml = document.getElementsByTagName('html')[0]
// GUI vars
TW.gui = {}
TW.gui.selectionActive = false // <== changes rendering mode
TW.gui.circleSize = 0;
TW.gui.checkBox=false;
TW.gui.shiftKey=false;
TW.gui.manuallyChecked = false;
TW.gui.lastFilters = {}
// contrasted color list for clusterColoring()
TW.gui.colorList = ["#000000", "#FFFF00", "#1CE6FF", "#FF34FF", "#FF4A46", "#008941",
"#006FA6", "#A30059", "#FFDBE5", "#7A4900", "#0000A6", "#63FFAC", "#B79762",
"#004D43", "#8FB0FF", "#997D87", "#5A0007", "#809693", "#FEFFE6", "#1B4400",
"#4FC601", "#3B5DFF", "#4A3B53", "#FF2F80", "#61615A", "#BA0900", "#6B7900",
"#00C2A0", "#FFAA92", "#FF90C9", "#B903AA", "#D16100", "#DDEFFF", "#000035",
"#7B4F4B", "#A1C299", "#300018", "#0AA6D8", "#013349", "#00846F", "#372101",
"#FFB500", "#C2FFED", "#A079BF", "#CC0744", "#C0B9B2", "#C2FF99", "#001E09",
"#00489C", "#6F0062", "#0CBD66", "#EEC3FF", "#456D75", "#B77B68", "#7A87A1",
"#788D66", "#885578", "#FAD09F", "#FF8A9A", "#D157A0", "#BEC459", "#456648",
"#0086ED", "#886F4C","#34362D", "#B4A8BD", "#00A6AA", "#452C2C", "#636375",
"#A3C8C9", "#FF913F", "#938A81", "#575329", "#00FECF", "#B05B6F", "#8CD0FF",
"#3B9700", "#04F757", "#C8A1A1", "#1E6E00", "#7900D7", "#A77500", "#6367A9",
"#A05837", "#6B002C", "#772600", "#D790FF", "#9B9700", "#549E79", "#FFF69F",
"#201625", "#72418F", "#BC23FF", "#99ADC0", "#3A2465", "#922329", "#5B4534",
"#FDE8DC", "#404E55", "#0089A3", "#CB7E98", "#A4E804", "#324E72", "#6A3A4C",
"#83AB58", "#001C1E", "#D1F7CE", "#004B28", "#C8D0F6", "#A3A489", "#806C66",
"#222800", "#BF5650", "#E83000", "#66796D", "#DA007C", "#FF1A59", "#8ADBB4",
"#1E0200", "#5B4E51", "#C895C5", "#320033", "#FF6832", "#66E1D3", "#CFCDAC",
"#D0AC94", "#7ED379", "#012C58"];
// 24 colors + White, divided in cold and warm range for getHeatmapColors() fun
TW.gui.heatmapColorListWhite = "#F9F7ED"
TW.gui.heatmapColorListToColdest = [
"#B4FF50",
"#A4FF24",
"#79FF23",
"#42F923",
"#22F226",
"#02CB36",
"#01C462",
"#01BC8D",
"#00B5B1",
"#0088AE",
"#005197",
"#002FA0"
]
TW.gui.heatmapColorListToWarmest = [
"#FFE37A",
"#FFE008",
"#F0C508",
"#E89A09",
"#E48509",
"#DF7009",
"#DB5B09",
"#D7450A",
"#D3300A",
"#CF1B0A",
"#CB060B",
"#B21014"
];
// provides a heatmap color list centered on a white class
// arg: number of classes
// NB: if number of classes is *even*,
// then the 2 middle categories
// will get the white "epsilon" color
function getHeatmapColors(nClasses) {
// our dev param == colorListToWarmest.length == colorListToColdest.length
let listsLen = 12
// our result
var outColors = []
if (nClasses > listsLen*2) {
throw(`this function implementation can only give up to ${listsLen*2} classes`)
}
let nHalfToPick = 0
if (nClasses % 2 == 0) {
nHalfToPick = nClasses/2 - 1
}
else {
nHalfToPick = (nClasses-1)/2
}
// floor
let skipStep = parseInt(listsLen / nHalfToPick)
// cold colors
for (let i = listsLen-1 ; i > 0 ; i -= skipStep ) {
outColors.push(TW.gui.heatmapColorListToColdest[i])
}
// white
outColors.push(TW.gui.heatmapColorListWhite)
if (nClasses % 2 == 0) {
outColors.push(TW.gui.heatmapColorListWhite)
}
// warm colors
for (let i = 0 ; i < listsLen ; i += skipStep ) {
outColors.push(TW.gui.heatmapColorListToWarmest[i])
}
return outColors
}
function writeBrand (brandString) {
document.getElementById('twbrand').innerHTML = brandString
}
......
......@@ -44,19 +44,6 @@ var bipartiteD2N = {};
var bipartiteN2D = {};
// GUI vars
TW.gui = {}
TW.gui.selectionActive = false // <== changes rendering mode
TW.gui.circleSize = 0;
TW.gui.checkBox=false;
TW.gui.shiftKey=false;
TW.gui.manuallyChecked = false;
TW.gui.lastFilters = {}
TW.colorList = ["#000000", "#FFFF00", "#1CE6FF", "#FF34FF", "#FF4A46", "#008941", "#006FA6", "#A30059", "#FFDBE5", "#7A4900", "#0000A6", "#63FFAC", "#B79762", "#004D43", "#8FB0FF", "#997D87", "#5A0007", "#809693", "#FEFFE6", "#1B4400", "#4FC601", "#3B5DFF", "#4A3B53", "#FF2F80", "#61615A", "#BA0900", "#6B7900", "#00C2A0", "#FFAA92", "#FF90C9", "#B903AA", "#D16100", "#DDEFFF", "#000035", "#7B4F4B", "#A1C299", "#300018", "#0AA6D8", "#013349", "#00846F", "#372101", "#FFB500", "#C2FFED", "#A079BF", "#CC0744", "#C0B9B2", "#C2FF99", "#001E09", "#00489C", "#6F0062", "#0CBD66", "#EEC3FF", "#456D75", "#B77B68", "#7A87A1", "#788D66", "#885578", "#FAD09F", "#FF8A9A", "#D157A0", "#BEC459", "#456648", "#0086ED", "#886F4C","#34362D", "#B4A8BD", "#00A6AA", "#452C2C", "#636375", "#A3C8C9", "#FF913F", "#938A81", "#575329", "#00FECF", "#B05B6F", "#8CD0FF", "#3B9700", "#04F757", "#C8A1A1", "#1E6E00", "#7900D7", "#A77500", "#6367A9", "#A05837", "#6B002C", "#772600", "#D790FF", "#9B9700", "#549E79", "#FFF69F", "#201625", "#72418F", "#BC23FF", "#99ADC0", "#3A2465", "#922329", "#5B4534", "#FDE8DC", "#404E55", "#0089A3", "#CB7E98", "#A4E804", "#324E72", "#6A3A4C", "#83AB58", "#001C1E", "#D1F7CE", "#004B28", "#C8D0F6", "#A3A489", "#806C66", "#222800", "#BF5650", "#E83000", "#66796D", "#DA007C", "#FF1A59", "#8ADBB4", "#1E0200", "#5B4E51", "#C895C5", "#320033", "#FF6832", "#66E1D3", "#CFCDAC", "#D0AC94", "#7ED379", "#012C58"];
// ======== [ what to do at start ] ========= //
console.log("Starting TWJS")
......@@ -255,17 +242,16 @@ function syncRemoteGraphData () {
var preRES = AjaxSync({ url: infofile, datatype:"json" });
if (preRES['OK'] && preRES.data) {
console.log('initial AjaxSync result preRES', preRES)
if (TW.conf.debug.logFetchers) console.log('initial AjaxSync result preRES', preRES)
}
var first_file = "" , first_path = ""
for( var path in preRES.data ) {
// console.log("db.json path", path)
if (TW.conf.debug.logFetchers) console.log("db.json path", path)
first_file = preRES.data[path]["first"]
first_path = path
console.log("db.json first_file", first_path, first_file)
break;
}
......
......@@ -101,8 +101,9 @@ function gexfCheckAttributesMap (someXMLContent) {
// excerpt from targeted XML:
// <graph defaultedgetype="undirected" mode="static">
// | <attributes class="node" mode="static">
// | <attribute id="0" title="category" type="string"></attribute>
// | <attribute id="1" title="country" type="float"></attribute>
// | <attribute id="0" title="category" type="string"></attribute>
// | <attribute id="1" title="country" type="float"></attribute>
// | <attribute id="mod_class" title="Modularity Class" type="float"></attribute>
// | </attributes>
// (...)
......@@ -127,7 +128,7 @@ function gexfCheckAttributesMap (someXMLContent) {
type = attributeNode.getAttribute('type');
// ex: id = "in-degree" or "3" <= can be an int to be resolved into the title
// ex: title = "in-degree"
// ex: title = "in-degree" or "In Degree" <= can be a label != id
// ex: type = "string"
var attribute = {
......@@ -294,6 +295,9 @@ function facetsBinning (valuesIdx) {
for (var at in valuesIdx[cat]) {
if (TW.conf.debug.logFacets) console.log(`======= ${cat}::${at} =======`)
// console.warn("all raw vals before binning" valuesIdx[cat][at].vals)
// new array of valueclass/interval/bin objects
facetIdx[cat][at] = []
......@@ -333,9 +337,11 @@ function facetsBinning (valuesIdx) {
workingVals = valuesIdx[cat][at].vals.vstr
}
console.debug("datatyping:", dataType)
console.debug("valuesIdx after datatyping:", valuesIdx[cat][at])
console.debug("workingVals after datatyping:", workingVals)
if (TW.conf.debug.logFacets) {
console.debug("datatyping:", dataType)
// console.debug("valuesIdx after datatyping:", valuesIdx[cat][at])
// console.debug("workingVals after datatyping:", workingVals)
}
// default options
let maxDiscreteValues = TW.conf.maxDiscreteValues
......@@ -353,14 +359,27 @@ function facetsBinning (valuesIdx) {
console.warn(`Can't use user-specified number of bins value 0 for attribute ${at}, using TW.conf.legendsBins ${TW.conf.legendsBins} instead`)
nBins = TW.conf.legendsBins
}
if (TW.conf.debug.logFacets) console.log("TW.conf.facetOptions[at]", TW.conf.facetOptions[at])
}
else {
if (TW.conf.debug.logFacets) console.log("(no specified options in settings for this attribute)")
}
// POSSible: auto-detect if vtypes ==> color
// else {
// }
// if (binningMode != "off") console.warn("maxDiscreteValues from settings", maxDiscreteValues)
var nDistinctVals = Object.keys(valuesIdx[cat][at].map).length
// if small number of distinct values doesn't need binify
if (Object.keys(valuesIdx[cat][at].map).length <= maxDiscreteValues) {
if ( dataType == 'str'
|| (TW.conf.facetOptions[at] // case with custom facetOptions
&& (nDistinctVals <= nBins || binningMode == "off"))
|| (nDistinctVals <= maxDiscreteValues ) // case with unspecified options
) {
for (var pval in valuesIdx[cat][at].map) {
var idList = valuesIdx[cat][at].map[pval]
......@@ -416,12 +435,18 @@ function facetsBinning (valuesIdx) {
if (TW.conf.debug.logFacets) console.debug("intervals for", at, legendRefTicks, "(list of minima)")
// the unique-d array will allow us to group ranges
var sortedDistinctVals = Object.keys(valuesIdx[cat][at].map).sort(function(a,b){return Number(a)-Number(b)})
// the unique-d array will serve as a todolist with lastCursor and k
// won't use keys(map) because of _non_numeric_ entry
let uniqueVals = {}
for (let k in workingVals) {
if (! uniqueVals[workingVals[k]]) {
uniqueVals[workingVals[k]] = 1
}
}
var sortedDistinctVals = Object.keys(uniqueVals).sort(function(a,b){return Number(a)-Number(b)})
var nTicks = legendRefTicks.length
var nDistinctVals = sortedDistinctVals.length
var lastCursor = 0
// create ticks objects with retrieved full info
......@@ -449,11 +474,27 @@ function facetsBinning (valuesIdx) {
'range':[lowThres, hiThres]
}
if (TW.conf.debug.logFacets) console.debug("...new interval:",[lowThres, hiThres])
// 1) union of idmaps
for (var k = lastCursor ; k <= nDistinctVals ; k++) {
var val = Number(sortedDistinctVals[k])
if (val == '_non_numeric_') {
continue
}
// FIXME why still NaN sometimes ?
// NB: however skipping them is enough for work
else if (isNaN(val)) {
console.debug('skipped undetected NaN ? attribute, lastCursor, k, sortedDistinctVals[k], nodes:', at, lastCursor, k, val, valuesIdx[cat][at].map[sortedDistinctVals[k]])
continue
}
// for debug
// console.debug('lastCursor, k, val', lastCursor, k, val)
if (val < lowThres) {
console.error("mixup !!", val, lowThres, at)
console.error("mixup !!", val, lowThres, at)
}
else if ((val >= lowThres) && (val < hiThres)) {
if (!valuesIdx[cat][at].map[val]) {
......@@ -470,8 +511,12 @@ function facetsBinning (valuesIdx) {
// we're over the interval upper bound
else if (val >= hiThres) {
// console.log("over hiThres", val, hiThres)
// normal case
if (binningMode != 'samerange' || l != nTicks-1 ) {
// console.log("...moving on to next interval")
// we just need to remember where we were for next interval
lastCursor = k
break
......@@ -479,6 +524,8 @@ function facetsBinning (valuesIdx) {
// samerange && last interval case: inclusive last interval upper bound
else {
// console.warn("last interval for samepop")
for (var j in valuesIdx[cat][at].map[val]) {
newTick.nids.push(valuesIdx[cat][at].map[val][j])
}
......@@ -878,6 +925,11 @@ function updateValueFacets(facetIdx, aNode) {
if (!facetIdx[aNode.type]) facetIdx[aNode.type]={}
for (var at in aNode.attributes) {
// we're not interested in node type/category at this point
if (at == 'category')
continue
let val = aNode.attributes[at]
if (!facetIdx[aNode.type][at]) facetIdx[aNode.type][at]={'vals':{'vstr':[], 'vnum':[]},'map':{}}
......@@ -1106,11 +1158,13 @@ function dictfyJSON( data , categories ) {
// }
// }
TW.colorList.sort(function(){ return Math.random()-0.5; });
// £TODO this could be a call to clusterColoring()
TW.gui.colorList.sort(function(){ return Math.random()-0.5; });
for (var i in nodes ){
if (nodes[i].color=="#FFFFFF") {
var attval = ( isUndef(nodes[i].attributes) || isUndef(nodes[i].attributes["clust_default"]) )? 0 : nodes[i].attributes["clust_default"] ;
nodes[i].color = TW.colorList[ attval ]
nodes[i].color = TW.gui.colorList[ attval ]
}
}
......
......@@ -760,7 +760,7 @@ function edgeInfos(anEdge) {
}
function clustersBy(daclass) {
function gradientColoring(daclass) {
cancelSelection(false);
......@@ -887,15 +887,29 @@ function repaintEdges() {
}
}
// rewrite of clustersBy with binning and for attributes that can have negative float values
// NB - binning is done at parseCustom
// - number of bins can be specified by attribute name in TW.conf.customLegendsBins
function colorsRelByBins(daclass) {
// heatmap from cold to warm with middle white
// (good for values centered around a neutral zone)
// NB - binning is done at parseCustom (cf. TW.Clusters)
// - number of bins can be specified by attribute name in TW.conf.facetOptions[daclass]["n"]
function heatmapColoring(daclass) {
var binColors
var doModifyLabel = false
var actypes = getActivetypes()
// default value
let nColors = TW.conf.legendsBins || 5
// possible user value
if (TW.conf.facetOptions[daclass]) {
if (TW.conf.facetOptions[daclass]["n"] != 0) {
nColors = TW.conf.facetOptions[daclass]["n"]
}
else {
console.warn(`Can't use user-specified number of bins value 0 for attribute ${at}, using TW.conf.legendsBins ${TW.conf.legendsBins} instead`)
}
}
// we have no specifications yet for colors and legends on multiple types
if (actypes.length > 1) {
console.warn("colors by bins will only color nodes of type 0")
......@@ -906,7 +920,14 @@ function colorsRelByBins(daclass) {
// our binning
var tickThresholds = TW.Clusters[ty][daclass]
// for debug of colorsRelByBins
// verifications
if (tickThresholds.length != nColors) {
console.warn (`heatmapColoring setup mismatch: TW.Clusters ticks ${tickThresholds} from scanClusters should == nColors ${nColors}`)
}
binColors = getHeatmapColors(nColors)
// for debug of heatmapColoring
var totalsPerBinMin = {}
// let's go
......@@ -915,271 +936,57 @@ function colorsRelByBins(daclass) {
// global flag
TW.handpickedcolor = true
if (daclass == 'age') {
// 9 colors
binColors = [
"#F9F588",//epsilon
"#f9f008", //yel
"#f9da08",
"#fab207",
"#fa9607",
"#fa6e07",
"#fa4607",
"#ff0000" // red binMin 125
];
}
else if (daclass == 'growth_rate') {
doModifyLabel = true
// 12 colors
binColors = [
"#005197", //blue binMin -∞
"#3c76fb", // binMin -75
"#5c8af2", // binMin -50
"#64c5f2", // binMin -25
"#F9F7ED",//epsilon binMin -15
"#bae64f", // binMin 15
"#f9f008", // binMin 25
"#fab207", // binMin 50
"#fa9607", // binMin 75
"#fa6e07", // binMin 100
"#fa4607", // red binMin 125
"#991B1E" // binMin 150
];
// use our valueclass => ids mapping
for (var k in tickThresholds) {
}
// console.debug('tick infos', tickThresholds[k])
// verification
if (tickThresholds.length != binColors.length) {
console.warn (`colorsRelByBins setup mismatch: TW.Clusters ticks ${tickThresholds} should == nColors ${binColors.length}`)
// skip grouped NaN values case => grey
if (tickThresholds[k].labl == '_non_numeric_') {
continue
}
// ex: {labl: "terms||growth_rate||[0 ; 0.583]", nids: Array(99), range: [0 ; 0.583210]}
// use our valueclass => ids mapping
for (var k in tickThresholds) {
totalsPerBinMin[tickThresholds[k].range[0]] = tickThresholds[k].nids.length
// console.debug('tick infos', tickThresholds[k])
// ex: {labl: "terms||growth_rate||[0 ; 0.583]", nids: Array(99), range: [0 ; 0.583210]}
// color the referred nodes
for (var j in tickThresholds[k].nids) {
let n = TW.partialGraph.graph.nodes(tickThresholds[k].nids[j])
totalsPerBinMin[tickThresholds[k].range[0]] = tickThresholds[k].nids.length
// color the referred nodes
for (var j in tickThresholds[k].nids) {
let n = TW.partialGraph.graph.nodes(tickThresholds[k].nids[j])
n.color = binColors[k]
n.customAttrs.alt_color = binColors[k]
n.customAttrs.altgrey_color = false
var originalLabel = TW.Nodes[n.id].label
if (doModifyLabel) {
var valSt = n.attributes[daclass]
n.label = `(${valSt}) ${originalLabel}`
}
else {
n.label = originalLabel
}
n.color = binColors[k]
n.customAttrs.alt_color = binColors[k]
n.customAttrs.altgrey_color = false
var originalLabel = TW.Nodes[n.id].label
if (doModifyLabel) {
var valSt = n.attributes[daclass]
n.label = `(${valSt}) ${originalLabel}`
}
else {
n.label = originalLabel
}
}
// console.debug(valArray)
console.info('coloring distribution per tick thresholds' , totalsPerBinMin)
// Edge precompute alt_rgb by new source-target nodes-colours combination
repaintEdges()
set_ClustersLegend ( daclass )
TW.partialGraph.render();
}
// KEPT FOR REFERENCE, BINNING NOW PRECOMPUTED in parseCustom
// rewrite of clustersBy with binning and for attributes that can have negative float values
// /!\ age and growth_rate attributes referred to by name
function colorsRelByBins_old(daclass) {
cancelSelection(false);
var binColors
var doModifyLabel = false
TW.handpickedcolor = true
// for debug of colorsRelByBins
var totalsPerBinMin = {}
// should be = binColors.length
var nTicksParam = (daclass == 'age') ? 8 : 12
// do first loop entirely to get percentiles => bins, then modify alt_color
// estimating ticks
let tickThresholds = []
let valArray = []
for (var j=0 ; j < TW.nNodes ; j++) {
let n = TW.partialGraph.graph.nodes(TW.nodeIds[j])
if (
!n.hidden
&& n.attributes
&& n.attributes.category == 'terms'
&& n.attributes[daclass] != undefined
) {
valArray.push(Number(n.attributes[daclass]))
}
}
var len = valArray.length
valArray.sort(function(a, b) {return a - b;}) // important :)
for (var l=0 ; l < nTicksParam ; l++) {
let nthVal = Math.floor(len * l / nTicksParam)
tickThresholds.push(valArray[nthVal])
}
// also always add the max+1 as last tick (excluded upper bound of last bin)
tickThresholds.push((valArray[len-1])+1)
console.info(`[|===|=== ${nTicksParam} color ticks ===|===|]\n`, tickThresholds)
cancelSelection(false);
if (daclass == 'age') {
// 9 colors
binColors = [
"#F9F588",//epsilon
"#f9f008", //yel
"#f9da08",
"#fab207",
"#fa9607",
"#fa6e07",
"#fa4607",
"#ff0000" // red binMin 125
];
}
else if (daclass == 'growth_rate') {
doModifyLabel = true
// 12 colors
binColors = [
"#005197", //blue binMin -∞
"#3c76fb", // binMin -75
"#5c8af2", // binMin -50
"#64c5f2", // binMin -25
"#F9F7ED",//epsilon binMin -15
"#bae64f", // binMin 15
"#f9f008", // binMin 25
"#fab207", // binMin 50
"#fa9607", // binMin 75
"#fa6e07", // binMin 100
"#fa4607", // red binMin 125
"#991B1E" // binMin 150
];
}
// verification
if (nTicksParam != binColors.length) {
console.warn (`colorsRelByBins setup mismatch: nTicksParam ${nTicksParam} should == nColors ${binColors.length}`)
}
console.info('coloring distribution per tick thresholds' , totalsPerBinMin)
// get the nodes
for (var j=0 ; j < TW.nNodes ; j++) {
let n = TW.partialGraph.graph.nodes(TW.nodeIds[j])
if (! n.hidden
&& n.attributes
&& n.attributes.category == 'terms'
&& ! isUndef(n.attributes[daclass])
) {
// Edge precompute alt_rgb by new source-target nodes-colours combination
repaintEdges()
var valSt = n.attributes[daclass]
var originalLabel = TW.Nodes[n.id].label
if (doModifyLabel) {
n.label = `(${valSt}) ${originalLabel}`
}
else {
n.label = originalLabel
}
set_ClustersLegend ( daclass )
var theVal = parseFloat(valSt)
var foundBin = false
// console.log('theVal:',theVal)
if( !isNaN(theVal) ) { //is float
// iterate over bins
for(var k=0 ; k < tickThresholds.length-1; k++) {
var binMin = tickThresholds[k]
var binMax = tickThresholds[(k+1)]
if((theVal >= binMin) && (theVal < binMax)) {
// TW.partialGraph._core.graph.nodesIndex[n.id].binMin = binMin
// TW.partialGraph._core.graph.nodesIndex[n.id].color = binColors[j]
n.binMin = binMin
n.color = binColors[k]
n.customAttrs.alt_color = binColors[k]
n.customAttrs.altgrey_color = false
foundBin = true
// console.log(`theVal ${theVal} => found its bin ${binMin} ... ${binColors[k]}`)
if (!totalsPerBinMin[binMin]) {
totalsPerBinMin[binMin] = 1
}
else {
totalsPerBinMin[binMin]++
}
break
}
}
// case no bin after loop (perhaps more ticks than colors-1 ??)
if (!foundBin) {
console.warn('no bin for theVal', theVal, n.id)
n.binMin = null
n.color = '#000'
n.customAttrs.alt_color = '#000'
}
}
else {
// case no val
// console.log('no val for', n.id)
n.binMin = null
n.color = '#555'
n.customAttrs.alt_color = '#555'
}
}
}
// console.debug(valArray)
console.info('coloring distribution per tick thresholds' , totalsPerBinMin)
// Edge precompute alt_rgb by new source-target nodes-colours combination
repaintEdges()
// set_ClustersLegend ( daclass )
TW.partialGraph.render();
TW.partialGraph.render();
}
function colorsBy(daclass) {
function clusterColoring(daclass) {
console.log("")
console.log(" = = = = = = = = = = = = = = = = = ")
console.log(" = = = = = = = = = = = = = = = = = ")
console.log("colorsBy ( "+daclass+" )")
console.log("clusterColoring ( "+daclass+" )")
console.log(" = = = = = = = = = = = = = = = = = ")
console.log(" = = = = = = = = = = = = = = = = = ")
console.log("")
......@@ -1206,7 +1013,7 @@ function colorsBy(daclass) {
}
else {
// shuffle on entire array is better than random sorting function on each element
var randomColorList = shuffle(TW.colorList)
var randomColorList = shuffle(TW.gui.colorList)
for(var j in TW.nodeIds) {
var the_node = TW.Nodes[ TW.nodeIds[j] ]
......@@ -1214,11 +1021,10 @@ function colorsBy(daclass) {
// ££TODO put louvain in graph.nodes() like other attrs ??
// then possible to use TW.partialGraph.graph.nodes(TW.nodeIds[j])
if (j< 15)
console.log(` ${TW.nodeIds[j]} : hidden?${the_node.hidden} the_node.attributes[daclass] = ${the_node.attributes[daclass]}`) // final val=${attval}
// if (j< 15)
// console.log(` ${TW.nodeIds[j]} : hidden?${the_node.hidden} the_node.attributes[daclass] = ${the_node.attributes[daclass]}`) // final val=${attval}
if (! the_node.hidden) {
console.log(the_node)
var attval = ( !isUndef(the_node.attributes) && !isUndef(the_node.attributes[daclass]) )? the_node.attributes[daclass] : TW.partialGraph.graph.nodes(TW.nodeIds[j])[daclass];
TW.partialGraph.graph.nodes(TW.nodeIds[j]).color = randomColorList[ attval ]
TW.partialGraph.graph.nodes(TW.nodeIds[j]).customAttrs.alt_color = randomColorList[ attval ]
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment