git ssb

2+

mixmix / ticktack



Tree: 906d2b16d835447b38891af55f5b7b58dcbed9c9

Files: 906d2b16d835447b38891af55f5b7b58dcbed9c9 / app / page / statsShow.js

9517 bytesRaw
1const nest = require('depnest')
2const { h, resolve, when, Value, Struct, Array: MutantArray, Dict, onceTrue, map, computed, throttle, watchAll } = require('mutant')
3const pull = require('pull-stream')
4const marksum = require('markdown-summary')
5const Chart = require('chart.js')
6const groupBy = require('lodash/groupBy')
7const flatMap = require('lodash/flatMap')
8const get = require('lodash/get')
9
10exports.gives = nest('app.page.statsShow')
11
12exports.needs = nest({
13 'sbot.obs.connection': 'first',
14 'history.sync.push': 'first',
15 'message.html.markdown': 'first'
16})
17
18const COMMENTS = 'comments'
19const LIKES = 'likes'
20const SHARES = 'shares'
21const DAY = 24 * 60 * 60 * 1000
22
23const getRoot = {
24 [COMMENTS]: (msg) => get(msg, 'value.content.root'),
25 [LIKES]: (msg) => get(msg, 'value.content.vote.link')
26}
27
28exports.create = (api) => {
29 return nest('app.page.statsShow', statsShow)
30
31 function statsShow (location) {
32 var store = Struct({
33 blogs: MutantArray([]),
34 comments: Dict(),
35 likes: Dict(),
36 shares: Dict()
37 })
38 onceTrue(api.sbot.obs.connection, server => fetchBlogData({ server, store }))
39
40 var foci = Struct({
41 [COMMENTS]: computed([throttle(store.comments, 1000)], (msgs) => {
42 return flatMap(msgs, (val, key) => val)
43 }),
44 [LIKES]: computed([throttle(store.likes, 1000)], (msgs) => {
45 return flatMap(msgs, (val, key) => val)
46 }),
47 [SHARES]: []
48 })
49
50 var howFarBack = Value(0)
51 // stats show a moving window of 30 days
52 var context = Struct({
53 focus: Value(COMMENTS),
54 blog: Value(),
55 range: computed([howFarBack], howFarBack => {
56 const now = Date.now()
57 const endOfDay = (Math.floor(now / DAY) + 1) * DAY
58 return {
59 upper: endOfDay - howFarBack * 30 * DAY,
60 lower: endOfDay - (howFarBack + 1) * 30 * DAY
61 }
62 })
63 })
64
65 function totalOnscreenData (focus) {
66 return computed([foci[focus], context], (msgs, context) => {
67 const { range, blog } = context
68 return msgs
69 .filter(msg => {
70 if (blog && getRoot[focus](msg) !== blog) return false
71
72 const ts = msg.value.timestamp
73 return ts > range.lower && ts <= range.upper
74 })
75 .length
76 })
77 }
78
79 const canvas = h('canvas', { height: 200, width: 600, style: { height: '200px', width: '600px' } })
80
81 const page = h('Page -statsShow', [
82 h('Scroller.content', [
83 h('div.content', [
84 h('h1', 'Stats'),
85 h('section.totals', [COMMENTS, LIKES, SHARES].map(focus => {
86 return h('div',
87 {
88 classList: computed(context.focus, f => f === focus ? [focus, '-selected'] : [focus]),
89 'ev-click': () => context.focus.set(focus)
90 }, [
91 h('div.count', totalOnscreenData(focus)),
92 h('strong', focus),
93 '(30 days)'
94 ])
95 })),
96 h('section.graph', [
97 canvas,
98 h('div.changeRange', [
99 '< ',
100 h('a', { 'ev-click': () => howFarBack.set(howFarBack() + 1) }, 'Prev 30 days'),
101 ' | ',
102 when(howFarBack,
103 h('a', { 'ev-click': () => howFarBack.set(howFarBack() - 1) }, 'Next 30 days'),
104 h('span', 'Next 30 days')
105 ),
106 ' >'
107 ])
108 ]),
109 h('table.blogs', [
110 h('thead', [
111 h('tr', [
112 h('th.details'),
113 h('th.comments', 'Comments'),
114 h('th.likes', 'Likes'),
115 h('th.shares', 'Shares')
116 ])
117 ]),
118 h('tbody', map(store.blogs, BlogRow))
119 ])
120 ])
121 ])
122 ])
123
124 function BlogRow (blog) {
125 const className = computed(context.blog, b => {
126 if (!b) return ''
127 if (b !== blog.key) return '-background'
128 })
129
130 return h('tr.blog', { id: blog.key, className }, [
131 h('td.details', [
132 h('div.title', {
133 'ev-click': () => {
134 if (context.blog() === blog.key) context.blog.set('')
135 else context.blog.set(blog.key)
136 }
137 }, getTitle({ blog, mdRenderer: api.message.html.markdown })),
138 h('a', {
139 href: '#',
140 'ev-click': ev => {
141 ev.stopPropagation() // stop the click catcher!
142 api.history.sync.push(blog)
143 }
144 }, 'View blog')
145 ]),
146 h('td.comments', computed(store.comments.get(blog.key), msgs => msgs ? msgs.length : 0)),
147 h('td.likes', computed(store.likes.get(blog.key), msgs => msgs ? msgs.length : 0)),
148 h('td.shares', computed(store.shares.get(blog.key), msgs => msgs ? msgs.length : 0))
149 ])
150 }
151
152 initialiseChart({ canvas, context, foci })
153
154 return page
155 }
156}
157
158function getTitle ({ blog, mdRenderer }) {
159 if (blog.value.content.title) return blog.value.content.title
160 else if (blog.value.content.text) {
161 var md = mdRenderer(marksum.title(blog.value.content.text))
162 if (md && md.innerText) return md.innerText
163 }
164
165 return blog.key
166}
167
168function fetchBlogData ({ server, store }) {
169 pull(
170 server.blogStats.readBlogs({ reverse: false }),
171 pull.drain(blog => {
172 store.blogs.push(blog)
173
174 fetchComments({ server, store, blog })
175 fetchLikes({ server, store, blog })
176 })
177 )
178
179 function fetchComments ({ server, store, blog }) {
180 if (!store.comments.has(blog.key)) store.comments.put(blog.key, MutantArray())
181
182 pull(
183 server.blogStats.readComments(blog),
184 pull.drain(msg => {
185 store.comments.get(blog.key).push(msg)
186 // TODO remove my comments from count?
187 })
188 )
189 }
190
191 function fetchLikes ({ server, store, blog }) {
192 if (!store.likes.has(blog.key)) store.likes.put(blog.key, MutantArray())
193
194 pull(
195 server.blogStats.readLikes(blog),
196 pull.drain(msg => {
197 store.likes.get(blog.key).push(msg)
198 // TODO this needs reducing... like + unlike are muddled in here
199 // find any thing by same author
200 // if exists - over-write or delete
201 })
202 )
203 }
204}
205
206function initialiseChart ({ canvas, context, foci }) {
207 var chart = new Chart(canvas.getContext('2d'), chartConfig({ context }))
208
209 const chartData = computed([context, foci], (context, foci) => {
210 fixAnimationWhenNeeded(context)
211
212 const msgs = foci[context.focus]
213 .filter(msg => {
214 if (!context.blog) return true
215
216 return context.blog === getRoot[context.focus](msg)
217 })
218 const grouped = groupBy(msgs, m => toDay(m.value.timestamp))
219
220 return Object.keys(grouped)
221 .map(day => {
222 return {
223 t: day * DAY,
224 y: grouped[day].length
225 }
226 })
227 })
228
229 chartData(data => {
230 chart.data.datasets[0].data = data
231
232 chart.update()
233 })
234
235 watchAll([chartData, context.range], (data, range) => {
236 const { lower, upper } = range
237 const slice = data
238 .filter(d => d.t > lower && d.t <= upper)
239 .map(d => d.y)
240 .sort((a, b) => a < b)
241
242 var h = slice[0]
243 if (!h || h < 10) h = 10
244 else h = h + (5 - h % 5)
245 // set the height of the graph to a minimum or 10,
246 // or some multiple of 5 above the max height
247
248 chart.options.scales.yAxes[0].ticks.max = h
249
250 chart.update()
251 })
252
253 context.range(range => {
254 const { lower, upper } = range
255
256 chart.options.scales.xAxes[0].time.min = new Date(lower - DAY / 2)
257 chart.options.scales.xAxes[0].time.max = new Date(upper - DAY / 2)
258 // the squeezing in by DAY/2 is to stop data outside range from half showing
259
260 chart.update()
261 })
262
263 // ///// HELPERS /////
264
265 // HACK - if the focus has changed, then zero the data
266 // this prevents the graph from showing some confusing animations when transforming between foci / selecting blog
267 var prevFocus = context.focus()
268 var prevBlog = context.blog()
269 function fixAnimationWhenNeeded (context) {
270 if (context.focus !== prevFocus || context.blog !== prevBlog) {
271 chart.data.datasets[0].data = []
272 chart.update()
273 prevFocus = context.focus
274 prevBlog = context.blog
275 }
276 }
277 function toDay (ts) { return Math.floor(ts / DAY) }
278}
279
280// TODO rm chartData and other overly smart things which didn't work from here
281function chartConfig ({ context }) {
282 const { lower, upper } = resolve(context.range)
283
284 return {
285 type: 'bar',
286 data: {
287 datasets: [{
288 backgroundColor: 'hsla(215, 57%, 60%, 1)',
289 // Ticktack Primary color:'hsla(215, 57%, 43%, 1)',
290 borderColor: 'hsla(215, 57%, 60%, 1)',
291 data: []
292 }]
293 },
294 options: {
295 legend: {
296 display: false
297 },
298 scales: {
299 xAxes: [{
300 type: 'time',
301 distribution: 'linear',
302 time: {
303 unit: 'day',
304 min: new Date(lower - DAY / 2),
305 max: new Date(upper - DAY / 2),
306 tooltipFormat: 'MMMM D',
307 stepSize: 7
308 },
309 bounds: 'ticks',
310 ticks: {
311 // maxTicksLimit: 4
312 },
313 gridLines: {
314 display: false
315 },
316 maxBarThickness: 20
317 }],
318
319 yAxes: [{
320 ticks: {
321 min: 0,
322 suggestedMax: 10,
323 // max: Math.max(localMax, 10),
324 stepSize: 5
325 }
326 }]
327 },
328 animation: {
329 // duration: 300
330 }
331 }
332 }
333}
334

Built with git-ssb-web