This demo shows how donuts make it possible to visualise numeric proportions of data. This anonymised data analyses the email traffic of an organisation by department. Emails are sent by both external organisations and the departments themselves.
The donut on each node contains at least one colour-coded segment:
- each colour represents the department an email was sent to.
- each segment is sized based on the proportion of emails sent to each department.
Use the option on the right hand side to hide donuts and see department nodes more clearly. You can also filter out traffic that's of less interest, and only show nodes that send emails to multiple departments.
By listening for the 'pointer-move' event, you can show tooltips with the percentage of emails for each donut segment. Colour highlighting also makes clear which segment you’ve hovered over or tapped.
Donuts are useful in many scenarios, including:
- Social Network Analysis - show the relative time spent in different social media networks.
- Review Fraud - identify people who give mainly positive or negative reviews.
- Cyber Crime - demonstrate what proportion of malware attacks have infiltrated each web browser.
- Intelligence - display the relative number of telephone calls made to different countries.
Object format used:
Key functions used:
import KeyLines from "keylines";
import { data } from "./data.js";
// The radius of a node, in world coordinates, without enlargement
const BASE_NODE_RADIUS = 27;
// The distance the tooltip arrow protrudes from the tool tip box
const TOOLTIP_ARROW_SIZE = 11;
const colourToRoles = {
"#FF1529": "Admin", // red - admin
"#FF8027": "Sales", // orange - sales
"#CA62C4": "Other", // purple - other
"#44D161": "Ops", // green - ops
"#29A1E7": "Tech", // blue - tech
"#F781BF": "Marketing", // pink - marketing
"#CB623C": "Support", // brown - support
"#C4C4C4": "External", // grey - external
};
const highlightColours = {
"#D22432": "#FF1529", // red - admin
"#DD6800": "#FF8027", // orange - sales
"#984ea3": "#CA62C4", // purple - other
"#4daf4a": "#44D161", // green - ops
"#377eb8": "#29A1E7", // blue - tech
"#E77BB0": "#F781BF", // pink - marketing
"#a65628": "#CB623C", // brown - support
"#aaaaaa": "#C4C4C4", // grey - external
};
let chart;
let showDonuts = true;
let showNodesWithOneLink = true;
let layoutName = "structural";
let brightSegment = {
id: null,
donutId: null,
originalColour: null,
};
const interactionFormElement = document.getElementById("rhsForm");
/* HELPER FUNCTIONS FOR TOOLTIP BEGIN */
const tooltipElement = document.getElementById("tooltip");
// Determines which (45 degree rotated) quadrant a position lies within
function findQuadrant(item, x, y) {
// Calculate vector from item centre to position
const itemCentre = chart.viewCoordinates(item.x, item.y);
const toPositionCoords = {
x: x - itemCentre.x,
y: y - itemCentre.y,
};
// Determine quadrant using gradient. The dividing lines between quadrants have gradient +/-1
const gradient = Math.abs(toPositionCoords.y / toPositionCoords.x);
if (gradient <= 1 && toPositionCoords.x >= 0) return "right";
if (gradient <= 1 && toPositionCoords.x < 0) return "left";
if (gradient > 1 && toPositionCoords.y >= 0) return "bottom";
return "top";
}
// Calculates the radial distance between the node centre and the donut segment centre
function distanceToTooltip(node) {
return (BASE_NODE_RADIUS + node.donut.bw + node.donut.w / 2) * (node.e || 1);
}
// Returns the hover position shifted radially to the segment centre
function shiftToSegmentCentre(hoverX, hoverY, node) {
// Calculate vector from node centre to hover event position
const nodeCentre = chart.viewCoordinates(node.x, node.y);
const toHoverCoords = {
x: hoverX - nodeCentre.x,
y: hoverY - nodeCentre.y,
};
const initialLength = Math.sqrt(toHoverCoords.x ** 2 + toHoverCoords.y ** 2);
const scaleFactor = distanceToTooltip(node) / initialLength;
// The correction vector from the hover event position to the segment centre
const toCorrectedCoords = {
x: toHoverCoords.x * scaleFactor,
y: toHoverCoords.y * scaleFactor,
};
return chart.viewCoordinates(
node.x + toCorrectedCoords.x,
node.y + toCorrectedCoords.y,
);
}
// Offsets the position such that the tooltip arrow aligns correctly
function compensatePosition(tooltip, quadrant, position) {
const newPosition = {};
switch (quadrant) {
case "right":
newPosition.x = position.x + TOOLTIP_ARROW_SIZE;
newPosition.y = position.y - tooltip.clientHeight / 2;
break;
case "left":
newPosition.x = position.x - (tooltip.clientWidth + TOOLTIP_ARROW_SIZE);
newPosition.y = position.y - tooltip.clientHeight / 2;
break;
case "bottom":
newPosition.x = position.x - tooltip.clientWidth / 2;
newPosition.y = position.y + TOOLTIP_ARROW_SIZE;
break;
case "top":
newPosition.x = position.x - tooltip.clientWidth / 2;
newPosition.y = position.y - (tooltip.clientHeight + TOOLTIP_ARROW_SIZE);
break;
default:
break;
}
return newPosition;
}
// Populates the tooltip with hover information
function populateTooltip(label, percentage, direction) {
// Reset the tooltip class list and append the current direction
tooltipElement.className = "popover";
tooltipElement.classList.add(`${direction}`);
// Fill in label and percentage text
document.getElementById("tooltip-label").innerText = `${label}:`;
document.getElementById("tooltip-percentage").innerText = `${percentage}%`;
}
// Fills the tooltip with relevant details and subsequently positions it
function populateAndPositionTooltip(x, y, item, donutId) {
const total = item.donut.v.reduce((a, b) => a + b, 0);
const percentage = Math.round((item.donut.v[donutId] / total) * 100);
const quadrant = findQuadrant(item, x, y);
// Add label, percentage and quadrant information to tooltip HTML
populateTooltip(colourToRoles[item.donut.c[donutId]], percentage, quadrant);
// Get position of hover when snapped to segment centre
const segmentCentrePosition = shiftToSegmentCentre(x, y, item);
// Tweak position to ensure tooltip arrow points to segmentCentrePosition
const position = compensatePosition(
tooltipElement,
quadrant,
segmentCentrePosition,
);
// Update tooltip position with calculated values
tooltipElement.style.left = `${position.x}px`;
tooltipElement.style.top = `${position.y}px`;
}
// Hides the tooltip by setting the visibility to hidden
function closeTooltip() {
if (tooltipElement) tooltipElement.style.visibility = "hidden";
}
// Shows the tooltip by setting the visibility to visible
function openTooltip() {
if (tooltipElement) tooltipElement.style.visibility = "visible";
}
/* HELPER FUNCTIONS FOR TOOLTIP END */
// Performs a layout
async function doLayout() {
await chart.layout(layoutName);
}
// Reveals or hides donuts on all nodes
async function showHideDonuts() {
const updatedProperties = [];
chart.each({ type: "node" }, (node) => {
updatedProperties.push({
id: node.id,
donut: {
w: showDonuts ? node.d.w : 0,
bw: showDonuts ? 2 : 0,
},
});
});
await chart.animateProperties(updatedProperties, { time: 250 });
}
// Filters the nodes based on selected options
async function filterNodes() {
if (showNodesWithOneLink) {
// Reveal all nodes
await chart.filter(() => true);
} else {
// Filter out nodes with a degree less than or equal to 1
const degrees = chart.graph().degrees();
await chart.filter((node) => degrees[node.id] > 1, { type: "node" });
}
await doLayout();
}
// Replaces the colour of the desired donut segment with a highlight counterpart
function makeSegmentBrighter(item, donutId) {
const donut = item.donut;
const originalColour = donut.c[donutId];
brightSegment = { id: item.id, donutId, originalColour };
donut.c[donutId] = highlightColours[originalColour];
chart.setProperties({ id: item.id, donut });
}
// Reverts the previously brightened donut segment (if any)
function clearBrightening() {
const item = chart.getItem(brightSegment.id);
if (item) {
const donut = item.donut;
donut.c[brightSegment.donutId] = brightSegment.originalColour;
chart.setProperties({ id: brightSegment.id, donut });
}
}
// If the provided sub item is a donut segment, it is brightened and a tooltip is shown
function highlightSegmentAndShowTooltip({ id, x, y, subItem }) {
clearBrightening();
const item = chart.getItem(id);
if (item && subItem.type === "donut") {
makeSegmentBrighter(item, subItem.index);
populateAndPositionTooltip(x, y, item, subItem.index);
openTooltip();
} else {
closeTooltip();
}
}
// Enables or disables interaction with the chart controls
function disableInteraction(disable) {
interactionFormElement.style.pointerEvents = disable ? "none" : "auto";
}
// Toggles any number of classes on a given element
function toggleClasses(element, ...classes) {
classes.forEach((c) => element.classList.toggle(c));
}
// Inverts the active class name for all buttons in a parent container
function swapButtons(parentId) {
const btns = document.getElementById(parentId).getElementsByClassName("btn");
Array.from(btns).forEach((btn) => toggleClasses(btn, "active", "btn-kl"));
}
// Handler for donut visibility button group
async function onDonutInputChange(shouldShow) {
if (shouldShow !== showDonuts) {
disableInteraction(true);
showDonuts = !showDonuts;
swapButtons("donut-btns");
await showHideDonuts();
disableInteraction(false);
}
}
// Handler for single link visibility button group
async function onOneLinkInputChange(shouldShow) {
if (shouldShow !== showNodesWithOneLink) {
disableInteraction(true);
showNodesWithOneLink = !showNodesWithOneLink;
swapButtons("onelink-btns");
await filterNodes();
disableInteraction(false);
}
}
// Handler for layout selection button group
async function onLayoutInputChange(newLayout) {
if (newLayout !== layoutName) {
disableInteraction(true);
layoutName = newLayout;
swapButtons("layout-btns");
await doLayout();
disableInteraction(false);
}
}
function foregroundSelectedItems() {
const selection = chart.selection();
// If applicable, foreground the items neighbouring the selection
if (selection.length > 0) {
const nodesToForeground = chart
.graph()
.neighbours(selection)
.nodes.concat(selection);
chart.foreground((node) => nodesToForeground.includes(node.id));
} else {
chart.foreground(() => true);
}
}
function attachEventHandlers() {
// Attach event listeners for button pairs
document
.getElementById("btn-show-donuts")
.addEventListener("click", () => onDonutInputChange(true));
document
.getElementById("btn-hide-donuts")
.addEventListener("click", () => onDonutInputChange(false));
document
.getElementById("btn-show-onelink")
.addEventListener("click", () => onOneLinkInputChange(true));
document
.getElementById("btn-hide-onelink")
.addEventListener("click", () => onOneLinkInputChange(false));
document
.getElementById("btn-organic-layout")
.addEventListener("click", () => onLayoutInputChange("organic"));
document
.getElementById("btn-structural-layout")
.addEventListener("click", () => onLayoutInputChange("structural"));
// Attach handler to selection change event
chart.on("selection-change", foregroundSelectedItems);
// Attach handler to pointer-move, so when pointer is over a donut segment,
// we highlight and show a tooltip
chart.on("pointer-move", highlightSegmentAndShowTooltip);
// Close the tooltip to prevent it from pointing to the wrong position
chart.on("view-change", closeTooltip);
}
async function loadKeyLines() {
const options = {
logo: { u: "/public/images/Logo.png" },
iconFontFamily: "Font Awesome 5 Free",
handMode: true,
hover: 5, // Trigger the hover event with a 5ms delay
};
chart = await KeyLines.create({ container: "klchart", options });
chart.load(data);
doLayout();
attachEventHandlers();
}
function loadFontsAndStart() {
document.fonts.load('24px "Font Awesome 5 Free"').then(loadKeyLines);
}
window.addEventListener("DOMContentLoaded", loadFontsAndStart); export const data = {
type: "LinkChart",
items: [
{
type: "link",
id: "sales:xinhuanet.com",
id1: "sales",
id2: "xinhuanet.com",
},
{
type: "link",
id: "sales:usnews.com",
id1: "sales",
id2: "usnews.com",
},
{
type: "link",
id: "sales:tech",
id1: "sales",
id2: "tech",
},
{
type: "link",
id: "sales:slashdot.org",
id1: "sales",
id2: "slashdot.org",
},
{
type: "link",
id: "sales:youku.com",
id1: "sales",
id2: "youku.com",
},
{
type: "link",
id: "sales:shutterfly.com",
id1: "sales",
id2: "shutterfly.com",
},
{
type: "link",
id: "sales:support",
id1: "sales",
id2: "support",
},
{
type: "link",
id: "sales:seesaa.net",
id1: "sales",
id2: "seesaa.net",
},
{
type: "link",
id: "sales:slate.com",
id1: "sales",
id2: "slate.com",
},
{
type: "link",
id: "sales:umn.edu",
id1: "sales",
id2: "umn.edu",
},
{
type: "link",
id: "sales:valeroenergy.com",
id1: "sales",
id2: "valeroenergy.com",
},
{
type: "link",
id: "sales:squarespace.com",
id1: "sales",
id2: "squarespace.com",
},
{
type: "link",
id: "sales:whitehouse.gov",
id1: "sales",
id2: "whitehouse.gov",
},
{
type: "link",
id: "sales:xe.com",
id1: "sales",
id2: "xe.com",
},
{
type: "link",
id: "sales:verizoncommunications.com",
id1: "sales",
id2: "verizoncommunications.com",
},
{
type: "link",
id: "sales:symantec.com",
id1: "sales",
id2: "symantec.com",
},
{
type: "link",
id: "sales:time.com",
id1: "sales",
id2: "time.com",
},
{
type: "link",
id: "sales:walmartstores.com",
id1: "sales",
id2: "walmartstores.com",
},
{
type: "link",
id: "sales:salon.com",
id1: "sales",
id2: "salon.com",
},
{
type: "link",
id: "sales:timesonline.co.uk",
id1: "sales",
id2: "timesonline.co.uk",
},
{
type: "link",
id: "sales:wikia.com",
id1: "sales",
id2: "wikia.com",
},
{
type: "link",
id: "sales:ucsd.edu",
id1: "sales",
id2: "ucsd.edu",
},
{
type: "link",
id: "sales:sina.com.cn",
id1: "sales",
id2: "sina.com.cn",
},
{
type: "link",
id: "sales:teennick.com",
id1: "sales",
id2: "teennick.com",
},
{
type: "link",
id: "sales:zanox.com",
id1: "sales",
id2: "zanox.com",
},
{
type: "link",
id: "sales:thedailybeast.com",
id1: "sales",
id2: "thedailybeast.com",
},
{
type: "link",
id: "sales:unitedparcelservice.com",
id1: "sales",
id2: "unitedparcelservice.com",
},
{
type: "link",
id: "sales:toplist.cz",
id1: "sales",
id2: "toplist.cz",
},
{
type: "link",
id: "sales:sphinn.com",
id1: "sales",
id2: "sphinn.com",
},
{
type: "link",
id: "sales:senderbase.org",
id1: "sales",
id2: "senderbase.org",
},
{
type: "link",
id: "sales:webs.com",
id1: "sales",
id2: "webs.com",
},
{
type: "link",
id: "sales:ucla.edu",
id1: "sales",
id2: "ucla.edu",
},
{
type: "link",
id: "sales:soundcloud.com",
id1: "sales",
id2: "soundcloud.com",
},
{
type: "link",
id: "sales:singtel.com",
id1: "sales",
id2: "singtel.com",
},
{
type: "link",
id: "sales:yahoo.com.au",
id1: "sales",
id2: "yahoo.com.au",
},
{
type: "link",
id: "sales:umich.edu",
id1: "sales",
id2: "umich.edu",
},
{
type: "link",
id: "sales:stumbleupon.com",
id1: "sales",
id2: "stumbleupon.com",
},
{
type: "link",
id: "tech:ufl.edu",
id1: "tech",
id2: "ufl.edu",
},
{
type: "link",
id: "tech:zanox.com",
id1: "tech",
id2: "zanox.com",
},
{
type: "link",
id: "tech:virginaustralia.com",
id1: "tech",
id2: "virginaustralia.com",
},
{
type: "link",
id: "tech:techcrunch.com",
id1: "tech",
id2: "techcrunch.com",
},
{
type: "link",
id: "tech:yellowbook.com",
id1: "tech",
id2: "yellowbook.com",
},
{
type: "link",
id: "tech:toplist.cz",
id1: "tech",
id2: "toplist.cz",
},
{
type: "link",
id: "tech:youku.com",
id1: "tech",
id2: "youku.com",
},
{
type: "link",
id: "tech:unesco.org",
id1: "tech",
id2: "unesco.org",
},
{
type: "link",
id: "tech:vimeo.com",
id1: "tech",
id2: "vimeo.com",
},
{
type: "link",
id: "tech:thedailybeast.com",
id1: "tech",
id2: "thedailybeast.com",
},
{
type: "link",
id: "tech:time.com",
id1: "tech",
id2: "time.com",
},
{
type: "link",
id: "tech:unc.edu",
id1: "tech",
id2: "unc.edu",
},
{
type: "link",
id: "tech:umd.edu",
id1: "tech",
id2: "umd.edu",
},
{
type: "link",
id: "ops:zendesk.com",
id1: "ops",
id2: "zendesk.com",
},
{
type: "link",
id: "ops:sales",
id1: "ops",
id2: "sales",
},
{
type: "link",
id: "ops:tech",
id1: "ops",
id2: "tech",
},
{
type: "link",
id: "ops:other",
id1: "ops",
id2: "other",
},
{
type: "link",
id: "ops:support",
id1: "ops",
id2: "support",
},
{
type: "link",
id: "ops:youku.com",
id1: "ops",
id2: "youku.com",
},
{
type: "link",
id: "ops:zanox.com",
id1: "ops",
id2: "zanox.com",
},
{
type: "link",
id: "ops:time.com",
id1: "ops",
id2: "time.com",
},
{
type: "link",
id: "ops:senate.gov",
id1: "ops",
id2: "senate.gov",
},
{
type: "link",
id: "ops:senderbase.org",
id1: "ops",
id2: "senderbase.org",
},
{
type: "link",
id: "ops:squarespace.com",
id1: "ops",
id2: "squarespace.com",
},
{
type: "link",
id: "ops:qantas.com.au",
id1: "ops",
id2: "qantas.com.au",
},
{
type: "link",
id: "admin:tudou.com",
id1: "admin",
id2: "tudou.com",
},
{
type: "link",
id: "admin:nortelgroup.com",
id1: "admin",
id2: "nortelgroup.com",
},
{
type: "link",
id: "admin:sales",
id1: "admin",
id2: "sales",
},
{
type: "link",
id: "admin:nealsyarddairy.co.uk",
id1: "admin",
id2: "nealsyarddairy.co.uk",
},
{
type: "link",
id: "admin:tech",
id1: "admin",
id2: "tech",
},
{
type: "link",
id: "admin:xinhuanet.com",
id1: "admin",
id2: "xinhuanet.com",
},
{
type: "link",
id: "admin:sendmail.com",
id1: "admin",
id2: "sendmail.com",
},
{
type: "link",
id: "admin:ops",
id1: "admin",
id2: "ops",
},
{
type: "link",
id: "admin:reference.com",
id1: "admin",
id2: "reference.com",
},
{
type: "link",
id: "admin:slate.com",
id1: "admin",
id2: "slate.com",
},
{
type: "link",
id: "admin:globalsources.com",
id1: "admin",
id2: "globalsources.com",
},
{
type: "link",
id: "admin:blinklist.com",
id1: "admin",
id2: "blinklist.com",
},
{
type: "link",
id: "admin:dupont.com",
id1: "admin",
id2: "dupont.com",
},
{
type: "link",
id: "admin:gnu.org",
id1: "admin",
id2: "gnu.org",
},
{
type: "link",
id: "admin:seesaa.net",
id1: "admin",
id2: "seesaa.net",
},
{
type: "link",
id: "admin:support",
id1: "admin",
id2: "support",
},
{
type: "link",
id: "admin:furl.net",
id1: "admin",
id2: "furl.net",
},
{
type: "link",
id: "admin:youku.com",
id1: "admin",
id2: "youku.com",
},
{
type: "link",
id: "admin:marketing",
id1: "admin",
id2: "marketing",
},
{
type: "link",
id: "admin:ucsd.edu",
id1: "admin",
id2: "ucsd.edu",
},
{
type: "link",
id: "admin:indiatimes.com",
id1: "admin",
id2: "indiatimes.com",
},
{
type: "link",
id: "admin:senderbase.org",
id1: "admin",
id2: "senderbase.org",
},
{
type: "link",
id: "admin:archerdanielsmidland.com",
id1: "admin",
id2: "archerdanielsmidland.com",
},
{
type: "link",
id: "admin:t-online.de",
id1: "admin",
id2: "t-online.de",
},
{
type: "link",
id: "admin:comcast.com",
id1: "admin",
id2: "comcast.com",
},
{
type: "link",
id: "admin:businessweek.com",
id1: "admin",
id2: "businessweek.com",
},
{
type: "link",
id: "admin:other",
id1: "admin",
id2: "other",
},
{
type: "link",
id: "admin:metacafe.com",
id1: "admin",
id2: "metacafe.com",
},
{
type: "link",
id: "admin:nydailynews.com",
id1: "admin",
id2: "nydailynews.com",
},
{
type: "link",
id: "admin:xe.com",
id1: "admin",
id2: "xe.com",
},
{
type: "link",
id: "admin:goos.com",
id1: "admin",
id2: "goos.com",
},
{
type: "link",
id: "admin:google.com",
id1: "admin",
id2: "google.com",
},
{
type: "link",
id: "admin:intermedia.com.au",
id1: "admin",
id2: "intermedia.com.au",
},
{
type: "link",
id: "admin:toplist.cz",
id1: "admin",
id2: "toplist.cz",
},
{
type: "link",
id: "admin:oracle.com",
id1: "admin",
id2: "oracle.com",
},
{
type: "link",
id: "admin:dataflex.com.au",
id1: "admin",
id2: "dataflex.com.au",
},
{
type: "link",
id: "admin:honeywellinternational.com",
id1: "admin",
id2: "honeywellinternational.com",
},
{
type: "link",
id: "admin:networkadvertising.org",
id1: "admin",
id2: "networkadvertising.org",
},
{
type: "link",
id: "admin:shareasale.com",
id1: "admin",
id2: "shareasale.com",
},
{
type: "link",
id: "admin:google.cn",
id1: "admin",
id2: "google.cn",
},
{
type: "link",
id: "admin:timesonline.co.uk",
id1: "admin",
id2: "timesonline.co.uk",
},
{
type: "link",
id: "admin:dropbox.com",
id1: "admin",
id2: "dropbox.com",
},
{
type: "link",
id: "admin:bettybegin.com",
id1: "admin",
id2: "bettybegin.com",
},
{
type: "link",
id: "admin:printfriendly.com",
id1: "admin",
id2: "printfriendly.com",
},
{
type: "link",
id: "admin:formspring.me",
id1: "admin",
id2: "formspring.me",
},
{
type: "link",
id: "admin:valeroenergy.com",
id1: "admin",
id2: "valeroenergy.com",
},
{
type: "link",
id: "admin:multimap.com",
id1: "admin",
id2: "multimap.com",
},
{
type: "link",
id: "admin:twitpic.com",
id1: "admin",
id2: "twitpic.com",
},
{
type: "link",
id: "admin:hhs.gov",
id1: "admin",
id2: "hhs.gov",
},
{
type: "link",
id: "admin:cornell.edu",
id1: "admin",
id2: "cornell.edu",
},
{
type: "link",
id: "bing.com:ops",
id1: "bing.com",
id2: "ops",
},
{
type: "link",
id: "bing.com:tech",
id1: "bing.com",
id2: "tech",
},
{
type: "link",
id: "bing.com:sales",
id1: "bing.com",
id2: "sales",
},
{
type: "link",
id: "cardinalhealth.com:tech",
id1: "cardinalhealth.com",
id2: "tech",
},
{
type: "link",
id: "cardinalhealth.com:sales",
id1: "cardinalhealth.com",
id2: "sales",
},
{
type: "link",
id: "nba.com:tech",
id1: "nba.com",
id2: "tech",
},
{
type: "link",
id: "nba.com:sales",
id1: "nba.com",
id2: "sales",
},
{
type: "link",
id: "nortelgroup.com:ops",
id1: "nortelgroup.com",
id2: "ops",
},
{
type: "link",
id: "nortelgroup.com:tech",
id1: "nortelgroup.com",
id2: "tech",
},
{
type: "link",
id: "nortelgroup.com:sales",
id1: "nortelgroup.com",
id2: "sales",
},
{
type: "link",
id: "nortelgroup.com:support",
id1: "nortelgroup.com",
id2: "support",
},
{
type: "link",
id: "support:tech",
id1: "support",
id2: "tech",
},
{
type: "link",
id: "support:thedailybeast.com",
id1: "support",
id2: "thedailybeast.com",
},
{
type: "link",
id: "support:time.com",
id1: "support",
id2: "time.com",
},
{
type: "link",
id: "support:xe.com",
id1: "support",
id2: "xe.com",
},
{
type: "link",
id: "marketing:nortelgroup.com",
id1: "marketing",
id2: "nortelgroup.com",
},
{
type: "link",
id: "marketing:tudou.com",
id1: "marketing",
id2: "tudou.com",
},
{
type: "link",
id: "marketing:people.com.cn",
id1: "marketing",
id2: "people.com.cn",
},
{
type: "link",
id: "marketing:sales",
id1: "marketing",
id2: "sales",
},
{
type: "link",
id: "marketing:tech",
id1: "marketing",
id2: "tech",
},
{
type: "link",
id: "marketing:rakuten.co.jp",
id1: "marketing",
id2: "rakuten.co.jp",
},
{
type: "link",
id: "marketing:ops",
id1: "marketing",
id2: "ops",
},
{
type: "link",
id: "marketing:state.gov",
id1: "marketing",
id2: "state.gov",
},
{
type: "link",
id: "marketing:senate.gov",
id1: "marketing",
id2: "senate.gov",
},
{
type: "link",
id: "marketing:other",
id1: "marketing",
id2: "other",
},
{
type: "link",
id: "marketing:salon.com",
id1: "marketing",
id2: "salon.com",
},
{
type: "link",
id: "marketing:servcorp.com",
id1: "marketing",
id2: "servcorp.com",
},
{
type: "link",
id: "marketing:t-online.de",
id1: "marketing",
id2: "t-online.de",
},
{
type: "link",
id: "marketing:printfriendly.com",
id1: "marketing",
id2: "printfriendly.com",
},
{
type: "link",
id: "marketing:redherring.com",
id1: "marketing",
id2: "redherring.com",
},
{
type: "link",
id: "marketing:support",
id1: "marketing",
id2: "support",
},
{
type: "link",
id: "marketing:prweb.com",
id1: "marketing",
id2: "prweb.com",
},
{
type: "link",
id: "marketing:youku.com",
id1: "marketing",
id2: "youku.com",
},
{
type: "link",
id: "marketing:roytanck.com",
id1: "marketing",
id2: "roytanck.com",
},
{
type: "link",
id: "marketing:salesforce.com",
id1: "marketing",
id2: "salesforce.com",
},
{
type: "link",
id: "marketing:mixx.com",
id1: "marketing",
id2: "mixx.com",
},
{
type: "link",
id: "marketing:sciencedaily.com",
id1: "marketing",
id2: "sciencedaily.com",
},
{
type: "link",
id: "marketing:yolasite.com",
id1: "marketing",
id2: "yolasite.com",
},
{
type: "link",
id: "marketing:wordpress.com",
id1: "marketing",
id2: "wordpress.com",
},
{
type: "link",
id: "marketing:verizoncommunications.com",
id1: "marketing",
id2: "verizoncommunications.com",
},
{
type: "link",
id: "marketing:metacafe.com",
id1: "marketing",
id2: "metacafe.com",
},
{
type: "link",
id: "marketing:unc.edu",
id1: "marketing",
id2: "unc.edu",
},
{
type: "link",
id: "marketing:westpac.com.au",
id1: "marketing",
id2: "westpac.com.au",
},
{
type: "link",
id: "marketing:microsoft.com",
id1: "marketing",
id2: "microsoft.com",
},
{
type: "link",
id: "marketing:nokia.com",
id1: "marketing",
id2: "nokia.com",
},
{
type: "link",
id: "marketing:senderbase.org",
id1: "marketing",
id2: "senderbase.org",
},
{
type: "link",
id: "cjb.net:tech",
id1: "cjb.net",
id2: "tech",
},
{
type: "link",
id: "cjb.net:sales",
id1: "cjb.net",
id2: "sales",
},
{
type: "link",
id: "csmonitor.com:sales",
id1: "csmonitor.com",
id2: "sales",
},
{
type: "link",
id: "csmonitor.com:marketing",
id1: "csmonitor.com",
id2: "marketing",
},
{
type: "link",
id: "geocities.com:marketing",
id1: "geocities.com",
id2: "marketing",
},
{
type: "link",
id: "globalsources.com:tech",
id1: "globalsources.com",
id2: "tech",
},
{
type: "link",
id: "globalsources.com:sales",
id1: "globalsources.com",
id2: "sales",
},
{
type: "link",
id: "globalsources.com:support",
id1: "globalsources.com",
id2: "support",
},
{
// ...truncated 4520 lines <!doctype html>
<html lang="en" style="background-color: #2d383f">
<head>
<meta charset="utf-8" />
<title>Donuts and Email Traffic</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="@fortawesome/[email protected]/css/fontawesome.css"
/>
<link
rel="stylesheet"
type="text/css"
href="@fortawesome/[email protected]/css/solid.css"
/>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div id="klchart" class="klchart"></div>
<script type="module" src="./code.js"></script>
</body>
</html> .klchart {
overflow: hidden;
}
.popover {
display: block;
position: absolute;
min-width: 50px;
margin: 0;
pointer-events: none;
visibility: hidden;
background-color: #fff;
border: 1px solid #ccc;
}
.popover-content {
z-index: 1;
}