git ssb

2+

mixmix / ticktack



Tree: cc9b1a7a5f4f79d8e0f7c4a2491a50f440ac9153

Files: cc9b1a7a5f4f79d8e0f7c4a2491a50f440ac9153 / app / page / statsShow.js

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

Built with git-ssb-web