git ssb

2+

mixmix / ticktack



Tree: 0d44fc79a12ebf427d26a10693fcfce4fa2e7451

Files: 0d44fc79a12ebf427d26a10693fcfce4fa2e7451 / app / page / statsShow.js

7959 bytesRaw
1const nest = require('depnest')
2const { h, resolve, when, Value, Struct, Array: MutantArray, Dict, onceTrue, map, computed, dictToCollection, throttle } = require('mutant')
3const pull = require('pull-stream')
4const marksum = require('markdown-summary')
5const Chart = require('chart.js')
6const groupBy = require('lodash/groupBy')
7const merge = require('lodash/merge')
8
9exports.gives = nest('app.page.statsShow')
10
11exports.needs = nest({
12 'sbot.obs.connection': 'first',
13 'history.sync.push': 'first'
14})
15
16exports.create = (api) => {
17 return nest('app.page.statsShow', statsShow)
18
19 function statsShow (location) {
20 var store = Struct({
21 blogs: MutantArray([]),
22 comments: Dict(),
23 likes: Dict()
24 })
25
26 var howFarBack = Value(0)
27 // stats show a moving window of 30 days
28 const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000
29
30 // TODO
31 var range = computed([howFarBack], howFarBack => {
32 const now = Date.now()
33 return {
34 upper: now - howFarBack * THIRTY_DAYS,
35 lower: now - (howFarBack + 1) * THIRTY_DAYS
36 }
37 })
38
39 var commentsAll = computed(throttle(dictToCollection(store.comments), 1000), (comments) => {
40 return comments
41 .map(c => c.value)
42 .reduce((n, sofar) => [...n, ...sofar], [])
43 })
44
45 // this should perhaps be reduced to just return commentsContextCount
46 var visibleComments = computed([commentsAll, range], (comments, range) => {
47 return comments
48 .filter(msg => {
49 const ts = msg.value.timestamp
50 return ts >= range.lower && ts <= range.upper
51 })
52 })
53
54 var rangeLikes = computed([throttle(dictToCollection(store.likes), 1000), range], (likes, range) => {
55 return likes
56 .map(c => c.value)
57 .reduce((n, sofar) => [...n, ...sofar], [])
58 // .filter(msg => {
59 // const ts = msg.value.timestamp
60 // return ts >= range.lower && ts <= range.upper
61 // })
62 })
63
64 onceTrue(api.sbot.obs.connection, server => {
65 fetchBlogs({ server, store })
66
67 // const query = {
68 // gt: ['C', null, range().lower],
69 // lt: ['C', undefined, range().upper],
70 // reverse: true,
71 // values: true,
72 // keys: false,
73 // seqs: false
74 // }
75 // console.log('test query', query)
76 // pull(server.blogStats.read(query), pull.log(() => console.log('DONE')))
77 })
78 const canvas = h('canvas', { height: 200, width: 600, style: { height: '200px', width: '600px' } })
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 h('div.count', computed(visibleComments, msgs => msgs.length)),
87 h('strong', 'Comments'),
88 '(30 days)'
89 ]),
90 h('div.likes', [
91 h('div.count', computed(rangeLikes, msgs => msgs.length)),
92 h('strong', 'Likes'),
93 '(30 days)'
94 ]),
95 h('div.shares', [
96 ])
97 ]),
98 h('section.graph', [
99 canvas,
100 h('div.changeRange', [
101 '< ',
102 h('a', { 'ev-click': () => howFarBack.set(howFarBack() + 1) }, 'Prev 30 days'),
103 ' | ',
104 when(howFarBack,
105 h('a', { 'ev-click': () => howFarBack.set(howFarBack() - 1) }, 'Next 30 days'),
106 h('span', 'Next 30 days')
107 ),
108 ' >'
109 ])
110 ]),
111 h('table.blogs', [
112 h('thead', [
113 h('tr', [
114 h('th.details'),
115 h('th.comment', 'Comments'),
116 h('th.likes', 'Likes')
117 ])
118 ]),
119 h('tbody', map(store.blogs, blog => h('tr.blog', { id: blog.key }, [
120 h('td.details', [
121 h('div.title', {}, getTitle(blog)),
122 h('a',
123 {
124 href: '#',
125 'ev-click': viewBlog(blog)
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 // ]), { comparer: (a, b) => a === b }))
133 ])))
134 ])
135 ])
136 ])
137 ])
138
139 // Chart.scaleService.updateScaleDefaults('linear', {
140 // ticks: { min: 0 }
141 // })
142 var chart = new Chart(canvas.getContext('2d'), chartConfig({ range, chartData: [] }))
143
144 const toDay = ts => Math.floor(ts / (24 * 60 * 60 * 1000))
145
146 // TODO take in context (comments/ likes / shares)
147 const chartData = computed(commentsAll, msgs => {
148 const grouped = groupBy(msgs, m => toDay(m.value.timestamp))
149
150 var data = Object.keys(grouped)
151 .map(day => {
152 return {
153 t: day * 24 * 60 * 60 * 1000,
154 y: grouped[day].length
155 }
156 })
157 return data
158 })
159
160 chartData(() => {
161 chart = merge(chart, chartConfig({ range, chartData }))
162 chart.update()
163 })
164
165 range(() => {
166 chart = merge(chart, chartConfig({ range, chartData }))
167 chart.update()
168 })
169
170 return page
171 }
172
173 function viewBlog (blog) {
174 return () => api.history.sync.push(blog)
175 }
176}
177
178function getTitle (blog) {
179 if (blog.value.content.title) return blog.value.content.title
180 else if (blog.value.content.text) return marksum.title(blog.value.content.text)
181 else return blog.key
182}
183
184function fetchBlogs ({ server, store }) {
185 pull(
186 server.blogStats.readBlogs({ reverse: false }),
187 pull.drain(blog => {
188 store.blogs.push(blog)
189
190 fetchComments({ server, store, blog })
191 fetchLikes({ server, store, blog })
192 })
193 )
194}
195
196function fetchComments ({ server, store, blog }) {
197 if (!store.comments.has(blog.key)) store.comments.put(blog.key, MutantArray())
198
199 pull(
200 server.blogStats.readComments(blog),
201 pull.drain(msg => {
202 store.comments.get(blog.key).push(msg)
203 // TODO remove my comments from count?
204 })
205 )
206}
207
208function fetchLikes ({ server, store, blog }) {
209 if (!store.likes.has(blog.key)) store.likes.put(blog.key, MutantArray())
210
211 pull(
212 server.blogStats.readLikes(blog),
213 pull.drain(msg => {
214 store.likes.get(blog.key).push(msg)
215 // TODO this needs reducing... like + unlike are muddled in here
216 // find any thing by same author
217 // if exists - over-write or delete
218 })
219 )
220}
221
222function chartConfig ({ range, chartData }) {
223 const { lower, upper } = resolve(range)
224
225 const data = resolve(chartData) || []
226 const slice = data
227 .filter(d => d.t >= lower && d.t <= upper)
228 .map(d => d.y)
229 .sort((a, b) => a < b)
230 const localMax = slice[0] ? Math.max(slice[0], 10) : 10
231
232 return {
233 type: 'bar',
234 data: {
235 datasets: [{
236 // label: 'My First dataset',
237 backgroundColor: 'hsla(215, 57%, 60%, 1)', // 'hsla(215, 57%, 43%, 1)',
238 borderColor: 'hsla(215, 57%, 60%, 1)',
239 // TODO set initial data as empty to make a good range
240 data
241 }]
242 },
243 options: {
244 legend: {
245 display: false
246 },
247 scales: {
248 xAxes: [{
249 type: 'time',
250 distribution: 'linear',
251 time: {
252 unit: 'day',
253 min: new Date(lower),
254 max: new Date(upper),
255 tooltipFormat: 'MMMM D',
256 stepSize: 7
257 },
258 bounds: 'ticks',
259 ticks: {
260 // maxTicksLimit: 4
261 },
262 gridLines: {
263 display: false
264 },
265 maxBarThickness: 20
266 }],
267
268 yAxes: [{
269 ticks: {
270 min: 0,
271 max: Math.max(localMax, 10),
272 stepSize: 5
273 }
274 }]
275 },
276 animation: {
277 // duration: 300
278 }
279 }
280 }
281}
282

Built with git-ssb-web