<!DOCTYPE html> <meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="style.css" />
<div class="row">
<div class="column">
<table>
<tr>
<td>Type:</td>
<td>Guide:</td>
<td></td>
</tr>
<tr>
<td><select id="dropdown_type" onchange="changeType()"></select></td>
<td><select id="dropdown_guide" onchange="changeGuide()"></select></td>
<td><a id="link" href="#" target="_blank">Link</a></td>
</tr>
</table>
<div id="info"></div>
</div>
<div class="column"><svg width="800" height="600"></svg></div>
</div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="graph.js"></script>
body {
font-family: Arial, Helvetica, sans-serif;
}
.column {
float: left;
width: 30%;
}
/* Clear floats after the columns */
.row:after {
content: "";
display: table;
clear: both;
}
svg {
display: block;
margin: auto;
border: 1px solid black;
}
.links line {
stroke: #999;
pointer-events: none;
}
.nodes circle {
stroke: #000;
stroke-width: 1px;
}
.labels {
font-size: 18px;
font-weight: bold;
}
.primary {
color: #af0000;
}
.secondary {
color: #00a700;
}
div.tooltip {
position: relative;
top:10px;
font-size: 14px;
background-color: white;
opacity: 0;
height: auto;
padding: 10px;
}
div.tooltip .Legendary {
color: #ff9b00;
}
div.tooltip .Epic {
color: #ac41c2;
}
div.tooltip .Elite {
color: #058cc3;
}
div.tooltip .Advanced {
color: #2d9830;
}
div.tooltip table {
position: relative;
width:100%;
font-size: 12px;
border: 1px solid black;
border-collapse: collapse;
}
div.tooltip table td {
padding: 2px;
}
// Set paths and options
const guides = {
General: {
'Legend Rhony': {
data: 'https://raw.githubusercontent.com/sho-87/rok-data/master/commander_pairings/data/legend_rhony.csv',
url: 'https://www.youtube.com/watch?v=gSY6CfG2vg4',
strength: -3000,
distance: 500,
radius: 1.5
},
detectiveG: {
data: 'https://raw.githubusercontent.com/sho-87/rok-data/master/commander_pairings/data/detectiveG.csv',
url:
'https://docs.google.com/spreadsheets/d/1YqsYjNAxzHHODfzJoPhN3kzyG9xwGvK7NmoK1e3ADdk',
strength: -1500,
distance: 500,
radius: 2
}
},
Garrison: {
'Legend Rhony': {
data: 'https://raw.githubusercontent.com/sho-87/rok-data/master/commander_pairings/data/legend_rhony_garrison.csv',
url: 'https://www.youtube.com/watch?v=YhxwVI6j1mI',
strength: -3000,
distance: 500,
radius: 3
}
}
};
const commanders = 'https://raw.githubusercontent.com/sho-87/rok-data/master/commander_pairings/commanders.json';
// Set initial options
const options_type = Object.keys(guides);
const select_type = document.getElementById('dropdown_type');
const select_guide = document.getElementById('dropdown_guide');
for (let i = 0; i < options_type.length; i++) {
opt = document.createElement('option');
opt.textContent = options_type[i];
select_type.appendChild(opt);
}
changeType();
// Functions for guide selection
function changeType() {
select_guide.length = 0;
guide_list = Object.keys(guides[select_type.value]);
for (let i = 0; i < guide_list.length; i++) {
opt = document.createElement('option');
opt.textContent = guide_list[i];
select_guide.appendChild(opt);
}
changeGuide();
}
function changeGuide() {
guide_type = select_type.value;
guide = select_guide.value;
a = document.getElementById('link');
a.href = guides[guide_type][guide].url;
loadGraph(
guides[guide_type][guide].data,
guides[guide_type][guide].strength,
guides[guide_type][guide].distance,
guides[guide_type][guide].radius
);
}
// Functions for graph generation
function loadGraph(data, strength, distance, radius) {
const svg = d3.select('svg'),
width = +svg.attr('width'),
height = +svg.attr('height');
svg.selectAll('*').remove();
const zoom = d3
.zoom()
.scaleExtent([-8 / 2, 4])
.on('zoom', zoomed);
svg.call(zoom);
const g = svg.append('g');
const color = d3
.scaleOrdinal()
.domain(['Legendary', 'Epic', 'Elite', 'Advanced'])
.range(['#ff9b00', '#ac41c2', '#058cc3', '#2d9830']);
const tooltip = d3.select('#info').attr('class', 'tooltip');
const simulation = d3
.forceSimulation()
.force('link', d3.forceLink().id(d => d.id))
.force(
'charge',
d3
.forceManyBody()
.strength(strength)
.distanceMax([distance])
)
.force('center', d3.forceCenter(width / 2, height / 2));
// Read data from files
d3.queue()
.defer(d3.json, commanders)
.defer(d3.csv, data)
.await(function(error, commanders, links) {
if (error) {
console.error(error);
} else {
links = links.map(d => ({
source: d.primary,
target: d.secondary,
rank: d.rank
}));
// calculate node weights (number of links)
commanders.nodes.forEach(n => {
n.weight = (function() {
let weight = 0;
links.forEach(l => {
if ((n.id === l.source) | (n.id === l.target)) {
weight++;
}
});
return weight;
})();
});
// links
const link = g
.attr('class', 'links')
.selectAll('line')
.data(links)
.enter()
.append('line')
.attr('stroke-width', d => (1 / d.rank) * 2.3);
// nodes
const node = g
.selectAll('.node')
.data(commanders.nodes)
.enter()
.append('g')
.attr('class', 'nodes')
.filter(d => {
if (d.weight != 0) {
return this;
}
})
.call(
d3
.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended)
);
node
.append('circle')
.attr('r', d => d.weight * radius)
.attr('fill', d => color(d.group))
.on('mouseover.tooltip', function(d) {
// Generate tooltip text
info = `<strong>${d.id}</strong><br/><span class=${d.group}>${
d.group
}</span><br>Versatility (# of pairs): ${d.weight}`;
table_primary = `<p><table id='tooltipTablePrimary' border=1>`;
table_secondary = `<p><table id='tooltipTableSecondary' border=1>`;
header = `<tr style="font-weight:bold">
<td class='primary' width='45%'>Primary</td>
<td class='secondary' width='45%'>Secondary</td>
<td>Rank</td></tr>`;
rank_sum = 0;
rows_primary = '';
rows_secondary = '';
links.forEach(pair => {
if (d.id === pair.source.id) {
rank_sum += Number(pair.rank);
rows_primary += `<tr><td style='font-weight:bold'>${
pair.source.id
}</td><td>${pair.target.id}</td><td>${pair.rank}</td></tr>`;
} else if (d.id === pair.target.id) {
rank_sum += Number(pair.rank);
rows_secondary += `<tr><td>${
pair.source.id
}</td><td style='font-weight:bold'>${pair.target.id}</td><td>${
pair.rank
}</td></tr>`;
}
});
table_primary += header + rows_primary + '</table>';
table_secondary += header + rows_secondary + '</table>';
tables_combined = '';
if (rows_primary !== '') {
tables_combined += table_primary;
}
if (rows_secondary !== '') {
tables_combined += table_secondary;
}
tooltip
.transition()
.duration(300)
.style('opacity', 1);
tooltip.html(info + tables_combined);
sortTable('tooltipTablePrimary');
sortTable('tooltipTableSecondary');
})
.on('mouseout.tooltip', function() {
tooltip
.transition()
.duration(100)
.style('opacity', 0);
})
.on('mouseover.fade', fade(0.05))
.on('mouseout.fade', fade(1));
// node labels
node
.append('text')
.text(d => d.id)
.attr('class', 'labels')
.attr('x', 0)
.attr('y', 0);
simulation.nodes(commanders.nodes).on('tick', ticked);
simulation.force('link').links(links);
function ticked() {
// zoom to bounding box of nodes
if (this.alpha() > 0.04) {
// set up zoom transform
var xExtent = d3.extent(node.data(), function(d) {
return d.x + 100;
});
var yExtent = d3.extent(node.data(), function(d) {
return d.y;
});
// get scales
var xScale = (width / (xExtent[1] - xExtent[0])) * 0.75;
var yScale = (height / (yExtent[1] - yExtent[0])) * 0.75;
// get most restrictive scale
var minScale = Math.min(xScale, yScale);
if (minScale < 1) {
var transform = d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(minScale)
.translate(
-(xExtent[0] + xExtent[1]) / 2,
-(yExtent[0] + yExtent[1]) / 2
);
svg.call(zoom.transform, transform);
}
} else {
svg.attr('cursor', 'pointer');
var check = false;
}
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('transform', d => `translate(${d.x},${d.y})`);
}
const linkedByIndex = {};
links.forEach(d => {
linkedByIndex[`${d.source.index},${d.target.index}`] = 1;
});
function isConnected(a, b) {
return (
linkedByIndex[`${a.index},${b.index}`] ||
linkedByIndex[`${b.index},${a.index}`] ||
a.index === b.index
);
}
function fade(opacity) {
return d => {
node.style('stroke-opacity', function(o) {
const thisOpacity = isConnected(d, o) ? 1 : opacity;
this.setAttribute('fill-opacity', thisOpacity);
return thisOpacity;
});
link.style('stroke-opacity', o =>
o.source === d || o.target === d ? 1 : opacity
);
};
}
function sortTable(id) {
var table, rows, switching, i, x, y, shouldSwitch;
table = document.getElementById(id);
switching = true;
while (switching) {
try {
rows = table.rows;
} catch (err) {
return;
}
switching = false;
for (i = 1; i < rows.length - 1; i++) {
shouldSwitch = false;
x = rows[i].getElementsByTagName('TD')[2];
y = rows[i + 1].getElementsByTagName('TD')[2];
if (Number(x.innerHTML) > Number(y.innerHTML)) {
shouldSwitch = true;
break;
}
}
if (shouldSwitch) {
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
switching = true;
}
}
}
}
});
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
function zoomed() {
g.attr('transform', d3.event.transform);
}
}
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.