Files: 5a871755c2cc2fd5aab4e83d3ce13641d29ac377 / app / page / network / replication-in.js
5067 bytesRaw
1 | const { h, Value, Dict, onceTrue, computed, watch, watchAll, throttle } = require('mutant') |
2 | const Chart = require('chart.js') |
3 | const pull = require('pull-stream') |
4 | |
5 | const MINUTE = 60 * 1000 |
6 | const DAY = 24 * 60 * MINUTE |
7 | |
8 | const GRAPH_Y_MIN_STEP = 50 |
9 | const GRAPH_Y_MIN = 100 |
10 | |
11 | module.exports = function ReplicationIn ({ connection }) { |
12 | const minsPerStep = 10 |
13 | const scale = 1 * DAY |
14 | const height = 300 |
15 | const width = 800 |
16 | |
17 | const state = buildState({ connection, minsPerStep, scale }) |
18 | const canvas = h('canvas', { height, width, style: { height: `${height}px`, width: `${width}px` } }) |
19 | |
20 | const body = h('ReplicationIn', [ |
21 | h('p', `Messages received per ${minsPerStep}-minute block over the last ${scale / DAY} days`), |
22 | canvas |
23 | ]) |
24 | // TODO hook to abort streams |
25 | |
26 | initialiseChart({ state, canvas }) |
27 | |
28 | return { |
29 | title: 'Incoming Traffic', |
30 | body |
31 | } |
32 | } |
33 | |
34 | function buildState ({ connection, minsPerStep, scale }) { |
35 | // build data, range |
36 | const data = Dict({ |
37 | [toTimeBlock(Date.now(), minsPerStep) + minsPerStep * MINUTE]: 0, |
38 | [toTimeBlock(Date.now(), minsPerStep) + minsPerStep * MINUTE - scale]: 0 |
39 | }) |
40 | onceTrue(connection, server => { |
41 | getData({ data, server, minsPerStep, scale }) |
42 | }) |
43 | |
44 | const latest = Value(toTimeBlock(Date.now(), minsPerStep)) |
45 | // start of the most recent bar |
46 | setInterval(() => { |
47 | latest.set(toTimeBlock(Date.now(), minsPerStep)) |
48 | }, minsPerStep / 4 * MINUTE) |
49 | |
50 | const range = computed([latest], (latest) => { |
51 | return { |
52 | upper: latest + minsPerStep * MINUTE, |
53 | lower: latest + minsPerStep * MINUTE - scale |
54 | } |
55 | }) |
56 | |
57 | return { |
58 | data, // TODO rename this !! |
59 | range |
60 | } |
61 | } |
62 | |
63 | function getData ({ data, server, minsPerStep, scale }) { |
64 | const upperEnd = toTimeBlock(Date.now(), minsPerStep) + minsPerStep * MINUTE |
65 | const lowerBound = upperEnd - scale |
66 | |
67 | const query = [ |
68 | { |
69 | $filter: { |
70 | timestamp: { $gte: lowerBound } |
71 | } |
72 | }, { |
73 | $filter: { |
74 | value: { |
75 | author: { $ne: server.id } |
76 | } |
77 | } |
78 | }, { |
79 | $map: { |
80 | ts: ['timestamp'] |
81 | } |
82 | } |
83 | ] |
84 | |
85 | pull( |
86 | server.query.read({ query, live: true }), |
87 | pull.filter(m => !m.sync), |
88 | pull.map(m => toTimeBlock(m.ts, minsPerStep)), |
89 | pull.drain(ts => { |
90 | if (data.has(ts)) data.put(ts, data.get(ts) + 1) |
91 | else data.put(ts, 1) |
92 | }) |
93 | ) |
94 | } |
95 | |
96 | function toTimeBlock (ts, minsPerStep) { |
97 | return Math.floor(ts / (minsPerStep * MINUTE)) * (minsPerStep * MINUTE) |
98 | } |
99 | |
100 | function initialiseChart ({ canvas, state: { data, range } }) { |
101 | var chart = new Chart(canvas.getContext('2d'), chartConfig(range)) |
102 | |
103 | watch(range, ({ lower, upper }) => { |
104 | // set horizontal scale |
105 | chart.options.scales.xAxes[0].time.min = lower |
106 | chart.options.scales.xAxes[0].time.max = upper |
107 | chart.update() |
108 | }) |
109 | |
110 | watchAll([throttle(data, 300), range], (data, { lower, upper }) => { |
111 | const _data = Object.keys(data) |
112 | .sort((a, b) => a < b ? -1 : +1) |
113 | .map(ts => { |
114 | return { |
115 | t: Number(ts), // NOTE - might need to offset by a half-step ? |
116 | y: data[ts] |
117 | } |
118 | }) |
119 | |
120 | // update chard data |
121 | chart.data.datasets[0].data = _data |
122 | |
123 | // scales the height of the graph (to the visible data)! |
124 | const slice = _data |
125 | .filter(d => d.t >= lower && d.t < upper) |
126 | .map(d => d.y) |
127 | .sort((a, b) => a > b ? -1 : +1) |
128 | |
129 | var max = slice[0] |
130 | var stepSize = GRAPH_Y_MIN_STEP |
131 | if (!max || max < GRAPH_Y_MIN) max = GRAPH_Y_MIN // min-height |
132 | else { |
133 | while ((max / stepSize) > 7) stepSize = stepSize * 2 |
134 | max = Math.ceil(max / stepSize) * stepSize // round height to multiples of stepSize |
135 | // max = max + (stepSize - max % stepSize) |
136 | } |
137 | |
138 | chart.options.scales.yAxes[0].ticks.max = max |
139 | chart.options.scales.yAxes[0].ticks.stepSize = stepSize // not sure this works |
140 | |
141 | chart.update() |
142 | }) |
143 | } |
144 | |
145 | function chartConfig ({ lower, upper }) { |
146 | const barColor = 'hsla(215, 57%, 60%, 1)' |
147 | |
148 | return { |
149 | type: 'bar', |
150 | data: { |
151 | datasets: [{ |
152 | backgroundColor: barColor, |
153 | borderColor: barColor, |
154 | data: [] |
155 | }] |
156 | }, |
157 | options: { |
158 | legend: { |
159 | display: false |
160 | }, |
161 | scales: { |
162 | xAxes: [{ |
163 | type: 'time', |
164 | distribution: 'linear', |
165 | time: { |
166 | // unit: 'day', |
167 | // min: lower, |
168 | // max: upper, |
169 | // tooltipFormat: 'MMMM D', |
170 | // stepSize: 7 |
171 | unit: 'minute', |
172 | min: lower, |
173 | max: upper, |
174 | tooltipFormat: 'HH:mm', |
175 | stepSize: 4 * 60 |
176 | // stepSize: 240 |
177 | }, |
178 | bounds: 'ticks', |
179 | ticks: { |
180 | // maxTicksLimit: 4 // already disabled |
181 | }, |
182 | gridLines: { |
183 | display: false |
184 | }, |
185 | maxBarThickness: 4 |
186 | }], |
187 | |
188 | yAxes: [{ |
189 | ticks: { |
190 | min: 0, |
191 | maxTicksLimit: 7, |
192 | stepSize: GRAPH_Y_MIN_STEP, |
193 | suggestedMax: GRAPH_Y_MIN |
194 | } |
195 | }] |
196 | }, |
197 | animation: { |
198 | // duration: 300 |
199 | } |
200 | } |
201 | } |
202 | } |
203 |
Built with git-ssb-web