git ssb

2+

mixmix / ticktack



Tree: 99cb8e2543b997e4f1db57956811f7789da47388

Files: 99cb8e2543b997e4f1db57956811f7789da47388 / app / page / statsShow.js

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

Built with git-ssb-web