Files: d006827557e63f5aabd07e480b910ca50ddb9646 / app / page / network.js
6848 bytesRaw
1 | const nest = require('depnest') |
2 | const { h, Value, Dict, onceTrue, computed, watch, watchAll, throttle } = require('mutant') |
3 | const Chart = require('chart.js') |
4 | const pull = require('pull-stream') |
5 | |
6 | const MINUTE = 60 * 1000 |
7 | const DAY = 24 * 60 * MINUTE |
8 | |
9 | const GRAPH_Y_STEP = 50 |
10 | const GRAPH_Y_MIN = 100 |
11 | |
12 | exports.gives = nest({ |
13 | 'app.html.menuItem': true, |
14 | 'app.page.network': true |
15 | }) |
16 | |
17 | exports.needs = nest({ |
18 | 'about.html.avatar': 'first', |
19 | 'app.html.scroller': 'first', |
20 | 'app.sync.goTo': 'first', |
21 | 'sbot.obs.connection': 'first', |
22 | 'sbot.obs.localPeers': 'first', |
23 | 'sbot.obs.connectedPeers': 'first' |
24 | }) |
25 | |
26 | exports.create = function (api) { |
27 | return nest({ |
28 | 'app.html.menuItem': menuItem, |
29 | 'app.page.network': networkPage |
30 | }) |
31 | |
32 | function menuItem () { |
33 | return h('a', { |
34 | 'ev-click': () => api.app.sync.goTo({ page: 'network' }) |
35 | }, '/network') |
36 | } |
37 | |
38 | function networkPage (location) { |
39 | const minsPerStep = 10 |
40 | const scale = 1 * DAY |
41 | |
42 | const state = buildState({ api, minsPerStep, scale }) |
43 | const canvas = h('canvas', { height: 500, width: 1200, style: { height: '500px', width: '1200px' } }) |
44 | |
45 | const page = h('NetworkPage', [ |
46 | h('div.container', [ |
47 | h('h1', 'Network'), |
48 | h('section', [ |
49 | h('h2', [ |
50 | 'Local Peers', |
51 | h('i.fa.fa-question-circle-o', { title: 'these are people on the same WiFi/ LAN as you right now. You might not know some of them yet, but you can click through to find out more about them and follow them if you like.' }) |
52 | ]), |
53 | computed(state.localPeers, peers => { |
54 | if (!peers.length) return h('p', 'No local peers (on same wifi/ LAN)') |
55 | |
56 | return peers.map(peer => api.about.html.avatar(peer)) |
57 | }) |
58 | ]), |
59 | h('section', [ |
60 | h('h2', [ |
61 | 'Remote Peers', |
62 | h('i.fa.fa-question-circle-o', { title: 'these are peers which have fixed addresses, and are likely friends of friends (a.k.a. pubs)' }) |
63 | ]), |
64 | computed(state.remotePeers, peers => { |
65 | if (!peers.length) return h('p', 'No remote peers connected') |
66 | |
67 | return peers.map(peer => api.about.html.avatar(peer)) |
68 | }) |
69 | ]), |
70 | h('section', [ |
71 | h('h2', [ |
72 | 'Received Messages', |
73 | h('i.fa.fa-question-circle-o', { |
74 | title: `Messages received per ${minsPerStep}-minute block over the last ${scale / DAY} days` |
75 | }) |
76 | ]), |
77 | canvas |
78 | ]) |
79 | ]) |
80 | ]) |
81 | |
82 | initialiseChart({ canvas, state }) |
83 | |
84 | var { container } = api.app.html.scroller({ prepend: page }) |
85 | container.title = '/network' |
86 | return container |
87 | } |
88 | } |
89 | |
90 | function initialiseChart ({ canvas, state: { data, range } }) { |
91 | var chart = new Chart(canvas.getContext('2d'), chartConfig(range)) |
92 | |
93 | watch(range, ({ lower, upper }) => { |
94 | // set horizontal scale |
95 | chart.options.scales.xAxes[0].time.min = lower |
96 | chart.options.scales.xAxes[0].time.max = upper |
97 | chart.update() |
98 | }) |
99 | |
100 | watchAll([throttle(data, 300), range], (data, { lower, upper }) => { |
101 | const _data = Object.keys(data) |
102 | .sort((a, b) => a < b ? -1 : +1) |
103 | .map(ts => { |
104 | return { |
105 | t: Number(ts), // NOTE - might need to offset by a half-step ? |
106 | y: data[ts] |
107 | } |
108 | }) |
109 | |
110 | // update chard data |
111 | chart.data.datasets[0].data = _data |
112 | |
113 | // scales the height of the graph (to the visible data)! |
114 | const slice = _data |
115 | .filter(d => d.t >= lower && d.t < upper) |
116 | .map(d => d.y) |
117 | .sort((a, b) => a > b ? -1 : +1) |
118 | |
119 | var h = slice[0] |
120 | if (!h || h < GRAPH_Y_MIN) h = GRAPH_Y_MIN // min-height |
121 | else h = h + (GRAPH_Y_STEP - h % GRAPH_Y_STEP) // round height to multiples of GRAPH_Y_STEP |
122 | chart.options.scales.yAxes[0].ticks.max = h |
123 | |
124 | chart.update() |
125 | }) |
126 | } |
127 | |
128 | // ///// HELPERS ///// |
129 | |
130 | function buildState ({ api, minsPerStep, scale }) { |
131 | const data = Dict({ |
132 | [toTimeBlock(Date.now(), minsPerStep) + minsPerStep * MINUTE]: 0, |
133 | [toTimeBlock(Date.now(), minsPerStep) + minsPerStep * MINUTE - scale]: 0 |
134 | }) |
135 | onceTrue(api.sbot.obs.connection, server => { |
136 | getData({ data, server, minsPerStep, scale }) |
137 | }) |
138 | |
139 | const latest = Value(toTimeBlock(Date.now(), minsPerStep)) |
140 | // start of the most recent bar |
141 | setInterval(() => { |
142 | latest.set(toTimeBlock(Date.now(), minsPerStep)) |
143 | }, minsPerStep / 4 * MINUTE) |
144 | |
145 | const range = computed([latest], (latest) => { |
146 | return { |
147 | upper: latest + minsPerStep * MINUTE, |
148 | lower: latest + minsPerStep * MINUTE - scale |
149 | } |
150 | }) |
151 | |
152 | const localPeers = throttle(api.sbot.obs.localPeers(), 1000) |
153 | const remotePeers = computed([localPeers, throttle(api.sbot.obs.connectedPeers(), 1000)], (local, connected) => { |
154 | return connected.filter(peer => !local.includes(peer)) |
155 | }) |
156 | |
157 | return { |
158 | data, |
159 | range, |
160 | localPeers, |
161 | remotePeers |
162 | } |
163 | } |
164 | |
165 | function getData ({ data, server, minsPerStep, scale }) { |
166 | const upperEnd = toTimeBlock(Date.now(), minsPerStep) + minsPerStep * MINUTE |
167 | const lowerBound = upperEnd - scale |
168 | |
169 | const query = [ |
170 | { |
171 | $filter: { |
172 | timestamp: { $gte: lowerBound } |
173 | } |
174 | }, { |
175 | $filter: { |
176 | value: { |
177 | author: { $ne: server.id } |
178 | } |
179 | } |
180 | }, { |
181 | $map: { |
182 | ts: ['timestamp'] |
183 | } |
184 | } |
185 | ] |
186 | |
187 | pull( |
188 | server.query.read({ query, live: true }), |
189 | pull.filter(m => !m.sync), |
190 | pull.map(m => toTimeBlock(m.ts, minsPerStep)), |
191 | pull.drain(ts => { |
192 | if (data.has(ts)) data.put(ts, data.get(ts) + 1) |
193 | else data.put(ts, 1) |
194 | }) |
195 | ) |
196 | } |
197 | |
198 | |
199 | function toTimeBlock (ts, minsPerStep) { |
200 | return Math.floor(ts / (minsPerStep * MINUTE)) * (minsPerStep * MINUTE) |
201 | } |
202 | |
203 | function chartConfig ({ lower, upper }) { |
204 | const barColor = 'hsla(215, 57%, 60%, 1)' |
205 | |
206 | return { |
207 | type: 'bar', |
208 | data: { |
209 | datasets: [{ |
210 | backgroundColor: barColor, |
211 | borderColor: barColor, |
212 | data: [] |
213 | }] |
214 | }, |
215 | options: { |
216 | legend: { |
217 | display: false |
218 | }, |
219 | scales: { |
220 | xAxes: [{ |
221 | type: 'time', |
222 | distribution: 'linear', |
223 | time: { |
224 | // unit: 'day', |
225 | // min: lower, |
226 | // max: upper, |
227 | // tooltipFormat: 'MMMM D', |
228 | // stepSize: 7 |
229 | unit: 'minute', |
230 | min: lower, |
231 | max: upper, |
232 | tooltipFormat: 'HH:mm', |
233 | stepSize: 4 * 60 |
234 | // stepSize: 240 |
235 | }, |
236 | bounds: 'ticks', |
237 | ticks: { |
238 | // maxTicksLimit: 4 // already disabled |
239 | }, |
240 | gridLines: { |
241 | display: false |
242 | } |
243 | // maxBarThickness: 2 |
244 | }], |
245 | |
246 | yAxes: [{ |
247 | ticks: { |
248 | min: 0, |
249 | stepSize: GRAPH_Y_STEP, |
250 | suggestedMax: GRAPH_Y_MIN |
251 | } |
252 | }] |
253 | }, |
254 | animation: { |
255 | // duration: 300 |
256 | } |
257 | } |
258 | } |
259 | } |
260 |
Built with git-ssb-web