git ssb

2+

mixmix / ticktack



Tree: 157f35fd4dc9f9c94e9aec261e4b634b9f525e11

Files: 157f35fd4dc9f9c94e9aec261e4b634b9f525e11 / app / page / statsShow.js

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

Built with git-ssb-web