Skip to content

Commit

Permalink
#16 - implemented bar charts
Browse files Browse the repository at this point in the history
  • Loading branch information
dangermccann committed Jun 30, 2024
1 parent cd15793 commit ad9c626
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 49 deletions.
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ The plugin provides an interface for storing Metrics and Data Points inside your

The plugin also supports Child Metrics to allow logically similar Metrics to be organized and visualized together. For example, a Metric for measuring exercise minutes can have Child Metrics for sub-categories of exercise such as running, cycling, cardio and stregnth training.

## New
As of verion 0.22 properties bar chars are now supported. [Read more](#properties-bar-chart).

## Usage

There are two ways to provide the data that this plugin needs to display graphs. One is the built-in "Metrics Add" command, which captures and writes your data into the metrics-plugin-data page. The other way is to simply store your own data as Journal properties and visualize the data using a [Properties Chart](#properties-charts). Simply add weight:: 20 to the first bullet of any Journal page, and this plugin can find and plot those values.

- **Metrics → Add** – Runs via `Command Palette` (⌘⇧P or Ctrl+Shift+P). Displays an interface to store a single Data Point for a Metric (and optionally a Child Metric) that you specify.
- **Metrics → Visualize** – `Slash-command` (type "/" while editing a block). Displays an interface to insert a [Card](#card), [Bar Chart](#bar-chart) or [Line Chart](#line-chart) visualization into the current block on the current page.
- **Metrics → Properties Chart** – `Slash-command` (type "/" while editing a block). Inserts a line chart that is populated from querying property values in your journal. [more...](#properties-charts)

- **Metrics → Properties Line Chart** – `Slash-command` (type "/" while editing a block). Inserts a line chart that is populated from querying property values in your journal. [more...](#properties-charts)
- **Metrics → Properties Bar Chart** – `Slash-command` (type "/" while editing a block). Inserts a bar chart that is populated from querying property values in your journal. [more...](#properties-charts)

## Visualization Types

Expand Down Expand Up @@ -67,7 +69,12 @@ The `metric` and `child metric` arguments refer to the Metric and Child Metric w
## Properties Charts
A Properties Chart visualizes how [Logseq properties](https://discuss.logseq.com/t/lesson-5-how-to-power-your-workflows-using-properties-and-dynamic-variables/10173#what-are-logseq-properties-1) in your journal change over time. To use this chart type first enter some numberic properties on your journal pages. In the example below, there are entries for the `weight::` property on three journal pages.

Use the **Metrics → Properties Chart** slash-command and enter the property name (`weight`) into the first argument of the renderer:
Both line charts and bar charts are supported. Line charts use node properties from your journal pages to visualize time series data. Bar charts enable grouping data into buckets of multiple days or a week to show the total or average values over each bucket. The following visualizations are supported:
- `properties-line` – Displays time series data in a line chart.
- `properties-cumulative-line` – Displays time series data in a cumulative fashion in a line chart.
- `properties-bar` – Displays data in configurable time buckets in a bar chart.

Example: use the **Metrics → Properties Line Chart** slash-command and enter the property name (`weight`) into the first argument of the renderer:
`{{renderer :metrics, weight, -, properties-line}}`. Or `{{renderer :metrics, weight, -, properties-cumulative-line}}` to aggegate property values over time. The result looks like:

![PropertiesChart](./images/properties-chart.png)
Expand All @@ -78,6 +85,15 @@ If you need to display two completly different properties on the same chart: add

The date range for the properties chart can be customized by providing the start and end dates to the renderer as arguments in the format `YYYY-MM-DD`. For example, to limit the date range to March 1, 2023 through March 31, 2023, add the dates as arguments passed to the renderer like this: `{{renderer :metrics, weight, -, properties-line, 2023-03-01, 2023-03-31}}`. You may also pass in a value that specifies a number of days in the form `-Xd`. For example, passing `-12d` will be interepred as "twelve days ago". Finally, you can pass in values of `today` and `yesterday` and they will be interpred appropriately.

### Properties Bar Chart
The syntax for the properties bar chart is: `{{renderer :metrics, [property], [title], properties-bar, [sum], [bucket size], [start date], [end date]}}`. The arguments are:
- `property` – The name of the property or properties to display. Multiple properties can be combined with a `|`.
- `title` – The title to display at the top of the chart, or `-` to show no title.
- `sum` – Either `sum` or `average` to show either the sum or average of the values in the bucket
- `bucket size`_(optional, default = 1)_ – The size of each bucket/grouping in days, or specify `week` to bucket by weeks starting on Sundays. Bucketing by month is not yet supported – if you want this feature open an issue on Github and I'll implement it.
- `start date`_(optional, defaults all the earliest available data)_ – The start date of the data for the chart in `YYYY-MM-DD`, or relative days `-Xd`
- `end date`_(optional, defaults to today's date)_ – The end date of the data for the chart in `YYYY-MM-DD`, or relative days `-Xd`

## Data Storage
Data for the metrics and data points is stored in the `metrics-plugin-data` page (could be changed in plugin settings). Each Metric, Child Metric and Data Point is stored on individual blocks on this page. For example, storage of a Metric called *Movies Watched* with Child Metrics for *Comedy*, *Drama* and *Horror* movies is stored as follows:

Expand Down
93 changes: 61 additions & 32 deletions data-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -412,57 +412,86 @@ export class DataUtils {
})
}

backToSunday(date) {
let dayOfWeek = date.getDay();
let result = new Date(date)
result.setDate(result.getDate() - dayOfWeek);
return result
}

backToFirstOfMonth(date) {

}

async propertiesQueryBarChart(properties, bucketSizeDays, start, end) {
let metrics = await this.propertiesQuery(properties, start, end)
if(metrics.length == 0)
let datasets = await this.propertiesQuery(properties, start, end)
if(datasets.length == 0)
return []

// flatten into single array
let single = []
metrics.forEach(propArray => {
datasets.forEach(propArray => {
propArray.forEach(val => {
single.push(val)
})
})

if(single.length == 0)
return []

single.sort((a, b) => {
return a.date - b.date
})

let startTime = single[0].date.getTime()
let endTime = single[single.length - 1].date
let bucketSizeMillis = bucketSizeDays * 24 * 60 * 60 * 1000
let buckets = {}

// create the buckets
let numBuckets = Math.ceil((endTime - startTime) / bucketSizeMillis)
for(var i = 0; i < numBuckets; i++) {
buckets[i.toString()] = []
let endTime = single[single.length - 1].date.getTime()

let bucketSizeMillis = 24 * 60 * 60 * 1000
if(bucketSizeDays == "week") {
bucketSizeMillis *= 7
startTime = this.backToSunday(single[0].date).getTime()
}
else if(bucketSizeDays == "month") {
}
else {
bucketSizeMillis *= parseInt(bucketSizeDays)
}

// populate the buckets
single.forEach((metric, idx) => {
let bucket = Math.floor((metric.date.getTime() - startTime) / bucketSizeMillis)
buckets[bucket.toString()].push(metric)
})
var results = []
datasets.forEach(dataset => {
let buckets = {}

// calculate sum and average
var results = { }
for (let key in buckets) {
let bucket = buckets[key]
let bucketStart = parseInt(key) * bucketSizeMillis + startTime
let sum = 0;
let average = 0;
for(var i = 0; i < bucket.length; i++) {
sum += bucket[i].value
// create the buckets
let numBuckets = Math.floor((endTime - startTime) / bucketSizeMillis) + 1
for(var i = 0; i < numBuckets; i++) {
buckets[i.toString()] = []
}
average = sum / bucket.length
results[key] = {
sum: sum,
average: average,
bucketTime: bucketStart
}
}

// populate the buckets
dataset.forEach((metric, idx) => {
let bucket = Math.floor((metric.date.getTime() - startTime) / bucketSizeMillis)
buckets[bucket.toString()].push(metric)
})

// calculate sum and average
var metrics = { }
for (let key in buckets) {
let bucket = buckets[key]
let bucketStart = parseInt(key) * bucketSizeMillis + startTime
let sum = 0;
let average = 0;
for(var i = 0; i < bucket.length; i++) {
sum += bucket[i].value
}
average = bucket.length > 0 ? sum / bucket.length : 0
metrics[key] = {
sum: sum,
average: average,
bucketTime: bucketStart
}
}
results.push(metrics)
})

return results
}
Expand Down
87 changes: 74 additions & 13 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ async function main() {
}, 200)
})

logseq.Editor.registerSlashCommand("Metrics → Properties Chart", async () => {
logseq.Editor.registerSlashCommand("Metrics → Properties Line Chart", async () => {
const content = '{{renderer :metrics, :property1 :property2, TITLE (use "-" to leave empty), properties-line}}'

const block = await logseq.Editor.getCurrentBlock()
Expand All @@ -89,6 +89,19 @@ async function main() {
}
})

logseq.Editor.registerSlashCommand("Metrics → Properties Bar Chart", async () => {
const content = '{{renderer :metrics, :property1 :property2, TITLE (use "-" to leave empty), properties-bar, sum}}'

const block = await logseq.Editor.getCurrentBlock()
if(block) {
await logseq.Editor.updateBlock(block.uuid, content)
await logseq.Editor.exitEditingMode()
setTimeout(() => {
logseq.Editor.editBlock(block.uuid, { pos: 21 })
}, 50)
}
})

logseq.provideModel({
async editBlock(e) {
const { uuid } = e.dataset
Expand Down Expand Up @@ -409,13 +422,17 @@ class _ChartVisualization extends Visualization {
}

async render() {
let showDownload = logseq.settings.show_csv_download
if(this.chartType == "bar")
showDownload = false

return `
<div class="metrics-chart flex flex-col border"
data-uuid="${this.uuid}"
data-on-click="editBlock"
>
<canvas id="chart_${this.slot}"></canvas>
<button id="chart_${this.slot}_download">Download Chart Data as CSV</button>
<button id="chart_${this.slot}_download" style="display: ${showDownload ? 'block' : 'none'}">Download Chart Data as CSV</button>
</div>
`.trim()
}
Expand Down Expand Up @@ -651,6 +668,29 @@ class BarChartVisualization extends _BarChartVisualization {
class PropertiesBarChartVisualization extends _BarChartVisualization {
chartType = "bar"

async getChartOptions() {
const config = await logseq.App.getUserConfigs()
const title = this.childMetric

const options = {
scales: {
x: {
time: {
tooltipFormat: config.preferredDateFormat
}
}
},
plugins: {
title: {
display: !!title,
text: title
}
}
}

return mergeDeep(await super.getChartOptions(), options)
}

async loadData(options) {
const properties = splitBy(this.metric)
let mode = this.args.length > 0 ? this.args[0] : "sum"
Expand All @@ -668,22 +708,43 @@ class PropertiesBarChartVisualization extends _BarChartVisualization {
properties, bucketSize, start, end
)

console.log(datasets)

var results = {}
var results = []

Object.keys(datasets).forEach((key) => {
let values = []
if(mode == "average")
values.push({ value: datasets[key].average })
else
values.push({ value: datasets[key].sum })

results[(new Date(datasets[key].bucketTime).toLocaleDateString())] = values
datasets.forEach((dataset, idx) => {
var result = {}
Object.keys(dataset).forEach((key) => {
let value = 0
if(mode == "average")
value = dataset[key].average
else
value = dataset[key].sum

result[(new Date(dataset[key].bucketTime).toLocaleDateString())] = value
})
results.push({
label: properties[idx],
data: result
})
})

return results
}

async getData(options) {
const datasets = await this.loadData(options)

const colors = this.getChartColors()
datasets.forEach((dataset, idx) => {
dataset.backgroundColor = dataset.borderColor = colors[idx % colors.length]
})

if(datasets.length > 1)
options.plugins.legend.display = true

return {
datasets: datasets
}
}
}

class _LineChartVisualization extends _ChartVisualization {
Expand Down
9 changes: 8 additions & 1 deletion settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const settingsDescription = [
key: "add_to_journal",
type: "boolean",
title: 'Checkbox state in add metric UI',
description: 'Wether "Also add entry to Journal" checked or not by default',
description: 'Whether "Also add entry to Journal" is checked or not by default',
default: true,
},
{
Expand All @@ -27,6 +27,13 @@ export const settingsDescription = [
description: "⚠️ If you change this settings and you already have entered some metrics data, you need to move it to new page manually!",
default: "metrics-plugin-data",
},
{
key: "show_csv_download",
type: "boolean",
title: 'Show CSV download link',
description: 'Whether to show a link below charts to Download Chart Data as CSV',
default: true,
},
]


Expand Down

0 comments on commit ad9c626

Please sign in to comment.