app/page/network.jsView |
---|
| 1 … | +const nest = require('depnest') |
| 2 … | +const { h, Value, Dict, onceTrue, computed, 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 … | + 'sbot.obs.connection': 'first' |
| 19 … | +}) |
| 20 … | + |
| 21 … | +exports.create = function (api) { |
| 22 … | + return nest({ |
| 23 … | + 'app.html.menuItem': menuItem, |
| 24 … | + 'app.page.network': networkPage |
| 25 … | + }) |
| 26 … | + |
| 27 … | + function menuItem () { |
| 28 … | + return h('a', { |
| 29 … | + 'ev-click': () => api.app.sync.goTo({ page: 'network' }) |
| 30 … | + }, '/network') |
| 31 … | + } |
| 32 … | + |
| 33 … | + function networkPage (location) { |
| 34 … | + const minsPerStep = 5 |
| 35 … | + |
| 36 … | + const data = Dict() |
| 37 … | + onceTrue(api.sbot.obs.connection, server => { |
| 38 … | + getData({ data, server, minsPerStep }) |
| 39 … | + }) |
| 40 … | + |
| 41 … | + const latest = Value(toTimeBlock(Date.now(), minsPerStep)) |
| 42 … | + |
| 43 … | + setInterval(() => { |
| 44 … | + console.log('boop') |
| 45 … | + latest.set(toTimeBlock(Date.now(), minsPerStep)) |
| 46 … | + }, 2 * MINUTE) |
| 47 … | + |
| 48 … | + const range = computed([latest], (latest) => { |
| 49 … | + return { |
| 50 … | + upper: latest + minsPerStep * MINUTE, |
| 51 … | + lower: latest + minsPerStep * MINUTE - DAY |
| 52 … | + } |
| 53 … | + }) |
| 54 … | + |
| 55 … | + |
| 56 … | + |
| 57 … | + const canvas = h('canvas', { height: 300, width: 1200, style: { height: '300px', width: '1200px' } }) |
| 58 … | + const page = h('NetworkPage', { title: '/network' }, [ |
| 59 … | + h('div.container', [ |
| 60 … | + h('h1', 'Network'), |
| 61 … | + canvas |
| 62 … | + ]) |
| 63 … | + ]) |
| 64 … | + |
| 65 … | + initialiseChart({ canvas, data, range }) |
| 66 … | + |
| 67 … | + return page |
| 68 … | + } |
| 69 … | +} |
| 70 … | + |
| 71 … | +function getData ({ data, server, minsPerStep }) { |
| 72 … | + const query = [ |
| 73 … | + { |
| 74 … | + $filter: { |
| 75 … | + timestamp: { $gte: toTimeBlock(Date.now(), minsPerStep) + minsPerStep * MINUTE - DAY } |
| 76 … | + } |
| 77 … | + }, { |
| 78 … | + $map: { |
| 79 … | + ts: ['timestamp'] |
| 80 … | + } |
| 81 … | + } |
| 82 … | + ] |
| 83 … | + |
| 84 … | + pull( |
| 85 … | + server.query.read({ query, live: true }), |
| 86 … | + pull.filter(m => !m.sync), |
| 87 … | + pull.map(m => toTimeBlock(m.ts, minsPerStep)), |
| 88 … | + pull.drain(ts => { |
| 89 … | + if (data.has(ts)) data.put(ts, data.get(ts) + 1) |
| 90 … | + else data.put(ts, 1) |
| 91 … | + }) |
| 92 … | + ) |
| 93 … | +} |
| 94 … | + |
| 95 … | +function initialiseChart ({ canvas, data, range }) { |
| 96 … | + var chart = new Chart(canvas.getContext('2d'), chartConfig(range)) |
| 97 … | + |
| 98 … | + const chartData = computed([throttle(data, 300), range], (data, range) => { |
| 99 … | + return Object.keys(data) |
| 100 … | + .filter(ts => ts >= range.lower && ts < range.upper) |
| 101 … | + .map(ts => { |
| 102 … | + return { |
| 103 … | + t: Number(ts), |
| 104 … | + y: data[ts] |
| 105 … | + } |
| 106 … | + }) |
| 107 … | + }) |
| 108 … | + chartData(data => { |
| 109 … | + chart.data.datasets[0].data = data |
| 110 … | + chart.update() |
| 111 … | + }) |
| 112 … | + |
| 113 … | + |
| 114 … | + watchAll([chartData, range], (chartData, range) => { |
| 115 … | + const { lower, upper } = range |
| 116 … | + const slice = chartData |
| 117 … | + .filter(d => d.t >= lower && d.t < upper) |
| 118 … | + .map(d => d.y) |
| 119 … | + .sort((a, b) => a > b ? -1 : +1) |
| 120 … | + |
| 121 … | + var h = slice[0] |
| 122 … | + if (!h || h < GRAPH_Y_MIN) h = GRAPH_Y_MIN |
| 123 … | + else h = h + (GRAPH_Y_STEP - h % GRAPH_Y_STEP) |
| 124 … | + |
| 125 … | + chart.options.scales.yAxes[0].ticks.max = h |
| 126 … | + |
| 127 … | + chart.update() |
| 128 … | + }) |
| 129 … | + |
| 130 … | + |
| 131 … | + range(range => { |
| 132 … | + const { lower, upper } = range |
| 133 … | + |
| 134 … | + chart.options.scales.xAxes[0].time.min = lower |
| 135 … | + chart.options.scales.xAxes[0].time.max = upper |
| 136 … | + |
| 137 … | + chart.update() |
| 138 … | + }) |
| 139 … | +} |
| 140 … | + |
| 141 … | + |
| 142 … | + |
| 143 … | +function toTimeBlock (ts, minsPerStep) { |
| 144 … | + return Math.floor(ts / (minsPerStep * MINUTE)) * (minsPerStep * MINUTE) |
| 145 … | +} |
| 146 … | + |
| 147 … | +function chartConfig ({ lower, upper }) { |
| 148 … | + const barColor = 'hsla(215, 57%, 60%, 1)' |
| 149 … | + |
| 150 … | + return { |
| 151 … | + type: 'bar', |
| 152 … | + data: { |
| 153 … | + datasets: [{ |
| 154 … | + backgroundColor: barColor, |
| 155 … | + borderColor: barColor, |
| 156 … | + data: [] |
| 157 … | + }] |
| 158 … | + }, |
| 159 … | + options: { |
| 160 … | + legend: { |
| 161 … | + display: false |
| 162 … | + }, |
| 163 … | + scales: { |
| 164 … | + xAxes: [{ |
| 165 … | + type: 'time', |
| 166 … | + distribution: 'linear', |
| 167 … | + time: { |
| 168 … | + |
| 169 … | + |
| 170 … | + |
| 171 … | + |
| 172 … | + |
| 173 … | + unit: 'minute', |
| 174 … | + min: lower, |
| 175 … | + max: upper, |
| 176 … | + tooltipFormat: 'HH:mm', |
| 177 … | + stepSize: 240 |
| 178 … | + }, |
| 179 … | + bounds: 'ticks', |
| 180 … | + ticks: { |
| 181 … | + |
| 182 … | + }, |
| 183 … | + gridLines: { |
| 184 … | + display: false |
| 185 … | + }, |
| 186 … | + |
| 187 … | + }], |
| 188 … | + |
| 189 … | + yAxes: [{ |
| 190 … | + ticks: { |
| 191 … | + min: 0, |
| 192 … | + stepSize: GRAPH_Y_STEP, |
| 193 … | + suggestedMax: GRAPH_Y_MIN |
| 194 … | + } |
|
| 195 … | + }] |
| 196 … | + }, |
| 197 … | + animation: { |
| 198 … | + |
| 199 … | + } |
| 200 … | + } |
| 201 … | + } |
| 202 … | +} |