git ssb

2+

mixmix / ticktack



Tree: e2016e7e072aaa1539f05778a0610bd68e812782

Files: e2016e7e072aaa1539f05778a0610bd68e812782 / app / page / statsShow.js

9775 bytesRaw
1const nest = require('depnest')
2const { h, resolve, when, Value, Struct, Array: MutantArray, Dict, onceTrue, map, computed, dictToCollection, 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 mergeWith = require('lodash/mergeWith')
8const flatMap = require('lodash/flatMap')
9
10exports.gives = nest('app.page.statsShow')
11
12exports.needs = nest({
13 'sbot.obs.connection': 'first',
14 'history.sync.push': 'first'
15})
16
17exports.create = (api) => {
18 return nest('app.page.statsShow', statsShow)
19
20 function statsShow (location) {
21 var store = Struct({
22 blogs: MutantArray([]),
23 comments: Dict(),
24 likes: Dict()
25 })
26 onceTrue(api.sbot.obs.connection, server => {
27 fetchBlogs({ server, store })
28 })
29
30 var howFarBack = Value(0)
31 // stats show a moving window of 30 days
32 const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000
33
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 })
46 })
47
48 var commentsAll = computed(throttle(dictToCollection(store.comments), 1000), (msgs) => {
49 return msgs
50 .map(c => c.value)
51 .reduce((n, sofar) => [...n, ...sofar], [])
52 })
53 // var commentsAll = computed([throttle(store.comments, 1000)], (msgs) => {
54 // return flatMap(msgs, (val, key) => {
55 // console.log(key, val)
56 // return val
57 // })
58 // })
59
60 var visibleCommentsCount = computed([commentsAll, context.range], (msgs, range) => {
61 return msgs
62 .filter(msg => {
63 const ts = msg.value.timestamp
64 return ts >= range.lower && ts <= range.upper
65 })
66 .length
67 })
68
69 var likesAll = computed(throttle(dictToCollection(store.likes), 1000), (msgs) => {
70 return msgs
71 .map(c => c.value)
72 .reduce((n, sofar) => [...n, ...sofar], [])
73 })
74
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 })
83
84 var focused = Struct({
85 [LIKES]: commentsAll,
86 [COMMENTS]: likesAll
87 })
88
89 const canvas = h('canvas', { height: 200, width: 600, style: { height: '200px', width: '600px' } })
90
91 const displayComments = () => context.focus.set(COMMENTS)
92 const displayLikes = () => context.focus.set(LIKES)
93
94 const page = h('Page -statsShow', [
95 h('Scroller.content', [
96 h('div.content', [
97 h('h1', 'Stats'),
98 h('section.totals', [
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 // 'ev-click': displayShares
122 }, [
123 // h('div.count', computed(rangeLikes, msgs => msgs.length)),
124 h('div.count', '--'),
125 h('strong', 'Shares'),
126 '(30 days)'
127 ]
128 )
129 ]),
130 h('section.graph', [
131 canvas,
132 h('div.changeRange', [
133 '< ',
134 h('a', { 'ev-click': () => howFarBack.set(howFarBack() + 1) }, 'Prev 30 days'),
135 ' | ',
136 when(howFarBack,
137 h('a', { 'ev-click': () => howFarBack.set(howFarBack() - 1) }, 'Next 30 days'),
138 h('span', 'Next 30 days')
139 ),
140 ' >'
141 ])
142 ]),
143 h('table.blogs', [
144 h('thead', [
145 h('tr', [
146 h('th.details'),
147 h('th.comment', 'Comments'),
148 h('th.likes', 'Likes')
149 ])
150 ]),
151 h('tbody', map(store.blogs, blog => h('tr.blog', { id: blog.key }, [
152 h('td.details', [
153 h('div.title', {}, getTitle(blog)),
154 h('a',
155 {
156 href: '#',
157 'ev-click': viewBlog(blog)
158 },
159 'View blog'
160 )
161 ]),
162 h('td.comments', computed(store.comments.get(blog.key), msgs => msgs ? msgs.length : 0)),
163 h('td.likes', computed(store.likes.get(blog.key), msgs => msgs ? msgs.length : 0))
164 // ]), { comparer: (a, b) => a === b }))
165 ])))
166 ])
167 ])
168 ])
169 ])
170
171 var chart = new Chart(canvas.getContext('2d'), chartConfig({ context, chartData: [] }))
172
173 const toDay = ts => Math.floor(ts / (24 * 60 * 60 * 1000))
174
175 // HACK - if the focus has changed, then zero the data
176 // this prevents the graph from showing some confusing animations when transforming between foci
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]
188 const grouped = groupBy(msgs, m => toDay(m.value.timestamp))
189
190 var data = Object.keys(grouped)
191 .map(day => {
192 return {
193 t: day * 24 * 60 * 60 * 1000,
194 y: grouped[day].length
195 }
196 })
197 return data
198 })
199
200 chartData(data => {
201 chart.data.datasets[0].data = data
202
203 chart.update()
204 })
205
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
219 chart.update()
220 })
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
231 return page
232 }
233
234 function viewBlog (blog) {
235 return () => api.history.sync.push(blog)
236 }
237}
238
239function getTitle (blog) {
240 if (blog.value.content.title) return blog.value.content.title
241 else if (blog.value.content.text) return marksum.title(blog.value.content.text)
242 else return blog.key
243}
244
245function fetchBlogs ({ server, store }) {
246 pull(
247 server.blogStats.readBlogs({ reverse: false }),
248 pull.drain(blog => {
249 store.blogs.push(blog)
250
251 fetchComments({ server, store, blog })
252 fetchLikes({ server, store, blog })
253 })
254 )
255}
256
257function fetchComments ({ server, store, blog }) {
258 if (!store.comments.has(blog.key)) store.comments.put(blog.key, MutantArray())
259
260 pull(
261 server.blogStats.readComments(blog),
262 pull.drain(msg => {
263 store.comments.get(blog.key).push(msg)
264 // TODO remove my comments from count?
265 })
266 )
267}
268
269function fetchLikes ({ server, store, blog }) {
270 if (!store.likes.has(blog.key)) store.likes.put(blog.key, MutantArray())
271
272 pull(
273 server.blogStats.readLikes(blog),
274 pull.drain(msg => {
275 store.likes.get(blog.key).push(msg)
276 // TODO this needs reducing... like + unlike are muddled in here
277 // find any thing by same author
278 // if exists - over-write or delete
279 })
280 )
281}
282
283function chartConfig ({ context, chartData }) {
284 const { lower, upper } = resolve(context.range)
285
286 const data = resolve(chartData) || []
287 const slice = data
288 .filter(d => d.t >= lower && d.t <= upper)
289 .map(d => d.y)
290 .sort((a, b) => a < b)
291 const localMax = slice[0] ? Math.max(slice[0], 10) : 10
292
293 return {
294 type: 'bar',
295 data: {
296 datasets: [{
297 backgroundColor: 'hsla(215, 57%, 60%, 1)',
298 // Ticktack Primary color:'hsla(215, 57%, 43%, 1)',
299 borderColor: 'hsla(215, 57%, 60%, 1)',
300 data
301 }]
302 },
303 options: {
304 legend: {
305 display: false
306 },
307 scales: {
308 xAxes: [{
309 type: 'time',
310 distribution: 'linear',
311 time: {
312 unit: 'day',
313 min: new Date(lower),
314 max: new Date(upper),
315 tooltipFormat: 'MMMM D',
316 stepSize: 7
317 },
318 bounds: 'ticks',
319 ticks: {
320 // maxTicksLimit: 4
321 },
322 gridLines: {
323 display: false
324 },
325 maxBarThickness: 20
326 }],
327
328 yAxes: [{
329 ticks: {
330 min: 0,
331 suggestedMax: 10,
332 // max: Math.max(localMax, 10),
333 stepSize: 5
334 }
335 }]
336 },
337 animation: {
338 // duration: 300
339 }
340 }
341 }
342}
343

Built with git-ssb-web