git ssb

2+

mixmix / ticktack



Tree: 2e6dc4c90652f68f6fc799235fb4a887ecef021f

Files: 2e6dc4c90652f68f6fc799235fb4a887ecef021f / app / page / statsShow.js

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

Built with git-ssb-web