Skip to content

Commit 889a911

Browse files
authored
feat: support weekly/monthly view in barchart visualization (#292)
1 parent 4d8e5ee commit 889a911

File tree

10 files changed

+390
-180
lines changed

10 files changed

+390
-180
lines changed

src/components/Header.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ div(:class="{'fixed-top-padding': fixedTopMenu}")
5656
b-dropdown-item(to="/search")
5757
icon(name="search")
5858
| Search
59+
b-dropdown-item(to="/trends")
60+
icon(name="chart-bar")
61+
| Trends
5962
b-dropdown-item(to="/query")
6063
icon(name="code")
6164
| Query
@@ -84,10 +87,13 @@ import 'vue-awesome/icons/stream';
8487
import 'vue-awesome/icons/database';
8588
import 'vue-awesome/icons/search';
8689
import 'vue-awesome/icons/code';
90+
import 'vue-awesome/icons/chart-bar';
8791
import 'vue-awesome/icons/stopwatch';
8892
import 'vue-awesome/icons/cog';
8993
import 'vue-awesome/icons/tools';
9094
95+
import 'vue-awesome/icons/ellipsis-h';
96+
9197
import 'vue-awesome/icons/mobile';
9298
import 'vue-awesome/icons/desktop';
9399

src/components/SelectableVisualization.vue

Lines changed: 9 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ div
7070
div(v-if="type == 'category_sunburst'")
7171
aw-sunburst-categories(:data="top_categories_hierarchy", style="height: 20em")
7272
div(v-if="type == 'timeline_barchart'")
73-
aw-timeline-barchart(:datasets="datasets", style="height: 100")
73+
aw-timeline-barchart(:datasets="datasets", :resolution="$store.state.activity.query_options.timeperiod.length[1]", style="height: 100")
7474
div(v-if="type == 'sunburst_clock'")
7575
aw-sunburst-clock(:date="date", :afkBucketId="$store.state.activity.buckets.afk[0]", :windowBucketId="$store.state.activity.buckets.window[0]")
7676
</template>
@@ -93,11 +93,10 @@ import 'vue-awesome/icons/cog';
9393
import 'vue-awesome/icons/times';
9494
import 'vue-awesome/icons/bars';
9595
96-
import { split_by_hour_into_data } from '~/util/transforms';
96+
import { buildBarchartDataset } from '~/util/datasets';
9797
9898
// TODO: Move this somewhere else
9999
import { build_category_hierarchy } from '~/util/classes';
100-
import { getColorFromCategory } from '~/util/color';
101100
102101
function pick_subname_as_name(c) {
103102
c.name = c.subname;
@@ -209,7 +208,7 @@ export default {
209208
return this.visualizations[this.type].available;
210209
},
211210
supports_period: function () {
212-
if (this.type == 'sunburst_clock' || this.type == 'timeline_barchart') {
211+
if (this.type == 'sunburst_clock') {
213212
return this.isSingleDay;
214213
}
215214
return true;
@@ -230,51 +229,12 @@ export default {
230229
}
231230
},
232231
datasets: function () {
233-
const METHOD_CATEGORY = 'category';
234-
const METHOD_ACTIVITY = 'activity';
235-
const method = METHOD_CATEGORY;
236-
if (method == METHOD_CATEGORY) {
237-
const SEP = '>>>';
238-
const data = this.$store.state.activity.category.by_hour;
239-
if (data) {
240-
const categories = new Set(
241-
Object.values(data)
242-
.map(result => {
243-
return result.cat_events.map(e => e.data['$category'].join(SEP));
244-
})
245-
.flat()
246-
);
247-
const ds = [...categories].map(c_ => {
248-
const c = this.$store.getters['categories/get_category'](c_.split(SEP));
249-
if (c) {
250-
return {
251-
label: c.name.join(' > '),
252-
backgroundColor: getColorFromCategory(c, this.$store.state.categories.classes),
253-
data: Object.values(data).map(results => {
254-
const cat = results.cat_events.find(e => _.isEqual(e.data['$category'], c.name));
255-
if (cat) return Math.round((cat.duration / (60 * 60)) * 1000) / 1000;
256-
else return null;
257-
}),
258-
};
259-
} else {
260-
console.log('missing c');
261-
}
262-
});
263-
return ds;
264-
} else {
265-
return [];
266-
}
267-
} else if (method == METHOD_ACTIVITY) {
268-
const data = split_by_hour_into_data(this.$store.state.activity.active.events);
269-
return [
270-
{
271-
label: 'Total time',
272-
backgroundColor: '#6699ff',
273-
data,
274-
},
275-
];
276-
}
277-
return [];
232+
return buildBarchartDataset(
233+
this.$store,
234+
this.$store.state.activity.category.by_period,
235+
this.$store.state.activity.active.events,
236+
this.$store.state.categories.classes
237+
);
278238
},
279239
date: function () {
280240
let date = this.$store.state.activity.query_options.date;

src/route.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const Buckets = () => import('./views/Buckets.vue');
1111
const Bucket = () => import('./views/Bucket.vue');
1212
const QueryExplorer = () => import('./views/QueryExplorer.vue');
1313
const Timeline = () => import('./views/Timeline.vue');
14+
const Trends = () => import('./views/Trends.vue');
1415
const Settings = () => import('./views/settings/Settings.vue');
1516
const Stopwatch = () => import('./views/Stopwatch.vue');
1617
const Search = () => import('./views/Search.vue');
@@ -51,6 +52,7 @@ const router = new VueRouter({
5152
{ path: '/buckets', component: Buckets },
5253
{ path: '/buckets/:id', component: Bucket, props: true },
5354
{ path: '/timeline', component: Timeline, meta: { fullContainer: true } },
55+
{ path: '/trends', component: Trends, meta: { fullContainer: true } },
5456
{ path: '/query', component: QueryExplorer },
5557
{ path: '/settings', component: Settings },
5658
{ path: '/stopwatch', component: Stopwatch },

src/store/modules/activity.ts

Lines changed: 64 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import moment from 'moment';
2-
import { unitOfTime } from 'moment';
32
import * as _ from 'lodash';
43
import { map, filter, values, groupBy, sortBy, flow, reverse } from 'lodash/fp';
54

@@ -8,23 +7,14 @@ import queries from '~/queries';
87
import { getColorFromCategory } from '~/util/color';
98
import { loadClassesForQuery } from '~/util/classes';
109
import { get_day_start_with_offset } from '~/util/time';
11-
12-
interface TimePeriod {
13-
start: string;
14-
length: [number, string];
15-
}
16-
17-
function dateToTimeperiod(date: string, duration?: [number, string]): TimePeriod {
18-
return { start: get_day_start_with_offset(date), length: duration || [1, 'day'] };
19-
}
20-
21-
function timeperiodToStr(tp: TimePeriod): string {
22-
const start = moment(tp.start).format();
23-
const end = moment(start)
24-
.add(tp.length[0], tp.length[1] as moment.unitOfTime.DurationConstructor)
25-
.format();
26-
return [start, end].join('/');
27-
}
10+
import {
11+
TimePeriod,
12+
dateToTimeperiod,
13+
timeperiodToStr,
14+
timeperiodsHoursOfPeriod,
15+
timeperiodsDaysOfPeriod,
16+
timeperiodsAroundTimeperiod,
17+
} from '~/util/timeperiod';
2818

2919
interface QueryOptions {
3020
host: string;
@@ -64,7 +54,7 @@ const _state = {
6454

6555
category: {
6656
available: false,
67-
by_hour: [],
57+
by_period: [],
6858
top: [],
6959
},
7060

@@ -96,32 +86,12 @@ const _state = {
9686
},
9787
};
9888

99-
function timeperiodsAroundTimeperiod(timeperiod: TimePeriod): TimePeriod[] {
100-
const periods = [];
101-
for (let i = -15; i <= 15; i++) {
102-
const start = moment(timeperiod.start)
103-
.add(i * timeperiod.length[0], timeperiod.length[1] as moment.unitOfTime.DurationConstructor)
104-
.format();
105-
periods.push({ ...timeperiod, start });
106-
}
107-
return periods;
89+
function timeperiodsStrsHoursOfPeriod(timeperiod: TimePeriod): string[] {
90+
return timeperiodsHoursOfPeriod(timeperiod).map(timeperiodToStr);
10891
}
10992

110-
function timeperiodsHoursOfDay(timeperiod: TimePeriod): TimePeriod[] {
111-
const periods = [];
112-
const _length: [number, string] = [1, 'hour'];
113-
for (let i = 0; i < 24; i++) {
114-
const start = moment(timeperiod.start)
115-
.add(i * _length[0], _length[1] as moment.unitOfTime.DurationConstructor)
116-
.format();
117-
periods.push({ start, length: _length });
118-
}
119-
// const periods = _.range(24).map(i => [TimePeriod(moment(i * 1 + dayOffset), [1, 'hour'])]);
120-
return periods;
121-
}
122-
123-
function timeperiodsStrsHoursOfDay(timeperiod: TimePeriod): string[] {
124-
return timeperiodsHoursOfDay(timeperiod).map(timeperiodToStr);
93+
function timeperiodsStrsDaysOfPeriod(timeperiod: TimePeriod): string[] {
94+
return timeperiodsDaysOfPeriod(timeperiod).map(timeperiodToStr);
12595
}
12696

12797
function timeperiodStrsAroundTimeperiod(timeperiod: TimePeriod): string[] {
@@ -162,15 +132,15 @@ const actions = {
162132

163133
if (state.window.available) {
164134
await dispatch('query_desktop_full', query_options);
165-
await dispatch('query_category_time_by_hour', query_options);
135+
await dispatch('query_category_time_by_period', query_options);
166136
} else if (state.android.available) {
167137
await dispatch('query_android', query_options);
168138
} else {
169139
console.log(
170140
'Cannot query windows as we are missing either an afk/window bucket pair or an android bucket'
171141
);
172-
await dispatch('query_window_empty', query_options);
173-
await dispatch('query_browser_empty', query_options);
142+
await dispatch('reset_window');
143+
await dispatch('reset_category');
174144
}
175145

176146
if (state.active.available) {
@@ -186,7 +156,7 @@ const actions = {
186156
await dispatch('query_editor', query_options);
187157
} else {
188158
console.log('Cannot call query_editor as we do not have any editor buckets');
189-
await dispatch('query_editor_empty', query_options);
159+
await dispatch('reset_editor');
190160
}
191161
} else {
192162
console.warn(
@@ -203,7 +173,14 @@ const actions = {
203173
commit('query_window_completed', data[0]);
204174
},
205175

206-
async query_window_empty({ commit }) {
176+
async reset({ dispatch }) {
177+
await dispatch('reset_window');
178+
await dispatch('reset_browser');
179+
await dispatch('reset_editor');
180+
await dispatch('reset_category');
181+
},
182+
183+
async reset_window({ commit }) {
207184
const data = {
208185
app_events: [],
209186
title_events: [],
@@ -214,6 +191,32 @@ const actions = {
214191
commit('query_window_completed', data);
215192
},
216193

194+
async reset_browser({ commit }) {
195+
const data = {
196+
domains: [],
197+
urls: [],
198+
duration: 0,
199+
};
200+
commit('query_browser_completed', data);
201+
},
202+
203+
async reset_editor({ commit }) {
204+
const data = {
205+
files: [],
206+
projects: [],
207+
languages: [],
208+
};
209+
commit('query_editor_completed', data);
210+
},
211+
212+
async reset_category({ commit }) {
213+
const data = {
214+
by_period: [],
215+
};
216+
217+
commit('query_category_time_by_period_completed', data);
218+
},
219+
217220
async query_desktop_full(
218221
{ state, commit, rootState, rootGetters },
219222
{ timeperiod, filterCategories, filterAFK, includeAudible }: QueryOptions
@@ -245,31 +248,13 @@ const actions = {
245248
commit('query_browser_completed', data_browser);
246249
},
247250

248-
async query_browser_empty({ commit }) {
249-
const data = {
250-
domains: [],
251-
urls: [],
252-
duration: 0,
253-
};
254-
commit('query_browser_completed', data);
255-
},
256-
257251
async query_editor({ state, commit }, { timeperiod }) {
258252
const periods = [timeperiodToStr(timeperiod)];
259253
const q = queries.editorActivityQuery(state.buckets.editor);
260254
const data = await this._vm.$aw.query(periods, q);
261255
commit('query_editor_completed', data[0]);
262256
},
263257

264-
async query_editor_empty({ commit }) {
265-
const data = {
266-
files: [],
267-
projects: [],
268-
languages: [],
269-
};
270-
commit('query_editor_completed', data);
271-
},
272-
273258
async query_active_history({ commit, state }, { timeperiod }: QueryOptions) {
274259
const periods = timeperiodStrsAroundTimeperiod(timeperiod).filter(tp_str => {
275260
return !_.includes(state.active.history, tp_str);
@@ -285,13 +270,20 @@ const actions = {
285270
commit('query_active_history_completed', { active_history });
286271
},
287272

288-
async query_category_time_by_hour(
273+
async query_category_time_by_period(
289274
{ commit, state },
290275
{ timeperiod, filterCategories, filterAFK }: QueryOptions
291276
) {
292277
// TODO: Only works for the 1 day timeperiod
293278
// TODO: Needs to be adapted for Android
294-
const periods = timeperiodsStrsHoursOfDay(timeperiod);
279+
let periods;
280+
if (timeperiod.length[1] == 'day') {
281+
periods = timeperiodsStrsHoursOfPeriod(timeperiod);
282+
} else if (timeperiod.length[1] == 'week' || timeperiod.length[1] == 'month') {
283+
periods = timeperiodsStrsDaysOfPeriod(timeperiod);
284+
} else {
285+
console.error('Unknown timeperiod');
286+
}
295287
const classes = loadClassesForQuery();
296288
const data = await this._vm.$aw.query(
297289
periods,
@@ -307,8 +299,7 @@ const actions = {
307299
filter_classes: filterCategories,
308300
})
309301
);
310-
const category_time_by_hour = _.zipObject(periods, data);
311-
commit('query_category_time_by_hour_completed', { category_time_by_hour });
302+
commit('query_category_time_by_period_completed', { by_period: _.zipObject(periods, data) });
312303
},
313304

314305
async query_active_history_android({ commit, state }, { timeperiod }: QueryOptions) {
@@ -451,6 +442,7 @@ const mutations = {
451442
state.editor.top_projects = null;
452443

453444
state.category.top = null;
445+
state.category.by_period = null;
454446

455447
state.active.duration = null;
456448

@@ -503,8 +495,8 @@ const mutations = {
503495
};
504496
},
505497

506-
query_category_time_by_hour_completed(state, { category_time_by_hour }) {
507-
state.category.by_hour = category_time_by_hour;
498+
query_category_time_by_period_completed(state, { by_period }) {
499+
state.category.by_period = by_period;
508500
},
509501

510502
buckets(state, data) {

0 commit comments

Comments
 (0)