Commit c764e903 authored by Romain Loth's avatar Romain Loth

new feature: titling of clusters

via user-configuration of a ranking metric to choose k best labels from the cluster's node set (available from the facet-options dialog box)
parent dda561c3
...@@ -99,5 +99,16 @@ These choices can be specified in the conf `facetOptions` entry. ...@@ -99,5 +99,16 @@ 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. 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.
These 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
- the mapping from attribute values to matching nodes is always in TW.Clusters.aType.anAttr.aClass.map - the mapping from attribute values to matching nodes is always in `TW.Clusters.aType.anAttr.invIdx.aClass.nids`
(where aClass is the chosen interval or distinct value) (where aClass is the chosen interval or distinct value)
We currently store the datatype of the observed values in `TW.Clusters.aType.anAttr.meta`
- the source datatype is always string in gexf, but real type ("vtype") can be numeric
- (ie numeric cast doesn't give NaN or it do so very rarely over the values)
NB: the use cases for stats go beyond numeric vs string ! we could easily autodiagnose in facetsBinning between :
- vnum with many distinct values => assumed continuous metric (useful for gradient or titling)
- vnum with few distinct values => assumed classes var
- vstr with few distinct values => assumed classes var
- vstr with many distinct values => assumed classes with zipf freq => create an "others" for the tail
...@@ -607,7 +607,7 @@ ...@@ -607,7 +607,7 @@
<select id="choose-attr" name="choose-attr" <select id="choose-attr" name="choose-attr"
class="custom-select form-control"> class="custom-select form-control">
<option selected value="0"></option> <option selected value="0"></option>
<!-- filled by fillAttrsInForm() --> <!-- filled by fillAttrsInForm(.) -->
</select> </select>
</div> </div>
</div> </div>
...@@ -616,7 +616,8 @@ ...@@ -616,7 +616,8 @@
<div class="input-group"> <div class="input-group">
<label for="attr-col" class="smlabel input-group-addon">Coloring function</label> <label for="attr-col" class="smlabel input-group-addon">Coloring function</label>
<select id="attr-col" name="attr-col" <select id="attr-col" name="attr-col"
class="custom-select form-control"> class="custom-select form-control"
onchange="conditiOpen('choose-titling-div', 'attr-col',['cluster'])">
<option selected value="0"></option> <option selected value="0"></option>
<option value="cluster">cluster</option> <option value="cluster">cluster</option>
<option value="gradient">gradient</option> <option value="gradient">gradient</option>
...@@ -625,11 +626,33 @@ ...@@ -625,11 +626,33 @@
</div> </div>
</div> </div>
<!-- this titling functionality is like a head -n inside each class -->
<div class="question conditional-q" id="choose-titling-div">
<p class="legend">Please choose a size/weight metric used to rank the "title-candidates" inside each cluster.</p>
<div class="input-group">
<label for="attr-titling-metric" class="smlabel input-group-addon">Titling metric</label>
<select id="attr-titling-metric" name="attr-titling-metric"
class="custom-select form-control">
<option selected value="size">Current size</option>
<!-- other attrs filled by fillAttrsInForm(., 'num') -->
</select>
</div>
<div class="input-group" style="margin-top:.5em;">
<label for="attr-titling-n" class="smlabel input-group-addon">Number of terms</label>
<input id="attr-titling-n" name="attr-titling-n" maxlength="2"
type="text" class="form-control autocomp" placeholder="number of node labels to pick as title (1-10)"
onchange="let castint = parseInt(this.value) ; if (castint != this.value || castint < 1) {this.value = 1} else if(castint > 10) {this.value = 10}"
>
</div>
</div>
<div class="question"> <div class="question">
<div class="input-group"> <div class="input-group">
<label for="attr-binmode" class="smlabel input-group-addon">Binning Mode</label> <label for="attr-binmode" class="smlabel input-group-addon">Binning Mode</label>
<select id="attr-binmode" name="attr-binmode" <select id="attr-binmode" name="attr-binmode"
class="custom-select form-control" onchange="binmodeOpenNBins()"> class="custom-select form-control" onchange="conditiOpen('attr-nbins-div', 'attr-binmode',['samerange', 'samepop'])">
<option selected value="0"></option> <option selected value="0"></option>
<option value="off">No binning</option> <option value="off">No binning</option>
<option value="samerange">Same range between ticks</option> <option value="samerange">Same range between ticks</option>
...@@ -665,7 +688,7 @@ ...@@ -665,7 +688,7 @@
Cancel Cancel
</button> </button>
<button class="btn btn-primary" <button class="btn btn-primary"
type=button onclick="newAttrConf()" data-dismiss="modal"> type=button onclick="newAttrConfAndColor()" data-dismiss="modal">
Ok Ok
</button> </button>
</div> </div>
......
...@@ -3,6 +3,15 @@ ...@@ -3,6 +3,15 @@
*/ */
// = = = = = = = = = = = [ Clusters Plugin ] = = = = = = = = = = = // // = = = = = = = = = = = [ Clusters Plugin ] = = = = = = = = = = = //
// settings to function name
TW.gui.colorFuns = {
'heatmap': "heatmapColoring",
'gradient': "gradientColoring",
'cluster': "clusterColoring"
}
// 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.gradientColoring(x) listener. // to add the button in the html with the sigmaUtils.gradientColoring(x) listener.
...@@ -11,13 +20,6 @@ function changeGraphAppearanceByFacets( manualflag ) { ...@@ -11,13 +20,6 @@ function changeGraphAppearanceByFacets( manualflag ) {
if ( !isUndef(manualflag) && !TW.conf.colorByAtt ) TW.conf.colorByAtt = manualflag; if ( !isUndef(manualflag) && !TW.conf.colorByAtt ) TW.conf.colorByAtt = manualflag;
if(!TW.conf.colorByAtt) return; if(!TW.conf.colorByAtt) return;
// settings to function name
var colorFuns = {
'heatmap': "heatmapColoring",
'gradient': "gradientColoring",
'cluster': "clusterColoring"
}
let currentNbNodes = TW.partialGraph.graph.nNodes() let currentNbNodes = TW.partialGraph.graph.nNodes()
// create colormenu and 1st default entry // create colormenu and 1st default entry
...@@ -55,7 +57,7 @@ function changeGraphAppearanceByFacets( manualflag ) { ...@@ -55,7 +57,7 @@ function changeGraphAppearanceByFacets( manualflag ) {
// read from user settings // read from user settings
if (TW.conf.facetOptions[attTitle] && TW.conf.facetOptions[attTitle]['col']) { if (TW.conf.facetOptions[attTitle] && TW.conf.facetOptions[attTitle]['col']) {
colMethod = colorFuns[TW.conf.facetOptions[attTitle]['col']] colMethod = TW.gui.colorFuns[TW.conf.facetOptions[attTitle]['col']]
} }
// fallback guess-values // fallback guess-values
...@@ -302,7 +304,37 @@ function set_ClustersLegend ( daclass, groupedByTicks ) { ...@@ -302,7 +304,37 @@ function set_ClustersLegend ( daclass, groupedByTicks ) {
// create the legend item // create the legend item
var preparedLabel = legendInfo[l]['labl'] var preparedLabel = legendInfo[l]['labl']
// console.log("preparedLabel", preparedLabel)
// we add a title to cluster classes
if (TW.conf.facetOptions[daclass] && TW.conf.facetOptions[daclass].col == 'cluster') {
// let t0 = performance.now()
let titles = []
let theRankingAttr = TW.conf.facetOptions[daclass].titlingMetric
let maxLen = TW.conf.facetOptions[daclass].titlingNTerms
for (let j in legendInfo[l]['nids']) {
let n = TW.partialGraph.graph.nodes(legendInfo[l]['nids'][j])
let lastMax = 0
if (titles.length) {
// we keep titles sorted for this
lastMax = titles.slice(-1)[0].val
}
if (n.attributes[theRankingAttr] > lastMax) {
titles.push({'key':n.label, 'val':n.attributes[theRankingAttr]})
}
titles.sort(function(a,b) {return b.val - a.val})
titles = titles.slice(0,maxLen)
}
// adding those k best titles to the legend
preparedLabel += " ["+titles.map(function(x){return x.key}).join('/')+"...]"
// console.log("finding title perf", performance.now() - t0, titles)
}
// all-in-one argument for SomeEffect // all-in-one argument for SomeEffect
var valueclassId = `${curType}::${daclass}::${l}` var valueclassId = `${curType}::${daclass}::${l}`
...@@ -671,12 +703,18 @@ function activateModules() { ...@@ -671,12 +703,18 @@ function activateModules() {
// Settings edition // Settings edition
// ================= // =================
function fillAttrsInForm() {
// creates a list of <options> for all present attributes
// (or a sublist on a meta.dataType condition
// NB condition on dataType could be on an extended meta "attrType"
// cf. doc/developer_manual.md autodiagnose remark)
function fillAttrsInForm(menuId, optionalAttTypeConstraint) {
var actypes = getActivetypes() var actypes = getActivetypes()
for (let tid in actypes) { for (let tid in actypes) {
let ty = actypes[tid] let ty = actypes[tid]
let elChooser = document.getElementById('choose-attr') let elChooser = document.getElementById(menuId)
// remove the possible previous options from possible previous graphs // remove the possible previous options from possible previous graphs
while (elChooser.lastChild) { while (elChooser.lastChild) {
...@@ -685,24 +723,33 @@ function fillAttrsInForm() { ...@@ -685,24 +723,33 @@ function fillAttrsInForm() {
// each facet family or clustering type was already prepared // each facet family or clustering type was already prepared
for (let att in TW.Clusters[ty]) { for (let att in TW.Clusters[ty]) {
let opt = document.createElement('option') if (!optionalAttTypeConstraint
opt.value = att || ( TW.Clusters[ty][att].meta.dataType
opt.innerText = att && TW.Clusters[ty][att].meta.dataType == optionalAttTypeConstraint)) {
elChooser.appendChild(opt) let opt = document.createElement('option')
opt.value = att
opt.innerText = att
elChooser.appendChild(opt)
}
} }
} }
} }
// for optional questions:
function binmodeOpenNBins() { // ( displays subQuestion iff mainQuestion has one of the mainQOkValues )
let mainq = document.getElementById('attr-binmode') function conditiOpen(subQId, mainQId, mainQOkValues) {
let subq = document.getElementById('attr-nbins-div') let mainq = document.getElementById(mainQId)
if (mainq.value == "samepop" || mainq.value == "samerange") { let subq = document.getElementById(subQId)
subq.style.display = 'block'
} let triggerVal = false
else { for (let i in mainQOkValues) {
subq.style.display = 'none' if (mainq.value == mainQOkValues[i]) {
triggerVal = true
break
}
} }
// show or not
subq.style.display = triggerVal ? 'block' : 'none'
} }
function showAttrConf() { function showAttrConf() {
...@@ -716,13 +763,22 @@ function showAttrConf() { ...@@ -716,13 +763,22 @@ function showAttrConf() {
document.getElementById('attr-nbins-div').style.display = 'block' document.getElementById('attr-nbins-div').style.display = 'block'
document.getElementById('attr-nbins').value = settings.n || 5 document.getElementById('attr-nbins').value = settings.n || 5
} }
if(settings.col == 'cluster') {
document.getElementById('choose-titling-div').style.display = 'block'
document.getElementById('attr-titling-metric').value = settings.titlingMetric || ''
document.getElementById('attr-titling-n').value = settings.titlingNTerms || 1
// no sense to ordinally bin clusters
document.getElementById('attr-binmode').value = "off"
document.getElementById('attr-binmode').disabled = true
}
} }
} }
// writes new attribute configuration from user form AND recreates facet bins // writes new attribute configuration from user form, recreates facet bins AND runs the new color
// processing time: ~~ 1.5 ms for 100 nodes // processing time: ~~ 1.5 ms for 100 nodes
function newAttrConf() { function newAttrConfAndColor() {
let attrTitle = document.getElementById('choose-attr').value let attrTitle = document.getElementById('choose-attr').value
// read values from GUI // read values from GUI
...@@ -730,7 +786,11 @@ function newAttrConf() { ...@@ -730,7 +786,11 @@ function newAttrConf() {
'col': document.getElementById('attr-col').value, 'col': document.getElementById('attr-col').value,
'binmode': document.getElementById('attr-binmode').value, 'binmode': document.getElementById('attr-binmode').value,
'n': document.getElementById('attr-nbins').value, 'n': document.getElementById('attr-nbins').value,
'menutransl': document.getElementById('attr-translation').value 'menutransl': document.getElementById('attr-translation').value,
// only for clusterings (ie currently <=> (col == "cluster"))
'titlingMetric': document.getElementById('attr-titling-metric').value,
'titlingNTerms': document.getElementById('attr-titling-n').value || 1
} }
// find the corresponding types // find the corresponding types
...@@ -757,9 +817,13 @@ function newAttrConf() { ...@@ -757,9 +817,13 @@ function newAttrConf() {
TW.Clusters[ty][attrTitle] = newClustering[ty][attrTitle] TW.Clusters[ty][attrTitle] = newClustering[ty][attrTitle]
} }
// console.log("reparse raw result", tmpVals)
// console.log("reparse binned result", newClustering)
// update the GUI menu // update the GUI menu
changeGraphAppearanceByFacets(true) changeGraphAppearanceByFacets(true)
// console.log("reparse raw result", tmpVals) // run the new color
// console.log("reparse binned result", newClustering) let colMethod = TW.gui.colorFuns[TW.conf.facetOptions[attrTitle]['col']]
window[colMethod](attrTitle)
} }
...@@ -1026,11 +1026,10 @@ var TinaWebJS = function ( sigmacanvas ) { ...@@ -1026,11 +1026,10 @@ var TinaWebJS = function ( sigmacanvas ) {
} }
} }
// attributes' facet-options init & handler // attributes' facet-options init & handler
fillAttrsInForm('choose-attr') fillAttrsInForm('choose-attr')
fillAttrsInForm('choose-titling-metric', 'metric')
document.getElementById('choose-attr').onchange = showAttrConf document.getElementById('choose-attr').onchange = showAttrConf
fillAttrsInForm('attr-titling-metric', 'num')
// cancelSelection(false); // cancelSelection(false);
} }
......
...@@ -561,6 +561,13 @@ function facetsBinning (valuesIdx) { ...@@ -561,6 +561,13 @@ function facetsBinning (valuesIdx) {
'nids': valuesIdx[cat][at].map['_non_numeric_'], 'nids': valuesIdx[cat][at].map['_non_numeric_'],
}) })
} }
// store this attribute's metadata
facetIdx[cat][at].meta.dataType = dataType
// POSS: here we could distinguish more precise attr types
// numeric continuous vs. discrete etc.
// cf. doc/developer_manual.md autodiagnose remark
} }
// 'clust_default' is an alias to the user-defined default clustering // 'clust_default' is an alias to the user-defined default clustering
......
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