Skip to content

Commit

Permalink
feat: support weekly/monthly view in barchart visualization (#292)
Browse files Browse the repository at this point in the history
  • Loading branch information
ErikBjare committed Jun 10, 2021
1 parent 4d8e5ee commit 889a911
Show file tree
Hide file tree
Showing 10 changed files with 390 additions and 180 deletions.
6 changes: 6 additions & 0 deletions src/components/Header.vue
Expand Up @@ -56,6 +56,9 @@ div(:class="{'fixed-top-padding': fixedTopMenu}")
b-dropdown-item(to="/search")
icon(name="search")
| Search
b-dropdown-item(to="/trends")
icon(name="chart-bar")
| Trends
b-dropdown-item(to="/query")
icon(name="code")
| Query
Expand Down Expand Up @@ -84,10 +87,13 @@ import 'vue-awesome/icons/stream';
import 'vue-awesome/icons/database';
import 'vue-awesome/icons/search';
import 'vue-awesome/icons/code';
import 'vue-awesome/icons/chart-bar';
import 'vue-awesome/icons/stopwatch';
import 'vue-awesome/icons/cog';
import 'vue-awesome/icons/tools';
import 'vue-awesome/icons/ellipsis-h';
import 'vue-awesome/icons/mobile';
import 'vue-awesome/icons/desktop';
Expand Down
58 changes: 9 additions & 49 deletions src/components/SelectableVisualization.vue
Expand Up @@ -70,7 +70,7 @@ div
div(v-if="type == 'category_sunburst'")
aw-sunburst-categories(:data="top_categories_hierarchy", style="height: 20em")
div(v-if="type == 'timeline_barchart'")
aw-timeline-barchart(:datasets="datasets", style="height: 100")
aw-timeline-barchart(:datasets="datasets", :resolution="$store.state.activity.query_options.timeperiod.length[1]", style="height: 100")
div(v-if="type == 'sunburst_clock'")
aw-sunburst-clock(:date="date", :afkBucketId="$store.state.activity.buckets.afk[0]", :windowBucketId="$store.state.activity.buckets.window[0]")
</template>
Expand All @@ -93,11 +93,10 @@ import 'vue-awesome/icons/cog';
import 'vue-awesome/icons/times';
import 'vue-awesome/icons/bars';
import { split_by_hour_into_data } from '~/util/transforms';
import { buildBarchartDataset } from '~/util/datasets';
// TODO: Move this somewhere else
import { build_category_hierarchy } from '~/util/classes';
import { getColorFromCategory } from '~/util/color';
function pick_subname_as_name(c) {
c.name = c.subname;
Expand Down Expand Up @@ -209,7 +208,7 @@ export default {
return this.visualizations[this.type].available;
},
supports_period: function () {
if (this.type == 'sunburst_clock' || this.type == 'timeline_barchart') {
if (this.type == 'sunburst_clock') {
return this.isSingleDay;
}
return true;
Expand All @@ -230,51 +229,12 @@ export default {
}
},
datasets: function () {
const METHOD_CATEGORY = 'category';
const METHOD_ACTIVITY = 'activity';
const method = METHOD_CATEGORY;
if (method == METHOD_CATEGORY) {
const SEP = '>>>';
const data = this.$store.state.activity.category.by_hour;
if (data) {
const categories = new Set(
Object.values(data)
.map(result => {
return result.cat_events.map(e => e.data['$category'].join(SEP));
})
.flat()
);
const ds = [...categories].map(c_ => {
const c = this.$store.getters['categories/get_category'](c_.split(SEP));
if (c) {
return {
label: c.name.join(' > '),
backgroundColor: getColorFromCategory(c, this.$store.state.categories.classes),
data: Object.values(data).map(results => {
const cat = results.cat_events.find(e => _.isEqual(e.data['$category'], c.name));
if (cat) return Math.round((cat.duration / (60 * 60)) * 1000) / 1000;
else return null;
}),
};
} else {
console.log('missing c');
}
});
return ds;
} else {
return [];
}
} else if (method == METHOD_ACTIVITY) {
const data = split_by_hour_into_data(this.$store.state.activity.active.events);
return [
{
label: 'Total time',
backgroundColor: '#6699ff',
data,
},
];
}
return [];
return buildBarchartDataset(
this.$store,
this.$store.state.activity.category.by_period,
this.$store.state.activity.active.events,
this.$store.state.categories.classes
);
},
date: function () {
let date = this.$store.state.activity.query_options.date;
Expand Down
2 changes: 2 additions & 0 deletions src/route.js
Expand Up @@ -11,6 +11,7 @@ const Buckets = () => import('./views/Buckets.vue');
const Bucket = () => import('./views/Bucket.vue');
const QueryExplorer = () => import('./views/QueryExplorer.vue');
const Timeline = () => import('./views/Timeline.vue');
const Trends = () => import('./views/Trends.vue');
const Settings = () => import('./views/settings/Settings.vue');
const Stopwatch = () => import('./views/Stopwatch.vue');
const Search = () => import('./views/Search.vue');
Expand Down Expand Up @@ -51,6 +52,7 @@ const router = new VueRouter({
{ path: '/buckets', component: Buckets },
{ path: '/buckets/:id', component: Bucket, props: true },
{ path: '/timeline', component: Timeline, meta: { fullContainer: true } },
{ path: '/trends', component: Trends, meta: { fullContainer: true } },
{ path: '/query', component: QueryExplorer },
{ path: '/settings', component: Settings },
{ path: '/stopwatch', component: Stopwatch },
Expand Down
136 changes: 64 additions & 72 deletions src/store/modules/activity.ts
@@ -1,5 +1,4 @@
import moment from 'moment';
import { unitOfTime } from 'moment';
import * as _ from 'lodash';
import { map, filter, values, groupBy, sortBy, flow, reverse } from 'lodash/fp';

Expand All @@ -8,23 +7,14 @@ import queries from '~/queries';
import { getColorFromCategory } from '~/util/color';
import { loadClassesForQuery } from '~/util/classes';
import { get_day_start_with_offset } from '~/util/time';

interface TimePeriod {
start: string;
length: [number, string];
}

function dateToTimeperiod(date: string, duration?: [number, string]): TimePeriod {
return { start: get_day_start_with_offset(date), length: duration || [1, 'day'] };
}

function timeperiodToStr(tp: TimePeriod): string {
const start = moment(tp.start).format();
const end = moment(start)
.add(tp.length[0], tp.length[1] as moment.unitOfTime.DurationConstructor)
.format();
return [start, end].join('/');
}
import {
TimePeriod,
dateToTimeperiod,
timeperiodToStr,
timeperiodsHoursOfPeriod,
timeperiodsDaysOfPeriod,
timeperiodsAroundTimeperiod,
} from '~/util/timeperiod';

interface QueryOptions {
host: string;
Expand Down Expand Up @@ -64,7 +54,7 @@ const _state = {

category: {
available: false,
by_hour: [],
by_period: [],
top: [],
},

Expand Down Expand Up @@ -96,32 +86,12 @@ const _state = {
},
};

function timeperiodsAroundTimeperiod(timeperiod: TimePeriod): TimePeriod[] {
const periods = [];
for (let i = -15; i <= 15; i++) {
const start = moment(timeperiod.start)
.add(i * timeperiod.length[0], timeperiod.length[1] as moment.unitOfTime.DurationConstructor)
.format();
periods.push({ ...timeperiod, start });
}
return periods;
function timeperiodsStrsHoursOfPeriod(timeperiod: TimePeriod): string[] {
return timeperiodsHoursOfPeriod(timeperiod).map(timeperiodToStr);
}

function timeperiodsHoursOfDay(timeperiod: TimePeriod): TimePeriod[] {
const periods = [];
const _length: [number, string] = [1, 'hour'];
for (let i = 0; i < 24; i++) {
const start = moment(timeperiod.start)
.add(i * _length[0], _length[1] as moment.unitOfTime.DurationConstructor)
.format();
periods.push({ start, length: _length });
}
// const periods = _.range(24).map(i => [TimePeriod(moment(i * 1 + dayOffset), [1, 'hour'])]);
return periods;
}

function timeperiodsStrsHoursOfDay(timeperiod: TimePeriod): string[] {
return timeperiodsHoursOfDay(timeperiod).map(timeperiodToStr);
function timeperiodsStrsDaysOfPeriod(timeperiod: TimePeriod): string[] {
return timeperiodsDaysOfPeriod(timeperiod).map(timeperiodToStr);
}

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

if (state.window.available) {
await dispatch('query_desktop_full', query_options);
await dispatch('query_category_time_by_hour', query_options);
await dispatch('query_category_time_by_period', query_options);
} else if (state.android.available) {
await dispatch('query_android', query_options);
} else {
console.log(
'Cannot query windows as we are missing either an afk/window bucket pair or an android bucket'
);
await dispatch('query_window_empty', query_options);
await dispatch('query_browser_empty', query_options);
await dispatch('reset_window');
await dispatch('reset_category');
}

if (state.active.available) {
Expand All @@ -186,7 +156,7 @@ const actions = {
await dispatch('query_editor', query_options);
} else {
console.log('Cannot call query_editor as we do not have any editor buckets');
await dispatch('query_editor_empty', query_options);
await dispatch('reset_editor');
}
} else {
console.warn(
Expand All @@ -203,7 +173,14 @@ const actions = {
commit('query_window_completed', data[0]);
},

async query_window_empty({ commit }) {
async reset({ dispatch }) {
await dispatch('reset_window');
await dispatch('reset_browser');
await dispatch('reset_editor');
await dispatch('reset_category');
},

async reset_window({ commit }) {
const data = {
app_events: [],
title_events: [],
Expand All @@ -214,6 +191,32 @@ const actions = {
commit('query_window_completed', data);
},

async reset_browser({ commit }) {
const data = {
domains: [],
urls: [],
duration: 0,
};
commit('query_browser_completed', data);
},

async reset_editor({ commit }) {
const data = {
files: [],
projects: [],
languages: [],
};
commit('query_editor_completed', data);
},

async reset_category({ commit }) {
const data = {
by_period: [],
};

commit('query_category_time_by_period_completed', data);
},

async query_desktop_full(
{ state, commit, rootState, rootGetters },
{ timeperiod, filterCategories, filterAFK, includeAudible }: QueryOptions
Expand Down Expand Up @@ -245,31 +248,13 @@ const actions = {
commit('query_browser_completed', data_browser);
},

async query_browser_empty({ commit }) {
const data = {
domains: [],
urls: [],
duration: 0,
};
commit('query_browser_completed', data);
},

async query_editor({ state, commit }, { timeperiod }) {
const periods = [timeperiodToStr(timeperiod)];
const q = queries.editorActivityQuery(state.buckets.editor);
const data = await this._vm.$aw.query(periods, q);
commit('query_editor_completed', data[0]);
},

async query_editor_empty({ commit }) {
const data = {
files: [],
projects: [],
languages: [],
};
commit('query_editor_completed', data);
},

async query_active_history({ commit, state }, { timeperiod }: QueryOptions) {
const periods = timeperiodStrsAroundTimeperiod(timeperiod).filter(tp_str => {
return !_.includes(state.active.history, tp_str);
Expand All @@ -285,13 +270,20 @@ const actions = {
commit('query_active_history_completed', { active_history });
},

async query_category_time_by_hour(
async query_category_time_by_period(
{ commit, state },
{ timeperiod, filterCategories, filterAFK }: QueryOptions
) {
// TODO: Only works for the 1 day timeperiod
// TODO: Needs to be adapted for Android
const periods = timeperiodsStrsHoursOfDay(timeperiod);
let periods;
if (timeperiod.length[1] == 'day') {
periods = timeperiodsStrsHoursOfPeriod(timeperiod);
} else if (timeperiod.length[1] == 'week' || timeperiod.length[1] == 'month') {
periods = timeperiodsStrsDaysOfPeriod(timeperiod);
} else {
console.error('Unknown timeperiod');
}
const classes = loadClassesForQuery();
const data = await this._vm.$aw.query(
periods,
Expand All @@ -307,8 +299,7 @@ const actions = {
filter_classes: filterCategories,
})
);
const category_time_by_hour = _.zipObject(periods, data);
commit('query_category_time_by_hour_completed', { category_time_by_hour });
commit('query_category_time_by_period_completed', { by_period: _.zipObject(periods, data) });
},

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

state.category.top = null;
state.category.by_period = null;

state.active.duration = null;

Expand Down Expand Up @@ -503,8 +495,8 @@ const mutations = {
};
},

query_category_time_by_hour_completed(state, { category_time_by_hour }) {
state.category.by_hour = category_time_by_hour;
query_category_time_by_period_completed(state, { by_period }) {
state.category.by_period = by_period;
},

buckets(state, data) {
Expand Down

1 comment on commit 889a911

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are screenshots of this commit:

Screenshots using aw-server v0.10.0 (click to expand)

Screenshots using aw-server-rust master (click to expand)

Screenshots using aw-server-rust v0.10.0 (click to expand)

CML watermark

Please sign in to comment.