Skip to content

Instantly share code, notes, and snippets.

@mhebrard
Last active September 4, 2017 04:18
Show Gist options
  • Save mhebrard/8588a6c5e8fd060171551d227c27c377 to your computer and use it in GitHub Desktop.
Save mhebrard/8588a6c5e8fd060171551d227c27c377 to your computer and use it in GitHub Desktop.
iCLiKVAL Content Overview

This representation shows the content of iCLiKVAL database in a pack layout.

Menu By mouse over the menu (top left) various options appears. The chart will be updated immediately at each modifications.

Count: Media, Group by: Media Type Display the number of media present in the database. and the number of media of each type (see legend).

Count: Annotations, Group by: Media Type Display the number of annotations present in the database. and the number of annotations of each media type (see legend).

Count: Annotations, Group by: Reviewer Display the number of annotations present in the database. and the number of annotations of each reviewer (in a different color).

Sub-group Sub-group option allows to combine the group by media type and reviewers.

Tooltip By mouse over one circle, a tooltip appears with detailed information about the item.

Loading The data are requested to iCLiKVAL. That can take time, therefore the loading stage is displayed on top right corner.

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>CountToPack</title>
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro" rel="stylesheet">
<style>
body {
font-family: 'Source Sans Pro', sans-serif;
}
.header {
text-align: center;
font-weight: bold;
font-size: 18px;
}
#chart {
text-align: center;
}
input:disabled+label {
color: #ccc;
}
.shadow {
border-radius: 3px;
border: 1px #000 solid;
box-shadow: 0 0 3px gray, inset 0 0 3px gray;
background: #fff;
}
/* log */
.right {
position: absolute;
top:1%;
right:1%;
}
#load {
height:20px;
padding:4px;
}
#legend {
display: flex;
justify-content: center;
}
/* option button */
.left {
position: absolute;
top:1%;
left:1%;
}
.menu {
white-space: nowrap;
transition: width 1s, height 1s ease-in-out;
width:50px;
height:20px;
padding:4px;
}
.menu div {
background: white;
}
.menu .wrapper {
overflow: hidden;
white-space: nowrap;
display: inline-block;
transition: width 1s, height 1s ease-in-out;
width:0%;
height:0%;
}
.menu .wrapper .content {
padding:2px;
}
.menu:hover {
-webkit-transition: width 1s, height 1s ease-in-out;
-moz-transition: width 1s, height 1s ease-in-out;
-o-transition: width 1s, height 1s ease-in-out;
transition: width 1s, height 1s ease-in-out;
width:100%;
height:100%;
}
.menu:hover .wrapper {
-webkit-transition: width 1s, height 1s ease-in-out;
-moz-transition: width 1s, height 1s ease-in-out;
-o-transition: width 1s, height 1s ease-in-out;
transition: width 1s, height 1s ease-in-out;
width:100%;
height:100%;
}
/* Tooltip */
#tip {
position:absolute;
z-index:3;
padding:10px;
white-space:nowrap;
pointer-events:none;
opacity:0;
background-color: #FFF;
}
#tip label {
font-weight: bold;
display: inline-block;
}
</style>
<script src="https://use.fontawesome.com/b6ac3d3b75.js"></script>
<script src="https://d3js.org/d3-array.v1.min.js"></script>
<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-dsv.v1.min.js"></script>
<script src="https://d3js.org/d3-ease.v1.min.js"></script>
<script src="https://d3js.org/d3-hierarchy.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>
<script src="https://d3js.org/d3-timer.v1.min.js"></script>
<script src="https://d3js.org/d3-transition.v1.min.js"></script>
</head>
<body>
<div class='left'>
<div class='menu shadow'>
<div><span class='fa fa-cog'></span>Menu</div>
<div class='wrapper'>
<div class='content'>
<fieldset>
<legend>Count:</legend>
<input type='radio' name='mode' value='none' id='modenone' checked /><label for='modenone'>-none-</label>
<input type='radio' name='mode' value='media' id='modemedia' /><label for='modemedia'>Media</label>
<input type='radio' name='mode' value='annots' id='modeannot' /><label for='modeannot'>Annotations</label>
</fieldset>
<fieldset>
<legend>Group by:</legend>
<input type='radio' name='group' value='none' id='groupnone' checked /><label for='groupnone'>-none-</label>
<input type='radio' name='group' value='type' id='grouptype' /><label for='grouptype'>Media Type</label>
<input type='radio' name='group' value='user' id='groupuser' /><label for='groupuser'>Reviewer</label>
</fieldset>
<fieldset>
<legend>Sub-group:</legend>
<input type='radio' name='sub' value='none' id='subnone' checked /><label for='subnone'>-none-</label>
<input type='radio' name='sub' value='type' id='subtype' /><label for='subtype'>Media Type</label>
<input type='radio' name='sub' value='user' id='subuser' /><label for='subuser'>Reviewer</label>
</fieldset>
<!--<button type='button' onClick='update()'>Update</button>-->
</div>
</div>
</div>
</div>
<div class='right shadow'>
<div id='load'></div>
</div>
<div class='header'>iCLiKVAL Content Overview</div>
<div class='header' id='mode'></div>
<div id='chart'></div>
<div id='legend'></div>
<div id='tip' class='shadow'></div>
<script type="text/javascript">
// params
var p = {
diameter: 600, // root circle diameter
margin: 5, // root circle margin
rmin: 55, // min bubble diameter to display full text
catmax: 100, // max number of users requested
labels: {
// media type use colorbrewer palette
root: {label: 'iCLiKVAL', fg: '#BBB', bg: '#EEE'},
iclikval: {label: 'system', fg: '#BBB', bg: '#EEE'},
audio: {label: 'Audio', fg: '#ff7f00', bg: '#fdbf6f'},
dataset: {label: 'Dataset', fg: '#33a02c', bg: '#b2df8a'},
image: {label: 'Image', fg: '#6a3d9a', bg: '#cab2d6'},
journal_article: {label: 'Journal Article', fg: '#1f78b4', bg: '#a6cee3'},
video: {label: 'Video', fg: '#e31a1c', bg: '#fb9a99'},
media: {label: 'Media'},
annots: {label: 'Annotations'}
},
// users use badges palette
bg: ['#F88', '#FA8', '#FF8', '#AF8', '#8F8', '#8FA', '#8FF', '#8AF', '#88F', '#A8F', '#F8F', '#F8A'],
fg: ['#900', '#960', '#990', '#690', '#090', '#096', '#099', '#069', '#009', '#609', '#909', '#906'],
color: d3.scaleOrdinal(d3.range(12))
};
// State
var state = {
mode: 'none',
group: 'none',
sub: 'none',
enabled: {
modenone: true, modemedia: false, modeannot: false,
groupnone: true, grouptype: false, groupuser: false,
subnone: true, subtype: false, subuser: false,
}
};
// tmp
var uMap = {others: 0};
// RUN
init();
function init() {
console.log('INIT');
// menu onchange check + init at none
d3.selectAll('input[type="radio"]').on('change', check);
d3.selectAll('input[value="none"]').property('checked', true);
// hide count none
d3.select('#modenone').style('display', 'none');
d3.select('label[for="modenone"]').style('display', 'none');
// init the options
check();
// loading msg
d3.select('#load').html('<span class="fa fa-spinner fa-spin"></span>Loading...')
// legend
var div = d3.select('#legend')
.style('display', 'none')
.selectAll('div')
.data(['audio', 'dataset', 'image', 'journal_article', 'video'])
.enter().append('div')
.style('display', 'flex')
div.append('div')
.style('border', d => `3px solid ${p.labels[d].fg}`)
.style('color', d => p.labels[d].fg)
.style('background', d => p.labels[d].bg)
.style('width', '25px')
.style('text-align', 'center')
.style('margin', '0 3px')
div.append('span').text(d => p.labels[d].label)
// request media and annot
var q = [];
q.push(requestMedia());
q.push(requestAnnots());
return Promise.all(q)
.then(() => {
d3.select('#load').html('<span class="fa fa-check"></span>Loaded');
}).catch(err => {
d3.select('#load').html('<span class="fa fa-exclamation"></span>Error')
console.log('init error', err);
});
}
function check() {
// console.log('CHECK');
// update selected
state.mode = d3.select('input[name="mode"]:checked').node().value;
state.group = d3.select('input[name="group"]:checked').node().value;
state.sub = d3.select('input[name="sub"]:checked').node().value;
// get inputs
var gnone = d3.select('#groupnone');
var guser = d3.select('#groupuser');
var gtype = d3.select('#grouptype');
var snone = d3.select('#subnone');
var stype = d3.select('#subtype');
var suser = d3.select('#subuser');
// mode
if(state.mode === 'media') {
// deselect group user
if (guser.property('checked')) {
gtype.property('checked', true);
}
// deselect sub
snone.property('checked', true);
// disabled
state.enabled.grouptype = true;
state.enabled.groupuser = false;
state.enabled.subtype = false;
state.enabled.subuser = false;
} else if(state.mode ==='annots') {
// group
state.enabled.grouptype = true;
if(state.byuser) {
state.enabled.groupuser = true;
}
if(state.group === 'type') {
// deselect sub type
if (stype.property('checked')) {
snone.property('checked', true);
}
state.enabled.subtype = false;
if(state.byuser) {
state.enabled.subuser = true;
}
} else if(state.group === 'user') {
// deselect sub user
if (suser.property('checked')) {
snone.property('checked', true);
}
state.enabled.subuser = false;
state.enabled.subtype = true;
} else {
state.enabled.subuser = false;
state.enabled.subtype = false;
}
} else { // group none
gnone.property('checked', true);
state.enabled.groupuser = false;
state.enabled.grouptype = false;
snone.property('checked', true);
state.enabled.subuser = false;
state.enabled.subtype = false;
}
// update state
state.mode = d3.select('input[name="mode"]:checked').node().value;
state.group = d3.select('input[name="group"]:checked').node().value;
state.sub = d3.select('input[name="sub"]:checked').node().value;
// update enabled
Object.keys(state.enabled).forEach(k => {
d3.select(`#${k}`).property('disabled', !state.enabled[k]);
})
// legend
if(state.group === 'type' || state.sub === 'type') {
d3.select('#legend').style('display', 'flex');
} else {
d3.select('#legend').style('display', 'none');
}
// Tooltip
if(state.mode === 'annots') {p.tipWidth = 95}
else {p.tipWidth = 55}
// update view
update();
}
function update() {
// console.log('UPDATE');
var title = d3.select('#mode');
var data;
if(state.mode === 'media') {
title.text(`Media Count: ${state.media.value.toLocaleString()}`);
// data = state.media;
if(state.group === 'type') {
data = state.media;
} else {
data = JSON.parse(JSON.stringify(state.media));
delete data.children;
}
} else if(state.mode === 'annots') {
title.text(`Annotation Count: ${state.bytype.value.toLocaleString()}`)
if(state.group === 'user') {
if(state.sub === 'none') {
data = JSON.parse(JSON.stringify(state.byuser));
data.children.forEach(f => {
delete f.children;
});
} else {
data = state.byuser;
}
} else if(state.group === 'none'){
data = JSON.parse(JSON.stringify(state.bytype));
delete data.children;
} else {
if(state.sub === 'none') {
data = JSON.parse(JSON.stringify(state.bytype));
data.children.forEach(f => {
delete f.children;
});
} else {
data = state.bytype;
}
}
}
if (data) {
toPack(data).then(pack => draw(pack));
}
}
function requestMedia() {
var t0, t1;
t0 = performance.now();
return queryMediaCount()
.catch(err => {
console.log('requestMedia delayed by 1min');
return Promise.resolve()
.then(DelayPromise(60000))
.then(() => queryMediaCount());
}).catch(err => {
console.log('Media error:', err);
}).then(response => {
t1 = performance.now();
console.log(`requestMedia [${t1-t0}ms]`);
return parseMedia(response);
}).then(data => save(data, 'media'));
}
function requestAnnots() {
// count annot by media type
var param = {group:['media_type']};
return queryAnnotCount(param)
.then(response => parseByType(response))
.then(data => {
save(data, 'bytype');
return byUsers(data);
});
}
function byUsers(data) {
// create root
var root = {
name: 'root',
value: 0,
children: [],
};
// query byusers
var param = {
group: ['reviewer'],
options:{size: p.catmax}
};
var q = data.children.map(m => {
param.filter = {media_type: m.name};
return requestUsers(param, m, root);
});
return Promise.all(q)
.then(list => {
var q = [];
q.push(save(list[0], 'byuser'));
q.push(save(data, 'bytype'));
return Promise.all(q);
});
}
function requestUsers(p, n, root) {
return queryAnnotCount(p)
.then(response => parseByUser(response, n, root));
}
function DelayPromise(delay) {
return function(data) {
return new Promise(function(resolve, reject) {
setTimeout(() => resolve(data), delay);
});
};
}
function parseMedia(data) {
return new Promise(function(resolve) {
// create root
var root = {
name: 'root',
value: data.result.count.total,
children: [],
};
// parse response
var list = data.result.count.media_type;
Object.keys(list).forEach(k => {
root.children.push({name: k, value: list[k]});
});
resolve(root);
}).catch(err => {
console.log('Error Parse Media', err);
});
}
function parseByType(data) {
return new Promise(function(resolve) {
// create root
var res = {
name: 'root',
value: 0,
children: []
}
// parse response
res.children = data.result.map(m => {
// add count to root
res.value += m.count;
// add type child
return {name: m.group.media_type, value: m.count};
});
resolve(res);
});
}
function parseByUser(data, node, root) {
return new Promise(function(resolve) {
var u, uidx;
var sum = 0;
node.children = data.result.map(m => {
u = m.group.reviewer;
// add user
if(uMap[u] === undefined) {
uMap[u] = root.children.length;
root.children.push({name: u, value: 0, children: []});
}
uidx = uMap[u];
// add user/type
root.children[uidx].children.push({name: node.name, value: m.count});
root.children[uidx].value += m.count;
root.value += m.count;
// add type/user child
sum += m.count;
return {name: m.group.reviewer, value: m.count}
});
// check others
if (sum < node.value) {
var val = node.value - sum;
// add type/others
node.children.push({name: 'other', value: val});
// add others/type
root.children[0].children.push({name: node.name, value: val});
root.children[0].value += val;
root.value += val;
}
resolve(root);
}).catch(err => {
console.log('parseByUser error:', err);
})
}
function save(data, mode) {
if(mode === 'media') {
state.media = data;
state.enabled.modemedia = true;
if (state.mode === 'none') {
d3.select('#modemedia').property('checked', true);
d3.select('#grouptype').property('checked', true);
}
} else if(mode ==='bytype') {
state.bytype = data;
state.enabled.modeannot = true;
if (state.mode === 'none') {
d3.select('#modeannot').property('checked', true);
d3.select('#grouptype').property('checked', true);
}
} else {
state.byuser = data;
}
check();
}
// Manage layout
function toPack(data) {
return new Promise(function(resolve) {
console.log('PACK', data);
// circle
var diam = p.diameter - (2 * p.margin);
// create pack layout
var layout = d3.pack()
.size([diam, diam])
.padding(5);
// compute layout
var pack = layout(
d3.hierarchy(data)
.sum(function(d) { return 10*Math.log10(d.value+1); }) // decibel - +1 !0
).descendants();
resolve(pack);
});
}
function draw(data) {
console.log('DRAW', state.mode);
if (data[0]) {
// headers
d3.select('#count')
.text(data[0].data.value.toLocaleString());
// transitions
const delay = 500;
const t1 = d3.transition().duration(delay);
const t2 = d3.transition().delay(delay).duration(delay);
const t3 = d3.transition().delay(delay * 2).duration(delay);
var sel, add;
var svg = d3.select('#chart').selectAll('svg')
.data([0]);
svg.enter()
.append('svg')
.attr('height', p.diameter)
.attr('width', p.diameter)
.append('g')
.style('font-weight', 'bold')
.attr('transform', `translate(${p.margin},${p.margin})`);
// nodes group
sel = d3.select('#chart').select('svg')
.select('g').selectAll('.node')
.data(data, d => d.parent ? d.parent.data.name+d.data.name : d.data.name);
// exit
sel.exit().transition(t1)
.remove();
// update
sel.transition(t2)
.attr('transform', d => `translate(${d.x},${d.y})`);
// add .node + content
add = sel.enter().append('g').attr('class', 'node')
.attr('fill', d => getColor('fg', d.data.name))
.attr('transform', `translate(${p.diameter / 2},${p.diameter / 2})`);
add.append('circle')
.attr('r', 0)
.attr('stroke-width', '3px')
.attr('stroke', d => getColor('fg', d.data.name))
.attr('fill', d => getColor('bg', d.data.name))
.on('mouseover', function(d){ tip("show",d); })
.on('mouseout', function(d){ tip("hide",d); })
.on("mousemove", function(d) { tip("move"); })
add.append('text')
.attr('class', 'type')
.attr('dy', '0.5ex')
.attr('y', '-2ex')
.attr('text-anchor', 'middle')
.attr('opacity', 0)
.style('pointer-events', 'none')
.text(d => getLabel(d));
add.append('text')
.attr('class', 'mode')
.attr('dy', '0.5ex')
.attr('text-anchor', 'middle')
.attr('opacity', 0)
.style('pointer-events', 'none')
add.append('text')
.attr('class', 'count')
.attr('dy', '0.5ex')
.attr('text-anchor', 'middle')
.attr('opacity', 0)
.style('pointer-events', 'none')
// update
sel = add.merge(sel);
sel.transition(t3)
.attr('transform', d => `translate(${d.x},${d.y})`);
// circles
sel = d3.select('#chart').select('svg')
.selectAll('circle')
.data(data, d => d.parent ? d.parent.data.name+d.data.name : d.data.name);
// exit
sel.exit().transition(t1)
.attr('r', 0)
.remove();
// update
sel.transition(t3)
.attr('r', d => d.r);
// types
sel = d3.select('#chart').select('svg')
.selectAll('.type')
.data(data, d => d.parent ? d.parent.data.name+d.data.name : d.data.name);
// exit
sel.exit().transition(t1)
.attr('opacity', 0)
.remove();
// update
sel.transition(t3)
.attr('opacity', d => {
if(!d.children && d.r > p.rmin) {
return 1;
} else {
return 0;
}
});
// mode
sel = d3.select('#chart').select('svg')
.selectAll('.mode')
.data(data, d => d.parent ? d.parent.data.name + d.data.name : d.data.name);
// exit
sel.exit().transition(t1)
.attr('opacity', 0)
.remove();
// update
sel.transition(t3)
.attr('opacity', d => {
if(!d.children && d.r > p.rmin) {
return 1;
} else {
return 0;
}
})
.text(p.labels[state.mode].label);
// count
sel = d3.select('#chart').select('svg')
.selectAll('.count')
.data(data, d => d.parent ? d.parent.data.name+d.data.name : d.data.name);
// exit
sel.exit().transition(t1)
.attr('opacity', 0)
.remove();
// update
sel.transition(t3)
.attr('y', d => d.r > p.rmin ? '2ex' : '0ex')
.attr('opacity', d => d.children ? 0 : 1)
.text(d => d.data.value.toLocaleString());
}
}
function getColor(g, name) {
var col;
if(p.labels[name]) {
col = p.labels[name][g];
} else {
col = p[g][p.color(name)];
}
return col;
}
function getTip(d) {
var str='';
// switch user + type
if(!d.parent) { // Root
str += '<b>iCLiKVAL Content</b>'
} else if(state.sub === 'type' && !d.children) {
str += '<label>Reviewer: </label>';
str += getLabel(d.parent);
str += '<br/><label>Type: </label>';
str += getLabel(d);
} else if(state.sub === 'user' && !d.children) {
str += '<label>Reviewer: </label>';
str += getLabel(d);
str += '<br/><label>Type: </label>';
str += getLabel(d.parent);
} else if(state.group === 'type') {
str += '<label>Type: </label>';
str += p.labels[d.data.name].label;
} else {
str += '<label>Reviewer: </label>';
str += getLabel(d);
}
// add mode + count
str += `<br/><label>${p.labels[state.mode].label}: </label>`;
str += d.data.value.toLocaleString();
return str;
}
function getLabel(d) {
var str;
if(p.labels[d.data.name]) {
str = p.labels[d.data.name].label;
} else {
str = d.data.name;
}
return str;
}
function tip(mode,d) {
if(mode === "show") {
d3.select("#tip")
.datum(d)
.style("opacity",1)
.html(d => getTip(d));
// width
d3.select('#tip').selectAll('label').style('width', `${p.tipWidth}px`);
}
else if(mode === "hide") {
d3.select("#tip").style("opacity",0)
}
else { // move
d3.select("#tip").style("top", (d3.event.pageY+10)+"px")
.style("left", (d3.event.pageX+10)+"px")
}
}
function queryAnnotCount(p) {
return new Promise(function(resolve, reject) {
d3.request('https://api.iclikval.riken.jp/annotation-count')
.header("Content-Type", "application/json")
.response(xhr => JSON.parse(xhr.responseText))
.post(JSON.stringify(p),(err, res) => {
if (err) {
console.log(`error - queryAnnotCount: ${err}`);
reject(err);
} else {
resolve(res);
}
});
});
};
function queryMediaCount() {
return new Promise(function(resolve, reject) {
d3.json('https://api.iclikval.riken.jp/media-count', (err, json) => {
if (err) {
console.log(`error - queryMediaCount: ${err}`);
reject(err);
} else {
resolve(json);
}
});
});
};
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment