Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a "Calendar view" #732

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
24 changes: 23 additions & 1 deletion models/view/summary.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package view

import (
"time"

conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"time"
)

type SummaryViewModel struct {
Expand All @@ -14,11 +15,32 @@ type SummaryViewModel struct {
EditorColors map[string]string
LanguageColors map[string]string
OSColors map[string]string
DailyStats []*DailyProjectViewModel
RawQuery string
UserFirstData time.Time
DataRetentionMonths int
}

type DailyProjectViewModel struct {
Date time.Time `json:"date"`
Project string `json:"project"`
Duration time.Duration `json:"duration"`
}

func NewDailyProjectStats(summaries []*models.Summary) []*DailyProjectViewModel {
dailyProjects := make([]*DailyProjectViewModel, 0)
for _, summary := range summaries {
for _, project := range summary.Projects {
muety marked this conversation as resolved.
Show resolved Hide resolved
dailyProjects = append(dailyProjects, &DailyProjectViewModel{
Date: summary.FromTime.T(),
Project: project.Key,
Duration: project.Total,
})
}
}
return dailyProjects
}

func (s SummaryViewModel) UserDataExpiring() bool {
cfg := conf.Get()
return cfg.Subscriptions.Enabled &&
Expand Down
28 changes: 26 additions & 2 deletions routes/summary.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package routes

import (
"fmt"
"net/http"
"time"

"github.com/go-chi/chi/v5"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/helpers"
Expand All @@ -10,8 +13,7 @@ import (
"github.com/muety/wakapi/models/view"
su "github.com/muety/wakapi/routes/utils"
"github.com/muety/wakapi/services"
"net/http"
"time"
"github.com/muety/wakapi/utils"
)

type SummaryHandler struct {
Expand Down Expand Up @@ -41,6 +43,19 @@ func (h *SummaryHandler) RegisterRoutes(router chi.Router) {
router.Mount("/summary", r)
}

func (h *SummaryHandler) FetchSummaryForDailyProjectStats(params *models.SummaryParams) ([]*models.Summary, error) {
Copy link
Owner

Choose a reason for hiding this comment

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

Minor one: make this a private method (lower-case) and move it below all public methods. Also, please stick to Go code style convention of having camel-case variables (e.g. cur_summary -> curSummary).

summaries := make([]*models.Summary, 0)
intervals := utils.SplitRangeByDays(params.From, params.To)
for _, interval := range intervals {
cur_summary, err := h.summarySrvc.Aliased(interval[0], interval[1], params.User, h.summarySrvc.Retrieve, params.Filters, false)
if err != nil {
return nil, err
}
summaries = append(summaries, cur_summary)
}
return summaries, nil
}

func (h *SummaryHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
if h.config.IsDev() {
loadTemplates()
Expand Down Expand Up @@ -86,6 +101,14 @@ func (h *SummaryHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
firstData, _ = time.Parse(time.RFC822Z, firstDataKv.Value)
}

dailyStats := []*view.DailyProjectViewModel{}
dailyStatsSummaries, err := h.FetchSummaryForDailyProjectStats(summaryParams)
if err != nil {
conf.Log().Request(r).Error("failed to load daily stats", "error", err)
} else {
dailyStats = view.NewDailyProjectStats(dailyStatsSummaries)
}

vm := view.SummaryViewModel{
SharedLoggedInViewModel: view.SharedLoggedInViewModel{
SharedViewModel: view.NewSharedViewModel(h.config, nil),
Expand All @@ -100,6 +123,7 @@ func (h *SummaryHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
RawQuery: rawQuery,
UserFirstData: firstData,
DataRetentionMonths: h.config.App.DataRetentionMonths,
DailyStats: dailyStats,
}

templates[conf.SummaryTemplate].Execute(w, vm)
Expand Down
1 change: 1 addition & 0 deletions routes/utils/summary_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func LoadUserSummaryByParams(ss services.ISummaryService, params *models.Summary
return nil, err, http.StatusInternalServerError
}

summary.User = params.User
muety marked this conversation as resolved.
Show resolved Hide resolved
summary.FromTime = models.CustomTime(summary.FromTime.T().In(params.User.TZ()))
summary.ToTime = models.CustomTime(summary.ToTime.T().In(params.User.TZ()))

Expand Down
106 changes: 93 additions & 13 deletions static/assets/js/summary.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const labelsCanvas = document.getElementById('chart-label')
const branchesCanvas = document.getElementById('chart-branches')
const entitiesCanvas = document.getElementById('chart-entities')
const categoriesCanvas = document.getElementById('chart-categories')
const dailyCanvas = document.getElementById('chart-daily-projects')

const projectContainer = document.getElementById('project-container')
const osContainer = document.getElementById('os-container')
Expand All @@ -25,10 +26,11 @@ const labelContainer = document.getElementById('label-container')
const branchContainer = document.getElementById('branch-container')
const entityContainer = document.getElementById('entity-container')
const categoryContainer = document.getElementById('category-container')
const daliyContainer = document.getElementById('daily-container')

const containers = [projectContainer, osContainer, editorContainer, languageContainer, machineContainer, labelContainer, branchContainer, entityContainer, categoryContainer]
const canvases = [projectsCanvas, osCanvas, editorsCanvas, languagesCanvas, machinesCanvas, labelsCanvas, branchesCanvas, entitiesCanvas, categoriesCanvas]
const data = [wakapiData.projects, wakapiData.operatingSystems, wakapiData.editors, wakapiData.languages, wakapiData.machines, wakapiData.labels, wakapiData.branches, wakapiData.entities, wakapiData.categories]
const containers = [projectContainer, osContainer, editorContainer, languageContainer, machineContainer, labelContainer, branchContainer, entityContainer, categoryContainer, daliyContainer]
justin-jiajia marked this conversation as resolved.
Show resolved Hide resolved
const canvases = [projectsCanvas, osCanvas, editorsCanvas, languagesCanvas, machinesCanvas, labelsCanvas, branchesCanvas, entitiesCanvas, categoriesCanvas, dailyCanvas]
const data = [wakapiData.projects, wakapiData.operatingSystems, wakapiData.editors, wakapiData.languages, wakapiData.machines, wakapiData.labels, wakapiData.branches, wakapiData.entities, wakapiData.categories, wakapiData.dailyStats]

let topNPickers = [...document.getElementsByClassName('top-picker')]
topNPickers.sort(((a, b) => parseInt(a.attributes['data-entity'].value) - parseInt(b.attributes['data-entity'].value)))
Expand Down Expand Up @@ -63,6 +65,13 @@ String.prototype.toHHMMSS = function () {
return `${hours}:${minutes}:${seconds}`
}

function filterLegendItem(item) {
if (!item || !item.text) return false;
item.text = item.text.length > LEGEND_CHARACTERS ? item.text.slice(0, LEGEND_CHARACTERS - 3).padEnd(LEGEND_CHARACTERS, '.') : item.text
item.text = item.text.padEnd(LEGEND_CHARACTERS + 3)
return true
}

function draw(subselection) {
function getTooltipOptions(key, stacked) {
return {
Expand All @@ -79,12 +88,6 @@ function draw(subselection) {
}
}

function filterLegendItem(item) {
item.text = item.text.length > LEGEND_CHARACTERS ? item.text.slice(0, LEGEND_CHARACTERS - 3).padEnd(LEGEND_CHARACTERS, '.') : item.text
item.text = item.text.padEnd(LEGEND_CHARACTERS + 3)
return true
}

function shouldUpdate(index) {
return !subselection || (subselection.includes(index) && data[index].length >= showTopN[index])
}
Expand All @@ -102,7 +105,7 @@ function draw(subselection) {
data: {
datasets: [{
data: wakapiData.projects
.slice(0, Math.min(showTopN[0], wakapiData.projects.length))
.slice(0, Math.min(showTopN[0], wakapiData.projects.length))
.map(p => parseInt(p.total)),
backgroundColor: wakapiData.projects.map((p, i) => {
const c = hexToRgb(vibrantColors ? getRandomColor(p.key) : getColor(p.key, i % baseColors.length))
Expand Down Expand Up @@ -455,7 +458,7 @@ function draw(subselection) {
callback: (label) => label.toString().toHHMMSS(),
},
stacked: true,
max: wakapiData.categories.map(c => c.total).reduce((a, b) => a+b, 0)
max: wakapiData.categories.map(c => c.total).reduce((a, b) => a + b, 0)
},
y: {
stacked: true,
Expand Down Expand Up @@ -503,7 +506,7 @@ function togglePlaceholders(mask) {
}

function getPresentDataMask() {
return data.map(list => (list ? list.reduce((acc, e) => acc + e.total, 0) : 0) > 0)
return data.map(list => (list ? list.reduce((acc, e) => acc + (e.total ? e.total : e.duration), 0) : 0) > 0)
}

function getColor(seed, index) {
Expand Down Expand Up @@ -547,11 +550,85 @@ function extractFile(filePath) {
}

function updateNumTotal() {
for (let i = 0; i < data.length; i++) {
// Why length - 1:
// We don't have a 'topN' for the DailyProjectStats
// So there isn't a input for it.
for (let i = 0; i < data.length - 1; i++) {
document.querySelector(`span[data-entity='${i}']`).innerText = data[i].length.toString()
}
}

function drawDailyProjectChart(dailyStats) {
const formattedStats = dailyStats.map(stat => ({
...stat,
date: new Date(stat.date).toISOString().split('T')[0] // convert to YYYY-MM-DD format

}));

const days = [...new Set(formattedStats.map(stat => stat.date))].sort()
const projects = [...new Set(formattedStats.map(stat => stat.project))]

// prepare for each project
const datasets = projects.map(project => {
const color = getRandomColor(project)
return {
label: project,
data: days.map(day => {
const stat = formattedStats.find(s => s.date === day && s.project === project)
return stat ? parseInt(stat.duration) : 0
}),
backgroundColor: color,
barPercentage: 0.95,
}
})

new Chart(dailyCanvas.getContext('2d'), {
type: 'bar',
data: {
labels: days.map(d => new Date(d).toLocaleDateString()),
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
stacked: true,
title: {
display: true,
text: 'Date'
}
},
y: {
stacked: true,
title: {
display: true,
text: 'Duration (hh:mm:ss)'
},
ticks: {
callback: value => value.toString().toHHMMSS()
}
}
},
plugins: {
tooltip: {
callbacks: {
label: (context) => {
return `${context.dataset.label}: ${context.raw.toString().toHHMMSS()}`
}
}
},
legend: {
position: 'right',
labels: {
filter: filterLegendItem
}
}
}
}
})
}

window.addEventListener('load', function () {
topNPickers.forEach(e => e.addEventListener('change', () => {
parseTopN()
Expand All @@ -562,4 +639,7 @@ window.addEventListener('load', function () {
togglePlaceholders(getPresentDataMask())
draw()
updateNumTotal()
if (wakapiData.dailyStats && wakapiData.dailyStats.length > 0) {
drawDailyProjectChart(wakapiData.dailyStats)
}
})
15 changes: 15 additions & 0 deletions views/summary.tpl.html
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,20 @@ <h4 class="font-semibold text-lg text-gray-500">{{ .TotalTime | duration }}</h4>
<span class="text-md font-semibold text-gray-500 mt-4">No data</span>
</div>
</div>

<div class="row-span-1 col-span-1 sm:row-span-3 sm:col-span-3 md:row-span-3 md:col-span-3 p-4 px-6 pb-10 bg-gray-850 text-gray-300 rounded-md shadow flex flex-col w-full no-break"
id="daily-container" style="max-height: 224px;">
<div class="flex justify-between">
<div class="flex items-center gap-x-2">
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap">Daily Project Time {{ if .IsProjectDetails }} For this Project {{ end }}</span>
</div>
</div>
<canvas id="chart-daily-projects" class="mt-2"></canvas>
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
<span class="text-md font-semibold text-gray-500 mt-4">No data</span>
</div>
</div>
</div>
</div>

<div class="mt-12 flex flex-col space-y-2 text-gray-300 w-full no-break">
Expand Down Expand Up @@ -358,6 +372,7 @@ <h1 class="font-semibold text-3xl text-white m-0 mb-2">Setup Instructions</h1>
wakapiData.machines = {{ .Machines | json }}
wakapiData.labels = {{ .Labels | json }}
wakapiData.categories = {{ .Categories | json }}
wakapiData.dailyStats = {{ .DailyStats | json }}
{{ if .IsProjectDetails }}
wakapiData.branches = {{ .Branches | json }}
wakapiData.entities = {{ .Entities | json }}
Expand Down
Loading