Files: 0d44fc79a12ebf427d26a10693fcfce4fa2e7451 / app / page / statsShow.js
7959 bytesRaw
1 | const nest = require('depnest') |
2 | const { h, resolve, when, Value, Struct, Array: MutantArray, Dict, onceTrue, map, computed, dictToCollection, throttle } = require('mutant') |
3 | const pull = require('pull-stream') |
4 | const marksum = require('markdown-summary') |
5 | const Chart = require('chart.js') |
6 | const groupBy = require('lodash/groupBy') |
7 | const merge = require('lodash/merge') |
8 | |
9 | exports.gives = nest('app.page.statsShow') |
10 | |
11 | exports.needs = nest({ |
12 | 'sbot.obs.connection': 'first', |
13 | 'history.sync.push': 'first' |
14 | }) |
15 | |
16 | exports.create = (api) => { |
17 | return nest('app.page.statsShow', statsShow) |
18 | |
19 | function statsShow (location) { |
20 | var store = Struct({ |
21 | blogs: MutantArray([]), |
22 | comments: Dict(), |
23 | likes: Dict() |
24 | }) |
25 | |
26 | var howFarBack = Value(0) |
27 | // stats show a moving window of 30 days |
28 | const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000 |
29 | |
30 | // TODO |
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 | } |
37 | }) |
38 | |
39 | var commentsAll = computed(throttle(dictToCollection(store.comments), 1000), (comments) => { |
40 | return comments |
41 | .map(c => c.value) |
42 | .reduce((n, sofar) => [...n, ...sofar], []) |
43 | }) |
44 | |
45 | // this should perhaps be reduced to just return commentsContextCount |
46 | var visibleComments = computed([commentsAll, range], (comments, range) => { |
47 | return comments |
48 | .filter(msg => { |
49 | const ts = msg.value.timestamp |
50 | return ts >= range.lower && ts <= range.upper |
51 | }) |
52 | }) |
53 | |
54 | var rangeLikes = computed([throttle(dictToCollection(store.likes), 1000), range], (likes, range) => { |
55 | return likes |
56 | .map(c => c.value) |
57 | .reduce((n, sofar) => [...n, ...sofar], []) |
58 | // .filter(msg => { |
59 | // const ts = msg.value.timestamp |
60 | // return ts >= range.lower && ts <= range.upper |
61 | // }) |
62 | }) |
63 | |
64 | onceTrue(api.sbot.obs.connection, server => { |
65 | fetchBlogs({ server, store }) |
66 | |
67 | // const query = { |
68 | // gt: ['C', null, range().lower], |
69 | // lt: ['C', undefined, range().upper], |
70 | // reverse: true, |
71 | // values: true, |
72 | // keys: false, |
73 | // seqs: false |
74 | // } |
75 | // console.log('test query', query) |
76 | // pull(server.blogStats.read(query), pull.log(() => console.log('DONE'))) |
77 | }) |
78 | const canvas = h('canvas', { height: 200, width: 600, style: { height: '200px', width: '600px' } }) |
79 | |
80 | const page = h('Page -statsShow', [ |
81 | h('Scroller.content', [ |
82 | h('div.content', [ |
83 | h('h1', 'Stats'), |
84 | 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 | ]) |
97 | ]), |
98 | h('section.graph', [ |
99 | canvas, |
100 | h('div.changeRange', [ |
101 | '< ', |
102 | h('a', { 'ev-click': () => howFarBack.set(howFarBack() + 1) }, 'Prev 30 days'), |
103 | ' | ', |
104 | when(howFarBack, |
105 | h('a', { 'ev-click': () => howFarBack.set(howFarBack() - 1) }, 'Next 30 days'), |
106 | h('span', 'Next 30 days') |
107 | ), |
108 | ' >' |
109 | ]) |
110 | ]), |
111 | h('table.blogs', [ |
112 | h('thead', [ |
113 | h('tr', [ |
114 | h('th.details'), |
115 | h('th.comment', 'Comments'), |
116 | h('th.likes', 'Likes') |
117 | ]) |
118 | ]), |
119 | h('tbody', map(store.blogs, blog => h('tr.blog', { id: blog.key }, [ |
120 | h('td.details', [ |
121 | h('div.title', {}, getTitle(blog)), |
122 | h('a', |
123 | { |
124 | href: '#', |
125 | 'ev-click': viewBlog(blog) |
126 | }, |
127 | 'View blog' |
128 | ) |
129 | ]), |
130 | h('td.comments', computed(store.comments.get(blog.key), msgs => msgs ? msgs.length : 0)), |
131 | h('td.likes', computed(store.likes.get(blog.key), msgs => msgs ? msgs.length : 0)) |
132 | // ]), { comparer: (a, b) => a === b })) |
133 | ]))) |
134 | ]) |
135 | ]) |
136 | ]) |
137 | ]) |
138 | |
139 | // Chart.scaleService.updateScaleDefaults('linear', { |
140 | // ticks: { min: 0 } |
141 | // }) |
142 | var chart = new Chart(canvas.getContext('2d'), chartConfig({ range, chartData: [] })) |
143 | |
144 | const toDay = ts => Math.floor(ts / (24 * 60 * 60 * 1000)) |
145 | |
146 | // TODO take in context (comments/ likes / shares) |
147 | const chartData = computed(commentsAll, msgs => { |
148 | const grouped = groupBy(msgs, m => toDay(m.value.timestamp)) |
149 | |
150 | var data = Object.keys(grouped) |
151 | .map(day => { |
152 | return { |
153 | t: day * 24 * 60 * 60 * 1000, |
154 | y: grouped[day].length |
155 | } |
156 | }) |
157 | return data |
158 | }) |
159 | |
160 | chartData(() => { |
161 | chart = merge(chart, chartConfig({ range, chartData })) |
162 | chart.update() |
163 | }) |
164 | |
165 | range(() => { |
166 | chart = merge(chart, chartConfig({ range, chartData })) |
167 | chart.update() |
168 | }) |
169 | |
170 | return page |
171 | } |
172 | |
173 | function viewBlog (blog) { |
174 | return () => api.history.sync.push(blog) |
175 | } |
176 | } |
177 | |
178 | function getTitle (blog) { |
179 | if (blog.value.content.title) return blog.value.content.title |
180 | else if (blog.value.content.text) return marksum.title(blog.value.content.text) |
181 | else return blog.key |
182 | } |
183 | |
184 | function fetchBlogs ({ server, store }) { |
185 | pull( |
186 | server.blogStats.readBlogs({ reverse: false }), |
187 | pull.drain(blog => { |
188 | store.blogs.push(blog) |
189 | |
190 | fetchComments({ server, store, blog }) |
191 | fetchLikes({ server, store, blog }) |
192 | }) |
193 | ) |
194 | } |
195 | |
196 | function fetchComments ({ server, store, blog }) { |
197 | if (!store.comments.has(blog.key)) store.comments.put(blog.key, MutantArray()) |
198 | |
199 | pull( |
200 | server.blogStats.readComments(blog), |
201 | pull.drain(msg => { |
202 | store.comments.get(blog.key).push(msg) |
203 | // TODO remove my comments from count? |
204 | }) |
205 | ) |
206 | } |
207 | |
208 | function fetchLikes ({ server, store, blog }) { |
209 | if (!store.likes.has(blog.key)) store.likes.put(blog.key, MutantArray()) |
210 | |
211 | pull( |
212 | server.blogStats.readLikes(blog), |
213 | pull.drain(msg => { |
214 | store.likes.get(blog.key).push(msg) |
215 | // TODO this needs reducing... like + unlike are muddled in here |
216 | // find any thing by same author |
217 | // if exists - over-write or delete |
218 | }) |
219 | ) |
220 | } |
221 | |
222 | function chartConfig ({ range, chartData }) { |
223 | const { lower, upper } = resolve(range) |
224 | |
225 | const data = resolve(chartData) || [] |
226 | const slice = data |
227 | .filter(d => d.t >= lower && d.t <= upper) |
228 | .map(d => d.y) |
229 | .sort((a, b) => a < b) |
230 | const localMax = slice[0] ? Math.max(slice[0], 10) : 10 |
231 | |
232 | return { |
233 | type: 'bar', |
234 | data: { |
235 | datasets: [{ |
236 | // label: 'My First dataset', |
237 | backgroundColor: 'hsla(215, 57%, 60%, 1)', // 'hsla(215, 57%, 43%, 1)', |
238 | borderColor: 'hsla(215, 57%, 60%, 1)', |
239 | // TODO set initial data as empty to make a good range |
240 | data |
241 | }] |
242 | }, |
243 | options: { |
244 | legend: { |
245 | display: false |
246 | }, |
247 | scales: { |
248 | xAxes: [{ |
249 | type: 'time', |
250 | distribution: 'linear', |
251 | time: { |
252 | unit: 'day', |
253 | min: new Date(lower), |
254 | max: new Date(upper), |
255 | tooltipFormat: 'MMMM D', |
256 | stepSize: 7 |
257 | }, |
258 | bounds: 'ticks', |
259 | ticks: { |
260 | // maxTicksLimit: 4 |
261 | }, |
262 | gridLines: { |
263 | display: false |
264 | }, |
265 | maxBarThickness: 20 |
266 | }], |
267 | |
268 | yAxes: [{ |
269 | ticks: { |
270 | min: 0, |
271 | max: Math.max(localMax, 10), |
272 | stepSize: 5 |
273 | } |
274 | }] |
275 | }, |
276 | animation: { |
277 | // duration: 300 |
278 | } |
279 | } |
280 | } |
281 | } |
282 |
Built with git-ssb-web