git ssb

16+

Dominic / patchbay



Tree: 2b05be8ee023fdd9580d6a845ba074a425756685

Files: 2b05be8ee023fdd9580d6a845ba074a425756685 / contact / html / stats.js

5557 bytesRaw
1const nest = require('depnest')
2const { h, onceTrue, computed, Value, Dict, watch, watchAll, throttle } = require('mutant')
3const Chart = require('chart.js')
4const pull = require('pull-stream')
5
6exports.gives = nest('contact.html.stats')
7
8exports.needs = nest({
9 'sbot.obs.connection': 'first'
10})
11
12const MINUTE = 60 * 1000
13const DAY = 24 * 60 * MINUTE
14
15const GRAPH_Y_STEP = 20
16const GRAPH_Y_MIN = 20
17
18exports.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
39function 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
90function 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
124function 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
165function toTimeBlock (ts, minsPerStep) {
166 return Math.floor(ts / (minsPerStep * MINUTE)) * (minsPerStep * MINUTE)
167}
168
169function 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