chart = {
const nodes = data.nodes.map(d => Object.create(d));
const links = data.links.map(d => Object.create(d));
var min_occ = 1;
var min_occ_link = 1;
var min_occ_iter_new = 0;
var min_occ_iter = 0;
var show_l1 = true;
var show_l2 = true;
var show_l34 = true;
const width = 2000;
const height = 2000;
const svg = d3.create("svg")
.attr("viewBox", [-width / 2, -height / 2, width, height])
.attr('style','background: #fff;');
const g = svg.append("g");
var legend_svg = svg.append('g')
.attr('class','legend_div')
.attr('active','no')
.attr('transform','translate(-'+width/2+',-'+height/2+')');
var legend_channel = svg.append('g')
.attr('class','legend_channel')
.attr('active','no')
.attr('transform','translate('+(width/2-300)+',-'+height/2+')');
var active_session = [];
var active_channel = [];
const tooltip = new Tooltip();
g.append(() => tooltip.node_link);
///// FORCE
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id))
.force("charge", d3.forceManyBody())
.force("x", d3.forceX())
.force("y", d3.forceY());
///// FULL SCREEN
function onresize () {
let fs = document.webkitFullscreenElement || document.mozFullscreenElement;
if (fs == svg) {
svg.width = window.innerWidth;
svg.height = window.innerHeight;
} else {
svg.width = width;
svg.height = height;
}
legend_svg.attr('transform','translate(-'+svg.width/2+',-'+svg.width/2+')');
}
window.addEventListener('resize', onresize, false);
///// ZOOM
function zoomed() {
g.attr("transform", d3.event.transform);
}
svg.call(d3.zoom()
.extent([[0, 0], [width, height]])
.scaleExtent([1, 8])
.on("zoom", zoomed));
///// GRAPH
// Rendering functions
var node_opacity = function(d){
if( (!show_l1) & d.group != 'recommendation' | (!show_l2) & d.group == 'recommendation' ) return 0.02;
if(parseInt(d.iteration_all) < min_occ_iter_new) return 0.02;
if(d.group != 'recommendation') return 1;
else return 0.2;
}
var link_opacity = function(d){
if( (!show_l1) & d.level == 1 | (!show_l2) & d.level == 2 | (!show_l34) & d.level > 2 ) return 0.002;
if(parseInt(d.iteration_all) < min_occ_iter_new) return 0.002;
if(d.level == 1) return 1;
else if (d.level == 2) return 0.2;
else return 0;
}
////////////////////////////////////////////////////////////////////////////////
// GRAPH
////////////////////////////////////////////////////////////////////////////////
const link = g.append("g")
.selectAll("line")
.data(links)
.join("line")
.attr("type", d => d.type)
.attr("level", d => d.level)
.attr("target", d => d.target.id)
.attr("source", d => d.source.id)
.attr("channel_source", d => d.source.channel)
.attr("channel_target", d => d.target.channel)
.attr("channel_source_id", d => d.source.channel_id)
.attr("channel_target_id", d => d.target.channel_id)
.attr("session_source", d => d.source.group)
.attr("session_target", d => d.target.group)
.attr("session_all", d => d.session_all)
.attr("session_direct", d => d.session_direct)
.attr("iteration_all", d => d.iteration_all)
.attr("stroke", '#000')
.attr('stroke-linecap', 'round')
.attr("stroke-opacity", d => link_opacity(d))
.attr("stroke-width", d => link_width(d.level));
// Nodes
const node = g.append("g")
.selectAll("circle")
.data(nodes)
.join("a")
.attr("href", d => 'https://www.youtube.com/watch?v=' + d.id)
.attr('target', '_blank')
.append("circle")
.attr("id", d => d.id)
.attr("class", 'video_node')
.attr("channel", d => d.channel)
.attr("channel_id", d => d.channel_id)
.attr("session", d => d.session)
.attr("session_all", d => d.session_all)
.attr("session_direct", d => d.session_direct)
.attr("session_from_reco", d => d.session_from_reco)
.attr("group", d => d.group)
.attr("iteration_all", d => d.iteration_all)
.attr("stroke", "#fff")
.attr("stroke-width", 0.5)
.attr("r", d => r_scale(d3.max([10^6, +d.views^0.5])))
.attr("fill", d => color_scale(d.session))
.attr("opacity", d => node_opacity(d))
.call(drag(simulation));
/////////////////////////////////////////////////////////////////////////////////////
///// UX FUNCTIONS
/////////////////////////////////////////////////////////////////////////////////////
const undo_highlight = function(){
d3.selectAll('.session_legend').attr('opacity',1);
d3.selectAll('.legend_selector_channel').attr('opacity',0);
d3.selectAll('circle').attr('opacity', d => node_opacity(d));
d3.selectAll('line').attr('stroke-opacity', d => link_opacity(d));
}
const hide_all = function(){
d3.selectAll('.session_legend').attr('opacity',0.5);
d3.selectAll('.legend_selector_channel').attr('opacity',0.5);
d3.selectAll('circle').attr('opacity',0.02);
d3.selectAll('line').attr('stroke-opacity',0.001);
}
const hide_all_light = function(){
d3.selectAll('.session_legend').attr('opacity',0.5);
d3.selectAll('.legend_selector_channel').attr('opacity',0.5);
d3.selectAll('circle').attr('opacity',0.08);
d3.selectAll('line').attr('stroke-opacity',0.03);
}
const show_session = function(label){
d3.selectAll('text[session_legend="'+label+'"]').attr('opacity', 1);
if(min_occ_iter == 0){
d3.selectAll('line[session_all*="'+label+'"]').attr("stroke-opacity", d => 0.1); // link_opacity(d));
d3.selectAll('line[session_all*="'+label+'"]').attr("stroke-opacity", d => link_opacity(d));
d3.selectAll('circle[session_direct*="'+label+'"]').attr('opacity', d => node_opacity(d));
d3.selectAll('circle[session_all*="'+label+'"]').attr('opacity', d => node_opacity(d));
d3.selectAll('circle[session_from_reco*="'+label+'"]').attr('opacity', d => node_opacity(d));
} else {
for(let i=1;i<=min_occ_iter;i++){
d3.selectAll('line[session_all*="'+label+'"][iteration_all*="'+i+'"]')
.attr("stroke-opacity", d => 0.1); // link_opacity(d));
d3.selectAll('line[session_all*="'+label+'"][iteration_all*="'+i+'"]')
.attr("stroke-opacity", d => link_opacity(d));
d3.selectAll('circle[session_direct*="'+label+'"][iteration_all*="'+i+'"]')
.attr('opacity', d => node_opacity(d));
d3.selectAll('circle[session_all*="'+label+'"][iteration_all*="'+i+'"]')
.attr('opacity', d => node_opacity(d));
d3.selectAll('circle[session_from_reco*="'+label+'"][iteration_all*="'+i+'"]')
.attr('opacity', d => node_opacity(d));
//await sleep(2000);
}
}
}
const show_session_direct = function(label){
d3.selectAll('text[session_legend="'+label+'"]').attr('opacity', 1);
if(min_occ_iter == 0){
d3.selectAll('line[session_direct*="'+label+'"][level="1"]')
.attr("stroke-opacity", d => link_opacity_nofilter(d));
d3.selectAll('circle[session_direct*="'+label+'"]').attr('opacity', d => node_opacity_nofilter(d));
} else {
for(let i=1;i<=min_occ_iter;i++){
d3.selectAll('line[session_direct*="'+label+'"][level="1"][iteration_all*="'+i+'"]')
.attr("stroke-opacity", d => link_opacity_nofilter(d));
d3.selectAll('circle[session_direct*="'+label+'"][iteration_all*="'+i+'"]')
.attr('opacity', d => node_opacity_nofilter(d));
//await sleep(2000);
}
}
}
const show_channel = function(label){
d3.selectAll('rect[channel_legend="'+label+'"]').attr('opacity', 0);
if(min_occ_iter == 0){
d3.selectAll('line[channel_source="'+label+'"]').attr("stroke-opacity", d => link_opacity(d));
d3.selectAll('line[channel_target="'+label+'"]').attr("stroke-opacity", d => link_opacity(d));
d3.selectAll('circle[channel="'+label+'"]').attr('opacity', d => node_opacity(d));
} else {
for(let i=1;i<=t;i++){
d3.selectAll('line[channel_source="'+label+'"][iteration_all*="'+i+'"]')
.attr("stroke-opacity", d => link_opacity(d));
d3.selectAll('line[channel_target="'+label+'"][iteration_all*="'+i+'"]')
.attr("stroke-opacity", d => link_opacity(d));
d3.selectAll('circle[channel="'+label+'"][iteration_all*="'+i+'"]')
.attr('opacity', d => node_opacity(d));
//await sleep(2000);
}
}
}
var reshow_session_channel = function(){
if(active_session.length == 0 & active_channel.length == 0 & min_occ_iter == 0){
d3.selectAll('.legend_session').attr('active', 'no');
d3.selectAll('.legend_channel').attr('active', 'no');
undo_highlight();
} else {
hide_all();
active_session.forEach(label => show_session(label));
active_channel.forEach(label => show_channel(label));
}
}
///// TOOLTIP - interaction
node.on('mouseover', d => {
tooltip.show(d);
hide_all_light();
// Higlight basic links
d3.selectAll('circle[id="'+d.id+'"]').attr('opacity', node_opacity(d));
var links_src = d3.selectAll('line[source="'+d.id+'"]');
var links_tar = d3.selectAll('line[target="'+d.id+'"]');
links_src.attr('stroke-opacity', dd => link_opacity(dd));
links_tar.attr('stroke-opacity', dd => link_opacity(dd));
links_src.each(e => {
d3.selectAll('circle[id="'+e.target.id+'"]').attr('opacity', node_opacity(e.target));
});
links_tar.each(e => {
d3.selectAll('circle[id="'+e.source.id+'"]').attr('opacity', node_opacity(e.source));
});
// Highlight nodes & session links
var session_list = [];
d.session_all.split(';').forEach(s => {
// s <- s.trim(); if(session_list.indexOf(s) ==-1) { session_list.push(s); }
session_list.push(s.trim());
});
session_list.forEach(label => {
show_session_direct(label);
d3.selectAll('text[session_legend="'+label+'"]').attr('opacity', 1);
});
});
node.on('mouseout', () => {
tooltip.hide();
hide_all();
reshow_session_channel();
});
////// LEGEND SESSION
// Rendering functions
var toggle_legend = function(label){
var element = d3.selectAll('rect[session_legend="'+label+'"]');
if(element.attr('active') == 'yes'){
element.attr('active', 'no');
active_session.splice(active_session.indexOf(label), 1);
hide_all();
reshow_session_channel();
} else {
element.attr('active', 'yes');
var div = d3.selectAll('.legend_div');
if(div.attr('active')=='no'){ div.attr('active', 'yes');}
active_session.push(label);
reshow_session_channel();
}
}
// Svg elements
legend_svg.selectAll("rect")
.data(groups)
.join("rect")
.attr('class', "session_swatch")
.attr("session_swatch", d => d)
.attr('width', 15)
.attr('height', 15)
.attr('fill', d => color_scale(d))
.attr('x',20)
.attr('y', (d,i) => 45+i*25);
legend_svg.selectAll("text")
.data(groups)
.join("text")
.attr('class', "session_legend")
.attr("session_legend", d => d)
.attr('height', 15)
.attr('fill', '#000')
.attr('x', 45)
.attr('y', (d,i) => 59+i*25)
.html(d => d);
var legend_rect = legend_svg.append("g")
.selectAll("rect")
.data(groups)
.join("rect")
.attr('class','legend_selector')
.attr('style', 'cursor: pointer;')
.attr('width', 270)
.attr('height', 15)
.attr('opacity', 0)
.attr('x', 10)
.attr('y', (d,i) => 45+i*25)
.attr('session_legend', d => d)
.attr('active','no')
legend_rect.on('click', d => toggle_legend(d));
legend_rect.on('mouseover', d => {
if(active_session.length == 0 & active_channel.length == 0 & min_occ == 1 & min_occ_link == 1) hide_all();
show_session_direct(d);
});
legend_rect.on('mouseout', d => {
reshow_session_channel();
});
legend_svg.append("text")
.html('Sessions:')
.attr('font-weight','bold')
.attr('x', 25)
.attr('y', 30);
////////////////////////////////////////////////////////////////////////
////// LEGEND CHANNEL
////////////////////////////////////////////////////////////////////////
// Rendering functions
var toggle_legend_channel = function(label){
var element = d3.selectAll('rect[channel_legend="'+label+'"]');
if(element.attr('active') == 'yes'){
element.attr('active', 'no');
d3.selectAll('rect[channel_legend="'+label+'"]').attr('opacity', 0.5);
active_channel.splice(active_channel.indexOf(label), 1);
hide_all();
reshow_session_channel();
} else {
element.attr('active', 'yes');
var div = d3.selectAll('.legend_channel');
if(div.attr('active')=='no'){
div.attr('active', 'yes');
}
active_channel.push(label);
reshow_session_channel();
}
}
// Svg elements
legend_channel.selectAll("rect")
.data(channel_sorted)
.join("rect")
.attr('class', "channel_swatch")
.attr("channel_swatch", d => d)
.attr('width', 15)
.attr('height', 15)
.attr('fill', '#ddd')
//.attr('fill', d => color_scale_channel(d))
.attr('x',20)
.attr('y', (d,i) => 45+i*25);
legend_channel.selectAll("text")
.data(channel_sorted)
.join("text")
.attr('class', "channel_legend")
.attr("channel_legend", d => d)
.attr('height', 15)
.attr('fill', '#000')
.attr('x', 45)
.attr('y', (d,i) => 59+i*25)
.html(d => d);
var legend_rect_channel = legend_channel.append("g")
.selectAll("rect")
.data(channel_sorted)
.join("rect")
.attr('class','legend_selector_channel')
.attr('style', 'cursor: pointer;')
.attr('width', 300)
.attr('height', 18)
.attr('opacity', 0)
.attr('fill', '#fff')
.attr('x', 10)
.attr('y', (d,i) => 45+i*25)
.attr('channel_legend', d => d)
.attr('active','no')
legend_rect_channel.on('click', d => toggle_legend_channel(d));
legend_rect_channel.on('mouseover', d => {
if(active_channel.length == 0 & active_session.length == 0 & min_occ == 1 & min_occ_link == 1) hide_all();
show_channel(d);
});
legend_rect_channel.on('mouseout', d => {
reshow_session_channel();
});
legend_channel.append("text")
.attr('x', 25)
.attr('y', 30)
.append('tspan')
.html('Channels:')
.attr('font-weight','bold')
.append('tspan')
.html(' (most frequent first)')
.attr('font-weight','normal');
////// FILTERS
// Channel or Video
var color_video = function(){
d3.select('#color_video').attr('opacity', 1);
d3.select('#color_channel').attr('opacity', 0.5);
d3.select('#color_none').attr('opacity', 0.5);
d3.selectAll('.session_swatch').attr('fill', d => color_scale(d));
d3.selectAll('.channel_swatch').attr('fill', '#ddd');
d3.selectAll('.video_node').attr('fill', d => color_scale(d.session));
};
var color_channel = function(){
d3.select('#color_video').attr('opacity', 0.5);
d3.select('#color_channel').attr('opacity', 1);
d3.select('#color_none').attr('opacity', 0.5);
d3.selectAll('.session_swatch').attr('fill', '#ddd');
d3.selectAll('.channel_swatch').attr('fill', d => color_scale_channel(d));
d3.selectAll('.video_node').attr('fill', d => color_scale_channel(d.channel));
};
var color_none = function(){
d3.select('#color_video').attr('opacity', 0.5);
d3.select('#color_channel').attr('opacity', 0.5);
d3.select('#color_none').attr('opacity', 1);
d3.selectAll('.session_swatch').attr('fill', '#ddd');
d3.selectAll('.channel_swatch').attr('fill', '#ddd');
d3.selectAll('.video_node').attr('fill', '#999');
};
var filter_level_label = svg.append('g')
.attr('transform','translate('+ (300 - width/2) +','+ (30 - height/2) +')')
.append('text').html(`<tspan font-weight="bold">Color</tspan>: `);
filter_level_label.append('a').attr('style', 'cursor: pointer;').attr('id', 'color_video')
.html(' Session ').on('click', () => color_video());
filter_level_label.append('a').attr('style', 'cursor: pointer;').attr('id', 'color_channel')
.html(' Channel ').on('click', () => color_channel())
.attr('opacity', 0.5);
filter_level_label.append('a').attr('style', 'cursor: pointer;').attr('id', 'color_none')
.html(' (none) ').on('click', () => color_none())
.attr('opacity', 0.5);
// Levels of links
var link_l1 = function(){
show_l1 = !show_l1;
if(show_l1) d3.select('#filter_l1').attr('opacity', 1);
else d3.select('#filter_l1').attr('opacity', 0.5);
hide_all();
reshow_session_channel();
};
var link_l2 = function(){
show_l2 = !show_l2;
if(show_l2) d3.select('#filter_l2').attr('opacity', 1);
else d3.select('#filter_l2').attr('opacity', 0.5);
hide_all();
reshow_session_channel();
};
var link_l34 = function(){
show_l34 = !show_l34;
if(show_l34) d3.select('#filter_l34').attr('opacity', 1);
else d3.select('#filter_l34').attr('opacity', 0.5);
hide_all();
reshow_session_channel();
};
var filter_level_label = svg.append('g')
.attr('transform','translate('+ (300 - width/2) +','+ (60 - height/2) +')')
.append('text').html(`<tspan font-weight="bold">Filter recommendations</tspan>:`);
filter_level_label.append('a').attr('style', 'cursor: pointer;').attr('id', 'filter_l1')
.html(' Watched ').on('click', () => link_l1());
filter_level_label.append('a').attr('style', 'cursor: pointer;').attr('id', 'filter_l2')
.html('→ Recommended ').on('click', () => link_l2());
//filter_level_label.append('a').attr('style', 'cursor: pointer;').attr('id', 'filter_l34')
// .html('→ Recommended from a recommendation').on('click', () => link_l34());
// Nodes
var min_occ_plus = function(){};
var min_occ_minus = function(){};
var reset_min_occ = function(){};
var filter_svg = svg.append('g').attr('transform','translate('+ (300 - width/2) +','+ (90 - height/2) +')');
var filter_node_label = filter_svg.append('text');
var min_occ_update = function(){
filter_node_label.html(`<tspan font-weight="bold">Filter nodes</tspan>: Min no. of occurences <tspan font-weight="bold">${min_occ}</tspan>`);
filter_node_label.append('a').attr('style', 'cursor: pointer;')
.html(' ▲ More').on('click', () => min_occ_plus());
filter_node_label.append('a').attr('style', 'cursor: pointer;')
.html(' ▼ Less').on('click', () => min_occ_minus());
filter_node_label.append('a').attr('style', 'cursor: pointer;')
.html(' (resest)').on('click', () => reset_min_occ());
}
min_occ_plus = function(){
min_occ = min_occ +1;
min_occ_update();
hide_all();
reshow_session_channel();
};
min_occ_minus = function(){
min_occ = min_occ -1;
min_occ_update();
hide_all();
reshow_session_channel();
};
reset_min_occ = function(){
min_occ = 1;
min_occ_update();
hide_all();
reshow_session_channel();
};
min_occ_update();
// Links
var min_occ_plus_link = function(){};
var min_occ_minus_link = function(){};
var reset_min_occ_link = function(){};
var filter_svg_link = svg.append('g').attr('transform','translate('+(300-width/2)+','+(120-height/2)+')');
var filter_link_label = filter_svg_link.append('text');
var min_occ_update_link = function(){
filter_link_label.html(`<tspan font-weight="bold">Filter links</tspan>: Min no. of occurences <tspan font-weight="bold">${min_occ_link}</tspan>`);
filter_link_label.append('a').attr('style', 'cursor: pointer;')
.html(' ▲ More').on('click', () => min_occ_plus_link());
filter_link_label.append('a').attr('style', 'cursor: pointer;')
.html(' ▼ Less').on('click', () => min_occ_minus_link());
filter_link_label.append('a').attr('style', 'cursor: pointer;')
.html(' (resest)').on('click', () => reset_min_occ_link());
}
min_occ_plus_link = function(){
min_occ_link = min_occ_link +1;
min_occ_update_link();
hide_all();
reshow_session_channel();
};
min_occ_minus_link = function(){
min_occ_link = min_occ_link -1;
min_occ_update_link();
hide_all();
reshow_session_channel();
};
reset_min_occ_link = function(){
min_occ_link = 1;
min_occ_update_link();
hide_all();
reshow_session_channel();
};
min_occ_update_link();
// Iteration
var min_occ_plus_iter = function(){};
var min_occ_minus_iter = function(){};
var reset_min_occ_iter = function(){};
var filter_svg_iter = svg.append('g').attr('transform','translate('+(300-width/2)+','+(150-height/2)+')');
var filter_iter_label = filter_svg_iter.append('text');
var min_occ_update_iter = function(){
var n_iter = min_occ_iter_new;
if(n_iter == 0) {
n_iter = '(All)';
}
filter_iter_label.html(`<tspan font-weight="bold">Filter iterations</tspan>: Show sessions until iteration <tspan font-weight="bold">${n_iter}</tspan>`);
filter_iter_label.append('a').attr('style', 'cursor: pointer;')
.html(' ▲ More').on('click', () => min_occ_plus_iter());
filter_iter_label.append('a').attr('style', 'cursor: pointer;')
.html(' ▼ Less').on('click', () => min_occ_minus_iter());
filter_iter_label.append('a').attr('style', 'cursor: pointer;')
.html(' (resest)').on('click', () => reset_min_occ_iter());
}
min_occ_plus_iter = function(){
min_occ_iter_new = min_occ_iter_new +1;
min_occ_update_iter();
hide_all();
reshow_session_channel();
};
min_occ_minus_iter = function(){
min_occ_iter_new = min_occ_iter_new -1;
min_occ_update_iter();
hide_all();
reshow_session_channel();
};
reset_min_occ_iter = function(){
min_occ_iter_new = 0;
min_occ_update_iter();
hide_all();
reshow_session_channel();
};
min_occ_update_iter();
///// ANIMATION
simulation.on("tick", () => {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
});
invalidation.then(function () {
simulation.stop();
window.removeEventListener('resize', onresize);
});
///// DONE
return svg.node();
}