const TODAY = new Date("2023-04-14"); const TODAY_LOCALE = TODAY.toLocaleDateString("en-GB"); const EMACS_ITEM = { backgroundColor: "#8261bb", borderColor: "black", borderWidth: 0, borderSkipped: false, borderRadius: 0, datalabels: { color: "white", }, }; const COLORS = [ "#77bceb", "#ff6384", "#73d9d9", "#ff9f40", "#ffcd56", "#c9cbcf", ]; let i = 0; const EMACS_DATA = { labels: [ "Editor/IDE", "File manager", "Email", "RSS", "Passwords", "Multimedia", "WM", "Messenger", ], datasets: [ { label: "Jupyter", data: [ { name: "Editor/IDE", span: [new Date("2018-10-24"), new Date("2021-04-01")], }, ], yAxisID: "yAxis1", }, { label: "NeoVim", data: [ { name: "Editor/IDE", span: [new Date("2019-03-30"), new Date("2020-10-12")], }, ], yAxisID: "yAxis1", }, { label: "DataGrip", data: [ { name: "Editor/IDE", span: [new Date("2020-02-01"), TODAY], }, ], yAxisID: "yAxis1", }, { label: "Emacs", data: [ { name: "Editor/IDE", span: [new Date("2020-10-12"), TODAY], }, ], yAxisID: "yAxis1", ...EMACS_ITEM, }, { label: "ranger", data: [ { name: "File manager", span: [new Date("2019-04-03"), new Date("2020-02-17")], }, ], yAxisID: "yAxis2", }, { label: "vifm", data: [ { name: "File manager", span: [new Date("2020-02-17"), new Date("2020-11-11")], }, ], yAxisID: "yAxis2", }, { label: "Dired", data: [ { name: "File manager", span: [new Date("2020-11-11"), TODAY], }, ], yAxisID: "yAxis2", ...EMACS_ITEM, }, { label: "Mailspring", data: [ { name: "Email", span: [new Date("2019-01-28"), new Date("2021-01-29")], }, ], yAxisID: "yAxis3", }, { label: "notmuch", data: [ { name: "Email", span: [new Date("2021-01-29"), TODAY], }, ], yAxisID: "yAxis3", ...EMACS_ITEM, }, { label: "newsboat", data: [ { name: "RSS", span: [new Date("2021-01-22"), new Date("2021-05-24")], }, ], yAxisID: "yAxis4", }, { label: "elfeed", data: [ { name: "RSS", span: [new Date("2021-05-24"), TODAY], }, ], yAxisID: "yAxis4", ...EMACS_ITEM, }, { label: "Tiny Tiny RSS", data: [ { name: "RSS", span: [new Date("2022-05-28"), TODAY], hint: "Sync with elfeed", }, ], yAxisID: "yAxis4", }, { label: "KeePassXC", data: [ { name: "Passwords", span: [new Date("2019-01-31"), new Date("2021-07-26")], }, ], yAxisID: "yAxis5", }, { label: "password-store & pass.el", data: [ { name: "Passwords", span: [new Date("2021-07-26"), TODAY], }, ], yAxisID: "yAxis5", ...EMACS_ITEM, }, { label: "Google Play Music", data: [ { name: "Multimedia", span: [new Date("2019-05-12"), new Date("2020-07-26")], }, ], yAxisID: "yAxis6", }, { label: "MPD", data: [ { name: "Multimedia", span: [new Date("2020-07-26"), TODAY], }, ], yAxisID: "yAxis6", }, { label: "ncmpcpp", data: [ { name: "Multimedia", span: [new Date("2020-07-26"), new Date("2021-07-31")], }, ], yAxisID: "yAxis6", }, { label: "EMMS", data: [ { name: "Multimedia", span: [new Date("2021-07-31"), TODAY], }, ], yAxisID: "yAxis6", ...EMACS_ITEM, }, { label: "MPV", data: [ { name: "Multimedia", span: [new Date("2021-09-07"), TODAY], }, ], yAxisID: "yAxis6", }, { label: "Cinnamon", data: [ { name: "WM", span: [new Date("2018-08-01"), new Date("2020-05-08")], }, ], yAxisID: "yAxis7", }, { label: "i3(-gaps)", data: [ { name: "WM", span: [new Date("2020-05-08"), new Date("2021-11-14")], }, ], yAxisID: "yAxis7", }, { label: "EXWM", data: [ { name: "WM", span: [new Date("2021-11-14"), TODAY], }, ], yAxisID: "yAxis7", ...EMACS_ITEM, }, { label: "Telegram Desktop", data: [ { name: "Messenger", span: [new Date("2022-03-22"), new Date("2023-01-07")], }, ], yAxisID: "yAxis8", }, { label: "telega.el", data: [ { name: "Messenger", span: [new Date("2023-01-07"), TODAY], }, ], yAxisID: "yAxis8", ...EMACS_ITEM, }, ].map((d) => { if (!d.backgroundColor) { d.backgroundColor = COLORS[i]; } i = (i + 1) % COLORS.length; return d; }), }; function replaceNumbers(data) { for (const [key, value] of Object.entries(data)) { const items = document.querySelectorAll(`[data-num="${key}"]`); for (const item of items) { item.innerHTML = value; } } } function emacsChart() { const ctx = document.getElementById("chart-emacs-history"); new Chart(ctx, { type: "bar", data: EMACS_DATA, plugins: [ChartDataLabels], options: { indexAxis: "y", grouped: true, aspectRatio: 1.1, parsing: { yAxisKey: "name", xAxisKey: "span", }, layout: { padding: 1, }, scales: { x: { type: "time", min: new Date("2018-09"), }, ...Object.fromEntries( [1, 2, 3, 4, 5, 6, 7].map((i) => [ `yAxis${i}`, { display: false, }, ]) ), }, plugins: { legend: { display: false, }, title: { display: true, text: "Figure 1. Everything goes into Emacs", color: "black", font: { size: 15, }, }, datalabels: { formatter: function (value, context) { return context.dataset.label; }, color: "black", }, tooltip: { callbacks: { label: function (context) { const startDate = new Date( context.parsed._custom.start ).toLocaleDateString("en-GB"); let endDate = new Date( context.parsed._custom.end ).toLocaleDateString("en-GB"); if (endDate === TODAY_LOCALE) { endDate = "Today"; } const label = context.dataset.label; return `${label}: ${startDate} - ${endDate}`; }, }, }, }, }, }); } async function emacsScreenTimeChart() { const response = await fetch("/data/2023-03-14-emacs/emacs-screen-time.json"); const rawData = await response.json(); const data = { labels: rawData.map((d) => new Date(d["date_trunc"])), datasets: [ { data: rawData.map((d) => ({ period: new Date(d["date_trunc"]), value: d["percent"], })), }, ], }; const ctx = document.getElementById("chart-emacs-screen-time"); new Chart(ctx, { type: "bar", data, options: { parsing: { xAxisKey: "period", yAxisKey: "value", }, scales: { x: { type: "time", min: data.labels[0], }, y: { title: { display: true, text: "Ratio of direct screen time", }, }, }, plugins: { legend: { display: false, }, title: { display: true, text: "Figure 2. Emacs direct screen time ratio over time", color: "black", font: { size: 15, }, }, }, }, }); } async function emacsTimeChart() { const response = await fetch( "/data/2023-03-14-emacs/emacs-related-time-per-month.json" ); const rawData = await response.json(); const labels = [ ["config_hours", "Config", "#A989C5"], ["package_hours", "Emacs Packages", "#7172AD"], ["orgmode_hours", "Org Mode", "#509EE3"], ["sqrt_hours", "sqrtminusone.xyz", "#51528D"], ["other_code_hours", "Other Code", "#F2A86F"], ["misc_hours", "Misc", "#F9D45C"], ]; const data = { labels: rawData.map((d) => new Date(d["period"])), datasets: labels.map(([key, label, color]) => ({ label, data: rawData.map((d) => ({ period: new Date(d["period"]), value: d[key], })), backgroundColor: color, })), }; const replaceData = {}; for (const [key] of labels) { replaceData[`${key}_total`] = 0; replaceData[`${key}_percent`] = 0; } let total = 0; for (const rawDatum of rawData) { for (const [key] of labels) { replaceData[`${key}_total`] += rawDatum[key] || 0; total += rawDatum[key] || 0; } } for (const [key] of labels) { replaceData[`${key}_total`] = replaceData[`${key}_total`].toFixed(1); replaceData[`${key}_percent`] = ( (replaceData[`${key}_total`] / total) * 100 ).toFixed(1); } replaceNumbers(replaceData); const ctx = document.getElementById("chart-emacs-time"); new Chart(ctx, { type: "bar", data, options: { parsing: { xAxisKey: "period", yAxisKey: "value", }, scales: { x: { type: "time", min: data.labels[0], stacked: true, }, y: { stacked: true, title: { display: true, text: "Hours", }, }, }, plugins: { title: { display: true, text: "Figure 3. Structure of Emacs usage per month", color: "black", font: { size: 15, }, }, }, }, }); const rawStackedData = rawData.map((d) => { let sum = 0; for (const [key] of labels) { sum += d[key]; } for (const [key] of labels) { d[key] /= sum; } return d; }); const stackedData = { labels: rawData.map((d) => new Date(d["period"])), datasets: labels.map(([key, label, color]) => ({ label, data: rawStackedData.map((d) => ({ period: new Date(d["period"]), value: d[key], })), backgroundColor: color, })), }; const stackedCtx = document.getElementById("chart-emacs-time-stacked"); new Chart(stackedCtx, { type: "bar", data: stackedData, options: { parsing: { xAxisKey: "period", yAxisKey: "value", }, scales: { x: { type: "time", min: data.labels[0], stacked: true, }, y: { stacked: true, min: 0, max: 1, title: { display: true, text: "Hours (%)", }, }, }, plugins: { title: { display: true, text: "Figure 4. Structure of Emacs usage per month (stacked)", color: "black", font: { size: 15, }, }, }, }, }); } async function configsChart() { const response = await fetch("/data/2023-03-14-emacs/lengths.csv"); const csv = await response.text(); const lines = csv.split("\n"); const labels = lines[0].split(","); const rawData = lines .slice(1) .reverse() .map((line) => { const values = line.split(","); return Object.fromEntries( values.map((value, i) => { const key = labels[i]; switch (key) { case "date": value = new Date(value); break; case "commit": break; default: value = Number(value); break; } return [key, value]; }) ); }); const data = { labels: rawData.map((d) => d.date), datasets: [ { label: "Emacs.org", data: rawData.map((d) => ({ x: d.date, y: d["Emacs.org"], })), }, { label: "init.el", data: rawData.map((d) => ({ x: d.date, y: d["init.el"], })), }, ], }; const numbers = { emacs_org_length: rawData[rawData.length - 1]["Emacs.org"], init_el_length: rawData[rawData.length - 1]["init.el"], init_vim_length: rawData[rawData.length - 1]["init.vim"], }; replaceNumbers(numbers); const ctx = document.getElementById("chart-emacs-config-size"); new Chart(ctx, { type: "line", data, options: { pointRadius: 0, tension: 0.1, parsing: { xAxisKey: "x", yAxisKey: "y", }, scales: { x: { type: "time", min: new Date("2020-10-01"), max: TODAY, }, y: { title: { display: true, text: "Lines of code", }, }, }, plugins: { title: { display: true, text: "Figure 6. Emacs.org and init.el lengths", color: "black", font: { size: 15, }, }, }, }, }); const emacsVimData = { datasets: [ { label: "Emacs.org", data: rawData.map((d) => ({ x: d.date, y: d["Emacs.org"], })), xAxisID: "xAxis1", }, { label: "init.el", data: rawData.map((d) => ({ x: d.date, y: d["init.el"], })), xAxisID: "xAxis1", }, { label: "init.vim", data: rawData.map((d) => ({ x: d.date, y: d["init.vim"], })), xAxisID: "xAxis2", }, ], }; const ctxEmacsVim = document.getElementById("chart-emacs-vim-config-size"); new Chart(ctxEmacsVim, { type: "line", data: emacsVimData, options: { pointRadius: 0, tension: 0.1, parsing: { xAxisKey: "x", yAxisKey: "y", }, scales: { xAxis1: { type: "time", min: new Date("2020-10-12"), max: new Date( new Date("2020-10-12").getTime() + 450 * (1000 * 60 * 60 * 24) ), display: false, }, xAxis2: { type: "time", min: new Date("2019-03-30"), max: new Date( new Date("2019-03-30").getTime() + 450 * (1000 * 60 * 60 * 24) ), title: { display: true, text: "Days into", }, ticks: { display: false, }, }, y: { title: { display: true, text: "Lines of code", }, }, }, plugins: { title: { display: true, text: "Figure 7. Emacs vs. Vim config size", color: "black", font: { size: 15, }, }, }, }, }); } async function packagesChart() { const response = await fetch("/data/2023-03-14-emacs/emacs-packages.json"); const rawData = await response.json(); const data = [ ...rawData.slice(0, 15), { name: "Other", hours: rawData.slice(15).reduce((acc, d) => acc + d.hours, 0), }, ]; const replaceData = {}; for (const datum of data) { replaceData[`${datum.name}_total`] = datum.hours.toFixed(1); } replaceNumbers(replaceData); const ctx = document.getElementById("chart-emacs-packages"); new Chart(ctx, { type: "bar", data: { labels: data.map((d) => d.name), datasets: [ { data, }, ], }, options: { aspectRatio: 1.3, parsing: { yAxisKey: "name", xAxisKey: "hours", }, indexAxis: "y", scales: { x: { title: { display: true, text: "Hours", }, }, }, plugins: { title: { display: true, text: "Figure 8. Time spent on Emacs packages", color: "black", font: { size: 15, }, }, legend: { display: false, }, }, }, }); } async function emacsVimSwitchChart() { const response = await fetch("/data/2023-03-14-emacs/emacs-vim-switch.json"); const rawData = await response.json(); const labels = [ ["config_hours", "Config", "#A989C5"], ["package_hours", "Emacs Packages", "#7172AD"], ["orgmode_hours", "Org Mode", "#509EE3"], ["emacs_other_code_hours", "Other Code (Emacs)", "#F2A86F"], ["vim_other_code_hours", "Other Code (Vim)", "#59c26e"], ["misc_emacs_hours", "Misc (Emacs)", "#F9D45C"], ]; const data = { labels: rawData.map((d) => new Date(d["period"])), datasets: labels.map(([key, label, color]) => ({ label, data: rawData.map((d) => ({ period: new Date(d["period"]), value: d[key], })), backgroundColor: color, })), }; const ctx = document.getElementById("chart-emacs-vim-switch"); new Chart(ctx, { type: "bar", data, options: { parsing: { xAxisKey: "period", yAxisKey: "value", }, scales: { x: { type: "time", min: data.labels[0], stacked: true, }, y: { stacked: true, title: { display: true, text: "Hours", }, }, }, plugins: { title: { display: true, text: "Figure 5. Switch from Neovim to Emacs", color: "black", font: { size: 15, }, }, }, }, }); } async function zkChart() { const response = await fetch("/data/2023-03-14-emacs/roam-stats.csv"); const csv = await response.text(); const lines = csv.split("\n"); const labels = lines[0].split(","); const rawData = lines .slice(1) .reverse() .map((line) => { const values = line.split(","); return Object.fromEntries( values.map((value, i) => { const key = labels[i]; switch (key) { case "date": value = new Date(value); break; case "commit": break; default: value = Number(value); break; } return [key, value]; }) ); }); const data = { labels: rawData.map((d) => d.date), datasets: [ { label: "Roam Nodes", data: rawData.map((d) => ({ x: d.date, y: d["nodes"], })), }, ], }; console.log(data) const ctx = document.getElementById("chart-roam-nodes"); new Chart(ctx, { type: "line", data, options: { pointRadius: 0, tension: 0.1, parsing: { xAxisKey: "x", yAxisKey: "y", }, scales: { x: { type: "time", min: data.labels[1], max: TODAY, }, y: { title: { display: true, text: "Roam Nodes", }, }, }, plugins: { title: { display: true, text: "Figure 9. Roam Nodes", color: "black", font: { size: 15, }, }, }, }, }); } document.addEventListener( "DOMContentLoaded", async function () { emacsChart(); emacsScreenTimeChart(); emacsTimeChart(); configsChart(); packagesChart(); emacsVimSwitchChart(); zkChart(); }, false );