Files: 99cb8e2543b997e4f1db57956811f7789da47388 / app / page / statsShow.js
7804 bytesRaw
1 | const nest = require('depnest') |
2 | const { h, Value, Struct, Array: MutantArray, Dict, onceTrue, map, computed, dictToCollection, throttle } = require('mutant') |
3 | const pull = require('pull-stream') |
4 | const marksum = require('markdown-summary') |
5 | const Chart = require('chart.js') |
6 | const groupBy = require('lodash/groupBy') |
7 | |
8 | exports.gives = nest('app.page.statsShow') |
9 | |
10 | exports.needs = nest({ |
11 | 'sbot.obs.connection': 'first', |
12 | 'history.sync.push': 'first' |
13 | }) |
14 | |
15 | exports.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 | |
224 | function 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 | |
230 | function 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 | |
242 | function 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 | |
254 | function 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