Skip to content

Instantly share code, notes, and snippets.

@mhebrard
Last active January 12, 2018 03:50
Show Gist options
  • Save mhebrard/7e4dda210943c2c7f00c87fed7dbe33f to your computer and use it in GitHub Desktop.
Save mhebrard/7e4dda210943c2c7f00c87fed7dbe33f to your computer and use it in GitHub Desktop.
iCLiKVAL 3D graph

iCLiKVAL three-Dimensions graph

This representation shows the content of iCLiKVAL database in a Force layout 3D.

Notes:

  • The keyword use for search will be represented as a octahedron
  • Each media will be represented as a cube, its color refers to its type (see legend).
  • Each key will be represented as a cone.
  • Each value will be represented as a sphere.
  • Each annotation will be represented as a cylinder.
  • The size of each node is proportional (log scale) to the number of annotations of this node.
  • Hover on one node will display additional information at the top right corner.
  • Click on one node will open iCLiKVAL web site on the corresponding page.

Use case:

  1. Enter a keyword in the search field (bottom) and click on search button
  • The app request iCLiKVAL and draw a graph
  • The keyword is plotted at the center of the graph
  • Each media is connected to the keyword by one link
  1. More Media
  • By default the app request all media. (redraw the graph each 10 media)
  • You can see the progression bar for media at the top left corner
  • Click on pause button to stop the request
  • Click on play button to restart the request
  1. Select Mode: Keys using the drop down menu at the bottom.
  • The app request iCLiKVAL for some annotations and draw another graph.
  • Media and keys are linked when an annotation describing the media, use the key
  1. More Annotations
  • By default the app request 25 annotations by media.
  • You can see the progression bar for annotations at the top left corner
  • Click on play button to request more annotations
  • Click on pause button to stop the request
  1. Select Mode: Values using the drop down menu at the bottom.
  • The app draw another graph.
  • Media and values are linked when an annotation describing the media, use the value
  1. Select Mode: Keys + Values using the drop down menu at the bottom.
  • The app draw another graph.
  • Media, keys and values are linked when an annotation describing the media, use the key and the value
  1. Select Mode: Annotations using the drop down menu at the bottom.
  • The app draw another graph.
  • Media and annotation are linked when an annotation describing the media, use the same key and value pair
  1. You can mouse over any item to display it's title
  2. If you modify the Filter parameter, the graph is redraw excluding the media, keys, values or annotations involves in less than "filter" annotations.
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>iCLiKVAL 3D Graph</title>
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro" rel="stylesheet">
<style>
body {
font-family: 'Source Sans Pro', sans-serif;
color: #eee;
margin: 0;
padding: 0;
overflow: hidden;
}
.header {
position: absolute;
top: 0px;
padding: 5px;
width: 100%;
text-align: center;
display: flex;
justify-content: center;
}
.left {
position: absolute;
top: 5px;
left: 5px;
}
.right {
position: absolute;
top: 5px;
right: 5px;
}
.footer {
position: absolute;
bottom: 15px;
padding: 5px;
width:100%;
text-align: center;
display: flex;
justify-content: center;
}
.content {
background-color: rgba(0,0,0,0.7);
padding: 5px;
}
.tronbox {
border-radius: 3px;
border: 1px #80EAFF solid;
box-shadow: 0 0 3px #22AFCA, inset 0 0 3px #22AFCA;
}
.flex {
display: flex;
align-items: center;
}
.progress-content {
width: 150px;
height: 12px;
border-radius: 3px;
margin: 2px 3px;
}
.progress-bar {
width: 0%;
height: 100%;
border-radius: 3px;
box-shadow: inset -3px 0 2px #80EAFF;
background: #22AFCA;
}
.progress-value {
float: left;
width: 100%;
text-align: center;
font-size: 10px;
cursor:pointer;
}
#txt-filter {
width:50px;
}
/* Tooltip */
#tip {
position:absolute;
z-index:3;
padding:10px;
pointer-events:none;
opacity:0;
max-width: 30%;
}
#tip label {
font-weight: bold;
}
</style>
<!-- D3js v4 -->
<script src="https://d3js.org/d3-collection.v1.min.js"></script>
<script src="https://d3js.org/d3-color.v1.min.js"></script>
<script src="https://d3js.org/d3-dispatch.v1.min.js"></script>
<script src="https://d3js.org/d3-interpolate.v1.min.js"></script>
<script src="https://d3js.org/d3-request.v1.min.js"></script>
<script src="https://d3js.org/d3-scale.v1.min.js"></script>
<script src="https://d3js.org/d3-selection.v1.min.js"></script>
<!-- Other -->
<script src="https://unpkg.com/3d-force-graph"></script>
<script src="https://unpkg.com/d3-octree"></script>
<script src="https://unpkg.com/d3-force-3d"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/89/three.min.js"></script>
<script src="https://use.fontawesome.com/b6ac3d3b75.js"></script>
</head>
<body>
<div class="header">
<div class="content tronbox">
<h1>iCLiKVAL 3D Graph</h1>
</div>
</div>
<div class="left content tronbox">
<div id="progress-media" class="flex">
<span>Media:</span>
<div class="progress-content tronbox">
<div class="progress-value">0</div>
<div class="progress-bar"></div>
</div>
<span class="fa" style="cursor:pointer"></span>
</div>
<div id="progress-annot" class="flex">
<span>Annot:</span>
<div class="progress-content tronbox">
<div class="progress-value">0</div>
<div class="progress-bar"></div>
</div>
<span class="fa fa-caret-right" style="cursor:pointer"></span>
</div>
<div id="log"></div>
<div>
<span>Legend</span>
<span id="btn-legend" class="fa" style="cursor:pointer"></span>
<ul id="txt-legend" class="fa-ul"></ul>
</div>
</div>
<div id='tip' class='right content tronbox'></div>
<div class="footer">
<div class="content tronbox flex">
<div>
<input type="text" id="txt-search"/>
<button type="button" id="btn-search">Search</button>
</div>
<div> | </div>
<div>
<label> Mode: </label>
<select id="btn-mode">
<option value='search' selected>Keyword</option>
<option value='keys'>Keys</option>
<option value='values'>Values</option>
<option value='keyval'>Keys + Values</option>
<option value='annot'>Annotations</option>
</select>
</div>
<div> | </div>
<div>
<label> Filter: </lable>
<input type="number" id="txt-filter" min="0" value="1" title="Hide nodes with no more that x links"/>
</div>
</div>
</div>
<div id="chart"></div>
<script type="text/javascript">
// Global parameters
var p = {
scaleRange: [1, 5], // geometry size scale
types: { // Colors
root: {label: 'Root', name: 'Term', fg: '#000', bg: '#ccc', geometry: 'octahedron'},
journal_article: {label: 'Article', name: 'Title', fg: '#1f78b4', bg: '#a6cee3', geometry: 'box'},
audio: {label: 'Audio', name: 'Title', fg: '#ff7f00', bg: '#fdbf6f', geometry: 'box'},
dataset: {label: 'Dataset', name: 'Title', fg: '#33a02c', bg: '#b2df8a', geometry: 'box'},
image: {label: 'Image', name: 'Title', fg: '#6a3d9a', bg: '#cab2d6', geometry: 'box'},
video: {label: 'Video', name: 'Title', fg: '#e31a1c', bg: '#fb9a99', geometry: 'box'},
key: {label: 'Key', name: 'Term', fg: '#990', bg: '#ff8', geometry: 'cone'},
value: {label: 'Value', name: 'Term', fg: '#099', bg: '#8fa', geometry: 'sphere'},
annot: {label: 'Annotation', name: 'Key/Value', fg: '#909', bg: '#f8f', geometry: 'cylinder'}
},
tipWidth: 200, // The tooltip div has a fixed width
numDim: 3, // Number of dimensions
loopMedia: false, // Loop on media request
loopAnnot: true, // Loop on annot request
maxLabel: 25 // Max lenght of node label (ellipsis)
};
// Global variables
var v = {
save: {search: {}},
graph: ForceGraph3D()(document.getElementById("chart")),
scale: d3.scaleLog().range(p.scaleRange),
weights: [1, 1] // Domain of weights
};
// RUN
init();
function init() {
// Add callback to search button and text field
// User enter a keyword
// App request Iclikval search with this keyword
// Then draw a network of media linked to a root node
d3.select('#btn-search').on('click', () => newsearch());
d3.select('#txt-search').on('change', () => newsearch());
// Add callback to link button
// User can select which type of network he want
// App draw the correspondig network from fetched data
d3.select('#btn-mode').on('change', () => {
return loopAnnot()
.catch(err => error('ERROR: Init - mode', err)); // Notify the error
});
// Add callback to log button
// User can stop and restart media request
// User can stop and restart annotation request
d3.select('#progress-media').on('click', () => loopSwitch('media'));
d3.select('#progress-annot').on('click', () => loopSwitch('annot'));
// Add callback to filter value
// Network is rebuild
d3.select('#txt-filter').on('change', () => {
return buildNetwork() // Build the network
.then(network => draw(network)) // Display the network
.catch(err => error('ERROR: change filter', err)); // Notify the error
});
// Add callback to legend button
// Click switch txt-legend between display:none/block
d3.select('#btn-legend').on('click', () => {
const status = d3.select('#btn-legend').classed('fa-caret-right');
if (status) { // Switch to display
d3.select('#btn-legend').attr('class', 'fa fa-caret-down');
d3.select('#txt-legend').style('display', 'block');
} else { // Switch to hide
d3.select('#btn-legend').attr('class', 'fa fa-caret-right');
d3.select('#txt-legend').style('display', 'none');
}
})
// Add Legend
var li = d3.select('#txt-legend').selectAll('li')
.data(Object.keys(p.types))
.enter().append('li')
li.append('span')
.attr('class', 'fa fa-li fa-circle')
.style('color', d => {
if (d === 'root' || d === 'key' || d === 'value' || d === 'annot') {
return p.types[d].bg;
}
return p.types[d].fg;
});
li.append('label').text(d => p.types[d].label);
// Graph config
v.graph
// .valField(n => v.scale(n.weight))
.lineOpacity(0.5)
.nodeThreeObject(n => objectHandler(n))
.onNodeHover(n => onHoverHandler(n))
.onNodeClick(n => onClickHandler(n))
.nodeLabel(n => (n.name.length > p.maxLabel) ? n.name.substr(0, p.maxLabel-1) + '...' : n.name)
.d3Force('collide', d3.forceCollide().radius(p.scaleRange[1] * 2));
// .forceEngine('ngraph');
// console.log(v.graph.forceEngine());
// Default example
d3.select('#txt-search').property("value", 'iclikval');
d3.select('#btn-search').on('click')();
d3.select('#progress-media').on('click')();
d3.select('#progress-annot').on('click')();
d3.select('#btn-legend').on('click')();
// Loading message
log('check', 'Initiated');
}
function objectHandler(n) {
var geometry = '';
var weight = v.scale(n.weight);
switch (p.types[n.type].geometry) {
case 'octahedron':
geometry = new THREE.OctahedronGeometry(2 * weight);
break;
case 'box' :
geometry = new THREE.BoxGeometry(4 * weight, 4 * weight, 4 * weight);
break;
case 'cone':
geometry = new THREE.ConeGeometry(2 * weight, 4 * weight);
break;
case 'cylinder':
geometry = new THREE.CylinderGeometry(2 * weight, 2 * weight, 6 * weight);
break;
default:
geometry = new THREE.SphereGeometry(2 * weight);
}
var color = '#ccc';
switch (n.type) {
case 'root':
case 'key':
case 'value':
case 'annot':
color = p.types[n.type].bg;
break;
default:
color = p.types[n.type].fg;
}
return new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({color, transparent: true, opacity: 0.75}));
}
function onHoverHandler(n, prev) {
if (n !== null) {
// Highlight n
tip("show", n);
} else {
// No hover
tip("hide");
}
// if (prev !== null) {
// Reset prev
// }
}
function onClickHandler(n) {
switch (n.type) {
case 'root':
case 'key':
case 'value': {
window.open(`https://iclikval.riken.jp/search?db=default&q="${n.name}"`, '_blank');
break;
}
case 'annot': {
var terms = n.id.split("|");
var qs = `https://iclikval.riken.jp/search?db=default&q={"bool":{"must":[{"term":{"key":"${terms[0]}"}},{"term":{"value":"${terms[1]}"}}]}}&term="Key=${terms[0]} & Value=${terms[1]}"`;
var url = encodeURI(qs);
window.open(qs, '_blank');
break;
}
default: {
window.open(`https://iclikval.riken.jp/review-media/${n.id}`, '_blank');
}
}
}
function newsearch() {
// Reset mode to search
d3.select('#btn-mode').property('value', 'search');
resetSave() // Reset data
.then(() => loopSearch()); // perform search
}
// Delete the previous search result
function resetSave() {
return new Promise(resolve => {
// Loading message
log('spinner fa-spin', 'Reset...');
// Get the keyword and save it
v.save.search.keyword = d3.select('#txt-search').node().value;
// Reset the global storage
v.save = {
media: {}, // Media map
annotations: {}, // Annotations map
keys: {}, // Keys map
values: {}, // Value map
annots: {}, // Annot (key/value paire) map
search: {
page: 0, // Last page fetched from search API
pageMax: { // Total number of page from search API
journal_article: 1,
audio: 1,
video: 1,
image: 1,
dataset: 1
},
annots: 0, // Current annots fetched
annotsMax: 0, // Max annots to fetch
keyword: v.save.search.keyword // Current key word for search API
}
};
// Reset counts
progress('media', 0, 0);
progress('annot', 0, 0);
resolve();
});
}
// Request search in parallele
// Build network
// Loop search
function loopSearch() {
var types = v.save.search.pageMax;
var page = v.save.search.page;
// Prepare media request
var q = Object.keys(types)
.filter(k => types[k] > page)
.map(k => requestSearch(page + 1, k)); // QS + fetch + parse
// Test if need request
if (q.length > 0) {
return Promise.all(q) // Run the requests in parallele
.then(() => buildNetwork()) // Build the network
.then(network => draw(network)) // Display the network
.catch(err => error('ERROR: loopSearch', err)) // Notify the error
.then(() => p.loopMedia ? loopSearch() : 'end'); // Loop on Media Request
};
}
// Prepare the querystring
// Request to search API
// Parse the response
function requestSearch(page, type) {
// Loading message
log('spinner fa-spin', 'Request...');
// Setup the query
var querystring = `?db=default&page=${page}&media_type=${type}&q=${v.save.search.keyword}&term=${v.save.search.keyword}`;
// Send the request + parse
return querySearch(querystring)
.catch(err => error('ERROR: querySearch', err)) // Notify the error
.then(response => parseSearch(response)); // Parse response
}
// AJAX request to Iclikval search API
function querySearch(qs) {
return new Promise((resolve, reject) => {
d3.request('https://api.iclikval.riken.jp/search' + qs)
.header("Content-Type", "application/json")
.response(xhr => JSON.parse(xhr.responseText))
.get((err, res) => {
if (err) {
reject(err);
} else {
resolve(res);
}
});
});
};
// Parse response from Iclikval search API
function parseSearch(data) {
return new Promise((resolve, reject) => {
if (data.total_items === 0) {
reject();
} else {
// Loading message
log('spinner fa-spin', 'Parsing...');
var annotsMax = v.save.search.annotsMax;
// Save media
data._embedded.media.forEach(m => {
// Manage wrong annotation count (work around bug in Iclikval)
// Clamp annotation count to 1
var annotCount = m.auto_annotation_count + m.user_annotation_count;
annotCount = annotCount < 1 ? 1 : annotCount;
// Create new media
if (v.save.media[m.id] === undefined) {
v.save.media[m.id] = {
id: m.id,
title: m.title,
type: m.media_type,
annot: {}, // annotations map link to this media
annotPage: 0, // last annotaton page fetched
annotPageCount: 1, // max annotation page for this media
annotCount // annotation count for this media
}
}
// Max annotation user need to fetch
annotsMax = Math.max(annotsMax, annotCount);
});
// PageMax
const pages = v.save.search.pageMax;
const counts = data.extra.media_count.media;
const size = data.page_size;
Object.keys(pages).forEach(k => {
pages[k] = counts[k] ? Math.ceil(counts[k] / size) : 0;
});
// Save search meta data
v.save.search = {...v.save.search,
annotsMax, // Max annotation count
page: data.page, // Current search page fetched
pageMax: pages, // Max number of page for current media
total: data.extra.media_count.total // Max media count
}
// Update media count
var count = Object.keys(v.save.media).length;
progress('media', count, v.save.search.total);
// Update annot count
progress('annot', v.save.search.annots, v.save.search.annotsMax);
resolve();
}
}).catch(err => error('No media', err)) // Notify the error
.catch(err => Promise.resolve()); // Continue the loop
}
// Request annot in parallele
// Build network
// Loop search
function loopAnnot() {
// Prepare annot request
var q = [];
// For each media, request next annotation page
Object.keys(v.save.media).forEach(mid => {
var page = v.save.media[mid].annotPage;
var count = v.save.media[mid].annotPageCount;
if (page < count) {
q.push(requestAnnot(page + 1, mid));
}
});
// Test if need request
if (q.length > 0) {
return Promise.all(q) // Run the requests in parallele
.then(counts => inferCount(counts)) // Capture the count of annotation fetched
.then(() => buildNetwork()) // Build the network
.then(network => draw(network)) // Display the network
.catch(err => error('ERROR: loopAnnot', err)) // Notify the error
.then(() => p.loopAnnot ? loopAnnot() : 'end'); // Loop on Media Request
}
// Else rebuild the network
return buildNetwork() // Build the network
.then(network => draw(network)) // Display the network
.catch(err => error('ERROR: loopAnnot', err)) // Notify the error
.then(() => p.loopAnnot ? loopAnnot() : 'end'); // Loop on Media Request
}
// Prepare the querystring
// Request to annotation API
// Parse the response
function requestAnnot(page, mid) {
// Loading message
log('spinner fa-spin', 'Request...');
// Setup the query
var querystring = `?page=${page}&media=${mid}`;
// Send the request + parse
return queryAnnot(querystring)
.catch(err => error('ERROR: queryAnnot', err)) // Notify the error
.then(response => parseAnnot(response, mid)) // Add the respond to the previous one
.catch(err => error('Annot Request Failed', err)) // Notify the error
.catch(err => Promise.resolve()); // Continue the loop
}
// AJAX request to Iclikval annotation API
function queryAnnot(qs) {
return new Promise((resolve, reject) => {
d3.request('https://api.iclikval.riken.jp/annotation' + qs)
.header("Content-Type", "application/json")
.response(xhr => JSON.parse(xhr.responseText))
.get((err, res) => {
if (err) {
console.log('ERROR: queryAnnot', err);
reject(err);
} else {
resolve(res);
}
});
});
};
// Parse response from Iclikval annotation API
function parseAnnot(data, mid) {
return new Promise(resolve => {
// Loading message
log('spinner fa-spin', 'Parsing...');
// Update media
var m = v.save.media[mid];
m.annotPage = data.page;
m.annotPageCount = data.page_count;
m.annotCount = data.total_items;
// Parse annot
data._embedded.annotation.forEach(a => {
// Annot
var id = `${a.key}|${a.value}`;
if (v.save.annots[id] === undefined) {
v.save.annots[id] = {media: {}, key: a.key, value: a.value, annotCount: 0};
}
v.save.annots[id].media[m.id] = true; // Media map, annot link to media
v.save.annots[id].annotCount++; // Count annotations involve
// Key
if (v.save.keys[a.key] === undefined) {
v.save.keys[a.key] = {media: {}, annotCount: 0}
}
v.save.keys[a.key].media[mid] = true; // Media map, key link to media
v.save.keys[a.key].annotCount++; // Count annotations involve
// Value
if (v.save.values[a.value] === undefined) {
v.save.values[a.value] = {media: {}, keys: {}, annotCount: 0}
}
v.save.values[a.value].media[mid] = true; // Media map, value is link to media
v.save.values[a.value].keys[a.key] = true; // Key map, value is link to key
v.save.values[a.value].annotCount++; // Count annotations involve
});
// Update annot count
v.save.search.annotsMax = Math.max(v.save.search.annotsMax, data.total_items);
// We need to catch the minimal annotation page fetched
// And infer the current annotation count
var count = 0;
if (data.page !== data.page_count) {
count = data.page * data.page_size;
}
resolve(count);
});
}
// Get the minimal count of annotations
function inferCount(counts) {
// Manage undefined
v.save.search.annots = Math.min(...counts.map(c => c === 0 ? v.save.search.annotsMax : c));
// Update annot count
progress('annot', v.save.search.annots, v.save.search.annotsMax);
return Promise.resolve();
}
// Build the network according to the mode selected in "link by" option
function buildNetwork() {
switch (d3.select('#btn-mode').node().value) {
case 'keys':
return networkKeys();
case 'values':
return networkValues();
case 'keyval':
return networkKeysValues();
case 'annot':
return networkAnnotations();
default: // search
return networkSearch();
};
}
// Build the network after search
// The keyword is the root node
// Each media is a node linked to the root
// The size of the node are proportional to the annotation count
function networkSearch() {
return new Promise(resolve => {
// Loading message
log('spinner fa-spin', 'Network...');
// Store network
var network = {nodes: [], links: []};
// add a root (fixed at center)
network.nodes.push({id: 'ROOT', type:'root', name: v.save.search.keyword, weight: 0});
// link each media to root
Object.keys(v.save.media).forEach((k, i) => {
var m = v.save.media[k];
network.nodes.push({id: m.id, type: m.type, name: m.title, weight: m.annotCount});
network.links.push({source: 'ROOT', target: m.id});
network.nodes[0].weight++;
});
resolve(network);
});
}
// Build the network linked by key
// Both media and key are nodes
// Media and key are linked by annotations
function networkKeys() {
return new Promise(resolve => {
// Loading message
log('spinner fa-spin', 'Network...');
// Store network
var network = {nodes: [], links: []};
// Get filter
var filter = d3.select('#txt-filter').node().value;
// Create node for each media
Object.keys(v.save.media).forEach(k => {
var m = v.save.media[k];
if (m.annotCount > filter) {
network.nodes.push({id: m.id, type: m.type, name: m.title, weight: m.annotCount});
}
});
// Create node for each key and link with media
Object.keys(v.save.keys).forEach(k => {
var key = v.save.keys[k];
if (key.annotCount > filter) {
network.nodes.push({id: k, type: 'key', name: k, weight: key.annotCount});
Object.keys(key.media).forEach(m => {
if (v.save.media[m].annotCount > filter) {
network.links.push({source: m, target: k});
}
});
}
});
resolve(network);
});
}
// Build the network linked by key and value
// Both media key and value are nodes
// Media and key are linked
// Key and value are linked
function networkKeysValues() {
return new Promise(resolve => {
// Loading message
log('spinner fa-spin', 'Network...');
// Store network
var network = {nodes: [], links: []};
// Get filter
var filter = d3.select('#txt-filter').node().value;
// Create node for each media
Object.keys(v.save.media).forEach(k => {
var m = v.save.media[k];
if (m.annotCount > filter) {
network.nodes.push({id: m.id, type: m.type, name: m.title, weight: m.annotCount});
}
});
// Create node for each key and link with media
Object.keys(v.save.keys).forEach(k => {
var key = v.save.keys[k];
if (key.annotCount > filter) {
network.nodes.push({id: k, type: 'key', name: k, weight: key.annotCount});
Object.keys(key.media).forEach(m => {
if (v.save.media[m].annotCount > filter) {
network.links.push({source: m, target: k});
}
});
}
});
// Create node for each value and link with key
Object.keys(v.save.values).forEach(k => {
var val = v.save.values[k];
if (val.annotCount > filter) {
network.nodes.push({id: k, type: 'value', name: k, weight: val.annotCount});
Object.keys(val.keys).forEach(m => {
if (v.save.keys[m].annotCount > filter) {
network.links.push({source: m, target: k});
}
});
}
});
resolve(network);
});
}
// Build the network linked by value
// Both media and value are nodes
// Media and value are linked by annotations
function networkValues() {
return new Promise(resolve => {
// Loading message
log('spinner fa-spin', 'Network...');
// Store network
var network = {nodes: [], links: []};
// Get filter
var filter = d3.select('#txt-filter').node().value;
// Create node for each media
Object.keys(v.save.media).forEach(k => {
var m = v.save.media[k];
if (m.annotCount > filter) {
network.nodes.push({id: m.id, type: m.type, name: m.title, weight: m.annotCount});
}
});
// Create node for each value and link with media
Object.keys(v.save.values).forEach(k => {
var val = v.save.values[k];
if (val.annotCount > filter) {
network.nodes.push({id: k, type: 'value', name: k, weight: val.annotCount});
Object.keys(val.media).forEach(m => {
if (v.save.media[m].annotCount > filter) {
network.links.push({source: m, target: k});
}
});
}
});
resolve(network);
});
}
// Build the network linked by annotation
// One annotation represent a unique key value pair
// Both media and annots are nodes
// Media and annots are linked by annotations
function networkAnnotations() {
return new Promise(resolve => {
// Loading message
log('spinner fa-spin', 'Network...');
// Store network
var network = {nodes: [], links: []};
// Get filter
var filter = d3.select('#txt-filter').node().value;
// Create node for each media
Object.keys(v.save.media).forEach(k => {
var m = v.save.media[k];
if (m.annotCount > filter) {
network.nodes.push({id: m.id, type: m.type, name: m.title, weight: m.annotCount});
}
});
// Create node for each annot and link with media
Object.keys(v.save.annots).forEach(k => {
var annot = v.save.annots[k];
if (annot.annotCount > filter) {
network.nodes.push({id: k, type: 'annot', name: `${annot.key} / ${annot.value}`, weight: annot.annotCount});
Object.keys(annot.media).forEach(m => {
if (v.save.media[m].annotCount > filter) {
network.links.push({source: m, target: k});
}
});
}
});
resolve(network);
});
}
// Set graph dimension and data
function draw(data) {
return new Promise(resolve => {
// scaleRange
var weights = data.nodes.map(n => n.weight);
v.scale.domain([Math.min(...weights), Math.max(...weights)]);
console.log('scale', Math.min(...weights), Math.max(...weights), " -> ", p.scaleRange);
v.graph.numDimensions(p.numDim).graphData(data);
log('check', 'Done');
resolve();
});
}
function progress(mode, count, total) {
const percent = total === 0 ? 0 : Math.round(count * 100 * 100 / total) / 100;
switch (mode) {
case 'media':
var div = d3.select('#progress-media');
div.select('.progress-value').attr('title', `${count}/${total}`).text(`${percent}%`);
div.select('.progress-bar').style('width', `${percent}%`);
break;
case 'annot':
var div = d3.select('#progress-annot');
div.select('.progress-value').attr('title', `${count}/${total}`).text(`${percent}%`);
div.select('.progress-bar').style('width', `${percent}%`);
break;
default:
error('ERROR wrong progress mode');
}
}
function loopSwitch(mode) {
let bool;
let span;
let callback;
switch (mode) {
case 'media':
p.loopMedia = !p.loopMedia;
bool = p.loopMedia;
span = d3.select('#progress-media').select('.fa');
callback = loopSearch;
break;
case 'annot':
p.loopAnnot = !p.loopAnnot;
bool = p.loopAnnot;
span = d3.select('#progress-annot').select('.fa');
callback = loopAnnot;
default:
}
if (bool) {
span.attr('class', 'fa fa-pause').attr('title', 'Stop request');
} else {
span.attr('class', 'fa fa-play').attr('title', 'Restart request');
}
callback();
}
// Manage tooltip
function tip(mode, d) {
if(mode === "show") {
d3.select("#tip")
.datum(d)
.style("opacity", 1)
.html(d => `<label>Type:&nbsp;</label><span>${p.types[d.type].label}</span><br/>` +
`<label>Annotations:&nbsp;</label><span>${d.weight}</span><br/>` +
`<label>${p.types[d.type].name}:&nbsp;</label><span>${d.name}</span>`);
} else if(mode === "hide") {
d3.select("#tip").style("opacity",0)
}
}
// Manage Errors
function error(msg, err) {
// Loading message
log('exclamation', msg);
return Promise.reject(msg);
}
// Manage Log
function log(icon, msg) {
d3.select('#log').html(`<span class="fa fa-${icon}"></span>${msg}`);
}
// Menu
function toggleDimensions(num) {
// Save dimensions
p.numDim = num;
// Reset coords
if (num < 2) {
v.graph.graphData().nodes.forEach(n => {
n.y = 0;
n.vy = 0;
n.z = 0;
n.vz = 0;
});
} else if (num < 3) {
v.graph.graphData().nodes.forEach(n => {
n.z = 0;
n.vz = 0;
});
}
}
</script>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment