Files: 2b05be8ee023fdd9580d6a845ba074a425756685 / contact / html / stats.js
5557 bytesRaw
1 | const nest = require('depnest') |
2 | const { h, onceTrue, computed, Value, Dict, watch, watchAll, throttle } = require('mutant') |
3 | const Chart = require('chart.js') |
4 | const pull = require('pull-stream') |
5 | |
6 | exports.gives = nest('contact.html.stats') |
7 | |
8 | exports.needs = nest({ |
9 | 'sbot.obs.connection': 'first' |
10 | }) |
11 | |
12 | const MINUTE = 60 * 1000 |
13 | const DAY = 24 * 60 * MINUTE |
14 | |
15 | const GRAPH_Y_STEP = 20 |
16 | const GRAPH_Y_MIN = 20 |
17 | |
18 | exports.create = function (api) { |
19 | return nest({ |
20 | 'contact.html.stats': stats |
21 | }) |
22 | |
23 | function stats (feedId) { |
24 | const minsPerStep = 60 * 24 |
25 | const scale = 90 * DAY |
26 | |
27 | const state = buildState({ api, feedId, minsPerStep, scale }) |
28 | const canvas = h('canvas', { height: 200, width: 1200, style: { height: '200px', width: '1200px' } }) |
29 | |
30 | const stats = h('ContactStats', [ |
31 | canvas |
32 | ]) |
33 | |
34 | initialiseChart({ canvas, state }) |
35 | return stats |
36 | } |
37 | } |
38 | |
39 | function initialiseChart ({ canvas, state: { data, range } }) { |
40 | var chart = new Chart(canvas.getContext('2d'), chartConfig(range)) |
41 | |
42 | watch(range, ({ lower, upper }) => { |
43 | // set horizontal scale |
44 | chart.options.scales.xAxes[0].time.min = lower |
45 | chart.options.scales.xAxes[0].time.max = upper |
46 | chart.update() |
47 | }) |
48 | |
49 | watchAll([throttle(data.pub, 300), throttle(data.pri, 300), range], (dataPub, dataPri, { lower, upper }) => { |
50 | const _dataPub = Object.keys(dataPub) |
51 | .sort((a, b) => a < b ? -1 : +1) |
52 | .map(ts => { |
53 | return { |
54 | t: Number(ts), // NOTE - might need to offset by a half-step ? |
55 | y: dataPub[ts] |
56 | } |
57 | }) |
58 | const _dataPri = Object.keys(dataPri) |
59 | .sort((a, b) => a < b ? -1 : +1) |
60 | .map(ts => { |
61 | return { |
62 | t: Number(ts), // NOTE - might need to offset by a half-step ? |
63 | y: dataPri[ts] |
64 | } |
65 | }) |
66 | |
67 | // update chard data |
68 | chart.data.datasets[0].data = _dataPub |
69 | chart.data.datasets[1].data = _dataPri |
70 | |
71 | // scales the height of the graph (to the visible data)! |
72 | const slice = _dataPub |
73 | .filter(d => d.t >= lower && d.t < upper) |
74 | .map(d => d.y) |
75 | .sort((a, b) => a > b ? -1 : +1) |
76 | |
77 | var h = slice[0] |
78 | if (!h || h < GRAPH_Y_MIN) h = GRAPH_Y_MIN // min-height |
79 | else h = h + (GRAPH_Y_STEP - h % GRAPH_Y_STEP) // round height to multiples of GRAPH_Y_STEP |
80 | chart.options.scales.yAxes[0].ticks.max = h |
81 | |
82 | chart.options.scales.yAxes[0].ticks.min = -h |
83 | |
84 | chart.update() |
85 | }) |
86 | } |
87 | |
88 | // ///// HELPERS ///// |
89 | |
90 | function buildState ({ api, feedId, minsPerStep, scale }) { |
91 | const data = { |
92 | pub: Dict({ |
93 | [toTimeBlock(Date.now(), minsPerStep) + minsPerStep * MINUTE]: 0, |
94 | [toTimeBlock(Date.now(), minsPerStep) + minsPerStep * MINUTE - scale]: 0 |
95 | }), |
96 | pri: Dict({ |
97 | [toTimeBlock(Date.now(), minsPerStep) + minsPerStep * MINUTE]: 0, |
98 | [toTimeBlock(Date.now(), minsPerStep) + minsPerStep * MINUTE - scale]: 0 |
99 | }) |
100 | } |
101 | onceTrue(api.sbot.obs.connection, server => { |
102 | getData({ data, server, feedId, minsPerStep, scale }) |
103 | }) |
104 | |
105 | const latest = Value(toTimeBlock(Date.now(), minsPerStep)) |
106 | // start of the most recent bar |
107 | setInterval(() => { |
108 | latest.set(toTimeBlock(Date.now(), minsPerStep)) |
109 | }, minsPerStep / 4 * MINUTE) |
110 | |
111 | const range = computed([latest], (latest) => { |
112 | return { |
113 | upper: latest + minsPerStep * MINUTE, |
114 | lower: latest + minsPerStep * MINUTE - scale |
115 | } |
116 | }) |
117 | |
118 | return { |
119 | data, |
120 | range |
121 | } |
122 | } |
123 | |
124 | function getData ({ data, server, feedId, minsPerStep, scale }) { |
125 | const upperEnd = toTimeBlock(Date.now(), minsPerStep) + minsPerStep * MINUTE |
126 | const lowerBound = upperEnd - scale |
127 | |
128 | const query = [ |
129 | { |
130 | $filter: { |
131 | timestamp: { $gte: lowerBound }, |
132 | value: { |
133 | author: feedId |
134 | } |
135 | } |
136 | }, { |
137 | $map: { |
138 | ts: ['value', 'timestamp'], |
139 | content: ['value', 'content'] |
140 | } |
141 | } |
142 | ] |
143 | |
144 | pull( |
145 | server.query.read({ query, live: true }), |
146 | pull.filter(m => !m.sync), |
147 | pull.map(m => { |
148 | return { |
149 | t: toTimeBlock(m.ts, minsPerStep), |
150 | isPrivate: typeof m.content === 'string' || Array.isArray(m.content.recps) |
151 | } |
152 | }), |
153 | pull.drain(m => { |
154 | if (!m.isPrivate) { |
155 | if (data.pub.has(m.t)) data.pub.put(m.t, data.pub.get(m.t) + 1) |
156 | else data.pub.put(m.t, 1) |
157 | } else { |
158 | if (data.pri.has(m.t)) data.pri.put(m.t, data.pri.get(m.t) - 1) |
159 | else data.pri.put(m.t, -1) |
160 | } |
161 | }) |
162 | ) |
163 | } |
164 | |
165 | function toTimeBlock (ts, minsPerStep) { |
166 | return Math.floor(ts / (minsPerStep * MINUTE)) * (minsPerStep * MINUTE) |
167 | } |
168 | |
169 | function chartConfig ({ lower, upper }) { |
170 | const barColor0 = 'hsla(290, 70%, 40%, 1)' |
171 | const barColor1 = 'hsla(0, 0%, 0%, 1)' |
172 | |
173 | return { |
174 | type: 'bar', |
175 | data: { |
176 | datasets: [{ |
177 | backgroundColor: barColor0, |
178 | borderColor: barColor0, |
179 | data: [] |
180 | }, { |
181 | backgroundColor: barColor1, |
182 | borderColor: barColor1, |
183 | data: [] |
184 | }] |
185 | }, |
186 | options: { |
187 | legend: { |
188 | display: false |
189 | }, |
190 | scales: { |
191 | xAxes: [{ |
192 | type: 'time', |
193 | distribution: 'linear', |
194 | time: { |
195 | min: lower, |
196 | max: upper, |
197 | stepSize: 4 * 60, |
198 | tooltipFormat: 'MMMM D', |
199 | unit: 'day' |
200 | }, |
201 | bounds: 'ticks', |
202 | gridLines: { display: false }, |
203 | stacked: true |
204 | }], |
205 | |
206 | yAxes: [{ |
207 | ticks: { |
208 | min: 0, |
209 | stepSize: GRAPH_Y_STEP, |
210 | suggestedMax: GRAPH_Y_MIN |
211 | }, |
212 | stacked: true |
213 | }] |
214 | }, |
215 | animation: { |
216 | // duration: 300 |
217 | } |
218 | } |
219 | } |
220 | } |
221 |
Built with git-ssb-web