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 ...@@ -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 - `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 - 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 - data type and style of processing (for heatmap, or for classes, etc.) should be stipulated in settings (cf. **data facets** below)
- 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`
## User interaction mecanisms ## User interaction mecanisms
...@@ -87,10 +76,28 @@ For any node `n` the relevant flags at selection are: ...@@ -87,10 +76,28 @@ For any node `n` the relevant flags at selection are:
#### Facets: node attributes as colors/clusters #### 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. These 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 mapping from attribute values to matching nodes is always in TW.Clusters.aType.anAttr.aClass.map
(the content is a list of ids with the value `possibleValue`) (where aClass is the chosen interval or distinct value)
- if continuous or many possible values (>30) (clustersBy, colorsRelByBins), the storage uses ordered ranges ("bins"):
`TW.Clusters[nodeType][clusterType].ranges.[interval]`
...@@ -639,11 +639,11 @@ ...@@ -639,11 +639,11 @@
<!-- <script src="tinawebJS/sigma.v1.customized.js" type="text/javascript" language="javascript"></script> --> <!-- <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="tinawebJS/sigma.forceatlas2.js" type="text/javascript" language="javascript"></script> -->
<script src="settings_explorerjs.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="tinawebJS/sigma.parseCustom.js" type="text/javascript" language="javascript"></script>
<script src="extras_explorerjs.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/sigmaUtils.js" type="text/javascript" language="javascript"></script>
<script src="tinawebJS/methods.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/asyncFA2.js" type="text/javascript" language="javascript"></script> -->
<script src="tinawebJS/Tinaweb.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> <script src="tinawebJS/main.js" type="text/javascript" language="javascript"></script>
......
...@@ -11,7 +11,7 @@ function newPopup(url) { ...@@ -11,7 +11,7 @@ function newPopup(url) {
// = = = = = = = = = = = [ Clusters Plugin ] = = = = = = = = = = = // // = = = = = = = = = = = [ Clusters Plugin ] = = = = = = = = = = = //
// Execution: changeGraphAppearanceByFacets( true ) // Execution: changeGraphAppearanceByFacets( true )
// It reads scanned node-attributes and prepared legends in TW.Clusters // 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 ) { function changeGraphAppearanceByFacets( manualflag ) {
if ( !isUndef(manualflag) && !TW.conf.colorByAtt ) TW.conf.colorByAtt = manualflag; if ( !isUndef(manualflag) && !TW.conf.colorByAtt ) TW.conf.colorByAtt = manualflag;
...@@ -20,16 +20,21 @@ function changeGraphAppearanceByFacets( manualflag ) { ...@@ -20,16 +20,21 @@ function changeGraphAppearanceByFacets( manualflag ) {
// for GUI html: if present, rename raw attribute key by a proper label // for GUI html: if present, rename raw attribute key by a proper label
var AttsTranslations = { var AttsTranslations = {
'clust_louvain': 'Groupes de voisins, méthode de Louvain', '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', 'age': 'Date initiale d\'apparition du terme dans le corpus',
'growth_rate': 'Tendances et oubliés de la semaine', '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>'; var color_menu_info = '<li><a href="#" onclick="graphResetColor()">By Default</a></li>';
if( $( "#colorgraph-menu" ).length>0 ) { if( $( "#colorgraph-menu" ).length>0 ) {
...@@ -43,15 +48,26 @@ function changeGraphAppearanceByFacets( manualflag ) { ...@@ -43,15 +48,26 @@ function changeGraphAppearanceByFacets( manualflag ) {
// POSS here distinguish [ty][att_s].classes.length and ranges.length // POSS here distinguish [ty][att_s].classes.length and ranges.length
var att_c = TW.Clusters[ty][att_s].length var att_c = TW.Clusters[ty][att_s].length
var the_method = "clustersBy"
// variants
// 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) { if(att_s.indexOf("clust")>-1||att_s.indexOf("class")>-1) {
// for classes and clusters // for classes and clusters
the_method = "colorsBy" the_method = "clusterColoring"
}
else {
the_method = "gradientColoring"
}
} }
if(att_s == "growth_rate") the_method = "colorsRelByBins"
if(att_s == "age") the_method = "colorsRelByBins"
// family label :) // family label :)
var lab_att_s ; var lab_att_s ;
...@@ -65,30 +81,15 @@ function changeGraphAppearanceByFacets( manualflag ) { ...@@ -65,30 +81,15 @@ function changeGraphAppearanceByFacets( manualflag ) {
} }
// we also add clust_louvain in all cases // 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) $("#colorgraph-menu").html(color_menu_info)
} }
// // 2) prepare legend slots // Legend slots were prepared in TW.Clusters
// 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
// }
// }
// }
} }
......
...@@ -31,8 +31,8 @@ ...@@ -31,8 +31,8 @@
.my-legend { .my-legend {
position:fixed; position:fixed;
/* width: we set it equal or smaller than #lefttopbox width */ /* width: we set it equal or smaller than #lefttopbox width */
max-width:18%; max-width:20%;
max-height: 25%; max-height: 30%;
padding: 0 5px; padding: 0 5px;
overflow-y:scroll; overflow-y:scroll;
bottom:0; bottom:0;
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
opacity: 0.8; opacity: 0.8;
color:#250587; color:#250587;
margin: 7px; margin: 7px;
font-size:small; font-size:120%;
} }
.my-legend .legend-title { .my-legend .legend-title {
......
...@@ -51,39 +51,43 @@ TW.conf = (function(TW){ ...@@ -51,39 +51,43 @@ TW.conf = (function(TW){
// create facets ? // create facets ?
TWConf.scanClusters = true 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 // facetOptions: choose here the visual result of your node attributes
// -------------------------------------------------------------------
// 3 possible coloring functions // 3 possible coloring functions
// - cluster (contrasted colors for attributes describing *classes*) // - cluster (contrasted colors for attributes describing *classes*)
// - gradient (uniform map from a numeric attribute to red/yellow gradient) // - gradient (uniform map from a numeric attribute to red/yellow gradient)
// - heatmap (from blue to red/brown, centered on a white "neutral" color) // - heatmap (from blue to red, centered on a white "neutral" color)
// 2 possible binning modes // 3 possible binning modes
// - samerange: constant intervals between each bin // - 'samerange': constant intervals between each bin
// - samepop: constant cardinality inside each class (~ quantiles) // - '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 // - 'off' : no binning (each distinct value will be a legend item)
TWConf.facetOptions = { TWConf.facetOptions = {
// gexf | | custom |
// attribute | coloring | number | binning // attribute | coloring | number | binning
// name | function | of bins | mode // title | function | of bins | mode
// -------------------------------------------------------------------- // --------------------------------------------------------------------
'numuniform' : {'col': "gradient", 'n': 3, 'binmode': 'samerange'}, 'age' : {'col': "gradient", 'n': 2, 'binmode': 'samerange'},
'numpareto' : {'col': "gradient", 'n': 8, 'binmode': 'samepop' }, 'growth_rate' : {'col': "heatmap", 'n': 11, 'binmode': 'samepop' },
'intfewvalues' : {'col': "heatmap" , 'n': 4, 'binmode': 'samerange'}, 'PageRank' : {'col': "gradient", 'n': 8, 'binmode': 'samepop' },
'countryuniform':{'col': "cluster" }, '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 ? // other POSS option: display attribute value in label or not ?
...@@ -125,7 +129,7 @@ TW.conf = (function(TW){ ...@@ -125,7 +129,7 @@ TW.conf = (function(TW){
// ----------------------------- // -----------------------------
TWConf.filterSliders = true // show sliders for nodes/edges subsets 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 ? TWConf.deselectOnclickStage = true // click on background remove selection ?
// (except when dragging) // (except when dragging)
...@@ -245,7 +249,7 @@ TW.conf = (function(TW){ ...@@ -245,7 +249,7 @@ TW.conf = (function(TW){
// show verbose console logs... // show verbose console logs...
logFetchers: false, // ...about ajax/fetching of graph data logFetchers: false, // ...about ajax/fetching of graph data
logParsers: false, // ...about parsing said 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 logSettings: false, // ...about settings at Tina and Sigma init time
logSelections: false logSelections: false
} }
......
...@@ -756,7 +756,7 @@ var TinaWebJS = function ( sigmacanvas ) { ...@@ -756,7 +756,7 @@ var TinaWebJS = function ( sigmacanvas ) {
TW.partialGraph.camera.goTo({x:0, y:0, ratio:1.2}) TW.partialGraph.camera.goTo({x:0, y:0, ratio:1.2})
}); });
if (!TW.conf.colorsByAtt) { if (!TW.conf.clusterColorsAtt) {
$("#setcolorsMenu").hide() $("#setcolorsMenu").hide()
} }
......
...@@ -3,6 +3,118 @@ ...@@ -3,6 +3,118 @@
// always useful // always useful
var theHtml = document.getElementsByTagName('html')[0] 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) { function writeBrand (brandString) {
document.getElementById('twbrand').innerHTML = brandString document.getElementById('twbrand').innerHTML = brandString
} }
......
...@@ -44,19 +44,6 @@ var bipartiteD2N = {}; ...@@ -44,19 +44,6 @@ var bipartiteD2N = {};
var bipartiteN2D = {}; 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 ] ========= // // ======== [ what to do at start ] ========= //
console.log("Starting TWJS") console.log("Starting TWJS")
...@@ -255,17 +242,16 @@ function syncRemoteGraphData () { ...@@ -255,17 +242,16 @@ function syncRemoteGraphData () {
var preRES = AjaxSync({ url: infofile, datatype:"json" }); var preRES = AjaxSync({ url: infofile, datatype:"json" });
if (preRES['OK'] && preRES.data) { 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 = "" var first_file = "" , first_path = ""
for( var path in preRES.data ) { 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_file = preRES.data[path]["first"]
first_path = path first_path = path
console.log("db.json first_file", first_path, first_file)
break; break;
} }
......
...@@ -103,6 +103,7 @@ function gexfCheckAttributesMap (someXMLContent) { ...@@ -103,6 +103,7 @@ function gexfCheckAttributesMap (someXMLContent) {
// | <attributes class="node" mode="static"> // | <attributes class="node" mode="static">
// | <attribute id="0" title="category" type="string"></attribute> // | <attribute id="0" title="category" type="string"></attribute>
// | <attribute id="1" title="country" type="float"></attribute> // | <attribute id="1" title="country" type="float"></attribute>
// | <attribute id="mod_class" title="Modularity Class" type="float"></attribute>
// | </attributes> // | </attributes>
// (...) // (...)
...@@ -127,7 +128,7 @@ function gexfCheckAttributesMap (someXMLContent) { ...@@ -127,7 +128,7 @@ function gexfCheckAttributesMap (someXMLContent) {
type = attributeNode.getAttribute('type'); type = attributeNode.getAttribute('type');
// ex: id = "in-degree" or "3" <= can be an int to be resolved into the title // 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" // ex: type = "string"
var attribute = { var attribute = {
...@@ -294,6 +295,9 @@ function facetsBinning (valuesIdx) { ...@@ -294,6 +295,9 @@ function facetsBinning (valuesIdx) {
for (var at in valuesIdx[cat]) { for (var at in valuesIdx[cat]) {
if (TW.conf.debug.logFacets) console.log(`======= ${cat}::${at} =======`) 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 // new array of valueclass/interval/bin objects
facetIdx[cat][at] = [] facetIdx[cat][at] = []
...@@ -333,9 +337,11 @@ function facetsBinning (valuesIdx) { ...@@ -333,9 +337,11 @@ function facetsBinning (valuesIdx) {
workingVals = valuesIdx[cat][at].vals.vstr workingVals = valuesIdx[cat][at].vals.vstr
} }
if (TW.conf.debug.logFacets) {
console.debug("datatyping:", dataType) console.debug("datatyping:", dataType)
console.debug("valuesIdx after datatyping:", valuesIdx[cat][at]) // console.debug("valuesIdx after datatyping:", valuesIdx[cat][at])
console.debug("workingVals after datatyping:", workingVals) // console.debug("workingVals after datatyping:", workingVals)
}
// default options // default options
let maxDiscreteValues = TW.conf.maxDiscreteValues let maxDiscreteValues = TW.conf.maxDiscreteValues
...@@ -353,14 +359,27 @@ function facetsBinning (valuesIdx) { ...@@ -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`) 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 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 // POSSible: auto-detect if vtypes ==> color
// else { // 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 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) { for (var pval in valuesIdx[cat][at].map) {
var idList = valuesIdx[cat][at].map[pval] var idList = valuesIdx[cat][at].map[pval]
...@@ -416,12 +435,18 @@ function facetsBinning (valuesIdx) { ...@@ -416,12 +435,18 @@ function facetsBinning (valuesIdx) {
if (TW.conf.debug.logFacets) console.debug("intervals for", at, legendRefTicks, "(list of minima)") if (TW.conf.debug.logFacets) console.debug("intervals for", at, legendRefTicks, "(list of minima)")
// the unique-d array will allow us to group ranges // the unique-d array will serve as a todolist with lastCursor and k
var sortedDistinctVals = Object.keys(valuesIdx[cat][at].map).sort(function(a,b){return Number(a)-Number(b)}) // 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 nTicks = legendRefTicks.length
var nDistinctVals = sortedDistinctVals.length
var lastCursor = 0 var lastCursor = 0
// create ticks objects with retrieved full info // create ticks objects with retrieved full info
...@@ -449,9 +474,25 @@ function facetsBinning (valuesIdx) { ...@@ -449,9 +474,25 @@ function facetsBinning (valuesIdx) {
'range':[lowThres, hiThres] 'range':[lowThres, hiThres]
} }
if (TW.conf.debug.logFacets) console.debug("...new interval:",[lowThres, hiThres])
// 1) union of idmaps // 1) union of idmaps
for (var k = lastCursor ; k <= nDistinctVals ; k++) { for (var k = lastCursor ; k <= nDistinctVals ; k++) {
var val = Number(sortedDistinctVals[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) { if (val < lowThres) {
console.error("mixup !!", val, lowThres, at) console.error("mixup !!", val, lowThres, at)
} }
...@@ -470,8 +511,12 @@ function facetsBinning (valuesIdx) { ...@@ -470,8 +511,12 @@ function facetsBinning (valuesIdx) {
// we're over the interval upper bound // we're over the interval upper bound
else if (val >= hiThres) { else if (val >= hiThres) {
// console.log("over hiThres", val, hiThres)
// normal case // normal case
if (binningMode != 'samerange' || l != nTicks-1 ) { if (binningMode != 'samerange' || l != nTicks-1 ) {
// console.log("...moving on to next interval")
// we just need to remember where we were for next interval // we just need to remember where we were for next interval
lastCursor = k lastCursor = k
break break
...@@ -479,6 +524,8 @@ function facetsBinning (valuesIdx) { ...@@ -479,6 +524,8 @@ function facetsBinning (valuesIdx) {
// samerange && last interval case: inclusive last interval upper bound // samerange && last interval case: inclusive last interval upper bound
else { else {
// console.warn("last interval for samepop")
for (var j in valuesIdx[cat][at].map[val]) { for (var j in valuesIdx[cat][at].map[val]) {
newTick.nids.push(valuesIdx[cat][at].map[val][j]) newTick.nids.push(valuesIdx[cat][at].map[val][j])
} }
...@@ -878,6 +925,11 @@ function updateValueFacets(facetIdx, aNode) { ...@@ -878,6 +925,11 @@ function updateValueFacets(facetIdx, aNode) {
if (!facetIdx[aNode.type]) facetIdx[aNode.type]={} if (!facetIdx[aNode.type]) facetIdx[aNode.type]={}
for (var at in aNode.attributes) { 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] let val = aNode.attributes[at]
if (!facetIdx[aNode.type][at]) facetIdx[aNode.type][at]={'vals':{'vstr':[], 'vnum':[]},'map':{}} if (!facetIdx[aNode.type][at]) facetIdx[aNode.type][at]={'vals':{'vstr':[], 'vnum':[]},'map':{}}
...@@ -1106,11 +1158,13 @@ function dictfyJSON( data , categories ) { ...@@ -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 ){ for (var i in nodes ){
if (nodes[i].color=="#FFFFFF") { if (nodes[i].color=="#FFFFFF") {
var attval = ( isUndef(nodes[i].attributes) || isUndef(nodes[i].attributes["clust_default"]) )? 0 : nodes[i].attributes["clust_default"] ; 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) { ...@@ -760,7 +760,7 @@ function edgeInfos(anEdge) {
} }
function clustersBy(daclass) { function gradientColoring(daclass) {
cancelSelection(false); cancelSelection(false);
...@@ -887,15 +887,29 @@ function repaintEdges() { ...@@ -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 // heatmap from cold to warm with middle white
// - number of bins can be specified by attribute name in TW.conf.customLegendsBins // (good for values centered around a neutral zone)
function colorsRelByBins(daclass) { // 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 binColors
var doModifyLabel = false var doModifyLabel = false
var actypes = getActivetypes() 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 // we have no specifications yet for colors and legends on multiple types
if (actypes.length > 1) { if (actypes.length > 1) {
console.warn("colors by bins will only color nodes of type 0") console.warn("colors by bins will only color nodes of type 0")
...@@ -906,7 +920,14 @@ function colorsRelByBins(daclass) { ...@@ -906,7 +920,14 @@ function colorsRelByBins(daclass) {
// our binning // our binning
var tickThresholds = TW.Clusters[ty][daclass] 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 = {} var totalsPerBinMin = {}
// let's go // let's go
...@@ -915,51 +936,16 @@ function colorsRelByBins(daclass) { ...@@ -915,51 +936,16 @@ function colorsRelByBins(daclass) {
// global flag // global flag
TW.handpickedcolor = true 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
];
}
// verification
if (tickThresholds.length != binColors.length) {
console.warn (`colorsRelByBins setup mismatch: TW.Clusters ticks ${tickThresholds} should == nColors ${binColors.length}`)
}
// use our valueclass => ids mapping // use our valueclass => ids mapping
for (var k in tickThresholds) { for (var k in tickThresholds) {
// console.debug('tick infos', tickThresholds[k]) // console.debug('tick infos', tickThresholds[k])
// 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]} // ex: {labl: "terms||growth_rate||[0 ; 0.583]", nids: Array(99), range: [0 ; 0.583210]}
totalsPerBinMin[tickThresholds[k].range[0]] = tickThresholds[k].nids.length totalsPerBinMin[tickThresholds[k].range[0]] = tickThresholds[k].nids.length
...@@ -984,8 +970,6 @@ function colorsRelByBins(daclass) { ...@@ -984,8 +970,6 @@ function colorsRelByBins(daclass) {
} }
} }
// console.debug(valArray)
console.info('coloring distribution per tick thresholds' , totalsPerBinMin) console.info('coloring distribution per tick thresholds' , totalsPerBinMin)
// Edge precompute alt_rgb by new source-target nodes-colours combination // Edge precompute alt_rgb by new source-target nodes-colours combination
...@@ -997,189 +981,12 @@ function colorsRelByBins(daclass) { ...@@ -997,189 +981,12 @@ function colorsRelByBins(daclass) {
} }
function clusterColoring(daclass) {
// 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}`)
}
// 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])
) {
var valSt = n.attributes[daclass]
var originalLabel = TW.Nodes[n.id].label
if (doModifyLabel) {
n.label = `(${valSt}) ${originalLabel}`
}
else {
n.label = originalLabel
}
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();
}
function colorsBy(daclass) {
console.log("") console.log("")
console.log(" = = = = = = = = = = = = = = = = = ") console.log(" = = = = = = = = = = = = = = = = = ")
console.log(" = = = = = = = = = = = = = = = = = ") console.log(" = = = = = = = = = = = = = = = = = ")
console.log("colorsBy ( "+daclass+" )") console.log("clusterColoring ( "+daclass+" )")
console.log(" = = = = = = = = = = = = = = = = = ") console.log(" = = = = = = = = = = = = = = = = = ")
console.log(" = = = = = = = = = = = = = = = = = ") console.log(" = = = = = = = = = = = = = = = = = ")
console.log("") console.log("")
...@@ -1206,7 +1013,7 @@ function colorsBy(daclass) { ...@@ -1206,7 +1013,7 @@ function colorsBy(daclass) {
} }
else { else {
// shuffle on entire array is better than random sorting function on each element // 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) { for(var j in TW.nodeIds) {
var the_node = TW.Nodes[ TW.nodeIds[j] ] var the_node = TW.Nodes[ TW.nodeIds[j] ]
...@@ -1214,11 +1021,10 @@ function colorsBy(daclass) { ...@@ -1214,11 +1021,10 @@ function colorsBy(daclass) {
// ££TODO put louvain in graph.nodes() like other attrs ?? // ££TODO put louvain in graph.nodes() like other attrs ??
// then possible to use TW.partialGraph.graph.nodes(TW.nodeIds[j]) // then possible to use TW.partialGraph.graph.nodes(TW.nodeIds[j])
if (j< 15) // if (j< 15)
console.log(` ${TW.nodeIds[j]} : hidden?${the_node.hidden} the_node.attributes[daclass] = ${the_node.attributes[daclass]}`) // final val=${attval} // console.log(` ${TW.nodeIds[j]} : hidden?${the_node.hidden} the_node.attributes[daclass] = ${the_node.attributes[daclass]}`) // final val=${attval}
if (! the_node.hidden) { 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]; 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]).color = randomColorList[ attval ]
TW.partialGraph.graph.nodes(TW.nodeIds[j]).customAttrs.alt_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