1
+ <!DOCTYPE html>
2
+ < html lang ="en ">
3
+ < head >
4
+ < meta charset ="utf-8 ">
5
+ < meta name ="viewport " content ="width=device-width, initial-scale=1 ">
6
+ < link rel ="icon " type ="image/svg " href ="https://files.oxl.at/img/oxl3_sm.png ">
7
+
8
+ < script src ="https://d3js.org/d3.v4.min.js "> </ script >
9
+ < style >
10
+ .bubble {
11
+ overflow : hidden;
12
+ }
13
+
14
+ .bubble-asn {
15
+ font-weight : bold;
16
+ }
17
+
18
+ .bubble-info {
19
+ visibility : hidden;
20
+ display : none;
21
+ }
22
+
23
+ # infos {
24
+ top : 10px ;
25
+ left : 20px ;
26
+ width : 40vw ;
27
+ height : 30vh ;
28
+ background-color : whitesmoke;
29
+ opacity : 0.7 ;
30
+ z-index : -1 ;
31
+ border-radius : 10px ;
32
+ border-width : 2px ;
33
+ border-color : black;
34
+ border-style : solid;
35
+ position : absolute;
36
+ padding : 20px ;
37
+ overflow : hidden;
38
+ }
39
+
40
+ # chart {
41
+ display : inline-block;
42
+ }
43
+
44
+ </ style >
45
+ </ head >
46
+ < body >
47
+ <!-- see: https://observablehq.com/@d3/bubble-chart/2 -->
48
+ < div id ="infos "> </ div >
49
+ < svg id ="chart "> </ svg >
50
+ < script >
51
+ // start test webserver: 'python3 test_server.py'
52
+
53
+ const TOP_N = 30 ;
54
+ const WIDTH = window . innerWidth ;
55
+ const HEIGHT = window . innerHeight - 50 ;
56
+ const BUBBLE_PAD = 50 ;
57
+ const COLORSCHEME = [
58
+ // red-ish
59
+ "#B93022" , "#FF383D" , "#D32669" , "#C43F03" , "#E70291" , "#E211C0" , "#6A3D52" , "#C75839" , "#E7A8B0" ,
60
+ "#DB7277" , "#A23E48" ,
61
+ // purple-ish
62
+ "#9F8AD2" , "#4B2ED1" , "#9C1091" , "#EA77DF" , "#8D0BE8" , "#BB65BC" , "#993CF3" , "#9F09EF" , "#CD0099" ,
63
+ "#EA2794" , "#DC8581" , "#9C1686" ,
64
+ // orange-ish
65
+ "#E37F6F" , "#C5824E" , "#FDCA29" , "#D99620" , "#E7C014" , "#F9C630" , "#F6BC55" , "#B9AB58" , "#DB8E94" ,
66
+ "#D27E7E" ,
67
+ ] ;
68
+
69
+ function numberWithDot ( x ) {
70
+ return x . toString ( ) . replace ( / \B (? = ( \d { 3 } ) + (? ! \d ) ) / g, "." ) ;
71
+ }
72
+
73
+ function showASNInfos ( e ) {
74
+ let infos = JSON . parse ( e . currentTarget . querySelectorAll ( ".bubble-info" ) [ 0 ] . textContent ) ;
75
+ let infoContainer = document . getElementById ( 'infos' ) ;
76
+ infoContainer . innerHTML = '<b>ASN</b>: ' + infos . asn + '<br>' ;
77
+ infoContainer . innerHTML += '<b>Org</b>: ' + infos . info . org . name + '<br>' ;
78
+ if ( infos . info . org . country != '' && infos . info . org . country !== null ) {
79
+ infoContainer . innerHTML += '<b>Country</b>: ' + infos . info . org . country + '<br>' ;
80
+ }
81
+ infoContainer . innerHTML += '<b>IPv4 Addresses</b>: ' + numberWithDot ( infos . info . ipv4 ) + '<br>' ;
82
+ infoContainer . innerHTML += '<b>IPv6 Addresses</b>: ' + numberWithDot ( infos . info . ipv6 ) + '<br>' ;
83
+ let kinds = [ ] ;
84
+ for ( let [ k , v ] of Object . entries ( infos . kind ) ) {
85
+ if ( v === true ) {
86
+ kinds . push ( k ) ;
87
+ }
88
+ }
89
+ if ( kinds . length > 0 ) {
90
+ infoContainer . innerHTML += '<b>Kind</b>: ' + kinds . join ( ', ' ) + '<br>' ;
91
+
92
+ }
93
+ for ( let [ k , v ] of Object . entries ( infos . reports ) ) {
94
+ let r = '<small><b>Reports ' + k + '</b>: ' ;
95
+ if ( k == 'rel_by_ip4' ) {
96
+ r += v ;
97
+ } else {
98
+ r += numberWithDot ( v ) ;
99
+ }
100
+ infoContainer . innerHTML += r + '</small><br>' ;
101
+ }
102
+ }
103
+
104
+ function createChart ( ) {
105
+ d3 . json ( 'risk_asn_med.json' , function ( resp ) {
106
+ let data = [ ] ;
107
+
108
+ for ( let [ asn , values ] of Object . entries ( resp ) ) {
109
+ data . push ( {
110
+ asn : asn , info : values . info , kind : values . kind , reports : values . reports ,
111
+ sortBy : values . reports . all ,
112
+ } )
113
+ }
114
+
115
+ const sortedData = data . sort ( ( a , b ) => b . reports . all - a . reports . all ) . slice ( 0 , TOP_N ) ;
116
+ console . log ( sortedData ) ;
117
+
118
+ const colorScale = d3 . scaleOrdinal ( )
119
+ . domain ( sortedData . map ( d => d . asn ) )
120
+ . range ( COLORSCHEME ) ;
121
+
122
+ const pack = d3 . pack ( )
123
+ . size ( [ WIDTH - BUBBLE_PAD , HEIGHT - BUBBLE_PAD ] )
124
+ . padding ( 5 ) ;
125
+
126
+ const hierarchy = d3 . hierarchy ( { children : sortedData } )
127
+ . sum ( d => d . sortBy ) ;
128
+
129
+ const root = pack ( hierarchy ) ;
130
+
131
+ const svg = d3 . select ( "#chart" )
132
+ . attr ( "width" , WIDTH )
133
+ . attr ( "height" , HEIGHT ) ;
134
+
135
+ const bubbles = svg . selectAll ( ".bubble" )
136
+ . data ( root . descendants ( ) . slice ( 1 ) )
137
+ . enter ( )
138
+ . append ( "g" )
139
+ . attr ( "class" , "bubble" )
140
+ . attr ( "transform" , d => `translate(${ d . x + BUBBLE_PAD } , ${ d . y + BUBBLE_PAD } )` ) ;
141
+
142
+ bubbles . append ( "circle" )
143
+ . attr ( "r" , d => d . r )
144
+ . attr ( "fill" , d => colorScale ( d . data . asn ) ) ;
145
+
146
+ bubbles . append ( "text" )
147
+ . style ( "text-anchor" , "middle" )
148
+ . append ( "tspan" )
149
+ . attr ( "class" , "bubble-asn" )
150
+ . text ( d => d . data . asn ) ;
151
+
152
+ // hover infos
153
+ bubbles . append ( "text" )
154
+ . style ( "text-anchor" , "bottom" )
155
+ . append ( "tspan" )
156
+ . attr ( "class" , "bubble-info" )
157
+ . text ( d => JSON . stringify ( d . data ) ) ;
158
+
159
+ d3 . selectAll ( "text" ) . style ( "fill" , "black" ) ;
160
+
161
+ for ( let b of document . querySelectorAll ( ".bubble" ) ) {
162
+ b . addEventListener ( "mouseover" , showASNInfos ) ;
163
+ }
164
+
165
+ } ) ;
166
+
167
+ }
168
+
169
+ createChart ( ) ;
170
+ </ script >
171
+ </ body >
172
+ </ html >
0 commit comments