This email data was collected by US federal investigators in the wake of the Enron collapse. It is a useful real data set that has been studied by many researchers.
When looking at email data from an organisation a common task is to try to work out individual roles and organisational hierarchies just from the metadata of email traffic directions.
Sub-teams within the organisation stand out as clusters of connected nodes in the chart, but identifying important individuals requires a little more analysis.
Weighted Links
Clicking on Email Volumes sizes link widths based on the number of emails sent between individuals. This highlights a prominent individual - Bill Williams - in a group on the edge of the chart. Bill is a prolific emailer, and probably the manager of a team. The team don't seem to be engaging with him though, as hardly any of them write back.
You can follow a link from Bill to Timothy who is more central. This is probably Bill's boss. Following the thicker links from Timothy, we see that his boss was probably either John, Louise or Kevin.
Centrality Analysis
KeyLines has some useful network algorithms that can be used to score the email accounts in different ways. We can use these scores to modify the size of nodes, highlighting influential individuals.
See the Graph Centrality documentation for more detail on different centrality measures.
Degrees counts the number of directly connected nodes each node has.
Closeness measures how may steps each node is from every other node in the graph. It helps identify 'broadcasters', or people who have good influence over the network.
Betweenness counts the number of shortest paths all nodes are on, identifying important 'bridges' in the network. In sociology, this has been shown to correlate with seniority in organisations.
Eigenvector centrality is a measure of influence that takes into account the number of links each person has and the number of links their connections have, and so on throughout the network. It is an effective measure of influence in social networks and malware propagation.
PageRank identifies important nodes by counting incoming links and weighting according to the relative scores of their originating nodes. It helps identify nodes which are indirectly influential to the network.
Key functions used:
- chart.animateProperties
- chart.graph().betweenness
- chart.graph().closeness
- chart.graph().degrees
- chart.graph().pageRank
- chart.graph().eigenCentrality
import KeyLines from "keylines";
import { data, colours } from "./data.js";
let chart;
let miniChart;
let restoreIds = [];
// Track UI state
const state = {
sizeBy: "same",
volume: "off",
direction: "any",
};
const colourMap = {};
/**
* debounce - This function delays execution of the passed "fn" until "timeToWait" milliseconds
* have passed since the last time it was called. This ensures that the function
* runs at the end of a particular action to keep performance high.
*/
function debounce(fn, timeToWait = 100) {
let timeoutId;
return function debouncedFn(...args) {
const timeoutFn = () => {
timeoutId = undefined;
fn.apply(this, args);
};
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(timeoutFn, timeToWait);
};
}
// ensure that this doesn't get called too often when dragging the selection
// marquee
const loadMiniChart = debounce((items) => {
miniChart.load({
type: "LinkChart",
items,
});
miniChart.layout("organic", { consistent: true });
});
function isLink(id) {
return id.match("-");
}
function updateHighlight(itemIds) {
const props = [];
// Remove previous styles
if (restoreIds) {
props.push(
...restoreIds.map((id) => {
const colour =
colourMap[id] || (isLink(id) ? colours.link : colours.node);
return {
id,
c: colour,
b: colour,
ha0: null,
};
}),
);
restoreIds = [];
}
// Add new styles
if (itemIds.length) {
// Find the neighbours of the provided items
const toHighlight = [];
itemIds.forEach((id) => {
if (isLink(id)) {
const link = chart.getItem(id);
toHighlight.push(id, link.id1, link.id2);
} else {
const neighbours = chart.graph().neighbours(id);
toHighlight.push(id, ...neighbours.nodes, ...neighbours.links);
}
});
// For each neighbouring item, add some styling
toHighlight.forEach((id) => {
// Cache the existing styles
restoreIds.push(id);
// Generate new styles
const style = {
id,
};
if (isLink(id)) {
// For links, just set the colour
style.c = colours.selected;
} else {
// For nodes, add a halo
style.ha0 = {
c: colours.selected,
r: 34,
w: 6,
};
}
props.push(style);
});
}
chart.setProperties(props);
}
async function runLayout(inconsistent, mode) {
const packing = mode === "adaptive" ? "adaptive" : "circle";
return chart.layout("organic", {
time: 500,
tightness: 4,
consistent: !inconsistent,
packing,
mode,
});
}
function klReady(charts) {
[chart, miniChart] = charts;
chart.load(data);
chart.zoom("fit", { animate: false }).then(runLayout);
// On selection change:
// 1) add styles to the targeted and neighbouring items
// 2) copy the selected item and its neighbours to the miniChart
chart.on("selection-change", () => {
const ids = chart.selection();
updateHighlight(ids);
const miniChartItems = [];
if (ids.length > 0) {
const { nodes, links } = chart.graph().neighbours(ids);
chart.each({ type: "node" }, (node) => {
if (ids.includes(node.id) || nodes.includes(node.id)) {
// Clear hover styling and position
node.x = 0;
node.y = 0;
delete node.ha0;
miniChartItems.push(node);
}
});
chart.each({ type: "link" }, (link) => {
if (ids.includes(link.id) || links.includes(link.id)) {
link.c = colours.link;
miniChartItems.push(link);
}
});
}
loadMiniChart(miniChartItems);
});
}
// this function picks a colour from a range of colours based on the value
function colourPicker(value) {
const { bands, node } = colours;
if (value > 0.75) {
return bands[2];
}
if (value > 0.5) {
return bands[1];
}
if (value > 0.25) {
return bands[0];
}
return node;
}
function normalize(max, min, value) {
if (max === min) {
return min;
}
return (value - min) / (max - min);
}
function miniChartFilter(items) {
return items.filter(({ id }) => miniChart.getItem(id));
}
async function animateValues(values, links) {
const valuesArray = Object.values(values);
const max = Math.max(...valuesArray);
const min = Math.min(...valuesArray);
const items = Object.entries(values)
.map(([id, value]) => {
// Normalize the value in the range 0 -> 1
const normalized = normalize(max, min, value);
// enlarge nodes with higher values
const e = Math.max(1, normalized * 5);
// Choose a colour (use bands if there is a range of values)
const c = max !== min ? colourPicker(normalized) : colours.node;
colourMap[id] = c;
return { id, e, c, b: c };
})
.concat(links);
const miniItems = miniChartFilter(items);
// Update the main chart and miniChart concurrently
return Promise.all([
chart
.animateProperties(items, { time: 500 })
.then(() => runLayout(undefined, "adaptive")),
miniChart
.animateProperties(miniItems, { time: 500 })
.then(() => miniChart.layout("organic", { consistent: true })),
]);
}
function same() {
return new Promise((resolve) => {
const sizes = {};
chart.each({ type: "node" }, (node) => {
sizes[node.id] = 0;
});
resolve(sizes);
});
}
function wrapCallback(fn) {
return (options) => new Promise((resolve) => resolve(fn(options)));
}
function getAnalysisFunction(name) {
if (name.match(/^(degrees|pageRank|eigenCentrality)$/)) {
return wrapCallback(chart.graph()[name]);
}
if (name.match(/^(closeness|betweenness)$/)) {
return chart.graph()[name];
}
return same;
}
async function analyseChart() {
const { sizeBy, volume } = state;
const options = {};
// Configure weighting
if (volume === "on") {
if (sizeBy.match(/^(betweenness|closeness)$/)) {
options.weights = true;
}
options.value = "count";
}
// Configure direction options
if (sizeBy.match(/^(betweenness|pageRank)$/)) {
options.directed = state.direction !== "any";
} else {
options.direction = state.direction;
}
const analyse = getAnalysisFunction(sizeBy);
const values = await analyse(options);
const linkWidths = calculateLinkWidths(volume === "on");
return animateValues(values, linkWidths);
}
function calculateLinkWidths(showValue) {
const links = [];
chart.each({ type: "link" }, (link) => {
const linkcount = link.d.count;
let width = 1;
if (showValue) {
if (linkcount > 300) {
width = 36;
} else if (linkcount > 200) {
width = 27;
} else if (linkcount > 100) {
width = 18;
} else if (linkcount > 50) {
width = 9;
}
}
links.push({ id: link.id, w: width });
});
return links;
}
function doZoom(name) {
chart.zoom(name, { animate: true, time: 350 });
}
function registerClickHandler(id, fn) {
document.getElementById(id).addEventListener("click", fn);
}
function updateActiveState(nodes, activeValue) {
nodes.forEach((node) => {
if (node.value === activeValue) {
node.classList.add("active");
} else {
node.classList.remove("active");
}
});
}
function registerButtonGroup(className, handler) {
const nodes = document.querySelectorAll(`.${className}`);
nodes.forEach((node) => {
node.addEventListener("click", () => {
const { value } = node;
updateActiveState(nodes, value);
handler(value);
});
});
}
function initUI() {
// Chart overlay
registerClickHandler("home", () => {
doZoom("fit");
});
registerClickHandler("zoomIn", () => {
doZoom("in");
});
registerClickHandler("zoomOut", () => {
doZoom("out");
});
registerClickHandler("changeMode", () => {
const hand = !!chart.options().handMode; // be careful with undefined
chart.options({ handMode: !hand });
const icon = document.getElementById("iconMode");
icon.classList.toggle("fa-arrows-alt");
icon.classList.toggle("fa-edit");
});
registerClickHandler("layout", () => {
runLayout(true, "full");
});
// Right hand menu
registerButtonGroup("volume", (volume) => {
state.volume = volume;
analyseChart();
});
registerButtonGroup("size", (sizeBy) => {
state.sizeBy = sizeBy;
analyseChart();
});
registerButtonGroup("direction", (direction) => {
state.direction = direction;
analyseChart();
});
}
async function loadKeyLines() {
initUI();
const baseOpts = {
arrows: "normal",
handMode: true,
navigation: { shown: false },
overview: { icon: false },
selectedNode: {
c: colours.selected,
b: colours.selected,
fbc: colours.selected,
},
selectedLink: {
c: colours.selected,
},
};
const mainChartConfig = {
container: "klchart",
options: Object.assign({}, baseOpts, {
drag: {
links: false,
},
logo: { u: "/public/images/Logo.png" },
}),
};
const miniChartConfig = {
container: "minikl",
options: baseOpts,
};
const charts = await KeyLines.create([mainChartConfig, miniChartConfig]);
klReady(charts);
}
window.addEventListener("DOMContentLoaded", loadKeyLines); export const colours = {
node: "#ffa726",
link: "#bcced8",
bands: ["#f57c00", "#ef5350", "#e91e63"],
hover: "#62efff",
selected: "#42a5f5",
};
export const data = {
type: "LinkChart",
items: [
{
id: "343116-343246",
type: "link",
id1: "343116",
id2: "343246",
d: {
count: 200,
},
a2: true,
},
{
id: "343116-343244",
type: "link",
id1: "343116",
id2: "343244",
d: {
count: 199,
},
a2: true,
},
{
id: "343116-343229",
type: "link",
id1: "343116",
id2: "343229",
d: {
count: 175,
},
a2: true,
},
{
id: "343116-343213",
type: "link",
id1: "343116",
id2: "343213",
d: {
count: 196,
},
a2: true,
},
{
id: "343116-343212",
type: "link",
id1: "343116",
id2: "343212",
d: {
count: 47,
},
a2: true,
},
{
id: "343116-343178",
type: "link",
id1: "343116",
id2: "343178",
d: {
count: 175,
},
a2: true,
},
{
id: "343116-343175",
type: "link",
id1: "343116",
id2: "343175",
d: {
count: 203,
},
a2: true,
},
{
id: "343116-343149",
type: "link",
id1: "343116",
id2: "343149",
d: {
count: 191,
},
a2: true,
},
{
id: "343116-343200",
type: "link",
id1: "343116",
id2: "343200",
d: {
count: 171,
},
a2: true,
},
{
id: "343116-343115",
type: "link",
id1: "343116",
id2: "343115",
d: {
count: 127,
},
a2: true,
},
{
id: "343121-343116",
type: "link",
id1: "343121",
id2: "343116",
d: {
count: 39,
},
a2: true,
},
{
id: "343213-343116",
type: "link",
id1: "343213",
id2: "343116",
d: {
count: 25,
},
a2: true,
},
{
id: "343121-343216",
type: "link",
id1: "343121",
id2: "343216",
d: {
count: 38,
},
a2: true,
},
{
id: "343121-343146",
type: "link",
id1: "343121",
id2: "343146",
d: {
count: 46,
},
a2: true,
},
{
id: "343121-343120",
type: "link",
id1: "343121",
id2: "343120",
d: {
count: 43,
},
a2: true,
},
{
id: "343121-343253",
type: "link",
id1: "343121",
id2: "343253",
d: {
count: 39,
},
a2: true,
},
{
id: "343121-343241",
type: "link",
id1: "343121",
id2: "343241",
d: {
count: 40,
},
a2: true,
},
{
id: "343121-343130",
type: "link",
id1: "343121",
id2: "343130",
d: {
count: 60,
},
a2: true,
},
{
id: "343121-343112",
type: "link",
id1: "343121",
id2: "343112",
d: {
count: 50,
},
a2: true,
},
{
id: "343241-343146",
type: "link",
id1: "343241",
id2: "343146",
d: {
count: 39,
},
a2: true,
},
{
id: "343134-343150",
type: "link",
id1: "343134",
id2: "343150",
d: {
count: 29,
},
a2: true,
},
{
id: "343119-343263",
type: "link",
id1: "343119",
id2: "343263",
d: {
count: 54,
},
a2: true,
},
{
id: "343119-343126",
type: "link",
id1: "343119",
id2: "343126",
d: {
count: 28,
},
a2: true,
},
{
id: "343134-343129",
type: "link",
id1: "343134",
id2: "343129",
d: {
count: 21,
},
a2: true,
},
{
id: "343134-343236",
type: "link",
id1: "343134",
id2: "343236",
d: {
count: 22,
},
a2: true,
},
{
id: "343134-343263",
type: "link",
id1: "343134",
id2: "343263",
d: {
count: 31,
},
a2: true,
},
{
id: "343134-343258",
type: "link",
id1: "343134",
id2: "343258",
d: {
count: 28,
},
a2: true,
},
{
id: "343134-343250",
type: "link",
id1: "343134",
id2: "343250",
d: {
count: 28,
},
a2: true,
},
{
id: "343134-343249",
type: "link",
id1: "343134",
id2: "343249",
d: {
count: 28,
},
a2: true,
},
{
id: "343134-343238",
type: "link",
id1: "343134",
id2: "343238",
d: {
count: 28,
},
a2: true,
},
{
id: "343134-343231",
type: "link",
id1: "343134",
id2: "343231",
d: {
count: 28,
},
a2: true,
},
{
id: "343134-343119",
type: "link",
id1: "343134",
id2: "343119",
d: {
count: 26,
},
a2: true,
},
{
id: "343134-343221",
type: "link",
id1: "343134",
id2: "343221",
d: {
count: 28,
},
a2: true,
},
{
id: "343134-343201",
type: "link",
id1: "343134",
id2: "343201",
d: {
count: 28,
},
a2: true,
},
{
id: "343134-343192",
type: "link",
id1: "343134",
id2: "343192",
d: {
count: 28,
},
a2: true,
},
{
id: "343134-343191",
type: "link",
id1: "343134",
id2: "343191",
d: {
count: 29,
},
a2: true,
},
{
id: "343134-343144",
type: "link",
id1: "343134",
id2: "343144",
d: {
count: 29,
},
a2: true,
},
{
id: "343134-343184",
type: "link",
id1: "343134",
id2: "343184",
d: {
count: 29,
},
a2: true,
},
{
id: "343134-343126",
type: "link",
id1: "343134",
id2: "343126",
d: {
count: 29,
},
a2: true,
},
{
id: "343134-343157",
type: "link",
id1: "343134",
id2: "343157",
d: {
count: 28,
},
a2: true,
},
{
id: "343134-343154",
type: "link",
id1: "343134",
id2: "343154",
d: {
count: 28,
},
a2: true,
},
{
id: "343134-343132",
type: "link",
id1: "343134",
id2: "343132",
d: {
count: 29,
},
a2: true,
},
{
id: "343134-343143",
type: "link",
id1: "343134",
id2: "343143",
d: {
count: 29,
},
a2: true,
},
{
id: "343134-343142",
type: "link",
id1: "343134",
id2: "343142",
d: {
count: 30,
},
a2: true,
},
{
id: "343134-343131",
type: "link",
id1: "343134",
id2: "343131",
d: {
count: 28,
},
a2: true,
},
{
id: "343134-343133",
type: "link",
id1: "343134",
id2: "343133",
d: {
count: 28,
},
a2: true,
},
{
id: "343134-343128",
type: "link",
id1: "343134",
id2: "343128",
d: {
count: 30,
},
a2: true,
},
{
id: "343134-343125",
type: "link",
id1: "343134",
id2: "343125",
d: {
count: 28,
},
a2: true,
},
{
id: "343230-343177",
type: "link",
id1: "343230",
id2: "343177",
d: {
count: 24,
},
a2: true,
},
{
id: "343222-343265",
type: "link",
id1: "343222",
id2: "343265",
d: {
count: 26,
},
a2: true,
},
{
id: "343222-343124",
type: "link",
id1: "343222",
id2: "343124",
d: {
count: 100,
},
a2: true,
},
{
id: "343222-343243",
type: "link",
id1: "343222",
id2: "343243",
d: {
count: 166,
},
a2: true,
},
{
id: "343222-343119",
type: "link",
id1: "343222",
id2: "343119",
d: {
count: 27,
},
a2: true,
},
{
id: "343222-343230",
type: "link",
id1: "343222",
id2: "343230",
d: {
count: 22,
},
a2: true,
},
{
id: "343222-343218",
type: "link",
id1: "343222",
id2: "343218",
d: {
count: 34,
},
a2: true,
},
{
id: "343222-343215",
type: "link",
id1: "343222",
id2: "343215",
d: {
count: 32,
},
a2: true,
},
{
id: "343222-343206",
type: "link",
id1: "343222",
id2: "343206",
d: {
count: 25,
},
a2: true,
},
{
id: "343222-343226",
type: "link",
id1: "343222",
id2: "343226",
d: {
count: 31,
},
a2: true,
},
{
id: "343222-343193",
type: "link",
id1: "343222",
id2: "343193",
d: {
count: 55,
},
a2: true,
},
{
id: "343222-343183",
type: "link",
id1: "343222",
id2: "343183",
d: {
count: 109,
},
a2: true,
},
{
id: "343222-343176",
type: "link",
id1: "343222",
id2: "343176",
d: {
count: 29,
},
a2: true,
},
{
id: "343222-343145",
type: "link",
id1: "343222",
id2: "343145",
d: {
count: 100,
},
a2: true,
},
{
id: "343222-343123",
type: "link",
id1: "343222",
id2: "343123",
d: {
count: 129,
},
a2: true,
},
{
id: "343130-343169",
type: "link",
id1: "343130",
id2: "343169",
d: {
count: 28,
},
a2: true,
},
{
id: "343130-343112",
type: "link",
id1: "343130",
id2: "343112",
d: {
count: 113,
},
a2: true,
},
{
id: "343130-343148",
type: "link",
id1: "343130",
id2: "343148",
d: {
count: 32,
},
a2: true,
},
{
id: "343130-343119",
type: "link",
id1: "343130",
id2: "343119",
d: {
count: 41,
},
a2: true,
},
{
id: "343130-343121",
type: "link",
id1: "343130",
id2: "343121",
d: {
count: 37,
},
a2: true,
},
{
id: "343130-343124",
type: "link",
id1: "343130",
id2: "343124",
d: {
count: 67,
},
a2: true,
},
{
id: "343130-343140",
type: "link",
id1: "343130",
id2: "343140",
d: {
count: 27,
},
a2: true,
},
{
id: "343130-343260",
type: "link",
id1: "343130",
id2: "343260",
d: {
count: 27,
},
a2: true,
},
{
id: "343130-343141",
type: "link",
id1: "343130",
id2: "343141",
d: {
count: 80,
},
a2: true,
},
{
id: "343130-343266",
type: "link",
id1: "343130",
id2: "343266",
d: {
count: 23,
},
a2: true,
},
{
id: "343130-343203",
type: "link",
id1: "343130",
id2: "343203",
d: {
count: 34,
},
a2: true,
},
{
id: "343130-343155",
type: "link",
id1: "343130",
id2: "343155",
d: {
count: 21,
},
a2: true,
},
{
id: "343112-343130",
type: "link",
id1: "343112",
id2: "343130",
d: {
count: 91,
},
a2: true,
},
{
id: "343148-343251",
type: "link",
id1: "343148",
id2: "343251",
d: {
count: 21,
},
a2: true,
},
{
id: "343188-343112",
type: "link",
id1: "343188",
id2: "343112",
d: {
count: 21,
},
a2: true,
},
{
id: "343188-343254",
type: "link",
id1: "343188",
id2: "343254",
d: {
count: 88,
},
a2: true,
},
{
id: "343188-343247",
type: "link",
id1: "343188",
id2: "343247",
d: {
count: 71,
},
a2: true,
},
{
id: "343188-343233",
type: "link",
id1: "343188",
id2: "343233",
d: {
count: 76,
},
a2: true,
},
{
id: "343188-343198",
type: "link",
id1: "343188",
id2: "343198",
d: {
count: 80,
},
a2: true,
},
{
id: "343188-343186",
type: "link",
id1: "343188",
id2: "343186",
d: {
count: 84,
},
a2: true,
},
{
id: "343188-343164",
type: "link",
id1: "343188",
id2: "343164",
d: {
count: 74,
},
a2: true,
},
{
id: "343188-343156",
type: "link",
id1: "343188",
id2: "343156",
d: {
count: 64,
},
a2: true,
},
{
id: "343188-343111",
type: "link",
id1: "343188",
id2: "343111",
d: {
count: 85,
},
a2: true,
},
{
id: "343119-343112",
type: "link",
id1: "343119",
id2: "343112",
d: {
count: 62,
},
a2: true,
},
{
id: "343112-343119",
type: "link",
id1: "343112",
id2: "343119",
d: {
count: 87,
},
a2: true,
},
{
id: "343112-343148",
type: "link",
id1: "343112",
id2: "343148",
d: {
count: 43,
},
a2: true,
},
{
id: "343114-343148",
type: "link",
id1: "343114",
id2: "343148",
d: {
count: 26,
},
a2: true,
},
{
id: "343119-343130",
type: "link",
id1: "343119",
id2: "343130",
d: {
count: 44,
},
a2: true,
},
{
id: "343119-343121",
type: "link",
id1: "343119",
id2: "343121",
d: {
count: 33,
},
a2: true,
},
{
id: "343112-343121",
type: "link",
id1: "343112",
id2: "343121",
d: {
count: 69,
},
a2: true,
},
{
id: "343112-343141",
type: "link",
id1: "343112",
id2: "343141",
d: {
count: 36,
},
a2: true,
},
{
id: "343141-343130",
type: "link",
id1: "343141",
id2: "343130",
d: {
count: 170,
},
a2: true,
},
{
id: "343141-343112",
type: "link",
id1: "343141",
id2: "343112",
d: {
count: 31,
},
a2: true,
},
{
id: "343203-343130",
type: "link",
id1: "343203",
id2: "343130",
d: {
count: 50,
},
a2: true,
},
{
id: "343112-343140",
type: "link",
id1: "343112",
id2: "343140",
d: {
count: 30,
},
a2: true,
},
{
id: "343112-343129",
type: "link",
id1: "343112",
id2: "343129",
d: {
count: 23,
},
a2: true,
// ...truncated 4062 lines <!doctype html>
<html lang="en" style="background-color: #2d383f">
<head>
<meta charset="utf-8" />
<title>Social Network Analysis</title>
<link rel="stylesheet" type="text/css" href="@ci/theme/kl/css/keylines.css" />
<link rel="stylesheet" type="text/css" href="@ci/theme/kl/css/minimalsdk.css" />
<link rel="stylesheet" type="text/css" href="@ci/theme/kl/css/sdk-layout.css" />
<link rel="stylesheet" type="text/css" href="@ci/theme/kl/css/demo.css" />
<link rel="stylesheet" type="text/css" href="font-awesome/css/font-awesome.css" />
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div id="klchart" class="klchart">
<div class="controloverlay">
<ul>
<li>
<a id="home" rel="tooltip" title="Home"><i class="fa fa-home"></i></a>
</li>
<li>
<a id="layout" rel="tooltip" title="Layout chart"><i class="fa fa-random"></i></a>
</li>
<li>
<a id="changeMode" rel="tooltip" title="Drag mode"
><i class="fa fa-arrows-alt" id="iconMode"></i
></a>
</li>
<li>
<a id="zoomIn" rel="tooltip" title="Zoom in"><i class="fa fa-plus-square"></i></a>
</li>
<li>
<a id="zoomOut" rel="tooltip" title="Zoom out"><i class="fa fa-minus-square"></i></a>
</li>
</ul>
</div>
</div>
<script type="module" src="./code.js"></script>
</body>
</html> #miniContainer {
background-color: #fff;
margin: 10px;
}
#miniContainer canvas {
margin: 0 auto;
}
#minikl {
height: 232px;
}
.controloverlay {
position: absolute;
left: 12px;
top: 10px;
padding: 0;
margin: 0;
font-size: 28px;
z-index: 9001;
background-color: rgba(250, 250, 250, 0.8);
border: solid 1px #ededed;
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1);
}
.controloverlay ul {
list-style-type: none;
margin: 0;
padding: 6px;
}
.controloverlay li {
margin-bottom: 3px;
text-align: center;
font-size: 24px;
}
.controloverlay li:last-child {
margin-bottom: 0;
}
.controloverlay a i {
color: #2d383f;
text-decoration: none;
cursor: pointer;
}
.btn-row {
overflow: auto;
display: flex;
justify-content: space-between;
gap: 5px;
}
.size-btn button.btn,
.size-btn button.btn.active {
min-width: 80px;
margin-top: 4px;
}
.btn-group.size-btn {
width: 100%;
display: flex;
justify-content: end;
flex-wrap: wrap;
gap: 5px;
}
#analysis-control {
margin-top: 10px;
width: 100%;
}
@media (max-width: 1023px) {
.btn-group button.size {
width: calc((100% / 2) + 2px);
}
}