app/page/statsShow.jsView |
---|
1 | 1 | const nest = require('depnest') |
2 | | -const { h, resolve, when, Value, Struct, Array: MutantArray, Dict, onceTrue, map, computed, dictToCollection, throttle } = require('mutant') |
| 2 | +const { h, resolve, when, Value, Struct, Array: MutantArray, Dict, onceTrue, map, computed, dictToCollection, throttle, watchAll } = require('mutant') |
3 | 3 | const pull = require('pull-stream') |
4 | 4 | const marksum = require('markdown-summary') |
5 | 5 | const Chart = require('chart.js') |
6 | 6 | const groupBy = require('lodash/groupBy') |
7 | | -const merge = require('lodash/merge') |
| 7 | +const mergeWith = require('lodash/mergeWith') |
| 8 | +const flatMap = require('lodash/flatMap') |
8 | 9 | |
9 | 10 | exports.gives = nest('app.page.statsShow') |
10 | 11 | |
11 | 12 | exports.needs = nest({ |
21 | 22 | blogs: MutantArray([]), |
22 | 23 | comments: Dict(), |
23 | 24 | likes: Dict() |
24 | 25 | }) |
| 26 | + onceTrue(api.sbot.obs.connection, server => { |
| 27 | + fetchBlogs({ server, store }) |
| 28 | + }) |
25 | 29 | |
26 | 30 | var howFarBack = Value(0) |
27 | 31 | |
28 | 32 | const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000 |
29 | 33 | |
30 | | - |
31 | | - var range = computed([howFarBack], howFarBack => { |
32 | | - const now = Date.now() |
33 | | - return { |
34 | | - upper: now - howFarBack * THIRTY_DAYS, |
35 | | - lower: now - (howFarBack + 1) * THIRTY_DAYS |
36 | | - } |
| 34 | + const COMMENTS = 'comments' |
| 35 | + const LIKES = 'likes' |
| 36 | + var context = Struct({ |
| 37 | + focus: Value(COMMENTS), |
| 38 | + blog: Value(), |
| 39 | + range: computed([howFarBack], howFarBack => { |
| 40 | + const now = Date.now() |
| 41 | + return { |
| 42 | + upper: now - howFarBack * THIRTY_DAYS, |
| 43 | + lower: now - (howFarBack + 1) * THIRTY_DAYS |
| 44 | + } |
| 45 | + }) |
37 | 46 | }) |
38 | 47 | |
39 | | - var commentsAll = computed(throttle(dictToCollection(store.comments), 1000), (comments) => { |
40 | | - return comments |
| 48 | + var commentsAll = computed(throttle(dictToCollection(store.comments), 1000), (msgs) => { |
| 49 | + return msgs |
41 | 50 | .map(c => c.value) |
42 | 51 | .reduce((n, sofar) => [...n, ...sofar], []) |
43 | 52 | }) |
| 53 | + |
| 54 | + |
| 55 | + |
| 56 | + |
| 57 | + |
| 58 | + |
44 | 59 | |
45 | | - |
46 | | - var visibleComments = computed([commentsAll, range], (comments, range) => { |
47 | | - return comments |
| 60 | + var visibleCommentsCount = computed([commentsAll, context.range], (msgs, range) => { |
| 61 | + return msgs |
48 | 62 | .filter(msg => { |
49 | 63 | const ts = msg.value.timestamp |
50 | 64 | return ts >= range.lower && ts <= range.upper |
51 | 65 | }) |
| 66 | + .length |
52 | 67 | }) |
53 | 68 | |
54 | | - var rangeLikes = computed([throttle(dictToCollection(store.likes), 1000), range], (likes, range) => { |
55 | | - return likes |
| 69 | + var likesAll = computed(throttle(dictToCollection(store.likes), 1000), (msgs) => { |
| 70 | + return msgs |
56 | 71 | .map(c => c.value) |
57 | 72 | .reduce((n, sofar) => [...n, ...sofar], []) |
58 | | - |
59 | | - |
60 | | - |
61 | | - |
62 | 73 | }) |
63 | 74 | |
64 | | - onceTrue(api.sbot.obs.connection, server => { |
65 | | - fetchBlogs({ server, store }) |
| 75 | + var visibleLikesCount = computed([likesAll, context.range], (msgs, range) => { |
| 76 | + return msgs |
| 77 | + .filter(msg => { |
| 78 | + const ts = msg.value.timestamp |
| 79 | + return ts >= range.lower && ts <= range.upper |
| 80 | + }) |
| 81 | + .length |
| 82 | + }) |
66 | 83 | |
67 | | - |
68 | | - |
69 | | - |
70 | | - |
71 | | - |
72 | | - |
73 | | - |
74 | | - |
75 | | - |
76 | | - |
| 84 | + var focused = Struct({ |
| 85 | + [LIKES]: commentsAll, |
| 86 | + [COMMENTS]: likesAll |
77 | 87 | }) |
| 88 | + |
78 | 89 | const canvas = h('canvas', { height: 200, width: 600, style: { height: '200px', width: '600px' } }) |
79 | 90 | |
| 91 | + const displayComments = () => context.focus.set(COMMENTS) |
| 92 | + const displayLikes = () => context.focus.set(LIKES) |
| 93 | + |
80 | 94 | const page = h('Page -statsShow', [ |
81 | 95 | h('Scroller.content', [ |
82 | | - h('div.content', [ |
| 96 | + h('div.content', [ |
83 | 97 | h('h1', 'Stats'), |
84 | 98 | h('section.totals', [ |
85 | | - h('div.comments', [ |
86 | | - h('div.count', computed(visibleComments, msgs => msgs.length)), |
87 | | - h('strong', 'Comments'), |
88 | | - '(30 days)' |
89 | | - ]), |
90 | | - h('div.likes', [ |
91 | | - h('div.count', computed(rangeLikes, msgs => msgs.length)), |
92 | | - h('strong', 'Likes'), |
93 | | - '(30 days)' |
94 | | - ]), |
95 | | - h('div.shares', [ |
96 | | - ]) |
| 99 | + h('div.comments', |
| 100 | + { |
| 101 | + className: computed(context.focus, focus => focus === COMMENTS ? '-selected' : ''), |
| 102 | + 'ev-click': displayComments |
| 103 | + }, [ |
| 104 | + h('div.count', visibleCommentsCount), |
| 105 | + h('strong', 'Comments'), |
| 106 | + '(30 days)' |
| 107 | + ]), |
| 108 | + h('div.likes', |
| 109 | + { |
| 110 | + className: computed(context.focus, focus => focus === LIKES ? '-selected' : ''), |
| 111 | + 'ev-click': displayLikes |
| 112 | + }, [ |
| 113 | + h('div.count', visibleLikesCount), |
| 114 | + h('strong', 'Likes'), |
| 115 | + '(30 days)' |
| 116 | + ] |
| 117 | + ), |
| 118 | + h('div.shares', |
| 119 | + { |
| 120 | + className: when(context.shares, '-selected') |
| 121 | + |
| 122 | + }, [ |
| 123 | + |
| 124 | + h('div.count', '--'), |
| 125 | + h('strong', 'Shares'), |
| 126 | + '(30 days)' |
| 127 | + ] |
| 128 | + ) |
97 | 129 | ]), |
98 | 130 | h('section.graph', [ |
99 | 131 | canvas, |
100 | 132 | h('div.changeRange', [ |
135 | 167 | ]) |
136 | 168 | ]) |
137 | 169 | ]) |
138 | 170 | |
139 | | - |
140 | | - |
141 | | - |
142 | | - var chart = new Chart(canvas.getContext('2d'), chartConfig({ range, chartData: [] })) |
| 171 | + var chart = new Chart(canvas.getContext('2d'), chartConfig({ context, chartData: [] })) |
143 | 172 | |
144 | 173 | const toDay = ts => Math.floor(ts / (24 * 60 * 60 * 1000)) |
145 | 174 | |
146 | | - |
147 | | - const chartData = computed(commentsAll, msgs => { |
| 175 | + |
| 176 | + |
| 177 | + var lastFocus = context.focus() |
| 178 | + const zeroGraphOnFocusChange = (focus) => { |
| 179 | + if (focus !== lastFocus) { |
| 180 | + chart.data.datasets[0].data = [] |
| 181 | + chart.update() |
| 182 | + lastFocus = focus |
| 183 | + } |
| 184 | + } |
| 185 | + const chartData = computed([context.focus, focused], (focus, focused) => { |
| 186 | + zeroGraphOnFocusChange(focus) |
| 187 | + const msgs = focused[focus] |
148 | 188 | const grouped = groupBy(msgs, m => toDay(m.value.timestamp)) |
149 | 189 | |
150 | 190 | var data = Object.keys(grouped) |
151 | 191 | .map(day => { |
156 | 196 | }) |
157 | 197 | return data |
158 | 198 | }) |
159 | 199 | |
160 | | - chartData(() => { |
161 | | - chart = merge(chart, chartConfig({ range, chartData })) |
| 200 | + chartData(data => { |
| 201 | + chart.data.datasets[0].data = data |
| 202 | + |
162 | 203 | chart.update() |
163 | 204 | }) |
164 | 205 | |
165 | | - range(() => { |
166 | | - chart = merge(chart, chartConfig({ range, chartData })) |
| 206 | + const graphHeight = computed([chartData, context.range], (data, range) => { |
| 207 | + const { lower, upper } = range |
| 208 | + const slice = data |
| 209 | + .filter(d => d.t >= lower && d.t <= upper) |
| 210 | + .map(d => d.y) |
| 211 | + .sort((a, b) => a < b) |
| 212 | + const localMax = slice[0] ? Math.max(slice[0], 10) : 10 |
| 213 | + |
| 214 | + return localMax + (5 - localMax % 5) |
| 215 | + }) |
| 216 | + graphHeight(h => { |
| 217 | + chart.options.scales.yAxes[0].ticks.max = h |
| 218 | + |
167 | 219 | chart.update() |
168 | 220 | }) |
169 | 221 | |
| 222 | + context.range(range => { |
| 223 | + const { lower, upper } = range |
| 224 | + |
| 225 | + chart.options.scales.xAxes[0].time.min = new Date(lower) |
| 226 | + chart.options.scales.xAxes[0].time.max = new Date(upper) |
| 227 | + |
| 228 | + chart.update() |
| 229 | + }) |
| 230 | + |
170 | 231 | return page |
171 | 232 | } |
172 | 233 | |
173 | 234 | function viewBlog (blog) { |
218 | 279 | }) |
219 | 280 | ) |
220 | 281 | } |
221 | 282 | |
222 | | -function chartConfig ({ range, chartData }) { |
223 | | - const { lower, upper } = resolve(range) |
| 283 | +function chartConfig ({ context, chartData }) { |
| 284 | + const { lower, upper } = resolve(context.range) |
224 | 285 | |
225 | 286 | const data = resolve(chartData) || [] |
226 | 287 | const slice = data |
227 | 288 | .filter(d => d.t >= lower && d.t <= upper) |
232 | 293 | return { |
233 | 294 | type: 'bar', |
234 | 295 | data: { |
235 | 296 | datasets: [{ |
236 | | - |
237 | | - backgroundColor: 'hsla(215, 57%, 60%, 1)', |
| 297 | + backgroundColor: 'hsla(215, 57%, 60%, 1)', |
| 298 | + |
238 | 299 | borderColor: 'hsla(215, 57%, 60%, 1)', |
239 | | - |
240 | 300 | data |
241 | 301 | }] |
242 | 302 | }, |
243 | 303 | options: { |
267 | 327 | |
268 | 328 | yAxes: [{ |
269 | 329 | ticks: { |
270 | 330 | min: 0, |
271 | | - max: Math.max(localMax, 10), |
| 331 | + suggestedMax: 10, |
| 332 | + |
272 | 333 | stepSize: 5 |
273 | 334 | } |
274 | 335 | }] |
275 | 336 | }, |
276 | 337 | animation: { |
277 | | - |
| 338 | + |
278 | 339 | } |
279 | 340 | } |
280 | 341 | } |
281 | 342 | } |