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.
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
- 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)
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 @@
<select id="choose-attr" name="choose-attr"
class="custom-select form-control">
<option selected value="0"></option>
<!-- filled by fillAttrsInForm() -->
<!-- filled by fillAttrsInForm(.) -->
</select>
</div>
</div>
......@@ -616,7 +616,8 @@
<div class="input-group">
<label for="attr-col" class="smlabel input-group-addon">Coloring function</label>
<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 value="cluster">cluster</option>
<option value="gradient">gradient</option>
......@@ -625,11 +626,33 @@
</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="input-group">
<label for="attr-binmode" class="smlabel input-group-addon">Binning Mode</label>
<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 value="off">No binning</option>
<option value="samerange">Same range between ticks</option>
......@@ -665,7 +688,7 @@
Cancel
</button>
<button class="btn btn-primary"
type=button onclick="newAttrConf()" data-dismiss="modal">
type=button onclick="newAttrConfAndColor()" data-dismiss="modal">
Ok
</button>
</div>
......
......@@ -3,6 +3,15 @@
*/
// = = = = = = = = = = = [ Clusters Plugin ] = = = = = = = = = = = //
// settings to function name
TW.gui.colorFuns = {
'heatmap': "heatmapColoring",
'gradient': "gradientColoring",
'cluster': "clusterColoring"
}
// Execution: changeGraphAppearanceByFacets( true )
// It reads scanned node-attributes and prepared legends in TW.Clusters
// to add the button in the html with the sigmaUtils.gradientColoring(x) listener.
......@@ -11,13 +20,6 @@ function changeGraphAppearanceByFacets( manualflag ) {
if ( !isUndef(manualflag) && !TW.conf.colorByAtt ) TW.conf.colorByAtt = manualflag;
if(!TW.conf.colorByAtt) return;
// settings to function name
var colorFuns = {
'heatmap': "heatmapColoring",
'gradient': "gradientColoring",
'cluster': "clusterColoring"
}
let currentNbNodes = TW.partialGraph.graph.nNodes()
// create colormenu and 1st default entry
......@@ -55,7 +57,7 @@ function changeGraphAppearanceByFacets( manualflag ) {
// read from user settings
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
......@@ -302,7 +304,37 @@ function set_ClustersLegend ( daclass, groupedByTicks ) {
// create the legend item
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
var valueclassId = `${curType}::${daclass}::${l}`
......@@ -671,12 +703,18 @@ function activateModules() {
// 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()
for (let tid in actypes) {
let ty = actypes[tid]
let elChooser = document.getElementById('choose-attr')
let elChooser = document.getElementById(menuId)
// remove the possible previous options from possible previous graphs
while (elChooser.lastChild) {
......@@ -685,24 +723,33 @@ function fillAttrsInForm() {
// each facet family or clustering type was already prepared
for (let att in TW.Clusters[ty]) {
let opt = document.createElement('option')
opt.value = att
opt.innerText = att
elChooser.appendChild(opt)
if (!optionalAttTypeConstraint
|| ( TW.Clusters[ty][att].meta.dataType
&& TW.Clusters[ty][att].meta.dataType == optionalAttTypeConstraint)) {
let opt = document.createElement('option')
opt.value = att
opt.innerText = att
elChooser.appendChild(opt)
}
}
}
}
function binmodeOpenNBins() {
let mainq = document.getElementById('attr-binmode')
let subq = document.getElementById('attr-nbins-div')
if (mainq.value == "samepop" || mainq.value == "samerange") {
subq.style.display = 'block'
}
else {
subq.style.display = 'none'
// for optional questions:
// ( displays subQuestion iff mainQuestion has one of the mainQOkValues )
function conditiOpen(subQId, mainQId, mainQOkValues) {
let mainq = document.getElementById(mainQId)
let subq = document.getElementById(subQId)
let triggerVal = false
for (let i in mainQOkValues) {
if (mainq.value == mainQOkValues[i]) {
triggerVal = true
break
}
}
// show or not
subq.style.display = triggerVal ? 'block' : 'none'
}
function showAttrConf() {
......@@ -716,13 +763,22 @@ function showAttrConf() {
document.getElementById('attr-nbins-div').style.display = 'block'
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
function newAttrConf() {
function newAttrConfAndColor() {
let attrTitle = document.getElementById('choose-attr').value
// read values from GUI
......@@ -730,7 +786,11 @@ function newAttrConf() {
'col': document.getElementById('attr-col').value,
'binmode': document.getElementById('attr-binmode').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
......@@ -757,9 +817,13 @@ function newAttrConf() {
TW.Clusters[ty][attrTitle] = newClustering[ty][attrTitle]
}
// console.log("reparse raw result", tmpVals)
// console.log("reparse binned result", newClustering)
// update the GUI menu
changeGraphAppearanceByFacets(true)
// console.log("reparse raw result", tmpVals)
// console.log("reparse binned result", newClustering)
// run the new color
let colMethod = TW.gui.colorFuns[TW.conf.facetOptions[attrTitle]['col']]
window[colMethod](attrTitle)
}
......@@ -1026,11 +1026,10 @@ var TinaWebJS = function ( sigmacanvas ) {
}
}
// attributes' facet-options init & handler
fillAttrsInForm('choose-attr')
fillAttrsInForm('choose-titling-metric', 'metric')
document.getElementById('choose-attr').onchange = showAttrConf
fillAttrsInForm('attr-titling-metric', 'num')
// cancelSelection(false);
}
......
......@@ -561,6 +561,13 @@ function facetsBinning (valuesIdx) {
'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
......
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