diff --git a/src/css/style.css b/src/css/style.css index bc7caa9..2646b85 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -1,656 +1,676 @@ - /* ----------- VISUALIZATION ----------- */ .links line { - stroke: #999; - stroke-opacity: 0.6; + stroke: #999; + stroke-opacity: 0.6; } .nodes circle { - stroke: #fff; - stroke-width: 1.5px; + stroke: #fff; + stroke-width: 1.5px; } + /* ----------- UI - General ----------- */ @font-face { - font-family: "Open Sans"; - src: url("../fonts/OpenSans-Regular.ttf"); + font-family: "Open Sans"; + src: url("../fonts/OpenSans-Regular.ttf"); } [hidden] { - display: none !important; + display: none !important; } :root { - --primary-color: #000; - --secondary-color: #404850; - --tertiary-color: #eaeaea; - --button-color: #171e25; - --button-border-color: #12181b; - --button-active-color: #73a4b8; - --button-active-border-color: #6fc3e5; - --primary-text-color: #eaeaea; - --secondary-text-color: #73a4b8; - --dialog-header-color: #4cc7e6; - --dialog-button-color: #4cc7e6; + --primary-color: #000; + --secondary-color: #404850; + --tertiary-color: #eaeaea; + --button-color: #171e25; + --button-border-color: #12181b; + --button-active-color: #73a4b8; + --button-active-border-color: #6fc3e5; + --primary-text-color: #eaeaea; + --secondary-text-color: #73a4b8; + --dialog-header-color: #4cc7e6; + --dialog-button-color: #4cc7e6; } -*::before, *::after, * { - box-sizing: border-box; +*::before, +*::after, +* { + box-sizing: border-box; } body { - display: grid; - grid-template-columns: 170px 1fr; - height: 100vh; - width: 100%; - margin: 0; - font-family: "Open Sans", sans-serif; - font-weight: normal; + display: grid; + grid-template-columns: 170px 1fr; + height: 100vh; + width: 100%; + margin: 0; + font-family: "Open Sans", sans-serif; + font-weight: normal; } -h1, h2, h3, dt, dd, label { - margin: 0; - font-weight: 400; +h1, +h2, +h3, +dt, +dd, +label { + margin: 0; + font-weight: 400; } h1 { - font-size: 1.75em; + font-size: 1.75em; } -h2, dt, label { - font-size: .75em; - font-weight: 700; - text-transform: uppercase; +h2, +dt, +label { + font-size: .75em; + font-weight: 700; + text-transform: uppercase; } -h3, dd { - font-size: 1.2em; - color: var(--secondary-text-color); - text-transform: uppercase; +h3, +dd { + font-size: 1.2em; + color: var(--secondary-text-color); + text-transform: uppercase; } var { - font-style: normal; + font-style: normal; } button { - display: flex; - align-items: center; - justify-content: flex-start; - width: 100%; - margin-top: 10px; - text-align: left; - padding: 10px; - font-size: .75em; - border: none; - border-width: 1px; - border-radius: 3px; - cursor: pointer; - color: var(--primary-text-color); - background-color: var(--button-color); - border-color: var(--button-border-color); - box-shadow: 0 3px 3px #12181B; + display: flex; + align-items: center; + justify-content: flex-start; + width: 100%; + margin-top: 10px; + text-align: left; + padding: 10px; + font-size: .75em; + border: none; + border-width: 1px; + border-radius: 3px; + cursor: pointer; + color: var(--primary-text-color); + background-color: var(--button-color); + border-color: var(--button-border-color); + box-shadow: 0 3px 3px #12181B; } button:hover { - background-color: var(--button-active-color); + background-color: var(--button-active-color); } -button::before, a.thumbs-up::before { - content: ""; - display: inline-block; - background-repeat: no-repeat; - background-position: center; - width: 15px; - height: 15px; - background-size: contain; - margin-right: 10px; +button::before, +a.thumbs-up::before { + content: ""; + display: inline-block; + background-repeat: no-repeat; + background-position: center; + width: 15px; + height: 15px; + background-size: contain; + margin-right: 10px; } dialog { - display: block; - position: fixed; - transform: translate(0, -50%); - top: 50%; - left: 2em; - right: 2em; - width: -moz-fit-content; - width: -webkit-fit-content; - width: fit-content; - max-width: 40em; - height: -moz-fit-content; - height: -webkit-fit-content; - height: fit-content; - margin: auto; - padding: 1em; - background: white; - color: black; - border: none; - border-radius: 0.5em; - box-shadow: 0.5em 0.5em 0 rgba(0,0,0,0.5); - overflow: hidden; + display: block; + position: fixed; + transform: translate(0, -50%); + top: 50%; + left: 2em; + right: 2em; + width: -moz-fit-content; + width: -webkit-fit-content; + width: fit-content; + max-width: 40em; + height: -moz-fit-content; + height: -webkit-fit-content; + height: fit-content; + margin: auto; + padding: 1em; + background: white; + color: black; + border: none; + border-radius: 0.5em; + box-shadow: 0.5em 0.5em 0 rgba(0, 0, 0, 0.5); + overflow: hidden; } dialog:not([open]) { - display: none; + display: none; } -dialog::backdrop, dialog + .backdrop { - background: rgba(0,0,0,0.25); +dialog::backdrop, +dialog+.backdrop { + background: rgba(0, 0, 0, 0.25); } -dialog + .backdrop { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; +dialog+.backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; } dialog .dialog-wrapper { - display: grid; - grid-template-columns: repeat(4, 1fr); - grid-gap: 10px; - grid-auto-rows: auto; + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-gap: 10px; + grid-auto-rows: auto; } dialog .dialog-title { - background: var(--dialog-header-color); - color: #FFF; - margin: -1em -1em 0; - padding: 1em; - font-size: 1em; - text-transform: uppercase; - grid-column: 1 / 5; - grid-row: 1; + background: var(--dialog-header-color); + color: #FFF; + margin: -1em -1em 0; + padding: 1em; + font-size: 1em; + text-transform: uppercase; + grid-column: 1 / 5; + grid-row: 1; } dialog .dialog-icon { - grid-column: 1; - grid-row: 2; - width: 100%; - display: block; + grid-column: 1; + grid-row: 2; + width: 100%; + display: block; } dialog .dialog-content { - grid-column: 1 / 5; - grid-row: 2; + grid-column: 1 / 5; + grid-row: 2; } -dialog .dialog-icon + .dialog-content { - grid-column: 2 / 5; +dialog .dialog-icon+.dialog-content { + grid-column: 2 / 5; } dialog .dialog-options { - text-align: right; - display: block; - margin: 0; - padding: 0; - grid-column: 1 / 5; - grid-row: 3; + text-align: right; + display: block; + margin: 0; + padding: 0; + grid-column: 1 / 5; + grid-row: 3; } dialog .dialog-options button { - background: var(--dialog-button-color); - display: inline-block; - width: auto; - min-width: 6em; - box-shadow: none; - margin: 0; - padding: 0.5em 1em; - font-size: inherit; - text-align: center; + background: var(--dialog-button-color); + display: inline-block; + width: auto; + min-width: 6em; + box-shadow: none; + margin: 0; + padding: 0.5em 1em; + font-size: inherit; + text-align: center; } dialog .dialog-options button::before { - display: none; + display: none; } dialog .dialog-options button::-moz-focus-inner { - padding: 0; - border: none; + padding: 0; + border: none; } dialog .dialog-options button:focus { - outline: dotted 1px grey; + outline: dotted 1px grey; } @media (max-width: 44em) { - dialog .dialog-icon { - display: none; - } - - dialog .dialog-content, - dialog .dialog-icon + .dialog-content { - grid-column: 1 / 5; - } + dialog .dialog-icon { + display: none; + } + dialog .dialog-content, + dialog .dialog-icon+.dialog-content { + grid-column: 1 / 5; + } } ._dialog_overlay { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; } .active { - background-color: var(--button-active-color); - border-top: 2px solid var(--button-active-border-color); + background-color: var(--button-active-color); + border-top: 2px solid var(--button-active-border-color); } .active:hover { - border-top-color: var(--tertiary-color); + border-top-color: var(--tertiary-color); } a { - color: var(--secondary-text-color); - font-size: .75em; + color: var(--secondary-text-color); + font-size: .75em; } .filter-menu button:first-child { - margin-top: 10px; - border-radius: 3px 3px 0 0; + margin-top: 10px; + border-radius: 3px 3px 0 0; } .filter-menu button:last-child { - border-radius: 0 0 3px 3px; + border-radius: 0 0 3px 3px; } .filter-menu button { - margin-top: 0; - border-radius: 0; - border-top: 2px solid var(--button-border-color); + margin-top: 0; + border-radius: 0; + border-top: 2px solid var(--button-border-color); } .filter-menu button:hover { - background-color: var(--button-active-color); + background-color: var(--button-active-color); } .filter-menu .active { - border-color: var(--button-active-border-color); + border-color: var(--button-active-border-color); } .filter-menu .active:hover { - border-color: #fff; + border-color: #fff; } + /* ----------- UI - Side Bar ----------- */ aside { - background-color: var(--secondary-color); - padding: 15px; + background-color: var(--secondary-color); + padding: 15px; } aside h1 { - color: var(--tertiary-color); + color: var(--tertiary-color); } .logo { - max-width: 100%; + max-width: 100%; } aside h2 { - margin-top: 20px; - color: var(--primary-text-color); + margin-top: 20px; + color: var(--primary-text-color); } .nav-links { - margin-top: 20px; - text-align: center; + margin-top: 20px; + text-align: center; } .nav-links .icon { - margin-right: 0; + margin-right: 0; } .graph::before { - background-image: url("../images/lightbeam_icon_graph.png"); + background-image: url("../images/lightbeam_icon_graph.png"); } .list:before { - background-image: url("../images/lightbeam_icon_list.png"); + background-image: url("../images/lightbeam_icon_list.png"); } .download::before { - background-image: url("../images/lightbeam_icon_download.png"); + background-image: url("../images/lightbeam_icon_download.png"); } .reset::before { - background-image: url("../images/lightbeam_icon_reset.png"); + background-image: url("../images/lightbeam_icon_reset.png"); } .thumbs-up::before { - background-image: url("../images/lightbeam_icon_feedback.png"); + background-image: url("../images/lightbeam_icon_feedback.png"); } + /* ----------- UI - Top Bar ----------- */ main { - display: grid; - grid-template-rows: 10% 60% 30%; - background-color: var(--primary-color); - height: 100%; + display: grid; + grid-template-rows: 10% 60% 30%; + background-color: var(--primary-color); + height: 100%; } .top-bar { - display: flex; - justify-content: space-between; - align-items: center; - background-color: var(--tertiary-color); - padding: 20px; + display: flex; + justify-content: space-between; + align-items: center; + background-color: var(--tertiary-color); + padding: 10px; + margin-top: auto; + margin-bottom: auto; } -.top-bar > dl { - display: flex; - flex-wrap: wrap; +.top-bar>dl { + display: flex; + flex-wrap: wrap; } -.top-bar > dl > div { - margin-right: 15px; +.top-bar>dl>div { + margin-right: 15px; } .tracking-protection { - display: flex; - align-items: center; - flex-wrap: wrap; + display: flex; + align-items: center; + flex-wrap: wrap; } -#tracking-protection-disabled > div { - display: inline-block; - width: 140px; - margin-inline-start: 10px; +#tracking-protection-disabled>div { + display: inline-block; + width: 140px; + margin-inline-start: 10px; } -#tracking-protection-disabled > div > a { - font-size: 1rem; - text-decoration: none; +#tracking-protection-disabled>div>a { + font-size: 1rem; + text-decoration: none; } .toggle-switch { - display: inline-flex; - align-items: center; - justify-content: space-around; - height: 30px; - width: 55px; - margin-left: 5px; - background-color: var(--secondary-color); - border-radius: 5px; - position: relative; - cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: space-around; + height: 30px; + width: 55px; + margin-left: 5px; + background-color: var(--secondary-color); + border-radius: 5px; + position: relative; + cursor: pointer; } .toggle-switch input { - -moz-appearance: none; - opacity: 0; + -moz-appearance: none; + opacity: 0; } .toggle-switch:active { - border:1px solid var(--button-active-border-color); + border: 1px solid var(--button-active-border-color); } .toggle:before { - content: ""; - height: 24px; - width: 25px; - background-color: var(--primary-color); - border-radius: 5px; - transition: all 0.4s; - position: absolute; - left: 5px; - top: 10%; + content: ""; + height: 24px; + width: 25px; + background-color: var(--primary-color); + border-radius: 5px; + transition: all 0.4s; + position: absolute; + left: 5px; + top: 10%; } .toggle:after { - content: "OFF"; - font-size: .75em; - font-weight: 700; - text-transform: uppercase; - color: var(--primary-text-color); - position: absolute; - right: 10%; - top: 30%; + content: "OFF"; + font-size: .75em; + font-weight: 700; + text-transform: uppercase; + color: var(--primary-text-color); + position: absolute; + right: 10%; + top: 30%; } -.toggle-switch input:checked + .toggle:before { - transform: translateX(20px); - right: 10%; - background-color: var(--secondary-text-color); +.toggle-switch input:checked+.toggle:before { + transform: translateX(20px); + right: 10%; + background-color: var(--secondary-text-color); } -.toggle-switch input:checked + .toggle:after { - content: "ON"; - left: 10%; +.toggle-switch input:checked+.toggle:after { + content: "ON"; + left: 10%; } + /* ----------- UI - Graph ----------- */ .vis { - display: grid; - grid-template-rows: 60px 1fr; - grid-template-columns: 1fr 40px; + display: grid; + grid-template-rows: 60px 1fr; + grid-template-columns: 1fr 40px; + position: static; + /* margin-top:30px; */ } .vis-header { - padding-left: 20px; - padding-top: 10px; - background-color: var(--primary-color); + padding-left: 20px; + padding-top: 10px; + background-color: var(--primary-color); + position: static; + /* margin-top : 70px; */ } -.vis-header > h1 { - color: var(--primary-text-color); +.vis-header>h1 { + color: var(--primary-text-color); } -.vis-header > h2 { - color: var(--secondary-text-color); +.vis-header>h2 { + color: var(--secondary-text-color); } .vis-content { - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - overflow: hidden; + position: static; + top: 0; + bottom: 0; + left: 0; + right: 0; + overflow: hidden; } #tooltip { - position: absolute; - z-index: 1; - display: none; - background-color: #FFF; - color: #010203; - padding: 5px 10px; - box-shadow: 0px 2px #4CC7E6; - border-radius: 5px; + position: absolute; + z-index: 1; + display: none; + background-color: #FFF; + color: #010203; + padding: 5px 10px; + box-shadow: 0px 2px #4CC7E6; + border-radius: 5px; } #tooltip::after { - content: ''; - width: 0; - height: 0; - border: 10px solid transparent; - border-top: 10px solid #FFF; - position: absolute; - top: 100%; - left: 50%; - margin-left: -10px; + content: ''; + width: 0; + height: 0; + border: 10px solid transparent; + border-top: 10px solid #FFF; + position: absolute; + top: 100%; + left: 50%; + margin-left: -10px; } .vis-controls { - grid-row-end: span 3; + grid-row-end: span 3; } -.info-panel-controls { - -} +.info-panel-controls {} .info-panel { - border-radius: 3px 0 0 3px; - margin-top: 5px; + border-radius: 3px 0 0 3px; + margin-top: 5px; } .info-panel::before { - margin-right: 0; + margin-right: 0; } .info-panel:first-child { - background-color: transparent; - box-shadow: none; - margin-top: 0; + background-color: transparent; + box-shadow: none; + margin-top: 0; } .website::before { - background-image: url('../images/lightbeam_icon_website.png'); - opacity: .5; + background-image: url('../images/lightbeam_icon_website.png'); + opacity: .5; } .help::before { - background-image: url('../images/lightbeam_icon_help.png'); + background-image: url('../images/lightbeam_icon_help.png'); } .about::before { - background-image: url('../images/lightbeam_icon_about.png'); + background-image: url('../images/lightbeam_icon_about.png'); } + /* ----------- UI - vis Controls ----------- */ + footer { - display: grid; - grid-template-columns: repeat(4, 1fr) minmax(150px, auto); - padding: 0 30px; + display: grid; + grid-template-columns: repeat(4, 1fr) minmax(150px, auto); + padding: 0 30px; } .footer-heading { - display: flex; - border-bottom: 1px solid var(--primary-text-color); - justify-content: space-between; + display: flex; + border-bottom: 1px solid var(--primary-text-color); + justify-content: space-between; } .footer-heading h2 { - padding-bottom: 5px; - color: var(--primary-text-color); + padding-bottom: 5px; + color: var(--primary-text-color); } .footer-heading .hide { - text-transform: none; - font-weight: 400; + text-transform: none; + font-weight: 400; } .footer-toggle { - grid-column: 1 / 5; + grid-column: 1 / 5; } .footer-filter { - grid-column-start: 5; + grid-column-start: 5; } .footer-toggle-buttons { - display: grid; - grid-column-gap: 10px; - grid-template-columns: repeat(3, minmax(auto, 150px)); - grid-template-rows: repeat(3, 1fr); + display: grid; + grid-column-gap: 10px; + grid-template-columns: repeat(3, minmax(auto, 150px)); + grid-template-rows: repeat(3, 1fr); } .footer-toggle-buttons .connections { - grid-column-start: 1; + grid-column-start: 1; } .visited-sites::before { - background-image: url('data:image/svg+xml,'); + background-image: url('data:image/svg+xml,'); } .third-party-sites::before { - background-image: url('data:image/svg+xml,'); + background-image: url('data:image/svg+xml,'); } .connections::before { - background-image: url('data:image/svg+xml,'); + background-image: url('data:image/svg+xml,'); } .two-icons::before { - width: 30px; + width: 30px; } .watched-sites::before { - background-image: url('data:image/svg+xml,'), url('data:image/svg+xml,'); + background-image: url('data:image/svg+xml,'), url('data:image/svg+xml,'); } .blocked-sites::before { - background-image: url('data:image/svg+xml,'), url('data:image/svg+xml,'); + background-image: url('data:image/svg+xml,'), url('data:image/svg+xml,'); } .cookies::before { - background-image: url('data:image/svg+xml,'); + background-image: url('data:image/svg+xml,'); } .filter-menu { - max-width: 150px; + max-width: 150px; } @media (max-width: 800px) { - footer { - grid-template-columns: 1fr 1fr; - } - - .footer-toggle { - grid-column: 1 / 1; - } - - .footer-filter { - grid-column-start: 2; - } - - .footer-toggle-buttons { - grid-template-columns: minmax(auto, 150px); - } - - .watched-sites { - order: 4; - } - - .blocked-sites { - order: 5; - } - - .cookies { - order: 6; - } + .vis-header { + position: static; + margin-top: 72px; + } + footer { + grid-template-columns: 1fr 1fr; + } + .footer-toggle { + grid-column: 1 / 1; + } + .footer-filter { + grid-column-start: 2; + } + .footer-toggle-buttons { + grid-template-columns: minmax(auto, 150px); + } + .watched-sites { + order: 4; + } + .blocked-sites { + order: 5; + } + .cookies { + order: 6; + } +} + +@media (min-width: 800px) { + .vis-header { + margin-top: 70px; + } } @media (max-width: 600px) { - main { - display: block; - } - - .vis { - height: 600px; - } - - footer { - grid-template-columns: 1fr; - } - - .footer-toggle { - grid-column: 1; - } - - .footer-filter { - grid-column-start: 1; - } + main { + display: block; + } + .vis { + height: 600px; + } + footer { + grid-template-columns: 1fr; + } + .footer-toggle { + grid-column: 1; + } + .footer-filter { + grid-column-start: 1; + } } .unimplemented { - visibility: hidden; + visibility: hidden; } .unimplemented.list, footer.unimplemented { - display: none; + display: none; } .max-graph { - position: relative; - z-index: 2; -} + position: static; + z-index: 2; +} \ No newline at end of file diff --git a/src/js/viz.js b/src/js/viz.js index 5139a64..59939be 100644 --- a/src/js/viz.js +++ b/src/js/viz.js @@ -1,465 +1,465 @@ // eslint-disable-next-line no-unused-vars const viz = { - scalingFactor: 2, - circleRadius: 5, - resizeTimer: null, - minZoom: 0.5, - maxZoom: 1.5, - collisionRadius: 10, - chargeStrength: -100, - tickCount: 100, - canvasColor: 'white', - alphaStart: 1, - alphaTargetStart: 0.1, - alphaTargetStop: 0, - - init(nodes, links) { - const { width, height } = this.getDimensions(); - const { canvas, context } = this.createCanvas(); - - this.canvas = canvas; - this.context = context; - this.tooltip = document.getElementById('tooltip'); - this.circleRadius = this.circleRadius * this.scalingFactor; - this.collisionRadius = this.collisionRadius * this.scalingFactor; - this.scale = (window.devicePixelRatio || 1) * this.scalingFactor; - this.transform = d3.zoomIdentity; - this.defaultIcon = this.loadImage('images/defaultFavicon.svg'); - - this.updateCanvas(width, height); - this.draw(nodes, links); - this.addListeners(); - }, - - draw(nodes, links) { - this.nodes = nodes; - this.links = links; - - this.simulateForce(); - this.drawOnCanvas(); - }, - - simulateForce() { - if (!this.simulation) { - this.simulation = d3.forceSimulation(this.nodes); - this.simulation.on('tick', () => { - return this.drawOnCanvas(); - }); - this.registerSimulationForces(); - } else { - this.simulation.nodes(this.nodes); - this.resetAlpha(); - } - this.registerLinkForce(); - }, - - resetAlpha() { - const alpha = this.simulation.alpha(); - const alphaRounded = Math.round((1 - alpha) * 100); - if (alphaRounded === 100) { - this.simulation.alpha(this.alphaStart); - this.restartSimulation(); - } - }, - - resetAlphaTarget() { - this.simulation.alphaTarget(this.alphaTargetStart); - this.restartSimulation(); - }, - - stopAlphaTarget() { - this.simulation.alphaTarget(this.alphaTargetStop); - }, - - restartSimulation() { - this.simulation.restart(); - }, - - registerLinkForce() { - const linkForce = d3.forceLink(this.links); - linkForce.id((d) => { - return d.hostname; - }); - this.simulation.force('link', linkForce); - }, - - registerSimulationForces() { - const centerForce = d3.forceCenter(this.width / 2, this.height / 2); - this.simulation.force('center', centerForce); - - const forceX = d3.forceX(this.width / 2); - this.simulation.force('x', forceX); - - const forceY = d3.forceY(this.height / 2); - this.simulation.force('y', forceY); - - const chargeForce = d3.forceManyBody(); - chargeForce.strength(this.chargeStrength); - this.simulation.force('charge', chargeForce); - - const collisionForce = d3.forceCollide(this.collisionRadius); - this.simulation.force('collide', collisionForce); - }, - - createCanvas() { - const base = document.getElementById('visualization'); - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - - base.appendChild(canvas); - - return { - canvas, - context - }; - }, - - updateCanvas(width, height) { - this.width = width; - this.height = height; - this.canvas.setAttribute('width', width * this.scale); - this.canvas.setAttribute('height', height * this.scale); - this.canvas.style.width = `${width}px`; - this.canvas.style.height = `${height}px`; - this.context.scale(this.scale, this.scale); - }, - - getDimensions() { - const element = document.body; - const { width, height } = element.getBoundingClientRect(); - - return { - width, - height - }; - }, - - drawOnCanvas() { - this.context.clearRect(0, 0, this.width, this.height); - this.context.save(); - this.context.translate(this.transform.x, this.transform.y); - this.context.scale(this.transform.k, this.transform.k); - this.drawLinks(); - this.drawNodes(); - this.context.restore(); - }, - - getRadius(thirdPartyLength) { - if (thirdPartyLength > 0) { - if (thirdPartyLength > this.collisionRadius) { - return this.circleRadius + this.collisionRadius; - } else { - return this.circleRadius + thirdPartyLength; - } - } - return this.circleRadius; - }, - - drawNodes() { - for (const node of this.nodes) { - const x = node.fx || node.x; - const y = node.fy || node.y; - let radius; - - this.context.beginPath(); - this.context.moveTo(x, y); - - if (node.firstParty) { - radius = this.getRadius(node.thirdParties.length); - this.drawFirstParty(x, y, radius); - } else { - this.drawThirdParty(x, y); - } - - if (node.shadow) { - this.drawShadow(x, y, radius); - } - - this.context.fillStyle = this.canvasColor; - this.context.closePath(); - this.context.fill(); - - if (node.favicon) { - this.drawFavicon(node, x, y, radius); - } else { - this.drawFavicon(node, x, y, this.circleRadius); - } - } - }, - - getSquare(radius) { - const side = Math.sqrt(radius * radius * 2); - const offset = side * 0.5; - - return { - side, - offset - }; - }, - - loadImage(URI) { - return new Promise((resolve, reject) => { - if (!URI) { - return reject(); - } - - const image = new Image(); - - image.onload = () => { - return resolve(image); - }; - image.onerror = () => { - return resolve(this.defaultIcon); - }; - image.src = URI; - }); - }, - - scaleFavicon(image, side) { - const canvas = document.createElement('canvas'), - context = canvas.getContext('2d'); - - canvas.width = side * this.scale; - canvas.height = side * this.scale; - context.fillStyle = this.canvasColor; - context.fillRect(0, 0, canvas.width, canvas.height); - - context.drawImage( - image, - 0, - 0, - side * this.scale, - side * this.scale); - - return context.getImageData(0, 0, canvas.width, canvas.height); - }, - - async drawFavicon(node, x, y, radius) { - const offset = this.getSquare(radius).offset, - side = this.getSquare(radius).side, - tx = this.transform.applyX(x) - offset, - ty = this.transform.applyY(y) - offset; - - if (!node.image) { - node.image = await this.loadImage(node.favicon); - } - - this.context.putImageData( - this.scaleFavicon(node.image, side), - tx * this.scale, - ty * this.scale - ); - }, - - drawShadow(x, y, radius) { - const lineWidth = 2, - shadowBlur = 15, - shadowRadius = 5; - this.context.beginPath(); - this.context.lineWidth = lineWidth; - this.context.shadowColor = this.canvasColor; - this.context.strokeStyle = 'rgba(0, 0, 0, 1)'; - this.context.shadowBlur = shadowBlur; - this.context.shadowOffsetX = 0; - this.context.shadowOffsetY = 0; - this.context.arc(x, y, radius + shadowRadius, 0, 2 * Math.PI); - this.context.stroke(); - this.context.closePath(); - }, - - drawFirstParty(x, y, radius) { - this.context.arc(x, y, radius, 0, 2 * Math.PI); - }, - - drawThirdParty(x, y) { - const deltaY = this.circleRadius / 2; - const deltaX = deltaY * Math.sqrt(3); - - this.context.moveTo(x - deltaX, y + deltaY); - this.context.lineTo(x, y - this.circleRadius); - this.context.lineTo(x + deltaX, y + deltaY); - }, - - getTooltipPosition(x, y) { - const tooltipArrowHeight = 20; - const { right: canvasRight } = this.canvas.getBoundingClientRect(); - const { - height: tooltipHeight, - width: tooltipWidth - } = this.tooltip.getBoundingClientRect(); - const top = y - tooltipHeight - this.circleRadius - tooltipArrowHeight; - - let left; - if (x + tooltipWidth >= canvasRight) { - left = x - tooltipWidth; - } else { - left = x - (tooltipWidth / 2); - } + scalingFactor: 2, + circleRadius: 5, + resizeTimer: null, + minZoom: 0.5, + maxZoom: 1.5, + collisionRadius: 10, + chargeStrength: -100, + tickCount: 100, + canvasColor: 'white', + alphaStart: 1, + alphaTargetStart: 0.1, + alphaTargetStop: 0, + + init(nodes, links) { + const { width, height } = this.getDimensions(); + const { canvas, context } = this.createCanvas(); + + this.canvas = canvas; + this.context = context; + this.tooltip = document.getElementById('tooltip'); + this.circleRadius = this.circleRadius * this.scalingFactor; + this.collisionRadius = this.collisionRadius * this.scalingFactor; + this.scale = (window.devicePixelRatio || 1) * this.scalingFactor; + this.transform = d3.zoomIdentity; + this.defaultIcon = this.loadImage('images/defaultFavicon.svg'); + + this.updateCanvas(width, height); + this.draw(nodes, links); + this.addListeners(); + }, + + draw(nodes, links) { + this.nodes = nodes; + this.links = links; + + this.simulateForce(); + this.drawOnCanvas(); + }, + + simulateForce() { + if (!this.simulation) { + this.simulation = d3.forceSimulation(this.nodes); + this.simulation.on('tick', () => { + return this.drawOnCanvas(); + }); + this.registerSimulationForces(); + } else { + this.simulation.nodes(this.nodes); + this.resetAlpha(); + } + this.registerLinkForce(); + }, + + resetAlpha() { + const alpha = this.simulation.alpha(); + const alphaRounded = Math.round((1 - alpha) * 100); + if (alphaRounded === 100) { + this.simulation.alpha(this.alphaStart); + this.restartSimulation(); + } + }, + + resetAlphaTarget() { + this.simulation.alphaTarget(this.alphaTargetStart); + this.restartSimulation(); + }, + + stopAlphaTarget() { + this.simulation.alphaTarget(this.alphaTargetStop); + }, + + restartSimulation() { + this.simulation.restart(); + }, + + registerLinkForce() { + const linkForce = d3.forceLink(this.links); + linkForce.id((d) => { + return d.hostname; + }); + this.simulation.force('link', linkForce); + }, + + registerSimulationForces() { + const centerForce = d3.forceCenter(this.width / 2, this.height / 2); + this.simulation.force('center', centerForce); + + const forceX = d3.forceX(this.width / 2); + this.simulation.force('x', forceX); + + const forceY = d3.forceY(this.height / 2); + this.simulation.force('y', forceY); + + const chargeForce = d3.forceManyBody(); + chargeForce.strength(this.chargeStrength); + this.simulation.force('charge', chargeForce); + + const collisionForce = d3.forceCollide(this.collisionRadius); + this.simulation.force('collide', collisionForce); + }, + + createCanvas() { + const base = document.getElementById('visualization'); + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + + base.appendChild(canvas); + + return { + canvas, + context + }; + }, + + updateCanvas(width, height) { + this.width = width; + this.height = height; + this.canvas.setAttribute('width', width * this.scale); + this.canvas.setAttribute('height', height * this.scale); + this.canvas.style.width = `${width}px`; + this.canvas.style.height = `${height}px`; + this.context.scale(this.scale, this.scale); + }, + + getDimensions() { + const element = document.body; + const { width, height } = element.getBoundingClientRect(); + + return { + width, + height + }; + }, + + drawOnCanvas() { + this.context.clearRect(0, 0, this.width, this.height); + this.context.save(); + this.context.translate(this.transform.x, this.transform.y); + this.context.scale(this.transform.k, this.transform.k); + this.drawLinks(); + this.drawNodes(); + this.context.restore(); + }, + + getRadius(thirdPartyLength) { + if (thirdPartyLength > 0) { + if (thirdPartyLength > this.collisionRadius) { + return this.circleRadius + this.collisionRadius; + } else { + return this.circleRadius + thirdPartyLength; + } + } + return this.circleRadius; + }, + + drawNodes() { + for (const node of this.nodes) { + const x = node.fx || node.x; + const y = node.fy || node.y; + let radius; + + this.context.beginPath(); + this.context.moveTo(x, y); + + if (node.firstParty) { + radius = this.getRadius(node.thirdParties.length); + this.drawFirstParty(x, y, radius); + } else { + this.drawThirdParty(x, y); + } + + if (node.shadow) { + this.drawShadow(x, y, radius); + } + + this.context.fillStyle = this.canvasColor; + this.context.closePath(); + this.context.fill(); + + if (node.favicon) { + this.drawFavicon(node, x, y, radius); + } else { + this.drawFavicon(node, x, y, this.circleRadius); + } + } + }, + + getSquare(radius) { + const side = Math.sqrt(radius * radius * 2); + const offset = side * 0.5; + + return { + side, + offset + }; + }, + + loadImage(URI) { + return new Promise((resolve, reject) => { + if (!URI) { + return reject(); + } + + const image = new Image(); + + image.onload = () => { + return resolve(image); + }; + image.onerror = () => { + return resolve(this.defaultIcon); + }; + image.src = URI; + }); + }, + + scaleFavicon(image, side) { + const canvas = document.createElement('canvas'), + context = canvas.getContext('2d'); + + canvas.width = side * this.scale; + canvas.height = side * this.scale; + context.fillStyle = this.canvasColor; + context.fillRect(0, 0, canvas.width, canvas.height); + + context.drawImage( + image, + 0, + 0, + side * this.scale, + side * this.scale); + + return context.getImageData(0, 0, canvas.width, canvas.height); + }, + + async drawFavicon(node, x, y, radius) { + const offset = this.getSquare(radius).offset, + side = this.getSquare(radius).side, + tx = this.transform.applyX(x) - offset, + ty = this.transform.applyY(y) - offset; + + if (!node.image) { + node.image = await this.loadImage(node.favicon); + } + + this.context.putImageData( + this.scaleFavicon(node.image, side), + tx * this.scale, + ty * this.scale + ); + }, + + drawShadow(x, y, radius) { + const lineWidth = 2, + shadowBlur = 15, + shadowRadius = 5; + this.context.beginPath(); + this.context.lineWidth = lineWidth; + this.context.shadowColor = this.canvasColor; + this.context.strokeStyle = 'rgba(0, 0, 0, 1)'; + this.context.shadowBlur = shadowBlur; + this.context.shadowOffsetX = 0; + this.context.shadowOffsetY = 0; + this.context.arc(x, y, radius + shadowRadius, 0, 2 * Math.PI); + this.context.stroke(); + this.context.closePath(); + }, + + drawFirstParty(x, y, radius) { + this.context.arc(x, y, radius, 0, 2 * Math.PI); + }, + + drawThirdParty(x, y) { + const deltaY = this.circleRadius / 2; + const deltaX = deltaY * Math.sqrt(3); + + this.context.moveTo(x - deltaX, y + deltaY); + this.context.lineTo(x, y - this.circleRadius); + this.context.lineTo(x + deltaX, y + deltaY); + }, + + getTooltipPosition(x, y) { + const tooltipArrowHeight = 20; + const { right: canvasRight } = this.canvas.getBoundingClientRect(); + const { + height: tooltipHeight, + width: tooltipWidth + } = this.tooltip.getBoundingClientRect(); + const top = y - tooltipHeight - this.circleRadius - tooltipArrowHeight; + + let left; + if (x + tooltipWidth >= canvasRight) { + left = x - tooltipWidth; + } else { + left = x - (tooltipWidth / 2); + } + + return { + left, + top + }; + }, + + showTooltip(title, x, y) { + this.tooltip.innerText = title; + this.tooltip.style.display = 'block'; + + const { left, top } = this.getTooltipPosition(x, y); + this.tooltip.style['left'] = `${left}px`; + this.tooltip.style['top'] = `${top}px`; + }, + + hideTooltip() { + this.tooltip.style.display = 'none'; + }, + + drawLinks() { + this.context.beginPath(); + for (const d of this.links) { + const sx = d.source.fx || d.source.x; + const sy = d.source.fy || d.source.y; + const tx = d.target.fx || d.target.x; + const ty = d.target.fy || d.target.y; + this.context.moveTo(sx, sy); + this.context.lineTo(tx, ty); + } + this.context.closePath(); + this.context.strokeStyle = '#ccc'; + this.context.stroke(); + }, + + isPointInsideCircle(x, y, cx, cy) { + const dx = Math.abs(x - cx); + const dy = Math.abs(y - cy); + const d = dx * dx + dy * dy; + const r = this.circleRadius; + + return d <= r * r; + }, + + getNodeAtCoordinates(x, y) { + for (const node of this.nodes) { + if (this.isPointInsideCircle(x, y, node.x, node.y)) { + return node; + } + } + return null; + }, + + getMousePosition(event) { + const { left, top } = this.canvas.getBoundingClientRect(); + + return { + mouseX: event.clientX - left, + mouseY: event.clientY - top + }; + }, + + addListeners() { + this.addMouseMove(); + this.addWindowResize(); + this.addDrag(); + this.addZoom(); + }, + + addMouseMove() { + this.canvas.addEventListener('mousemove', (event) => { + const { mouseX, mouseY } = this.getMousePosition(event); + const [invertX, invertY] = this.transform.invert([mouseX, mouseY]); + const node = this.getNodeAtCoordinates(invertX, invertY); + + if (node) { + this.showTooltip(node.hostname, mouseX, mouseY); + } else { + this.hideTooltip(); + } + }); + }, + + addWindowResize() { + window.addEventListener('resize', () => { + clearTimeout(this.resizeTimer); + this.resizeTimer = setTimeout(() => { + this.resize(); + }, 250); + }); + }, + + resize() { + this.canvas.style.width = 0; + this.canvas.style.height = 0; + + const { width, height } = this.getDimensions('visualization'); + this.updateCanvas(width, height); + this.draw(this.nodes, this.links); + }, + + addDrag() { + const drag = d3.drag(); + drag.subject(() => { + return this.dragSubject(); + }); + drag.on('start', () => { + return this.dragStart(); + }); + drag.on('drag', () => { + return this.drag(); + }); + drag.on('end', () => { + return this.dragEnd(); + }); + + d3.select(this.canvas) + .call(drag); + }, + + dragSubject() { + const x = this.transform.invertX(d3.event.x); + const y = this.transform.invertY(d3.event.y); + return this.getNodeAtCoordinates(x, y); + }, + + dragStart() { + if (!d3.event.active) { + this.resetAlphaTarget(); + } + d3.event.subject.shadow = true; + d3.event.subject.fx = d3.event.subject.x; + d3.event.subject.fy = d3.event.subject.y; + }, + + drag() { + d3.event.subject.fx = d3.event.x; + d3.event.subject.fy = d3.event.y; - return { - left, - top - }; - }, - - showTooltip(title, x, y) { - this.tooltip.innerText = title; - this.tooltip.style.display = 'block'; - - const { left, top } = this.getTooltipPosition(x, y); - this.tooltip.style['left'] = `${left}px`; - this.tooltip.style['top'] = `${top}px`; - }, - - hideTooltip() { - this.tooltip.style.display = 'none'; - }, - - drawLinks() { - this.context.beginPath(); - for (const d of this.links) { - const sx = d.source.fx || d.source.x; - const sy = d.source.fy || d.source.y; - const tx = d.target.fx || d.target.x; - const ty = d.target.fy || d.target.y; - this.context.moveTo(sx, sy); - this.context.lineTo(tx, ty); - } - this.context.closePath(); - this.context.strokeStyle = '#ccc'; - this.context.stroke(); - }, - - isPointInsideCircle(x, y, cx, cy) { - const dx = Math.abs(x - cx); - const dy = Math.abs(y - cy); - const d = dx * dx + dy * dy; - const r = this.circleRadius; - - return d <= r * r; - }, - - getNodeAtCoordinates(x, y) { - for (const node of this.nodes) { - if (this.isPointInsideCircle(x, y, node.x, node.y)) { - return node; - } - } - return null; - }, - - getMousePosition(event) { - const { left, top } = this.canvas.getBoundingClientRect(); - - return { - mouseX: event.clientX - left, - mouseY: event.clientY - top - }; - }, - - addListeners() { - this.addMouseMove(); - this.addWindowResize(); - this.addDrag(); - this.addZoom(); - }, - - addMouseMove() { - this.canvas.addEventListener('mousemove', (event) => { - const { mouseX, mouseY } = this.getMousePosition(event); - const [ invertX, invertY ] = this.transform.invert([mouseX, mouseY]); - const node = this.getNodeAtCoordinates(invertX, invertY); - - if (node) { - this.showTooltip(node.hostname, mouseX, mouseY); - } else { this.hideTooltip(); - } - }); - }, - - addWindowResize() { - window.addEventListener('resize', () => { - clearTimeout(this.resizeTimer); - this.resizeTimer = setTimeout(() => { - this.resize(); - }, 250); - }); - }, - - resize() { - this.canvas.style.width = 0; - this.canvas.style.height = 0; - - const { width, height } = this.getDimensions('visualization'); - this.updateCanvas(width, height); - this.draw(this.nodes, this.links); - }, - - addDrag() { - const drag = d3.drag(); - drag.subject(() => { - return this.dragSubject(); - }); - drag.on('start', () => { - return this.dragStart(); - }); - drag.on('drag', () => { - return this.drag(); - }); - drag.on('end', () => { - return this.dragEnd(); - }); - - d3.select(this.canvas) - .call(drag); - }, - - dragSubject() { - const x = this.transform.invertX(d3.event.x); - const y = this.transform.invertY(d3.event.y); - return this.getNodeAtCoordinates(x, y); - }, - - dragStart() { - if (!d3.event.active) { - this.resetAlphaTarget(); - } - d3.event.subject.shadow = true; - d3.event.subject.fx = d3.event.subject.x; - d3.event.subject.fy = d3.event.subject.y; - }, - - drag() { - d3.event.subject.fx = d3.event.x; - d3.event.subject.fy = d3.event.y; - - this.hideTooltip(); - }, - - dragEnd() { - if (!d3.event.active) { - this.stopAlphaTarget(); + }, + + dragEnd() { + if (!d3.event.active) { + this.stopAlphaTarget(); + } + d3.event.subject.x = d3.event.subject.fx; + d3.event.subject.y = d3.event.subject.fy; + d3.event.subject.fx = null; + d3.event.subject.fy = null; + d3.event.subject.shadow = false; + }, + + addZoom() { + const zoom = d3.zoom().scaleExtent([this.minZoom, this.maxZoom]); + zoom.on('zoom', () => { + return this.zoom(); + }); + + d3.select(this.canvas) + .call(zoom); + }, + + zoom() { + this.transform = d3.event.transform; + this.drawOnCanvas(); } - d3.event.subject.x = d3.event.subject.fx; - d3.event.subject.y = d3.event.subject.fy; - d3.event.subject.fx = null; - d3.event.subject.fy = null; - d3.event.subject.shadow = false; - }, - - addZoom() { - const zoom = d3.zoom().scaleExtent([this.minZoom, this.maxZoom]); - zoom.on('zoom', () => { - return this.zoom(); - }); - - d3.select(this.canvas) - .call(zoom); - }, - - zoom() { - this.transform = d3.event.transform; - this.drawOnCanvas(); - } -}; +}; \ No newline at end of file