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