Skip to content

Commit f4e0793

Browse files
feat: add productivity score for categories (#258)
Co-authored-by: nicolae <stroncea.nicolae@gmail.com>
1 parent af12a54 commit f4e0793

File tree

8 files changed

+148
-12
lines changed

8 files changed

+148
-12
lines changed

src/components/CategoryEditModal.vue

+19-8
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,14 @@ b-modal(id="edit" ref="edit" title="Edit category" @show="resetModal" @hidden="h
3434
| Inherit parent color
3535
div.mt-1(v-show="!editing.inherit_color")
3636
color-picker(v-model="editing.color")
37-
38-
//
39-
div.my-1
40-
b Productivity score
41-
b-input-group.my-1(prepend="Points")
42-
b-form-input(v-model="editing.productivity")
37+
38+
hr
39+
div.my-1
40+
b Productivity score
41+
b-form-checkbox(v-model="editing.inherit_score" switch)
42+
| Inherit parent score
43+
b-input-group.my-1(prepend="Score" v-if="!editing.inherit_score")
44+
b-form-input(v-model="editing.score")
4345

4446
hr
4547
div.my-1
@@ -76,6 +78,8 @@ export default {
7678
parent: [],
7779
inherit_color: true,
7880
color: null,
81+
inherit_score: true,
82+
score: null,
7983
},
8084
};
8185
},
@@ -146,7 +150,10 @@ export default {
146150
id: this.editing.id,
147151
name: this.editing.parent.concat(this.editing.name),
148152
rule: this.editing.rule.type !== 'none' ? this.editing.rule : { type: 'none' },
149-
data: { color: this.editing.inherit_color === true ? undefined : this.editing.color },
153+
data: {
154+
color: this.editing.inherit_color === true ? undefined : this.editing.color,
155+
score: this.editing.inherit_score === true ? undefined : this.editing.score,
156+
},
150157
};
151158
this.categoryStore.updateClass(new_class);
152159
@@ -159,13 +166,17 @@ export default {
159166
const cat = this.categoryStore.get_category_by_id(this.categoryId);
160167
const color = cat.data ? cat.data.color : undefined;
161168
const inherit_color = !color;
169+
const score = cat.data ? cat.data.score : undefined;
170+
const inherit_score = !score;
162171
this.editing = {
163172
id: cat.id,
164173
name: cat.subname,
165174
rule: _.cloneDeep(cat.rule),
175+
parent: cat.parent ? cat.parent : [],
166176
color,
167177
inherit_color,
168-
parent: cat.parent ? cat.parent : [],
178+
score,
179+
inherit_score,
169180
};
170181
},
171182
},

src/components/CategoryEditTree.vue

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ div
1010
| {{ _class.name.slice(depth).join(" ➤ ")}}
1111
icon.ml-1(v-if="_class.data && _class.data.color" name="circle" :style="'color: ' + _class.data.color")
1212
span.ml-1(v-if="_class.children.length > 0" style="opacity: 0.5") ({{totalChildren}})
13+
span.d-none.d-md-inline
14+
span(v-if="_class.data && _class.data.score !== undefined" :style="'color: ' + (_class.data.score > 0 ? 'green' : 'red')")
15+
| &nbsp; {{ _class.data.score >= 0 ? '+' : '' }}{{ _class.data.score }}
1316

1417
div.col-4.col-md-8
1518
span.d-none.d-md-inline

src/components/SelectableVisualization.vue

+7
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ div
7878
aw-custom-vis(:visname="props.visname" :title="props.title")
7979
div(v-if="type == 'vis_timeline' && isSingleDay")
8080
vis-timeline(:buckets="timeline_buckets", :showRowLabels='true', :queriedInterval="timeline_daterange")
81+
div(v-if="type == 'score'")
82+
aw-score(:date="date")
8183
</template>
8284

8385
<style lang="scss">
@@ -143,6 +145,7 @@ export default {
143145
'sunburst_clock',
144146
'custom_vis',
145147
'vis_timeline',
148+
'score',
146149
],
147150
// TODO: Move this function somewhere else
148151
top_editor_files_namefunc: e => {
@@ -224,6 +227,10 @@ export default {
224227
title: 'Custom Visualization',
225228
available: true, // TODO: Implement
226229
},
230+
score: {
231+
title: 'Score',
232+
available: this.activityStore.category.available,
233+
},
227234
};
228235
},
229236
has_prerequisites() {

src/main.js

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ Vue.component('aw-categorytree', () => import('./visualizations/CategoryTree.vue
5959
Vue.component('aw-timeline-barchart', () => import('./visualizations/TimelineBarChart.vue'));
6060
Vue.component('aw-calendar', () => import('./visualizations/Calendar.vue'));
6161
Vue.component('aw-custom-vis', () => import('./visualizations/CustomVisualization.vue'));
62+
Vue.component('aw-score', () => import('./visualizations/Score.vue'));
6263

6364
// A mixin to make async method errors propagate
6465
Vue.mixin(require('~/mixins/asyncErrorCaptured.js'));

src/stores/activity.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ function colorCategories(events: IEvent[]): IEvent[] {
4848
});
4949
}
5050

51+
function scoreCategories(events: IEvent[]): IEvent[] {
52+
// Set $score for categories
53+
const categoryStore = useCategoryStore();
54+
return events.map((e: IEvent) => {
55+
e.data['$score'] = categoryStore.get_category_score(e.data['$category']);
56+
return e;
57+
});
58+
}
59+
5160
export interface QueryOptions {
5261
host: string;
5362
date?: string;
@@ -334,8 +343,9 @@ export const useActivityStore = defineStore('activity', {
334343
const data = await getClient().query(periods, q);
335344
const data_window = data[0].window;
336345

337-
// Set $color for categories
346+
// Set $color and $score for categories
338347
data_window.cat_events = colorCategories(data_window.cat_events);
348+
data_window.cat_events = scoreCategories(data_window.cat_events);
339349

340350
this.query_window_completed(data_window);
341351
},
@@ -369,8 +379,9 @@ export const useActivityStore = defineStore('activity', {
369379
const data_window = data[0].window;
370380
const data_browser = data[0].browser;
371381

372-
// Set $color for categories
382+
// Set $color and $score for categories
373383
data_window.cat_events = colorCategories(data_window.cat_events);
384+
data_window.cat_events = scoreCategories(data_window.cat_events);
374385

375386
this.query_window_completed(data_window);
376387
this.query_browser_completed(data_browser);

src/stores/categories.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,21 @@ interface State {
1818
classes_unsaved_changes: boolean;
1919
}
2020

21+
function getScoreFromCategory(c: Category, allCats: Category[]): number {
22+
// Returns the score for a certain category, falling back to parents if none set
23+
// Very similar to getColorFromCategory
24+
if (c && c.data && c.data.score) {
25+
return c.data.score;
26+
} else if (c && c.name.slice(0, -1).length > 0) {
27+
// If no color is set on category, traverse parents until one is found
28+
const parent = c.name.slice(0, -1);
29+
const parentCat = allCats.find(cc => _.isEqual(cc.name, parent));
30+
return getScoreFromCategory(parentCat, allCats);
31+
} else {
32+
return 0;
33+
}
34+
}
35+
2136
export const useCategoryStore = defineStore('categories', {
2237
state: (): State => ({
2338
classes: [],
@@ -83,10 +98,15 @@ export const useCategoryStore = defineStore('categories', {
8398
};
8499
},
85100
get_category_color() {
86-
return (cat: string[]) => {
101+
return (cat: string[]): string => {
87102
return getColorFromCategory(this.get_category(cat), this.classes);
88103
};
89104
},
105+
get_category_score() {
106+
return (cat: string[]): number => {
107+
return getScoreFromCategory(this.get_category(cat), this.classes);
108+
};
109+
},
90110
category_select() {
91111
return (insertMeta: boolean): { text: string; value?: string[] }[] => {
92112
// Useful for <select> elements enumerating categories

src/util/classes.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export const defaultCategories: Category[] = [
3131
{
3232
name: ['Work'],
3333
rule: { type: 'regex', regex: 'Google Docs|libreoffice|ReText' },
34-
data: { color: '#0F0' },
34+
data: { color: '#0F0', score: 10 },
3535
},
3636
{
3737
name: ['Work', 'Programming'],

src/visualizations/Score.vue

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<template lang="pug">
2+
div
3+
div(style="text-align: center")
4+
| Your total score today is:
5+
div(:style="'font-size: 2em; color: ' + (score >= 0 ? '#0A0' : '#F00')")
6+
| {{score >= 0 ? '+' : ''}}{{ (Math.round(score * 10) / 10).toFixed(1) }}
7+
div.small.text-muted
8+
| ({{score_productive_percent.toFixed(1)}}% productive)
9+
hr
10+
div
11+
b Top productive:
12+
div.mt-2(v-for="cat in top_productive")
13+
div.d-flex
14+
div
15+
div
16+
| {{cat.data.$category.slice(-1)[0]}}
17+
div(style="font-size: 0.7em; color: #666;")
18+
| {{cat.data.$category.slice(0, -1).join(" > ")}}
19+
div.ml-auto
20+
span(style="font-size: 1.2em; color: #0A0")
21+
| +{{ (Math.round(cat.data.$total_score * 10) / 10).toFixed(1) }}
22+
hr
23+
div
24+
b Top distracting:
25+
div.mt-2(v-for="cat in top_distracting")
26+
div.d-flex
27+
div
28+
div
29+
| {{cat.data.$category.slice(-1)[0]}}
30+
div(style="font-size: 0.7em; color: #666;")
31+
| {{cat.data.$category.slice(0, -1).join(" > ")}}
32+
div.ml-auto
33+
span(style="font-size: 1.2em; color: #F00")
34+
| {{ (Math.round(cat.data.$total_score * 10) / 10).toFixed(1) }}
35+
</template>
36+
37+
<script>
38+
import _ from 'lodash';
39+
import { useActivityStore } from '~/stores/activity';
40+
41+
// TODO: Maybe add a "Category Tree"-style visualization?
42+
43+
export default {
44+
name: 'aw-score',
45+
props: {
46+
fields: Array,
47+
},
48+
computed: {
49+
categories_with_score: function () {
50+
// FIXME: Does this get all category time? Or just top ones?
51+
const top_categories = useActivityStore().category.top;
52+
return _.map(top_categories, cat => {
53+
cat.data.$total_score = (cat.duration / (60 * 60)) * cat.data.$score;
54+
return cat;
55+
});
56+
},
57+
score: function () {
58+
return _.sum(_.map(this.categories_with_score, cat => cat.data.$total_score));
59+
},
60+
score_productive_percent() {
61+
// Compute the percentage of time spent on productive activities (score > 0)
62+
const total_time = _.sumBy(this.categories_with_score, cat => cat.duration);
63+
const productive_time = _.sumBy(
64+
_.filter(this.categories_with_score, cat => cat.data.$total_score > 0),
65+
cat => cat.duration
66+
);
67+
return (productive_time / total_time) * 100;
68+
},
69+
top_productive: function () {
70+
return _.sortBy(
71+
_.filter(this.categories_with_score, cat => cat.data.$total_score > 0.1),
72+
c => -c.data.$total_score
73+
);
74+
},
75+
top_distracting: function () {
76+
return _.sortBy(
77+
_.filter(this.categories_with_score, cat => cat.data.$total_score < -0.1),
78+
c => c.data.$total_score
79+
);
80+
},
81+
},
82+
};
83+
</script>

0 commit comments

Comments
 (0)