From 1638e4e1296779edbd0a7ac4d9276125dd26f6d5 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Fri, 21 May 2021 11:11:13 +0800 Subject: [PATCH 001/402] Fix getting multiple xDataset for each target --- src/parsing.ts | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/src/parsing.ts b/src/parsing.ts index 68ab572..d80957f 100644 --- a/src/parsing.ts +++ b/src/parsing.ts @@ -811,10 +811,23 @@ export function getRenderInfoFromYaml( multipleValueSparator = retMultipleValueSparator; // xDataset - let xDataset = null; - if (typeof yaml.xDataset === "number") { - xDataset = yaml.xDataset; + let retXDataset = getNumberArrayFromInput( + "xDataset", + yaml.xDataset, + numDatasets, + -1, + true + ); + if (typeof retXDataset === "string") { + return retXDataset; // errorMessage } + let xDataset = retXDataset.map((d: number) => { + if (d < 0 || d >= numDatasets) { + return -1; + } + return d; + }); + // assign this to renderInfo later // Create queries let queries: Array = []; @@ -825,7 +838,7 @@ export function getRenderInfoFromYaml( searchTarget[ind] ); query.setSeparator(multipleValueSparator[ind]); - if (ind === xDataset) query.usedAsXDataset = true; + if (xDataset.includes(ind)) query.usedAsXDataset = true; queries.push(query); } // console.log(queries); @@ -975,23 +988,7 @@ export function getRenderInfoFromYaml( // console.log(renderInfo.endDate); // xDataset - let retXDataset = getNumberArrayFromInput( - "xDataset", - yaml.xDataset, - numDatasets, - -1, - true - ); - if (typeof retXDataset === "string") { - return retXDataset; // errorMessage - } - retXDataset = retXDataset.map((d: number) => { - if (d < 0 || d >= numDatasets) { - return -1; - } - return d; - }); - renderInfo.xDataset = retXDataset; + renderInfo.xDataset = xDataset; // console.log(renderInfo.xDataset); // Dataset name (need xDataset to set default name) From 12b5e5bfd7bb297e47082c5d3431179fdc3699ea Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Fri, 21 May 2021 21:43:51 +0800 Subject: [PATCH 002/402] Fix using comma as separator --- README.md | 4 ++-- examples/TestMultipleTargesMultipleValues.md | 5 +++-- src/collecting.ts | 7 +++++-- src/parsing.ts | 8 +++++++- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 968a7af..c9c3b86 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,10 @@ This is an [Obsidian](https://obsidian.md/) plugin that helps you do tracking in | content | #blood-pressure:180/120 | searchType: tag
searchTarget: blood-pressure[0], blood-pressure[1] | V | | content | dvTarget:: 20.5 | searchType: dvField
searchTarget: dvTarget | V | | content | dvTarget:: 20.5/30.5 | searchType: dvField
searchTarget: dvTarget[0], dvTarget[1] | V | -| content | dvTarget:: 20.5, 30.5 | searchType: dvField
searchTarget: dvTarget[0], dvTarget[1]
separator: ',' | V | +| content | dvTarget:: 20.5, 30.5 | searchType: dvField
searchTarget: dvTarget[0], dvTarget[1]
separator: 'comma' | V | | frontmatter | ---
mood: 10
--- | searchType: frontmatter
searchTarget: mood | V | | frontmatter | ---
bp: 184.4/118.8
--- | searchType: frontmatter
searchTarget: bp[0], bp[1] | V | -| frontmatter | ---
bp: 184.4, 118.8
--- | searchType: frontmatter
searchTarget: bp[0], bp[1]
separator: ',' | V | +| frontmatter | ---
bp: 184.4, 118.8
--- | searchType: frontmatter
searchTarget: bp[0], bp[1]
separator: 'comma' | V | | frontmatter | ---
bp: [184.4, 118.8]
--- | searchType: frontmatter
searchTarget: bp[0], bp[1] | V | | content | [[journal]] | searchType: wiki
searchTarget: journal | O | | content | ⭐ | searchType: text
searchTarget: ⭐ | O | diff --git a/examples/TestMultipleTargesMultipleValues.md b/examples/TestMultipleTargesMultipleValues.md index 1402518..1a8607c 100644 --- a/examples/TestMultipleTargesMultipleValues.md +++ b/examples/TestMultipleTargesMultipleValues.md @@ -68,7 +68,7 @@ searchTarget: bloodpressure1[0], bloodpressure1[1] folder: diary startDate: 2021-01-01 endDate: 2021-01-31 -separator: "," +separator: comma line: title: Blood Pressure lineColor: yellow, red @@ -142,13 +142,14 @@ line: lineColor: green, red ``` -Multiple values seprated by ',' +Multiple values seprated by 'comma' ``` tracker searchType: dvField searchTarget: dataviewTarget3[0], dataviewTarget3[1] folder: diary startDate: 2021-01-01 endDate: 2021-01-31 +separator: comma line: title: dvField lineColor: green, red diff --git a/src/collecting.ts b/src/collecting.ts index a770f46..8200ab6 100644 --- a/src/collecting.ts +++ b/src/collecting.ts @@ -355,12 +355,12 @@ export function collectDataFromDvField( dataMap: DataMap, xValueMap: XValueMap ) { - // Test this in Regex101 - // (^|\s)\*{0,2}dvTarget\*{0,2}(::\s*(?[\d\.\/\-\w,@;\s]*))(\s|$) let dvTarget = query.getTarget(); if (query.getParentTarget()) { dvTarget = query.getParentTarget(); // use parent tag name for multiple values } + // Test this in Regex101 + // (^|\s)\*{0,2}dvTarget\*{0,2}(::\s*(?[\d\.\/\-\w,@;\s]*))(\s|$) let strHashtagRegex = "(^|\\s)\\*{0,2}" + dvTarget + @@ -377,7 +377,10 @@ export function collectDataFromDvField( typeof match.groups.values !== "undefined" ) { let values = match.groups.values.trim(); + // console.log(values); + // console.log(query.getSeparator()); let splitted = values.split(query.getSeparator()); + // console.log(splitted); if (!splitted) continue; if (splitted.length === 1) { // console.log("single-value"); diff --git a/src/parsing.ts b/src/parsing.ts index d80957f..12e1041 100644 --- a/src/parsing.ts +++ b/src/parsing.ts @@ -808,7 +808,13 @@ export function getRenderInfoFromYaml( if (typeof retMultipleValueSparator === "string") { return retMultipleValueSparator; // errorMessage } - multipleValueSparator = retMultipleValueSparator; + multipleValueSparator = retMultipleValueSparator.map( sep => { + if (sep === "comma") { + return ","; + } + return sep; + }); + // console.log(multipleValueSparator); // xDataset let retXDataset = getNumberArrayFromInput( From 28315873546bbb5913407c0c8614414139673260 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Fri, 21 May 2021 21:49:47 +0800 Subject: [PATCH 003/402] Update examples --- examples/diary/2021-01-01.md | 2 ++ examples/diary/2021-01-02.md | 2 ++ examples/diary/2021-01-03.md | 2 ++ examples/diary/2021-01-04.md | 2 ++ examples/diary/2021-01-05.md | 2 ++ examples/diary/2021-01-06.md | 2 ++ examples/diary/2021-01-07.md | 2 ++ examples/diary/2021-01-08.md | 2 ++ examples/diary/2021-01-09.md | 2 ++ examples/diary/2021-01-10.md | 2 ++ examples/diary/2021-01-11.md | 2 ++ examples/diary/2021-01-12.md | 2 ++ examples/diary/2021-01-13.md | 2 ++ examples/diary/2021-01-14.md | 2 ++ examples/diary/2021-01-15.md | 2 ++ examples/diary/2021-01-16.md | 2 ++ examples/diary/2021-01-17.md | 2 ++ examples/diary/2021-01-18.md | 2 ++ examples/diary/2021-01-19.md | 2 ++ examples/diary/2021-01-20.md | 2 ++ examples/diary/2021-01-21.md | 2 ++ examples/diary/2021-01-22.md | 2 ++ examples/diary/2021-01-23.md | 2 ++ examples/diary/2021-01-24.md | 2 ++ examples/diary/2021-01-25.md | 2 ++ examples/diary/2021-01-26.md | 2 ++ examples/diary/2021-01-27.md | 2 ++ examples/diary/2021-01-28.md | 2 ++ examples/diary/2021-01-29.md | 2 ++ examples/diary/2021-01-30.md | 2 ++ examples/diary/2021-01-31.md | 2 ++ examples/example_generator.ts | 2 ++ 32 files changed, 64 insertions(+) diff --git a/examples/diary/2021-01-01.md b/examples/diary/2021-01-01.md index 4b97fd2..fc7e51d 100644 --- a/examples/diary/2021-01-01.md +++ b/examples/diary/2021-01-01.md @@ -27,6 +27,8 @@ obsidian-tracker+1@gmail.com weightlifting: 16 dataviewTarget:: 80 +- Make Progress:: 11 +- Make-Progress:: 74 dataviewTarget1:: 80/11 dataviewTarget2:: 11 @ 74 dataviewTarget3:: 11, 74 diff --git a/examples/diary/2021-01-02.md b/examples/diary/2021-01-02.md index a6c9767..4671c72 100644 --- a/examples/diary/2021-01-02.md +++ b/examples/diary/2021-01-02.md @@ -27,6 +27,8 @@ sleep: 9:49 pm/5:12 am weightlifting: 19 dataviewTarget:: 45 +- Make Progress:: 24 +- Make-Progress:: 62 dataviewTarget1:: 45/24 dataviewTarget2:: 24 @ 62 dataviewTarget3:: 24, 62 diff --git a/examples/diary/2021-01-03.md b/examples/diary/2021-01-03.md index ea1d43b..f8ca1af 100644 --- a/examples/diary/2021-01-03.md +++ b/examples/diary/2021-01-03.md @@ -29,6 +29,8 @@ obsidian-tracker@yahoo.com weightlifting: 16 dataviewTarget:: 28 +- Make Progress:: 27 +- Make-Progress:: 59 dataviewTarget1:: 28/27 dataviewTarget2:: 27 @ 59 dataviewTarget3:: 27, 59 diff --git a/examples/diary/2021-01-04.md b/examples/diary/2021-01-04.md index 6bac30d..322da2a 100644 --- a/examples/diary/2021-01-04.md +++ b/examples/diary/2021-01-04.md @@ -26,6 +26,8 @@ obsidian-tracker@yahoo.com dataviewTarget:: 18 +- Make Progress:: 47 +- Make-Progress:: 67 dataviewTarget1:: 18/47 dataviewTarget2:: 47 @ 67 dataviewTarget3:: 47, 67 diff --git a/examples/diary/2021-01-05.md b/examples/diary/2021-01-05.md index 322bc84..0f0d4e7 100644 --- a/examples/diary/2021-01-05.md +++ b/examples/diary/2021-01-05.md @@ -27,6 +27,8 @@ obsidian-tracker+1@gmail.com dataviewTarget:: 97 +- Make Progress:: 6 +- Make-Progress:: 80 dataviewTarget1:: 97/6 dataviewTarget2:: 6 @ 80 dataviewTarget3:: 6, 80 diff --git a/examples/diary/2021-01-06.md b/examples/diary/2021-01-06.md index b81331e..8405eda 100644 --- a/examples/diary/2021-01-06.md +++ b/examples/diary/2021-01-06.md @@ -29,6 +29,8 @@ obsidian-tracker+1@gmail.com weightlifting: 17 dataviewTarget:: 67 +- Make Progress:: 26 +- Make-Progress:: 62 dataviewTarget1:: 67/26 dataviewTarget2:: 26 @ 62 dataviewTarget3:: 26, 62 diff --git a/examples/diary/2021-01-07.md b/examples/diary/2021-01-07.md index e894a25..827f95e 100644 --- a/examples/diary/2021-01-07.md +++ b/examples/diary/2021-01-07.md @@ -26,6 +26,8 @@ sleep: 9:11 pm/5:55 am weightlifting: 19 dataviewTarget:: 90 +- Make Progress:: 19 +- Make-Progress:: 83 dataviewTarget1:: 90/19 dataviewTarget2:: 19 @ 83 dataviewTarget3:: 19, 83 diff --git a/examples/diary/2021-01-08.md b/examples/diary/2021-01-08.md index ba849cb..acec5e3 100644 --- a/examples/diary/2021-01-08.md +++ b/examples/diary/2021-01-08.md @@ -26,6 +26,8 @@ obsidian-tracker@yahoo.com dataviewTarget:: 21 +- Make Progress:: 0 +- Make-Progress:: 51 dataviewTarget1:: 21/0 dataviewTarget2:: 0 @ 51 dataviewTarget3:: 0, 51 diff --git a/examples/diary/2021-01-09.md b/examples/diary/2021-01-09.md index c958119..3b97488 100644 --- a/examples/diary/2021-01-09.md +++ b/examples/diary/2021-01-09.md @@ -30,6 +30,8 @@ obsidian-tracker@yahoo.com weightlifting: 14 dataviewTarget:: 54 +- Make Progress:: 40 +- Make-Progress:: 52 dataviewTarget1:: 54/40 dataviewTarget2:: 40 @ 52 dataviewTarget3:: 40, 52 diff --git a/examples/diary/2021-01-10.md b/examples/diary/2021-01-10.md index 4d0a0a5..ae58672 100644 --- a/examples/diary/2021-01-10.md +++ b/examples/diary/2021-01-10.md @@ -29,6 +29,8 @@ obsidian-tracker@yahoo.com dataviewTarget:: 52 +- Make Progress:: 18 +- Make-Progress:: 86 dataviewTarget1:: 52/18 dataviewTarget2:: 18 @ 86 dataviewTarget3:: 18, 86 diff --git a/examples/diary/2021-01-11.md b/examples/diary/2021-01-11.md index 7b9377c..8ccedd2 100644 --- a/examples/diary/2021-01-11.md +++ b/examples/diary/2021-01-11.md @@ -30,6 +30,8 @@ obsidian-tracker@yahoo.com weightlifting: 11 dataviewTarget:: 0 +- Make Progress:: 45 +- Make-Progress:: 57 dataviewTarget1:: 0/45 dataviewTarget2:: 45 @ 57 dataviewTarget3:: 45, 57 diff --git a/examples/diary/2021-01-12.md b/examples/diary/2021-01-12.md index 1595967..e8a3954 100644 --- a/examples/diary/2021-01-12.md +++ b/examples/diary/2021-01-12.md @@ -29,6 +29,8 @@ obsidian-tracker+1@gmail.com weightlifting: 16 dataviewTarget:: 38 +- Make Progress:: 11 +- Make-Progress:: 72 dataviewTarget1:: 38/11 dataviewTarget2:: 11 @ 72 dataviewTarget3:: 11, 72 diff --git a/examples/diary/2021-01-13.md b/examples/diary/2021-01-13.md index 4d27100..49e2b29 100644 --- a/examples/diary/2021-01-13.md +++ b/examples/diary/2021-01-13.md @@ -26,6 +26,8 @@ sleep: 9:0 pm/7:43 am dataviewTarget:: 1 +- Make Progress:: 17 +- Make-Progress:: 82 dataviewTarget1:: 1/17 dataviewTarget2:: 17 @ 82 dataviewTarget3:: 17, 82 diff --git a/examples/diary/2021-01-14.md b/examples/diary/2021-01-14.md index 827f231..d6d4e52 100644 --- a/examples/diary/2021-01-14.md +++ b/examples/diary/2021-01-14.md @@ -27,6 +27,8 @@ sleep: 11:43 pm/5:28 am weightlifting: 15 dataviewTarget:: 64 +- Make Progress:: 28 +- Make-Progress:: 84 dataviewTarget1:: 64/28 dataviewTarget2:: 28 @ 84 dataviewTarget3:: 28, 84 diff --git a/examples/diary/2021-01-15.md b/examples/diary/2021-01-15.md index 393f8d8..9c2cf67 100644 --- a/examples/diary/2021-01-15.md +++ b/examples/diary/2021-01-15.md @@ -27,6 +27,8 @@ obsidian-tracker+1@gmail.com dataviewTarget:: 68 +- Make Progress:: 47 +- Make-Progress:: 71 dataviewTarget1:: 68/47 dataviewTarget2:: 47 @ 71 dataviewTarget3:: 47, 71 diff --git a/examples/diary/2021-01-16.md b/examples/diary/2021-01-16.md index eaf9f2d..023d888 100644 --- a/examples/diary/2021-01-16.md +++ b/examples/diary/2021-01-16.md @@ -28,6 +28,8 @@ obsidian-tracker+1@gmail.com weightlifting: 15 dataviewTarget:: 58 +- Make Progress:: 34 +- Make-Progress:: 91 dataviewTarget1:: 58/34 dataviewTarget2:: 34 @ 91 dataviewTarget3:: 34, 91 diff --git a/examples/diary/2021-01-17.md b/examples/diary/2021-01-17.md index 63b0b3b..e594e13 100644 --- a/examples/diary/2021-01-17.md +++ b/examples/diary/2021-01-17.md @@ -29,6 +29,8 @@ obsidian-tracker@yahoo.com weightlifting: 18 dataviewTarget:: 14 +- Make Progress:: 27 +- Make-Progress:: 96 dataviewTarget1:: 14/27 dataviewTarget2:: 27 @ 96 dataviewTarget3:: 27, 96 diff --git a/examples/diary/2021-01-18.md b/examples/diary/2021-01-18.md index 0952563..6b772c6 100644 --- a/examples/diary/2021-01-18.md +++ b/examples/diary/2021-01-18.md @@ -28,6 +28,8 @@ obsidian-tracker@gmail.com weightlifting: 14 dataviewTarget:: 73 +- Make Progress:: 11 +- Make-Progress:: 58 dataviewTarget1:: 73/11 dataviewTarget2:: 11 @ 58 dataviewTarget3:: 11, 58 diff --git a/examples/diary/2021-01-19.md b/examples/diary/2021-01-19.md index 6e4b0d4..b639039 100644 --- a/examples/diary/2021-01-19.md +++ b/examples/diary/2021-01-19.md @@ -28,6 +28,8 @@ obsidian-tracker@yahoo.com weightlifting: 17 dataviewTarget:: 46 +- Make Progress:: 38 +- Make-Progress:: 82 dataviewTarget1:: 46/38 dataviewTarget2:: 38 @ 82 dataviewTarget3:: 38, 82 diff --git a/examples/diary/2021-01-20.md b/examples/diary/2021-01-20.md index 0803e46..43c965e 100644 --- a/examples/diary/2021-01-20.md +++ b/examples/diary/2021-01-20.md @@ -28,6 +28,8 @@ obsidian-tracker@yahoo.com weightlifting: 16 dataviewTarget:: 44 +- Make Progress:: 48 +- Make-Progress:: 64 dataviewTarget1:: 44/48 dataviewTarget2:: 48 @ 64 dataviewTarget3:: 48, 64 diff --git a/examples/diary/2021-01-21.md b/examples/diary/2021-01-21.md index a48c4bd..3c2b862 100644 --- a/examples/diary/2021-01-21.md +++ b/examples/diary/2021-01-21.md @@ -28,6 +28,8 @@ obsidian-tracker@gmail.com weightlifting: 13 dataviewTarget:: 76 +- Make Progress:: 38 +- Make-Progress:: 61 dataviewTarget1:: 76/38 dataviewTarget2:: 38 @ 61 dataviewTarget3:: 38, 61 diff --git a/examples/diary/2021-01-22.md b/examples/diary/2021-01-22.md index 3356ad7..3065d02 100644 --- a/examples/diary/2021-01-22.md +++ b/examples/diary/2021-01-22.md @@ -30,6 +30,8 @@ obsidian-tracker@yahoo.com weightlifting: 11 dataviewTarget:: 63 +- Make Progress:: 33 +- Make-Progress:: 58 dataviewTarget1:: 63/33 dataviewTarget2:: 33 @ 58 dataviewTarget3:: 33, 58 diff --git a/examples/diary/2021-01-23.md b/examples/diary/2021-01-23.md index 41fc3e5..42ec814 100644 --- a/examples/diary/2021-01-23.md +++ b/examples/diary/2021-01-23.md @@ -27,6 +27,8 @@ obsidian-tracker+1@gmail.com dataviewTarget:: 34 +- Make Progress:: 49 +- Make-Progress:: 86 dataviewTarget1:: 34/49 dataviewTarget2:: 49 @ 86 dataviewTarget3:: 49, 86 diff --git a/examples/diary/2021-01-24.md b/examples/diary/2021-01-24.md index 61a5105..c03e46f 100644 --- a/examples/diary/2021-01-24.md +++ b/examples/diary/2021-01-24.md @@ -29,6 +29,8 @@ obsidian-tracker+1@gmail.com weightlifting: 16 dataviewTarget:: 23 +- Make Progress:: 10 +- Make-Progress:: 92 dataviewTarget1:: 23/10 dataviewTarget2:: 10 @ 92 dataviewTarget3:: 10, 92 diff --git a/examples/diary/2021-01-25.md b/examples/diary/2021-01-25.md index ac13b66..35ac46f 100644 --- a/examples/diary/2021-01-25.md +++ b/examples/diary/2021-01-25.md @@ -28,6 +28,8 @@ obsidian-tracker@yahoo.com weightlifting: 16 dataviewTarget:: 52 +- Make Progress:: 18 +- Make-Progress:: 58 dataviewTarget1:: 52/18 dataviewTarget2:: 18 @ 58 dataviewTarget3:: 18, 58 diff --git a/examples/diary/2021-01-26.md b/examples/diary/2021-01-26.md index 5ac4721..1a0ed93 100644 --- a/examples/diary/2021-01-26.md +++ b/examples/diary/2021-01-26.md @@ -28,6 +28,8 @@ obsidian-tracker+1@gmail.com dataviewTarget:: 21 +- Make Progress:: 1 +- Make-Progress:: 77 dataviewTarget1:: 21/1 dataviewTarget2:: 1 @ 77 dataviewTarget3:: 1, 77 diff --git a/examples/diary/2021-01-27.md b/examples/diary/2021-01-27.md index cf3e408..497e800 100644 --- a/examples/diary/2021-01-27.md +++ b/examples/diary/2021-01-27.md @@ -29,6 +29,8 @@ obsidian-tracker+1@gmail.com weightlifting: 12 dataviewTarget:: 85 +- Make Progress:: 36 +- Make-Progress:: 64 dataviewTarget1:: 85/36 dataviewTarget2:: 36 @ 64 dataviewTarget3:: 36, 64 diff --git a/examples/diary/2021-01-28.md b/examples/diary/2021-01-28.md index d64e14a..252566c 100644 --- a/examples/diary/2021-01-28.md +++ b/examples/diary/2021-01-28.md @@ -28,6 +28,8 @@ obsidian-tracker@yahoo.com weightlifting: 10 dataviewTarget:: 24 +- Make Progress:: 46 +- Make-Progress:: 85 dataviewTarget1:: 24/46 dataviewTarget2:: 46 @ 85 dataviewTarget3:: 46, 85 diff --git a/examples/diary/2021-01-29.md b/examples/diary/2021-01-29.md index 0cc7e47..0adad37 100644 --- a/examples/diary/2021-01-29.md +++ b/examples/diary/2021-01-29.md @@ -27,6 +27,8 @@ obsidian-tracker+1@gmail.com weightlifting: 11 dataviewTarget:: 21 +- Make Progress:: 7 +- Make-Progress:: 71 dataviewTarget1:: 21/7 dataviewTarget2:: 7 @ 71 dataviewTarget3:: 7, 71 diff --git a/examples/diary/2021-01-30.md b/examples/diary/2021-01-30.md index c982e27..e22783e 100644 --- a/examples/diary/2021-01-30.md +++ b/examples/diary/2021-01-30.md @@ -29,6 +29,8 @@ obsidian-tracker@yahoo.com dataviewTarget:: 26 +- Make Progress:: 5 +- Make-Progress:: 87 dataviewTarget1:: 26/5 dataviewTarget2:: 5 @ 87 dataviewTarget3:: 5, 87 diff --git a/examples/diary/2021-01-31.md b/examples/diary/2021-01-31.md index 8a8af8d..ace81d2 100644 --- a/examples/diary/2021-01-31.md +++ b/examples/diary/2021-01-31.md @@ -29,6 +29,8 @@ obsidian-tracker+1@gmail.com weightlifting: 17 dataviewTarget:: 15 +- Make Progress:: 25 +- Make-Progress:: 98 dataviewTarget1:: 15/25 dataviewTarget2:: 25 @ 98 dataviewTarget3:: 25, 98 diff --git a/examples/example_generator.ts b/examples/example_generator.ts index a95d179..7aa3294 100644 --- a/examples/example_generator.ts +++ b/examples/example_generator.ts @@ -193,6 +193,8 @@ for ( let dataviewValue1 = randomIntFromInterval(0, 50); let dataviewValue2 = randomIntFromInterval(50, 100); content += "dataviewTarget:: " + dataviewValue + "\n"; + content += "- Make Progress:: " + dataviewValue1 + "\n"; + content += "- Make-Progress:: " + dataviewValue2 + "\n"; content += "dataviewTarget1:: " + dataviewValue + "/" + dataviewValue1 + "\n"; content += From 8fe2331cc4a34dfe3f0cae7e2fa3efa2348b1275 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Fri, 21 May 2021 21:51:31 +0800 Subject: [PATCH 004/402] Add example trackers for dvField --- examples/TestDvField.md | 87 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 examples/TestDvField.md diff --git a/examples/TestDvField.md b/examples/TestDvField.md new file mode 100644 index 0000000..f78346b --- /dev/null +++ b/examples/TestDvField.md @@ -0,0 +1,87 @@ +# Test dvField + +Simple inline field +``` tracker +searchType: dvField +searchTarget: dataviewTarget +folder: diary +startDate: 2021-01-01 +endDate: 2021-01-31 +line: + title: dvField + lineColor: green +``` + +Field with a space +``` tracker +searchType: dvField +searchTarget: Make Progress +folder: diary +startDate: 2021-01-01 +endDate: 2021-01-31 +line: + title: dvField + lineColor: yellow +``` + +Field with a dash line +``` tracker +searchType: dvField +searchTarget: Make-Progress +folder: diary +startDate: 2021-01-01 +endDate: 2021-01-31 +line: + title: dvField + lineColor: red +``` + +Extract the first value from multiple values +``` tracker +searchType: dvField +searchTarget: dataviewTarget1[0] +folder: diary +startDate: 2021-01-01 +endDate: 2021-01-31 +line: + title: dvField + lineColor: blue +``` + +Multiple values separated by '/' (default) +``` tracker +searchType: dvField +searchTarget: dataviewTarget1[0], dataviewTarget1[1] +folder: diary +startDate: 2021-01-01 +endDate: 2021-01-31 +line: + title: dvField + lineColor: green, red +``` + +Multiple values seprated by 'comma' +``` tracker +searchType: dvField +searchTarget: dataviewTarget3[0], dataviewTarget3[1] +folder: diary +startDate: 2021-01-01 +endDate: 2021-01-31 +separator: 'comma' +line: + title: dvField + lineColor: green, red +``` + +Use custom multiple value separator +``` tracker +searchType: dvField +searchTarget: dataviewTarget2[0], dataviewTarget2[1] +separator: '@' +folder: diary +startDate: 2021-01-01 +endDate: 2021-01-31 +line: + title: dvField + lineColor: green, red +``` \ No newline at end of file From 45faf3536456866f620eb60be6daeff5a4001952 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Sun, 23 May 2021 21:00:01 +0800 Subject: [PATCH 005/402] Update examples --- examples/TestTable.md | 14 +++++++++++++- examples/data/Tables.md | 24 ++++++++++++------------ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/examples/TestTable.md b/examples/TestTable.md index 53dba8f..e88bc8c 100644 --- a/examples/TestTable.md +++ b/examples/TestTable.md @@ -1,5 +1,5 @@ # Test Table -Use first column as X dataset and others as Y values +Use first column as X dataset , and second and third columns as Y values ``` tracker searchType: table searchTarget: data/Tables[0][0], data/Tables[0][1], data/Tables[0][2] @@ -10,6 +10,18 @@ line: showLegend: true ``` +Use first column as X dataset , and third and forth columns as Y values +``` tracker +searchType: table +searchTarget: data/Tables[0][0], data/Tables[0][2], data/Tables[0][3] +xDataset: 0 +line: + yAxisLocation: none, left, right + lineColor: none, yellow, red + showLegend: true + legendPosition: right +``` + Use first column as X dataset. Second column posses multiple values in each cell. ``` tracker searchType: table diff --git a/examples/data/Tables.md b/examples/data/Tables.md index 6585f08..1b568c5 100644 --- a/examples/data/Tables.md +++ b/examples/data/Tables.md @@ -1,18 +1,18 @@ # Tables ## Weight Table - Date | Jeffrey (kg) | Lucas (kg) | -| - | - | - -| 2021-05-01 | 60.0 | 20.1 | - 2021-05-02 | 62.1 | 20.5 -| 2021-05-03 | 62.2 | 20.3 | -| 2021-05-04 | 62.3 | 20.4 | - 2021-05-05 | 62.2 | 20.2 -| 2021-05-06 | 62.4 | 21.0 | -| 2021-05-07 | 62.6 | 20.9 | -| 2021-05-08 | 62.3 | 20.9 -| 2021-05-09 | 62.2 | 20.3 | -| 2021-05-10 | 62.1 | 20.8 | + Date | Jeffrey (kg) | Lucas (kg) | Anne (kg) | +| - | - | - | - | +| 2021-05-01 | 60.0 | 20.1 | 18.2 + 2021-05-02 | 62.1 | 20.5 | 18.1 | +| 2021-05-03 | 62.2 | 20.3 | 18.3 +| 2021-05-04 | 62.3 | 20.4 | 18.2 | + 2021-05-05 | 62.2 | 20.2 | 18.4 +| 2021-05-06 | 62.4 | 21.0 | 18.4 +| 2021-05-07 | 62.6 | 20.9 | 18.3 | +| 2021-05-08 | 62.3 | 20.9 | 18.2 +| 2021-05-09 | 62.2 | 20.3 | 18.5 +| 2021-05-10 | 62.1 | 20.8 | 18.9 ## Running Records From a7b7a60e2ac85f69b95c72f8f8d9e5db64b67956 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Fri, 21 May 2021 11:13:20 +0800 Subject: [PATCH 006/402] Add TestTimeValues.md --- examples/TestTimeValues.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 examples/TestTimeValues.md diff --git a/examples/TestTimeValues.md b/examples/TestTimeValues.md new file mode 100644 index 0000000..4d33c08 --- /dev/null +++ b/examples/TestTimeValues.md @@ -0,0 +1,25 @@ +# Test Time Values + +``` tracker +searchType: frontmatter +searchTarget: clock-in, clock-out +endDate: 2021-01-15 +folder: diary +line: + title: "Working Hours" + yAxisLabel: "Time" + lineColor: yellow, red + showPoint: false +``` + +``` tracker +searchType: frontmatter +searchTarget: sleep[0], sleep[1] +endDate: 2021-01-15 +folder: diary +line: + title: "Sleep" + yAxisLabel: "Time" + lineColor: yellow, red + showPoint: false +``` \ No newline at end of file From 3b4cc2f61eca753d3180f5c01a95beb5c32e7c4d Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Mon, 24 May 2021 18:08:40 +0800 Subject: [PATCH 007/402] Add a new template variable lastBreaks --- examples/HabitTracker.md | 12 ++++++++++ examples/diary/2021-01-01.md | 20 ++++++++-------- examples/diary/2021-01-02.md | 40 ++++++++++++++++---------------- examples/diary/2021-01-03.md | 39 ++++++++++++++++--------------- examples/diary/2021-01-04.md | 39 ++++++++++++++++--------------- examples/diary/2021-01-05.md | 42 ++++++++++++++++++---------------- examples/diary/2021-01-06.md | 35 ++++++++++++++-------------- examples/diary/2021-01-07.md | 36 ++++++++++++++++------------- examples/diary/2021-01-08.md | 37 +++++++++++++++--------------- examples/diary/2021-01-09.md | 41 ++++++++++++++++----------------- examples/diary/2021-01-10.md | 41 +++++++++++++++++---------------- examples/diary/2021-01-11.md | 41 +++++++++++++++++---------------- examples/diary/2021-01-12.md | 38 +++++++++++++++---------------- examples/diary/2021-01-13.md | 38 +++++++++++++++++-------------- examples/diary/2021-01-14.md | 37 +++++++++++++++--------------- examples/diary/2021-01-15.md | 39 +++++++++++++++---------------- examples/diary/2021-01-16.md | 37 +++++++++++++++--------------- examples/diary/2021-01-17.md | 42 +++++++++++++++++----------------- examples/diary/2021-01-18.md | 43 ++++++++++++++++++----------------- examples/diary/2021-01-19.md | 39 +++++++++++++++---------------- examples/diary/2021-01-20.md | 41 +++++++++++++++++---------------- examples/diary/2021-01-21.md | 35 ++++++++++++++-------------- examples/diary/2021-01-22.md | 41 ++++++++++++++++----------------- examples/diary/2021-01-23.md | 40 +++++++++++++++++--------------- examples/diary/2021-01-24.md | 42 +++++++++++++++++----------------- examples/diary/2021-01-25.md | 39 +++++++++++++++---------------- examples/diary/2021-01-26.md | 39 +++++++++++++++---------------- examples/diary/2021-01-27.md | 40 ++++++++++++++++---------------- examples/diary/2021-01-28.md | 41 +++++++++++++++++---------------- examples/diary/2021-01-29.md | 41 +++++++++++++++++---------------- examples/diary/2021-01-30.md | 34 ++++++++++++++------------- examples/diary/2021-01-31.md | 39 ++++++++++++++++--------------- examples/example_generator.ts | 9 ++++++++ src/rendering.ts | 16 ++++++++++++- 34 files changed, 649 insertions(+), 584 deletions(-) diff --git a/examples/HabitTracker.md b/examples/HabitTracker.md index 594ab92..1b606d4 100644 --- a/examples/HabitTracker.md +++ b/examples/HabitTracker.md @@ -35,6 +35,7 @@ line: ``` ## Summary +Meditation ``` tracker searchType: tag searchTarget: meditation @@ -43,6 +44,17 @@ summary: template: "Longest Streak: {{maxStreak}} day(s)\nLongest Break: {{maxBreak}} day(s)\nLast streak: {{lastStreak}} day(s)" ``` +CleanUp +``` tracker +searchType: tag +searchTarget: clean-up +folder: diary +endDate: 2021-01-31 +summary: + template: "Last Break: {{lastBreaks}} day(s)" +``` + +Work log ``` tracker searchType: tag searchTarget: work_log diff --git a/examples/diary/2021-01-01.md b/examples/diary/2021-01-01.md index fc7e51d..183ddf5 100644 --- a/examples/diary/2021-01-01.md +++ b/examples/diary/2021-01-01.md @@ -17,21 +17,23 @@ sleep: 10:11 pm/7:5 am ⭐⭐⭐⭐⭐ -#finance/bank1:-2.1USD + +#finance/bank1:-2.3USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker+1@gmail.com +obsidian-tracker@gmail.com +obsidian-tracker@yahoo.com -weightlifting: 16 +weightlifting: 15 -dataviewTarget:: 80 -- Make Progress:: 11 -- Make-Progress:: 74 -dataviewTarget1:: 80/11 -dataviewTarget2:: 11 @ 74 -dataviewTarget3:: 11, 74 +dataviewTarget:: 21 +- Make Progress:: 24 +- Make-Progress:: 79 +dataviewTarget1:: 21/24 +dataviewTarget2:: 24 @ 79 +dataviewTarget3:: 24, 79 #sin:0.00000/0.20791/0.40674/0.58779/0.74314/0.86603/0.95106/0.99452/0.99452 diff --git a/examples/diary/2021-01-02.md b/examples/diary/2021-01-02.md index 4671c72..50fc695 100644 --- a/examples/diary/2021-01-02.md +++ b/examples/diary/2021-01-02.md @@ -1,37 +1,39 @@ --- tags: -mood: 6 -bloodpressure: 181.8/117.6 -bloodpressure1: 181.8, 117.6 -bloodpressure2: [181.8, 117.6] -clock-in: 9:41 -clock-out: 19:16 -sleep: 9:49 pm/5:12 am +mood: 8 +bloodpressure: 173.8/120.6 +bloodpressure1: 173.8, 120.6 +bloodpressure2: [173.8, 120.6] +clock-in: 10:37 +clock-out: 17:7 +sleep: 11:10 pm/5:37 am --- -#weight:73.0kg +#weight:74.4kg -#exercise-pushup:44 -#exercise-plank:116sec +#exercise-pushup:50 +#exercise-plank:41sec -#meditation ⭐⭐⭐⭐ -#finance/bank1:-3.6USD + +#finance/bank1:-2.0USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] +obsidian-tracker+1@gmail.com +obsidian-tracker@yahoo.com -weightlifting: 19 +weightlifting: 14 -dataviewTarget:: 45 -- Make Progress:: 24 -- Make-Progress:: 62 -dataviewTarget1:: 45/24 -dataviewTarget2:: 24 @ 62 -dataviewTarget3:: 24, 62 +dataviewTarget:: 25 +- Make Progress:: 14 +- Make-Progress:: 64 +dataviewTarget1:: 25/14 +dataviewTarget2:: 14 @ 64 +dataviewTarget3:: 14, 64 #sin:0.20791/0.40674/0.58779/0.74314/0.86603/0.95106/0.99452/0.99452/0.95106 diff --git a/examples/diary/2021-01-03.md b/examples/diary/2021-01-03.md index f8ca1af..82b3cad 100644 --- a/examples/diary/2021-01-03.md +++ b/examples/diary/2021-01-03.md @@ -1,39 +1,38 @@ --- tags: mood: 3 -bloodpressure: 176.2/118.4 -bloodpressure1: 176.2, 118.4 -bloodpressure2: [176.2, 118.4] -clock-in: 8:26 -clock-out: 18:38 -sleep: 11:22 pm/7:49 am +bloodpressure: 174.2/119.4 +bloodpressure1: 174.2, 119.4 +bloodpressure2: [174.2, 119.4] +clock-in: 9:38 +clock-out: 19:22 +sleep: 11:49 pm/5:33 am --- -#weight:64.1kg +#weight:65.5kg -#exercise-pushup:41 -#exercise-plank:53sec +#exercise-pushup:36 +#exercise-plank:74sec -#meditation -⭐⭐ +⭐⭐⭐⭐ -#finance/bank1:-3.6USD + +#finance/bank1:-3.8USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] obsidian-tracker@gmail.com -obsidian-tracker@yahoo.com -weightlifting: 16 +weightlifting: 15 -dataviewTarget:: 28 -- Make Progress:: 27 -- Make-Progress:: 59 -dataviewTarget1:: 28/27 -dataviewTarget2:: 27 @ 59 -dataviewTarget3:: 27, 59 +dataviewTarget:: 7 +- Make Progress:: 10 +- Make-Progress:: 90 +dataviewTarget1:: 7/10 +dataviewTarget2:: 10 @ 90 +dataviewTarget3:: 10, 90 #sin:0.40674/0.58779/0.74314/0.86603/0.95106/0.99452/0.99452/0.95106/0.86603 diff --git a/examples/diary/2021-01-04.md b/examples/diary/2021-01-04.md index 322da2a..ae0d5da 100644 --- a/examples/diary/2021-01-04.md +++ b/examples/diary/2021-01-04.md @@ -1,36 +1,39 @@ --- tags: work_log -mood: 1 -bloodpressure: 174.6/120.2 -bloodpressure1: 174.6, 120.2 -bloodpressure2: [174.6, 120.2] -clock-in: 10:6 -clock-out: 16:17 -sleep: 10:54 pm/5:24 am +mood: 9 +bloodpressure: 173.6/117.2 +bloodpressure1: 173.6, 117.2 +bloodpressure2: [173.6, 117.2] +clock-in: 8:38 +clock-out: 20:14 +sleep: 10:49 pm/7:41 am --- -#weight:77.3kg +#weight:80.3kg -#exercise-pushup:50 -#exercise-plank:93sec +#exercise-pushup:48 +#exercise-plank:60sec +#meditation -⭐⭐⭐⭐⭐ +⭐⭐ -#finance/bank1:-2.7USD + +#finance/bank1:-3.8USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] obsidian-tracker@yahoo.com +weightlifting: 13 -dataviewTarget:: 18 -- Make Progress:: 47 -- Make-Progress:: 67 -dataviewTarget1:: 18/47 -dataviewTarget2:: 47 @ 67 -dataviewTarget3:: 47, 67 +dataviewTarget:: 27 +- Make Progress:: 45 +- Make-Progress:: 89 +dataviewTarget1:: 27/45 +dataviewTarget2:: 45 @ 89 +dataviewTarget3:: 45, 89 #sin:0.58779/0.74314/0.86603/0.95106/0.99452/0.99452/0.95106/0.86603/0.74314 diff --git a/examples/diary/2021-01-05.md b/examples/diary/2021-01-05.md index 0f0d4e7..d990407 100644 --- a/examples/diary/2021-01-05.md +++ b/examples/diary/2021-01-05.md @@ -1,37 +1,39 @@ --- tags: work_log -mood: 3 -bloodpressure: 174/121 -bloodpressure1: 174, 121 -bloodpressure2: [174, 121] -clock-in: 10:38 -clock-out: 18:49 -sleep: 9:25 pm/6:29 am +mood: 7 +bloodpressure: 178/121 +bloodpressure1: 178, 121 +bloodpressure2: [178, 121] +clock-in: 8:25 +clock-out: 18:29 +sleep: 9:10 pm/7:21 am --- -#weight:60.9kg +#weight:72.9kg -#exercise-pushup:33 -#exercise-plank:103sec +#exercise-pushup:30 +#exercise-plank:73sec -#meditation -⭐⭐⭐ -#finance/bank1:-2.0USD + + +#finance/bank1:-2.3USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker+1@gmail.com +obsidian-tracker@gmail.com +obsidian-tracker@yahoo.com +weightlifting: 19 -dataviewTarget:: 97 -- Make Progress:: 6 -- Make-Progress:: 80 -dataviewTarget1:: 97/6 -dataviewTarget2:: 6 @ 80 -dataviewTarget3:: 6, 80 +dataviewTarget:: 38 +- Make Progress:: 13 +- Make-Progress:: 97 +dataviewTarget1:: 38/13 +dataviewTarget2:: 13 @ 97 +dataviewTarget3:: 13, 97 #sin:0.74314/0.86603/0.95106/0.99452/0.99452/0.95106/0.86603/0.74314/0.58779 diff --git a/examples/diary/2021-01-06.md b/examples/diary/2021-01-06.md index 8405eda..0a105d4 100644 --- a/examples/diary/2021-01-06.md +++ b/examples/diary/2021-01-06.md @@ -1,39 +1,38 @@ --- tags: work_log mood: 10 -bloodpressure: 174.4/117.8 -bloodpressure1: 174.4, 117.8 -bloodpressure2: [174.4, 117.8] -clock-in: 8:56 -clock-out: 20:50 -sleep: 10:14 pm/7:40 am +bloodpressure: 180.4/119.8 +bloodpressure1: 180.4, 119.8 +bloodpressure2: [180.4, 119.8] +clock-in: 8:41 +clock-out: 19:51 +sleep: 10:48 pm/6:42 am --- -#weight:78.0kg +#weight:61.0kg -#exercise-pushup:40 -#exercise-plank:102sec +#exercise-pushup:41 +#exercise-plank:101sec #meditation ⭐⭐⭐⭐ -#finance/bank1:-2.1USD + +#finance/bank1:-3.3USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] obsidian-tracker@gmail.com -obsidian-tracker+1@gmail.com -weightlifting: 17 -dataviewTarget:: 67 -- Make Progress:: 26 -- Make-Progress:: 62 -dataviewTarget1:: 67/26 -dataviewTarget2:: 26 @ 62 -dataviewTarget3:: 26, 62 +dataviewTarget:: 80 +- Make Progress:: 44 +- Make-Progress:: 57 +dataviewTarget1:: 80/44 +dataviewTarget2:: 44 @ 57 +dataviewTarget3:: 44, 57 #sin:0.86603/0.95106/0.99452/0.99452/0.95106/0.86603/0.74314/0.58779/0.40674 diff --git a/examples/diary/2021-01-07.md b/examples/diary/2021-01-07.md index 827f95e..de33cd1 100644 --- a/examples/diary/2021-01-07.md +++ b/examples/diary/2021-01-07.md @@ -1,36 +1,40 @@ --- tags: work_log -mood: 5 +mood: 6 bloodpressure: 172.8/116.6 bloodpressure1: 172.8, 116.6 bloodpressure2: [172.8, 116.6] -clock-in: 10:52 -clock-out: 16:30 -sleep: 9:11 pm/5:55 am +clock-in: 8:55 +clock-out: 18:53 +sleep: 9:58 pm/7:29 am --- -#weight:70.4kg +#weight:65.6kg -#exercise-pushup:48 -#exercise-plank:59sec +#exercise-pushup:30 +#exercise-plank:75sec -⭐⭐⭐⭐⭐ +⭐⭐⭐ -#finance/bank1:-3.0USD + +#finance/bank1:-2.8USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] +obsidian-tracker@gmail.com +obsidian-tracker+1@gmail.com +obsidian-tracker@yahoo.com -weightlifting: 19 +weightlifting: 18 -dataviewTarget:: 90 -- Make Progress:: 19 -- Make-Progress:: 83 -dataviewTarget1:: 90/19 -dataviewTarget2:: 19 @ 83 -dataviewTarget3:: 19, 83 +dataviewTarget:: 47 +- Make Progress:: 24 +- Make-Progress:: 59 +dataviewTarget1:: 47/24 +dataviewTarget2:: 24 @ 59 +dataviewTarget3:: 24, 59 #sin:0.95106/0.99452/0.99452/0.95106/0.86603/0.74314/0.58779/0.40674/0.20791 diff --git a/examples/diary/2021-01-08.md b/examples/diary/2021-01-08.md index acec5e3..12f2a96 100644 --- a/examples/diary/2021-01-08.md +++ b/examples/diary/2021-01-08.md @@ -1,36 +1,37 @@ --- tags: work_log -mood: 6 -bloodpressure: 179.2/119.4 -bloodpressure1: 179.2, 119.4 -bloodpressure2: [179.2, 119.4] -clock-in: 8:28 -clock-out: 18:11 -sleep: 11:50 pm/5:45 am +mood: 9 +bloodpressure: 179.2/117.4 +bloodpressure1: 179.2, 117.4 +bloodpressure2: [179.2, 117.4] +clock-in: 10:17 +clock-out: 19:10 +sleep: 11:5 pm/6:26 am --- -#weight:66.1kg +#weight:62.9kg -#exercise-pushup:46 -#exercise-plank:46sec +#exercise-pushup:41 +#exercise-plank:57sec +#meditation +⭐ -#finance/bank1:-3.2USD +#finance/bank1:-2.1USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker@yahoo.com -dataviewTarget:: 21 -- Make Progress:: 0 -- Make-Progress:: 51 -dataviewTarget1:: 21/0 -dataviewTarget2:: 0 @ 51 -dataviewTarget3:: 0, 51 +dataviewTarget:: 58 +- Make Progress:: 48 +- Make-Progress:: 84 +dataviewTarget1:: 58/48 +dataviewTarget2:: 48 @ 84 +dataviewTarget3:: 48, 84 #sin:0.99452/0.99452/0.95106/0.86603/0.74314/0.58779/0.40674/0.20791/0.00000 diff --git a/examples/diary/2021-01-09.md b/examples/diary/2021-01-09.md index 3b97488..ac22602 100644 --- a/examples/diary/2021-01-09.md +++ b/examples/diary/2021-01-09.md @@ -1,40 +1,39 @@ --- tags: -mood: 1 -bloodpressure: 171.6/116.2 -bloodpressure1: 171.6, 116.2 -bloodpressure2: [171.6, 116.2] -clock-in: 8:1 -clock-out: 18:56 -sleep: 11:39 pm/5:31 am +mood: 7 +bloodpressure: 171.6/118.2 +bloodpressure1: 171.6, 118.2 +bloodpressure2: [171.6, 118.2] +clock-in: 9:17 +clock-out: 20:27 +sleep: 10:11 pm/7:51 am --- -#weight:71.1kg +#weight:80.7kg -#exercise-pushup:36 -#exercise-plank:120sec +#exercise-pushup:38 +#exercise-plank:76sec -#meditation -⭐⭐⭐ +⭐⭐⭐⭐ -#finance/bank1:-2.4USD +#clean-up + +#finance/bank1:-3.1USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] obsidian-tracker@gmail.com -obsidian-tracker+1@gmail.com obsidian-tracker@yahoo.com -weightlifting: 14 -dataviewTarget:: 54 -- Make Progress:: 40 -- Make-Progress:: 52 -dataviewTarget1:: 54/40 -dataviewTarget2:: 40 @ 52 -dataviewTarget3:: 40, 52 +dataviewTarget:: 23 +- Make Progress:: 20 +- Make-Progress:: 58 +dataviewTarget1:: 23/20 +dataviewTarget2:: 20 @ 58 +dataviewTarget3:: 20, 58 #sin:0.99452/0.95106/0.86603/0.74314/0.58779/0.40674/0.20791/0.00000/-0.20791 diff --git a/examples/diary/2021-01-10.md b/examples/diary/2021-01-10.md index ae58672..ce5cc33 100644 --- a/examples/diary/2021-01-10.md +++ b/examples/diary/2021-01-10.md @@ -1,39 +1,40 @@ --- tags: -mood: 6 -bloodpressure: 175/117 -bloodpressure1: 175, 117 -bloodpressure2: [175, 117] -clock-in: 9:21 -clock-out: 16:13 -sleep: 10:10 pm/7:40 am +mood: 9 +bloodpressure: 176/118 +bloodpressure1: 176, 118 +bloodpressure2: [176, 118] +clock-in: 8:54 +clock-out: 18:37 +sleep: 11:54 pm/6:52 am --- -#weight:68.8kg +#weight:72.6kg -#exercise-pushup:35 -#exercise-plank:112sec +#exercise-pushup:31 +#exercise-plank:77sec #meditation -⭐⭐⭐ +⭐⭐⭐⭐ -#finance/bank1:-3.8USD +#clean-up + +#finance/bank1:-2.2USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker@gmail.com obsidian-tracker+1@gmail.com -obsidian-tracker@yahoo.com +weightlifting: 20 -dataviewTarget:: 52 -- Make Progress:: 18 -- Make-Progress:: 86 -dataviewTarget1:: 52/18 -dataviewTarget2:: 18 @ 86 -dataviewTarget3:: 18, 86 +dataviewTarget:: 35 +- Make Progress:: 47 +- Make-Progress:: 70 +dataviewTarget1:: 35/47 +dataviewTarget2:: 47 @ 70 +dataviewTarget3:: 47, 70 #sin:0.95106/0.86603/0.74314/0.58779/0.40674/0.20791/0.00000/-0.20791/-0.40674 diff --git a/examples/diary/2021-01-11.md b/examples/diary/2021-01-11.md index 8ccedd2..7be3f2f 100644 --- a/examples/diary/2021-01-11.md +++ b/examples/diary/2021-01-11.md @@ -1,40 +1,41 @@ --- tags: work_log -mood: 1 -bloodpressure: 169.4/116.8 -bloodpressure1: 169.4, 116.8 -bloodpressure2: [169.4, 116.8] -clock-in: 10:5 -clock-out: 20:25 -sleep: 10:55 pm/6:11 am +mood: 2 +bloodpressure: 168.4/117.8 +bloodpressure1: 168.4, 117.8 +bloodpressure2: [168.4, 117.8] +clock-in: 8:6 +clock-out: 18:40 +sleep: 10:31 pm/6:6 am --- -#weight:61.7kg +#weight:72.0kg -#exercise-pushup:41 -#exercise-plank:45sec +#exercise-pushup:30 +#exercise-plank:110sec #meditation -⭐⭐⭐ -#finance/bank1:-3.4USD + +#clean-up + +#finance/bank1:-2.4USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] obsidian-tracker@gmail.com obsidian-tracker+1@gmail.com -obsidian-tracker@yahoo.com -weightlifting: 11 +weightlifting: 15 -dataviewTarget:: 0 -- Make Progress:: 45 -- Make-Progress:: 57 -dataviewTarget1:: 0/45 -dataviewTarget2:: 45 @ 57 -dataviewTarget3:: 45, 57 +dataviewTarget:: 64 +- Make Progress:: 36 +- Make-Progress:: 93 +dataviewTarget1:: 64/36 +dataviewTarget2:: 36 @ 93 +dataviewTarget3:: 36, 93 #sin:0.86603/0.74314/0.58779/0.40674/0.20791/0.00000/-0.20791/-0.40674/-0.58779 diff --git a/examples/diary/2021-01-12.md b/examples/diary/2021-01-12.md index e8a3954..98bb8a5 100644 --- a/examples/diary/2021-01-12.md +++ b/examples/diary/2021-01-12.md @@ -1,39 +1,39 @@ --- tags: work_log -mood: 2 -bloodpressure: 167.8/116.6 -bloodpressure1: 167.8, 116.6 -bloodpressure2: [167.8, 116.6] -clock-in: 10:54 -clock-out: 18:30 -sleep: 10:38 pm/7:51 am +mood: 6 +bloodpressure: 177.8/118.6 +bloodpressure1: 177.8, 118.6 +bloodpressure2: [177.8, 118.6] +clock-in: 9:8 +clock-out: 17:59 +sleep: 11:10 pm/6:41 am --- -#weight:72.4kg +#weight:67.9kg -#exercise-pushup:50 -#exercise-plank:101sec +#exercise-pushup:34 +#exercise-plank:70sec #meditation +⭐⭐⭐ -#finance/bank1:-2.5USD +#finance/bank1:-2.1USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] obsidian-tracker@gmail.com -obsidian-tracker+1@gmail.com +obsidian-tracker@yahoo.com -weightlifting: 16 -dataviewTarget:: 38 -- Make Progress:: 11 -- Make-Progress:: 72 -dataviewTarget1:: 38/11 -dataviewTarget2:: 11 @ 72 -dataviewTarget3:: 11, 72 +dataviewTarget:: 88 +- Make Progress:: 36 +- Make-Progress:: 54 +dataviewTarget1:: 88/36 +dataviewTarget2:: 36 @ 54 +dataviewTarget3:: 36, 54 #sin:0.74314/0.58779/0.40674/0.20791/0.00000/-0.20791/-0.40674/-0.58779/-0.74314 diff --git a/examples/diary/2021-01-13.md b/examples/diary/2021-01-13.md index 49e2b29..2b15abb 100644 --- a/examples/diary/2021-01-13.md +++ b/examples/diary/2021-01-13.md @@ -1,36 +1,40 @@ --- tags: work_log -mood: 4 -bloodpressure: 172.2/116.4 -bloodpressure1: 172.2, 116.4 -bloodpressure2: [172.2, 116.4] -clock-in: 8:57 -clock-out: 17:38 -sleep: 9:0 pm/7:43 am +mood: 10 +bloodpressure: 167.2/116.4 +bloodpressure1: 167.2, 116.4 +bloodpressure2: [167.2, 116.4] +clock-in: 10:23 +clock-out: 17:15 +sleep: 10:4 pm/5:0 am --- -#weight:61.8kg +#weight:67.3kg -#exercise-pushup:49 -#exercise-plank:36sec +#exercise-pushup:43 +#exercise-plank:110sec #meditation ⭐⭐⭐⭐⭐ -#finance/bank1:-2.8USD + +#finance/bank1:-2.4USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] +obsidian-tracker+1@gmail.com +obsidian-tracker@yahoo.com +weightlifting: 17 -dataviewTarget:: 1 -- Make Progress:: 17 -- Make-Progress:: 82 -dataviewTarget1:: 1/17 -dataviewTarget2:: 17 @ 82 -dataviewTarget3:: 17, 82 +dataviewTarget:: 47 +- Make Progress:: 47 +- Make-Progress:: 55 +dataviewTarget1:: 47/47 +dataviewTarget2:: 47 @ 55 +dataviewTarget3:: 47, 55 #sin:0.58779/0.40674/0.20791/0.00000/-0.20791/-0.40674/-0.58779/-0.74314/-0.86603 diff --git a/examples/diary/2021-01-14.md b/examples/diary/2021-01-14.md index d6d4e52..63fb0d9 100644 --- a/examples/diary/2021-01-14.md +++ b/examples/diary/2021-01-14.md @@ -1,37 +1,38 @@ --- tags: work_log -mood: 9 -bloodpressure: 169.6/119.2 -bloodpressure1: 169.6, 119.2 -bloodpressure2: [169.6, 119.2] -clock-in: 9:11 -clock-out: 16:57 -sleep: 11:43 pm/5:28 am +mood: 3 +bloodpressure: 167.6/115.2 +bloodpressure1: 167.6, 115.2 +bloodpressure2: [167.6, 115.2] +clock-in: 9:0 +clock-out: 16:2 +sleep: 10:37 pm/6:33 am --- -#weight:79.5kg +#weight:74.1kg -#exercise-pushup:32 -#exercise-plank:53sec +#exercise-pushup:33 +#exercise-plank:75sec #meditation +⭐⭐⭐ -#finance/bank1:-2.8USD +#finance/bank1:-2.3USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -weightlifting: 15 +weightlifting: 11 -dataviewTarget:: 64 -- Make Progress:: 28 -- Make-Progress:: 84 -dataviewTarget1:: 64/28 -dataviewTarget2:: 28 @ 84 -dataviewTarget3:: 28, 84 +dataviewTarget:: 91 +- Make Progress:: 23 +- Make-Progress:: 54 +dataviewTarget1:: 91/23 +dataviewTarget2:: 23 @ 54 +dataviewTarget3:: 23, 54 #sin:0.40674/0.20791/0.00000/-0.20791/-0.40674/-0.58779/-0.74314/-0.86603/-0.95106 diff --git a/examples/diary/2021-01-15.md b/examples/diary/2021-01-15.md index 9c2cf67..57514b8 100644 --- a/examples/diary/2021-01-15.md +++ b/examples/diary/2021-01-15.md @@ -1,37 +1,38 @@ --- tags: work_log -mood: 2 -bloodpressure: 171/115 -bloodpressure1: 171, 115 -bloodpressure2: [171, 115] -clock-in: 9:41 -clock-out: 16:9 -sleep: 9:13 pm/5:51 am +mood: 5 +bloodpressure: 175/117 +bloodpressure1: 175, 117 +bloodpressure2: [175, 117] +clock-in: 8:53 +clock-out: 17:19 +sleep: 9:40 pm/7:25 am --- -#weight:79.0kg +#weight:72.8kg -#exercise-pushup:39 -#exercise-plank:38sec +#exercise-pushup:47 +#exercise-plank:63sec -#meditation -⭐⭐⭐⭐⭐ +⭐ -#finance/bank1:-2.9USD + +#finance/bank1:-2.7USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] obsidian-tracker+1@gmail.com +weightlifting: 11 -dataviewTarget:: 68 -- Make Progress:: 47 -- Make-Progress:: 71 -dataviewTarget1:: 68/47 -dataviewTarget2:: 47 @ 71 -dataviewTarget3:: 47, 71 +dataviewTarget:: 89 +- Make Progress:: 21 +- Make-Progress:: 55 +dataviewTarget1:: 89/21 +dataviewTarget2:: 21 @ 55 +dataviewTarget3:: 21, 55 #sin:0.20791/0.00000/-0.20791/-0.40674/-0.58779/-0.74314/-0.86603/-0.95106/-0.99452 diff --git a/examples/diary/2021-01-16.md b/examples/diary/2021-01-16.md index 023d888..edc0e18 100644 --- a/examples/diary/2021-01-16.md +++ b/examples/diary/2021-01-16.md @@ -1,38 +1,39 @@ --- tags: -mood: 7 -bloodpressure: 173.4/115.8 -bloodpressure1: 173.4, 115.8 -bloodpressure2: [173.4, 115.8] -clock-in: 9:13 -clock-out: 17:19 -sleep: 9:33 pm/5:6 am +mood: 9 +bloodpressure: 166.4/114.8 +bloodpressure1: 166.4, 114.8 +bloodpressure2: [166.4, 114.8] +clock-in: 10:4 +clock-out: 18:20 +sleep: 10:40 pm/7:14 am --- -#weight:70.1kg +#weight:71.9kg -#exercise-pushup:48 +#exercise-pushup:45 #exercise-plank:69sec #meditation -⭐⭐⭐⭐ +⭐⭐ -#finance/bank1:-2.3USD + +#finance/bank1:-2.1USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] obsidian-tracker+1@gmail.com -weightlifting: 15 +weightlifting: 10 -dataviewTarget:: 58 -- Make Progress:: 34 -- Make-Progress:: 91 -dataviewTarget1:: 58/34 -dataviewTarget2:: 34 @ 91 -dataviewTarget3:: 34, 91 +dataviewTarget:: 86 +- Make Progress:: 20 +- Make-Progress:: 63 +dataviewTarget1:: 86/20 +dataviewTarget2:: 20 @ 63 +dataviewTarget3:: 20, 63 #sin:0.00000/-0.20791/-0.40674/-0.58779/-0.74314/-0.86603/-0.95106/-0.99452/-0.99452 diff --git a/examples/diary/2021-01-17.md b/examples/diary/2021-01-17.md index e594e13..32a9ce7 100644 --- a/examples/diary/2021-01-17.md +++ b/examples/diary/2021-01-17.md @@ -1,39 +1,39 @@ --- tags: -mood: 3 -bloodpressure: 170.8/117.6 -bloodpressure1: 170.8, 117.6 -bloodpressure2: [170.8, 117.6] -clock-in: 9:27 -clock-out: 18:46 -sleep: 9:8 pm/6:15 am +mood: 7 +bloodpressure: 165.8/117.6 +bloodpressure1: 165.8, 117.6 +bloodpressure2: [165.8, 117.6] +clock-in: 10:48 +clock-out: 17:8 +sleep: 10:54 pm/6:3 am --- -#weight:61.7kg +#weight:79.8kg -#exercise-pushup:34 -#exercise-plank:107sec +#exercise-pushup:38 +#exercise-plank:79sec -#meditation -⭐ +⭐⭐⭐⭐ -#finance/bank1:-3.2USD + +#finance/bank1:-2.8USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] +obsidian-tracker@gmail.com obsidian-tracker+1@gmail.com -obsidian-tracker@yahoo.com -weightlifting: 18 +weightlifting: 13 -dataviewTarget:: 14 -- Make Progress:: 27 -- Make-Progress:: 96 -dataviewTarget1:: 14/27 -dataviewTarget2:: 27 @ 96 -dataviewTarget3:: 27, 96 +dataviewTarget:: 5 +- Make Progress:: 35 +- Make-Progress:: 50 +dataviewTarget1:: 5/35 +dataviewTarget2:: 35 @ 50 +dataviewTarget3:: 35, 50 #sin:-0.20791/-0.40674/-0.58779/-0.74314/-0.86603/-0.95106/-0.99452/-0.99452/-0.95106 diff --git a/examples/diary/2021-01-18.md b/examples/diary/2021-01-18.md index 6b772c6..2f387fd 100644 --- a/examples/diary/2021-01-18.md +++ b/examples/diary/2021-01-18.md @@ -1,38 +1,39 @@ --- tags: work_log -mood: 6 -bloodpressure: 164.2/118.4 -bloodpressure1: 164.2, 118.4 -bloodpressure2: [164.2, 118.4] -clock-in: 9:32 -clock-out: 19:48 -sleep: 10:23 pm/7:38 am +mood: 8 +bloodpressure: 165.2/114.4 +bloodpressure1: 165.2, 114.4 +bloodpressure2: [165.2, 114.4] +clock-in: 9:50 +clock-out: 19:13 +sleep: 9:10 pm/5:3 am --- -#weight:68.9kg +#weight:61.2kg -#exercise-pushup:35 -#exercise-plank:57sec +#exercise-pushup:38 +#exercise-plank:73sec -#meditation -⭐⭐⭐⭐ +⭐⭐ -#finance/bank1:-2.0USD + +#finance/bank1:-2.9USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker@gmail.com +obsidian-tracker+1@gmail.com +obsidian-tracker@yahoo.com -weightlifting: 14 +weightlifting: 18 -dataviewTarget:: 73 -- Make Progress:: 11 -- Make-Progress:: 58 -dataviewTarget1:: 73/11 -dataviewTarget2:: 11 @ 58 -dataviewTarget3:: 11, 58 +dataviewTarget:: 67 +- Make Progress:: 41 +- Make-Progress:: 76 +dataviewTarget1:: 67/41 +dataviewTarget2:: 41 @ 76 +dataviewTarget3:: 41, 76 #sin:-0.40674/-0.58779/-0.74314/-0.86603/-0.95106/-0.99452/-0.99452/-0.95106/-0.86603 diff --git a/examples/diary/2021-01-19.md b/examples/diary/2021-01-19.md index b639039..6057d79 100644 --- a/examples/diary/2021-01-19.md +++ b/examples/diary/2021-01-19.md @@ -1,23 +1,24 @@ --- tags: work_log -mood: 2 -bloodpressure: 165.6/114.2 -bloodpressure1: 165.6, 114.2 -bloodpressure2: [165.6, 114.2] -clock-in: 8:23 -clock-out: 18:42 -sleep: 10:30 pm/6:13 am +mood: 1 +bloodpressure: 171.6/117.2 +bloodpressure1: 171.6, 117.2 +bloodpressure2: [171.6, 117.2] +clock-in: 8:27 +clock-out: 19:38 +sleep: 11:7 pm/5:53 am --- -#weight:78.6kg +#weight:63.5kg -#exercise-pushup:44 -#exercise-plank:98sec +#exercise-pushup:46 +#exercise-plank:87sec -⭐⭐⭐ -#finance/bank1:-3.6USD + + +#finance/bank1:-3.7USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] @@ -25,14 +26,14 @@ sleep: 10:30 pm/6:13 am obsidian-tracker@gmail.com obsidian-tracker@yahoo.com -weightlifting: 17 +weightlifting: 16 -dataviewTarget:: 46 -- Make Progress:: 38 -- Make-Progress:: 82 -dataviewTarget1:: 46/38 -dataviewTarget2:: 38 @ 82 -dataviewTarget3:: 38, 82 +dataviewTarget:: 4 +- Make Progress:: 11 +- Make-Progress:: 100 +dataviewTarget1:: 4/11 +dataviewTarget2:: 11 @ 100 +dataviewTarget3:: 11, 100 #sin:-0.58779/-0.74314/-0.86603/-0.95106/-0.99452/-0.99452/-0.95106/-0.86603/-0.74314 diff --git a/examples/diary/2021-01-20.md b/examples/diary/2021-01-20.md index 43c965e..6af17c5 100644 --- a/examples/diary/2021-01-20.md +++ b/examples/diary/2021-01-20.md @@ -1,38 +1,39 @@ --- tags: work_log -mood: 9 -bloodpressure: 164/115 -bloodpressure1: 164, 115 -bloodpressure2: [164, 115] -clock-in: 10:10 -clock-out: 20:37 -sleep: 11:1 pm/5:51 am +mood: 7 +bloodpressure: 169/114 +bloodpressure1: 169, 114 +bloodpressure2: [169, 114] +clock-in: 9:57 +clock-out: 17:16 +sleep: 11:37 pm/6:33 am --- -#weight:78.1kg +#weight:64.3kg -#exercise-pushup:34 -#exercise-plank:111sec +#exercise-pushup:44 +#exercise-plank:45sec -⭐⭐⭐ +⭐ -#finance/bank1:-2.1USD + +#finance/bank1:-2.4USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] +obsidian-tracker@gmail.com obsidian-tracker+1@gmail.com -obsidian-tracker@yahoo.com -weightlifting: 16 +weightlifting: 13 -dataviewTarget:: 44 -- Make Progress:: 48 -- Make-Progress:: 64 -dataviewTarget1:: 44/48 -dataviewTarget2:: 48 @ 64 -dataviewTarget3:: 48, 64 +dataviewTarget:: 82 +- Make Progress:: 21 +- Make-Progress:: 52 +dataviewTarget1:: 82/21 +dataviewTarget2:: 21 @ 52 +dataviewTarget3:: 21, 52 #sin:-0.74314/-0.86603/-0.95106/-0.99452/-0.99452/-0.95106/-0.86603/-0.74314/-0.58779 diff --git a/examples/diary/2021-01-21.md b/examples/diary/2021-01-21.md index 3c2b862..4aff3c6 100644 --- a/examples/diary/2021-01-21.md +++ b/examples/diary/2021-01-21.md @@ -1,38 +1,37 @@ --- tags: work_log -mood: 3 +mood: 4 bloodpressure: 170.4/116.8 bloodpressure1: 170.4, 116.8 bloodpressure2: [170.4, 116.8] -clock-in: 9:33 -clock-out: 17:41 -sleep: 9:30 pm/5:42 am +clock-in: 10:13 +clock-out: 16:37 +sleep: 10:43 pm/5:10 am --- -#weight:63.9kg +#weight:80.1kg -#exercise-pushup:42 -#exercise-plank:112sec +#exercise-pushup:38 +#exercise-plank:37sec -#meditation -⭐ +⭐⭐ -#finance/bank1:-2.9USD + +#finance/bank1:-4.0USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker@gmail.com -weightlifting: 13 +weightlifting: 17 -dataviewTarget:: 76 -- Make Progress:: 38 -- Make-Progress:: 61 -dataviewTarget1:: 76/38 -dataviewTarget2:: 38 @ 61 -dataviewTarget3:: 38, 61 +dataviewTarget:: 56 +- Make Progress:: 50 +- Make-Progress:: 57 +dataviewTarget1:: 56/50 +dataviewTarget2:: 50 @ 57 +dataviewTarget3:: 50, 57 #sin:-0.86603/-0.95106/-0.99452/-0.99452/-0.95106/-0.86603/-0.74314/-0.58779/-0.40674 diff --git a/examples/diary/2021-01-22.md b/examples/diary/2021-01-22.md index 3065d02..0c7c474 100644 --- a/examples/diary/2021-01-22.md +++ b/examples/diary/2021-01-22.md @@ -1,40 +1,39 @@ --- tags: work_log -mood: 1 -bloodpressure: 167.8/114.6 -bloodpressure1: 167.8, 114.6 -bloodpressure2: [167.8, 114.6] -clock-in: 10:9 -clock-out: 16:57 -sleep: 10:4 pm/7:24 am +mood: 7 +bloodpressure: 167.8/116.6 +bloodpressure1: 167.8, 116.6 +bloodpressure2: [167.8, 116.6] +clock-in: 8:35 +clock-out: 16:5 +sleep: 9:49 pm/6:38 am --- -#weight:70.8kg +#weight:60.2kg -#exercise-pushup:50 -#exercise-plank:50sec +#exercise-pushup:31 +#exercise-plank:46sec #meditation -⭐⭐ +⭐⭐⭐⭐ -#finance/bank1:-3.4USD + +#finance/bank1:-2.7USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker@gmail.com obsidian-tracker+1@gmail.com -obsidian-tracker@yahoo.com -weightlifting: 11 +weightlifting: 15 -dataviewTarget:: 63 -- Make Progress:: 33 -- Make-Progress:: 58 -dataviewTarget1:: 63/33 -dataviewTarget2:: 33 @ 58 -dataviewTarget3:: 33, 58 +dataviewTarget:: 48 +- Make Progress:: 32 +- Make-Progress:: 55 +dataviewTarget1:: 48/32 +dataviewTarget2:: 32 @ 55 +dataviewTarget3:: 32, 55 #sin:-0.95106/-0.99452/-0.99452/-0.95106/-0.86603/-0.74314/-0.58779/-0.40674/-0.20791 diff --git a/examples/diary/2021-01-23.md b/examples/diary/2021-01-23.md index 42ec814..35b0478 100644 --- a/examples/diary/2021-01-23.md +++ b/examples/diary/2021-01-23.md @@ -1,37 +1,41 @@ --- tags: -mood: 6 -bloodpressure: 162.2/113.4 -bloodpressure1: 162.2, 113.4 -bloodpressure2: [162.2, 113.4] -clock-in: 8:49 -clock-out: 19:38 -sleep: 9:5 pm/5:21 am +mood: 4 +bloodpressure: 171.2/116.4 +bloodpressure1: 171.2, 116.4 +bloodpressure2: [171.2, 116.4] +clock-in: 9:38 +clock-out: 19:23 +sleep: 11:8 pm/5:50 am --- -#weight:76.1kg +#weight:73.9kg -#exercise-pushup:31 -#exercise-plank:62sec +#exercise-pushup:48 +#exercise-plank:104sec #meditation -⭐⭐⭐⭐ +⭐⭐ -#finance/bank1:-3.0USD + +#finance/bank1:-2.8USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] +obsidian-tracker@gmail.com obsidian-tracker+1@gmail.com +obsidian-tracker@yahoo.com +weightlifting: 18 -dataviewTarget:: 34 -- Make Progress:: 49 -- Make-Progress:: 86 -dataviewTarget1:: 34/49 -dataviewTarget2:: 49 @ 86 -dataviewTarget3:: 49, 86 +dataviewTarget:: 57 +- Make Progress:: 21 +- Make-Progress:: 61 +dataviewTarget1:: 57/21 +dataviewTarget2:: 21 @ 61 +dataviewTarget3:: 21, 61 #sin:-0.99452/-0.99452/-0.95106/-0.86603/-0.74314/-0.58779/-0.40674/-0.20791/-0.00000 diff --git a/examples/diary/2021-01-24.md b/examples/diary/2021-01-24.md index c03e46f..e82e8f2 100644 --- a/examples/diary/2021-01-24.md +++ b/examples/diary/2021-01-24.md @@ -1,39 +1,39 @@ --- tags: -mood: 5 -bloodpressure: 167.6/116.2 -bloodpressure1: 167.6, 116.2 -bloodpressure2: [167.6, 116.2] -clock-in: 9:50 -clock-out: 16:12 -sleep: 11:39 pm/7:48 am +mood: 3 +bloodpressure: 169.6/114.2 +bloodpressure1: 169.6, 114.2 +bloodpressure2: [169.6, 114.2] +clock-in: 8:46 +clock-out: 20:12 +sleep: 10:38 pm/7:16 am --- -#weight:70.5kg +#weight:63.7kg -#exercise-pushup:37 -#exercise-plank:104sec +#exercise-pushup:31 +#exercise-plank:59sec #meditation -⭐⭐⭐ -#finance/bank1:-3.3USD + + +#finance/bank1:-2.2USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker@gmail.com -obsidian-tracker+1@gmail.com +obsidian-tracker@yahoo.com -weightlifting: 16 +weightlifting: 18 -dataviewTarget:: 23 -- Make Progress:: 10 -- Make-Progress:: 92 -dataviewTarget1:: 23/10 -dataviewTarget2:: 10 @ 92 -dataviewTarget3:: 10, 92 +dataviewTarget:: 62 +- Make Progress:: 26 +- Make-Progress:: 68 +dataviewTarget1:: 62/26 +dataviewTarget2:: 26 @ 68 +dataviewTarget3:: 26, 68 #sin:-0.99452/-0.95106/-0.86603/-0.74314/-0.58779/-0.40674/-0.20791/-0.00000/0.20791 diff --git a/examples/diary/2021-01-25.md b/examples/diary/2021-01-25.md index 35ac46f..f730597 100644 --- a/examples/diary/2021-01-25.md +++ b/examples/diary/2021-01-25.md @@ -1,38 +1,39 @@ --- tags: work_log -mood: 3 -bloodpressure: 160/116 -bloodpressure1: 160, 116 -bloodpressure2: [160, 116] -clock-in: 10:12 -clock-out: 19:38 -sleep: 11:16 pm/5:5 am +mood: 2 +bloodpressure: 167/115 +bloodpressure1: 167, 115 +bloodpressure2: [167, 115] +clock-in: 8:20 +clock-out: 20:35 +sleep: 11:46 pm/6:22 am --- -#weight:66.8kg +#weight:71.9kg -#exercise-pushup:37 -#exercise-plank:31sec +#exercise-pushup:35 +#exercise-plank:46sec +#meditation +⭐⭐ -#finance/bank1:-2.0USD +#finance/bank1:-3.1USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] +obsidian-tracker@gmail.com obsidian-tracker+1@gmail.com -obsidian-tracker@yahoo.com -weightlifting: 16 -dataviewTarget:: 52 -- Make Progress:: 18 -- Make-Progress:: 58 -dataviewTarget1:: 52/18 -dataviewTarget2:: 18 @ 58 -dataviewTarget3:: 18, 58 +dataviewTarget:: 21 +- Make Progress:: 1 +- Make-Progress:: 77 +dataviewTarget1:: 21/1 +dataviewTarget2:: 1 @ 77 +dataviewTarget3:: 1, 77 #sin:-0.95106/-0.86603/-0.74314/-0.58779/-0.40674/-0.20791/-0.00000/0.20791/0.40674 diff --git a/examples/diary/2021-01-26.md b/examples/diary/2021-01-26.md index 1a0ed93..4321670 100644 --- a/examples/diary/2021-01-26.md +++ b/examples/diary/2021-01-26.md @@ -1,38 +1,39 @@ --- tags: work_log -mood: 7 -bloodpressure: 164.4/113.8 -bloodpressure1: 164.4, 113.8 -bloodpressure2: [164.4, 113.8] -clock-in: 9:49 -clock-out: 18:41 -sleep: 11:25 pm/6:33 am +mood: 2 +bloodpressure: 162.4/116.8 +bloodpressure1: 162.4, 116.8 +bloodpressure2: [162.4, 116.8] +clock-in: 8:33 +clock-out: 17:11 +sleep: 10:9 pm/5:42 am --- -#weight:65.8kg +#weight:63.4kg -#exercise-pushup:33 -#exercise-plank:54sec +#exercise-pushup:44 +#exercise-plank:107sec #meditation -⭐⭐ +⭐⭐⭐ -#finance/bank1:-3.1USD + +#finance/bank1:-3.9USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] obsidian-tracker@gmail.com -obsidian-tracker+1@gmail.com +weightlifting: 19 -dataviewTarget:: 21 -- Make Progress:: 1 -- Make-Progress:: 77 -dataviewTarget1:: 21/1 -dataviewTarget2:: 1 @ 77 -dataviewTarget3:: 1, 77 +dataviewTarget:: 71 +- Make Progress:: 14 +- Make-Progress:: 83 +dataviewTarget1:: 71/14 +dataviewTarget2:: 14 @ 83 +dataviewTarget3:: 14, 83 #sin:-0.86603/-0.74314/-0.58779/-0.40674/-0.20791/-0.00000/0.20791/0.40674/0.58779 diff --git a/examples/diary/2021-01-27.md b/examples/diary/2021-01-27.md index 497e800..8624562 100644 --- a/examples/diary/2021-01-27.md +++ b/examples/diary/2021-01-27.md @@ -1,39 +1,39 @@ --- tags: work_log -mood: 2 -bloodpressure: 161.8/116.6 -bloodpressure1: 161.8, 116.6 -bloodpressure2: [161.8, 116.6] -clock-in: 8:33 -clock-out: 17:11 -sleep: 10:9 pm/5:42 am +mood: 9 +bloodpressure: 160.8/116.6 +bloodpressure1: 160.8, 116.6 +bloodpressure2: [160.8, 116.6] +clock-in: 9:53 +clock-out: 17:3 +sleep: 11:6 pm/5:52 am --- -#weight:63.4kg +#weight:79.1kg -#exercise-pushup:44 -#exercise-plank:107sec +#exercise-pushup:32 +#exercise-plank:102sec -#meditation -⭐⭐⭐ +⭐ -#finance/bank1:-2.2USD + +#finance/bank1:-2.9USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] obsidian-tracker@gmail.com -obsidian-tracker+1@gmail.com +obsidian-tracker@yahoo.com weightlifting: 12 -dataviewTarget:: 85 -- Make Progress:: 36 -- Make-Progress:: 64 -dataviewTarget1:: 85/36 -dataviewTarget2:: 36 @ 64 -dataviewTarget3:: 36, 64 +dataviewTarget:: 71 +- Make Progress:: 13 +- Make-Progress:: 66 +dataviewTarget1:: 71/13 +dataviewTarget2:: 13 @ 66 +dataviewTarget3:: 13, 66 #sin:-0.74314/-0.58779/-0.40674/-0.20791/-0.00000/0.20791/0.40674/0.58779/0.74314 diff --git a/examples/diary/2021-01-28.md b/examples/diary/2021-01-28.md index 252566c..0d35093 100644 --- a/examples/diary/2021-01-28.md +++ b/examples/diary/2021-01-28.md @@ -1,38 +1,39 @@ --- tags: work_log -mood: 7 -bloodpressure: 167.2/113.4 -bloodpressure1: 167.2, 113.4 -bloodpressure2: [167.2, 113.4] -clock-in: 10:24 -clock-out: 20:21 -sleep: 9:48 pm/5:2 am +mood: 6 +bloodpressure: 166.2/114.4 +bloodpressure1: 166.2, 114.4 +bloodpressure2: [166.2, 114.4] +clock-in: 9:12 +clock-out: 19:36 +sleep: 10:2 pm/7:36 am --- -#weight:78.5kg +#weight:72.2kg #exercise-pushup:49 -#exercise-plank:40sec +#exercise-plank:68sec +#meditation -⭐⭐⭐⭐ +⭐⭐ -#finance/bank1:-2.5USD + +#finance/bank1:-2.6USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker@gmail.com -obsidian-tracker@yahoo.com +obsidian-tracker+1@gmail.com -weightlifting: 10 +weightlifting: 11 -dataviewTarget:: 24 -- Make Progress:: 46 -- Make-Progress:: 85 -dataviewTarget1:: 24/46 -dataviewTarget2:: 46 @ 85 -dataviewTarget3:: 46, 85 +dataviewTarget:: 39 +- Make Progress:: 43 +- Make-Progress:: 76 +dataviewTarget1:: 39/43 +dataviewTarget2:: 43 @ 76 +dataviewTarget3:: 43, 76 #sin:-0.58779/-0.40674/-0.20791/-0.00000/0.20791/0.40674/0.58779/0.74314/0.86603 diff --git a/examples/diary/2021-01-29.md b/examples/diary/2021-01-29.md index 0adad37..8cfa359 100644 --- a/examples/diary/2021-01-29.md +++ b/examples/diary/2021-01-29.md @@ -1,37 +1,38 @@ --- tags: work_log -mood: 3 -bloodpressure: 160.6/114.2 -bloodpressure1: 160.6, 114.2 -bloodpressure2: [160.6, 114.2] -clock-in: 10:24 -clock-out: 19:12 -sleep: 11:36 pm/6:2 am +mood: 4 +bloodpressure: 164.6/113.2 +bloodpressure1: 164.6, 113.2 +bloodpressure2: [164.6, 113.2] +clock-in: 10:28 +clock-out: 18:4 +sleep: 11:34 pm/6:12 am --- -#weight:75.2kg +#weight:76.3kg -#exercise-pushup:42 -#exercise-plank:82sec +#exercise-pushup:33 +#exercise-plank:100sec +#meditation -⭐⭐ +⭐ -#finance/bank1:-2.6USD + +#finance/bank1:-2.3USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker+1@gmail.com -weightlifting: 11 +weightlifting: 18 -dataviewTarget:: 21 -- Make Progress:: 7 -- Make-Progress:: 71 -dataviewTarget1:: 21/7 -dataviewTarget2:: 7 @ 71 -dataviewTarget3:: 7, 71 +dataviewTarget:: 54 +- Make Progress:: 29 +- Make-Progress:: 81 +dataviewTarget1:: 54/29 +dataviewTarget2:: 29 @ 81 +dataviewTarget3:: 29, 81 #sin:-0.40674/-0.20791/-0.00000/0.20791/0.40674/0.58779/0.74314/0.86603/0.95106 diff --git a/examples/diary/2021-01-30.md b/examples/diary/2021-01-30.md index e22783e..e7f7c71 100644 --- a/examples/diary/2021-01-30.md +++ b/examples/diary/2021-01-30.md @@ -1,23 +1,24 @@ --- tags: mood: 4 -bloodpressure: 166/114 -bloodpressure1: 166, 114 -bloodpressure2: [166, 114] -clock-in: 9:41 -clock-out: 17:53 -sleep: 10:28 pm/5:58 am +bloodpressure: 163/112 +bloodpressure1: 163, 112 +bloodpressure2: [163, 112] +clock-in: 8:39 +clock-out: 20:44 +sleep: 9:41 pm/6:27 am --- -#weight:72.2kg +#weight:60.1kg -#exercise-pushup:40 -#exercise-plank:49sec +#exercise-pushup:33 +#exercise-plank:102sec ⭐ -#finance/bank1:-3.5USD + +#finance/bank1:-3.2USD #finance/bank2:200USD #finance/bank2/transfer:-100USD #finance/bank1/transfer:100USD @@ -27,13 +28,14 @@ sleep: 10:28 pm/5:58 am obsidian-tracker@yahoo.com +weightlifting: 19 -dataviewTarget:: 26 -- Make Progress:: 5 -- Make-Progress:: 87 -dataviewTarget1:: 26/5 -dataviewTarget2:: 5 @ 87 -dataviewTarget3:: 5, 87 +dataviewTarget:: 99 +- Make Progress:: 38 +- Make-Progress:: 64 +dataviewTarget1:: 99/38 +dataviewTarget2:: 38 @ 64 +dataviewTarget3:: 38, 64 #sin:-0.20791/-0.00000/0.20791/0.40674/0.58779/0.74314/0.86603/0.95106/0.99452 diff --git a/examples/diary/2021-01-31.md b/examples/diary/2021-01-31.md index ace81d2..dfa3907 100644 --- a/examples/diary/2021-01-31.md +++ b/examples/diary/2021-01-31.md @@ -1,39 +1,38 @@ --- tags: -mood: 2 -bloodpressure: 161.4/113.8 -bloodpressure1: 161.4, 113.8 -bloodpressure2: [161.4, 113.8] -clock-in: 9:21 -clock-out: 19:7 -sleep: 9:39 pm/7:44 am +mood: 4 +bloodpressure: 163.4/115.8 +bloodpressure1: 163.4, 115.8 +bloodpressure2: [163.4, 115.8] +clock-in: 9:44 +clock-out: 18:10 +sleep: 9:19 pm/5:35 am --- -#weight:63.7kg +#weight:78.1kg -#exercise-pushup:44 -#exercise-plank:85sec +#exercise-pushup:46 +#exercise-plank:74sec -#meditation +⭐ -#finance/bank1:-2.4USD +#finance/bank1:-3.2USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker@gmail.com obsidian-tracker+1@gmail.com -weightlifting: 17 +weightlifting: 20 -dataviewTarget:: 15 -- Make Progress:: 25 -- Make-Progress:: 98 -dataviewTarget1:: 15/25 -dataviewTarget2:: 25 @ 98 -dataviewTarget3:: 25, 98 +dataviewTarget:: 6 +- Make Progress:: 2 +- Make-Progress:: 89 +dataviewTarget1:: 6/2 +dataviewTarget2:: 2 @ 89 +dataviewTarget3:: 2, 89 #sin:-0.00000/0.20791/0.40674/0.58779/0.74314/0.86603/0.95106/0.99452/0.99452 diff --git a/examples/example_generator.ts b/examples/example_generator.ts index 7aa3294..6320e92 100644 --- a/examples/example_generator.ts +++ b/examples/example_generator.ts @@ -144,6 +144,15 @@ for ( content += "\n"; + // clean up + let tagCleanUp = "#clean-up"; + let doCleanUp = randomIntFromInterval(0, 30); + if (doCleanUp === 1) { + content += tagCleanUp + "\n"; + } + + content += "\n"; + // finance let tagFinanceBank1 = "#finance/bank1"; let tagFinanceBank2 = "#finance/bank2"; diff --git a/src/rendering.ts b/src/rendering.ts index 286acd3..46fa7ac 100644 --- a/src/rendering.ts +++ b/src/rendering.ts @@ -1538,7 +1538,7 @@ let fnSet = { } return maxStreak; }, - maxBreak: function (renderInfo: RenderInfo, datasetId: number) { + maxBreaks: function (renderInfo: RenderInfo, datasetId: number) { let streak = 0; let maxBreak = 0; let dataset = renderInfo.datasets.getDatasetById(datasetId); @@ -1569,6 +1569,20 @@ let fnSet = { } return streak; }, + lastBreaks: function (renderInfo: RenderInfo, datasetId: number) { + let breakDays = 0; + let dataset = renderInfo.datasets.getDatasetById(datasetId); + let values = dataset.getValues(); + for (let ind = values.length - 1; ind >= 0; ind--) { + let value = values[ind]; + if (value === null) { + breakDays++; + } else { + break; + } + } + return breakDays; + }, average: function (renderInfo: RenderInfo, datasetId: number) { let dataset = renderInfo.datasets.getDatasetById(datasetId); let countNotNull = dataset.getLengthNotNull(); From a1bd5c464492503974fef58eb7978bc5afeece1f Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Mon, 24 May 2021 18:31:37 +0800 Subject: [PATCH 008/402] Refactoring --- src/rendering.ts | 261 +---------------------------------------------- src/summary.ts | 257 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 259 insertions(+), 259 deletions(-) create mode 100644 src/summary.ts diff --git a/src/rendering.ts b/src/rendering.ts index 46fa7ac..b28683d 100644 --- a/src/rendering.ts +++ b/src/rendering.ts @@ -4,14 +4,12 @@ import { DataPoint, RenderInfo, Dataset, - LineInfo, - BarInfo, Size, Transform, - CommonChartInfo, ChartElements, OutputType, } from "./data"; +import * as summary from './summary'; function getTickInterval(datasets: Datasets) { let tickInterval; @@ -122,7 +120,7 @@ export function render(canvas: HTMLElement, renderInfo: RenderInfo) { case OutputType.Bar: return renderBarChart(canvas, renderInfo); case OutputType.Summary: - return renderSummary(canvas, renderInfo); + return summary.renderSummary(canvas, renderInfo); default: return "Unknown output type"; } @@ -1496,261 +1494,6 @@ function renderBarChart(canvas: HTMLElement, renderInfo: RenderInfo) { setChartScale(canvas, chartElements, renderInfo); } -function checkSummaryTemplateValid(summaryTemplate: string): boolean { - return true; -} - -let fnSet = { - min: function (renderInfo: RenderInfo, datasetId: number) { - let dataset = renderInfo.datasets.getDatasetById(datasetId); - return d3.min(dataset.getValues()); - }, - max: function (renderInfo: RenderInfo, datasetId: number) { - let dataset = renderInfo.datasets.getDatasetById(datasetId); - return d3.max(dataset.getValues()); - }, - sum: function (renderInfo: RenderInfo, datasetId: number) { - let dataset = renderInfo.datasets.getDatasetById(datasetId); - return d3.sum(dataset.getValues()); - }, - count: function (renderInfo: RenderInfo, datasetId: number) { - let dataset = renderInfo.datasets.getDatasetById(datasetId); - return dataset.getLengthNotNull(); - }, - days: function (renderInfo: RenderInfo, datasetId: number) { - let dataset = renderInfo.datasets.getDatasetById(datasetId); - let result = dataset.getLength(); - return result; - }, - maxStreak: function (renderInfo: RenderInfo, datasetId: number) { - let streak = 0; - let maxStreak = 0; - let dataset = renderInfo.datasets.getDatasetById(datasetId); - for (let dataPoint of dataset) { - if (dataPoint.value !== null) { - streak++; - } else { - streak = 0; - } - if (streak > maxStreak) { - maxStreak = streak; - } - } - return maxStreak; - }, - maxBreaks: function (renderInfo: RenderInfo, datasetId: number) { - let streak = 0; - let maxBreak = 0; - let dataset = renderInfo.datasets.getDatasetById(datasetId); - - for (let dataPoint of dataset) { - if (dataPoint.value === null) { - streak++; - } else { - streak = 0; - } - if (streak > maxBreak) { - maxBreak = streak; - } - } - return maxBreak; - }, - lastStreak: function (renderInfo: RenderInfo, datasetId: number) { - let streak = 0; - let dataset = renderInfo.datasets.getDatasetById(datasetId); - let values = dataset.getValues(); - for (let ind = values.length - 1; ind >= 0; ind--) { - let value = values[ind]; - if (value === null) { - break; - } else { - streak++; - } - } - return streak; - }, - lastBreaks: function (renderInfo: RenderInfo, datasetId: number) { - let breakDays = 0; - let dataset = renderInfo.datasets.getDatasetById(datasetId); - let values = dataset.getValues(); - for (let ind = values.length - 1; ind >= 0; ind--) { - let value = values[ind]; - if (value === null) { - breakDays++; - } else { - break; - } - } - return breakDays; - }, - average: function (renderInfo: RenderInfo, datasetId: number) { - let dataset = renderInfo.datasets.getDatasetById(datasetId); - let countNotNull = dataset.getLengthNotNull(); - if (countNotNull > 0) { - let sum = d3.sum(dataset.getValues()); - return sum / countNotNull; - } - return null; - }, - median: function (renderInfo: RenderInfo, datasetId: number) { - let dataset = renderInfo.datasets.getDatasetById(datasetId); - return d3.median(dataset.getValues()); - }, - variance: function (renderInfo: RenderInfo, datasetId: number) { - let dataset = renderInfo.datasets.getDatasetById(datasetId); - return d3.variance(dataset.getValues()); - }, -}; - -function renderSummary(canvas: HTMLElement, renderInfo: RenderInfo) { - // console.log("renderSummary"); - // console.log(renderInfo); - if (renderInfo.summary === null) return; - - let outputSummary = ""; - if (checkSummaryTemplateValid(renderInfo.summary.template)) { - outputSummary = renderInfo.summary.template; - } else { - return "Invalid summary template"; - } - - let replaceMap: { [key: string]: string } = {}; - // Loop over fnSet, prepare replaceMap - Object.entries(fnSet).forEach(([fnName, fn]) => { - // {{\s*max(\(\s*Dataset\(\s*(?\d+)\s*\)\s*\))?\s*}} - let strRegex = - "{{\\s*" + - fnName + - "(\\(\\s*Dataset\\(\\s*((?\\d+)|(?\\w+))\\s*\\)\\s*\\))?\\s*}}"; - // console.log(strRegex); - let regex = new RegExp(strRegex, "gm"); - let match; - while ((match = regex.exec(outputSummary))) { - // console.log(match); - if (typeof match.groups !== "undefined") { - if (typeof match.groups.datasetId !== "undefined") { - let datasetId = parseInt(match.groups.datasetId); - // console.log(datasetId); - if (Number.isInteger(datasetId)) { - let strReplaceRegex = - "{{\\s*" + - fnName + - "(\\(\\s*Dataset\\(\\s*" + - datasetId.toString() + - "\\s*\\)\\s*\\))?\\s*}}"; - - if (!(strReplaceRegex in replaceMap)) { - let result = fn(renderInfo, datasetId); // calculate result - let strResult = "{{NA}}"; - if ( - typeof result !== "undefined" && - result !== null - ) { - if (Number.isInteger(result)) { - strResult = result.toFixed(0); - } else { - strResult = result.toFixed(2); - } - } - - replaceMap[strReplaceRegex] = strResult; - } - } - } else if (typeof match.groups.datasetName !== "undefined") { - let datasetName = match.groups.datasetName; - // console.log(datasetName); - let strReplaceRegex = - "{{\\s*" + - fnName + - "(\\(\\s*Dataset\\(\\s*" + - datasetName + - "\\s*\\)\\s*\\))?\\s*}}"; - - let datasetId = renderInfo.datasetName.indexOf(datasetName); - // console.log(datasetName); - // console.log(renderInfo.datasetName); - // console.log(datasetId); - if (!(strReplaceRegex in replaceMap)) { - let strResult = "{{NA}}"; - if (datasetId >= 0) { - let result = fn(renderInfo, datasetId); // calculate result - if ( - typeof result !== "undefined" && - result !== null - ) { - if (Number.isInteger(result)) { - strResult = result.toFixed(0); - } else { - strResult = result.toFixed(2); - } - } - } - replaceMap[strReplaceRegex] = strResult; - } - } else { - // no datasetId assigned use id 0 - // console.log("{{" + fnName + "}}") - let strReplaceRegex = "{{\\s*" + fnName + "\\s*}}"; - if (!(strReplaceRegex in replaceMap)) { - let result = fn(renderInfo, 0); // calculate result - let strResult = "{{NA}}"; - if (typeof result !== "undefined" && result !== null) { - if (Number.isInteger(result)) { - strResult = result.toFixed(0); - } else { - strResult = result.toFixed(2); - } - } - - replaceMap[strReplaceRegex] = strResult; - } - } - } else { - // groups undefined - // no datasetId assigned use id 0 - // console.log("{{" + fnName + "}}") - let strReplaceRegex = "{{\\s*" + fnName + "\\s*}}"; - if (!(strReplaceRegex in replaceMap)) { - let result = fn(renderInfo, 0); // calculate result - let strResult = "{{NA}}"; - if (typeof result !== "undefined" && result !== null) { - if (Number.isInteger(result)) { - strResult = result.toFixed(0); - } else { - strResult = result.toFixed(2); - } - } - - replaceMap[strReplaceRegex] = strResult; - } - } - } - }); - // console.log(replaceMap); - // Do replace - for (let strReplaceRegex in replaceMap) { - let strResult = replaceMap[strReplaceRegex]; - let regex = new RegExp(strReplaceRegex, "gi"); - outputSummary = outputSummary.replace(regex, strResult); - } - - if (outputSummary !== "") { - let textBlock = d3.select(canvas).append("div"); - if (outputSummary.includes("\n")) { - let outputLines = outputSummary.split("\n"); - for (let outputLine of outputLines) { - textBlock.append("div").text(outputLine); - } - } else { - textBlock.text(outputSummary); - } - - if (renderInfo.summary.style !== "") { - textBlock.attr("style", renderInfo.summary.style); - } - } -} - export function renderErrorMessage(canvas: HTMLElement, errorMessage: string) { let svg = d3 .select(canvas) diff --git a/src/summary.ts b/src/summary.ts new file mode 100644 index 0000000..3de3821 --- /dev/null +++ b/src/summary.ts @@ -0,0 +1,257 @@ +import * as d3 from "d3"; +import { RenderInfo } from "./data"; + +function checkSummaryTemplateValid(summaryTemplate: string): boolean { + return true; +} + +let fnSet = { + min: function (renderInfo: RenderInfo, datasetId: number) { + let dataset = renderInfo.datasets.getDatasetById(datasetId); + return d3.min(dataset.getValues()); + }, + max: function (renderInfo: RenderInfo, datasetId: number) { + let dataset = renderInfo.datasets.getDatasetById(datasetId); + return d3.max(dataset.getValues()); + }, + sum: function (renderInfo: RenderInfo, datasetId: number) { + let dataset = renderInfo.datasets.getDatasetById(datasetId); + return d3.sum(dataset.getValues()); + }, + count: function (renderInfo: RenderInfo, datasetId: number) { + let dataset = renderInfo.datasets.getDatasetById(datasetId); + return dataset.getLengthNotNull(); + }, + days: function (renderInfo: RenderInfo, datasetId: number) { + let dataset = renderInfo.datasets.getDatasetById(datasetId); + let result = dataset.getLength(); + return result; + }, + maxStreak: function (renderInfo: RenderInfo, datasetId: number) { + let streak = 0; + let maxStreak = 0; + let dataset = renderInfo.datasets.getDatasetById(datasetId); + for (let dataPoint of dataset) { + if (dataPoint.value !== null) { + streak++; + } else { + streak = 0; + } + if (streak > maxStreak) { + maxStreak = streak; + } + } + return maxStreak; + }, + maxBreaks: function (renderInfo: RenderInfo, datasetId: number) { + let streak = 0; + let maxBreak = 0; + let dataset = renderInfo.datasets.getDatasetById(datasetId); + + for (let dataPoint of dataset) { + if (dataPoint.value === null) { + streak++; + } else { + streak = 0; + } + if (streak > maxBreak) { + maxBreak = streak; + } + } + return maxBreak; + }, + lastStreak: function (renderInfo: RenderInfo, datasetId: number) { + let streak = 0; + let dataset = renderInfo.datasets.getDatasetById(datasetId); + let values = dataset.getValues(); + for (let ind = values.length - 1; ind >= 0; ind--) { + let value = values[ind]; + if (value === null) { + break; + } else { + streak++; + } + } + return streak; + }, + lastBreaks: function (renderInfo: RenderInfo, datasetId: number) { + let breakDays = 0; + let dataset = renderInfo.datasets.getDatasetById(datasetId); + let values = dataset.getValues(); + for (let ind = values.length - 1; ind >= 0; ind--) { + let value = values[ind]; + if (value === null) { + breakDays++; + } else { + break; + } + } + return breakDays; + }, + average: function (renderInfo: RenderInfo, datasetId: number) { + let dataset = renderInfo.datasets.getDatasetById(datasetId); + let countNotNull = dataset.getLengthNotNull(); + if (countNotNull > 0) { + let sum = d3.sum(dataset.getValues()); + return sum / countNotNull; + } + return null; + }, + median: function (renderInfo: RenderInfo, datasetId: number) { + let dataset = renderInfo.datasets.getDatasetById(datasetId); + return d3.median(dataset.getValues()); + }, + variance: function (renderInfo: RenderInfo, datasetId: number) { + let dataset = renderInfo.datasets.getDatasetById(datasetId); + return d3.variance(dataset.getValues()); + }, +}; + +export function renderSummary(canvas: HTMLElement, renderInfo: RenderInfo) { + // console.log("renderSummary"); + // console.log(renderInfo); + if (renderInfo.summary === null) return; + + let outputSummary = ""; + if (checkSummaryTemplateValid(renderInfo.summary.template)) { + outputSummary = renderInfo.summary.template; + } else { + return "Invalid summary template"; + } + + let replaceMap: { [key: string]: string } = {}; + // Loop over fnSet, prepare replaceMap + Object.entries(fnSet).forEach(([fnName, fn]) => { + // {{\s*max(\(\s*Dataset\(\s*(?\d+)\s*\)\s*\))?\s*}} + let strRegex = + "{{\\s*" + + fnName + + "(\\(\\s*Dataset\\(\\s*((?\\d+)|(?\\w+))\\s*\\)\\s*\\))?\\s*}}"; + // console.log(strRegex); + let regex = new RegExp(strRegex, "gm"); + let match; + while ((match = regex.exec(outputSummary))) { + // console.log(match); + if (typeof match.groups !== "undefined") { + if (typeof match.groups.datasetId !== "undefined") { + let datasetId = parseInt(match.groups.datasetId); + // console.log(datasetId); + if (Number.isInteger(datasetId)) { + let strReplaceRegex = + "{{\\s*" + + fnName + + "(\\(\\s*Dataset\\(\\s*" + + datasetId.toString() + + "\\s*\\)\\s*\\))?\\s*}}"; + + if (!(strReplaceRegex in replaceMap)) { + let result = fn(renderInfo, datasetId); // calculate result + let strResult = "{{NA}}"; + if ( + typeof result !== "undefined" && + result !== null + ) { + if (Number.isInteger(result)) { + strResult = result.toFixed(0); + } else { + strResult = result.toFixed(2); + } + } + + replaceMap[strReplaceRegex] = strResult; + } + } + } else if (typeof match.groups.datasetName !== "undefined") { + let datasetName = match.groups.datasetName; + // console.log(datasetName); + let strReplaceRegex = + "{{\\s*" + + fnName + + "(\\(\\s*Dataset\\(\\s*" + + datasetName + + "\\s*\\)\\s*\\))?\\s*}}"; + + let datasetId = renderInfo.datasetName.indexOf(datasetName); + // console.log(datasetName); + // console.log(renderInfo.datasetName); + // console.log(datasetId); + if (!(strReplaceRegex in replaceMap)) { + let strResult = "{{NA}}"; + if (datasetId >= 0) { + let result = fn(renderInfo, datasetId); // calculate result + if ( + typeof result !== "undefined" && + result !== null + ) { + if (Number.isInteger(result)) { + strResult = result.toFixed(0); + } else { + strResult = result.toFixed(2); + } + } + } + replaceMap[strReplaceRegex] = strResult; + } + } else { + // no datasetId assigned use id 0 + // console.log("{{" + fnName + "}}") + let strReplaceRegex = "{{\\s*" + fnName + "\\s*}}"; + if (!(strReplaceRegex in replaceMap)) { + let result = fn(renderInfo, 0); // calculate result + let strResult = "{{NA}}"; + if (typeof result !== "undefined" && result !== null) { + if (Number.isInteger(result)) { + strResult = result.toFixed(0); + } else { + strResult = result.toFixed(2); + } + } + + replaceMap[strReplaceRegex] = strResult; + } + } + } else { + // groups undefined + // no datasetId assigned use id 0 + // console.log("{{" + fnName + "}}") + let strReplaceRegex = "{{\\s*" + fnName + "\\s*}}"; + if (!(strReplaceRegex in replaceMap)) { + let result = fn(renderInfo, 0); // calculate result + let strResult = "{{NA}}"; + if (typeof result !== "undefined" && result !== null) { + if (Number.isInteger(result)) { + strResult = result.toFixed(0); + } else { + strResult = result.toFixed(2); + } + } + + replaceMap[strReplaceRegex] = strResult; + } + } + } + }); + // console.log(replaceMap); + // Do replace + for (let strReplaceRegex in replaceMap) { + let strResult = replaceMap[strReplaceRegex]; + let regex = new RegExp(strReplaceRegex, "gi"); + outputSummary = outputSummary.replace(regex, strResult); + } + + if (outputSummary !== "") { + let textBlock = d3.select(canvas).append("div"); + if (outputSummary.includes("\n")) { + let outputLines = outputSummary.split("\n"); + for (let outputLine of outputLines) { + textBlock.append("div").text(outputLine); + } + } else { + textBlock.text(outputSummary); + } + + if (renderInfo.summary.style !== "") { + textBlock.attr("style", renderInfo.summary.style); + } + } +} From 7645873561ce24d3d313759e54909661bf98e9f6 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Wed, 26 May 2021 08:55:33 +0800 Subject: [PATCH 009/402] Refactoring --- src/collecting.ts | 13 ++++++------ src/data.ts | 54 +++++++++++++++++++++++------------------------ src/main.ts | 3 +-- src/parsing.ts | 14 ++++++++++++ src/rendering.ts | 3 ++- 5 files changed, 51 insertions(+), 36 deletions(-) diff --git a/src/collecting.ts b/src/collecting.ts index 8200ab6..08fbc4a 100644 --- a/src/collecting.ts +++ b/src/collecting.ts @@ -7,6 +7,7 @@ import { QueryValuePair, TableData, SearchType, + ValueType } from "./data"; import * as helper from "./helper"; @@ -108,7 +109,7 @@ export function collectDataFromFrontmatterKey( // time value let timeValue = window.moment(toParse, timeFormat, true); if (timeValue.isValid()) { - query.setUsingTimeValue(); + query.valueType = ValueType.Time; value = timeValue.diff( window.moment("00:00", "HH:mm", true), "seconds" @@ -159,7 +160,7 @@ export function collectDataFromFrontmatterKey( true ); if (timeValue.isValid()) { - query.setUsingTimeValue(); + query.valueType = ValueType.Time; value = timeValue.diff( window.moment("00:00", "HH:mm", true), "seconds" @@ -242,7 +243,7 @@ export function collectDataFromInlineTag( if (toParse.includes(":")) { let timeValue = window.moment(toParse, timeFormat, true); if (timeValue.isValid()) { - query.setUsingTimeValue(); + query.valueType = ValueType.Time; tagMeasure = timeValue.diff( window.moment("00:00", "HH:mm", true), "seconds" @@ -272,7 +273,7 @@ export function collectDataFromInlineTag( if (toParse.includes(":")) { let timeValue = window.moment(toParse, timeFormat, true); if (timeValue.isValid()) { - query.setUsingTimeValue(); + query.valueType = ValueType.Time; tagMeasure = timeValue.diff( window.moment("00:00", "HH:mm", true), "seconds" @@ -388,7 +389,7 @@ export function collectDataFromDvField( if (toParse.includes(":")) { let timeValue = window.moment(toParse, timeFormat, true); if (timeValue.isValid()) { - query.setUsingTimeValue(); + query.valueType = ValueType.Time; tagMeasure = timeValue.diff( window.moment("00:00", "HH:mm", true), "seconds" @@ -418,7 +419,7 @@ export function collectDataFromDvField( if (toParse.includes(":")) { let timeValue = window.moment(toParse, timeFormat, true); if (timeValue.isValid()) { - query.setUsingTimeValue(); + query.valueType = ValueType.Time; tagMeasure = timeValue.diff( window.moment("00:00", "HH:mm", true), "seconds" diff --git a/src/data.ts b/src/data.ts index 842175c..3e79d10 100644 --- a/src/data.ts +++ b/src/data.ts @@ -1,7 +1,5 @@ import { Moment } from "moment"; -export type NullableNumber = number | null; - export enum SearchType { Tag, Frontmatter, @@ -20,11 +18,20 @@ export enum OutputType { Heatmap, } +export enum ValueType { + Number, + Int, + Date, + Time, + DateTime, + String +} + export class DataPoint { date: Moment; - value: NullableNumber; + value: number; - constructor(date: Moment, value: NullableNumber) { + constructor(date: Moment, value: number) { this.date = date; this.value = value; } @@ -40,7 +47,7 @@ export class Query { private accessor1: number; private accessor2: number; - private valueIsTime: boolean; + valueType: ValueType; usedAsXDataset: boolean; constructor(id: number, searchType: SearchType, searchTarget: string) { @@ -51,7 +58,7 @@ export class Query { this.accessor = -1; this.accessor1 = -1; this.accessor2 = -1; - this.valueIsTime = false; + this.valueType = ValueType.Number; this.usedAsXDataset = false; if (searchType === SearchType.Table) { @@ -145,14 +152,6 @@ export class Query { return null; } - public isUsingTimeValue() { - return this.valueIsTime; - } - - public setUsingTimeValue() { - this.valueIsTime = true; - } - public setSeparator(sep: string) { this.separator = sep; } @@ -164,21 +163,22 @@ export class Query { export interface QueryValuePair { query: Query; - value: NullableNumber; + value: number; } export class Dataset implements IterableIterator { // Array of DataPoints private name: string; private query: Query; - private values: NullableNumber[]; + private values: number[]; private parent: Datasets; private id: number; - private yMin: NullableNumber; - private yMax: NullableNumber; + private yMin: number; + private yMax: number; private lineInfo: LineInfo; private barInfo: BarInfo; - private valueIsTime: boolean; + + valueType: ValueType; private currentIndex = 0; // IterableIterator @@ -192,7 +192,7 @@ export class Dataset implements IterableIterator { this.yMax = null; this.lineInfo = null; this.barInfo = null; - this.valueIsTime = query.isUsingTimeValue(); + this.valueType = query.valueType; for (let ind = 0; ind < parent.getDates().length; ind++) { this.values.push(null); @@ -230,7 +230,7 @@ export class Dataset implements IterableIterator { this.id = id; } - public setValue(date: Moment, value: NullableNumber) { + public setValue(date: Moment, value: number) { let ind = this.parent.getIndexOfDate(date); // console.log(ind); if (ind >= 0) { @@ -271,10 +271,6 @@ export class Dataset implements IterableIterator { return this.query; } - public isUsingTimeValue() { - return this.valueIsTime; - } - public accumulateValues() { let accumValue = 0.0; for (let ind = 0; ind < this.values.length; ind++) { @@ -471,6 +467,8 @@ export class RenderInfo { ignoreZeroValue: boolean[]; accum: boolean[]; penalty: number[]; + shift: number[]; + valueType: string[];// number/float, int, string, boolean, date, time, datetime dataAreaSize: Size; margin: Margin; @@ -501,6 +499,8 @@ export class RenderInfo { this.ignoreZeroValue = []; // false this.accum = []; // false, accum values start from zero over days this.penalty = []; // null, use this value instead of null value + this.shift = []; + this.valueType = []; this.dataAreaSize = new Size(300, 300); this.margin = new Margin(10, 10, 10, 10); // top, right, bottom, left @@ -535,8 +535,8 @@ export class CommonChartInfo { yAxisColor: string[]; yAxisLabelColor: string[]; yAxisUnit: string[]; - yMin: NullableNumber[]; - yMax: NullableNumber[]; + yMin: number[]; + yMax: number[]; allowInspectData: boolean; showLegend: boolean; legendPosition: string; diff --git a/src/main.ts b/src/main.ts index cd2153d..64ef50a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,6 @@ import { TFile, TFolder, normalizePath } from "obsidian"; import { render, renderErrorMessage } from "./rendering"; import { getRenderInfoFromYaml } from "./parsing"; import { - NullableNumber, Datasets, Query, QueryValuePair, @@ -123,7 +122,7 @@ export default class Tracker extends Plugin { dataMap: DataMap, date: string, query: Query, - value: NullableNumber + value: number ) { if (!dataMap.has(date)) { let queryValuePairs = new Array(); diff --git a/src/parsing.ts b/src/parsing.ts index 12e1041..2ac75b2 100644 --- a/src/parsing.ts +++ b/src/parsing.ts @@ -1097,6 +1097,20 @@ export function getRenderInfoFromYaml( renderInfo.penalty = retPenalty; // console.log(renderInfo.penalty); + // shift + let retShift = getNumberArrayFromInput( + "shift", + yaml.shift, + numDatasets, + null, + true + ); + if (typeof retShift === "string") { + return retShift; + } + renderInfo.shift = retShift; + // console.log(renderInfo.shift); + // fixedScale if (typeof yaml.fixedScale === "number") { renderInfo.fixedScale = yaml.fixedScale; diff --git a/src/rendering.ts b/src/rendering.ts index b28683d..f5a6501 100644 --- a/src/rendering.ts +++ b/src/rendering.ts @@ -8,6 +8,7 @@ import { Transform, ChartElements, OutputType, + ValueType, } from "./data"; import * as summary from './summary'; @@ -251,7 +252,7 @@ function renderYAxis( } // Need all datasets have same settings for time value - valueIsTime = dataset.isUsingTimeValue(); + valueIsTime = dataset.valueType === ValueType.Time; if (tmpValueIsTime === null) { tmpValueIsTime = valueIsTime; } else { From 868c30a2ff6a61a465528858c9e682254e2db2b5 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Thu, 13 May 2021 10:54:44 +0800 Subject: [PATCH 010/402] Prepare inputs for rendering calendar --- examples/TestCalendar.md | 9 +++++++++ src/data.ts | 12 +++++++++++- src/parsing.ts | 42 +++++++++++++++++++++++++++++++++++++--- src/rendering.ts | 8 ++++++++ 4 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 examples/TestCalendar.md diff --git a/examples/TestCalendar.md b/examples/TestCalendar.md new file mode 100644 index 0000000..e986adc --- /dev/null +++ b/examples/TestCalendar.md @@ -0,0 +1,9 @@ +# Test Calendar + +``` tracker +searchType: tag +searchTarget: meditation +folder: diary +calendar: + heatMap: false +``` \ No newline at end of file diff --git a/src/data.ts b/src/data.ts index 3e79d10..f76c158 100644 --- a/src/data.ts +++ b/src/data.ts @@ -15,7 +15,7 @@ export enum OutputType { Radar, Summary, Table, - Heatmap, + Calendar, } export enum ValueType { @@ -481,6 +481,7 @@ export class RenderInfo { line: LineInfo | null; bar: BarInfo | null; summary: SummaryInfo | null; + calendar: CalendarInfo | null; public datasets: Datasets | null; @@ -513,6 +514,7 @@ export class RenderInfo { this.line = null; this.summary = null; this.bar = null; + this.calendar = null; this.datasets = null; } @@ -612,6 +614,14 @@ export class SummaryInfo { } } +export class CalendarInfo { + heatMap: boolean; + + constructor() { + this.heatMap = false; + } +} + export class Size { width: number; height: number; diff --git a/src/parsing.ts b/src/parsing.ts index 2ac75b2..ffe39c8 100644 --- a/src/parsing.ts +++ b/src/parsing.ts @@ -9,6 +9,7 @@ import { Margin, OutputType, LineInfo, + CalendarInfo, } from "./data"; import { TFolder, normalizePath } from "obsidian"; import { parseYaml } from "obsidian"; @@ -1150,15 +1151,24 @@ export function getRenderInfoFromYaml( if (typeof yaml.summary !== "undefined") { hasSummary = true; } - let sumOutput = Number(hasLine) + Number(hasBar) + Number(hasSummary); + let hasCalendar = false; + if (typeof yaml.calendar !== "undefined") { + hasCalendar = true; + } + let sumOutput = + Number(hasLine) + + Number(hasBar) + + Number(hasSummary) + + Number(hasCalendar); if (sumOutput === 0) { - return "No output parameter provided, please place line, bar, or summary."; + return "No output parameter provided, please place line, bar, calendar, or summary."; } else if (sumOutput === 1) { if (hasLine) renderInfo.output = OutputType.Line; if (hasBar) renderInfo.output = OutputType.Bar; if (hasSummary) renderInfo.output = OutputType.Summary; + if (hasCalendar) renderInfo.output = OutputType.Calendar; } else if (sumOutput >= 2) { - return "Too many output parameters, pick line, bar, or summary."; + return "Too many output parameters, pick line, bar, calendar, or summary."; } // line related parameters @@ -1411,6 +1421,32 @@ export function getRenderInfoFromYaml( } } } // summary related parameters + // calendar related parameters + if (renderInfo.output === OutputType.Calendar) { + renderInfo.calendar = new CalendarInfo(); + + if (yaml.calendar !== null) { + let keysOfSummaryInfo = getAvailableKeysOfClass( + renderInfo.calendar + ); + let keysFoundInYAML = getAvailableKeysOfClass(yaml.calendar); + // console.log(keysOfSummaryInfo); + // console.log(keysFoundInYAML); + for (let key of keysFoundInYAML) { + if (!keysOfSummaryInfo.includes(key)) { + errorMessage = "'" + key + "' is not an available key"; + return errorMessage; + } + } + } + + if (yaml.calendar !== null) { + // heatMap + if (typeof yaml.calendar.heatMap === "boolean") { + renderInfo.calendar.heatMap = yaml.calendar.heatMap; + } + } + } // calendar related parameters return renderInfo; } diff --git a/src/rendering.ts b/src/rendering.ts index f5a6501..69df1d8 100644 --- a/src/rendering.ts +++ b/src/rendering.ts @@ -122,6 +122,8 @@ export function render(canvas: HTMLElement, renderInfo: RenderInfo) { return renderBarChart(canvas, renderInfo); case OutputType.Summary: return summary.renderSummary(canvas, renderInfo); + case OutputType.Calendar: + return renderCalendar(canvas, renderInfo); default: return "Unknown output type"; } @@ -1495,6 +1497,12 @@ function renderBarChart(canvas: HTMLElement, renderInfo: RenderInfo) { setChartScale(canvas, chartElements, renderInfo); } +function renderCalendar(canvas: HTMLElement, renderInfo: RenderInfo) { + console.log("renderCalendar"); + // console.log(renderInfo); + if (renderInfo.calendar === null) return; +} + export function renderErrorMessage(canvas: HTMLElement, errorMessage: string) { let svg = d3 .select(canvas) From 6e87d432de00bbd5fa22874ba21d5165abaf9525 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Sat, 15 May 2021 10:19:35 +0800 Subject: [PATCH 011/402] Render days --- examples/TestCalendar.md | 4 +- src/data.ts | 12 ++-- src/parsing.ts | 41 +++++++------- src/rendering.ts | 118 +++++++++++++++++++++++++++++++++++++-- 4 files changed, 141 insertions(+), 34 deletions(-) diff --git a/examples/TestCalendar.md b/examples/TestCalendar.md index e986adc..cdf2bbd 100644 --- a/examples/TestCalendar.md +++ b/examples/TestCalendar.md @@ -4,6 +4,6 @@ searchType: tag searchTarget: meditation folder: diary -calendar: - heatMap: false +month: + startWeekOn: ``` \ No newline at end of file diff --git a/src/data.ts b/src/data.ts index f76c158..1f9c930 100644 --- a/src/data.ts +++ b/src/data.ts @@ -15,7 +15,7 @@ export enum OutputType { Radar, Summary, Table, - Calendar, + Month, } export enum ValueType { @@ -481,7 +481,7 @@ export class RenderInfo { line: LineInfo | null; bar: BarInfo | null; summary: SummaryInfo | null; - calendar: CalendarInfo | null; + month: MonthInfo | null; public datasets: Datasets | null; @@ -514,7 +514,7 @@ export class RenderInfo { this.line = null; this.summary = null; this.bar = null; - this.calendar = null; + this.month = null; this.datasets = null; } @@ -614,11 +614,11 @@ export class SummaryInfo { } } -export class CalendarInfo { - heatMap: boolean; +export class MonthInfo { + startWeekOn: string; constructor() { - this.heatMap = false; + this.startWeekOn = "Sun"; } } diff --git a/src/parsing.ts b/src/parsing.ts index ffe39c8..cdda59a 100644 --- a/src/parsing.ts +++ b/src/parsing.ts @@ -9,7 +9,7 @@ import { Margin, OutputType, LineInfo, - CalendarInfo, + MonthInfo, } from "./data"; import { TFolder, normalizePath } from "obsidian"; import { parseYaml } from "obsidian"; @@ -1151,24 +1151,24 @@ export function getRenderInfoFromYaml( if (typeof yaml.summary !== "undefined") { hasSummary = true; } - let hasCalendar = false; - if (typeof yaml.calendar !== "undefined") { - hasCalendar = true; + let hasMonth = false; + if (typeof yaml.month !== "undefined") { + hasMonth = true; } let sumOutput = Number(hasLine) + Number(hasBar) + Number(hasSummary) + - Number(hasCalendar); + Number(hasMonth); if (sumOutput === 0) { - return "No output parameter provided, please place line, bar, calendar, or summary."; + return "No output parameter provided, please place line, bar, month, or summary."; } else if (sumOutput === 1) { if (hasLine) renderInfo.output = OutputType.Line; if (hasBar) renderInfo.output = OutputType.Bar; if (hasSummary) renderInfo.output = OutputType.Summary; - if (hasCalendar) renderInfo.output = OutputType.Calendar; + if (hasMonth) renderInfo.output = OutputType.Month; } else if (sumOutput >= 2) { - return "Too many output parameters, pick line, bar, calendar, or summary."; + return "Too many output parameters, pick line, bar, month, or summary."; } // line related parameters @@ -1421,15 +1421,13 @@ export function getRenderInfoFromYaml( } } } // summary related parameters - // calendar related parameters - if (renderInfo.output === OutputType.Calendar) { - renderInfo.calendar = new CalendarInfo(); + // month related parameters + if (renderInfo.output === OutputType.Month) { + renderInfo.month = new MonthInfo(); - if (yaml.calendar !== null) { - let keysOfSummaryInfo = getAvailableKeysOfClass( - renderInfo.calendar - ); - let keysFoundInYAML = getAvailableKeysOfClass(yaml.calendar); + if (yaml.month !== null) { + let keysOfSummaryInfo = getAvailableKeysOfClass(renderInfo.month); + let keysFoundInYAML = getAvailableKeysOfClass(yaml.month); // console.log(keysOfSummaryInfo); // console.log(keysFoundInYAML); for (let key of keysFoundInYAML) { @@ -1440,13 +1438,14 @@ export function getRenderInfoFromYaml( } } - if (yaml.calendar !== null) { - // heatMap - if (typeof yaml.calendar.heatMap === "boolean") { - renderInfo.calendar.heatMap = yaml.calendar.heatMap; + if (yaml.month !== null) { + if (typeof yaml.month.startWeekOn === "string") { + if (yaml.month.startWeekOn === "month") { + renderInfo.month.startWeekOn = yaml.month.startWeekOn; + } } } - } // calendar related parameters + } // month related parameters return renderInfo; } diff --git a/src/rendering.ts b/src/rendering.ts index 69df1d8..5ddfb83 100644 --- a/src/rendering.ts +++ b/src/rendering.ts @@ -1,4 +1,5 @@ import * as d3 from "d3"; +import { Moment } from "moment"; import { Datasets, DataPoint, @@ -122,8 +123,8 @@ export function render(canvas: HTMLElement, renderInfo: RenderInfo) { return renderBarChart(canvas, renderInfo); case OutputType.Summary: return summary.renderSummary(canvas, renderInfo); - case OutputType.Calendar: - return renderCalendar(canvas, renderInfo); + case OutputType.Month: + return renderMonth(canvas, renderInfo); default: return "Unknown output type"; } @@ -1497,10 +1498,117 @@ function renderBarChart(canvas: HTMLElement, renderInfo: RenderInfo) { setChartScale(canvas, chartElements, renderInfo); } -function renderCalendar(canvas: HTMLElement, renderInfo: RenderInfo) { - console.log("renderCalendar"); +interface DayInfo { + date: string; + dayInMonth: number; + row: number; + col: number; +} + +function renderMonthHeader( + chartElements: ChartElements, + renderInfo: RenderInfo, + dataset: Dataset +) {} + +function renderMonthDays( + chartElements: ChartElements, + renderInfo: RenderInfo, + dataset: Dataset +) { + // console.log("renderMonthDays"); + + let maxDayTextSize = measureTextSize("30"); + let dayCellWidth = maxDayTextSize.width * 1.2; + let dayCellSpacing = dayCellWidth * 0.1; + + let lastDate = renderInfo.datasets.getDates().last(); + let monthOfLastDate = lastDate.month(); // 0~11 + let daysInMonth = lastDate.daysInMonth(); // 28~31 + + let daysInThisMonth: Array = []; + const monthStartDate = lastDate.clone().startOf("month"); + const monthEndDate = lastDate.endOf("month"); + for ( + let curDate = monthStartDate.clone(); + curDate <= monthEndDate; + curDate.add(1, "days") + ) { + let indCol = curDate.day(); + let indRow = Math.floor( + (monthStartDate.day() - 1 + curDate.date()) / 7.0 + ); + daysInThisMonth.push({ + date: curDate.format(renderInfo.dateFormat), + dayInMonth: curDate.date(), + row: indRow, + col: indCol, + }); + } + + // scale + let scale = d3 + .scaleLinear() + .domain([0, 8]) + .range([0, renderInfo.dataAreaSize.width]); + + // streak lines + + // days in this month + let dots = chartElements.dataArea + .selectAll("dot") + .data(daysInThisMonth) + .enter() + .append("circle") + .attr("r", dayCellWidth / 2.0) + .attr("cx", function (d: DayInfo) { + return scale(d.col); + }) + .attr("cy", function (d: DayInfo) { + return scale(d.row); + }) + .attr("class", "tracker-dot"); + + // labels + let dayLabals = chartElements.dataArea + .selectAll("dayLabel") + .data(daysInThisMonth) + .enter() + .append("text") + .text(function (d: DayInfo) { + return d.dayInMonth.toString(); + }) + .attr("transform", function (d: DayInfo) { + let strTranslate = + "translate(" + + scale(d.col) + + "," + + (scale(d.row) + maxDayTextSize.height / 4) + + ")"; + + return strTranslate; + }) + .attr("class", "tracker-axis-label"); +} + +function renderMonth(canvas: HTMLElement, renderInfo: RenderInfo) { + // console.log("renderMonth"); // console.log(renderInfo); - if (renderInfo.calendar === null) return; + if (renderInfo.month === null) return; + + let chartElements = createAreas(canvas, renderInfo); + + renderMonthHeader( + chartElements, + renderInfo, + renderInfo.datasets.getDatasetById(0) + ); + + renderMonthDays( + chartElements, + renderInfo, + renderInfo.datasets.getDatasetById(0) + ); } export function renderErrorMessage(canvas: HTMLElement, errorMessage: string) { From 5bc46073306e750925e01f918eeb9878bf90cbae Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Wed, 26 May 2021 09:19:22 +0800 Subject: [PATCH 012/402] Refactoring --- src/helper.ts | 27 ++++++++- src/rendering.ts | 152 +++-------------------------------------------- 2 files changed, 35 insertions(+), 144 deletions(-) diff --git a/src/helper.ts b/src/helper.ts index c02b19d..9b01977 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -1,6 +1,8 @@ -import { RenderInfo } from "./data"; +import { RenderInfo, Size } from "./data"; import { TFile, TFolder, normalizePath } from "obsidian"; +import * as d3 from "d3"; +// String helpers export function trimByChar(str: string, char: string) { const arr = Array.from(str); const first = arr.findIndex((c) => c !== char); @@ -35,3 +37,26 @@ export function getDateFromFilename(file: TFile, renderInfo: RenderInfo) { return fileDate; } + +// Chart helpers +export function measureTextSize( + text: string, + styleClass: string = "", + rotate: string = "" +): Size { + var container = d3.select("body").append("svg"); + let textBlock = container + .append("text") + .text(text) + .attr("x", -99999) + .attr("y", -99999); + if (styleClass) { + textBlock.attr("class", styleClass); + } + if (rotate) { + textBlock.attr("transform", "rotate(" + rotate + ")"); + } + var size = container.node().getBBox(); + container.remove(); + return { width: size.width, height: size.height }; +} \ No newline at end of file diff --git a/src/rendering.ts b/src/rendering.ts index 5ddfb83..88e65cf 100644 --- a/src/rendering.ts +++ b/src/rendering.ts @@ -12,6 +12,8 @@ import { ValueType, } from "./data"; import * as summary from './summary'; +import * as month from './month'; +import * as helper from './helper' function getTickInterval(datasets: Datasets) { let tickInterval; @@ -77,29 +79,6 @@ function getYTickFormat() { return tickFormat; } -// Is there a better way to measure text size?? -function measureTextSize( - text: string, - styleClass: string = "", - rotate: string = "" -): Size { - var container = d3.select("body").append("svg"); - let textBlock = container - .append("text") - .text(text) - .attr("x", -99999) - .attr("y", -99999); - if (styleClass) { - textBlock.attr("class", styleClass); - } - if (rotate) { - textBlock.attr("transform", "rotate(" + rotate + ")"); - } - var size = container.node().getBBox(); - container.remove(); - return { width: size.width, height: size.height }; -} - export function render(canvas: HTMLElement, renderInfo: RenderInfo) { // console.log("render"); // console.log(renderInfo.datasets); @@ -124,7 +103,7 @@ export function render(canvas: HTMLElement, renderInfo: RenderInfo) { case OutputType.Summary: return summary.renderSummary(canvas, renderInfo); case OutputType.Month: - return renderMonth(canvas, renderInfo); + return month.renderMonth(canvas, renderInfo); default: return "Unknown output type"; } @@ -172,7 +151,7 @@ function renderXAxis(chartElements: ChartElements, renderInfo: RenderInfo) { } chartElements["xAxis"] = xAxis; - let textSize = measureTextSize("99-99-99"); + let textSize = helper.measureTextSize("99-99-99"); let xAxisTickLabels = xAxis .selectAll("text") @@ -406,11 +385,11 @@ function renderYAxis( // Get max tick label width let yTickFormat = d3.tickFormat(yLower, yUpper, 10); - let yLowerLabelSize = measureTextSize( + let yLowerLabelSize = helper.measureTextSize( yTickFormat(yLower), "tracker-axis-label" ); - let yUpperLabelSize = measureTextSize( + let yUpperLabelSize = helper.measureTextSize( yTickFormat(yUpper), "tracker-axis-label" ); @@ -423,7 +402,7 @@ function renderYAxis( yAxisLabelText += " (" + yAxisUnitText + ")"; } let yTickLength = 6; - let yAxisLabelSize = measureTextSize(yAxisLabelText); + let yAxisLabelSize = helper.measureTextSize(yAxisLabelText); let yAxisLabel = yAxis .append("text") .text(yAxisLabelText) @@ -783,7 +762,7 @@ function renderLegend(chartElements: ChartElements, renderInfo: RenderInfo) { // Get names and their dimension let names = datasets.getNames(); // xDataset name included let nameSizes = names.map(function (n) { - return measureTextSize(n, "tracker-legend-label"); + return helper.measureTextSize(n, "tracker-legend-label"); }); let indMaxName = 0; let maxNameWidth = 0.0; @@ -1199,7 +1178,7 @@ function renderTitle(chartElements: ChartElements, renderInfo: RenderInfo) { if (!chartInfo) return; if (!chartInfo.title) return; - let titleSize = measureTextSize(chartInfo.title, "tracker-title"); + let titleSize = helper.measureTextSize(chartInfo.title, "tracker-title"); // Append title let title = chartElements.graphArea @@ -1498,119 +1477,6 @@ function renderBarChart(canvas: HTMLElement, renderInfo: RenderInfo) { setChartScale(canvas, chartElements, renderInfo); } -interface DayInfo { - date: string; - dayInMonth: number; - row: number; - col: number; -} - -function renderMonthHeader( - chartElements: ChartElements, - renderInfo: RenderInfo, - dataset: Dataset -) {} - -function renderMonthDays( - chartElements: ChartElements, - renderInfo: RenderInfo, - dataset: Dataset -) { - // console.log("renderMonthDays"); - - let maxDayTextSize = measureTextSize("30"); - let dayCellWidth = maxDayTextSize.width * 1.2; - let dayCellSpacing = dayCellWidth * 0.1; - - let lastDate = renderInfo.datasets.getDates().last(); - let monthOfLastDate = lastDate.month(); // 0~11 - let daysInMonth = lastDate.daysInMonth(); // 28~31 - - let daysInThisMonth: Array = []; - const monthStartDate = lastDate.clone().startOf("month"); - const monthEndDate = lastDate.endOf("month"); - for ( - let curDate = monthStartDate.clone(); - curDate <= monthEndDate; - curDate.add(1, "days") - ) { - let indCol = curDate.day(); - let indRow = Math.floor( - (monthStartDate.day() - 1 + curDate.date()) / 7.0 - ); - daysInThisMonth.push({ - date: curDate.format(renderInfo.dateFormat), - dayInMonth: curDate.date(), - row: indRow, - col: indCol, - }); - } - - // scale - let scale = d3 - .scaleLinear() - .domain([0, 8]) - .range([0, renderInfo.dataAreaSize.width]); - - // streak lines - - // days in this month - let dots = chartElements.dataArea - .selectAll("dot") - .data(daysInThisMonth) - .enter() - .append("circle") - .attr("r", dayCellWidth / 2.0) - .attr("cx", function (d: DayInfo) { - return scale(d.col); - }) - .attr("cy", function (d: DayInfo) { - return scale(d.row); - }) - .attr("class", "tracker-dot"); - - // labels - let dayLabals = chartElements.dataArea - .selectAll("dayLabel") - .data(daysInThisMonth) - .enter() - .append("text") - .text(function (d: DayInfo) { - return d.dayInMonth.toString(); - }) - .attr("transform", function (d: DayInfo) { - let strTranslate = - "translate(" + - scale(d.col) + - "," + - (scale(d.row) + maxDayTextSize.height / 4) + - ")"; - - return strTranslate; - }) - .attr("class", "tracker-axis-label"); -} - -function renderMonth(canvas: HTMLElement, renderInfo: RenderInfo) { - // console.log("renderMonth"); - // console.log(renderInfo); - if (renderInfo.month === null) return; - - let chartElements = createAreas(canvas, renderInfo); - - renderMonthHeader( - chartElements, - renderInfo, - renderInfo.datasets.getDatasetById(0) - ); - - renderMonthDays( - chartElements, - renderInfo, - renderInfo.datasets.getDatasetById(0) - ); -} - export function renderErrorMessage(canvas: HTMLElement, errorMessage: string) { let svg = d3 .select(canvas) From ffbc18c7e0ad312fe281978a0f37c2bf32c3e7e4 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Wed, 26 May 2021 09:20:40 +0800 Subject: [PATCH 013/402] Prettify the source code --- src/collecting.ts | 2 +- src/data.ts | 6 +++--- src/helper.ts | 2 +- src/main.ts | 28 +++++++++++++--------------- src/parsing.ts | 8 ++------ src/rendering.ts | 6 +++--- 6 files changed, 23 insertions(+), 29 deletions(-) diff --git a/src/collecting.ts b/src/collecting.ts index 08fbc4a..1d337a3 100644 --- a/src/collecting.ts +++ b/src/collecting.ts @@ -7,7 +7,7 @@ import { QueryValuePair, TableData, SearchType, - ValueType + ValueType, } from "./data"; import * as helper from "./helper"; diff --git a/src/data.ts b/src/data.ts index 1f9c930..b648507 100644 --- a/src/data.ts +++ b/src/data.ts @@ -24,7 +24,7 @@ export enum ValueType { Date, Time, DateTime, - String + String, } export class DataPoint { @@ -177,7 +177,7 @@ export class Dataset implements IterableIterator { private yMax: number; private lineInfo: LineInfo; private barInfo: BarInfo; - + valueType: ValueType; private currentIndex = 0; // IterableIterator @@ -468,7 +468,7 @@ export class RenderInfo { accum: boolean[]; penalty: number[]; shift: number[]; - valueType: string[];// number/float, int, string, boolean, date, time, datetime + valueType: string[]; // number/float, int, string, boolean, date, time, datetime dataAreaSize: Size; margin: Margin; diff --git a/src/helper.ts b/src/helper.ts index 9b01977..69f8f9f 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -59,4 +59,4 @@ export function measureTextSize( var size = container.node().getBBox(); container.remove(); return { width: size.width, height: size.height }; -} \ No newline at end of file +} diff --git a/src/main.ts b/src/main.ts index 64ef50a..13f7f7f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -118,12 +118,7 @@ export default class Tracker extends Plugin { } // To be moved to collecting.ts - addToDataMap( - dataMap: DataMap, - date: string, - query: Query, - value: number - ) { + addToDataMap(dataMap: DataMap, date: string, query: Query, value: number) { if (!dataMap.has(date)) { let queryValuePairs = new Array(); queryValuePairs.push({ query: query, value: value }); @@ -472,7 +467,7 @@ export default class Tracker extends Plugin { let columnXDataset = xDatasetQuery.getAccessor(1); if (columnXDataset >= numColumns) continue; let xValues = []; - + let indLine = 0; for (let tableLine of tableLines) { let dataRow = helper.trimByChar(tableLine.trim(), "|"); @@ -496,20 +491,17 @@ export default class Tracker extends Plugin { if (date.isValid()) { xValues.push(date); - } - else { + } else { xValues.push(null); } - - } - else { + } else { xValues.push(null); } indLine++; } // console.log(xValues); - if (xValues.every(v => v === null)) { + if (xValues.every((v) => v === null)) { let errorMessage = "No valid X value found"; renderErrorMessage(canvas, errorMessage); el.appendChild(canvas); @@ -533,7 +525,10 @@ export default class Tracker extends Plugin { if (splitted.length === 1) { let value = parseFloat(splitted[0]); if (Number.isNumber(value)) { - if (indLine < xValues.length && xValues[indLine]) { + if ( + indLine < xValues.length && + xValues[indLine] + ) { this.addToDataMap( dataMap, xValues[indLine].format( @@ -553,7 +548,10 @@ export default class Tracker extends Plugin { splitted[yDatasetQuery.getAccessor(2)].trim(); value = parseFloat(splittedPart); if (Number.isNumber(value)) { - if (indLine < xValues.length && xValues[indLine]) { + if ( + indLine < xValues.length && + xValues[indLine] + ) { this.addToDataMap( dataMap, xValues[indLine].format( diff --git a/src/parsing.ts b/src/parsing.ts index cdda59a..9ca588f 100644 --- a/src/parsing.ts +++ b/src/parsing.ts @@ -809,7 +809,7 @@ export function getRenderInfoFromYaml( if (typeof retMultipleValueSparator === "string") { return retMultipleValueSparator; // errorMessage } - multipleValueSparator = retMultipleValueSparator.map( sep => { + multipleValueSparator = retMultipleValueSparator.map((sep) => { if (sep === "comma") { return ","; } @@ -853,11 +853,7 @@ export function getRenderInfoFromYaml( // Create grarph info let renderInfo = new RenderInfo(queries); let keysOfRenderInfo = getAvailableKeysOfClass(renderInfo); - let additionalAllowedKeys = [ - "searchType", - "searchTarget", - "separator", - ]; + let additionalAllowedKeys = ["searchType", "searchTarget", "separator"]; // console.log(keysOfRenderInfo); for (let key of keysFoundInYAML) { if ( diff --git a/src/rendering.ts b/src/rendering.ts index 88e65cf..2510e29 100644 --- a/src/rendering.ts +++ b/src/rendering.ts @@ -11,9 +11,9 @@ import { OutputType, ValueType, } from "./data"; -import * as summary from './summary'; -import * as month from './month'; -import * as helper from './helper' +import * as summary from "./summary"; +import * as month from "./month"; +import * as helper from "./helper"; function getTickInterval(datasets: Datasets) { let tickInterval; From 649b6dfd6846685c35cbc9d5308ab7b14240b988 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Wed, 26 May 2021 09:27:42 +0800 Subject: [PATCH 014/402] Add missing month.ts --- src/month.ts | 181 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 src/month.ts diff --git a/src/month.ts b/src/month.ts new file mode 100644 index 0000000..c1dabe4 --- /dev/null +++ b/src/month.ts @@ -0,0 +1,181 @@ +import { Moment } from "moment"; +import { + Datasets, + DataPoint, + RenderInfo, + Dataset, + Size, + Transform, + ChartElements, + OutputType, + ValueType, +} from "./data"; +import * as helper from "./helper"; +import * as d3 from "d3"; + +interface DayInfo { + date: string; + dayInMonth: number; + row: number; + col: number; +} + +function createAreas( + canvas: HTMLElement, + renderInfo: RenderInfo +): ChartElements { + let chartElements: ChartElements = {}; + // whole area for plotting, includes margins + let svg = d3 + .select(canvas) + .append("svg") + .attr("id", "svg") + .attr( + "width", + renderInfo.dataAreaSize.width + + renderInfo.margin.left + + renderInfo.margin.right + ) + .attr( + "height", + renderInfo.dataAreaSize.height + + renderInfo.margin.top + + renderInfo.margin.bottom + ); + chartElements["svg"] = svg; + + // graphArea, includes chartArea, title, legend + let graphArea = svg + .append("g") + .attr("id", "graphArea") + .attr( + "transform", + "translate(" + + renderInfo.margin.left + + "," + + renderInfo.margin.top + + ")" + ) + .attr("width", renderInfo.dataAreaSize.width + renderInfo.margin.right) + .attr( + "height", + renderInfo.dataAreaSize.height + renderInfo.margin.bottom + ); + chartElements["graphArea"] = graphArea; + + // dataArea, under graphArea, includes points, lines, xAxis, yAxis + let dataArea = graphArea + .append("g") + .attr("id", "dataArea") + .attr("width", renderInfo.dataAreaSize.width) + .attr("height", renderInfo.dataAreaSize.height); + chartElements["dataArea"] = dataArea; + + return chartElements; +} + +function renderMonthHeader( + chartElements: ChartElements, + renderInfo: RenderInfo, + dataset: Dataset +) {} + +function renderMonthDays( + chartElements: ChartElements, + renderInfo: RenderInfo, + dataset: Dataset +) { + // console.log("renderMonthDays"); + + let maxDayTextSize = helper.measureTextSize("30"); + let dayCellWidth = maxDayTextSize.width * 1.2; + let dayCellSpacing = dayCellWidth * 0.1; + + let lastDate = renderInfo.datasets.getDates().last(); + let monthOfLastDate = lastDate.month(); // 0~11 + let daysInMonth = lastDate.daysInMonth(); // 28~31 + + let daysInThisMonth: Array = []; + const monthStartDate = lastDate.clone().startOf("month"); + const monthEndDate = lastDate.endOf("month"); + for ( + let curDate = monthStartDate.clone(); + curDate <= monthEndDate; + curDate.add(1, "days") + ) { + let indCol = curDate.day(); + let indRow = Math.floor( + (monthStartDate.day() - 1 + curDate.date()) / 7.0 + ); + daysInThisMonth.push({ + date: curDate.format(renderInfo.dateFormat), + dayInMonth: curDate.date(), + row: indRow, + col: indCol, + }); + } + + // scale + let scale = d3 + .scaleLinear() + .domain([0, 8]) + .range([0, renderInfo.dataAreaSize.width]); + + // streak lines + + // days in this month + let dots = chartElements.dataArea + .selectAll("dot") + .data(daysInThisMonth) + .enter() + .append("circle") + .attr("r", dayCellWidth / 2.0) + .attr("cx", function (d: DayInfo) { + return scale(d.col); + }) + .attr("cy", function (d: DayInfo) { + return scale(d.row); + }) + .attr("class", "tracker-dot"); + + // labels + let dayLabals = chartElements.dataArea + .selectAll("dayLabel") + .data(daysInThisMonth) + .enter() + .append("text") + .text(function (d: DayInfo) { + return d.dayInMonth.toString(); + }) + .attr("transform", function (d: DayInfo) { + let strTranslate = + "translate(" + + scale(d.col) + + "," + + (scale(d.row) + maxDayTextSize.height / 4) + + ")"; + + return strTranslate; + }) + .attr("class", "tracker-axis-label"); +} + +export function renderMonth(canvas: HTMLElement, renderInfo: RenderInfo) { + // console.log("renderMonth"); + // console.log(renderInfo); + if (renderInfo.month === null) return; + + let chartElements = createAreas(canvas, renderInfo); + + renderMonthHeader( + chartElements, + renderInfo, + renderInfo.datasets.getDatasetById(0) + ); + + renderMonthDays( + chartElements, + renderInfo, + renderInfo.datasets.getDatasetById(0) + ); +} From 40f42fc52fa613ba6b3b258dd5ef7325dd89f297 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Wed, 26 May 2021 17:59:50 +0800 Subject: [PATCH 015/402] Fix yAxis tick label and tool-tip for time values --- examples/TestTimeValues.md | 4 +-- src/rendering.ts | 66 ++++++++++++++++++++++++++++++++++---- 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/examples/TestTimeValues.md b/examples/TestTimeValues.md index 4d33c08..60f6f2e 100644 --- a/examples/TestTimeValues.md +++ b/examples/TestTimeValues.md @@ -9,7 +9,7 @@ line: title: "Working Hours" yAxisLabel: "Time" lineColor: yellow, red - showPoint: false + showPoint: true ``` ``` tracker @@ -21,5 +21,5 @@ line: title: "Sleep" yAxisLabel: "Time" lineColor: yellow, red - showPoint: false + showPoint: true ``` \ No newline at end of file diff --git a/src/rendering.ts b/src/rendering.ts index 2510e29..73f6998 100644 --- a/src/rendering.ts +++ b/src/rendering.ts @@ -15,7 +15,7 @@ import * as summary from "./summary"; import * as month from "./month"; import * as helper from "./helper"; -function getTickInterval(datasets: Datasets) { +function getXTickInterval(datasets: Datasets) { let tickInterval; let days = datasets.getDates().length; @@ -67,13 +67,55 @@ function getXTickFormat(datasets: Datasets) { return tickFormat; } -function getYTickFormat() { +function getYTickValues(yLower: number, yUpper: number) { + // currently used for time value tick only, value in seconds + const absExtent = Math.abs(yUpper - yLower); + let tickValues = []; + if (absExtent > 5 * 60 * 60) {// extent over than 5 hours + // tick on the hour + yLower = Math.floor(yLower / 3600) * 3600; + yUpper = Math.ceil(yUpper / 3600) * 3600; + + tickValues = d3.range(yLower, yUpper, 3600); + } + else { + // tick on the half hour + yLower = Math.floor(yLower / 1800) * 1800; + yUpper = Math.ceil(yUpper / 1800) * 1800; + + tickValues = d3.range(yLower, yUpper, 1800); + } + + return tickValues; +} + +function getYTickFormat(yLower: number, yUpper: number) { // currently used for time value tick only // return a function convert value to time string function tickFormat(value: number): string { + const absExtent = Math.abs(yUpper - yLower); let dayStart = window.moment("00:00", "HH:mm", true); let tickTime = dayStart.add(value, "seconds"); - return tickTime.format("HH:mm"); + let format = tickTime.format("HH:mm"); + if (absExtent > 12 * 60 * 60) { + + let devHour = (value - yLower) / 3600; + let interleave = devHour % 2; + if (value <= yLower) { + format = ""; + } + else if (value >= yUpper) { + format = ""; + } + else if ( interleave > 1.0) { + format = tickTime.format("HH:mm"); + } + else { + format = ""; + } + } + + return format; } return tickFormat; @@ -130,7 +172,7 @@ function renderXAxis(chartElements: ChartElements, renderInfo: RenderInfo) { .range([0, renderInfo.dataAreaSize.width]); chartElements["xScale"] = xScale; - let tickInterval = getTickInterval(datasets); + let tickInterval = getXTickInterval(datasets); let tickFormat = getXTickFormat(datasets); let xAxisGen = d3 @@ -346,9 +388,13 @@ function renderYAxis( yAxisGen = d3.axisRight(yScale); } if (yAxisGen && valueIsTime) { - let tickFormat = getYTickFormat(); - yAxisGen.tickFormat(tickFormat); + let tickFormat = getYTickFormat(yLower, yUpper); + let tickValues = getYTickValues(yLower, yUpper); + yAxisGen + .tickValues(tickValues) + .tickFormat(tickFormat); } + let yAxis = chartElements.dataArea .append("g") .attr("id", "yAxis") @@ -588,9 +634,15 @@ function renderPoints( .attr("y", (renderInfo.tooltipSize.height / 5) * 4); dots.on("mouseenter", function (event: any) { + // Date tooltipLabelDate.text("date:" + d3.select(this).attr("date")); + // Value + let strValue = d3.select(this).attr("value"); + let dayStart = window.moment("00:00", "HH:mm", true); + let tickTime = dayStart.add(parseFloat(strValue), "seconds"); + let dateValue = tickTime.format("HH:mm"); tooltipLabelValue.text( - "value:" + d3.select(this).attr("value") + "value:" + dateValue ); const [x, y] = d3.pointer(event); From 3e8ff9b9571d4445a8d699a2d486fe4f7d0e0da1 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Wed, 26 May 2021 18:07:36 +0800 Subject: [PATCH 016/402] Fix tool-tip value for non-time value --- src/rendering.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/rendering.ts b/src/rendering.ts index 73f6998..3599a06 100644 --- a/src/rendering.ts +++ b/src/rendering.ts @@ -593,6 +593,7 @@ function renderPoints( return p.value.toFixed(2); } }) + .attr("valueType", ValueType[dataset.valueType]) .attr("class", "tracker-dot"); if (lineInfo.pointColor[dataset.getId()]) { dots.style("fill", lineInfo.pointColor[dataset.getId()]); @@ -637,13 +638,21 @@ function renderPoints( // Date tooltipLabelDate.text("date:" + d3.select(this).attr("date")); // Value + let valueType = d3.select(this).attr("valueType"); let strValue = d3.select(this).attr("value"); - let dayStart = window.moment("00:00", "HH:mm", true); - let tickTime = dayStart.add(parseFloat(strValue), "seconds"); - let dateValue = tickTime.format("HH:mm"); - tooltipLabelValue.text( - "value:" + dateValue - ); + if (valueType === "Time") { + let dayStart = window.moment("00:00", "HH:mm", true); + let tickTime = dayStart.add(parseFloat(strValue), "seconds"); + let dateValue = tickTime.format("HH:mm"); + tooltipLabelValue.text( + "value:" + dateValue + ); + } + else { + tooltipLabelValue.text( + "value:" + strValue + ); + } const [x, y] = d3.pointer(event); if (x < renderInfo.dataAreaSize.width / 2) { From be8b3733017c107df90d29779e8beaa1576dd656 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Wed, 26 May 2021 19:38:28 +0800 Subject: [PATCH 017/402] Allow retrieving deep values in the front matter --- examples/TestFrontmatter.md | 11 ++++++++ examples/diary/2021-01-01.md | 34 ++++++++++++++---------- examples/diary/2021-01-02.md | 50 ++++++++++++++++++++--------------- examples/diary/2021-01-03.md | 45 ++++++++++++++++++------------- examples/diary/2021-01-04.md | 45 ++++++++++++++++++------------- examples/diary/2021-01-05.md | 47 ++++++++++++++++++-------------- examples/diary/2021-01-06.md | 45 ++++++++++++++++++------------- examples/diary/2021-01-07.md | 44 +++++++++++++++++------------- examples/diary/2021-01-08.md | 46 +++++++++++++++++++------------- examples/diary/2021-01-09.md | 47 ++++++++++++++++++-------------- examples/diary/2021-01-10.md | 47 +++++++++++++++++--------------- examples/diary/2021-01-11.md | 47 +++++++++++++++++--------------- examples/diary/2021-01-12.md | 46 +++++++++++++++++--------------- examples/diary/2021-01-13.md | 45 +++++++++++++++++-------------- examples/diary/2021-01-14.md | 46 +++++++++++++++++++------------- examples/diary/2021-01-15.md | 45 ++++++++++++++++++------------- examples/diary/2021-01-16.md | 45 ++++++++++++++++++------------- examples/diary/2021-01-17.md | 45 +++++++++++++++++-------------- examples/diary/2021-01-18.md | 47 ++++++++++++++++++-------------- examples/diary/2021-01-19.md | 48 ++++++++++++++++++--------------- examples/diary/2021-01-20.md | 48 ++++++++++++++++++--------------- examples/diary/2021-01-21.md | 46 +++++++++++++++++++------------- examples/diary/2021-01-22.md | 48 ++++++++++++++++++--------------- examples/diary/2021-01-23.md | 47 +++++++++++++++++--------------- examples/diary/2021-01-24.md | 46 +++++++++++++++++++------------- examples/diary/2021-01-25.md | 45 ++++++++++++++++++------------- examples/diary/2021-01-26.md | 45 ++++++++++++++++++------------- examples/diary/2021-01-27.md | 47 ++++++++++++++++++-------------- examples/diary/2021-01-28.md | 46 ++++++++++++++++++-------------- examples/diary/2021-01-29.md | 46 +++++++++++++++++++------------- examples/diary/2021-01-30.md | 45 ++++++++++++++++++------------- examples/diary/2021-01-31.md | 48 +++++++++++++++++++-------------- examples/example_generator.ts | 10 +++++++ src/collecting.ts | 15 ++++++----- src/helper.ts | 16 +++++++++++ 35 files changed, 861 insertions(+), 612 deletions(-) create mode 100644 examples/TestFrontmatter.md diff --git a/examples/TestFrontmatter.md b/examples/TestFrontmatter.md new file mode 100644 index 0000000..076117c --- /dev/null +++ b/examples/TestFrontmatter.md @@ -0,0 +1,11 @@ +# Test Frontmatter + + +``` tracker +searchType: frontmatter +searchTarget: deepValue.very.very.very.very.very.deep +folder: diary +endDate: 2021-01-31 +line: + title: Deep Values +``` diff --git a/examples/diary/2021-01-01.md b/examples/diary/2021-01-01.md index 183ddf5..b118da9 100644 --- a/examples/diary/2021-01-01.md +++ b/examples/diary/2021-01-01.md @@ -7,33 +7,39 @@ bloodpressure2: [184.4, 118.8] clock-in: 10:45 clock-out: 20:51 sleep: 10:11 pm/7:5 am +deepValue: + very: + very: + very: + very: + very: + deep: 27.4 --- -#weight:65.7kg +#weight:74.1kg -#exercise-pushup:44 -#exercise-plank:36sec +#exercise-pushup:31 +#exercise-plank:109sec -⭐⭐⭐⭐⭐ -#finance/bank1:-2.3USD + +#finance/bank1:-3.5USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker@gmail.com -obsidian-tracker@yahoo.com +obsidian-tracker+1@gmail.com -weightlifting: 15 +weightlifting: 18 -dataviewTarget:: 21 -- Make Progress:: 24 -- Make-Progress:: 79 -dataviewTarget1:: 21/24 -dataviewTarget2:: 24 @ 79 -dataviewTarget3:: 24, 79 +dataviewTarget:: 48 +- Make Progress:: 29 +- Make-Progress:: 88 +dataviewTarget1:: 48/29 +dataviewTarget2:: 29 @ 88 +dataviewTarget3:: 29, 88 #sin:0.00000/0.20791/0.40674/0.58779/0.74314/0.86603/0.95106/0.99452/0.99452 diff --git a/examples/diary/2021-01-02.md b/examples/diary/2021-01-02.md index 50fc695..e97b2b4 100644 --- a/examples/diary/2021-01-02.md +++ b/examples/diary/2021-01-02.md @@ -1,39 +1,47 @@ --- tags: -mood: 8 -bloodpressure: 173.8/120.6 -bloodpressure1: 173.8, 120.6 -bloodpressure2: [173.8, 120.6] -clock-in: 10:37 -clock-out: 17:7 -sleep: 11:10 pm/5:37 am +mood: 1 +bloodpressure: 180.8/120.6 +bloodpressure1: 180.8, 120.6 +bloodpressure2: [180.8, 120.6] +clock-in: 9:16 +clock-out: 16:49 +sleep: 9:12 pm/6:41 am +deepValue: + very: + very: + very: + very: + very: + deep: 96.3 --- -#weight:74.4kg +#weight:62.8kg -#exercise-pushup:50 -#exercise-plank:41sec +#exercise-pushup:46 +#exercise-plank:101sec +#meditation -⭐⭐⭐⭐ -#finance/bank1:-2.0USD +#clean-up + +#finance/bank1:-3.8USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker+1@gmail.com -obsidian-tracker@yahoo.com +obsidian-tracker@gmail.com -weightlifting: 14 +weightlifting: 12 -dataviewTarget:: 25 -- Make Progress:: 14 -- Make-Progress:: 64 -dataviewTarget1:: 25/14 -dataviewTarget2:: 14 @ 64 -dataviewTarget3:: 14, 64 +dataviewTarget:: 27 +- Make Progress:: 12 +- Make-Progress:: 55 +dataviewTarget1:: 27/12 +dataviewTarget2:: 12 @ 55 +dataviewTarget3:: 12, 55 #sin:0.20791/0.40674/0.58779/0.74314/0.86603/0.95106/0.99452/0.99452/0.95106 diff --git a/examples/diary/2021-01-03.md b/examples/diary/2021-01-03.md index 82b3cad..d4d59ce 100644 --- a/examples/diary/2021-01-03.md +++ b/examples/diary/2021-01-03.md @@ -1,38 +1,45 @@ --- tags: -mood: 3 -bloodpressure: 174.2/119.4 -bloodpressure1: 174.2, 119.4 -bloodpressure2: [174.2, 119.4] -clock-in: 9:38 -clock-out: 19:22 -sleep: 11:49 pm/5:33 am +mood: 5 +bloodpressure: 178.2/120.4 +bloodpressure1: 178.2, 120.4 +bloodpressure2: [178.2, 120.4] +clock-in: 10:22 +clock-out: 20:49 +sleep: 9:33 pm/5:17 am +deepValue: + very: + very: + very: + very: + very: + deep: 49.3 --- -#weight:65.5kg +#weight:76.8kg -#exercise-pushup:36 -#exercise-plank:74sec +#exercise-pushup:45 +#exercise-plank:43sec -⭐⭐⭐⭐ +⭐⭐⭐ -#finance/bank1:-3.8USD +#finance/bank1:-2.6USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] obsidian-tracker@gmail.com -weightlifting: 15 +weightlifting: 12 -dataviewTarget:: 7 -- Make Progress:: 10 -- Make-Progress:: 90 -dataviewTarget1:: 7/10 -dataviewTarget2:: 10 @ 90 -dataviewTarget3:: 10, 90 +dataviewTarget:: 88 +- Make Progress:: 5 +- Make-Progress:: 56 +dataviewTarget1:: 88/5 +dataviewTarget2:: 5 @ 56 +dataviewTarget3:: 5, 56 #sin:0.40674/0.58779/0.74314/0.86603/0.95106/0.99452/0.99452/0.95106/0.86603 diff --git a/examples/diary/2021-01-04.md b/examples/diary/2021-01-04.md index ae0d5da..ba8f3c5 100644 --- a/examples/diary/2021-01-04.md +++ b/examples/diary/2021-01-04.md @@ -1,39 +1,46 @@ --- tags: work_log -mood: 9 -bloodpressure: 173.6/117.2 -bloodpressure1: 173.6, 117.2 -bloodpressure2: [173.6, 117.2] -clock-in: 8:38 -clock-out: 20:14 -sleep: 10:49 pm/7:41 am +mood: 3 +bloodpressure: 179.6/121.2 +bloodpressure1: 179.6, 121.2 +bloodpressure2: [179.6, 121.2] +clock-in: 8:24 +clock-out: 20:58 +sleep: 11:57 pm/7:20 am +deepValue: + very: + very: + very: + very: + very: + deep: 18.0 --- -#weight:80.3kg +#weight:70.1kg -#exercise-pushup:48 -#exercise-plank:60sec +#exercise-pushup:42 +#exercise-plank:109sec #meditation -⭐⭐ +⭐ -#finance/bank1:-3.8USD +#finance/bank1:-2.7USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] obsidian-tracker@yahoo.com -weightlifting: 13 +weightlifting: 18 -dataviewTarget:: 27 -- Make Progress:: 45 -- Make-Progress:: 89 -dataviewTarget1:: 27/45 -dataviewTarget2:: 45 @ 89 -dataviewTarget3:: 45, 89 +dataviewTarget:: 58 +- Make Progress:: 41 +- Make-Progress:: 59 +dataviewTarget1:: 58/41 +dataviewTarget2:: 41 @ 59 +dataviewTarget3:: 41, 59 #sin:0.58779/0.74314/0.86603/0.95106/0.99452/0.99452/0.95106/0.86603/0.74314 diff --git a/examples/diary/2021-01-05.md b/examples/diary/2021-01-05.md index d990407..ea5d52c 100644 --- a/examples/diary/2021-01-05.md +++ b/examples/diary/2021-01-05.md @@ -1,39 +1,46 @@ --- tags: work_log -mood: 7 -bloodpressure: 178/121 -bloodpressure1: 178, 121 -bloodpressure2: [178, 121] -clock-in: 8:25 -clock-out: 18:29 -sleep: 9:10 pm/7:21 am +mood: 5 +bloodpressure: 178/119 +bloodpressure1: 178, 119 +bloodpressure2: [178, 119] +clock-in: 8:10 +clock-out: 20:21 +sleep: 10:1 pm/6:57 am +deepValue: + very: + very: + very: + very: + very: + deep: 11.3 --- -#weight:72.9kg +#weight:63.2kg -#exercise-pushup:30 -#exercise-plank:73sec +#exercise-pushup:32 +#exercise-plank:117sec +#meditation +⭐⭐⭐ - -#finance/bank1:-2.3USD +#finance/bank1:-2.6USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker@gmail.com obsidian-tracker@yahoo.com -weightlifting: 19 +weightlifting: 20 -dataviewTarget:: 38 -- Make Progress:: 13 -- Make-Progress:: 97 -dataviewTarget1:: 38/13 -dataviewTarget2:: 13 @ 97 -dataviewTarget3:: 13, 97 +dataviewTarget:: 66 +- Make Progress:: 12 +- Make-Progress:: 85 +dataviewTarget1:: 66/12 +dataviewTarget2:: 12 @ 85 +dataviewTarget3:: 12, 85 #sin:0.74314/0.86603/0.95106/0.99452/0.99452/0.95106/0.86603/0.74314/0.58779 diff --git a/examples/diary/2021-01-06.md b/examples/diary/2021-01-06.md index 0a105d4..eb0fcd6 100644 --- a/examples/diary/2021-01-06.md +++ b/examples/diary/2021-01-06.md @@ -1,38 +1,47 @@ --- tags: work_log -mood: 10 -bloodpressure: 180.4/119.8 -bloodpressure1: 180.4, 119.8 -bloodpressure2: [180.4, 119.8] -clock-in: 8:41 -clock-out: 19:51 -sleep: 10:48 pm/6:42 am +mood: 7 +bloodpressure: 180.4/118.8 +bloodpressure1: 180.4, 118.8 +bloodpressure2: [180.4, 118.8] +clock-in: 10:23 +clock-out: 19:2 +sleep: 10:47 pm/6:41 am +deepValue: + very: + very: + very: + very: + very: + deep: 44.5 --- -#weight:61.0kg +#weight:74.0kg -#exercise-pushup:41 -#exercise-plank:101sec +#exercise-pushup:40 +#exercise-plank:51sec #meditation -⭐⭐⭐⭐ +⭐ -#finance/bank1:-3.3USD +#finance/bank1:-3.6USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] obsidian-tracker@gmail.com +obsidian-tracker@yahoo.com +weightlifting: 12 -dataviewTarget:: 80 -- Make Progress:: 44 -- Make-Progress:: 57 -dataviewTarget1:: 80/44 -dataviewTarget2:: 44 @ 57 -dataviewTarget3:: 44, 57 +dataviewTarget:: 25 +- Make Progress:: 47 +- Make-Progress:: 75 +dataviewTarget1:: 25/47 +dataviewTarget2:: 47 @ 75 +dataviewTarget3:: 47, 75 #sin:0.86603/0.95106/0.99452/0.99452/0.95106/0.86603/0.74314/0.58779/0.40674 diff --git a/examples/diary/2021-01-07.md b/examples/diary/2021-01-07.md index de33cd1..3330e10 100644 --- a/examples/diary/2021-01-07.md +++ b/examples/diary/2021-01-07.md @@ -1,40 +1,46 @@ --- tags: work_log -mood: 6 -bloodpressure: 172.8/116.6 -bloodpressure1: 172.8, 116.6 -bloodpressure2: [172.8, 116.6] -clock-in: 8:55 -clock-out: 18:53 -sleep: 9:58 pm/7:29 am +mood: 9 +bloodpressure: 173.8/120.6 +bloodpressure1: 173.8, 120.6 +bloodpressure2: [173.8, 120.6] +clock-in: 10:29 +clock-out: 17:0 +sleep: 10:54 pm/6:53 am +deepValue: + very: + very: + very: + very: + very: + deep: 38.4 --- -#weight:65.6kg +#weight:73.7kg -#exercise-pushup:30 -#exercise-plank:75sec +#exercise-pushup:40 +#exercise-plank:111sec -⭐⭐⭐ +⭐ -#finance/bank1:-2.8USD +#finance/bank1:-3.0USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker@gmail.com obsidian-tracker+1@gmail.com obsidian-tracker@yahoo.com -weightlifting: 18 +weightlifting: 12 -dataviewTarget:: 47 -- Make Progress:: 24 +dataviewTarget:: 29 +- Make Progress:: 38 - Make-Progress:: 59 -dataviewTarget1:: 47/24 -dataviewTarget2:: 24 @ 59 -dataviewTarget3:: 24, 59 +dataviewTarget1:: 29/38 +dataviewTarget2:: 38 @ 59 +dataviewTarget3:: 38, 59 #sin:0.95106/0.99452/0.99452/0.95106/0.86603/0.74314/0.58779/0.40674/0.20791 diff --git a/examples/diary/2021-01-08.md b/examples/diary/2021-01-08.md index 12f2a96..3c364b1 100644 --- a/examples/diary/2021-01-08.md +++ b/examples/diary/2021-01-08.md @@ -1,37 +1,47 @@ --- tags: work_log -mood: 9 -bloodpressure: 179.2/117.4 -bloodpressure1: 179.2, 117.4 -bloodpressure2: [179.2, 117.4] -clock-in: 10:17 -clock-out: 19:10 -sleep: 11:5 pm/6:26 am +mood: 10 +bloodpressure: 170.2/119.4 +bloodpressure1: 170.2, 119.4 +bloodpressure2: [170.2, 119.4] +clock-in: 9:8 +clock-out: 18:18 +sleep: 9:12 pm/5:1 am +deepValue: + very: + very: + very: + very: + very: + deep: 9.8 --- -#weight:62.9kg +#weight:63.9kg -#exercise-pushup:41 -#exercise-plank:57sec +#exercise-pushup:32 +#exercise-plank:48sec #meditation -⭐ +⭐⭐⭐ -#finance/bank1:-2.1USD +#finance/bank1:-3.4USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] +obsidian-tracker@gmail.com +obsidian-tracker@yahoo.com +weightlifting: 15 -dataviewTarget:: 58 -- Make Progress:: 48 -- Make-Progress:: 84 -dataviewTarget1:: 58/48 -dataviewTarget2:: 48 @ 84 -dataviewTarget3:: 48, 84 +dataviewTarget:: 100 +- Make Progress:: 23 +- Make-Progress:: 79 +dataviewTarget1:: 100/23 +dataviewTarget2:: 23 @ 79 +dataviewTarget3:: 23, 79 #sin:0.99452/0.99452/0.95106/0.86603/0.74314/0.58779/0.40674/0.20791/0.00000 diff --git a/examples/diary/2021-01-09.md b/examples/diary/2021-01-09.md index ac22602..09e8f66 100644 --- a/examples/diary/2021-01-09.md +++ b/examples/diary/2021-01-09.md @@ -1,39 +1,46 @@ --- tags: -mood: 7 -bloodpressure: 171.6/118.2 -bloodpressure1: 171.6, 118.2 -bloodpressure2: [171.6, 118.2] -clock-in: 9:17 -clock-out: 20:27 -sleep: 10:11 pm/7:51 am +mood: 2 +bloodpressure: 177.6/120.2 +bloodpressure1: 177.6, 120.2 +bloodpressure2: [177.6, 120.2] +clock-in: 10:23 +clock-out: 18:32 +sleep: 11:3 pm/6:35 am +deepValue: + very: + very: + very: + very: + very: + deep: 23.3 --- -#weight:80.7kg +#weight:73.1kg -#exercise-pushup:38 -#exercise-plank:76sec +#exercise-pushup:37 +#exercise-plank:44sec +#meditation -⭐⭐⭐⭐ +⭐⭐ -#clean-up -#finance/bank1:-3.1USD +#finance/bank1:-3.7USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] obsidian-tracker@gmail.com -obsidian-tracker@yahoo.com +weightlifting: 19 -dataviewTarget:: 23 -- Make Progress:: 20 -- Make-Progress:: 58 -dataviewTarget1:: 23/20 -dataviewTarget2:: 20 @ 58 -dataviewTarget3:: 20, 58 +dataviewTarget:: 62 +- Make Progress:: 44 +- Make-Progress:: 96 +dataviewTarget1:: 62/44 +dataviewTarget2:: 44 @ 96 +dataviewTarget3:: 44, 96 #sin:0.99452/0.95106/0.86603/0.74314/0.58779/0.40674/0.20791/0.00000/-0.20791 diff --git a/examples/diary/2021-01-10.md b/examples/diary/2021-01-10.md index ce5cc33..d8b26df 100644 --- a/examples/diary/2021-01-10.md +++ b/examples/diary/2021-01-10.md @@ -1,40 +1,45 @@ --- tags: -mood: 9 -bloodpressure: 176/118 -bloodpressure1: 176, 118 -bloodpressure2: [176, 118] -clock-in: 8:54 -clock-out: 18:37 -sleep: 11:54 pm/6:52 am +mood: 7 +bloodpressure: 178/118 +bloodpressure1: 178, 118 +bloodpressure2: [178, 118] +clock-in: 8:31 +clock-out: 17:43 +sleep: 9:7 pm/5:59 am +deepValue: + very: + very: + very: + very: + very: + deep: 8.4 --- -#weight:72.6kg +#weight:79.3kg -#exercise-pushup:31 -#exercise-plank:77sec +#exercise-pushup:38 +#exercise-plank:62sec -#meditation -⭐⭐⭐⭐ +⭐⭐ -#clean-up #finance/bank1:-2.2USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker+1@gmail.com +obsidian-tracker@gmail.com -weightlifting: 20 +weightlifting: 15 -dataviewTarget:: 35 -- Make Progress:: 47 -- Make-Progress:: 70 -dataviewTarget1:: 35/47 -dataviewTarget2:: 47 @ 70 -dataviewTarget3:: 47, 70 +dataviewTarget:: 65 +- Make Progress:: 27 +- Make-Progress:: 77 +dataviewTarget1:: 65/27 +dataviewTarget2:: 27 @ 77 +dataviewTarget3:: 27, 77 #sin:0.95106/0.86603/0.74314/0.58779/0.40674/0.20791/0.00000/-0.20791/-0.40674 diff --git a/examples/diary/2021-01-11.md b/examples/diary/2021-01-11.md index 7be3f2f..359bd58 100644 --- a/examples/diary/2021-01-11.md +++ b/examples/diary/2021-01-11.md @@ -1,41 +1,44 @@ --- tags: work_log mood: 2 -bloodpressure: 168.4/117.8 -bloodpressure1: 168.4, 117.8 -bloodpressure2: [168.4, 117.8] -clock-in: 8:6 -clock-out: 18:40 -sleep: 10:31 pm/6:6 am +bloodpressure: 174.4/115.8 +bloodpressure1: 174.4, 115.8 +bloodpressure2: [174.4, 115.8] +clock-in: 10:8 +clock-out: 16:2 +sleep: 9:49 pm/7:29 am +deepValue: + very: + very: + very: + very: + very: + deep: 50.7 --- -#weight:72.0kg +#weight:71.0kg -#exercise-pushup:30 -#exercise-plank:110sec +#exercise-pushup:43 +#exercise-plank:95sec -#meditation +⭐⭐⭐ -#clean-up - -#finance/bank1:-2.4USD +#finance/bank1:-3.6USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker@gmail.com -obsidian-tracker+1@gmail.com -weightlifting: 15 +weightlifting: 20 -dataviewTarget:: 64 -- Make Progress:: 36 -- Make-Progress:: 93 -dataviewTarget1:: 64/36 -dataviewTarget2:: 36 @ 93 -dataviewTarget3:: 36, 93 +dataviewTarget:: 18 +- Make Progress:: 31 +- Make-Progress:: 85 +dataviewTarget1:: 18/31 +dataviewTarget2:: 31 @ 85 +dataviewTarget3:: 31, 85 #sin:0.86603/0.74314/0.58779/0.40674/0.20791/0.00000/-0.20791/-0.40674/-0.58779 diff --git a/examples/diary/2021-01-12.md b/examples/diary/2021-01-12.md index 98bb8a5..c740387 100644 --- a/examples/diary/2021-01-12.md +++ b/examples/diary/2021-01-12.md @@ -1,39 +1,43 @@ --- tags: work_log -mood: 6 -bloodpressure: 177.8/118.6 -bloodpressure1: 177.8, 118.6 -bloodpressure2: [177.8, 118.6] -clock-in: 9:8 -clock-out: 17:59 -sleep: 11:10 pm/6:41 am +mood: 4 +bloodpressure: 169.8/117.6 +bloodpressure1: 169.8, 117.6 +bloodpressure2: [169.8, 117.6] +clock-in: 9:31 +clock-out: 17:4 +sleep: 11:22 pm/6:1 am +deepValue: + very: + very: + very: + very: + very: + deep: 1.4 --- -#weight:67.9kg +#weight:78.4kg -#exercise-pushup:34 -#exercise-plank:70sec +#exercise-pushup:45 +#exercise-plank:37sec -#meditation -⭐⭐⭐ -#finance/bank1:-2.1USD + +#finance/bank1:-3.7USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker@gmail.com -obsidian-tracker@yahoo.com -dataviewTarget:: 88 -- Make Progress:: 36 -- Make-Progress:: 54 -dataviewTarget1:: 88/36 -dataviewTarget2:: 36 @ 54 -dataviewTarget3:: 36, 54 +dataviewTarget:: 12 +- Make Progress:: 0 +- Make-Progress:: 67 +dataviewTarget1:: 12/0 +dataviewTarget2:: 0 @ 67 +dataviewTarget3:: 0, 67 #sin:0.74314/0.58779/0.40674/0.20791/0.00000/-0.20791/-0.40674/-0.58779/-0.74314 diff --git a/examples/diary/2021-01-13.md b/examples/diary/2021-01-13.md index 2b15abb..9cd34f6 100644 --- a/examples/diary/2021-01-13.md +++ b/examples/diary/2021-01-13.md @@ -1,40 +1,45 @@ --- tags: work_log -mood: 10 -bloodpressure: 167.2/116.4 -bloodpressure1: 167.2, 116.4 -bloodpressure2: [167.2, 116.4] +mood: 7 +bloodpressure: 176.2/116.4 +bloodpressure1: 176.2, 116.4 +bloodpressure2: [176.2, 116.4] clock-in: 10:23 -clock-out: 17:15 -sleep: 10:4 pm/5:0 am +clock-out: 16:5 +sleep: 11:47 pm/7:19 am +deepValue: + very: + very: + very: + very: + very: + deep: 47.9 --- -#weight:67.3kg +#weight:79.5kg -#exercise-pushup:43 -#exercise-plank:110sec +#exercise-pushup:32 +#exercise-plank:53sec #meditation -⭐⭐⭐⭐⭐ -#finance/bank1:-2.4USD + +#finance/bank1:-2.0USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker+1@gmail.com -obsidian-tracker@yahoo.com -weightlifting: 17 +weightlifting: 16 -dataviewTarget:: 47 -- Make Progress:: 47 -- Make-Progress:: 55 -dataviewTarget1:: 47/47 -dataviewTarget2:: 47 @ 55 -dataviewTarget3:: 47, 55 +dataviewTarget:: 56 +- Make Progress:: 34 +- Make-Progress:: 58 +dataviewTarget1:: 56/34 +dataviewTarget2:: 34 @ 58 +dataviewTarget3:: 34, 58 #sin:0.58779/0.40674/0.20791/0.00000/-0.20791/-0.40674/-0.58779/-0.74314/-0.86603 diff --git a/examples/diary/2021-01-14.md b/examples/diary/2021-01-14.md index 63fb0d9..ee320fe 100644 --- a/examples/diary/2021-01-14.md +++ b/examples/diary/2021-01-14.md @@ -1,38 +1,46 @@ --- tags: work_log -mood: 3 -bloodpressure: 167.6/115.2 -bloodpressure1: 167.6, 115.2 -bloodpressure2: [167.6, 115.2] -clock-in: 9:0 -clock-out: 16:2 -sleep: 10:37 pm/6:33 am +mood: 5 +bloodpressure: 167.6/117.2 +bloodpressure1: 167.6, 117.2 +bloodpressure2: [167.6, 117.2] +clock-in: 10:7 +clock-out: 16:5 +sleep: 9:9 pm/7:54 am +deepValue: + very: + very: + very: + very: + very: + deep: 46.6 --- -#weight:74.1kg +#weight:61.9kg -#exercise-pushup:33 -#exercise-plank:75sec +#exercise-pushup:39 +#exercise-plank:111sec #meditation -⭐⭐⭐ -#finance/bank1:-2.3USD + +#finance/bank1:-2.7USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] +obsidian-tracker@yahoo.com -weightlifting: 11 +weightlifting: 20 -dataviewTarget:: 91 -- Make Progress:: 23 -- Make-Progress:: 54 -dataviewTarget1:: 91/23 -dataviewTarget2:: 23 @ 54 -dataviewTarget3:: 23, 54 +dataviewTarget:: 61 +- Make Progress:: 41 +- Make-Progress:: 68 +dataviewTarget1:: 61/41 +dataviewTarget2:: 41 @ 68 +dataviewTarget3:: 41, 68 #sin:0.40674/0.20791/0.00000/-0.20791/-0.40674/-0.58779/-0.74314/-0.86603/-0.95106 diff --git a/examples/diary/2021-01-15.md b/examples/diary/2021-01-15.md index 57514b8..3ecd7ef 100644 --- a/examples/diary/2021-01-15.md +++ b/examples/diary/2021-01-15.md @@ -1,21 +1,28 @@ --- tags: work_log -mood: 5 -bloodpressure: 175/117 -bloodpressure1: 175, 117 -bloodpressure2: [175, 117] -clock-in: 8:53 -clock-out: 17:19 -sleep: 9:40 pm/7:25 am +mood: 7 +bloodpressure: 168/116 +bloodpressure1: 168, 116 +bloodpressure2: [168, 116] +clock-in: 8:13 +clock-out: 18:16 +sleep: 9:28 pm/7:25 am +deepValue: + very: + very: + very: + very: + very: + deep: 10.0 --- -#weight:72.8kg +#weight:77.4kg + +#exercise-pushup:33 +#exercise-plank:32sec -#exercise-pushup:47 -#exercise-plank:63sec -⭐ #finance/bank1:-2.7USD @@ -23,16 +30,18 @@ sleep: 9:40 pm/7:25 am [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] +obsidian-tracker@gmail.com obsidian-tracker+1@gmail.com +obsidian-tracker@yahoo.com -weightlifting: 11 +weightlifting: 12 -dataviewTarget:: 89 -- Make Progress:: 21 -- Make-Progress:: 55 -dataviewTarget1:: 89/21 -dataviewTarget2:: 21 @ 55 -dataviewTarget3:: 21, 55 +dataviewTarget:: 74 +- Make Progress:: 22 +- Make-Progress:: 73 +dataviewTarget1:: 74/22 +dataviewTarget2:: 22 @ 73 +dataviewTarget3:: 22, 73 #sin:0.20791/0.00000/-0.20791/-0.40674/-0.58779/-0.74314/-0.86603/-0.95106/-0.99452 diff --git a/examples/diary/2021-01-16.md b/examples/diary/2021-01-16.md index edc0e18..12688a7 100644 --- a/examples/diary/2021-01-16.md +++ b/examples/diary/2021-01-16.md @@ -1,39 +1,46 @@ --- tags: -mood: 9 -bloodpressure: 166.4/114.8 -bloodpressure1: 166.4, 114.8 -bloodpressure2: [166.4, 114.8] -clock-in: 10:4 -clock-out: 18:20 -sleep: 10:40 pm/7:14 am +mood: 5 +bloodpressure: 173.4/114.8 +bloodpressure1: 173.4, 114.8 +bloodpressure2: [173.4, 114.8] +clock-in: 8:32 +clock-out: 17:4 +sleep: 9:51 pm/6:15 am +deepValue: + very: + very: + very: + very: + very: + deep: 61.0 --- -#weight:71.9kg +#weight:63.1kg -#exercise-pushup:45 -#exercise-plank:69sec +#exercise-pushup:43 +#exercise-plank:110sec -#meditation ⭐⭐ -#finance/bank1:-2.1USD +#finance/bank1:-3.1USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] +obsidian-tracker@gmail.com obsidian-tracker+1@gmail.com -weightlifting: 10 +weightlifting: 20 -dataviewTarget:: 86 -- Make Progress:: 20 -- Make-Progress:: 63 -dataviewTarget1:: 86/20 -dataviewTarget2:: 20 @ 63 -dataviewTarget3:: 20, 63 +dataviewTarget:: 54 +- Make Progress:: 34 +- Make-Progress:: 91 +dataviewTarget1:: 54/34 +dataviewTarget2:: 34 @ 91 +dataviewTarget3:: 34, 91 #sin:0.00000/-0.20791/-0.40674/-0.58779/-0.74314/-0.86603/-0.95106/-0.99452/-0.99452 diff --git a/examples/diary/2021-01-17.md b/examples/diary/2021-01-17.md index 32a9ce7..5b43b54 100644 --- a/examples/diary/2021-01-17.md +++ b/examples/diary/2021-01-17.md @@ -1,39 +1,44 @@ --- tags: -mood: 7 -bloodpressure: 165.8/117.6 -bloodpressure1: 165.8, 117.6 -bloodpressure2: [165.8, 117.6] -clock-in: 10:48 -clock-out: 17:8 -sleep: 10:54 pm/6:3 am +mood: 5 +bloodpressure: 168.8/118.6 +bloodpressure1: 168.8, 118.6 +bloodpressure2: [168.8, 118.6] +clock-in: 9:25 +clock-out: 17:18 +sleep: 9:42 pm/5:42 am +deepValue: + very: + very: + very: + very: + very: + deep: 15.4 --- -#weight:79.8kg +#weight:63.5kg #exercise-pushup:38 -#exercise-plank:79sec +#exercise-plank:107sec -⭐⭐⭐⭐ +⭐ -#finance/bank1:-2.8USD +#finance/bank1:-2.4USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker@gmail.com -obsidian-tracker+1@gmail.com -weightlifting: 13 +weightlifting: 14 -dataviewTarget:: 5 -- Make Progress:: 35 -- Make-Progress:: 50 -dataviewTarget1:: 5/35 -dataviewTarget2:: 35 @ 50 -dataviewTarget3:: 35, 50 +dataviewTarget:: 70 +- Make Progress:: 17 +- Make-Progress:: 75 +dataviewTarget1:: 70/17 +dataviewTarget2:: 17 @ 75 +dataviewTarget3:: 17, 75 #sin:-0.20791/-0.40674/-0.58779/-0.74314/-0.86603/-0.95106/-0.99452/-0.99452/-0.95106 diff --git a/examples/diary/2021-01-18.md b/examples/diary/2021-01-18.md index 2f387fd..22baab9 100644 --- a/examples/diary/2021-01-18.md +++ b/examples/diary/2021-01-18.md @@ -1,39 +1,46 @@ --- tags: work_log -mood: 8 -bloodpressure: 165.2/114.4 -bloodpressure1: 165.2, 114.4 -bloodpressure2: [165.2, 114.4] -clock-in: 9:50 -clock-out: 19:13 -sleep: 9:10 pm/5:3 am +mood: 5 +bloodpressure: 166.2/118.4 +bloodpressure1: 166.2, 118.4 +bloodpressure2: [166.2, 118.4] +clock-in: 10:44 +clock-out: 19:39 +sleep: 11:31 pm/5:44 am +deepValue: + very: + very: + very: + very: + very: + deep: 68.6 --- -#weight:61.2kg +#weight:64.2kg -#exercise-pushup:38 -#exercise-plank:73sec +#exercise-pushup:39 +#exercise-plank:99sec -⭐⭐ +⭐⭐⭐⭐⭐ -#finance/bank1:-2.9USD +#finance/bank1:-2.6USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker+1@gmail.com +obsidian-tracker@gmail.com obsidian-tracker@yahoo.com -weightlifting: 18 +weightlifting: 16 -dataviewTarget:: 67 -- Make Progress:: 41 -- Make-Progress:: 76 -dataviewTarget1:: 67/41 -dataviewTarget2:: 41 @ 76 -dataviewTarget3:: 41, 76 +dataviewTarget:: 2 +- Make Progress:: 8 +- Make-Progress:: 93 +dataviewTarget1:: 2/8 +dataviewTarget2:: 8 @ 93 +dataviewTarget3:: 8, 93 #sin:-0.40674/-0.58779/-0.74314/-0.86603/-0.95106/-0.99452/-0.99452/-0.95106/-0.86603 diff --git a/examples/diary/2021-01-19.md b/examples/diary/2021-01-19.md index 6057d79..6edf412 100644 --- a/examples/diary/2021-01-19.md +++ b/examples/diary/2021-01-19.md @@ -1,39 +1,45 @@ --- tags: work_log -mood: 1 -bloodpressure: 171.6/117.2 -bloodpressure1: 171.6, 117.2 -bloodpressure2: [171.6, 117.2] -clock-in: 8:27 -clock-out: 19:38 -sleep: 11:7 pm/5:53 am +mood: 9 +bloodpressure: 165.6/118.2 +bloodpressure1: 165.6, 118.2 +bloodpressure2: [165.6, 118.2] +clock-in: 9:31 +clock-out: 16:13 +sleep: 11:39 pm/6:11 am +deepValue: + very: + very: + very: + very: + very: + deep: 44.1 --- -#weight:63.5kg +#weight:80.0kg -#exercise-pushup:46 -#exercise-plank:87sec +#exercise-pushup:35 +#exercise-plank:55sec +⭐⭐⭐ - -#finance/bank1:-3.7USD +#finance/bank1:-3.1USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker@gmail.com -obsidian-tracker@yahoo.com +obsidian-tracker+1@gmail.com -weightlifting: 16 +weightlifting: 15 -dataviewTarget:: 4 -- Make Progress:: 11 -- Make-Progress:: 100 -dataviewTarget1:: 4/11 -dataviewTarget2:: 11 @ 100 -dataviewTarget3:: 11, 100 +dataviewTarget:: 71 +- Make Progress:: 9 +- Make-Progress:: 79 +dataviewTarget1:: 71/9 +dataviewTarget2:: 9 @ 79 +dataviewTarget3:: 9, 79 #sin:-0.58779/-0.74314/-0.86603/-0.95106/-0.99452/-0.99452/-0.95106/-0.86603/-0.74314 diff --git a/examples/diary/2021-01-20.md b/examples/diary/2021-01-20.md index 6af17c5..6556c0e 100644 --- a/examples/diary/2021-01-20.md +++ b/examples/diary/2021-01-20.md @@ -1,39 +1,45 @@ --- tags: work_log -mood: 7 -bloodpressure: 169/114 -bloodpressure1: 169, 114 -bloodpressure2: [169, 114] -clock-in: 9:57 -clock-out: 17:16 -sleep: 11:37 pm/6:33 am +mood: 10 +bloodpressure: 163/115 +bloodpressure1: 163, 115 +bloodpressure2: [163, 115] +clock-in: 9:49 +clock-out: 18:2 +sleep: 10:46 pm/7:45 am +deepValue: + very: + very: + very: + very: + very: + deep: 22.5 --- -#weight:64.3kg +#weight:60.6kg -#exercise-pushup:44 -#exercise-plank:45sec +#exercise-pushup:43 +#exercise-plank:66sec -⭐ -#finance/bank1:-2.4USD + +#finance/bank1:-3.9USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker@gmail.com -obsidian-tracker+1@gmail.com +obsidian-tracker@yahoo.com -weightlifting: 13 +weightlifting: 14 -dataviewTarget:: 82 -- Make Progress:: 21 -- Make-Progress:: 52 -dataviewTarget1:: 82/21 -dataviewTarget2:: 21 @ 52 -dataviewTarget3:: 21, 52 +dataviewTarget:: 99 +- Make Progress:: 11 +- Make-Progress:: 53 +dataviewTarget1:: 99/11 +dataviewTarget2:: 11 @ 53 +dataviewTarget3:: 11, 53 #sin:-0.74314/-0.86603/-0.95106/-0.99452/-0.99452/-0.95106/-0.86603/-0.74314/-0.58779 diff --git a/examples/diary/2021-01-21.md b/examples/diary/2021-01-21.md index 4aff3c6..009ce2b 100644 --- a/examples/diary/2021-01-21.md +++ b/examples/diary/2021-01-21.md @@ -1,37 +1,45 @@ --- tags: work_log -mood: 4 -bloodpressure: 170.4/116.8 -bloodpressure1: 170.4, 116.8 -bloodpressure2: [170.4, 116.8] -clock-in: 10:13 -clock-out: 16:37 -sleep: 10:43 pm/5:10 am +mood: 5 +bloodpressure: 169.4/117.8 +bloodpressure1: 169.4, 117.8 +bloodpressure2: [169.4, 117.8] +clock-in: 9:59 +clock-out: 16:39 +sleep: 10:38 pm/5:35 am +deepValue: + very: + very: + very: + very: + very: + deep: 12.5 --- -#weight:80.1kg +#weight:61.8kg -#exercise-pushup:38 -#exercise-plank:37sec +#exercise-pushup:32 +#exercise-plank:105sec -⭐⭐ +⭐⭐⭐ -#finance/bank1:-4.0USD +#finance/bank1:-2.2USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] +obsidian-tracker@yahoo.com -weightlifting: 17 +weightlifting: 10 -dataviewTarget:: 56 -- Make Progress:: 50 -- Make-Progress:: 57 -dataviewTarget1:: 56/50 -dataviewTarget2:: 50 @ 57 -dataviewTarget3:: 50, 57 +dataviewTarget:: 8 +- Make Progress:: 37 +- Make-Progress:: 74 +dataviewTarget1:: 8/37 +dataviewTarget2:: 37 @ 74 +dataviewTarget3:: 37, 74 #sin:-0.86603/-0.95106/-0.99452/-0.99452/-0.95106/-0.86603/-0.74314/-0.58779/-0.40674 diff --git a/examples/diary/2021-01-22.md b/examples/diary/2021-01-22.md index 0c7c474..0d69867 100644 --- a/examples/diary/2021-01-22.md +++ b/examples/diary/2021-01-22.md @@ -1,39 +1,45 @@ --- tags: work_log -mood: 7 -bloodpressure: 167.8/116.6 -bloodpressure1: 167.8, 116.6 -bloodpressure2: [167.8, 116.6] -clock-in: 8:35 -clock-out: 16:5 -sleep: 9:49 pm/6:38 am +mood: 5 +bloodpressure: 168.8/115.6 +bloodpressure1: 168.8, 115.6 +bloodpressure2: [168.8, 115.6] +clock-in: 9:6 +clock-out: 17:57 +sleep: 11:28 pm/6:45 am +deepValue: + very: + very: + very: + very: + very: + deep: 39.0 --- -#weight:60.2kg +#weight:77.6kg -#exercise-pushup:31 -#exercise-plank:46sec +#exercise-pushup:32 +#exercise-plank:48sec -#meditation -⭐⭐⭐⭐ +⭐⭐⭐ -#finance/bank1:-2.7USD +#finance/bank1:-3.6USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker+1@gmail.com +obsidian-tracker@yahoo.com -weightlifting: 15 +weightlifting: 14 -dataviewTarget:: 48 -- Make Progress:: 32 -- Make-Progress:: 55 -dataviewTarget1:: 48/32 -dataviewTarget2:: 32 @ 55 -dataviewTarget3:: 32, 55 +dataviewTarget:: 64 +- Make Progress:: 27 +- Make-Progress:: 90 +dataviewTarget1:: 64/27 +dataviewTarget2:: 27 @ 90 +dataviewTarget3:: 27, 90 #sin:-0.95106/-0.99452/-0.99452/-0.95106/-0.86603/-0.74314/-0.58779/-0.40674/-0.20791 diff --git a/examples/diary/2021-01-23.md b/examples/diary/2021-01-23.md index 35b0478..68a1da4 100644 --- a/examples/diary/2021-01-23.md +++ b/examples/diary/2021-01-23.md @@ -1,41 +1,46 @@ --- tags: -mood: 4 -bloodpressure: 171.2/116.4 -bloodpressure1: 171.2, 116.4 -bloodpressure2: [171.2, 116.4] -clock-in: 9:38 -clock-out: 19:23 -sleep: 11:8 pm/5:50 am +mood: 5 +bloodpressure: 167.2/115.4 +bloodpressure1: 167.2, 115.4 +bloodpressure2: [167.2, 115.4] +clock-in: 8:12 +clock-out: 20:13 +sleep: 9:46 pm/7:12 am +deepValue: + very: + very: + very: + very: + very: + deep: 66.0 --- -#weight:73.9kg +#weight:73.5kg -#exercise-pushup:48 -#exercise-plank:104sec +#exercise-pushup:46 +#exercise-plank:55sec #meditation -⭐⭐ -#finance/bank1:-2.8USD + +#finance/bank1:-2.7USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker@gmail.com obsidian-tracker+1@gmail.com -obsidian-tracker@yahoo.com -weightlifting: 18 +weightlifting: 10 -dataviewTarget:: 57 -- Make Progress:: 21 -- Make-Progress:: 61 -dataviewTarget1:: 57/21 -dataviewTarget2:: 21 @ 61 -dataviewTarget3:: 21, 61 +dataviewTarget:: 53 +- Make Progress:: 38 +- Make-Progress:: 81 +dataviewTarget1:: 53/38 +dataviewTarget2:: 38 @ 81 +dataviewTarget3:: 38, 81 #sin:-0.99452/-0.99452/-0.95106/-0.86603/-0.74314/-0.58779/-0.40674/-0.20791/-0.00000 diff --git a/examples/diary/2021-01-24.md b/examples/diary/2021-01-24.md index e82e8f2..f0a44b0 100644 --- a/examples/diary/2021-01-24.md +++ b/examples/diary/2021-01-24.md @@ -1,39 +1,47 @@ --- tags: -mood: 3 -bloodpressure: 169.6/114.2 -bloodpressure1: 169.6, 114.2 -bloodpressure2: [169.6, 114.2] -clock-in: 8:46 -clock-out: 20:12 -sleep: 10:38 pm/7:16 am +mood: 7 +bloodpressure: 165.6/114.2 +bloodpressure1: 165.6, 114.2 +bloodpressure2: [165.6, 114.2] +clock-in: 8:41 +clock-out: 18:17 +sleep: 10:49 pm/6:41 am +deepValue: + very: + very: + very: + very: + very: + deep: 78.8 --- -#weight:63.7kg +#weight:68.8kg -#exercise-pushup:31 -#exercise-plank:59sec +#exercise-pushup:37 +#exercise-plank:81sec #meditation +⭐ - -#finance/bank1:-2.2USD +#finance/bank1:-3.0USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] +obsidian-tracker+1@gmail.com obsidian-tracker@yahoo.com -weightlifting: 18 +weightlifting: 19 -dataviewTarget:: 62 -- Make Progress:: 26 -- Make-Progress:: 68 -dataviewTarget1:: 62/26 -dataviewTarget2:: 26 @ 68 -dataviewTarget3:: 26, 68 +dataviewTarget:: 98 +- Make Progress:: 1 +- Make-Progress:: 60 +dataviewTarget1:: 98/1 +dataviewTarget2:: 1 @ 60 +dataviewTarget3:: 1, 60 #sin:-0.99452/-0.95106/-0.86603/-0.74314/-0.58779/-0.40674/-0.20791/-0.00000/0.20791 diff --git a/examples/diary/2021-01-25.md b/examples/diary/2021-01-25.md index f730597..85adb37 100644 --- a/examples/diary/2021-01-25.md +++ b/examples/diary/2021-01-25.md @@ -1,25 +1,31 @@ --- tags: work_log -mood: 2 -bloodpressure: 167/115 -bloodpressure1: 167, 115 -bloodpressure2: [167, 115] -clock-in: 8:20 -clock-out: 20:35 -sleep: 11:46 pm/6:22 am +mood: 1 +bloodpressure: 165/113 +bloodpressure1: 165, 113 +bloodpressure2: [165, 113] +clock-in: 9:53 +clock-out: 16:33 +sleep: 10:11 pm/6:9 am +deepValue: + very: + very: + very: + very: + very: + deep: 33.4 --- -#weight:71.9kg +#weight:74.9kg -#exercise-pushup:35 -#exercise-plank:46sec +#exercise-pushup:33 +#exercise-plank:91sec -#meditation -⭐⭐ +⭐ -#finance/bank1:-3.1USD +#finance/bank1:-2.2USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] @@ -27,13 +33,14 @@ sleep: 11:46 pm/6:22 am obsidian-tracker@gmail.com obsidian-tracker+1@gmail.com +weightlifting: 12 -dataviewTarget:: 21 -- Make Progress:: 1 -- Make-Progress:: 77 -dataviewTarget1:: 21/1 -dataviewTarget2:: 1 @ 77 -dataviewTarget3:: 1, 77 +dataviewTarget:: 85 +- Make Progress:: 36 +- Make-Progress:: 64 +dataviewTarget1:: 85/36 +dataviewTarget2:: 36 @ 64 +dataviewTarget3:: 36, 64 #sin:-0.95106/-0.86603/-0.74314/-0.58779/-0.40674/-0.20791/-0.00000/0.20791/0.40674 diff --git a/examples/diary/2021-01-26.md b/examples/diary/2021-01-26.md index 4321670..ba3b8dd 100644 --- a/examples/diary/2021-01-26.md +++ b/examples/diary/2021-01-26.md @@ -1,39 +1,46 @@ --- tags: work_log -mood: 2 -bloodpressure: 162.4/116.8 -bloodpressure1: 162.4, 116.8 -bloodpressure2: [162.4, 116.8] -clock-in: 8:33 -clock-out: 17:11 -sleep: 10:9 pm/5:42 am +mood: 7 +bloodpressure: 168.4/113.8 +bloodpressure1: 168.4, 113.8 +bloodpressure2: [168.4, 113.8] +clock-in: 10:24 +clock-out: 20:21 +sleep: 9:48 pm/5:2 am +deepValue: + very: + very: + very: + very: + very: + deep: 89.2 --- -#weight:63.4kg +#weight:79.1kg -#exercise-pushup:44 -#exercise-plank:107sec +#exercise-pushup:32 +#exercise-plank:102sec -#meditation -⭐⭐⭐ +⭐ -#finance/bank1:-3.9USD +#finance/bank1:-2.9USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] obsidian-tracker@gmail.com +obsidian-tracker@yahoo.com -weightlifting: 19 +weightlifting: 12 dataviewTarget:: 71 -- Make Progress:: 14 -- Make-Progress:: 83 -dataviewTarget1:: 71/14 -dataviewTarget2:: 14 @ 83 -dataviewTarget3:: 14, 83 +- Make Progress:: 13 +- Make-Progress:: 66 +dataviewTarget1:: 71/13 +dataviewTarget2:: 13 @ 66 +dataviewTarget3:: 13, 66 #sin:-0.86603/-0.74314/-0.58779/-0.40674/-0.20791/-0.00000/0.20791/0.40674/0.58779 diff --git a/examples/diary/2021-01-27.md b/examples/diary/2021-01-27.md index 8624562..59526d2 100644 --- a/examples/diary/2021-01-27.md +++ b/examples/diary/2021-01-27.md @@ -1,39 +1,46 @@ --- tags: work_log -mood: 9 -bloodpressure: 160.8/116.6 -bloodpressure1: 160.8, 116.6 -bloodpressure2: [160.8, 116.6] -clock-in: 9:53 -clock-out: 17:3 -sleep: 11:6 pm/5:52 am +mood: 6 +bloodpressure: 166.8/114.6 +bloodpressure1: 166.8, 114.6 +bloodpressure2: [166.8, 114.6] +clock-in: 9:12 +clock-out: 19:36 +sleep: 10:2 pm/7:36 am +deepValue: + very: + very: + very: + very: + very: + deep: 58.7 --- -#weight:79.1kg +#weight:79.5kg -#exercise-pushup:32 -#exercise-plank:102sec +#exercise-pushup:38 +#exercise-plank:59sec +#meditation -⭐ +⭐⭐⭐ -#finance/bank1:-2.9USD +#finance/bank1:-2.2USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] obsidian-tracker@gmail.com -obsidian-tracker@yahoo.com -weightlifting: 12 +weightlifting: 14 -dataviewTarget:: 71 -- Make Progress:: 13 -- Make-Progress:: 66 -dataviewTarget1:: 71/13 -dataviewTarget2:: 13 @ 66 -dataviewTarget3:: 13, 66 +dataviewTarget:: 86 +- Make Progress:: 26 +- Make-Progress:: 68 +dataviewTarget1:: 86/26 +dataviewTarget2:: 26 @ 68 +dataviewTarget3:: 26, 68 #sin:-0.74314/-0.58779/-0.40674/-0.20791/-0.00000/0.20791/0.40674/0.58779/0.74314 diff --git a/examples/diary/2021-01-28.md b/examples/diary/2021-01-28.md index 0d35093..5943460 100644 --- a/examples/diary/2021-01-28.md +++ b/examples/diary/2021-01-28.md @@ -1,39 +1,45 @@ --- tags: work_log -mood: 6 -bloodpressure: 166.2/114.4 -bloodpressure1: 166.2, 114.4 -bloodpressure2: [166.2, 114.4] -clock-in: 9:12 -clock-out: 19:36 -sleep: 10:2 pm/7:36 am +mood: 7 +bloodpressure: 160.2/116.4 +bloodpressure1: 160.2, 116.4 +bloodpressure2: [160.2, 116.4] +clock-in: 9:28 +clock-out: 16:58 +sleep: 10:28 pm/5:46 am +deepValue: + very: + very: + very: + very: + very: + deep: 17.7 --- -#weight:72.2kg +#weight:76.2kg -#exercise-pushup:49 -#exercise-plank:68sec +#exercise-pushup:32 +#exercise-plank:58sec -#meditation -⭐⭐ -#finance/bank1:-2.6USD + +#finance/bank1:-2.5USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] obsidian-tracker+1@gmail.com -weightlifting: 11 +weightlifting: 15 -dataviewTarget:: 39 -- Make Progress:: 43 -- Make-Progress:: 76 -dataviewTarget1:: 39/43 -dataviewTarget2:: 43 @ 76 -dataviewTarget3:: 43, 76 +dataviewTarget:: 62 +- Make Progress:: 17 +- Make-Progress:: 81 +dataviewTarget1:: 62/17 +dataviewTarget2:: 17 @ 81 +dataviewTarget3:: 17, 81 #sin:-0.58779/-0.40674/-0.20791/-0.00000/0.20791/0.40674/0.58779/0.74314/0.86603 diff --git a/examples/diary/2021-01-29.md b/examples/diary/2021-01-29.md index 8cfa359..9139fcd 100644 --- a/examples/diary/2021-01-29.md +++ b/examples/diary/2021-01-29.md @@ -1,38 +1,46 @@ --- tags: work_log -mood: 4 -bloodpressure: 164.6/113.2 -bloodpressure1: 164.6, 113.2 -bloodpressure2: [164.6, 113.2] -clock-in: 10:28 -clock-out: 18:4 -sleep: 11:34 pm/6:12 am +mood: 2 +bloodpressure: 157.6/115.2 +bloodpressure1: 157.6, 115.2 +bloodpressure2: [157.6, 115.2] +clock-in: 10:44 +clock-out: 16:41 +sleep: 10:27 pm/5:11 am +deepValue: + very: + very: + very: + very: + very: + deep: 80.6 --- -#weight:76.3kg +#weight:79.2kg -#exercise-pushup:33 -#exercise-plank:100sec +#exercise-pushup:36 +#exercise-plank:90sec -#meditation -⭐ -#finance/bank1:-2.3USD + +#finance/bank1:-3.9USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] +obsidian-tracker@gmail.com +obsidian-tracker@yahoo.com weightlifting: 18 -dataviewTarget:: 54 -- Make Progress:: 29 -- Make-Progress:: 81 -dataviewTarget1:: 54/29 -dataviewTarget2:: 29 @ 81 -dataviewTarget3:: 29, 81 +dataviewTarget:: 36 +- Make Progress:: 33 +- Make-Progress:: 92 +dataviewTarget1:: 36/33 +dataviewTarget2:: 33 @ 92 +dataviewTarget3:: 33, 92 #sin:-0.40674/-0.20791/-0.00000/0.20791/0.40674/0.58779/0.74314/0.86603/0.95106 diff --git a/examples/diary/2021-01-30.md b/examples/diary/2021-01-30.md index e7f7c71..30adfd6 100644 --- a/examples/diary/2021-01-30.md +++ b/examples/diary/2021-01-30.md @@ -1,24 +1,32 @@ --- tags: -mood: 4 -bloodpressure: 163/112 -bloodpressure1: 163, 112 -bloodpressure2: [163, 112] -clock-in: 8:39 -clock-out: 20:44 -sleep: 9:41 pm/6:27 am +mood: 7 +bloodpressure: 165/114 +bloodpressure1: 165, 114 +bloodpressure2: [165, 114] +clock-in: 8:16 +clock-out: 17:2 +sleep: 10:51 pm/7:29 am +deepValue: + very: + very: + very: + very: + very: + deep: 65.7 --- -#weight:60.1kg +#weight:63.9kg #exercise-pushup:33 -#exercise-plank:102sec +#exercise-plank:83sec +#meditation -⭐ +⭐⭐⭐⭐ -#finance/bank1:-3.2USD +#finance/bank1:-3.9USD #finance/bank2:200USD #finance/bank2/transfer:-100USD #finance/bank1/transfer:100USD @@ -26,16 +34,15 @@ sleep: 9:41 pm/6:27 am [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker@yahoo.com -weightlifting: 19 +weightlifting: 18 -dataviewTarget:: 99 -- Make Progress:: 38 -- Make-Progress:: 64 -dataviewTarget1:: 99/38 -dataviewTarget2:: 38 @ 64 -dataviewTarget3:: 38, 64 +dataviewTarget:: 69 +- Make Progress:: 48 +- Make-Progress:: 94 +dataviewTarget1:: 69/48 +dataviewTarget2:: 48 @ 94 +dataviewTarget3:: 48, 94 #sin:-0.20791/-0.00000/0.20791/0.40674/0.58779/0.74314/0.86603/0.95106/0.99452 diff --git a/examples/diary/2021-01-31.md b/examples/diary/2021-01-31.md index dfa3907..50858f6 100644 --- a/examples/diary/2021-01-31.md +++ b/examples/diary/2021-01-31.md @@ -1,38 +1,46 @@ --- tags: -mood: 4 -bloodpressure: 163.4/115.8 -bloodpressure1: 163.4, 115.8 -bloodpressure2: [163.4, 115.8] -clock-in: 9:44 -clock-out: 18:10 -sleep: 9:19 pm/5:35 am +mood: 8 +bloodpressure: 159.4/113.8 +bloodpressure1: 159.4, 113.8 +bloodpressure2: [159.4, 113.8] +clock-in: 8:30 +clock-out: 20:24 +sleep: 10:4 pm/7:10 am +deepValue: + very: + very: + very: + very: + very: + deep: 32.2 --- -#weight:78.1kg +#weight:76.4kg -#exercise-pushup:46 -#exercise-plank:74sec +#exercise-pushup:33 +#exercise-plank:44sec +#meditation -⭐ +⭐⭐⭐ -#finance/bank1:-3.2USD +#finance/bank1:-3.0USD [[todo_family|To-Do @Family]] [[todo_work|To-Do @Work]] -obsidian-tracker+1@gmail.com +obsidian-tracker@gmail.com +obsidian-tracker@yahoo.com -weightlifting: 20 -dataviewTarget:: 6 -- Make Progress:: 2 -- Make-Progress:: 89 -dataviewTarget1:: 6/2 -dataviewTarget2:: 2 @ 89 -dataviewTarget3:: 2, 89 +dataviewTarget:: 7 +- Make Progress:: 24 +- Make-Progress:: 72 +dataviewTarget1:: 7/24 +dataviewTarget2:: 24 @ 72 +dataviewTarget3:: 24, 72 #sin:-0.00000/0.20791/0.40674/0.58779/0.74314/0.86603/0.95106/0.99452/0.99452 diff --git a/examples/example_generator.ts b/examples/example_generator.ts index 6320e92..d206bdb 100644 --- a/examples/example_generator.ts +++ b/examples/example_generator.ts @@ -104,6 +104,16 @@ for ( " am"; frontmatter += "sleep: " + time_in_bed + "/" + time_out_of_bed + "\n"; + // deep value + let deepValue = randomFloatFromInterval(0.0, 100.0); + frontmatter += "deepValue: " + "\n"; + let indent = " "; + for (let ind = 0; ind < 5; ind++) { + frontmatter += indent + "very: " + "\n"; + indent = indent + " "; + } + frontmatter += indent + "deep: " + deepValue.toFixed(1) + "\n"; + frontmatter += "---\n"; content += frontmatter; diff --git a/src/collecting.ts b/src/collecting.ts index 1d337a3..94594a6 100644 --- a/src/collecting.ts +++ b/src/collecting.ts @@ -97,13 +97,13 @@ export function collectDataFromFrontmatterKey( dataMap: DataMap, xValueMap: XValueMap ) { + // console.log("collectDataFromFrontmatterKey"); + let frontMatter = fileCache.frontmatter; if (frontMatter) { - if (frontMatter[query.getTarget()]) { - // console.log("single value"); - // console.log(frontMatter[query.getTarget()]); + if (helper.deepValue(frontMatter, query.getTarget())) { let value = null; - let toParse = frontMatter[query.getTarget()]; + let toParse = helper.deepValue(frontMatter, query.getTarget()); if (typeof toParse === "string") { if (toParse.includes(":")) { // time value @@ -126,7 +126,7 @@ export function collectDataFromFrontmatterKey( } } else if ( query.getParentTarget() && - frontMatter[query.getParentTarget()] + helper.deepValue(frontMatter, query.getParentTarget()) ) { // console.log("multiple values"); // console.log(query.getTarget()); @@ -135,7 +135,10 @@ export function collectDataFromFrontmatterKey( // console.log( // frontMatter[query.getParentTarget()] // ); - let toParse = frontMatter[query.getParentTarget()]; + let toParse = helper.deepValue( + frontMatter, + query.getParentTarget() + ); let splitted = null; if (Array.isArray(toParse)) { splitted = toParse.map((p) => { diff --git a/src/helper.ts b/src/helper.ts index 69f8f9f..f1edb43 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -2,6 +2,22 @@ import { RenderInfo, Size } from "./data"; import { TFile, TFolder, normalizePath } from "obsidian"; import * as d3 from "d3"; +// http://jsfiddle.net/alnitak/hEsys/ +export function deepValue(obj: any, str: string) { + str = str.replace(/\[(\w+)\]/g, ".$1"); + str = str.replace(/^\./, ""); + var a = str.split("."); + for (var i = 0, n = a.length; i < n; ++i) { + var k = a[i]; + if (k in obj) { + obj = obj[k]; + } else { + return; + } + } + return obj; +} + // String helpers export function trimByChar(str: string, char: string) { const arr = Array.from(str); From 89ad5818768281b2e3575c3a7cb3ff3064af39e7 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Wed, 26 May 2021 20:08:40 +0800 Subject: [PATCH 018/402] Fix searchTarget containing accessor with deep value parsing --- src/helper.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/helper.ts b/src/helper.ts index f1edb43..bd218a6 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -4,7 +4,6 @@ import * as d3 from "d3"; // http://jsfiddle.net/alnitak/hEsys/ export function deepValue(obj: any, str: string) { - str = str.replace(/\[(\w+)\]/g, ".$1"); str = str.replace(/^\./, ""); var a = str.split("."); for (var i = 0, n = a.length; i < n; ++i) { From b08daba198355d41252b22b4b1ef5e172f1e0e84 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Wed, 26 May 2021 20:28:48 +0800 Subject: [PATCH 019/402] Add a parameter valueShift --- src/data.ts | 14 ++++++++++++-- src/parsing.ts | 18 +++++++++--------- src/rendering.ts | 43 ++++++++++++++++++++----------------------- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/data.ts b/src/data.ts index b648507..c6b22b1 100644 --- a/src/data.ts +++ b/src/data.ts @@ -253,6 +253,16 @@ export class Dataset implements IterableIterator { return this.yMax; } + public shift(shiftAmount: number) { + for (let ind = 0; ind < this.values.length; ind++) { + if (this.values[ind] !== null) { + this.values[ind] = this.values[ind] + shiftAmount; + } + } + this.yMin = this.yMin + shiftAmount; + this.yMax = this.yMax + shiftAmount; + } + public setPenalty(penalty: number) { for (let ind = 0; ind < this.values.length; ind++) { if (this.values[ind] === null) { @@ -467,7 +477,7 @@ export class RenderInfo { ignoreZeroValue: boolean[]; accum: boolean[]; penalty: number[]; - shift: number[]; + valueShift: number[]; valueType: string[]; // number/float, int, string, boolean, date, time, datetime dataAreaSize: Size; @@ -500,7 +510,7 @@ export class RenderInfo { this.ignoreZeroValue = []; // false this.accum = []; // false, accum values start from zero over days this.penalty = []; // null, use this value instead of null value - this.shift = []; + this.valueShift = []; this.valueType = []; this.dataAreaSize = new Size(300, 300); diff --git a/src/parsing.ts b/src/parsing.ts index 9ca588f..bd13af3 100644 --- a/src/parsing.ts +++ b/src/parsing.ts @@ -1094,19 +1094,19 @@ export function getRenderInfoFromYaml( renderInfo.penalty = retPenalty; // console.log(renderInfo.penalty); - // shift - let retShift = getNumberArrayFromInput( - "shift", - yaml.shift, + // valueShift + let retValueShift = getNumberArrayFromInput( + "valueShift", + yaml.valueShift, numDatasets, - null, + 0, true ); - if (typeof retShift === "string") { - return retShift; + if (typeof retValueShift === "string") { + return retValueShift; } - renderInfo.shift = retShift; - // console.log(renderInfo.shift); + renderInfo.valueShift = retValueShift; + // console.log(renderInfo.valueShift); // fixedScale if (typeof yaml.fixedScale === "number") { diff --git a/src/rendering.ts b/src/rendering.ts index 3599a06..cfd1216 100644 --- a/src/rendering.ts +++ b/src/rendering.ts @@ -71,14 +71,14 @@ function getYTickValues(yLower: number, yUpper: number) { // currently used for time value tick only, value in seconds const absExtent = Math.abs(yUpper - yLower); let tickValues = []; - if (absExtent > 5 * 60 * 60) {// extent over than 5 hours + if (absExtent > 5 * 60 * 60) { + // extent over than 5 hours // tick on the hour yLower = Math.floor(yLower / 3600) * 3600; yUpper = Math.ceil(yUpper / 3600) * 3600; tickValues = d3.range(yLower, yUpper, 3600); - } - else { + } else { // tick on the half hour yLower = Math.floor(yLower / 1800) * 1800; yUpper = Math.ceil(yUpper / 1800) * 1800; @@ -98,19 +98,15 @@ function getYTickFormat(yLower: number, yUpper: number) { let tickTime = dayStart.add(value, "seconds"); let format = tickTime.format("HH:mm"); if (absExtent > 12 * 60 * 60) { - let devHour = (value - yLower) / 3600; let interleave = devHour % 2; if (value <= yLower) { format = ""; - } - else if (value >= yUpper) { + } else if (value >= yUpper) { format = ""; - } - else if ( interleave > 1.0) { + } else if (interleave > 1.0) { format = tickTime.format("HH:mm"); - } - else { + } else { format = ""; } } @@ -126,12 +122,17 @@ export function render(canvas: HTMLElement, renderInfo: RenderInfo) { // console.log(renderInfo.datasets); // Data preprocessing - for (let dataset of renderInfo.datasets) { if (dataset.getQuery().usedAsXDataset) continue; + // valueShift + if (renderInfo.valueShift[dataset.getId()] !== null) { + dataset.shift(renderInfo.valueShift[dataset.getId()]); + } + // penalty if (renderInfo.penalty[dataset.getId()] !== null) { dataset.setPenalty(renderInfo.penalty[dataset.getId()]); } + // accum if (renderInfo.accum[dataset.getId()]) { dataset.accumulateValues(); } @@ -390,9 +391,7 @@ function renderYAxis( if (yAxisGen && valueIsTime) { let tickFormat = getYTickFormat(yLower, yUpper); let tickValues = getYTickValues(yLower, yUpper); - yAxisGen - .tickValues(tickValues) - .tickFormat(tickFormat); + yAxisGen.tickValues(tickValues).tickFormat(tickFormat); } let yAxis = chartElements.dataArea @@ -642,16 +641,14 @@ function renderPoints( let strValue = d3.select(this).attr("value"); if (valueType === "Time") { let dayStart = window.moment("00:00", "HH:mm", true); - let tickTime = dayStart.add(parseFloat(strValue), "seconds"); - let dateValue = tickTime.format("HH:mm"); - tooltipLabelValue.text( - "value:" + dateValue - ); - } - else { - tooltipLabelValue.text( - "value:" + strValue + let tickTime = dayStart.add( + parseFloat(strValue), + "seconds" ); + let dateValue = tickTime.format("HH:mm"); + tooltipLabelValue.text("value:" + dateValue); + } else { + tooltipLabelValue.text("value:" + strValue); } const [x, y] = d3.pointer(event); From f239b14ed47a518b1a64add66eca547267e4ad72 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Thu, 27 May 2021 11:25:24 +0800 Subject: [PATCH 020/402] Refactoring float parsing --- src/collecting.ts | 155 +++++++++++++--------------------------------- src/helper.ts | 41 ++++++++++++ 2 files changed, 85 insertions(+), 111 deletions(-) diff --git a/src/collecting.ts b/src/collecting.ts index 94594a6..8d50f90 100644 --- a/src/collecting.ts +++ b/src/collecting.ts @@ -11,21 +11,6 @@ import { } from "./data"; import * as helper from "./helper"; -let timeFormat = [ - "HH:mm", - "HH:m", - "H:mm", - "H:m", - "hh:mm A", - "hh:mm a", - "hh:m A", - "hh:m a", - "h:mm A", - "h:mm a", - "h:m A", - "h:m a", -]; - function addToDataMap( dataMap: DataMap, date: string, @@ -102,27 +87,13 @@ export function collectDataFromFrontmatterKey( let frontMatter = fileCache.frontmatter; if (frontMatter) { if (helper.deepValue(frontMatter, query.getTarget())) { - let value = null; let toParse = helper.deepValue(frontMatter, query.getTarget()); - if (typeof toParse === "string") { - if (toParse.includes(":")) { - // time value - let timeValue = window.moment(toParse, timeFormat, true); - if (timeValue.isValid()) { - query.valueType = ValueType.Time; - value = timeValue.diff( - window.moment("00:00", "HH:mm", true), - "seconds" - ); - } - } else { - value = parseFloat(toParse); + let retParse = helper.parseFloatFromAny(toParse); + if (retParse.value !== null) { + if (retParse.type === ValueType.Time) { + query.valueType = ValueType.Time; } - } else { - value = parseFloat(toParse); - } - if (Number.isNumber(value)) { - addToDataMap(dataMap, xValueMap.get(-1), query, value); + addToDataMap(dataMap, xValueMap.get(-1), query, retParse.value); } } else if ( query.getParentTarget() && @@ -153,28 +124,18 @@ export function collectDataFromFrontmatterKey( query.getAccessor() >= 0 ) { // TODO: it's not efficent to retrieve one value at a time, enhance this - let value = null; let splittedPart = splitted[query.getAccessor()].trim(); - if (toParse.includes(":")) { - // time value - let timeValue = window.moment( - splittedPart, - timeFormat, - true - ); - if (timeValue.isValid()) { + let retParse = helper.parseFloatFromAny(splittedPart); + if (retParse.value !== null) { + if (retParse.type === ValueType.Time) { query.valueType = ValueType.Time; - value = timeValue.diff( - window.moment("00:00", "HH:mm", true), - "seconds" - ); } - } else { - value = parseFloat(splittedPart); - } - - if (Number.isNumber(value)) { - addToDataMap(dataMap, xValueMap.get(-1), query, value); + addToDataMap( + dataMap, + xValueMap.get(-1), + query, + retParse.value + ); } } } @@ -243,25 +204,18 @@ export function collectDataFromInlineTag( if (splitted.length === 1) { // console.log("single-value"); let toParse = splitted[0].trim(); - if (toParse.includes(":")) { - let timeValue = window.moment(toParse, timeFormat, true); - if (timeValue.isValid()) { - query.valueType = ValueType.Time; - tagMeasure = timeValue.diff( - window.moment("00:00", "HH:mm", true), - "seconds" - ); + let retParse = helper.parseFloatFromAny(toParse); + if (retParse.value) { + if (retParse.type === ValueType.Time) { + tagMeasure = retParse.value; tagExist = true; - } - } else { - let value = parseFloat(toParse); - // console.log(value); - if (!Number.isNaN(value)) { + query.valueType = ValueType.Time; + } else { if ( !renderInfo.ignoreZeroValue[query.getId()] || - value !== 0 + retParse.value !== 0 ) { - tagMeasure += value; + tagMeasure += retParse.value; tagExist = true; } } @@ -270,23 +224,15 @@ export function collectDataFromInlineTag( splitted.length > query.getAccessor() && query.getAccessor() >= 0 ) { - // TODO: it's not efficent to retrieve one value at a time, enhance this - // console.log("multiple-values"); let toParse = splitted[query.getAccessor()].trim(); - if (toParse.includes(":")) { - let timeValue = window.moment(toParse, timeFormat, true); - if (timeValue.isValid()) { - query.valueType = ValueType.Time; - tagMeasure = timeValue.diff( - window.moment("00:00", "HH:mm", true), - "seconds" - ); + let retParse = helper.parseFloatFromAny(toParse); + if (retParse.value) { + if (retParse.type === ValueType.Time) { + tagMeasure = retParse.value; tagExist = true; - } - } else { - let value = parseFloat(toParse); - if (Number.isNumber(value)) { - tagMeasure += value; + query.valueType = ValueType.Time; + } else { + tagMeasure += retParse.value; tagExist = true; } } @@ -389,25 +335,18 @@ export function collectDataFromDvField( if (splitted.length === 1) { // console.log("single-value"); let toParse = splitted[0]; - if (toParse.includes(":")) { - let timeValue = window.moment(toParse, timeFormat, true); - if (timeValue.isValid()) { - query.valueType = ValueType.Time; - tagMeasure = timeValue.diff( - window.moment("00:00", "HH:mm", true), - "seconds" - ); + let retParse = helper.parseFloatFromAny(toParse); + if (retParse.value) { + if (retParse.type === ValueType.Time) { + tagMeasure = retParse.value; tagExist = true; - } - } else { - let value = parseFloat(toParse); - // console.log(value); - if (!Number.isNaN(value)) { + query.valueType = ValueType.Time; + } else { if ( !renderInfo.ignoreZeroValue[query.getId()] || - value !== 0 + retParse.value !== 0 ) { - tagMeasure += value; + tagMeasure += retParse.value; tagExist = true; } } @@ -419,20 +358,14 @@ export function collectDataFromDvField( // TODO: it's not efficent to retrieve one value at a time, enhance this // console.log("multiple-values"); let toParse = splitted[query.getAccessor()].trim(); - if (toParse.includes(":")) { - let timeValue = window.moment(toParse, timeFormat, true); - if (timeValue.isValid()) { - query.valueType = ValueType.Time; - tagMeasure = timeValue.diff( - window.moment("00:00", "HH:mm", true), - "seconds" - ); + let retParse = helper.parseFloatFromAny(toParse); + if (retParse.value) { + if (retParse.type === ValueType.Time) { + tagMeasure = retParse.value; tagExist = true; - } - } else { - let value = parseFloat(toParse); - if (Number.isNumber(value)) { - tagMeasure += value; + query.valueType = ValueType.Time; + } else { + tagMeasure += retParse.value; tagExist = true; } } diff --git a/src/helper.ts b/src/helper.ts index bd218a6..004935d 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -1,7 +1,23 @@ import { RenderInfo, Size } from "./data"; import { TFile, TFolder, normalizePath } from "obsidian"; +import { ValueType } from "./data"; import * as d3 from "d3"; +const timeFormat = [ + "HH:mm", + "HH:m", + "H:mm", + "H:m", + "hh:mm A", + "hh:mm a", + "hh:m A", + "hh:m a", + "h:mm A", + "h:mm a", + "h:m A", + "h:m a", +]; + // http://jsfiddle.net/alnitak/hEsys/ export function deepValue(obj: any, str: string) { str = str.replace(/^\./, ""); @@ -53,6 +69,31 @@ export function getDateFromFilename(file: TFile, renderInfo: RenderInfo) { return fileDate; } +// Parsing +export function parseFloatFromAny(toParse: any) { + let value = null; + let valueType = ValueType.Number; + if (typeof toParse === "string") { + if (toParse.includes(":")) { + // time value + let timeValue = window.moment(toParse, timeFormat, true); + if (timeValue.isValid()) { + value = timeValue.diff( + window.moment("00:00", "HH:mm", true), + "seconds" + ); + valueType = ValueType.Time; + } + } else { + value = parseFloat(toParse); + } + } else if (typeof toParse === "number") { + value = toParse; + } + + return { type: valueType, value: value }; +} + // Chart helpers export function measureTextSize( text: string, From 9bda0fe5cc4eede5a8f075622110ae4a3826234f Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Thu, 27 May 2021 12:38:20 +0800 Subject: [PATCH 021/402] Allow using valueShift on time values --- examples/TestTimeValues.md | 1 + src/helper.ts | 10 +++++++++- src/parsing.ts | 9 +++++---- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/examples/TestTimeValues.md b/examples/TestTimeValues.md index 60f6f2e..d0c125f 100644 --- a/examples/TestTimeValues.md +++ b/examples/TestTimeValues.md @@ -17,6 +17,7 @@ searchType: frontmatter searchTarget: sleep[0], sleep[1] endDate: 2021-01-15 folder: diary +valueShift: -24:00, 00:00 line: title: "Sleep" yAxisLabel: "Time" diff --git a/src/helper.ts b/src/helper.ts index 004935d..515df7b 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -74,14 +74,22 @@ export function parseFloatFromAny(toParse: any) { let value = null; let valueType = ValueType.Number; if (typeof toParse === "string") { + // time value if (toParse.includes(":")) { - // time value + let negativeValue = false; + if (toParse.startsWith("-")) { + negativeValue = true; + toParse = toParse.substring(1); + } let timeValue = window.moment(toParse, timeFormat, true); if (timeValue.isValid()) { value = timeValue.diff( window.moment("00:00", "HH:mm", true), "seconds" ); + if (negativeValue) { + value = -1 * value; + } valueType = ValueType.Time; } } else { diff --git a/src/parsing.ts b/src/parsing.ts index bd13af3..15dd2c6 100644 --- a/src/parsing.ts +++ b/src/parsing.ts @@ -14,6 +14,7 @@ import { import { TFolder, normalizePath } from "obsidian"; import { parseYaml } from "obsidian"; import { getDailyNoteSettings } from "obsidian-daily-notes-interface"; +import * as helper from "./helper"; function strToBool(str: string): boolean | null { str = str.trim().toLowerCase(); @@ -274,7 +275,7 @@ function getNumberArrayFromInput( let curr = splitted[ind].trim(); let prev = null; if (ind > 0) { - prev = parseFloat(splitted[ind - 1].trim()); + prev = helper.parseFloatFromAny(splitted[ind - 1].trim()).value; } if (curr === "") { if (prev !== null && Number.isNumber(prev)) { @@ -283,7 +284,7 @@ function getNumberArrayFromInput( array[ind] = defaultValue; } } else { - let currNum = parseFloat(curr); + let currNum = helper.parseFloatFromAny(curr).value; if (Number.isNumber(currNum)) { array[ind] = currNum; numValidValue++; @@ -294,7 +295,7 @@ function getNumberArrayFromInput( } } else { // Exceeds the length of input, use prev value - let last = parseFloat(splitted[input.length - 1].trim()); + let last = helper.parseFloatFromAny(splitted[input.length - 1].trim()).value; if (numValidValue > 0 && Number.isNumber(last)) { array[ind] = last; } else { @@ -306,7 +307,7 @@ function getNumberArrayFromInput( if (input === "") { // all defaultValue } else { - let inputNum = parseFloat(input); + let inputNum = helper.parseFloatFromAny(input).value; if (Number.isNumber(inputNum)) { array[0] = inputNum; numValidValue++; From f258bba2e12374884f0cbbd91f9812d6aa7fcb17 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Thu, 27 May 2021 12:54:38 +0800 Subject: [PATCH 022/402] Add a parameter 'reverseYAxis' --- examples/TestTimeValues.md | 1 + src/data.ts | 2 ++ src/parsing.ts | 25 +++++++++++++++++++++++-- src/rendering.ts | 9 ++++++++- 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/examples/TestTimeValues.md b/examples/TestTimeValues.md index d0c125f..1728bd3 100644 --- a/examples/TestTimeValues.md +++ b/examples/TestTimeValues.md @@ -8,6 +8,7 @@ folder: diary line: title: "Working Hours" yAxisLabel: "Time" + reverseYAxis: true lineColor: yellow, red showPoint: true ``` diff --git a/src/data.ts b/src/data.ts index c6b22b1..969a9a8 100644 --- a/src/data.ts +++ b/src/data.ts @@ -549,6 +549,7 @@ export class CommonChartInfo { yAxisUnit: string[]; yMin: number[]; yMax: number[]; + reverseYAxis: boolean[]; allowInspectData: boolean; showLegend: boolean; legendPosition: string; @@ -567,6 +568,7 @@ export class CommonChartInfo { this.yAxisUnit = []; // "", 2 elements this.yMin = []; // null, 2 elements this.yMax = []; // null, 2 elements + this.reverseYAxis = []; // false, 2 elements this.allowInspectData = true; this.showLegend = false; this.legendPosition = ""; // top, bottom, left, right diff --git a/src/parsing.ts b/src/parsing.ts index 15dd2c6..597f1c5 100644 --- a/src/parsing.ts +++ b/src/parsing.ts @@ -275,7 +275,9 @@ function getNumberArrayFromInput( let curr = splitted[ind].trim(); let prev = null; if (ind > 0) { - prev = helper.parseFloatFromAny(splitted[ind - 1].trim()).value; + prev = helper.parseFloatFromAny( + splitted[ind - 1].trim() + ).value; } if (curr === "") { if (prev !== null && Number.isNumber(prev)) { @@ -295,7 +297,9 @@ function getNumberArrayFromInput( } } else { // Exceeds the length of input, use prev value - let last = helper.parseFloatFromAny(splitted[input.length - 1].trim()).value; + let last = helper.parseFloatFromAny( + splitted[input.length - 1].trim() + ).value; if (numValidValue > 0 && Number.isNumber(last)) { array[ind] = last; } else { @@ -665,6 +669,23 @@ function parseCommonChartInfo(yaml: any, renderInfo: CommonChartInfo) { } renderInfo.yMax = retYMax; // console.log(renderInfo.yMax); + + // reverseYAxis + let retReverseYAxis = getBoolArrayFromInput( + "reverseYAxis", + yaml?.reverseYAxis, + 2, + false, + true + ); + if (typeof retReverseYAxis === "string") { + return retReverseYAxis; // errorMessage + } + if (retReverseYAxis.length > 2) { + return "reverseYAxis accepts not more than two values for left and right y-axes"; + } + renderInfo.reverseYAxis = retReverseYAxis; + // console.log(renderInfo.reverseYAxis); } function getAvailableKeysOfClass(obj: object): string[] { diff --git a/src/rendering.ts b/src/rendering.ts index cfd1216..876d1e2 100644 --- a/src/rendering.ts +++ b/src/rendering.ts @@ -346,7 +346,14 @@ function renderYAxis( yLower = 0.0; } } - yScale.domain([yLower, yUpper]).range([renderInfo.dataAreaSize.height, 0]); + let domain = [yLower, yUpper]; + if ( + (yAxisLocation === "left" && chartInfo.reverseYAxis[0]) || + (yAxisLocation === "right" && chartInfo.reverseYAxis[1]) + ) { + domain = [yUpper, yLower]; + } + yScale.domain(domain).range([renderInfo.dataAreaSize.height, 0]); if (yAxisLocation === "left") { chartElements["leftYScale"] = yScale; From 7e66823793bcb3b197e5e90b777d6e8f2582031b Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Thu, 27 May 2021 13:06:25 +0800 Subject: [PATCH 023/402] Fix yAxisLabel width for the time value --- examples/TestTimeValues.md | 4 ++-- src/rendering.ts | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/TestTimeValues.md b/examples/TestTimeValues.md index 1728bd3..181cf12 100644 --- a/examples/TestTimeValues.md +++ b/examples/TestTimeValues.md @@ -7,7 +7,7 @@ endDate: 2021-01-15 folder: diary line: title: "Working Hours" - yAxisLabel: "Time" + yAxisLabel: "Time (24h)" reverseYAxis: true lineColor: yellow, red showPoint: true @@ -21,7 +21,7 @@ folder: diary valueShift: -24:00, 00:00 line: title: "Sleep" - yAxisLabel: "Time" + yAxisLabel: "Time (24h)" lineColor: yellow, red showPoint: true ``` \ No newline at end of file diff --git a/src/rendering.ts b/src/rendering.ts index 876d1e2..2d184f2 100644 --- a/src/rendering.ts +++ b/src/rendering.ts @@ -89,7 +89,7 @@ function getYTickValues(yLower: number, yUpper: number) { return tickValues; } -function getYTickFormat(yLower: number, yUpper: number) { +function getYTickFormat(yLower: number, yUpper: number, skip: boolean = true) { // currently used for time value tick only // return a function convert value to time string function tickFormat(value: number): string { @@ -97,7 +97,7 @@ function getYTickFormat(yLower: number, yUpper: number) { let dayStart = window.moment("00:00", "HH:mm", true); let tickTime = dayStart.add(value, "seconds"); let format = tickTime.format("HH:mm"); - if (absExtent > 12 * 60 * 60) { + if (skip && absExtent > 12 * 60 * 60) { let devHour = (value - yLower) / 3600; let interleave = devHour % 2; if (value <= yLower) { @@ -437,6 +437,9 @@ function renderYAxis( // Get max tick label width let yTickFormat = d3.tickFormat(yLower, yUpper, 10); + if (valueIsTime) { + yTickFormat = getYTickFormat(yLower, yUpper, false); + } let yLowerLabelSize = helper.measureTextSize( yTickFormat(yLower), "tracker-axis-label" From 78df635a53a0a2c93d72cc4491e7c544e9a4e512 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Thu, 27 May 2021 14:46:10 +0800 Subject: [PATCH 024/402] Add output type bullet --- src/bullet.ts | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ src/data.ts | 11 +++++++++++ src/parsing.ts | 46 +++++++++++++++++++++++++++++++++++++--------- src/rendering.ts | 4 ++++ 4 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 src/bullet.ts diff --git a/src/bullet.ts b/src/bullet.ts new file mode 100644 index 0000000..6c3b073 --- /dev/null +++ b/src/bullet.ts @@ -0,0 +1,48 @@ +import { + Datasets, + DataPoint, + RenderInfo, + Dataset, + Size, + Transform, + ChartElements, + OutputType, + ValueType, +} from "./data"; + +function renderTitle() { + // title +} + +function renderBackPanel() { + // quantitative range, poor/average/good/... + // +} + +function renderBarAndMark() { + // bar for actual value + // mark line for target value +} + +function renderAxis() { + // ticks + // tick labels + // unit +} + +// Bullet graph https://en.wikipedia.org/wiki/Bullet_graph +export function renderBullet(canvas: HTMLElement, renderInfo: RenderInfo) { + // console.log("renderBullet"); + // console.log(renderInfo); + // if (renderInfo.bullet === null) return; + + // let chartElements = createAreas(canvas, renderInfo); + + renderTitle(); + + renderBackPanel(); + + renderBarAndMark(); + + renderAxis(); +} diff --git a/src/data.ts b/src/data.ts index 969a9a8..2ff728b 100644 --- a/src/data.ts +++ b/src/data.ts @@ -16,6 +16,7 @@ export enum OutputType { Summary, Table, Month, + Bullet, } export enum ValueType { @@ -492,6 +493,7 @@ export class RenderInfo { bar: BarInfo | null; summary: SummaryInfo | null; month: MonthInfo | null; + bullet: BulletInfo | null; public datasets: Datasets | null; @@ -525,6 +527,7 @@ export class RenderInfo { this.summary = null; this.bar = null; this.month = null; + this.bullet = null; this.datasets = null; } @@ -634,6 +637,14 @@ export class MonthInfo { } } +export class BulletInfo { + orientation: string; + + constructor() { + this.orientation = "horizontal"; // or vertical + } +} + export class Size { width: number; height: number; diff --git a/src/parsing.ts b/src/parsing.ts index 597f1c5..13d04ac 100644 --- a/src/parsing.ts +++ b/src/parsing.ts @@ -10,6 +10,7 @@ import { OutputType, LineInfo, MonthInfo, + BulletInfo, } from "./data"; import { TFolder, normalizePath } from "obsidian"; import { parseYaml } from "obsidian"; @@ -1173,20 +1174,26 @@ export function getRenderInfoFromYaml( if (typeof yaml.month !== "undefined") { hasMonth = true; } + let hasBullet = false; + if (typeof yaml.bullet !== "undefined") { + hasBullet = true; + } let sumOutput = Number(hasLine) + Number(hasBar) + Number(hasSummary) + - Number(hasMonth); + Number(hasMonth) + + Number(hasBullet); if (sumOutput === 0) { - return "No output parameter provided, please place line, bar, month, or summary."; + return "No output parameter provided, please place line, bar, month, bullet, or summary."; } else if (sumOutput === 1) { if (hasLine) renderInfo.output = OutputType.Line; if (hasBar) renderInfo.output = OutputType.Bar; if (hasSummary) renderInfo.output = OutputType.Summary; if (hasMonth) renderInfo.output = OutputType.Month; + if (hasBullet) renderInfo.output = OutputType.Bullet; } else if (sumOutput >= 2) { - return "Too many output parameters, pick line, bar, month, or summary."; + return "Too many output parameters, pick line, bar, month, bullet, or summary."; } // line related parameters @@ -1439,17 +1446,17 @@ export function getRenderInfoFromYaml( } } } // summary related parameters - // month related parameters + // Month related parameters if (renderInfo.output === OutputType.Month) { renderInfo.month = new MonthInfo(); if (yaml.month !== null) { - let keysOfSummaryInfo = getAvailableKeysOfClass(renderInfo.month); + let keysOfMonthInfo = getAvailableKeysOfClass(renderInfo.month); let keysFoundInYAML = getAvailableKeysOfClass(yaml.month); // console.log(keysOfSummaryInfo); // console.log(keysFoundInYAML); for (let key of keysFoundInYAML) { - if (!keysOfSummaryInfo.includes(key)) { + if (!keysOfMonthInfo.includes(key)) { errorMessage = "'" + key + "' is not an available key"; return errorMessage; } @@ -1458,12 +1465,33 @@ export function getRenderInfoFromYaml( if (yaml.month !== null) { if (typeof yaml.month.startWeekOn === "string") { - if (yaml.month.startWeekOn === "month") { - renderInfo.month.startWeekOn = yaml.month.startWeekOn; + renderInfo.month.startWeekOn = yaml.month.startWeekOn; + } + } + } // Month related parameters + // Bullet related parameters + if (renderInfo.output === OutputType.Bullet) { + renderInfo.bullet = new BulletInfo(); + + if (yaml.bullet !== null) { + let keysOfBulletInfo = getAvailableKeysOfClass(renderInfo.bullet); + let keysFoundInYAML = getAvailableKeysOfClass(yaml.bullet); + // console.log(keysOfSummaryInfo); + // console.log(keysFoundInYAML); + for (let key of keysFoundInYAML) { + if (!keysOfBulletInfo.includes(key)) { + errorMessage = "'" + key + "' is not an available key"; + return errorMessage; } } } - } // month related parameters + + if (yaml.bullet !== null) { + if (typeof yaml.bullet.orientation === "string") { + renderInfo.bullet.orientation = yaml.bullet.orientation; + } + } + } // Bullet related parameters return renderInfo; } diff --git a/src/rendering.ts b/src/rendering.ts index 2d184f2..da412b6 100644 --- a/src/rendering.ts +++ b/src/rendering.ts @@ -10,9 +10,11 @@ import { ChartElements, OutputType, ValueType, + BulletInfo, } from "./data"; import * as summary from "./summary"; import * as month from "./month"; +import * as bullet from "./bullet"; import * as helper from "./helper"; function getXTickInterval(datasets: Datasets) { @@ -147,6 +149,8 @@ export function render(canvas: HTMLElement, renderInfo: RenderInfo) { return summary.renderSummary(canvas, renderInfo); case OutputType.Month: return month.renderMonth(canvas, renderInfo); + case OutputType.Bullet: + return bullet.renderBullet(canvas, renderInfo); default: return "Unknown output type"; } From 25261a6fd3c4fc769016e0ff37b9875289a1ec63 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Fri, 28 May 2021 23:24:39 +0800 Subject: [PATCH 025/402] Manage drawing bullet chart --- examples/TestBullet.md | 39 +++++ src/bullet.ts | 390 +++++++++++++++++++++++++++++++++++++++-- src/data.ts | 18 ++ src/helper.ts | 24 ++- src/parsing.ts | 75 ++++++++ src/rendering.ts | 51 ++---- styles.css | 6 + 7 files changed, 548 insertions(+), 55 deletions(-) create mode 100644 examples/TestBullet.md diff --git a/examples/TestBullet.md b/examples/TestBullet.md new file mode 100644 index 0000000..4d9c53f --- /dev/null +++ b/examples/TestBullet.md @@ -0,0 +1,39 @@ +# Test Bullet +Horizontal bullet chart +``` tracker +searchType: tag +searchTarget: meditation +folder: diary +endDate: 2021-01-31 +fixedScale: 1 +bullet: + title: "Clean Up" + dataset: 0 + orientation: horizontal + range: 10, 20, 30 + rangeColor: '#ccc, #ddd, #eee' + value: "{{lastBreaks}}" + valueColor: steelblue + showMark: false + markerValue: 20 + markerColor: black +``` + +Vertical bullet chart +``` tracker +searchType: tag +searchTarget: meditation +folder: diary +endDate: 2021-01-31 +bullet: + title: "Clean Up" + dataset: 0 + orientation: vertical + range: 10, 20, 30 + rangeColor: '#ccc, #ddd, #eee' + value: "{{lastBreaks}}" + valueColor: steelblue + showMark: false + markerValue: 20 + markerColor: black +``` \ No newline at end of file diff --git a/src/bullet.ts b/src/bullet.ts index 6c3b073..7408c3b 100644 --- a/src/bullet.ts +++ b/src/bullet.ts @@ -9,40 +9,394 @@ import { OutputType, ValueType, } from "./data"; +import * as helper from "./helper"; +import * as d3 from "d3"; -function renderTitle() { - // title +function createAreas( + canvas: HTMLElement, + renderInfo: RenderInfo +): ChartElements { + let chartElements: ChartElements = {}; + // whole area for plotting, includes margins + + let bulletInfo = renderInfo.bullet; + if (!bulletInfo) return; + + let svg = d3 + .select(canvas) + .append("svg") + .attr("id", "svg") + .attr( + "width", + renderInfo.dataAreaSize.width + + renderInfo.margin.left + + renderInfo.margin.right + ) + .attr( + "height", + renderInfo.dataAreaSize.height + + renderInfo.margin.top + + renderInfo.margin.bottom + ); + chartElements["svg"] = svg; + + // graphArea, includes chartArea, title, legend + let graphArea = svg + .append("g") + .attr("id", "graphArea") + .attr( + "transform", + "translate(" + + renderInfo.margin.left + + "," + + renderInfo.margin.top + + ")" + ) + .attr("width", renderInfo.dataAreaSize.width + renderInfo.margin.right) + .attr( + "height", + renderInfo.dataAreaSize.height + renderInfo.margin.bottom + ); + chartElements["graphArea"] = graphArea; + + // dataArea, under graphArea, includes points, lines, xAxis, yAxis + let dataArea = graphArea + .append("g") + .attr("id", "dataArea") + .attr("width", renderInfo.dataAreaSize.width) + .attr("height", renderInfo.dataAreaSize.height); + chartElements["dataArea"] = dataArea; + + return chartElements; +} + +function renderTitle(chartElements: ChartElements, renderInfo: RenderInfo) { + // console.log("renderTitle"); + // under graphArea + + let bulletInfo = renderInfo.bullet; + if (!bulletInfo) return; + + if (!bulletInfo.title) return; + + let titleSize = helper.measureTextSize( + bulletInfo.title, + "tracker-title-small" + ); + let spacing = 6; // spacing between title and dataArea + + if (bulletInfo.orientation === "horizontal") { + let title = chartElements.graphArea + .append("text") + .text(bulletInfo.title) // pivot at center + .attr("id", "title") + .attr("x", titleSize.width / 2.0) + .attr("y", renderInfo.dataAreaSize.height / 2.0) + .attr("height", titleSize.height) // for later use + .attr("class", "tracker-title-small"); + chartElements["title"] = title; + + // Expand parent areas + helper.expandArea(chartElements.svg, titleSize.width + spacing, 0); + helper.expandArea( + chartElements.graphArea, + titleSize.width + spacing, + 0 + ); + + // Move sibling areas + helper.moveArea(chartElements.dataArea, titleSize.width + spacing, 0); + } else if (bulletInfo.orientation === "vertical") { + let title = chartElements.graphArea + .append("text") + .text(bulletInfo.title) // pivot at center + .attr("id", "title") + .attr( + "transform", + "translate(" + + renderInfo.dataAreaSize.width / 2.0 + + "," + + titleSize.height / 2.0 + + ")" + ) + .attr("height", titleSize.height) // for later use + .attr("class", "tracker-title-small"); + chartElements["title"] = title; + + // Expand parent areas + let xExpand = helper.expandArea( + chartElements.svg, + 0, + titleSize.height + spacing + ); + helper.expandArea( + chartElements.graphArea, + 0, + titleSize.height + spacing + ); + + // Move sibling areas + helper.moveArea(chartElements.dataArea, 0, titleSize.height + spacing); + } +} + +// Render ticks, tick labels +function renderAxis( + chartElements: ChartElements, + renderInfo: RenderInfo, + dataset: Dataset +) { + // console.log("renderAxis"); + // console.log(chartElements); + // console.log(dataset); + + let bulletInfo = renderInfo.bullet; + if (!bulletInfo) return; + + let range = bulletInfo.range; + let domain = [0, range[range.length - 1]]; + let tickLength = 6; + + if (bulletInfo.orientation === "horizontal") { + let scale = d3.scaleLinear(); + scale.domain(domain).range([0, renderInfo.dataAreaSize.width]); + chartElements["scale"] = scale; + + let axisGen = d3.axisBottom(scale); + let axis = chartElements.dataArea + .append("g") + .attr("id", "axis") + .attr( + "transform", + "translate(0," + renderInfo.dataAreaSize.height + ")" + ) + .call(axisGen) + .attr("class", "tracker-axis"); + + let axisLine = axis.selectAll("path"); + + let axisTicks = axis.selectAll("line"); + + let tickLabelSize = helper.measureTextSize("123456789"); + let tickLabelHeight = tickLabelSize.height; + + axis.attr("width", renderInfo.dataAreaSize.width); + axis.attr("height", tickLength + tickLabelHeight); + + // Expand areas + helper.expandArea(chartElements.svg, 0, tickLength + tickLabelHeight); + helper.expandArea( + chartElements.graphArea, + 0, + tickLength + tickLabelHeight + ); + } else if (bulletInfo.orientation === "vertical") { + let scale = d3.scaleLinear(); + scale.domain(domain).range([0, renderInfo.dataAreaSize.height]); + chartElements["scale"] = scale; + + let axisGen = d3.axisLeft(scale); + let axis = chartElements.dataArea + .append("g") + .attr("id", "axis") + .attr( + "transform", + "translate(" + renderInfo.dataAreaSize.width + " ,0)" + ) + .call(axisGen) + .attr("class", "tracker-axis"); + + let axisLine = axis.selectAll("path"); + + let axisTicks = axis.selectAll("line"); + + axis.attr("width", tickLength); + axis.attr("height", renderInfo.dataAreaSize.width); + + // Expand areas + helper.expandArea(chartElements.svg, tickLength, 0); + helper.expandArea(chartElements.graphArea, tickLength, 0); + } } -function renderBackPanel() { - // quantitative range, poor/average/good/... - // +// Render quantitative range, poor/average/good/... +function renderBackPanel( + chartElements: ChartElements, + renderInfo: RenderInfo, + dataset: Dataset +) { + // console.log("renderBackPanel"); + // console.log(dataset); + + let bulletInfo = renderInfo.bullet; + if (!bulletInfo) return; + + let scale = chartElements.scale; + + // Prepare data + let range = bulletInfo.range; + let rangeColor = bulletInfo.rangeColor; + let data = []; + let lastBound = 0; + for (let ind = 0; ind < range.length; ind++) { + data.push({ + start: lastBound, + end: range[ind], + color: rangeColor[ind], + }); + lastBound = range[ind]; + } + + if (bulletInfo.orientation === "horizontal") { + let panel = chartElements.dataArea + .selectAll("backPanel") + .data(data) + .enter() + .append("rect") + .attr("x", function (d: any, i: number) { + return scale(d.start); + }) + .attr("y", function (d: any) { + return 0; + }) + .attr("width", function (d: any, i: number) { + return scale(d.end - d.start); + }) + .attr("height", renderInfo.dataAreaSize.height) + .style("fill", function (d: any) { + return d.color; + }); + } else if (bulletInfo.orientation === "vertical") { + let panel = chartElements.dataArea + .selectAll("backPanel") + .data(data) + .enter() + .append("rect") + .attr("x", function (d: any, i: number) { + return 0; + }) + .attr("y", function (d: any) { + return scale(d.start); + }) + .attr("width", renderInfo.dataAreaSize.width) + .attr("height", function (d: any) { + return scale(d.end - d.start); + }) + .style("fill", function (d: any) { + return d.color; + }); + } } -function renderBarAndMark() { - // bar for actual value - // mark line for target value +// Render bar for actual value +function renderBar( + chartElements: ChartElements, + renderInfo: RenderInfo, + dataset: Dataset +) { + // console.log("renderBar"); + // console.log(dataset); + + let bulletInfo = renderInfo.bullet; + if (!bulletInfo) return; + + let strActualValue = bulletInfo.value; + let actualValue = 25; // TODO + let valueColor = bulletInfo.valueColor; + + let scale = chartElements.scale; + + if (bulletInfo.orientation === "horizontal") { + let barWidth = renderInfo.dataAreaSize.height / 3; + let bar = chartElements.dataArea + .append("rect") + .attr("x", scale(0)) + .attr("y", barWidth) + .attr("width", scale(actualValue)) + .attr("height", barWidth) + .style("fill", valueColor); + } else if (bulletInfo.orientation === "vertical") { + let barWidth = renderInfo.dataAreaSize.width / 3; + let bar = chartElements.dataArea + .append("rect") + .attr("x", barWidth) + .attr("y", renderInfo.dataAreaSize.height - scale(actualValue)) + .attr("width", barWidth) + .attr("height", scale(actualValue)) + .style("fill", valueColor); + } } -function renderAxis() { - // ticks - // tick labels - // unit +// Render mark line for target value +function renderMark( + chartElements: ChartElements, + renderInfo: RenderInfo, + dataset: Dataset +) { + // console.log("renderMark"); + // console.log(dataset); + + let bulletInfo = renderInfo.bullet; + if (!bulletInfo) return; + + let markerValue = bulletInfo.markerValue; + let markerColor = bulletInfo.markerColor; + + let scale = chartElements.scale; + + if (bulletInfo.orientation === "horizontal") { + let markerLength = (renderInfo.dataAreaSize.height * 2) / 3; + let mark = chartElements.dataArea + .append("rect") + .attr("x", scale(markerValue) - 1.5) + .attr("y", markerLength / 4) + .attr("width", 3) + .attr("height", markerLength) + .style("fill", markerColor); + } else if (bulletInfo.orientation === "vertical") { + let markerLength = (renderInfo.dataAreaSize.width * 2) / 3; + let mark = chartElements.dataArea + .append("rect") + .attr("x", markerLength / 4) + .attr( + "y", + renderInfo.dataAreaSize.height - scale(markerValue) - 1.5 + ) + .attr("width", markerLength) + .attr("height", 3) + .style("fill", markerColor); + } } // Bullet graph https://en.wikipedia.org/wiki/Bullet_graph export function renderBullet(canvas: HTMLElement, renderInfo: RenderInfo) { // console.log("renderBullet"); // console.log(renderInfo); - // if (renderInfo.bullet === null) return; + let bulletInfo = renderInfo.bullet; + if (bulletInfo === null) return; + + let datasetId = parseFloat(bulletInfo.dataset); + let dataset = renderInfo.datasets.getDatasetById(datasetId); + + // Set initial dataArea size + if (bulletInfo.orientation === "horizontal") { + renderInfo.dataAreaSize = { width: 250, height: 24 }; + } else if (bulletInfo.orientation === "vertical") { + renderInfo.dataAreaSize = { width: 24, height: 250 }; + } + + let chartElements = createAreas(canvas, renderInfo); - // let chartElements = createAreas(canvas, renderInfo); + let retRenderAxis = renderAxis(chartElements, renderInfo, dataset); + if (typeof retRenderAxis === "string") { + return retRenderAxis; + } - renderTitle(); + renderTitle(chartElements, renderInfo); - renderBackPanel(); + renderBackPanel(chartElements, renderInfo, dataset); - renderBarAndMark(); + renderBar(chartElements, renderInfo, dataset); - renderAxis(); + renderMark(chartElements, renderInfo, dataset); } diff --git a/src/data.ts b/src/data.ts index 2ff728b..bb4b826 100644 --- a/src/data.ts +++ b/src/data.ts @@ -638,10 +638,28 @@ export class MonthInfo { } export class BulletInfo { + title: string; + dataset: string; orientation: string; + range: number[]; + rangeColor: string[]; + value: string; + valueColor: string; + showMark: boolean; + markerValue: number; + markerColor: string; constructor() { + this.title = ""; + this.dataset = "0"; // dataset id or name this.orientation = "horizontal"; // or vertical + this.range = []; + this.rangeColor = []; + this.value = ""; // Can possess template varialbe + this.valueColor = ""; + this.showMark = false; + this.markerValue = 0; + this.markerColor = ""; } } diff --git a/src/helper.ts b/src/helper.ts index 515df7b..cec53dd 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -1,4 +1,4 @@ -import { RenderInfo, Size } from "./data"; +import { RenderInfo, Size, Transform } from "./data"; import { TFile, TFolder, normalizePath } from "obsidian"; import { ValueType } from "./data"; import * as d3 from "d3"; @@ -124,3 +124,25 @@ export function measureTextSize( container.remove(); return { width: size.width, height: size.height }; } + +// dom +export function expandArea(area: any, addW: number, addH: number) { + let oriWidth = parseFloat(area.attr("width")) | 0; + let oriHeight = parseFloat(area.attr("height")) | 0; + let newWidth = oriWidth + addW; + let newHeight = oriHeight + addH; + area.attr("width", newWidth); + area.attr("height", newHeight); +} + +export function moveArea(area: any, shiftX: number, shiftY: number) { + let trans = new Transform(area.attr("transform")); + area.attr( + "transform", + "translate(" + + (trans.translateX + shiftX) + + "," + + (trans.translateY + shiftY) + + ")" + ); +} diff --git a/src/parsing.ts b/src/parsing.ts index 13d04ac..9c2b561 100644 --- a/src/parsing.ts +++ b/src/parsing.ts @@ -1487,9 +1487,84 @@ export function getRenderInfoFromYaml( } if (yaml.bullet !== null) { + // title + if (typeof yaml.bullet.title === "string") { + renderInfo.bullet.title = yaml.bullet.title; + } + // console.log(renderInfo.bullet.title); + + // dataset + if (typeof yaml.bullet.dataset === "string") { + renderInfo.bullet.dataset = yaml.bullet.dataset; + } + // console.log(renderInfo.bullet.dataset); + + // orientation if (typeof yaml.bullet.orientation === "string") { renderInfo.bullet.orientation = yaml.bullet.orientation; } + // console.log(renderInfo.bullet.orientation); + + // range + let numRange = 0; + let retRange = getNumberArrayFromInput( + "range", + yaml?.bullet?.range, + 3, // TODO + null, + true + ); + if (typeof retRange === "string") { + return retRange; // errorMessage + } + renderInfo.bullet.range = retRange; + numRange = retRange.length; + // console.log(renderInfo.bullet.range); + + // range color + let retRangeColor = getStringArrayFromInput( + "rangeColor", + yaml?.bullet?.rangeColor, + numRange, + "", + validateColor, + true + ); + if (typeof retRangeColor === "string") { + return retRangeColor; // errorMessage + } + renderInfo.bullet.rangeColor = retRangeColor; + // console.log(renderInfo.bullet.rangeColor); + + // actual value, can possess template variable + if (typeof yaml.bullet.value === "string") { + renderInfo.bullet.value = yaml.bullet.value; + } + // console.log(renderInfo.bullet.value); + + // value color + if (typeof yaml.bullet.valueColor === "string") { + renderInfo.bullet.valueColor = yaml.bullet.valueColor; + } + // console.log(renderInfo.bullet.valueColor); + + // show mark + if (typeof yaml.bullet.showMark === "boolean") { + renderInfo.bullet.showMark = yaml.bullet.showMark; + } + // console.log(renderInfo.bullet.showMark); + + // mark value + if (typeof yaml.bullet.markerValue === "number") { + renderInfo.bullet.markerValue = yaml.bullet.markerValue; + } + // console.log(renderInfo.bullet.markValue); + + // mark color + if (typeof yaml.bullet.markerColor === "number") { + renderInfo.bullet.markerColor = yaml.bullet.markerColor; + } + // console.log(renderInfo.bullet.markValue); } } // Bullet related parameters diff --git a/src/rendering.ts b/src/rendering.ts index da412b6..22d2b4b 100644 --- a/src/rendering.ts +++ b/src/rendering.ts @@ -233,8 +233,8 @@ function renderXAxis(chartElements: ChartElements, renderInfo: RenderInfo) { xAxis.attr("height", tickLength + tickLabelHeight); // Expand areas - expandArea(chartElements.svg, 0, tickLength + tickLabelHeight); - expandArea(chartElements.graphArea, 0, tickLength + tickLabelHeight); + helper.expandArea(chartElements.svg, 0, tickLength + tickLabelHeight); + helper.expandArea(chartElements.graphArea, 0, tickLength + tickLabelHeight); } function renderYAxis( @@ -487,17 +487,17 @@ function renderYAxis( yAxis.attr("width", yAxisWidth); // Expand areas - expandArea(chartElements.svg, yAxisWidth, 0); - expandArea(chartElements.graphArea, yAxisWidth, 0); + helper.expandArea(chartElements.svg, yAxisWidth, 0); + helper.expandArea(chartElements.graphArea, yAxisWidth, 0); // Move areas if (yAxisLocation === "left") { // Move dataArea - moveArea(chartElements.dataArea, yAxisWidth, 0); + helper.moveArea(chartElements.dataArea, yAxisWidth, 0); // Move title if (chartElements.title) { - moveArea(chartElements.title, yAxisWidth, 0); + helper.moveArea(chartElements.title, yAxisWidth, 0); } } } @@ -887,9 +887,9 @@ function renderLegend(chartElements: ChartElements, renderInfo: RenderInfo) { legendWidth / 2.0; legendY = titleHeight; // Expand svg - expandArea(svg, 0, legendHeight + ySpacing); + helper.expandArea(svg, 0, legendHeight + ySpacing); // Move dataArea down - moveArea(dataArea, 0, legendHeight + ySpacing); + helper.moveArea(dataArea, 0, legendHeight + ySpacing); } else if (chartInfo.legendPosition === "bottom") { // bellow x-axis label legendX = @@ -902,7 +902,7 @@ function renderLegend(chartElements: ChartElements, renderInfo: RenderInfo) { xAxisHeight + ySpacing; // Expand svg - expandArea(svg, 0, legendHeight + ySpacing); + helper.expandArea(svg, 0, legendHeight + ySpacing); } else if (chartInfo.legendPosition === "left") { legendX = 0; legendY = @@ -910,9 +910,9 @@ function renderLegend(chartElements: ChartElements, renderInfo: RenderInfo) { renderInfo.dataAreaSize.height / 2.0 - legendHeight / 2.0; // Expand svg - expandArea(svg, legendWidth + xSpacing, 0); + helper.expandArea(svg, legendWidth + xSpacing, 0); // Move dataArea right - moveArea(dataArea, legendWidth + xSpacing, 0); + helper.moveArea(dataArea, legendWidth + xSpacing, 0); } else if (chartInfo.legendPosition === "right") { legendX = renderInfo.dataAreaSize.width + @@ -924,7 +924,7 @@ function renderLegend(chartElements: ChartElements, renderInfo: RenderInfo) { renderInfo.dataAreaSize.height / 2.0 - legendHeight / 2.0; // Expand svg - expandArea(svg, legendWidth + xSpacing, 0); + helper.expandArea(svg, legendWidth + xSpacing, 0); } else { return; } @@ -1270,11 +1270,11 @@ function renderTitle(chartElements: ChartElements, renderInfo: RenderInfo) { chartElements["title"] = title; // Expand parent areas - expandArea(chartElements.svg, 0, titleSize.height); - expandArea(chartElements.graphArea, 0, titleSize.height); + helper.expandArea(chartElements.svg, 0, titleSize.height); + helper.expandArea(chartElements.graphArea, 0, titleSize.height); // Move sibling areas - moveArea(chartElements.dataArea, 0, titleSize.height); + helper.moveArea(chartElements.dataArea, 0, titleSize.height); return; } @@ -1361,27 +1361,6 @@ function createAreas( return chartElements; } -function expandArea(area: any, addW: number, addH: number) { - let oriWidth = parseFloat(area.attr("width")) | 0; - let oriHeight = parseFloat(area.attr("height")) | 0; - let newWidth = oriWidth + addW; - let newHeight = oriHeight + addH; - area.attr("width", newWidth); - area.attr("height", newHeight); -} - -function moveArea(area: any, shiftX: number, shiftY: number) { - let trans = new Transform(area.attr("transform")); - area.attr( - "transform", - "translate(" + - (trans.translateX + shiftX) + - "," + - (trans.translateY + shiftY) + - ")" - ); -} - function renderLineChart(canvas: HTMLElement, renderInfo: RenderInfo) { // console.log("renderLineChart"); // console.log(renderInfo); diff --git a/styles.css b/styles.css index bc74a53..7ccbcf3 100644 --- a/styles.css +++ b/styles.css @@ -44,6 +44,12 @@ font-size: 18px; } +.tracker-title-small { + fill: var(--color-title); + text-anchor: middle; + font-size: 16px; +} + .tracker-axis { fill: none; stroke: var(--color-axis); From f525bc149b64de16dccf7ef666feee83f798b21d Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Sat, 29 May 2021 16:35:01 +0800 Subject: [PATCH 026/402] Parse bullet info and refine the bullet chart --- examples/TestBullet.md | 20 ++-- src/bullet.ts | 73 ++++++++----- src/data.ts | 2 +- src/expr.ts | 229 +++++++++++++++++++++++++++++++++++++++++ src/parsing.ts | 83 ++++++++++++--- src/summary.ts | 224 +--------------------------------------- 6 files changed, 358 insertions(+), 273 deletions(-) create mode 100644 src/expr.ts diff --git a/examples/TestBullet.md b/examples/TestBullet.md index 4d9c53f..0dd94a7 100644 --- a/examples/TestBullet.md +++ b/examples/TestBullet.md @@ -2,7 +2,7 @@ Horizontal bullet chart ``` tracker searchType: tag -searchTarget: meditation +searchTarget: clean-up folder: diary endDate: 2021-01-31 fixedScale: 1 @@ -10,30 +10,30 @@ bullet: title: "Clean Up" dataset: 0 orientation: horizontal - range: 10, 20, 30 - rangeColor: '#ccc, #ddd, #eee' + range: 10, 20, 40 + rangeColor: darkgray, silver, lightgray value: "{{lastBreaks}}" - valueColor: steelblue + valueColor: '#69b3a2' showMark: false - markerValue: 20 + markerValue: 24 markerColor: black ``` Vertical bullet chart ``` tracker searchType: tag -searchTarget: meditation +searchTarget: clean-up folder: diary endDate: 2021-01-31 bullet: title: "Clean Up" dataset: 0 orientation: vertical - range: 10, 20, 30 - rangeColor: '#ccc, #ddd, #eee' + range: 10, 20, 40 + rangeColor: darkgray, silver, lightgray value: "{{lastBreaks}}" - valueColor: steelblue + valueColor: '#69b3a2' showMark: false - markerValue: 20 + markerValue: 24 markerColor: black ``` \ No newline at end of file diff --git a/src/bullet.ts b/src/bullet.ts index 7408c3b..7b35d70 100644 --- a/src/bullet.ts +++ b/src/bullet.ts @@ -11,6 +11,7 @@ import { } from "./data"; import * as helper from "./helper"; import * as d3 from "d3"; +import * as expr from "./expr"; function createAreas( canvas: HTMLElement, @@ -107,28 +108,40 @@ function renderTitle(chartElements: ChartElements, renderInfo: RenderInfo) { // Move sibling areas helper.moveArea(chartElements.dataArea, titleSize.width + spacing, 0); } else if (bulletInfo.orientation === "vertical") { + // if label width > dataArea width + let xMiddle = renderInfo.dataAreaSize.width / 2.0; + if (titleSize.width > renderInfo.dataAreaSize.width) { + helper.expandArea( + chartElements.svg, + titleSize.width - renderInfo.dataAreaSize.width, + 0 + ); + helper.expandArea( + chartElements.graphArea, + titleSize.width - renderInfo.dataAreaSize.width, + 0 + ); + + helper.moveArea( + chartElements.dataArea, + titleSize.width / 2.0 - renderInfo.dataAreaSize.width / 2.0, + 0 + ); + xMiddle = titleSize.width / 2.0; + } + let title = chartElements.graphArea .append("text") .text(bulletInfo.title) // pivot at center .attr("id", "title") - .attr( - "transform", - "translate(" + - renderInfo.dataAreaSize.width / 2.0 + - "," + - titleSize.height / 2.0 + - ")" - ) + .attr("x", xMiddle) + .attr("y", titleSize.height / 2.0) .attr("height", titleSize.height) // for later use .attr("class", "tracker-title-small"); chartElements["title"] = title; // Expand parent areas - let xExpand = helper.expandArea( - chartElements.svg, - 0, - titleSize.height + spacing - ); + helper.expandArea(chartElements.svg, 0, titleSize.height + spacing); helper.expandArea( chartElements.graphArea, 0, @@ -192,17 +205,15 @@ function renderAxis( ); } else if (bulletInfo.orientation === "vertical") { let scale = d3.scaleLinear(); - scale.domain(domain).range([0, renderInfo.dataAreaSize.height]); + scale.domain(domain).range([renderInfo.dataAreaSize.height, 0]); chartElements["scale"] = scale; let axisGen = d3.axisLeft(scale); let axis = chartElements.dataArea .append("g") .attr("id", "axis") - .attr( - "transform", - "translate(" + renderInfo.dataAreaSize.width + " ,0)" - ) + .attr("x", 0) + .attr("y", 0) .call(axisGen) .attr("class", "tracker-axis"); @@ -276,11 +287,11 @@ function renderBackPanel( return 0; }) .attr("y", function (d: any) { - return scale(d.start); + return scale(d.end); }) .attr("width", renderInfo.dataAreaSize.width) .attr("height", function (d: any) { - return scale(d.end - d.start); + return renderInfo.dataAreaSize.height - scale(d.end - d.start); }) .style("fill", function (d: any) { return d.color; @@ -296,12 +307,18 @@ function renderBar( ) { // console.log("renderBar"); // console.log(dataset); + let errorMessage = ""; let bulletInfo = renderInfo.bullet; if (!bulletInfo) return; let strActualValue = bulletInfo.value; - let actualValue = 25; // TODO + strActualValue = expr.resolveTemplate(strActualValue, renderInfo); + let actualValue = parseFloat(strActualValue); + if (Number.isNaN(actualValue)) { + errorMessage = "Invalid paramter 'actualValue'"; + return errorMessage; + } let valueColor = bulletInfo.valueColor; let scale = chartElements.scale; @@ -320,9 +337,9 @@ function renderBar( let bar = chartElements.dataArea .append("rect") .attr("x", barWidth) - .attr("y", renderInfo.dataAreaSize.height - scale(actualValue)) + .attr("y", scale(actualValue)) .attr("width", barWidth) - .attr("height", scale(actualValue)) + .attr("height", renderInfo.dataAreaSize.height - scale(actualValue)) .style("fill", valueColor); } } @@ -358,10 +375,7 @@ function renderMark( let mark = chartElements.dataArea .append("rect") .attr("x", markerLength / 4) - .attr( - "y", - renderInfo.dataAreaSize.height - scale(markerValue) - 1.5 - ) + .attr("y", scale(markerValue) - 1.5) .attr("width", markerLength) .attr("height", 3) .style("fill", markerColor); @@ -396,7 +410,10 @@ export function renderBullet(canvas: HTMLElement, renderInfo: RenderInfo) { renderBackPanel(chartElements, renderInfo, dataset); - renderBar(chartElements, renderInfo, dataset); + let retRenderBar = renderBar(chartElements, renderInfo, dataset); + if (typeof retRenderBar === "string") { + return retRenderBar; + } renderMark(chartElements, renderInfo, dataset); } diff --git a/src/data.ts b/src/data.ts index bb4b826..11a8b3d 100644 --- a/src/data.ts +++ b/src/data.ts @@ -656,7 +656,7 @@ export class BulletInfo { this.range = []; this.rangeColor = []; this.value = ""; // Can possess template varialbe - this.valueColor = ""; + this.valueColor = "#69b3a2"; this.showMark = false; this.markerValue = 0; this.markerColor = ""; diff --git a/src/expr.ts b/src/expr.ts new file mode 100644 index 0000000..668531a --- /dev/null +++ b/src/expr.ts @@ -0,0 +1,229 @@ +import { RenderInfo } from "./data"; +import * as d3 from "d3"; + +let fnSet = { + min: function (renderInfo: RenderInfo, datasetId: number) { + let dataset = renderInfo.datasets.getDatasetById(datasetId); + return d3.min(dataset.getValues()); + }, + max: function (renderInfo: RenderInfo, datasetId: number) { + let dataset = renderInfo.datasets.getDatasetById(datasetId); + return d3.max(dataset.getValues()); + }, + sum: function (renderInfo: RenderInfo, datasetId: number) { + let dataset = renderInfo.datasets.getDatasetById(datasetId); + return d3.sum(dataset.getValues()); + }, + count: function (renderInfo: RenderInfo, datasetId: number) { + let dataset = renderInfo.datasets.getDatasetById(datasetId); + return dataset.getLengthNotNull(); + }, + days: function (renderInfo: RenderInfo, datasetId: number) { + let dataset = renderInfo.datasets.getDatasetById(datasetId); + let result = dataset.getLength(); + return result; + }, + maxStreak: function (renderInfo: RenderInfo, datasetId: number) { + let streak = 0; + let maxStreak = 0; + let dataset = renderInfo.datasets.getDatasetById(datasetId); + for (let dataPoint of dataset) { + if (dataPoint.value !== null) { + streak++; + } else { + streak = 0; + } + if (streak > maxStreak) { + maxStreak = streak; + } + } + return maxStreak; + }, + maxBreaks: function (renderInfo: RenderInfo, datasetId: number) { + let streak = 0; + let maxBreak = 0; + let dataset = renderInfo.datasets.getDatasetById(datasetId); + + for (let dataPoint of dataset) { + if (dataPoint.value === null) { + streak++; + } else { + streak = 0; + } + if (streak > maxBreak) { + maxBreak = streak; + } + } + return maxBreak; + }, + lastStreak: function (renderInfo: RenderInfo, datasetId: number) { + let streak = 0; + let dataset = renderInfo.datasets.getDatasetById(datasetId); + let values = dataset.getValues(); + for (let ind = values.length - 1; ind >= 0; ind--) { + let value = values[ind]; + if (value === null) { + break; + } else { + streak++; + } + } + return streak; + }, + lastBreaks: function (renderInfo: RenderInfo, datasetId: number) { + let breakDays = 0; + let dataset = renderInfo.datasets.getDatasetById(datasetId); + let values = dataset.getValues(); + for (let ind = values.length - 1; ind >= 0; ind--) { + let value = values[ind]; + if (value === null) { + breakDays++; + } else { + break; + } + } + return breakDays; + }, + average: function (renderInfo: RenderInfo, datasetId: number) { + let dataset = renderInfo.datasets.getDatasetById(datasetId); + let countNotNull = dataset.getLengthNotNull(); + if (countNotNull > 0) { + let sum = d3.sum(dataset.getValues()); + return sum / countNotNull; + } + return null; + }, + median: function (renderInfo: RenderInfo, datasetId: number) { + let dataset = renderInfo.datasets.getDatasetById(datasetId); + return d3.median(dataset.getValues()); + }, + variance: function (renderInfo: RenderInfo, datasetId: number) { + let dataset = renderInfo.datasets.getDatasetById(datasetId); + return d3.variance(dataset.getValues()); + }, +}; + +export function resolveTemplate(template: string, renderInfo: RenderInfo) { + let replaceMap: { [key: string]: string } = {}; + // Loop over fnSet, prepare replaceMap + Object.entries(fnSet).forEach(([fnName, fn]) => { + // {{\s*max(\(\s*Dataset\(\s*(?\d+)\s*\)\s*\))?\s*}} + let strRegex = + "{{\\s*" + + fnName + + "(\\(\\s*Dataset\\(\\s*((?\\d+)|(?\\w+))\\s*\\)\\s*\\))?\\s*}}"; + // console.log(strRegex); + let regex = new RegExp(strRegex, "gm"); + let match; + while ((match = regex.exec(template))) { + // console.log(match); + if (typeof match.groups !== "undefined") { + if (typeof match.groups.datasetId !== "undefined") { + let datasetId = parseInt(match.groups.datasetId); + // console.log(datasetId); + if (Number.isInteger(datasetId)) { + let strReplaceRegex = + "{{\\s*" + + fnName + + "(\\(\\s*Dataset\\(\\s*" + + datasetId.toString() + + "\\s*\\)\\s*\\))?\\s*}}"; + + if (!(strReplaceRegex in replaceMap)) { + let result = fn(renderInfo, datasetId); // calculate result + let strResult = "{{NA}}"; + if ( + typeof result !== "undefined" && + result !== null + ) { + if (Number.isInteger(result)) { + strResult = result.toFixed(0); + } else { + strResult = result.toFixed(2); + } + } + + replaceMap[strReplaceRegex] = strResult; + } + } + } else if (typeof match.groups.datasetName !== "undefined") { + let datasetName = match.groups.datasetName; + // console.log(datasetName); + let strReplaceRegex = + "{{\\s*" + + fnName + + "(\\(\\s*Dataset\\(\\s*" + + datasetName + + "\\s*\\)\\s*\\))?\\s*}}"; + + let datasetId = renderInfo.datasetName.indexOf(datasetName); + // console.log(datasetName); + // console.log(renderInfo.datasetName); + // console.log(datasetId); + if (!(strReplaceRegex in replaceMap)) { + let strResult = "{{NA}}"; + if (datasetId >= 0) { + let result = fn(renderInfo, datasetId); // calculate result + if ( + typeof result !== "undefined" && + result !== null + ) { + if (Number.isInteger(result)) { + strResult = result.toFixed(0); + } else { + strResult = result.toFixed(2); + } + } + } + replaceMap[strReplaceRegex] = strResult; + } + } else { + // no datasetId assigned use id 0 + // console.log("{{" + fnName + "}}") + let strReplaceRegex = "{{\\s*" + fnName + "\\s*}}"; + if (!(strReplaceRegex in replaceMap)) { + let result = fn(renderInfo, 0); // calculate result + let strResult = "{{NA}}"; + if (typeof result !== "undefined" && result !== null) { + if (Number.isInteger(result)) { + strResult = result.toFixed(0); + } else { + strResult = result.toFixed(2); + } + } + + replaceMap[strReplaceRegex] = strResult; + } + } + } else { + // groups undefined + // no datasetId assigned use id 0 + // console.log("{{" + fnName + "}}") + let strReplaceRegex = "{{\\s*" + fnName + "\\s*}}"; + if (!(strReplaceRegex in replaceMap)) { + let result = fn(renderInfo, 0); // calculate result + let strResult = "{{NA}}"; + if (typeof result !== "undefined" && result !== null) { + if (Number.isInteger(result)) { + strResult = result.toFixed(0); + } else { + strResult = result.toFixed(2); + } + } + + replaceMap[strReplaceRegex] = strResult; + } + } + } + }); + + // console.log(replaceMap); + // Do replace + for (let strReplaceRegex in replaceMap) { + let strResult = replaceMap[strReplaceRegex]; + let regex = new RegExp(strReplaceRegex, "gi"); + template = template.replace(regex, strResult); + } + + return template; +} diff --git a/src/parsing.ts b/src/parsing.ts index 9c2b561..b393fbe 100644 --- a/src/parsing.ts +++ b/src/parsing.ts @@ -1506,19 +1506,78 @@ export function getRenderInfoFromYaml( // console.log(renderInfo.bullet.orientation); // range - let numRange = 0; - let retRange = getNumberArrayFromInput( - "range", - yaml?.bullet?.range, - 3, // TODO - null, - true - ); - if (typeof retRange === "string") { - return retRange; // errorMessage + let range: Array = []; + if ( + typeof yaml.bullet.range === "object" && + yaml.bullet.range !== null + ) { + if (Array.isArray(yaml.bullet.range)) { + for (let r of yaml.bullet.range) { + if (typeof r === "string") { + let v = parseFloat(r); + if (Number.isNumber(v)) { + range.push(v); + } else { + errorMessage = + "Parameter 'range' accepts only numbers"; + return errorMessage; + } + } + } + } + } else if (typeof yaml.bullet.range === "string") { + let splitted = yaml.bullet.range.split(","); + if (splitted.length > 1) { + for (let piece of splitted) { + let v = parseFloat(piece.trim()); + if (!Number.isNaN(v)) { + // Number.isNumber(NaN) --> true + range.push(v); + } else { + errorMessage = + "Parameter 'range' accepts only numbers"; + return errorMessage; + } + } + } else if (yaml.bullet.range === "") { + errorMessage = "Empty range is not allowed."; + } else { + let v = parseFloat(yaml.bullet.range); + if (Number.isNumber(v)) { + range.push(v); + } else { + errorMessage = "Parameter 'range' accepts only numbers"; + return errorMessage; + } + } + } else { + errorMessage = "Invalid range"; + return errorMessage; + } + // Check the value is monotonically increasing + // Check the value is not negative + if (range.length === 1) { + if (range[0] < 0) { + errorMessage = "Negative range value is not allowed"; + return errorMessage; + } + } else if (range.length > 1) { + let lastBound = range[0]; + if (lastBound < 0) { + errorMessage = "Negative range value is not allowed"; + return errorMessage; + } else { + for (let ind = 1; ind < range.length; ind++) { + if (range[ind] <= lastBound) { + errorMessage = + "Values in parameter 'range' should be monotonically increasing"; + return errorMessage; + } + } + } } - renderInfo.bullet.range = retRange; - numRange = retRange.length; + renderInfo.bullet.range = range; + let numRange = range.length; // console.log(renderInfo.bullet.range); // range color diff --git a/src/summary.ts b/src/summary.ts index 3de3821..0fd0fb9 100644 --- a/src/summary.ts +++ b/src/summary.ts @@ -1,112 +1,11 @@ import * as d3 from "d3"; import { RenderInfo } from "./data"; +import * as expr from "./expr"; function checkSummaryTemplateValid(summaryTemplate: string): boolean { return true; } -let fnSet = { - min: function (renderInfo: RenderInfo, datasetId: number) { - let dataset = renderInfo.datasets.getDatasetById(datasetId); - return d3.min(dataset.getValues()); - }, - max: function (renderInfo: RenderInfo, datasetId: number) { - let dataset = renderInfo.datasets.getDatasetById(datasetId); - return d3.max(dataset.getValues()); - }, - sum: function (renderInfo: RenderInfo, datasetId: number) { - let dataset = renderInfo.datasets.getDatasetById(datasetId); - return d3.sum(dataset.getValues()); - }, - count: function (renderInfo: RenderInfo, datasetId: number) { - let dataset = renderInfo.datasets.getDatasetById(datasetId); - return dataset.getLengthNotNull(); - }, - days: function (renderInfo: RenderInfo, datasetId: number) { - let dataset = renderInfo.datasets.getDatasetById(datasetId); - let result = dataset.getLength(); - return result; - }, - maxStreak: function (renderInfo: RenderInfo, datasetId: number) { - let streak = 0; - let maxStreak = 0; - let dataset = renderInfo.datasets.getDatasetById(datasetId); - for (let dataPoint of dataset) { - if (dataPoint.value !== null) { - streak++; - } else { - streak = 0; - } - if (streak > maxStreak) { - maxStreak = streak; - } - } - return maxStreak; - }, - maxBreaks: function (renderInfo: RenderInfo, datasetId: number) { - let streak = 0; - let maxBreak = 0; - let dataset = renderInfo.datasets.getDatasetById(datasetId); - - for (let dataPoint of dataset) { - if (dataPoint.value === null) { - streak++; - } else { - streak = 0; - } - if (streak > maxBreak) { - maxBreak = streak; - } - } - return maxBreak; - }, - lastStreak: function (renderInfo: RenderInfo, datasetId: number) { - let streak = 0; - let dataset = renderInfo.datasets.getDatasetById(datasetId); - let values = dataset.getValues(); - for (let ind = values.length - 1; ind >= 0; ind--) { - let value = values[ind]; - if (value === null) { - break; - } else { - streak++; - } - } - return streak; - }, - lastBreaks: function (renderInfo: RenderInfo, datasetId: number) { - let breakDays = 0; - let dataset = renderInfo.datasets.getDatasetById(datasetId); - let values = dataset.getValues(); - for (let ind = values.length - 1; ind >= 0; ind--) { - let value = values[ind]; - if (value === null) { - breakDays++; - } else { - break; - } - } - return breakDays; - }, - average: function (renderInfo: RenderInfo, datasetId: number) { - let dataset = renderInfo.datasets.getDatasetById(datasetId); - let countNotNull = dataset.getLengthNotNull(); - if (countNotNull > 0) { - let sum = d3.sum(dataset.getValues()); - return sum / countNotNull; - } - return null; - }, - median: function (renderInfo: RenderInfo, datasetId: number) { - let dataset = renderInfo.datasets.getDatasetById(datasetId); - return d3.median(dataset.getValues()); - }, - variance: function (renderInfo: RenderInfo, datasetId: number) { - let dataset = renderInfo.datasets.getDatasetById(datasetId); - return d3.variance(dataset.getValues()); - }, -}; - export function renderSummary(canvas: HTMLElement, renderInfo: RenderInfo) { // console.log("renderSummary"); // console.log(renderInfo); @@ -119,126 +18,7 @@ export function renderSummary(canvas: HTMLElement, renderInfo: RenderInfo) { return "Invalid summary template"; } - let replaceMap: { [key: string]: string } = {}; - // Loop over fnSet, prepare replaceMap - Object.entries(fnSet).forEach(([fnName, fn]) => { - // {{\s*max(\(\s*Dataset\(\s*(?\d+)\s*\)\s*\))?\s*}} - let strRegex = - "{{\\s*" + - fnName + - "(\\(\\s*Dataset\\(\\s*((?\\d+)|(?\\w+))\\s*\\)\\s*\\))?\\s*}}"; - // console.log(strRegex); - let regex = new RegExp(strRegex, "gm"); - let match; - while ((match = regex.exec(outputSummary))) { - // console.log(match); - if (typeof match.groups !== "undefined") { - if (typeof match.groups.datasetId !== "undefined") { - let datasetId = parseInt(match.groups.datasetId); - // console.log(datasetId); - if (Number.isInteger(datasetId)) { - let strReplaceRegex = - "{{\\s*" + - fnName + - "(\\(\\s*Dataset\\(\\s*" + - datasetId.toString() + - "\\s*\\)\\s*\\))?\\s*}}"; - - if (!(strReplaceRegex in replaceMap)) { - let result = fn(renderInfo, datasetId); // calculate result - let strResult = "{{NA}}"; - if ( - typeof result !== "undefined" && - result !== null - ) { - if (Number.isInteger(result)) { - strResult = result.toFixed(0); - } else { - strResult = result.toFixed(2); - } - } - - replaceMap[strReplaceRegex] = strResult; - } - } - } else if (typeof match.groups.datasetName !== "undefined") { - let datasetName = match.groups.datasetName; - // console.log(datasetName); - let strReplaceRegex = - "{{\\s*" + - fnName + - "(\\(\\s*Dataset\\(\\s*" + - datasetName + - "\\s*\\)\\s*\\))?\\s*}}"; - - let datasetId = renderInfo.datasetName.indexOf(datasetName); - // console.log(datasetName); - // console.log(renderInfo.datasetName); - // console.log(datasetId); - if (!(strReplaceRegex in replaceMap)) { - let strResult = "{{NA}}"; - if (datasetId >= 0) { - let result = fn(renderInfo, datasetId); // calculate result - if ( - typeof result !== "undefined" && - result !== null - ) { - if (Number.isInteger(result)) { - strResult = result.toFixed(0); - } else { - strResult = result.toFixed(2); - } - } - } - replaceMap[strReplaceRegex] = strResult; - } - } else { - // no datasetId assigned use id 0 - // console.log("{{" + fnName + "}}") - let strReplaceRegex = "{{\\s*" + fnName + "\\s*}}"; - if (!(strReplaceRegex in replaceMap)) { - let result = fn(renderInfo, 0); // calculate result - let strResult = "{{NA}}"; - if (typeof result !== "undefined" && result !== null) { - if (Number.isInteger(result)) { - strResult = result.toFixed(0); - } else { - strResult = result.toFixed(2); - } - } - - replaceMap[strReplaceRegex] = strResult; - } - } - } else { - // groups undefined - // no datasetId assigned use id 0 - // console.log("{{" + fnName + "}}") - let strReplaceRegex = "{{\\s*" + fnName + "\\s*}}"; - if (!(strReplaceRegex in replaceMap)) { - let result = fn(renderInfo, 0); // calculate result - let strResult = "{{NA}}"; - if (typeof result !== "undefined" && result !== null) { - if (Number.isInteger(result)) { - strResult = result.toFixed(0); - } else { - strResult = result.toFixed(2); - } - } - - replaceMap[strReplaceRegex] = strResult; - } - } - } - }); - // console.log(replaceMap); - // Do replace - for (let strReplaceRegex in replaceMap) { - let strResult = replaceMap[strReplaceRegex]; - let regex = new RegExp(strReplaceRegex, "gi"); - outputSummary = outputSummary.replace(regex, strResult); - } - + outputSummary = expr.resolveTemplate(outputSummary, renderInfo); if (outputSummary !== "") { let textBlock = d3.select(canvas).append("div"); if (outputSummary.includes("\n")) { From 090805756d45460892a34fb0dfc48a4e95214f16 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Sun, 30 May 2021 15:40:22 +0800 Subject: [PATCH 027/402] Refine the bullet chart --- examples/TestBullet.md | 22 +++-- src/bullet.ts | 211 ++++++++++++++++++++++++++++------------- src/data.ts | 14 +-- src/parsing.ts | 15 ++- 4 files changed, 178 insertions(+), 84 deletions(-) diff --git a/examples/TestBullet.md b/examples/TestBullet.md index 0dd94a7..8693bd2 100644 --- a/examples/TestBullet.md +++ b/examples/TestBullet.md @@ -5,7 +5,7 @@ searchType: tag searchTarget: clean-up folder: diary endDate: 2021-01-31 -fixedScale: 1 +fixedScale: 0.1 bullet: title: "Clean Up" dataset: 0 @@ -13,8 +13,9 @@ bullet: range: 10, 20, 40 rangeColor: darkgray, silver, lightgray value: "{{lastBreaks}}" + valueUnit: times valueColor: '#69b3a2' - showMark: false + showMarker: true markerValue: 24 markerColor: black ``` @@ -22,18 +23,19 @@ bullet: Vertical bullet chart ``` tracker searchType: tag -searchTarget: clean-up +searchTarget: meditation folder: diary endDate: 2021-01-31 bullet: - title: "Clean Up" + title: "Meditation" dataset: 0 orientation: vertical - range: 10, 20, 40 + range: 30, 60, 100 rangeColor: darkgray, silver, lightgray - value: "{{lastBreaks}}" - valueColor: '#69b3a2' - showMark: false - markerValue: 24 - markerColor: black + value: "{{lastStreak}}" + valueUnit: % + valueColor: steelblue + showMarker: true + markerValue: 80 + markerColor: red ``` \ No newline at end of file diff --git a/src/bullet.ts b/src/bullet.ts index 7b35d70..b9d44b0 100644 --- a/src/bullet.ts +++ b/src/bullet.ts @@ -78,78 +78,134 @@ function renderTitle(chartElements: ChartElements, renderInfo: RenderInfo) { let bulletInfo = renderInfo.bullet; if (!bulletInfo) return; - if (!bulletInfo.title) return; + const spacing = 6; // spacing between title and dataArea - let titleSize = helper.measureTextSize( - bulletInfo.title, - "tracker-title-small" - ); - let spacing = 6; // spacing between title and dataArea - - if (bulletInfo.orientation === "horizontal") { - let title = chartElements.graphArea - .append("text") - .text(bulletInfo.title) // pivot at center - .attr("id", "title") - .attr("x", titleSize.width / 2.0) - .attr("y", renderInfo.dataAreaSize.height / 2.0) - .attr("height", titleSize.height) // for later use - .attr("class", "tracker-title-small"); - chartElements["title"] = title; - - // Expand parent areas - helper.expandArea(chartElements.svg, titleSize.width + spacing, 0); - helper.expandArea( - chartElements.graphArea, - titleSize.width + spacing, - 0 + if (bulletInfo.title) { + let titleSize = helper.measureTextSize( + bulletInfo.title, + "tracker-title-small" ); - // Move sibling areas - helper.moveArea(chartElements.dataArea, titleSize.width + spacing, 0); - } else if (bulletInfo.orientation === "vertical") { - // if label width > dataArea width - let xMiddle = renderInfo.dataAreaSize.width / 2.0; - if (titleSize.width > renderInfo.dataAreaSize.width) { + if (bulletInfo.orientation === "horizontal") { + let title = chartElements.graphArea + .append("text") + .text(bulletInfo.title) // pivot at center + .attr("id", "title") + .attr("x", titleSize.width / 2.0) + .attr("y", renderInfo.dataAreaSize.height / 2.0) + .attr("height", titleSize.height) // for later use + .attr("class", "tracker-title-small"); + chartElements["title"] = title; + + // Expand parent areas + helper.expandArea(chartElements.svg, titleSize.width + spacing, 0); helper.expandArea( - chartElements.svg, - titleSize.width - renderInfo.dataAreaSize.width, + chartElements.graphArea, + titleSize.width + spacing, + 0 + ); + + // Move sibling areas + helper.moveArea( + chartElements.dataArea, + titleSize.width + spacing, 0 ); + } else if (bulletInfo.orientation === "vertical") { + // if label width > dataArea width + let xMiddle = renderInfo.dataAreaSize.width / 2.0; + if (titleSize.width > renderInfo.dataAreaSize.width) { + helper.expandArea( + chartElements.svg, + titleSize.width - renderInfo.dataAreaSize.width, + 0 + ); + helper.expandArea( + chartElements.graphArea, + titleSize.width - renderInfo.dataAreaSize.width, + 0 + ); + + helper.moveArea( + chartElements.dataArea, + titleSize.width / 2.0 - renderInfo.dataAreaSize.width / 2.0, + 0 + ); + xMiddle = titleSize.width / 2.0; + } + + let title = chartElements.graphArea + .append("text") + .text(bulletInfo.title) // pivot at center + .attr("id", "title") + .attr("x", xMiddle) + .attr("y", titleSize.height / 2.0) + .attr("height", titleSize.height) // for later use + .attr("class", "tracker-title-small"); + chartElements["title"] = title; + + // Expand parent areas + helper.expandArea(chartElements.svg, 0, titleSize.height + spacing); helper.expandArea( chartElements.graphArea, - titleSize.width - renderInfo.dataAreaSize.width, - 0 + 0, + titleSize.height + spacing ); + // Move sibling areas helper.moveArea( chartElements.dataArea, - titleSize.width / 2.0 - renderInfo.dataAreaSize.width / 2.0, - 0 + 0, + titleSize.height + spacing ); - xMiddle = titleSize.width / 2.0; } + } - let title = chartElements.graphArea - .append("text") - .text(bulletInfo.title) // pivot at center - .attr("id", "title") - .attr("x", xMiddle) - .attr("y", titleSize.height / 2.0) - .attr("height", titleSize.height) // for later use - .attr("class", "tracker-title-small"); - chartElements["title"] = title; - - // Expand parent areas - helper.expandArea(chartElements.svg, 0, titleSize.height + spacing); - helper.expandArea( - chartElements.graphArea, - 0, - titleSize.height + spacing + if (bulletInfo.valueUnit) { + let unitSize = helper.measureTextSize( + bulletInfo.valueUnit, + "tracker-tick-label" ); - // Move sibling areas - helper.moveArea(chartElements.dataArea, 0, titleSize.height + spacing); + if (bulletInfo.orientation === "horizontal") { + let unit = chartElements.dataArea + .append("text") + .text(bulletInfo.valueUnit) + .attr("id", "unit") + .attr("x", -1 * (unitSize.width + spacing)) + .attr("y", renderInfo.dataAreaSize.height + spacing) + .attr("height", unitSize.height) // for later use + .attr("class", "tracker-tick-label"); // pivot at corder + chartElements["unit"] = unit; + } else if (bulletInfo.orientation === "vertical") { + let unit = chartElements.dataArea + .append("text") + .text(bulletInfo.valueUnit) + .attr("id", "unit") + .attr( + "x", + renderInfo.dataAreaSize.width / 2 - unitSize.width / 2 + ) + .attr("y", -(unitSize.height / 2.0 + spacing)) + .attr("height", unitSize.height) // for later use + .attr("class", "tracker-tick-label"); // pivot at corder + chartElements["unit"] = unit; + + // Expand parent areas + helper.expandArea(chartElements.svg, 0, unitSize.height + spacing); + helper.expandArea( + chartElements.graphArea, + 0, + unitSize.height + spacing + ); + + // Move dataArea down + helper.moveArea( + chartElements.dataArea, + 0, + unitSize.height + spacing + ); + } } } @@ -167,15 +223,25 @@ function renderAxis( if (!bulletInfo) return; let range = bulletInfo.range; - let domain = [0, range[range.length - 1]]; + let lastRange = range[range.length - 1]; + let domain = [0, lastRange]; + let tickFormat = d3.tickFormat(0, lastRange, 7); let tickLength = 6; + let valueUnit = bulletInfo.valueUnit; + if (bulletInfo.orientation === "horizontal") { let scale = d3.scaleLinear(); scale.domain(domain).range([0, renderInfo.dataAreaSize.width]); chartElements["scale"] = scale; let axisGen = d3.axisBottom(scale); + axisGen.tickFormat((v) => { + if (valueUnit && valueUnit.endsWith("%")) { + return tickFormat(v) + " %"; + } + return tickFormat(v); + }); let axis = chartElements.dataArea .append("g") .attr("id", "axis") @@ -209,6 +275,12 @@ function renderAxis( chartElements["scale"] = scale; let axisGen = d3.axisLeft(scale); + axisGen.tickFormat((v) => { + if (valueUnit && valueUnit.endsWith("%")) { + return tickFormat(v) + " %"; + } + return tickFormat(v); + }); let axis = chartElements.dataArea .append("g") .attr("id", "axis") @@ -265,13 +337,13 @@ function renderBackPanel( .enter() .append("rect") .attr("x", function (d: any, i: number) { - return scale(d.start); + return Math.floor(scale(d.start)); }) .attr("y", function (d: any) { return 0; }) .attr("width", function (d: any, i: number) { - return scale(d.end - d.start); + return Math.ceil(scale(d.end - d.start)); }) .attr("height", renderInfo.dataAreaSize.height) .style("fill", function (d: any) { @@ -287,11 +359,14 @@ function renderBackPanel( return 0; }) .attr("y", function (d: any) { - return scale(d.end); + return Math.floor(scale(d.end)); }) .attr("width", renderInfo.dataAreaSize.width) .attr("height", function (d: any) { - return renderInfo.dataAreaSize.height - scale(d.end - d.start); + return ( + renderInfo.dataAreaSize.height - + Math.floor(scale(d.end - d.start)) + ); }) .style("fill", function (d: any) { return d.color; @@ -329,7 +404,7 @@ function renderBar( .append("rect") .attr("x", scale(0)) .attr("y", barWidth) - .attr("width", scale(actualValue)) + .attr("width", Math.floor(scale(actualValue))) .attr("height", barWidth) .style("fill", valueColor); } else if (bulletInfo.orientation === "vertical") { @@ -337,9 +412,12 @@ function renderBar( let bar = chartElements.dataArea .append("rect") .attr("x", barWidth) - .attr("y", scale(actualValue)) + .attr("y", Math.floor(scale(actualValue))) .attr("width", barWidth) - .attr("height", renderInfo.dataAreaSize.height - scale(actualValue)) + .attr( + "height", + renderInfo.dataAreaSize.height - Math.floor(scale(actualValue)) + ) .style("fill", valueColor); } } @@ -356,6 +434,9 @@ function renderMark( let bulletInfo = renderInfo.bullet; if (!bulletInfo) return; + let showMarker = bulletInfo.showMarker; + if (!showMarker) return; + let markerValue = bulletInfo.markerValue; let markerColor = bulletInfo.markerColor; @@ -363,7 +444,7 @@ function renderMark( if (bulletInfo.orientation === "horizontal") { let markerLength = (renderInfo.dataAreaSize.height * 2) / 3; - let mark = chartElements.dataArea + let marker = chartElements.dataArea .append("rect") .attr("x", scale(markerValue) - 1.5) .attr("y", markerLength / 4) @@ -372,7 +453,7 @@ function renderMark( .style("fill", markerColor); } else if (bulletInfo.orientation === "vertical") { let markerLength = (renderInfo.dataAreaSize.width * 2) / 3; - let mark = chartElements.dataArea + let marker = chartElements.dataArea .append("rect") .attr("x", markerLength / 4) .attr("y", scale(markerValue) - 1.5) diff --git a/src/data.ts b/src/data.ts index 11a8b3d..94a07a2 100644 --- a/src/data.ts +++ b/src/data.ts @@ -641,11 +641,12 @@ export class BulletInfo { title: string; dataset: string; orientation: string; - range: number[]; - rangeColor: string[]; value: string; + valueUnit: string; valueColor: string; - showMark: boolean; + range: number[]; + rangeColor: string[]; + showMarker: boolean; markerValue: number; markerColor: string; @@ -653,11 +654,12 @@ export class BulletInfo { this.title = ""; this.dataset = "0"; // dataset id or name this.orientation = "horizontal"; // or vertical - this.range = []; - this.rangeColor = []; this.value = ""; // Can possess template varialbe + this.valueUnit = ""; this.valueColor = "#69b3a2"; - this.showMark = false; + this.range = []; + this.rangeColor = []; + this.showMarker = false; this.markerValue = 0; this.markerColor = ""; } diff --git a/src/parsing.ts b/src/parsing.ts index b393fbe..e79aa6e 100644 --- a/src/parsing.ts +++ b/src/parsing.ts @@ -1575,6 +1575,9 @@ export function getRenderInfoFromYaml( } } } + } else { + errorMessage = "Empty range is not allowed"; + return errorMessage; } renderInfo.bullet.range = range; let numRange = range.length; @@ -1601,6 +1604,12 @@ export function getRenderInfoFromYaml( } // console.log(renderInfo.bullet.value); + // value unit + if (typeof yaml.bullet.valueUnit === "string") { + renderInfo.bullet.valueUnit = yaml.bullet.valueUnit; + } + // console.log(renderInfo.bullet.valueUnit); + // value color if (typeof yaml.bullet.valueColor === "string") { renderInfo.bullet.valueColor = yaml.bullet.valueColor; @@ -1608,8 +1617,8 @@ export function getRenderInfoFromYaml( // console.log(renderInfo.bullet.valueColor); // show mark - if (typeof yaml.bullet.showMark === "boolean") { - renderInfo.bullet.showMark = yaml.bullet.showMark; + if (typeof yaml.bullet.showMarker === "boolean") { + renderInfo.bullet.showMarker = yaml.bullet.showMarker; } // console.log(renderInfo.bullet.showMark); @@ -1620,7 +1629,7 @@ export function getRenderInfoFromYaml( // console.log(renderInfo.bullet.markValue); // mark color - if (typeof yaml.bullet.markerColor === "number") { + if (typeof yaml.bullet.markerColor === "string") { renderInfo.bullet.markerColor = yaml.bullet.markerColor; } // console.log(renderInfo.bullet.markValue); From b1d346a967eaa4481e7107ca06d7501e6e5b1a63 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Sun, 30 May 2021 15:43:53 +0800 Subject: [PATCH 028/402] Enable scaling bullet chart --- examples/TestBullet.md | 2 +- src/bullet.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/examples/TestBullet.md b/examples/TestBullet.md index 8693bd2..37f4b22 100644 --- a/examples/TestBullet.md +++ b/examples/TestBullet.md @@ -5,7 +5,7 @@ searchType: tag searchTarget: clean-up folder: diary endDate: 2021-01-31 -fixedScale: 0.1 +fixedScale: 1.1 bullet: title: "Clean Up" dataset: 0 diff --git a/src/bullet.ts b/src/bullet.ts index b9d44b0..d8892ae 100644 --- a/src/bullet.ts +++ b/src/bullet.ts @@ -71,6 +71,34 @@ function createAreas( return chartElements; } +function setChartScale( + _canvas: HTMLElement, + chartElements: ChartElements, + renderInfo: RenderInfo +) { + let canvas = d3.select(_canvas); + let svg = chartElements.svg; + let svgWidth = parseFloat(svg.attr("width")); + let svgHeight = parseFloat(svg.attr("height")); + svg.attr("width", null) + .attr("height", null) + .attr("viewBox", `0 0 ${svgWidth} ${svgHeight}`) + .attr("preserveAspectRatio", "xMidYMid meet"); + + if (renderInfo.fitPanelWidth) { + canvas.style("width", "100%"); + } else { + canvas.style( + "width", + (svgWidth * renderInfo.fixedScale).toString() + "px" + ); + canvas.style( + "height", + (svgHeight * renderInfo.fixedScale).toString() + "px" + ); + } +} + function renderTitle(chartElements: ChartElements, renderInfo: RenderInfo) { // console.log("renderTitle"); // under graphArea @@ -497,4 +525,6 @@ export function renderBullet(canvas: HTMLElement, renderInfo: RenderInfo) { } renderMark(chartElements, renderInfo, dataset); + + setChartScale(canvas, chartElements, renderInfo); } From b9ff4c33596b006d8b5e67500842ae2f80ec3841 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Sun, 30 May 2021 17:33:27 +0800 Subject: [PATCH 029/402] Refine the bullet chart --- src/bullet.ts | 63 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/src/bullet.ts b/src/bullet.ts index d8892ae..fe0047b 100644 --- a/src/bullet.ts +++ b/src/bullet.ts @@ -143,6 +143,7 @@ function renderTitle(chartElements: ChartElements, renderInfo: RenderInfo) { // if label width > dataArea width let xMiddle = renderInfo.dataAreaSize.width / 2.0; if (titleSize.width > renderInfo.dataAreaSize.width) { + // console.log("expand area for vertical title"); helper.expandArea( chartElements.svg, titleSize.width - renderInfo.dataAreaSize.width, @@ -162,11 +163,13 @@ function renderTitle(chartElements: ChartElements, renderInfo: RenderInfo) { xMiddle = titleSize.width / 2.0; } + let axisWidth = parseFloat(chartElements.axis.attr("width")); + let title = chartElements.graphArea .append("text") .text(bulletInfo.title) // pivot at center .attr("id", "title") - .attr("x", xMiddle) + .attr("x", xMiddle + axisWidth) .attr("y", titleSize.height / 2.0) .attr("height", titleSize.height) // for later use .attr("class", "tracker-title-small"); @@ -253,10 +256,17 @@ function renderAxis( let range = bulletInfo.range; let lastRange = range[range.length - 1]; let domain = [0, lastRange]; - let tickFormat = d3.tickFormat(0, lastRange, 7); + let tickLength = 6; - let valueUnit = bulletInfo.valueUnit; + let tickFormatFn: any = function(value: any) { + if (valueUnit && valueUnit.endsWith("%")) { + return d3.tickFormat(0, lastRange, 7)(value) + " %"; + } + return d3.tickFormat(0, lastRange, 7)(value); + }; + let maxTickLabel = tickFormatFn(lastRange); + let maxTickLabelSize = helper.measureTextSize(maxTickLabel, "tracker-tick-label"); if (bulletInfo.orientation === "horizontal") { let scale = d3.scaleLinear(); @@ -264,12 +274,7 @@ function renderAxis( chartElements["scale"] = scale; let axisGen = d3.axisBottom(scale); - axisGen.tickFormat((v) => { - if (valueUnit && valueUnit.endsWith("%")) { - return tickFormat(v) + " %"; - } - return tickFormat(v); - }); + axisGen.tickFormat(tickFormatFn); let axis = chartElements.dataArea .append("g") .attr("id", "axis") @@ -279,23 +284,25 @@ function renderAxis( ) .call(axisGen) .attr("class", "tracker-axis"); + chartElements["axis"] = axis; let axisLine = axis.selectAll("path"); let axisTicks = axis.selectAll("line"); - let tickLabelSize = helper.measureTextSize("123456789"); - let tickLabelHeight = tickLabelSize.height; + let axisTickLabels = axis + .selectAll("text") + .attr("class", "tracker-tick-label"); - axis.attr("width", renderInfo.dataAreaSize.width); - axis.attr("height", tickLength + tickLabelHeight); + axis.attr("width", renderInfo.dataAreaSize.width + maxTickLabelSize.width); + axis.attr("height", tickLength + maxTickLabelSize.height); // Expand areas - helper.expandArea(chartElements.svg, 0, tickLength + tickLabelHeight); + helper.expandArea(chartElements.svg, + maxTickLabelSize.width, tickLength + maxTickLabelSize.height); helper.expandArea( chartElements.graphArea, - 0, - tickLength + tickLabelHeight + + maxTickLabelSize.width, + tickLength + maxTickLabelSize.height ); } else if (bulletInfo.orientation === "vertical") { let scale = d3.scaleLinear(); @@ -303,12 +310,7 @@ function renderAxis( chartElements["scale"] = scale; let axisGen = d3.axisLeft(scale); - axisGen.tickFormat((v) => { - if (valueUnit && valueUnit.endsWith("%")) { - return tickFormat(v) + " %"; - } - return tickFormat(v); - }); + axisGen.tickFormat(tickFormatFn); let axis = chartElements.dataArea .append("g") .attr("id", "axis") @@ -316,17 +318,28 @@ function renderAxis( .attr("y", 0) .call(axisGen) .attr("class", "tracker-axis"); + chartElements["axis"] = axis; let axisLine = axis.selectAll("path"); let axisTicks = axis.selectAll("line"); - axis.attr("width", tickLength); + let axisTickLabels = axis + .selectAll("text") + .attr("class", "tracker-tick-label"); + + axis.attr("width", tickLength + maxTickLabelSize.width); axis.attr("height", renderInfo.dataAreaSize.width); // Expand areas - helper.expandArea(chartElements.svg, tickLength, 0); - helper.expandArea(chartElements.graphArea, tickLength, 0); + helper.expandArea(chartElements.svg, tickLength + maxTickLabelSize.width, 0); + helper.expandArea(chartElements.graphArea, tickLength + maxTickLabelSize.width, 0); + + helper.moveArea( + chartElements.dataArea, + tickLength + maxTickLabelSize.width, + 0 + ); } } From b6407475b92e14ce1b03990c2d5d02487db8b879 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Sun, 30 May 2021 17:44:06 +0800 Subject: [PATCH 030/402] Hide the axis line in the bullet chart --- examples/TestBullet.md | 2 +- src/bullet.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/TestBullet.md b/examples/TestBullet.md index 37f4b22..45604d0 100644 --- a/examples/TestBullet.md +++ b/examples/TestBullet.md @@ -32,7 +32,7 @@ bullet: orientation: vertical range: 30, 60, 100 rangeColor: darkgray, silver, lightgray - value: "{{lastStreak}}" + value: "{{sum}}" valueUnit: % valueColor: steelblue showMarker: true diff --git a/src/bullet.ts b/src/bullet.ts index fe0047b..7a61332 100644 --- a/src/bullet.ts +++ b/src/bullet.ts @@ -286,7 +286,7 @@ function renderAxis( .attr("class", "tracker-axis"); chartElements["axis"] = axis; - let axisLine = axis.selectAll("path"); + let axisLine = axis.selectAll("path").style("stroke", "none"); let axisTicks = axis.selectAll("line"); @@ -320,7 +320,7 @@ function renderAxis( .attr("class", "tracker-axis"); chartElements["axis"] = axis; - let axisLine = axis.selectAll("path"); + let axisLine = axis.selectAll("path").style("stroke", "none"); let axisTicks = axis.selectAll("line"); From f53acd4e5019910beac06ec515461daf9e4bc7d2 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Sun, 30 May 2021 17:44:59 +0800 Subject: [PATCH 031/402] Prettify the source code --- src/bullet.ts | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/bullet.ts b/src/bullet.ts index 7a61332..864df72 100644 --- a/src/bullet.ts +++ b/src/bullet.ts @@ -256,17 +256,20 @@ function renderAxis( let range = bulletInfo.range; let lastRange = range[range.length - 1]; let domain = [0, lastRange]; - + let tickLength = 6; let valueUnit = bulletInfo.valueUnit; - let tickFormatFn: any = function(value: any) { + let tickFormatFn: any = function (value: any) { if (valueUnit && valueUnit.endsWith("%")) { return d3.tickFormat(0, lastRange, 7)(value) + " %"; } return d3.tickFormat(0, lastRange, 7)(value); }; let maxTickLabel = tickFormatFn(lastRange); - let maxTickLabelSize = helper.measureTextSize(maxTickLabel, "tracker-tick-label"); + let maxTickLabelSize = helper.measureTextSize( + maxTickLabel, + "tracker-tick-label" + ); if (bulletInfo.orientation === "horizontal") { let scale = d3.scaleLinear(); @@ -294,14 +297,21 @@ function renderAxis( .selectAll("text") .attr("class", "tracker-tick-label"); - axis.attr("width", renderInfo.dataAreaSize.width + maxTickLabelSize.width); + axis.attr( + "width", + renderInfo.dataAreaSize.width + maxTickLabelSize.width + ); axis.attr("height", tickLength + maxTickLabelSize.height); // Expand areas - helper.expandArea(chartElements.svg, + maxTickLabelSize.width, tickLength + maxTickLabelSize.height); + helper.expandArea( + chartElements.svg, + +maxTickLabelSize.width, + tickLength + maxTickLabelSize.height + ); helper.expandArea( chartElements.graphArea, - + maxTickLabelSize.width, + +maxTickLabelSize.width, tickLength + maxTickLabelSize.height ); } else if (bulletInfo.orientation === "vertical") { @@ -332,9 +342,17 @@ function renderAxis( axis.attr("height", renderInfo.dataAreaSize.width); // Expand areas - helper.expandArea(chartElements.svg, tickLength + maxTickLabelSize.width, 0); - helper.expandArea(chartElements.graphArea, tickLength + maxTickLabelSize.width, 0); - + helper.expandArea( + chartElements.svg, + tickLength + maxTickLabelSize.width, + 0 + ); + helper.expandArea( + chartElements.graphArea, + tickLength + maxTickLabelSize.width, + 0 + ); + helper.moveArea( chartElements.dataArea, tickLength + maxTickLabelSize.width, From dc9ff4c1280732c151810787965fd35e61533cae Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Sun, 30 May 2021 21:30:18 +0800 Subject: [PATCH 032/402] Fix missing zero values --- src/collecting.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/collecting.ts b/src/collecting.ts index 8d50f90..388dec1 100644 --- a/src/collecting.ts +++ b/src/collecting.ts @@ -205,7 +205,7 @@ export function collectDataFromInlineTag( // console.log("single-value"); let toParse = splitted[0].trim(); let retParse = helper.parseFloatFromAny(toParse); - if (retParse.value) { + if (retParse.value !== null) { if (retParse.type === ValueType.Time) { tagMeasure = retParse.value; tagExist = true; @@ -226,7 +226,8 @@ export function collectDataFromInlineTag( ) { let toParse = splitted[query.getAccessor()].trim(); let retParse = helper.parseFloatFromAny(toParse); - if (retParse.value) { + //console.log(retParse); + if (retParse.value !== null) { if (retParse.type === ValueType.Time) { tagMeasure = retParse.value; tagExist = true; From d32e9468bed0741c707a17c6a596fbea1c614a4b Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Sun, 30 May 2021 21:31:52 +0800 Subject: [PATCH 033/402] Update examples --- examples/HabitTracker.md | 2 +- examples/TestTimeValues.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/HabitTracker.md b/examples/HabitTracker.md index 1b606d4..9f69a6d 100644 --- a/examples/HabitTracker.md +++ b/examples/HabitTracker.md @@ -41,7 +41,7 @@ searchType: tag searchTarget: meditation folder: diary summary: - template: "Longest Streak: {{maxStreak}} day(s)\nLongest Break: {{maxBreak}} day(s)\nLast streak: {{lastStreak}} day(s)" + template: "Longest Streak: {{maxStreak}} day(s)\nLongest Breaks: {{maxBreaks}} day(s)\nLast streak: {{lastStreak}} day(s)" ``` CleanUp diff --git a/examples/TestTimeValues.md b/examples/TestTimeValues.md index 181cf12..dc72220 100644 --- a/examples/TestTimeValues.md +++ b/examples/TestTimeValues.md @@ -5,12 +5,14 @@ searchType: frontmatter searchTarget: clock-in, clock-out endDate: 2021-01-15 folder: diary +datasetName: Clock-In, Clock-Out line: title: "Working Hours" yAxisLabel: "Time (24h)" reverseYAxis: true lineColor: yellow, red showPoint: true + showLegend: true ``` ``` tracker @@ -19,9 +21,11 @@ searchTarget: sleep[0], sleep[1] endDate: 2021-01-15 folder: diary valueShift: -24:00, 00:00 +datasetName: Sleep, WakeUp line: title: "Sleep" yAxisLabel: "Time (24h)" lineColor: yellow, red showPoint: true + showLegend: true ``` \ No newline at end of file From a63fe885cd855d6d7226fdfea4d4c55f589dff8d Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Tue, 1 Jun 2021 08:46:52 +0800 Subject: [PATCH 034/402] Allow collecting data from fileMeta --- examples/Test FileMeta.md | 11 ++++ src/collecting.ts | 79 +++++++++++++++++++++++++ src/data.ts | 1 + src/helper.ts | 26 --------- src/main.ts | 117 ++++++++++++++++++++++---------------- src/parsing.ts | 6 +- 6 files changed, 163 insertions(+), 77 deletions(-) create mode 100644 examples/Test FileMeta.md diff --git a/examples/Test FileMeta.md b/examples/Test FileMeta.md new file mode 100644 index 0000000..a3a6710 --- /dev/null +++ b/examples/Test FileMeta.md @@ -0,0 +1,11 @@ +# Test FileMeta + +``` tracker +searchType: fileMeta +searchTarget: size +folder: diary +line: + title: File Size + yAxisLabel: Size + yAxisUnit: byte +``` \ No newline at end of file diff --git a/src/collecting.ts b/src/collecting.ts index 388dec1..99f16fc 100644 --- a/src/collecting.ts +++ b/src/collecting.ts @@ -11,6 +11,57 @@ import { } from "./data"; import * as helper from "./helper"; +export function getDateFromFilename(file: TFile, renderInfo: RenderInfo) { + let fileBaseName = file.basename; + + if ( + renderInfo.dateFormatPrefix && + fileBaseName.startsWith(renderInfo.dateFormatPrefix) + ) { + fileBaseName = fileBaseName.slice(renderInfo.dateFormatPrefix.length); + } + if ( + renderInfo.dateFormatSuffix && + fileBaseName.endsWith(renderInfo.dateFormatSuffix) + ) { + fileBaseName = fileBaseName.slice( + 0, + fileBaseName.length - renderInfo.dateFormatSuffix.length + ); + } + // console.log(fileBaseName); + + let fileDate = window.moment(fileBaseName, renderInfo.dateFormat, true); + // console.log(fileDate); + + return fileDate; +} + +export function getDateFromFrontmatter() { + let date = window.moment(""); + return date; +} + +export function getDateFromTag() { + let date = window.moment(""); + return date; +} + +export function getDateFromText() { + let date = window.moment(""); + return date; +} + +export function getDateFromDvField() { + let date = window.moment(""); + return date; +} + +export function getDateFromFileMeta() { + let date = window.moment(""); + return date; +} + function addToDataMap( dataMap: DataMap, date: string, @@ -299,6 +350,34 @@ export function collectDataFromText( } } +export function collectDataFromFileMeta( + file: TFile, + query: Query, + renderInfo: RenderInfo, + dataMap: DataMap, + xValueMap: XValueMap +) { + // console.log("collectDataFromFileMeta"); + + if (file && file instanceof TFile) { + // console.log(file.stat); + + let target = query.getTarget(); + if (target === "cDate") { + let ctime = file.stat.ctime; + query.valueType = ValueType.Date; + addToDataMap(dataMap, xValueMap.get(-1), query, ctime); + } else if (target === "mDate") { + let mtime = file.stat.mtime; + query.valueType = ValueType.Date; + addToDataMap(dataMap, xValueMap.get(-1), query, mtime); + } else if (target === "size") { + let size = file.stat.size; + addToDataMap(dataMap, xValueMap.get(-1), query, size); + } + } +} + export function collectDataFromDvField( content: string, query: Query, diff --git a/src/data.ts b/src/data.ts index 94a07a2..02364c1 100644 --- a/src/data.ts +++ b/src/data.ts @@ -7,6 +7,7 @@ export enum SearchType { Text, dvField, Table, + FileMeta, } export enum OutputType { diff --git a/src/helper.ts b/src/helper.ts index cec53dd..71b5a45 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -43,32 +43,6 @@ export function trimByChar(str: string, char: string) { : str.substring(first, str.length - last); } -export function getDateFromFilename(file: TFile, renderInfo: RenderInfo) { - let fileBaseName = file.basename; - - if ( - renderInfo.dateFormatPrefix && - fileBaseName.startsWith(renderInfo.dateFormatPrefix) - ) { - fileBaseName = fileBaseName.slice(renderInfo.dateFormatPrefix.length); - } - if ( - renderInfo.dateFormatSuffix && - fileBaseName.endsWith(renderInfo.dateFormatSuffix) - ) { - fileBaseName = fileBaseName.slice( - 0, - fileBaseName.length - renderInfo.dateFormatSuffix.length - ); - } - // console.log(fileBaseName); - - let fileDate = window.moment(fileBaseName, renderInfo.dateFormat, true); - // console.log(fileDate); - - return fileDate; -} - // Parsing export function parseFloatFromAny(toParse: any) { let value = null; diff --git a/src/main.ts b/src/main.ts index 13f7f7f..76fd0b4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -212,62 +212,68 @@ export default class Tracker extends Plugin { let skipThisFile = false; for (let xDatasetId of renderInfo.xDataset) { if (!xValueMap.has(xDatasetId)) { + let xDate = window.moment(""); if (xDatasetId === -1) { // Default using date in filename as xValue - let fileDate = helper.getDateFromFilename( + xDate = collecting.getDateFromFilename( file, renderInfo ); - // console.log(fileDate); - if (!fileDate.isValid()) { - // console.log("file " + file.basename + " rejected"); - skipThisFile = true; - } else { - // console.log("file " + file.basename + " accepted"); - if (renderInfo.startDate !== null) { - if (fileDate < renderInfo.startDate) { - skipThisFile = true; - } - } - if (renderInfo.endDate !== null) { - if (fileDate > renderInfo.endDate) { - skipThisFile = true; - } - } - } - - if (!skipThisFile) { - xValueMap.set( - -1, - fileDate.format(renderInfo.dateFormat) - ); - fileCounter++; - - // Get min/max date - if (fileCounter == 1) { - minDate = fileDate.clone(); - maxDate = fileDate.clone(); - } else { - if (fileDate < minDate) { - minDate = fileDate.clone(); - } - if (fileDate > maxDate) { - maxDate = fileDate.clone(); - } - } - } + // console.log(xDate); } else { let xDatasetQuery = renderInfo.queries[xDatasetId]; // console.log(xDatasetQuery); switch (xDatasetQuery.getType()) { case SearchType.Frontmatter: + xDate = collecting.getDateFromFrontmatter(); break; case SearchType.Tag: + xDate = collecting.getDateFromTag(); break; case SearchType.Text: + xDate = collecting.getDateFromText(); break; case SearchType.dvField: + xDate = collecting.getDateFromDvField(); break; + case SearchType.FileMeta: + xDate = collecting.getDateFromFileMeta(); + break; + } + } + + if (!xDate.isValid()) { + // console.log("file " + file.basename + " rejected"); + skipThisFile = true; + } else { + // console.log("file " + file.basename + " accepted"); + if (renderInfo.startDate !== null) { + if (xDate < renderInfo.startDate) { + skipThisFile = true; + } + } + if (renderInfo.endDate !== null) { + if (xDate > renderInfo.endDate) { + skipThisFile = true; + } + } + } + + if (!skipThisFile) { + xValueMap.set(-1, xDate.format(renderInfo.dateFormat)); + fileCounter++; + + // Get min/max date + if (fileCounter == 1) { + minDate = xDate.clone(); + maxDate = xDate.clone(); + } else { + if (xDate < minDate) { + minDate = xDate.clone(); + } + if (xDate > maxDate) { + maxDate = xDate.clone(); + } } } } @@ -344,6 +350,17 @@ export default class Tracker extends Plugin { ); } // Search text + // console.log("Search FileMeta"); + if (query.getType() === SearchType.FileMeta) { + collecting.collectDataFromFileMeta( + file, + query, + renderInfo, + dataMap, + xValueMap + ); + } // Search FileMeta + // console.log("Search dvField"); if (content && query.getType() === SearchType.dvField) { collecting.collectDataFromDvField( @@ -477,20 +494,20 @@ export default class Tracker extends Plugin { let date = window.moment(data, renderInfo.dateFormat, true); - if (!minDate.isValid() && !maxDate.isValid()) { - minDate = date.clone(); - maxDate = date.clone(); - } else { - if (date < minDate) { + if (date.isValid()) { + xValues.push(date); + + if (!minDate.isValid() && !maxDate.isValid()) { minDate = date.clone(); - } - if (date > maxDate) { maxDate = date.clone(); + } else { + if (date < minDate) { + minDate = date.clone(); + } + if (date > maxDate) { + maxDate = date.clone(); + } } - } - - if (date.isValid()) { - xValues.push(date); } else { xValues.push(null); } diff --git a/src/parsing.ts b/src/parsing.ts index e79aa6e..9692913 100644 --- a/src/parsing.ts +++ b/src/parsing.ts @@ -41,7 +41,8 @@ function validateSearchType(searchType: string): boolean { searchType === "frontmatter" || searchType === "wiki" || searchType === "dvField" || - searchType === "table" + searchType === "table" || + searchType === "fileMeta" ) { return true; } @@ -806,6 +807,9 @@ export function getRenderInfoFromYaml( case "table": searchType.push(SearchType.Table); break; + case "fileMeta": + searchType.push(SearchType.FileMeta); + break; } } // Currently, we don't allow type 'table' used with other types From 9bad5d1d027c77de6655c2ad59c307256b1b0035 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Tue, 1 Jun 2021 18:18:09 +0800 Subject: [PATCH 035/402] Get date from frontmatter and fileMeta --- examples/Test FileMeta.md | 31 ++++++++++++-- examples/data/data1.md | 33 +++++++++++++++ examples/data/data2.md | 35 ++++++++++++++++ examples/data/data3.md | 33 +++++++++++++++ src/collecting.ts | 86 +++++++++++++++++++++++++++++++-------- src/data.ts | 2 + src/main.ts | 25 +++++++++--- 7 files changed, 220 insertions(+), 25 deletions(-) create mode 100644 examples/data/data1.md create mode 100644 examples/data/data2.md create mode 100644 examples/data/data3.md diff --git a/examples/Test FileMeta.md b/examples/Test FileMeta.md index a3a6710..4b0e7f8 100644 --- a/examples/Test FileMeta.md +++ b/examples/Test FileMeta.md @@ -1,11 +1,34 @@ # Test FileMeta - +Track the size variation of diaries ``` tracker searchType: fileMeta searchTarget: size folder: diary +endDate: 2021-01-31 line: - title: File Size + title: File Size Variation yAxisLabel: Size - yAxisUnit: byte -``` \ No newline at end of file + yAxisUnit: bytes +``` + +Use file created dates (cDate) as x values +``` tracker +searchType: fileMeta, dvField +searchTarget: cDate, dataviewTarget +xDataset: 0 +folder: data +line: + fillGap: true +``` + +Use date variables from front matter +``` tracker +searchType: frontmatter, dvField +searchTarget: date, dataviewTarget +xDataset: 0 +folder: data +line: + title: Dataview Inline Field +``` + + diff --git a/examples/data/data1.md b/examples/data/data1.md new file mode 100644 index 0000000..d1f5380 --- /dev/null +++ b/examples/data/data1.md @@ -0,0 +1,33 @@ +--- +date: 2021-01-01 +--- + +#weight:74.1kg + +#exercise-pushup:31 +#exercise-plank:109sec + + + + + +#finance/bank1:-3.5USD + +[[todo_family|To-Do @Family]] +[[todo_work|To-Do @Work]] + +obsidian-tracker+1@gmail.com + +weightlifting: 18 + +dataviewTarget:: 48 +- Make Progress:: 29 +- Make-Progress:: 88 +dataviewTarget1:: 48/29 +dataviewTarget2:: 29 @ 88 +dataviewTarget3:: 29, 88 + +#sin:0.00000/0.20791/0.40674/0.58779/0.74314/0.86603/0.95106/0.99452/0.99452 + +#sinsquare:0.00000/0.04323/0.16543/0.34549/0.55226/0.75000/0.90451/0.98907/0.98907 + diff --git a/examples/data/data2.md b/examples/data/data2.md new file mode 100644 index 0000000..b9e9102 --- /dev/null +++ b/examples/data/data2.md @@ -0,0 +1,35 @@ +--- +date: 2021-01-02 +--- + +#weight:62.8kg + +#exercise-pushup:46 +#exercise-plank:101sec + +#meditation + + + +#clean-up + +#finance/bank1:-3.8USD + +[[todo_family|To-Do @Family]] +[[todo_work|To-Do @Work]] + +obsidian-tracker@gmail.com + +weightlifting: 12 + +dataviewTarget:: 27 +- Make Progress:: 12 +- Make-Progress:: 55 +dataviewTarget1:: 27/12 +dataviewTarget2:: 12 @ 55 +dataviewTarget3:: 12, 55 + +#sin:0.20791/0.40674/0.58779/0.74314/0.86603/0.95106/0.99452/0.99452/0.95106 + +#sinsquare:0.04323/0.16543/0.34549/0.55226/0.75000/0.90451/0.98907/0.98907/0.90451 + diff --git a/examples/data/data3.md b/examples/data/data3.md new file mode 100644 index 0000000..0aa5108 --- /dev/null +++ b/examples/data/data3.md @@ -0,0 +1,33 @@ +--- +date: 2021-01-03 +--- + +#weight:76.8kg + +#exercise-pushup:45 +#exercise-plank:43sec + + +⭐⭐⭐ + + +#finance/bank1:-2.6USD + +[[todo_family|To-Do @Family]] +[[todo_work|To-Do @Work]] + +obsidian-tracker@gmail.com + +weightlifting: 12 + +dataviewTarget:: 88 +- Make Progress:: 5 +- Make-Progress:: 56 +dataviewTarget1:: 88/5 +dataviewTarget2:: 5 @ 56 +dataviewTarget3:: 5, 56 + +#sin:0.40674/0.58779/0.74314/0.86603/0.95106/0.99452/0.99452/0.95106/0.86603 + +#sinsquare:0.16543/0.34549/0.55226/0.75000/0.90451/0.98907/0.98907/0.90451/0.75000 + diff --git a/src/collecting.ts b/src/collecting.ts index 99f16fc..8059ed5 100644 --- a/src/collecting.ts +++ b/src/collecting.ts @@ -37,8 +37,24 @@ export function getDateFromFilename(file: TFile, renderInfo: RenderInfo) { return fileDate; } -export function getDateFromFrontmatter() { +export function getDateFromFrontmatter( + fileCache: CachedMetadata, + query: Query, + renderInfo: RenderInfo +) { + // console.log("getDateFromFrontmatter"); + let date = window.moment(""); + + let frontMatter = fileCache.frontmatter; + if (frontMatter) { + if (helper.deepValue(frontMatter, query.getTarget())) { + let strDate = helper.deepValue(frontMatter, query.getTarget()); + date = window.moment(strDate, renderInfo.dateFormat, true); + // console.log(date); + } + } + return date; } @@ -57,8 +73,39 @@ export function getDateFromDvField() { return date; } -export function getDateFromFileMeta() { +export function getDateFromFileMeta( + file: TFile, + query: Query, + renderInfo: RenderInfo +) { + // console.log("getDateFromFileMeta"); + let date = window.moment(""); + + if (file && file instanceof TFile) { + // console.log(file.stat); + + let target = query.getTarget(); + if (target === "cDate") { + let ctime = file.stat.ctime; + date = window.moment( + window.moment(ctime).format(renderInfo.dateFormat), + renderInfo.dateFormat, + true + ); + } else if (target === "mDate") { + let mtime = file.stat.mtime; + date = window.moment( + window.moment(mtime).format(renderInfo.dateFormat), + renderInfo.dateFormat, + true + ); + date = window.moment(new Date(mtime)); + } else if (target === "size") { + } + } + + // console.log(date); return date; } @@ -121,7 +168,8 @@ export function collectDataFromFrontmatterTag( if (tagExist) { value = tagMeasure; } - addToDataMap(dataMap, xValueMap.get(-1), query, value); + let xValue = xValueMap.get(renderInfo.xDataset[query.getId()]); + addToDataMap(dataMap, xValue, query, value); } } } @@ -144,7 +192,8 @@ export function collectDataFromFrontmatterKey( if (retParse.type === ValueType.Time) { query.valueType = ValueType.Time; } - addToDataMap(dataMap, xValueMap.get(-1), query, retParse.value); + let xValue = xValueMap.get(renderInfo.xDataset[query.getId()]); + addToDataMap(dataMap, xValue, query, retParse.value); } } else if ( query.getParentTarget() && @@ -181,12 +230,10 @@ export function collectDataFromFrontmatterKey( if (retParse.type === ValueType.Time) { query.valueType = ValueType.Time; } - addToDataMap( - dataMap, - xValueMap.get(-1), - query, - retParse.value + let xValue = xValueMap.get( + renderInfo.xDataset[query.getId()] ); + addToDataMap(dataMap, xValue, query, retParse.value); } } } @@ -215,7 +262,8 @@ export function collectDataFromWiki( if (linkExist) { linkValue = linkMeasure; } - addToDataMap(dataMap, xValueMap.get(-1), query, linkValue); + let xValue = xValueMap.get(renderInfo.xDataset[query.getId()]); + addToDataMap(dataMap, xValue, query, linkValue); } export function collectDataFromInlineTag( @@ -300,7 +348,8 @@ export function collectDataFromInlineTag( if (tagExist) { value = tagMeasure; } - addToDataMap(dataMap, xValueMap.get(-1), query, value); + let xValue = xValueMap.get(renderInfo.xDataset[query.getId()]); + addToDataMap(dataMap, xValue, query, value); } export function collectDataFromText( @@ -346,7 +395,8 @@ export function collectDataFromText( } if (textExist) { - addToDataMap(dataMap, xValueMap.get(-1), query, textMeasure); + let xValue = xValueMap.get(renderInfo.xDataset[query.getId()]); + addToDataMap(dataMap, xValue, query, textMeasure); } } @@ -366,14 +416,17 @@ export function collectDataFromFileMeta( if (target === "cDate") { let ctime = file.stat.ctime; query.valueType = ValueType.Date; - addToDataMap(dataMap, xValueMap.get(-1), query, ctime); + let xValue = xValueMap.get(renderInfo.xDataset[query.getId()]); + addToDataMap(dataMap, xValue, query, ctime); } else if (target === "mDate") { let mtime = file.stat.mtime; query.valueType = ValueType.Date; - addToDataMap(dataMap, xValueMap.get(-1), query, mtime); + let xValue = xValueMap.get(renderInfo.xDataset[query.getId()]); + addToDataMap(dataMap, xValue, query, mtime); } else if (target === "size") { let size = file.stat.size; - addToDataMap(dataMap, xValueMap.get(-1), query, size); + let xValue = xValueMap.get(renderInfo.xDataset[query.getId()]); + addToDataMap(dataMap, xValue, query, size); } } } @@ -461,5 +514,6 @@ export function collectDataFromDvField( if (tagExist) { value = tagMeasure; } - addToDataMap(dataMap, xValueMap.get(-1), query, value); + let xValue = xValueMap.get(renderInfo.xDataset[query.getId()]); + addToDataMap(dataMap, xValue, query, value); } diff --git a/src/data.ts b/src/data.ts index 02364c1..e3f99dc 100644 --- a/src/data.ts +++ b/src/data.ts @@ -352,7 +352,9 @@ export class Datasets implements IterableIterator { constructor(startDate: Moment, endDate: Moment) { this.dates = []; this.datasets = []; + let cData = startDate.creationData(); + // console.log(cData); const dateFormat = cData.format.toString(); for ( let curDate = startDate.clone(); diff --git a/src/main.ts b/src/main.ts index 76fd0b4..b706e85 100644 --- a/src/main.ts +++ b/src/main.ts @@ -208,8 +208,9 @@ export default class Tracker extends Plugin { } // Get xValue and add it into xValueMap for later use - let xValueMap: XValueMap = new Map(); // queryId: xValue + let xValueMap: XValueMap = new Map(); // queryId: xValue for this file let skipThisFile = false; + // console.log(renderInfo.xDataset); for (let xDatasetId of renderInfo.xDataset) { if (!xValueMap.has(xDatasetId)) { let xDate = window.moment(""); @@ -225,7 +226,11 @@ export default class Tracker extends Plugin { // console.log(xDatasetQuery); switch (xDatasetQuery.getType()) { case SearchType.Frontmatter: - xDate = collecting.getDateFromFrontmatter(); + xDate = collecting.getDateFromFrontmatter( + fileCache, + xDatasetQuery, + renderInfo + ); break; case SearchType.Tag: xDate = collecting.getDateFromTag(); @@ -237,13 +242,17 @@ export default class Tracker extends Plugin { xDate = collecting.getDateFromDvField(); break; case SearchType.FileMeta: - xDate = collecting.getDateFromFileMeta(); + xDate = collecting.getDateFromFileMeta( + file, + xDatasetQuery, + renderInfo + ); break; } } if (!xDate.isValid()) { - // console.log("file " + file.basename + " rejected"); + // console.log("Invalid xDate"); skipThisFile = true; } else { // console.log("file " + file.basename + " accepted"); @@ -260,7 +269,10 @@ export default class Tracker extends Plugin { } if (!skipThisFile) { - xValueMap.set(-1, xDate.format(renderInfo.dateFormat)); + xValueMap.set( + xDatasetId, + xDate.format(renderInfo.dateFormat) + ); fileCounter++; // Get min/max date @@ -280,6 +292,8 @@ export default class Tracker extends Plugin { } if (skipThisFile) return; // console.log(xValueMap); + // console.log(`minDate: ${minDate}`); + // console.log(`maxDate: ${maxDate}`); // Loop over queries let yDatasetQueries = renderInfo.queries.filter((q) => { @@ -375,6 +389,7 @@ export default class Tracker extends Plugin { await Promise.all(loopQueryPromises); }); await Promise.all(loopFilePromises); + // console.log(dataMap); // Collect data from a file, one file contains full dataset let tableQueries = renderInfo.queries.filter( From a61c52c24b61847e7390b8e85282f5add837d2e7 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Tue, 1 Jun 2021 19:28:26 +0800 Subject: [PATCH 036/402] Set version to 1.6.0 --- manifest.json | 2 +- package.json | 2 +- versions.json | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/manifest.json b/manifest.json index fb1591c..2179fa0 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-tracker", "name": "Tracker", - "version": "1.5.1", + "version": "1.6.0", "minAppVersion": "0.9.12", "description": "A plugin tracks occurrences and numbers in your notes", "author": "pyrochlore", diff --git a/package.json b/package.json index 324368c..4253915 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-tracker", - "version": "1.5.1", + "version": "1.6.0", "description": "A plugin tracks occurrences and numbers in your notes", "main": "main.js", "scripts": { diff --git a/versions.json b/versions.json index 5b3014a..c346c9d 100644 --- a/versions.json +++ b/versions.json @@ -9,5 +9,6 @@ "1.4.0": "0.9.12", "1.4.1": "0.9.12", "1.5.0": "0.9.12", - "1.5.1": "0.9.12" + "1.5.1": "0.9.12", + "1.6.0": "0.9.12" } From 9b28048157a5027dda4051e9bd6236b96d7bd0d1 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Tue, 1 Jun 2021 19:40:19 +0800 Subject: [PATCH 037/402] Fix getDateFromFileMeta for mDate --- examples/Test FileMeta.md | 8 ++++---- src/collecting.ts | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/Test FileMeta.md b/examples/Test FileMeta.md index 4b0e7f8..9674291 100644 --- a/examples/Test FileMeta.md +++ b/examples/Test FileMeta.md @@ -21,14 +21,14 @@ line: fillGap: true ``` -Use date variables from front matter +Use file modified dates (mDate) as x values ``` tracker -searchType: frontmatter, dvField -searchTarget: date, dataviewTarget +searchType: fileMeta, dvField +searchTarget: mDate, dataviewTarget xDataset: 0 folder: data line: - title: Dataview Inline Field + fillGap: true ``` diff --git a/src/collecting.ts b/src/collecting.ts index 8059ed5..7820ce9 100644 --- a/src/collecting.ts +++ b/src/collecting.ts @@ -100,7 +100,6 @@ export function getDateFromFileMeta( renderInfo.dateFormat, true ); - date = window.moment(new Date(mtime)); } else if (target === "size") { } } From 7f1e01548a9a54f3373d9612975fe4c4ceb47318 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Tue, 1 Jun 2021 23:03:26 +0800 Subject: [PATCH 038/402] Enable getDateFromXXXX; Add examples for testing xDataset; --- examples/TestXDataset.md | 55 ++++++++++++++++ examples/data/Set-FileDates.ps1 | 6 ++ examples/data/data1.md | 31 ++------- examples/data/data2.md | 33 ++-------- examples/data/data3.md | 31 ++------- src/collecting.ts | 112 +++++++++++++++++++++++++++++++- src/main.ts | 18 ++++- 7 files changed, 197 insertions(+), 89 deletions(-) create mode 100644 examples/TestXDataset.md create mode 100644 examples/data/Set-FileDates.ps1 diff --git a/examples/TestXDataset.md b/examples/TestXDataset.md new file mode 100644 index 0000000..321637e --- /dev/null +++ b/examples/TestXDataset.md @@ -0,0 +1,55 @@ +# Test X Dataset + +Use file created dates (cDate) as x values +``` tracker +searchType: fileMeta, dvField +searchTarget: cDate, dataviewTarget +xDataset: 0 +folder: data +line: + fillGap: true +``` + +Use dates from front matter as x values +``` tracker +searchType: frontmatter, dvField +searchTarget: date, dataviewTarget +xDataset: 0 +folder: data +line: + title: Dataview Inline Field + fillGap: true +``` + +Use dates from date-attached tags as x values +``` tracker +searchType: tag, dvField +searchTarget: date, dataviewTarget +xDataset: 0 +folder: data +line: + title: Dataview Inline Field + fillGap: true +``` + +Ue dates from Dataview inline fields as x values +``` tracker +searchType: dvField, dvField +searchTarget: date, dataviewTarget +xDataset: 0 +folder: data +line: + title: Dataview Inline Field + fillGap: true +``` + +Ue dates from formatted text as x values +``` tracker +searchType: text, dvField +searchTarget: 'Today\sis\s(?([0-9]{4})-([0-9]{2})-([0-9]{2})), dataviewTarget' +xDataset: 0 +folder: data +line: + title: Dataview Inline Field + fillGap: true +``` \ No newline at end of file diff --git a/examples/data/Set-FileDates.ps1 b/examples/data/Set-FileDates.ps1 new file mode 100644 index 0000000..1f9bd2b --- /dev/null +++ b/examples/data/Set-FileDates.ps1 @@ -0,0 +1,6 @@ +(Get-Item "./data1.md").CreationTime=("25 May 2021 17:00:00") +(Get-Item "./data2.md").CreationTime=("27 May 2021 17:00:00") +(Get-Item "./data3.md").CreationTime=("29 May 2021 17:00:00") +(Get-Item "./data1.md").LastWriteTime=("25 May 2021 17:00:00") +(Get-Item "./data2.md").LastWriteTime=("27 May 2021 17:00:00") +(Get-Item "./data3.md").LastWriteTime=("29 May 2021 17:00:00") \ No newline at end of file diff --git a/examples/data/data1.md b/examples/data/data1.md index d1f5380..0e221e2 100644 --- a/examples/data/data1.md +++ b/examples/data/data1.md @@ -2,32 +2,9 @@ date: 2021-01-01 --- -#weight:74.1kg +#date:2021-01-01 +date:: 2021-01-01 -#exercise-pushup:31 -#exercise-plank:109sec - - - - - -#finance/bank1:-3.5USD - -[[todo_family|To-Do @Family]] -[[todo_work|To-Do @Work]] - -obsidian-tracker+1@gmail.com - -weightlifting: 18 - -dataviewTarget:: 48 -- Make Progress:: 29 -- Make-Progress:: 88 -dataviewTarget1:: 48/29 -dataviewTarget2:: 29 @ 88 -dataviewTarget3:: 29, 88 - -#sin:0.00000/0.20791/0.40674/0.58779/0.74314/0.86603/0.95106/0.99452/0.99452 - -#sinsquare:0.00000/0.04323/0.16543/0.34549/0.55226/0.75000/0.90451/0.98907/0.98907 +dataviewTarget:: 50 +Today is 2021-01-01 \ No newline at end of file diff --git a/examples/data/data2.md b/examples/data/data2.md index b9e9102..e92ab3d 100644 --- a/examples/data/data2.md +++ b/examples/data/data2.md @@ -2,34 +2,9 @@ date: 2021-01-02 --- -#weight:62.8kg +#date:2021-01-03 +date:: 2021-01-05 -#exercise-pushup:46 -#exercise-plank:101sec - -#meditation - - - -#clean-up - -#finance/bank1:-3.8USD - -[[todo_family|To-Do @Family]] -[[todo_work|To-Do @Work]] - -obsidian-tracker@gmail.com - -weightlifting: 12 - -dataviewTarget:: 27 -- Make Progress:: 12 -- Make-Progress:: 55 -dataviewTarget1:: 27/12 -dataviewTarget2:: 12 @ 55 -dataviewTarget3:: 12, 55 - -#sin:0.20791/0.40674/0.58779/0.74314/0.86603/0.95106/0.99452/0.99452/0.95106 - -#sinsquare:0.04323/0.16543/0.34549/0.55226/0.75000/0.90451/0.98907/0.98907/0.90451 +dataviewTarget:: 30 +Today is 2021-01-10 \ No newline at end of file diff --git a/examples/data/data3.md b/examples/data/data3.md index 0aa5108..ed4b32d 100644 --- a/examples/data/data3.md +++ b/examples/data/data3.md @@ -2,32 +2,9 @@ date: 2021-01-03 --- -#weight:76.8kg +#date:2021-01-05 +date:: 2021-01-09 -#exercise-pushup:45 -#exercise-plank:43sec - - -⭐⭐⭐ - - -#finance/bank1:-2.6USD - -[[todo_family|To-Do @Family]] -[[todo_work|To-Do @Work]] - -obsidian-tracker@gmail.com - -weightlifting: 12 - -dataviewTarget:: 88 -- Make Progress:: 5 -- Make-Progress:: 56 -dataviewTarget1:: 88/5 -dataviewTarget2:: 5 @ 56 -dataviewTarget3:: 5, 56 - -#sin:0.40674/0.58779/0.74314/0.86603/0.95106/0.99452/0.99452/0.95106/0.86603 - -#sinsquare:0.16543/0.34549/0.55226/0.75000/0.90451/0.98907/0.98907/0.90451/0.75000 +dataviewTarget:: 80 +Today is 2021-01-20 \ No newline at end of file diff --git a/src/collecting.ts b/src/collecting.ts index 7820ce9..e110795 100644 --- a/src/collecting.ts +++ b/src/collecting.ts @@ -37,6 +37,8 @@ export function getDateFromFilename(file: TFile, renderInfo: RenderInfo) { return fileDate; } +// Not support multiple targets +// May merge with collectDataFromFrontmatterKey export function getDateFromFrontmatter( fileCache: CachedMetadata, query: Query, @@ -58,21 +60,125 @@ export function getDateFromFrontmatter( return date; } -export function getDateFromTag() { +// Inline tags only +// Not support multiple targets +// May merge with collectDataFromInlineTag +export function getDateFromTag( + content: string, + query: Query, + renderInfo: RenderInfo +) { + // console.log("getDateFromTag"); + let date = window.moment(""); + + let tagName = query.getTarget(); + if (query.getParentTarget()) { + tagName = query.getParentTarget(); // use parent tag name for multiple values + } + // console.log(tagName); + let strHashtagRegex = + "(^|\\s)#" + + tagName + + "(\\/[\\w-]+)*(:(?[\\d\\.\\/-]*)[a-zA-Z]*)?([\\.!,\\?;~-]*)?(\\s|$)"; + // console.log(strHashtagRegex); + let hashTagRegex = new RegExp(strHashtagRegex, "gm"); + let match; + while ((match = hashTagRegex.exec(content))) { + // console.log(match); + if ( + typeof match.groups !== "undefined" && + typeof match.groups.values !== "undefined" + ) { + let strDate = match.groups.values; + date = window.moment(strDate, renderInfo.dateFormat, true); + if (date.isValid()) { + break; + } + } + } + // console.log(date); return date; } -export function getDateFromText() { +// Not support multiple targets +// May merge with colllectDataFromText +export function getDateFromText( + content: string, + query: Query, + renderInfo: RenderInfo +) { + // console.log("getDateFromText"); + let date = window.moment(""); + + let strTextRegex = query.getTarget(); + // console.log(strTextRegex); + let textRegex = new RegExp(strTextRegex, "gm"); + let match; + let textMeasure = 0.0; + let textExist = false; + while ((match = textRegex.exec(content))) { + // console.log(match); + if ( + typeof match.groups !== "undefined" && + typeof match.groups.value !== "undefined" + ) { + let strDate = match.groups.value.trim(); + // console.log(strDate); + date = window.moment(strDate, renderInfo.dateFormat, true); + if (date.isValid()) { + break; + } + } + } + // console.log(date); return date; } -export function getDateFromDvField() { +// Not support multiple targets +// May merge with colllectDataFromDvField +export function getDateFromDvField( + content: string, + query: Query, + renderInfo: RenderInfo +) { + // console.log("getDateFromDvField"); + let date = window.moment(""); + + let dvTarget = query.getTarget(); + if (query.getParentTarget()) { + dvTarget = query.getParentTarget(); // use parent tag name for multiple values + } + // Test this in Regex101 + // (^|\s)\*{0,2}dvTarget\*{0,2}(::\s*(?[\d\.\/\-\w,@;\s]*))(\s|$) + let strHashtagRegex = + "(^|\\s)\\*{0,2}" + + dvTarget + + "\\*{0,2}(::\\s*(?[\\d\\.\\/\\-\\w,@;\\s]*))(\\s|$)"; + // console.log(strHashtagRegex); + let hashTagRegex = new RegExp(strHashtagRegex, "gm"); + let match; + while ((match = hashTagRegex.exec(content))) { + // console.log(match); + if ( + typeof match.groups !== "undefined" && + typeof match.groups.values !== "undefined" + ) { + let strDate = match.groups.values.trim(); + date = window.moment(strDate, renderInfo.dateFormat, true); + if (date.isValid()) { + break; + } + } + } + // console.log(date); return date; } +// Not support multiple targets +// May merge with colllectDataFromFileMeta export function getDateFromFileMeta( file: TFile, query: Query, diff --git a/src/main.ts b/src/main.ts index b706e85..5ac6bbc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -233,13 +233,25 @@ export default class Tracker extends Plugin { ); break; case SearchType.Tag: - xDate = collecting.getDateFromTag(); + xDate = collecting.getDateFromTag( + content, + xDatasetQuery, + renderInfo + ); break; case SearchType.Text: - xDate = collecting.getDateFromText(); + xDate = collecting.getDateFromText( + content, + xDatasetQuery, + renderInfo + ); break; case SearchType.dvField: - xDate = collecting.getDateFromDvField(); + xDate = collecting.getDateFromDvField( + content, + xDatasetQuery, + renderInfo + ); break; case SearchType.FileMeta: xDate = collecting.getDateFromFileMeta( From 15001141cbd9cb4ca8cb49edb7d04d9539d7f2f5 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Tue, 1 Jun 2021 23:23:48 +0800 Subject: [PATCH 039/402] Update examples --- examples/ErrorMessages.md | 5 ++++- examples/HabitTracker.md | 6 +++--- examples/MoodTracker.md | 1 + examples/Test FileMeta.md | 1 + examples/TestBarChart.md | 2 ++ examples/TestBullet.md | 4 +++- examples/TestCalendar.md | 4 +++- examples/TestDateFormats.md | 1 + examples/TestDvField.md | 4 +++- examples/TestFrontmatter.md | 2 ++ examples/TestLegends.md | 2 ++ examples/TestMultipleTargesMultipleValues.md | 12 +++++++++--- examples/TestScalingAndPositioing.md | 4 +++- examples/TestTable.md | 1 + examples/TestTimeValues.md | 4 +++- examples/TestXDataset.md | 4 +++- examples/diary/2021-01-01.md | 3 +++ examples/diary/2021-01-02.md | 3 +++ examples/diary/2021-01-03.md | 3 +++ examples/diary/2021-01-04.md | 3 +++ examples/diary/2021-01-05.md | 3 +++ examples/diary/2021-01-06.md | 3 +++ examples/diary/2021-01-07.md | 3 +++ examples/diary/2021-01-08.md | 3 +++ examples/diary/2021-01-09.md | 3 +++ examples/diary/2021-01-10.md | 3 +++ examples/diary/2021-01-11.md | 3 +++ examples/diary/2021-01-12.md | 3 +++ examples/diary/2021-01-13.md | 3 +++ examples/diary/2021-01-14.md | 3 +++ examples/diary/2021-01-15.md | 3 +++ examples/diary/2021-01-16.md | 3 +++ examples/diary/2021-01-17.md | 3 +++ examples/diary/2021-01-18.md | 3 +++ examples/diary/2021-01-19.md | 3 +++ examples/diary/2021-01-20.md | 3 +++ examples/diary/2021-01-21.md | 3 +++ examples/diary/2021-01-22.md | 3 +++ examples/diary/2021-01-23.md | 3 +++ examples/diary/2021-01-24.md | 3 +++ examples/diary/2021-01-25.md | 3 +++ examples/diary/2021-01-26.md | 3 +++ examples/diary/2021-01-27.md | 3 +++ examples/diary/2021-01-28.md | 3 +++ examples/diary/2021-01-29.md | 3 +++ examples/diary/2021-01-30.md | 3 +++ examples/diary/2021-01-31.md | 3 +++ examples/example_generator.ts | 4 ++++ 48 files changed, 141 insertions(+), 13 deletions(-) diff --git a/examples/ErrorMessages.md b/examples/ErrorMessages.md index 37535b5..fb2212e 100644 --- a/examples/ErrorMessages.md +++ b/examples/ErrorMessages.md @@ -135,4 +135,7 @@ searchTarget: data/Tables[4][0], data/Tables[4][1] xDataset: 0 line: lineColor: none, yellow -``` \ No newline at end of file +``` + + +Please also check those search targets in markdown files under folder 'diary' and 'data'. \ No newline at end of file diff --git a/examples/HabitTracker.md b/examples/HabitTracker.md index 9f69a6d..7566007 100644 --- a/examples/HabitTracker.md +++ b/examples/HabitTracker.md @@ -35,7 +35,7 @@ line: ``` ## Summary -Meditation +### Meditation ``` tracker searchType: tag searchTarget: meditation @@ -44,7 +44,7 @@ summary: template: "Longest Streak: {{maxStreak}} day(s)\nLongest Breaks: {{maxBreaks}} day(s)\nLast streak: {{lastStreak}} day(s)" ``` -CleanUp +### CleanUp ``` tracker searchType: tag searchTarget: clean-up @@ -54,7 +54,7 @@ summary: template: "Last Break: {{lastBreaks}} day(s)" ``` -Work log +### Work log ``` tracker searchType: tag searchTarget: work_log diff --git a/examples/MoodTracker.md b/examples/MoodTracker.md index d3434ab..48c83c8 100644 --- a/examples/MoodTracker.md +++ b/examples/MoodTracker.md @@ -11,3 +11,4 @@ line: lineColor: "#d65d0e" ``` +Please also check those search targets in markdown files under folder 'diary'. \ No newline at end of file diff --git a/examples/Test FileMeta.md b/examples/Test FileMeta.md index 9674291..d1436a9 100644 --- a/examples/Test FileMeta.md +++ b/examples/Test FileMeta.md @@ -31,4 +31,5 @@ line: fillGap: true ``` +Please also check those search targets in markdown files under folder 'diary' and 'data'. diff --git a/examples/TestBarChart.md b/examples/TestBarChart.md index 8af3e97..3fb2478 100644 --- a/examples/TestBarChart.md +++ b/examples/TestBarChart.md @@ -52,3 +52,5 @@ bar: yMin: 0 barColor: yellow, red, green, blue, orange, white ``` + +Please also check those search targets in markdown files under folder 'diary'. \ No newline at end of file diff --git a/examples/TestBullet.md b/examples/TestBullet.md index 45604d0..d4eca5d 100644 --- a/examples/TestBullet.md +++ b/examples/TestBullet.md @@ -38,4 +38,6 @@ bullet: showMarker: true markerValue: 80 markerColor: red -``` \ No newline at end of file +``` + +Please also check those search targets in markdown files under folder 'diary'. \ No newline at end of file diff --git a/examples/TestCalendar.md b/examples/TestCalendar.md index cdf2bbd..dac62be 100644 --- a/examples/TestCalendar.md +++ b/examples/TestCalendar.md @@ -6,4 +6,6 @@ searchTarget: meditation folder: diary month: startWeekOn: -``` \ No newline at end of file +``` + +Please also check those search targets in markdown files under folder 'diary'. \ No newline at end of file diff --git a/examples/TestDateFormats.md b/examples/TestDateFormats.md index b529689..6c03e4a 100644 --- a/examples/TestDateFormats.md +++ b/examples/TestDateFormats.md @@ -128,3 +128,4 @@ line: lineColor: yellow ``` +Please also check those search targets in markdown files under folder 'diary'. diff --git a/examples/TestDvField.md b/examples/TestDvField.md index f78346b..7fb7877 100644 --- a/examples/TestDvField.md +++ b/examples/TestDvField.md @@ -84,4 +84,6 @@ endDate: 2021-01-31 line: title: dvField lineColor: green, red -``` \ No newline at end of file +``` + +Please also check those search targets in markdown files under folder 'diary'. \ No newline at end of file diff --git a/examples/TestFrontmatter.md b/examples/TestFrontmatter.md index 076117c..c8f547b 100644 --- a/examples/TestFrontmatter.md +++ b/examples/TestFrontmatter.md @@ -9,3 +9,5 @@ endDate: 2021-01-31 line: title: Deep Values ``` + +Please also check those search targets in markdown files under folder 'diary'. \ No newline at end of file diff --git a/examples/TestLegends.md b/examples/TestLegends.md index 96a5bde..17fffb2 100644 --- a/examples/TestLegends.md +++ b/examples/TestLegends.md @@ -192,3 +192,5 @@ bar: showLegend: true legendPosition: right ``` + +Please also check those search targets in markdown files under folder 'diary'. \ No newline at end of file diff --git a/examples/TestMultipleTargesMultipleValues.md b/examples/TestMultipleTargesMultipleValues.md index 1a8607c..bd6a330 100644 --- a/examples/TestMultipleTargesMultipleValues.md +++ b/examples/TestMultipleTargesMultipleValues.md @@ -36,6 +36,7 @@ Retrieve values from a multiple-values-attached tag searchType: tag searchTarget: sin[0], sin[1], sin[2], sin[3], sin[4], sin[5], sin[6], sin[7], sin[8] folder: diary +datasetName: Sin1, Sin2, Sin3, Sin4, Sin5, Sin6, Sin7, Sin8, Sin9 startDate: 2021-01-01 endDate: 2021-01-31 line: @@ -53,6 +54,7 @@ Multiple values separated by slash (/) searchType: frontmatter searchTarget: bloodpressure[0], bloodpressure[1] folder: diary +datasetName: systolic, diastolic startDate: 2021-01-01 endDate: 2021-01-31 line: @@ -66,6 +68,7 @@ Multiple values separated by comma (,) searchType: frontmatter searchTarget: bloodpressure1[0], bloodpressure1[1] folder: diary +datasetName: systolic, diastolic startDate: 2021-01-01 endDate: 2021-01-31 separator: comma @@ -80,6 +83,7 @@ Values from array searchType: frontmatter searchTarget: bloodpressure2[0], bloodpressure2[1] folder: diary +datasetName: systolic, diastolic startDate: 2021-01-01 endDate: 2021-01-31 line: @@ -88,12 +92,12 @@ line: showLegend: true ``` -Values in nested parameters -Not works yet. +Values from nested keys ``` tracker searchType: frontmatter -searchTarget: blood-pressure[0], blood-pressure[1] +searchTarget: bp.systolic, bp.diastolic folder: diary +datasetName: systolic, diastolic startDate: 2021-01-01 endDate: 2021-01-31 line: @@ -180,3 +184,5 @@ line: yAxisLocation: none, left, right lineColor: none, yellow, red ``` + +Please also check those search targets in markdown files under folder 'diary' and 'data'. \ No newline at end of file diff --git a/examples/TestScalingAndPositioing.md b/examples/TestScalingAndPositioing.md index b7fc267..626fb36 100644 --- a/examples/TestScalingAndPositioing.md +++ b/examples/TestScalingAndPositioing.md @@ -98,4 +98,6 @@ line: title: PushUp yAxisLabel: Count lineColor: "#d65d0e" -``` \ No newline at end of file +``` + +Please also check those search targets in markdown files under folder 'diary'. \ No newline at end of file diff --git a/examples/TestTable.md b/examples/TestTable.md index e88bc8c..4a87434 100644 --- a/examples/TestTable.md +++ b/examples/TestTable.md @@ -53,3 +53,4 @@ line: lineColor: none, yellow ``` +Please also check those search targets in markdown file /data/Tables. diff --git a/examples/TestTimeValues.md b/examples/TestTimeValues.md index dc72220..66f5506 100644 --- a/examples/TestTimeValues.md +++ b/examples/TestTimeValues.md @@ -28,4 +28,6 @@ line: lineColor: yellow, red showPoint: true showLegend: true -``` \ No newline at end of file +``` + +Please also check those search targets in markdown files under folder 'diary'. \ No newline at end of file diff --git a/examples/TestXDataset.md b/examples/TestXDataset.md index 321637e..288284c 100644 --- a/examples/TestXDataset.md +++ b/examples/TestXDataset.md @@ -52,4 +52,6 @@ folder: data line: title: Dataview Inline Field fillGap: true -``` \ No newline at end of file +``` + +Please also check those search targets in markdown files under folder 'data'. \ No newline at end of file diff --git a/examples/diary/2021-01-01.md b/examples/diary/2021-01-01.md index b118da9..8144865 100644 --- a/examples/diary/2021-01-01.md +++ b/examples/diary/2021-01-01.md @@ -4,6 +4,9 @@ mood: 8 bloodpressure: 184.4/118.8 bloodpressure1: 184.4, 118.8 bloodpressure2: [184.4, 118.8] +bp: + systolic: 184.4 + diastolic: 118.8 clock-in: 10:45 clock-out: 20:51 sleep: 10:11 pm/7:5 am diff --git a/examples/diary/2021-01-02.md b/examples/diary/2021-01-02.md index e97b2b4..6a37be3 100644 --- a/examples/diary/2021-01-02.md +++ b/examples/diary/2021-01-02.md @@ -4,6 +4,9 @@ mood: 1 bloodpressure: 180.8/120.6 bloodpressure1: 180.8, 120.6 bloodpressure2: [180.8, 120.6] +bp: + systolic: 180.8 + diastolic: 120.6 clock-in: 9:16 clock-out: 16:49 sleep: 9:12 pm/6:41 am diff --git a/examples/diary/2021-01-03.md b/examples/diary/2021-01-03.md index d4d59ce..6ac6973 100644 --- a/examples/diary/2021-01-03.md +++ b/examples/diary/2021-01-03.md @@ -4,6 +4,9 @@ mood: 5 bloodpressure: 178.2/120.4 bloodpressure1: 178.2, 120.4 bloodpressure2: [178.2, 120.4] +bp: + systolic: 178.2 + diastolic: 120.4 clock-in: 10:22 clock-out: 20:49 sleep: 9:33 pm/5:17 am diff --git a/examples/diary/2021-01-04.md b/examples/diary/2021-01-04.md index ba8f3c5..73fce11 100644 --- a/examples/diary/2021-01-04.md +++ b/examples/diary/2021-01-04.md @@ -4,6 +4,9 @@ mood: 3 bloodpressure: 179.6/121.2 bloodpressure1: 179.6, 121.2 bloodpressure2: [179.6, 121.2] +bp: + systolic: 179.6 + diastolic: 121.2 clock-in: 8:24 clock-out: 20:58 sleep: 11:57 pm/7:20 am diff --git a/examples/diary/2021-01-05.md b/examples/diary/2021-01-05.md index ea5d52c..51dca8b 100644 --- a/examples/diary/2021-01-05.md +++ b/examples/diary/2021-01-05.md @@ -4,6 +4,9 @@ mood: 5 bloodpressure: 178/119 bloodpressure1: 178, 119 bloodpressure2: [178, 119] +bp: + systolic: 178 + diastolic: 119 clock-in: 8:10 clock-out: 20:21 sleep: 10:1 pm/6:57 am diff --git a/examples/diary/2021-01-06.md b/examples/diary/2021-01-06.md index eb0fcd6..3cce288 100644 --- a/examples/diary/2021-01-06.md +++ b/examples/diary/2021-01-06.md @@ -4,6 +4,9 @@ mood: 7 bloodpressure: 180.4/118.8 bloodpressure1: 180.4, 118.8 bloodpressure2: [180.4, 118.8] +bp: + systolic: 180.4 + diastolic: 118.8 clock-in: 10:23 clock-out: 19:2 sleep: 10:47 pm/6:41 am diff --git a/examples/diary/2021-01-07.md b/examples/diary/2021-01-07.md index 3330e10..4be6c2c 100644 --- a/examples/diary/2021-01-07.md +++ b/examples/diary/2021-01-07.md @@ -4,6 +4,9 @@ mood: 9 bloodpressure: 173.8/120.6 bloodpressure1: 173.8, 120.6 bloodpressure2: [173.8, 120.6] +bp: + systolic: 173.8 + diastolic: 120.6 clock-in: 10:29 clock-out: 17:0 sleep: 10:54 pm/6:53 am diff --git a/examples/diary/2021-01-08.md b/examples/diary/2021-01-08.md index 3c364b1..8c4f6c9 100644 --- a/examples/diary/2021-01-08.md +++ b/examples/diary/2021-01-08.md @@ -4,6 +4,9 @@ mood: 10 bloodpressure: 170.2/119.4 bloodpressure1: 170.2, 119.4 bloodpressure2: [170.2, 119.4] +bp: + systolic: 170.2 + diastolic: 119.4 clock-in: 9:8 clock-out: 18:18 sleep: 9:12 pm/5:1 am diff --git a/examples/diary/2021-01-09.md b/examples/diary/2021-01-09.md index 09e8f66..cf13e63 100644 --- a/examples/diary/2021-01-09.md +++ b/examples/diary/2021-01-09.md @@ -4,6 +4,9 @@ mood: 2 bloodpressure: 177.6/120.2 bloodpressure1: 177.6, 120.2 bloodpressure2: [177.6, 120.2] +bp: + systolic: 177.6 + diastolic: 120.2 clock-in: 10:23 clock-out: 18:32 sleep: 11:3 pm/6:35 am diff --git a/examples/diary/2021-01-10.md b/examples/diary/2021-01-10.md index d8b26df..7f01e58 100644 --- a/examples/diary/2021-01-10.md +++ b/examples/diary/2021-01-10.md @@ -4,6 +4,9 @@ mood: 7 bloodpressure: 178/118 bloodpressure1: 178, 118 bloodpressure2: [178, 118] +bp: + systolic: 178 + diastolic: 118 clock-in: 8:31 clock-out: 17:43 sleep: 9:7 pm/5:59 am diff --git a/examples/diary/2021-01-11.md b/examples/diary/2021-01-11.md index 359bd58..847c842 100644 --- a/examples/diary/2021-01-11.md +++ b/examples/diary/2021-01-11.md @@ -4,6 +4,9 @@ mood: 2 bloodpressure: 174.4/115.8 bloodpressure1: 174.4, 115.8 bloodpressure2: [174.4, 115.8] +bp: + systolic: 174.4 + diastolic: 115.8 clock-in: 10:8 clock-out: 16:2 sleep: 9:49 pm/7:29 am diff --git a/examples/diary/2021-01-12.md b/examples/diary/2021-01-12.md index c740387..ac51000 100644 --- a/examples/diary/2021-01-12.md +++ b/examples/diary/2021-01-12.md @@ -4,6 +4,9 @@ mood: 4 bloodpressure: 169.8/117.6 bloodpressure1: 169.8, 117.6 bloodpressure2: [169.8, 117.6] +bp: + systolic: 169.8 + diastolic: 117.6 clock-in: 9:31 clock-out: 17:4 sleep: 11:22 pm/6:1 am diff --git a/examples/diary/2021-01-13.md b/examples/diary/2021-01-13.md index 9cd34f6..ad01bfc 100644 --- a/examples/diary/2021-01-13.md +++ b/examples/diary/2021-01-13.md @@ -4,6 +4,9 @@ mood: 7 bloodpressure: 176.2/116.4 bloodpressure1: 176.2, 116.4 bloodpressure2: [176.2, 116.4] +bp: + systolic: 176.2 + diastolic: 116.4 clock-in: 10:23 clock-out: 16:5 sleep: 11:47 pm/7:19 am diff --git a/examples/diary/2021-01-14.md b/examples/diary/2021-01-14.md index ee320fe..6575a5e 100644 --- a/examples/diary/2021-01-14.md +++ b/examples/diary/2021-01-14.md @@ -4,6 +4,9 @@ mood: 5 bloodpressure: 167.6/117.2 bloodpressure1: 167.6, 117.2 bloodpressure2: [167.6, 117.2] +bp: + systolic: 167.6 + diastolic: 117.2 clock-in: 10:7 clock-out: 16:5 sleep: 9:9 pm/7:54 am diff --git a/examples/diary/2021-01-15.md b/examples/diary/2021-01-15.md index 3ecd7ef..729cf3d 100644 --- a/examples/diary/2021-01-15.md +++ b/examples/diary/2021-01-15.md @@ -4,6 +4,9 @@ mood: 7 bloodpressure: 168/116 bloodpressure1: 168, 116 bloodpressure2: [168, 116] +bp: + systolic: 168 + diastolic: 116 clock-in: 8:13 clock-out: 18:16 sleep: 9:28 pm/7:25 am diff --git a/examples/diary/2021-01-16.md b/examples/diary/2021-01-16.md index 12688a7..44d6d23 100644 --- a/examples/diary/2021-01-16.md +++ b/examples/diary/2021-01-16.md @@ -4,6 +4,9 @@ mood: 5 bloodpressure: 173.4/114.8 bloodpressure1: 173.4, 114.8 bloodpressure2: [173.4, 114.8] +bp: + systolic: 173.4 + diastolic: 114.8 clock-in: 8:32 clock-out: 17:4 sleep: 9:51 pm/6:15 am diff --git a/examples/diary/2021-01-17.md b/examples/diary/2021-01-17.md index 5b43b54..188c795 100644 --- a/examples/diary/2021-01-17.md +++ b/examples/diary/2021-01-17.md @@ -4,6 +4,9 @@ mood: 5 bloodpressure: 168.8/118.6 bloodpressure1: 168.8, 118.6 bloodpressure2: [168.8, 118.6] +bp: + systolic: 168.8 + diastolic: 118.6 clock-in: 9:25 clock-out: 17:18 sleep: 9:42 pm/5:42 am diff --git a/examples/diary/2021-01-18.md b/examples/diary/2021-01-18.md index 22baab9..662c4d1 100644 --- a/examples/diary/2021-01-18.md +++ b/examples/diary/2021-01-18.md @@ -4,6 +4,9 @@ mood: 5 bloodpressure: 166.2/118.4 bloodpressure1: 166.2, 118.4 bloodpressure2: [166.2, 118.4] +bp: + systolic: 166.2 + diastolic: 118.4 clock-in: 10:44 clock-out: 19:39 sleep: 11:31 pm/5:44 am diff --git a/examples/diary/2021-01-19.md b/examples/diary/2021-01-19.md index 6edf412..1eb4693 100644 --- a/examples/diary/2021-01-19.md +++ b/examples/diary/2021-01-19.md @@ -4,6 +4,9 @@ mood: 9 bloodpressure: 165.6/118.2 bloodpressure1: 165.6, 118.2 bloodpressure2: [165.6, 118.2] +bp: + systolic: 165.6 + diastolic: 118.2 clock-in: 9:31 clock-out: 16:13 sleep: 11:39 pm/6:11 am diff --git a/examples/diary/2021-01-20.md b/examples/diary/2021-01-20.md index 6556c0e..ff9c856 100644 --- a/examples/diary/2021-01-20.md +++ b/examples/diary/2021-01-20.md @@ -4,6 +4,9 @@ mood: 10 bloodpressure: 163/115 bloodpressure1: 163, 115 bloodpressure2: [163, 115] +bp: + systolic: 163 + diastolic: 115 clock-in: 9:49 clock-out: 18:2 sleep: 10:46 pm/7:45 am diff --git a/examples/diary/2021-01-21.md b/examples/diary/2021-01-21.md index 009ce2b..8d0f391 100644 --- a/examples/diary/2021-01-21.md +++ b/examples/diary/2021-01-21.md @@ -4,6 +4,9 @@ mood: 5 bloodpressure: 169.4/117.8 bloodpressure1: 169.4, 117.8 bloodpressure2: [169.4, 117.8] +bp: + systolic: 169.4 + diastolic: 117.8 clock-in: 9:59 clock-out: 16:39 sleep: 10:38 pm/5:35 am diff --git a/examples/diary/2021-01-22.md b/examples/diary/2021-01-22.md index 0d69867..b021ca6 100644 --- a/examples/diary/2021-01-22.md +++ b/examples/diary/2021-01-22.md @@ -4,6 +4,9 @@ mood: 5 bloodpressure: 168.8/115.6 bloodpressure1: 168.8, 115.6 bloodpressure2: [168.8, 115.6] +bp: + systolic: 168.8 + diastolic: 115.6 clock-in: 9:6 clock-out: 17:57 sleep: 11:28 pm/6:45 am diff --git a/examples/diary/2021-01-23.md b/examples/diary/2021-01-23.md index 68a1da4..6a79477 100644 --- a/examples/diary/2021-01-23.md +++ b/examples/diary/2021-01-23.md @@ -4,6 +4,9 @@ mood: 5 bloodpressure: 167.2/115.4 bloodpressure1: 167.2, 115.4 bloodpressure2: [167.2, 115.4] +bp: + systolic: 167.2 + diastolic: 115.4 clock-in: 8:12 clock-out: 20:13 sleep: 9:46 pm/7:12 am diff --git a/examples/diary/2021-01-24.md b/examples/diary/2021-01-24.md index f0a44b0..1dc610d 100644 --- a/examples/diary/2021-01-24.md +++ b/examples/diary/2021-01-24.md @@ -4,6 +4,9 @@ mood: 7 bloodpressure: 165.6/114.2 bloodpressure1: 165.6, 114.2 bloodpressure2: [165.6, 114.2] +bp: + systolic: 165.6 + diastolic: 114.2 clock-in: 8:41 clock-out: 18:17 sleep: 10:49 pm/6:41 am diff --git a/examples/diary/2021-01-25.md b/examples/diary/2021-01-25.md index 85adb37..ad22288 100644 --- a/examples/diary/2021-01-25.md +++ b/examples/diary/2021-01-25.md @@ -4,6 +4,9 @@ mood: 1 bloodpressure: 165/113 bloodpressure1: 165, 113 bloodpressure2: [165, 113] +bp: + systolic: 165 + diastolic: 113 clock-in: 9:53 clock-out: 16:33 sleep: 10:11 pm/6:9 am diff --git a/examples/diary/2021-01-26.md b/examples/diary/2021-01-26.md index ba3b8dd..8ebefc0 100644 --- a/examples/diary/2021-01-26.md +++ b/examples/diary/2021-01-26.md @@ -4,6 +4,9 @@ mood: 7 bloodpressure: 168.4/113.8 bloodpressure1: 168.4, 113.8 bloodpressure2: [168.4, 113.8] +bp: + systolic: 168.4 + diastolic: 113.8 clock-in: 10:24 clock-out: 20:21 sleep: 9:48 pm/5:2 am diff --git a/examples/diary/2021-01-27.md b/examples/diary/2021-01-27.md index 59526d2..892bba6 100644 --- a/examples/diary/2021-01-27.md +++ b/examples/diary/2021-01-27.md @@ -4,6 +4,9 @@ mood: 6 bloodpressure: 166.8/114.6 bloodpressure1: 166.8, 114.6 bloodpressure2: [166.8, 114.6] +bp: + systolic: 166.8 + diastolic: 114.6 clock-in: 9:12 clock-out: 19:36 sleep: 10:2 pm/7:36 am diff --git a/examples/diary/2021-01-28.md b/examples/diary/2021-01-28.md index 5943460..c9eddbc 100644 --- a/examples/diary/2021-01-28.md +++ b/examples/diary/2021-01-28.md @@ -4,6 +4,9 @@ mood: 7 bloodpressure: 160.2/116.4 bloodpressure1: 160.2, 116.4 bloodpressure2: [160.2, 116.4] +bp: + systolic: 160.2 + diastolic: 116.4 clock-in: 9:28 clock-out: 16:58 sleep: 10:28 pm/5:46 am diff --git a/examples/diary/2021-01-29.md b/examples/diary/2021-01-29.md index 9139fcd..909d652 100644 --- a/examples/diary/2021-01-29.md +++ b/examples/diary/2021-01-29.md @@ -4,6 +4,9 @@ mood: 2 bloodpressure: 157.6/115.2 bloodpressure1: 157.6, 115.2 bloodpressure2: [157.6, 115.2] +bp: + systolic: 157.6 + diastolic: 115.2 clock-in: 10:44 clock-out: 16:41 sleep: 10:27 pm/5:11 am diff --git a/examples/diary/2021-01-30.md b/examples/diary/2021-01-30.md index 30adfd6..9943c26 100644 --- a/examples/diary/2021-01-30.md +++ b/examples/diary/2021-01-30.md @@ -4,6 +4,9 @@ mood: 7 bloodpressure: 165/114 bloodpressure1: 165, 114 bloodpressure2: [165, 114] +bp: + systolic: 165 + diastolic: 114 clock-in: 8:16 clock-out: 17:2 sleep: 10:51 pm/7:29 am diff --git a/examples/diary/2021-01-31.md b/examples/diary/2021-01-31.md index 50858f6..cf2c8ac 100644 --- a/examples/diary/2021-01-31.md +++ b/examples/diary/2021-01-31.md @@ -4,6 +4,9 @@ mood: 8 bloodpressure: 159.4/113.8 bloodpressure1: 159.4, 113.8 bloodpressure2: [159.4, 113.8] +bp: + systolic: 159.4 + diastolic: 113.8 clock-in: 8:30 clock-out: 20:24 sleep: 10:4 pm/7:10 am diff --git a/examples/example_generator.ts b/examples/example_generator.ts index d206bdb..69f43be 100644 --- a/examples/example_generator.ts +++ b/examples/example_generator.ts @@ -79,6 +79,10 @@ for ( frontmatter += "bloodpressure2: [" + systolic + ", " + diastolic + "]" + "\n"; + frontmatter += "bp:" + "\n"; + frontmatter += " systolic: " + systolic + "\n"; + frontmatter += " diastolic: " + diastolic + "\n"; + // clock-in clock-out, 24hr let time_clock_in = randomIntFromInterval(8, 10).toString() + From 713892feea22dfef07786b1f43165b248679a891 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Thu, 3 Jun 2021 11:33:36 +0800 Subject: [PATCH 040/402] Add template variables and examples for them --- examples/TestTemplateVariables.md | 233 +++++++++++++++++++++ src/collecting.ts | 20 ++ src/data.ts | 40 ++++ src/expr.ts | 323 ++++++++++++++++++++++++++---- src/main.ts | 16 +- 5 files changed, 589 insertions(+), 43 deletions(-) create mode 100644 examples/TestTemplateVariables.md diff --git a/examples/TestTemplateVariables.md b/examples/TestTemplateVariables.md new file mode 100644 index 0000000..5380520 --- /dev/null +++ b/examples/TestTemplateVariables.md @@ -0,0 +1,233 @@ +# Test Template Variables + +### {{min}} +``` tracker +searchType: dvField +searchTarget: dataviewTarget +folder: /diary +endDate: 2021-01-03 +summary: + template: 'Minimum value: {{min}} <-- should be 27' +``` + +### {{max}} +``` tracker +searchType: dvField +searchTarget: dataviewTarget +folder: /diary +endDate: 2021-01-03 +summary: + template: 'Maximum value: {{max}} <-- should be 88' +``` + +### {{startDate}} +``` tracker +searchType: dvField +searchTarget: dataviewTarget +folder: /diary +endDate: 2021-01-03 +summary: + template: 'Start date: {{startDate}} <-- should be 2021-01-01' +``` + +### {{endDate}} +``` tracker +searchType: dvField +searchTarget: dataviewTarget +folder: /diary +endDate: 2021-01-03 +summary: + template: 'End date: {{endDate}} <-- should be 2021-01-03' +``` + +### {{sum}} +``` tracker +searchType: tag +searchTarget: meditation +folder: /diary +endDate: 2021-01-04 +summary: + template: 'Sum: {{sum}} <-- should be 2' +``` + +### {{numTargets}} +``` tracker +searchType: tag +searchTarget: meditation +folder: /diary +endDate: 2021-01-04 +summary: + template: 'Number of targets: {{numTargets}} <-- should be 3' +``` + +### {{numDays}} +``` tracker +searchType: tag +searchTarget: meditation +folder: /diary +endDate: 2021-01-04 +summary: + template: 'Number of days: {{numDays}} <-- should be 4' +``` + +### {{numDaysHavingData}} +``` tracker +searchType: tag +searchTarget: meditation +folder: /diary +endDate: 2021-01-04 +summary: + template: 'Number of days having data: {{numDaysHavingData}} <-- should be 2' +``` + +### {{maxStreak}} +``` tracker +searchType: tag +searchTarget: meditation +folder: /diary +endDate: 2021-01-09 +summary: + template: 'Maximum streak: {{maxStreak}} <-- should be 3' +``` + +### {{maxStreakStart}} +``` tracker +searchType: tag +searchTarget: meditation +folder: /diary +endDate: 2021-01-09 +summary: + template: 'The start date of maximum streak: {{maxStreakStart}} <-- should be 2021-01-04' +``` + +### {{maxStreakEnd}} +``` tracker +searchType: tag +searchTarget: meditation +folder: /diary +endDate: 2021-01-09 +summary: + template: 'The end date of maximum streak: {{maxStreakEnd}} <-- should be 2021-01-06' +``` + +### {{maxBreaks}} +``` tracker +searchType: tag +searchTarget: meditation +folder: /diary +endDate: 2021-01-09 +summary: + template: 'Maximum breaks: {{maxBreaks}} <-- should be 1' +``` + +### {{maxBreaksStart}} +``` tracker +searchType: tag +searchTarget: meditation +folder: /diary +endDate: 2021-01-09 +summary: + template: 'The start date of maximum breaks: {{maxBreaksStart}} <-- should be 2021-01-07' +``` + +### {{maxBreaksEnd}} +``` tracker +searchType: tag +searchTarget: meditation +folder: /diary +endDate: 2021-01-09 +summary: + template: 'The end date of maximum breaks: {{maxBreaksEnd}} <-- should be 2021-01-07' +``` + +### {{currentStreak}} +``` tracker +searchType: tag +searchTarget: meditation +folder: /diary +endDate: 2021-01-24 +summary: + template: 'Latest streak: {{currentStreak}} <-- should be 2' +``` + +### {{currentStreakStart}} +``` tracker +searchType: tag +searchTarget: meditation +folder: /diary +endDate: 2021-01-24 +summary: + template: 'The start date of current streak: {{currentStreakStart}} <-- should be 2021-01-23' +``` + +### {{currentStreakEnd}} +``` tracker +searchType: tag +searchTarget: meditation +folder: /diary +endDate: 2021-01-24 +summary: + template: 'The end date of current streak: {{currentStreakEnd}} <-- should be 2021-01-24' +``` + +### {{currentBreaks}} +``` tracker +searchType: tag +searchTarget: meditation +folder: /diary +endDate: 2021-01-22 +summary: + template: 'Current breaks: {{currentBreaks}} <-- should be 8' +``` + +### {{currentBreaksStart}} +``` tracker +searchType: tag +searchTarget: meditation +folder: /diary +endDate: 2021-01-22 +summary: + template: 'The start date of current breaks: {{currentBreaksStart}} <-- should be 2021-01-15' +``` + +### {{currentBreaksEnd}} +``` tracker +searchType: tag +searchTarget: meditation +folder: /diary +endDate: 2021-01-22 +summary: + template: 'The end date of current breaks: {{currentBreaksEnd}} <-- should be 2021-01-22' +``` + +### {{average}} +(48+27+88)/3 = 54.33 +``` tracker +searchType: dvField +searchTarget: dataviewTarget +folder: /diary +endDate: 2021-01-03 +summary: + template: 'Average value: {{average}} <-- should be 54.33' +``` + +### {{median}} +``` tracker +searchType: dvField +searchTarget: dataviewTarget +folder: /diary +endDate: 2021-01-03 +summary: + template: 'Median value: {{median}} <-- should be 48' +``` + +### {{variance}} +https://mathworld.wolfram.com/SampleVariance.html +``` tracker +searchType: dvField +searchTarget: dataviewTarget +folder: /diary +endDate: 2021-01-03 +summary: + template: 'Variance value: {{variance}} <-- should be 960.33' +``` diff --git a/src/collecting.ts b/src/collecting.ts index e110795..078f09e 100644 --- a/src/collecting.ts +++ b/src/collecting.ts @@ -258,10 +258,12 @@ export function collectDataFromFrontmatterTag( // simple tag tagMeasure = tagMeasure + renderInfo.constValue[query.getId()]; tagExist = true; + query.addNumTargets(); } else if (tag.startsWith(query.getTarget() + "/")) { // nested tag tagMeasure = tagMeasure + renderInfo.constValue[query.getId()]; tagExist = true; + query.addNumTargets(); } else { continue; } @@ -297,6 +299,7 @@ export function collectDataFromFrontmatterKey( if (retParse.type === ValueType.Time) { query.valueType = ValueType.Time; } + query.addNumTargets(); let xValue = xValueMap.get(renderInfo.xDataset[query.getId()]); addToDataMap(dataMap, xValue, query, retParse.value); } @@ -335,6 +338,7 @@ export function collectDataFromFrontmatterKey( if (retParse.type === ValueType.Time) { query.valueType = ValueType.Time; } + query.addNumTargets(); let xValue = xValueMap.get( renderInfo.xDataset[query.getId()] ); @@ -360,6 +364,7 @@ export function collectDataFromWiki( if (link.link === query.getTarget()) { linkExist = true; linkMeasure = linkMeasure + renderInfo.constValue[query.getId()]; + query.addNumTargets(); } } @@ -414,6 +419,7 @@ export function collectDataFromInlineTag( tagMeasure = retParse.value; tagExist = true; query.valueType = ValueType.Time; + query.addNumTargets(); } else { if ( !renderInfo.ignoreZeroValue[query.getId()] || @@ -421,6 +427,7 @@ export function collectDataFromInlineTag( ) { tagMeasure += retParse.value; tagExist = true; + query.addNumTargets(); } } } @@ -436,9 +443,11 @@ export function collectDataFromInlineTag( tagMeasure = retParse.value; tagExist = true; query.valueType = ValueType.Time; + query.addNumTargets(); } else { tagMeasure += retParse.value; tagExist = true; + query.addNumTargets(); } } } @@ -446,6 +455,7 @@ export function collectDataFromInlineTag( // console.log("simple-tag"); tagMeasure = tagMeasure + renderInfo.constValue[query.getId()]; tagExist = true; + query.addNumTargets(); } } @@ -489,6 +499,7 @@ export function collectDataFromText( ) { textMeasure += value; textExist = true; + query.addNumTargets(); } } } @@ -496,6 +507,7 @@ export function collectDataFromText( // console.log("simple-text"); textMeasure = textMeasure + renderInfo.constValue[query.getId()]; textExist = true; + query.addNumTargets(); } } @@ -521,15 +533,18 @@ export function collectDataFromFileMeta( if (target === "cDate") { let ctime = file.stat.ctime; query.valueType = ValueType.Date; + query.addNumTargets(); let xValue = xValueMap.get(renderInfo.xDataset[query.getId()]); addToDataMap(dataMap, xValue, query, ctime); } else if (target === "mDate") { let mtime = file.stat.mtime; query.valueType = ValueType.Date; + query.addNumTargets(); let xValue = xValueMap.get(renderInfo.xDataset[query.getId()]); addToDataMap(dataMap, xValue, query, mtime); } else if (target === "size") { let size = file.stat.size; + query.addNumTargets(); let xValue = xValueMap.get(renderInfo.xDataset[query.getId()]); addToDataMap(dataMap, xValue, query, size); } @@ -579,6 +594,7 @@ export function collectDataFromDvField( tagMeasure = retParse.value; tagExist = true; query.valueType = ValueType.Time; + query.addNumTargets(); } else { if ( !renderInfo.ignoreZeroValue[query.getId()] || @@ -586,6 +602,7 @@ export function collectDataFromDvField( ) { tagMeasure += retParse.value; tagExist = true; + query.addNumTargets(); } } } @@ -602,9 +619,11 @@ export function collectDataFromDvField( tagMeasure = retParse.value; tagExist = true; query.valueType = ValueType.Time; + query.addNumTargets(); } else { tagMeasure += retParse.value; tagExist = true; + query.addNumTargets(); } } } @@ -612,6 +631,7 @@ export function collectDataFromDvField( // console.log("simple-tag"); tagMeasure = tagMeasure + renderInfo.constValue[query.getId()]; tagExist = true; + query.addNumTargets(); } } diff --git a/src/data.ts b/src/data.ts index e3f99dc..9bb75d8 100644 --- a/src/data.ts +++ b/src/data.ts @@ -48,6 +48,7 @@ export class Query { private accessor: number; private accessor1: number; private accessor2: number; + private numTargets: number; valueType: ValueType; usedAsXDataset: boolean; @@ -62,6 +63,7 @@ export class Query { this.accessor2 = -1; this.valueType = ValueType.Number; this.usedAsXDataset = false; + this.numTargets = 0; if (searchType === SearchType.Table) { // searchTarget --> {{filePath}}[{{table}}][{{column}}] @@ -161,6 +163,14 @@ export class Query { public getSeparator() { return this.separator; } + + public addNumTargets(num: number = 1) { + this.numTargets = this.numTargets + num; + } + + public getNumTargets() { + return this.numTargets; + } } export interface QueryValuePair { @@ -177,6 +187,9 @@ export class Dataset implements IterableIterator { private id: number; private yMin: number; private yMax: number; + private startDate: Moment; + private endDate: Moment; + private numTargets: number; private lineInfo: LineInfo; private barInfo: BarInfo; @@ -192,6 +205,9 @@ export class Dataset implements IterableIterator { this.id = -1; this.yMin = null; this.yMax = null; + this.startDate = null; + this.endDate = null; + this.numTargets = 0; this.lineInfo = null; this.barInfo = null; this.valueType = query.valueType; @@ -232,6 +248,14 @@ export class Dataset implements IterableIterator { this.id = id; } + public addNumTargets(num: number) { + this.numTargets = this.numTargets + num; + } + + public getNumTargets() { + return this.numTargets; + } + public setValue(date: Moment, value: number) { let ind = this.parent.getIndexOfDate(date); // console.log(ind); @@ -244,6 +268,12 @@ export class Dataset implements IterableIterator { if (this.yMax === null || value > this.yMax) { this.yMax = value; } + if (this.startDate === null || date < this.startDate) { + this.startDate = date.clone(); + } + if (this.endDate === null || date > this.endDate) { + this.endDate = date.clone(); + } } } @@ -255,6 +285,14 @@ export class Dataset implements IterableIterator { return this.yMax; } + public getStartDate() { + return this.startDate; + } + + public getEndDate() { + return this.endDate; + } + public shift(shiftAmount: number) { for (let ind = 0; ind < this.values.length; ind++) { if (this.values[ind] !== null) { @@ -418,6 +456,8 @@ export class Datasets implements IterableIterator { return dataset; } } + + return null; } public getXDatasetIds() { diff --git a/src/expr.ts b/src/expr.ts index 668531a..506566a 100644 --- a/src/expr.ts +++ b/src/expr.ts @@ -1,27 +1,61 @@ import { RenderInfo } from "./data"; import * as d3 from "d3"; +import { Moment } from "moment"; let fnSet = { + // min value of a dataset min: function (renderInfo: RenderInfo, datasetId: number) { let dataset = renderInfo.datasets.getDatasetById(datasetId); return d3.min(dataset.getValues()); }, + // max value of a dataset max: function (renderInfo: RenderInfo, datasetId: number) { let dataset = renderInfo.datasets.getDatasetById(datasetId); return d3.max(dataset.getValues()); }, + // start date of a dataset + // if datasetId not found, return overall startDate + startDate: function (renderInfo: RenderInfo, datasetId: number) { + let dataset = renderInfo.datasets.getDatasetById(datasetId); + if (dataset) { + let startDate = dataset.getStartDate(); + if (startDate && startDate.isValid()) { + return startDate.format(renderInfo.dateFormat); + } + } + return renderInfo.startDate.format(renderInfo.dateFormat); + }, + // end date of a dataset + // if datasetId not found, return overall endDate + endDate: function (renderInfo: RenderInfo, datasetId: number) { + let dataset = renderInfo.datasets.getDatasetById(datasetId); + if (dataset) { + let endDate = dataset.getEndDate(); + if (endDate && endDate.isValid()) { + return endDate.format(renderInfo.dateFormat); + } + } + return renderInfo.endDate.format(renderInfo.dateFormat); + }, + // sum of all values in a dataset sum: function (renderInfo: RenderInfo, datasetId: number) { let dataset = renderInfo.datasets.getDatasetById(datasetId); return d3.sum(dataset.getValues()); }, - count: function (renderInfo: RenderInfo, datasetId: number) { + // count --> deprecated + // number of occurrences of a target in a dataset + numTargets: function (renderInfo: RenderInfo, datasetId: number) { let dataset = renderInfo.datasets.getDatasetById(datasetId); - return dataset.getLengthNotNull(); + return dataset.getNumTargets(); + }, + // days --> deprecated + numDays: function (renderInfo: RenderInfo, datasetId: number) { + let dataset = renderInfo.datasets.getDatasetById(datasetId); + return dataset.getLength(); }, - days: function (renderInfo: RenderInfo, datasetId: number) { + numDaysHavingData: function (renderInfo: RenderInfo, datasetId: number) { let dataset = renderInfo.datasets.getDatasetById(datasetId); - let result = dataset.getLength(); - return result; + return dataset.getLengthNotNull(); }, maxStreak: function (renderInfo: RenderInfo, datasetId: number) { let streak = 0; @@ -33,56 +67,267 @@ let fnSet = { } else { streak = 0; } - if (streak > maxStreak) { + if (streak >= maxStreak) { maxStreak = streak; } } return maxStreak; }, - maxBreaks: function (renderInfo: RenderInfo, datasetId: number) { + maxStreakStart: function (renderInfo: RenderInfo, datasetId: number) { let streak = 0; - let maxBreak = 0; + let maxStreak = 0; + let streakStart: Moment = null; + let maxStreakStart: Moment = null; + let dataset = renderInfo.datasets.getDatasetById(datasetId); + if (dataset) { + for (let dataPoint of dataset) { + if (dataPoint.value !== null) { + if (streak === 0) { + streakStart = dataPoint.date; + } + streak++; + } else { + streak = 0; + } + if (streak >= maxStreak) { + maxStreak = streak; + maxStreakStart = streakStart; + } + } + } + return maxStreakStart?.format(renderInfo.dateFormat); + }, + maxStreakEnd: function (renderInfo: RenderInfo, datasetId: number) { + let streak = 0; + let maxStreak = 0; + let streakEnd: Moment = null; + let maxStreakEnd: Moment = null; + let dataset = renderInfo.datasets.getDatasetById(datasetId); + if (dataset) { + let arrayDataset = Array.from(dataset); + for (let ind = 0; ind < arrayDataset.length; ind++) { + let point = arrayDataset[ind]; + let nextPoint = null; + if (ind < arrayDataset.length - 1) { + nextPoint = arrayDataset[ind + 1]; + } + if (point.value !== null) { + streak++; + if (nextPoint?.value === null) { + streakEnd = point.date; + } + } else { + streak = 0; + } + if (streak >= maxStreak) { + // console.log(streak); + // console.log(maxStreak); + maxStreak = streak; + maxStreakEnd = streakEnd; + } + } + } + return maxStreakEnd?.format(renderInfo.dateFormat); + }, + maxBreaks: function (renderInfo: RenderInfo, datasetId: number) { + let breaks = 0; + let maxBreaks = 0; let dataset = renderInfo.datasets.getDatasetById(datasetId); for (let dataPoint of dataset) { if (dataPoint.value === null) { - streak++; + breaks++; } else { - streak = 0; + breaks = 0; } - if (streak > maxBreak) { - maxBreak = streak; + if (breaks > maxBreaks) { + maxBreaks = breaks; } } - return maxBreak; + return maxBreaks; }, - lastStreak: function (renderInfo: RenderInfo, datasetId: number) { - let streak = 0; + maxBreaksStart: function (renderInfo: RenderInfo, datasetId: number) { + let breaks = 0; + let maxBreaks = 0; + let breaksStart: Moment = null; + let maxBreaksStart: Moment = null; let dataset = renderInfo.datasets.getDatasetById(datasetId); - let values = dataset.getValues(); - for (let ind = values.length - 1; ind >= 0; ind--) { - let value = values[ind]; - if (value === null) { - break; - } else { - streak++; + if (dataset) { + for (let dataPoint of dataset) { + if (dataPoint.value === null) { + if (breaks === 0) { + breaksStart = dataPoint.date; + } + breaks++; + } else { + breaks = 0; + } + if (breaks >= maxBreaks) { + maxBreaks = breaks; + maxBreaksStart = breaksStart; + } } } - return streak; + return maxBreaksStart?.format(renderInfo.dateFormat); }, - lastBreaks: function (renderInfo: RenderInfo, datasetId: number) { - let breakDays = 0; + maxBreaksEnd: function (renderInfo: RenderInfo, datasetId: number) { + let breaks = 0; + let maxBreaks = 0; + let breaksEnd: Moment = null; + let maxBreaksEnd: Moment = null; let dataset = renderInfo.datasets.getDatasetById(datasetId); - let values = dataset.getValues(); - for (let ind = values.length - 1; ind >= 0; ind--) { - let value = values[ind]; - if (value === null) { - breakDays++; - } else { - break; + if (dataset) { + let arrayDataset = Array.from(dataset); + for (let ind = 0; ind < arrayDataset.length; ind++) { + let point = arrayDataset[ind]; + let nextPoint = null; + if (ind < arrayDataset.length - 1) { + nextPoint = arrayDataset[ind + 1]; + } + if (point.value === null) { + breaks++; + if (nextPoint?.value !== null) { + breaksEnd = point.date; + } + } else { + breaks = 0; + } + if (breaks >= maxBreaks) { + maxBreaks = breaks; + maxBreaksEnd = breaksEnd; + } + } + } + return maxBreaksEnd?.format(renderInfo.dateFormat); + }, + // lastStreak --> deprecated + currentStreak: function (renderInfo: RenderInfo, datasetId: number) { + let currentStreak = 0; + let dataset = renderInfo.datasets.getDatasetById(datasetId); + if (dataset) { + let arrayDataset = Array.from(dataset); + for (let ind = arrayDataset.length - 1; ind >= 0; ind--) { + let point = arrayDataset[ind]; + if (point.value === null) { + break; + } else { + currentStreak++; + } + } + } + return currentStreak; + }, + currentStreakStart: function (renderInfo: RenderInfo, datasetId: number) { + let currentStreak = 0; + let currentStreakStart: Moment = null; + let dataset = renderInfo.datasets.getDatasetById(datasetId); + if (dataset) { + let arrayDataset = Array.from(dataset); + for (let ind = arrayDataset.length - 1; ind >= 0; ind--) { + let point = arrayDataset[ind]; + if (ind < arrayDataset.length - 1) { + currentStreakStart = arrayDataset[ind + 1].date; + } + if (point.value === null) { + break; + } else { + currentStreak++; + } + } + } + + if (currentStreakStart === null) { + return "absense"; + } + return currentStreakStart?.format(renderInfo.dateFormat); + }, + currentStreakEnd: function (renderInfo: RenderInfo, datasetId: number) { + let currentStreak = 0; + let currentStreakEnd: Moment = null; + let dataset = renderInfo.datasets.getDatasetById(datasetId); + if (dataset) { + let arrayDataset = Array.from(dataset); + for (let ind = arrayDataset.length - 1; ind >= 0; ind--) { + let point = arrayDataset[ind]; + if (point.value === null) { + break; + } else { + if (currentStreak === 0) { + currentStreakEnd = point.date; + } + currentStreak++; + } } } - return breakDays; + + if (currentStreakEnd === null) { + return "absense"; + } + return currentStreakEnd?.format(renderInfo.dateFormat); + }, + currentBreaks: function (renderInfo: RenderInfo, datasetId: number) { + let currentBreaks = 0; + let dataset = renderInfo.datasets.getDatasetById(datasetId); + if (dataset) { + let arrayDataset = Array.from(dataset); + for (let ind = arrayDataset.length - 1; ind >= 0; ind--) { + let point = arrayDataset[ind]; + if (point.value === null) { + currentBreaks++; + } else { + break; + } + } + } + return currentBreaks; + }, + currentBreaksStart: function (renderInfo: RenderInfo, datasetId: number) { + let currentBreaks = 0; + let currentBreaksStart: Moment = null; + let dataset = renderInfo.datasets.getDatasetById(datasetId); + if (dataset) { + let arrayDataset = Array.from(dataset); + for (let ind = arrayDataset.length - 1; ind >= 0; ind--) { + let point = arrayDataset[ind]; + if (ind < arrayDataset.length - 1) { + currentBreaksStart = arrayDataset[ind + 1].date; + } + if (point.value === null) { + currentBreaks++; + } else { + break; + } + } + } + + if (currentBreaksStart === null) { + return "absense"; + } + return currentBreaksStart?.format(renderInfo.dateFormat); + }, + currentBreaksEnd: function (renderInfo: RenderInfo, datasetId: number) { + let currentBreaks = 0; + let currentBreaksEnd: Moment = null; + let dataset = renderInfo.datasets.getDatasetById(datasetId); + if (dataset) { + let arrayDataset = Array.from(dataset); + for (let ind = arrayDataset.length - 1; ind >= 0; ind--) { + let point = arrayDataset[ind]; + if (point.value === null) { + if (currentBreaks === 0) { + currentBreaksEnd = point.date; + } + currentBreaks++; + } else { + break; + } + } + } + + if (currentBreaksEnd === null) { + return "absense"; + } + return currentBreaksEnd?.format(renderInfo.dateFormat); }, average: function (renderInfo: RenderInfo, datasetId: number) { let dataset = renderInfo.datasets.getDatasetById(datasetId); @@ -185,10 +430,14 @@ export function resolveTemplate(template: string, renderInfo: RenderInfo) { let result = fn(renderInfo, 0); // calculate result let strResult = "{{NA}}"; if (typeof result !== "undefined" && result !== null) { - if (Number.isInteger(result)) { - strResult = result.toFixed(0); - } else { - strResult = result.toFixed(2); + if (typeof result === "number") { + if (Number.isInteger(result)) { + strResult = result.toFixed(0); + } else { + strResult = result.toFixed(2); + } + } else if (typeof result === "string") { + strResult = result; } } diff --git a/src/main.ts b/src/main.ts index 5ac6bbc..0d2f7fe 100644 --- a/src/main.ts +++ b/src/main.ts @@ -682,6 +682,9 @@ export default class Tracker extends Plugin { // We still create a dataset for xDataset, // to keep the sequence and order of targets let dataset = datasets.createDataset(query, renderInfo); + // Add number of targets to the dataset + // Number of targets has been accumulated while collecting data + dataset.addNumTargets(query.getNumTargets()); for ( let curDate = renderInfo.startDate.clone(); curDate <= renderInfo.endDate; @@ -698,22 +701,23 @@ export default class Tracker extends Plugin { }); if (queryValuePairs.length > 0) { // Merge values of the same day same query - let pair = queryValuePairs[0]; - let value = 0; - let hasValue = false; + let value = null; for ( let indPair = 0; indPair < queryValuePairs.length; indPair++ ) { if (queryValuePairs[indPair].value !== null) { - value += queryValuePairs[indPair].value; - hasValue = true; + if (value === null) { + value = queryValuePairs[indPair].value; + } else { + value += queryValuePairs[indPair].value; + } } } // console.log(hasValue); // console.log(value); - if (hasValue) { + if (value) { dataset.setValue(curDate, value); } } From 09c9cbfcb4c95ccedf85e2fa7350b23b02ec7547 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Thu, 3 Jun 2021 11:55:32 +0800 Subject: [PATCH 041/402] Allow parsing date wrapped in wiki brackets --- examples/data/Tables.md | 6 +++--- src/collecting.ts | 22 ++++++++++++++++++---- src/main.ts | 6 ++++-- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/examples/data/Tables.md b/examples/data/Tables.md index 1b568c5..826db9b 100644 --- a/examples/data/Tables.md +++ b/examples/data/Tables.md @@ -8,11 +8,11 @@ | 2021-05-03 | 62.2 | 20.3 | 18.3 | 2021-05-04 | 62.3 | 20.4 | 18.2 | 2021-05-05 | 62.2 | 20.2 | 18.4 -| 2021-05-06 | 62.4 | 21.0 | 18.4 +| [[2021-05-06]] | 62.4 | 21.0 | 18.4 | 2021-05-07 | 62.6 | 20.9 | 18.3 | | 2021-05-08 | 62.3 | 20.9 | 18.2 -| 2021-05-09 | 62.2 | 20.3 | 18.5 -| 2021-05-10 | 62.1 | 20.8 | 18.9 +| [[2021-05-09]] | 62.2 | 20.3 | 18.5 +| [[2021-05-10]] | 62.1 | 20.8 | 18.9 ## Running Records diff --git a/src/collecting.ts b/src/collecting.ts index 078f09e..0350f29 100644 --- a/src/collecting.ts +++ b/src/collecting.ts @@ -11,6 +11,18 @@ import { } from "./data"; import * as helper from "./helper"; +export function strToDate(strDate: string, dateFormat: string) { + if ( + strDate.length > 4 && + strDate.startsWith("[[") && + strDate.endsWith("]]") + ) { + strDate = strDate.substring(2, strDate.length - 2); + } + + return window.moment(strDate, dateFormat, true); +} + export function getDateFromFilename(file: TFile, renderInfo: RenderInfo) { let fileBaseName = file.basename; @@ -52,7 +64,8 @@ export function getDateFromFrontmatter( if (frontMatter) { if (helper.deepValue(frontMatter, query.getTarget())) { let strDate = helper.deepValue(frontMatter, query.getTarget()); - date = window.moment(strDate, renderInfo.dateFormat, true); + + date = strToDate(strDate, renderInfo.dateFormat); // console.log(date); } } @@ -91,7 +104,7 @@ export function getDateFromTag( typeof match.groups.values !== "undefined" ) { let strDate = match.groups.values; - date = window.moment(strDate, renderInfo.dateFormat, true); + date = strToDate(strDate, renderInfo.dateFormat); if (date.isValid()) { break; } @@ -126,7 +139,8 @@ export function getDateFromText( ) { let strDate = match.groups.value.trim(); // console.log(strDate); - date = window.moment(strDate, renderInfo.dateFormat, true); + + date = strToDate(strDate, renderInfo.dateFormat); if (date.isValid()) { break; } @@ -167,7 +181,7 @@ export function getDateFromDvField( typeof match.groups.values !== "undefined" ) { let strDate = match.groups.values.trim(); - date = window.moment(strDate, renderInfo.dateFormat, true); + date = strToDate(strDate, renderInfo.dateFormat); if (date.isValid()) { break; } diff --git a/src/main.ts b/src/main.ts index 0d2f7fe..cfae891 100644 --- a/src/main.ts +++ b/src/main.ts @@ -518,8 +518,10 @@ export default class Tracker extends Plugin { let dataRowSplitted = dataRow.split("|"); if (columnXDataset < dataRowSplitted.length) { let data = dataRowSplitted[columnXDataset].trim(); - - let date = window.moment(data, renderInfo.dateFormat, true); + let date = collecting.strToDate( + data, + renderInfo.dateFormat + ); if (date.isValid()) { xValues.push(date); From d98178d6e3af0e6c2d405baffccafd48961fefbd Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Thu, 3 Jun 2021 17:10:07 +0800 Subject: [PATCH 042/402] Allow using a dash to replace a space for dvField searchTarget --- src/collecting.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/collecting.ts b/src/collecting.ts index 0350f29..c6b1f79 100644 --- a/src/collecting.ts +++ b/src/collecting.ts @@ -165,12 +165,16 @@ export function getDateFromDvField( if (query.getParentTarget()) { dvTarget = query.getParentTarget(); // use parent tag name for multiple values } + // Dataview ask user to add dashes for spaces as search target + // So a dash may stands for a real dash or a space + dvTarget = dvTarget.replace("-", "[\\s\\-]"); + // Test this in Regex101 // (^|\s)\*{0,2}dvTarget\*{0,2}(::\s*(?[\d\.\/\-\w,@;\s]*))(\s|$) let strHashtagRegex = "(^|\\s)\\*{0,2}" + dvTarget + - "\\*{0,2}(::\\s*(?[\\d\\.\\/\\-\\w,@;\\s]*))(\\s|$)"; + "\\*{0,2}(::\\s*(?[\\d\\.\\/\\-\\w,@;\\s]*))(\r?\n|\r)"; // console.log(strHashtagRegex); let hashTagRegex = new RegExp(strHashtagRegex, "gm"); let match; @@ -576,12 +580,16 @@ export function collectDataFromDvField( if (query.getParentTarget()) { dvTarget = query.getParentTarget(); // use parent tag name for multiple values } + // Dataview ask user to add dashes for spaces as search target + // So a dash may stands for a real dash or a space + dvTarget = dvTarget.replace("-", "[\\s\\-]"); + // Test this in Regex101 // (^|\s)\*{0,2}dvTarget\*{0,2}(::\s*(?[\d\.\/\-\w,@;\s]*))(\s|$) let strHashtagRegex = "(^|\\s)\\*{0,2}" + dvTarget + - "\\*{0,2}(::\\s*(?[\\d\\.\\/\\-\\w,@;\\s]*))(\\s|$)"; + "\\*{0,2}(::\\s*(?[\\d\\.\\/\\-\\w,@;\\s]*))(\r?\n|\r)"; // console.log(strHashtagRegex); let hashTagRegex = new RegExp(strHashtagRegex, "gm"); let match; @@ -603,7 +611,7 @@ export function collectDataFromDvField( // console.log("single-value"); let toParse = splitted[0]; let retParse = helper.parseFloatFromAny(toParse); - if (retParse.value) { + if (retParse.value !== null) { if (retParse.type === ValueType.Time) { tagMeasure = retParse.value; tagExist = true; @@ -628,7 +636,7 @@ export function collectDataFromDvField( // console.log("multiple-values"); let toParse = splitted[query.getAccessor()].trim(); let retParse = helper.parseFloatFromAny(toParse); - if (retParse.value) { + if (retParse.value !== null) { if (retParse.type === ValueType.Time) { tagMeasure = retParse.value; tagExist = true; From 57b545303e85489d727dbcb7e9d46bf1f8dbf75d Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Thu, 3 Jun 2021 17:12:21 +0800 Subject: [PATCH 043/402] Fix missing zero values --- src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index cfae891..12604e1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -719,7 +719,7 @@ export default class Tracker extends Plugin { } // console.log(hasValue); // console.log(value); - if (value) { + if (value !== null) { dataset.setValue(curDate, value); } } From 4504c984479cfdcd742e620ee245741325dc8d19 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Thu, 3 Jun 2021 20:28:11 +0800 Subject: [PATCH 044/402] Remove TestCalendar.md temporarily --- examples/TestCalendar.md | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 examples/TestCalendar.md diff --git a/examples/TestCalendar.md b/examples/TestCalendar.md deleted file mode 100644 index dac62be..0000000 --- a/examples/TestCalendar.md +++ /dev/null @@ -1,11 +0,0 @@ -# Test Calendar - -``` tracker -searchType: tag -searchTarget: meditation -folder: diary -month: - startWeekOn: -``` - -Please also check those search targets in markdown files under folder 'diary'. \ No newline at end of file From 05652fa0532a847f5263e30ce599d07a98f005dd Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Thu, 3 Jun 2021 21:15:54 +0800 Subject: [PATCH 045/402] Add warning for deprecated template variables --- src/expr.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/expr.ts b/src/expr.ts index 506566a..61d0c11 100644 --- a/src/expr.ts +++ b/src/expr.ts @@ -42,13 +42,17 @@ let fnSet = { let dataset = renderInfo.datasets.getDatasetById(datasetId); return d3.sum(dataset.getValues()); }, - // count --> deprecated + count: function (renderInfo: RenderInfo, datasetId: number) { + return "Deprecated template variable: count"; + }, // number of occurrences of a target in a dataset numTargets: function (renderInfo: RenderInfo, datasetId: number) { let dataset = renderInfo.datasets.getDatasetById(datasetId); return dataset.getNumTargets(); }, - // days --> deprecated + days: function (renderInfo: RenderInfo, datasetId: number) { + return "Deprecated template variable: days"; + }, numDays: function (renderInfo: RenderInfo, datasetId: number) { let dataset = renderInfo.datasets.getDatasetById(datasetId); return dataset.getLength(); @@ -200,7 +204,9 @@ let fnSet = { } return maxBreaksEnd?.format(renderInfo.dateFormat); }, - // lastStreak --> deprecated + lastStreak: function(renderInfo: RenderInfo, datasetId: number) { + return "Deprecated template variable: lastStreak"; + }, currentStreak: function (renderInfo: RenderInfo, datasetId: number) { let currentStreak = 0; let dataset = renderInfo.datasets.getDatasetById(datasetId); @@ -349,6 +355,7 @@ let fnSet = { }; export function resolveTemplate(template: string, renderInfo: RenderInfo) { + //console.log("resolveTemplate"); let replaceMap: { [key: string]: string } = {}; // Loop over fnSet, prepare replaceMap Object.entries(fnSet).forEach(([fnName, fn]) => { From 45597eb3665eae3bc6a9c5c6c76f3642d69c9b47 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Thu, 3 Jun 2021 21:23:22 +0800 Subject: [PATCH 046/402] Remove graph not completed before renderErrorMessage --- src/rendering.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/rendering.ts b/src/rendering.ts index 22d2b4b..446332c 100644 --- a/src/rendering.ts +++ b/src/rendering.ts @@ -1529,6 +1529,9 @@ function renderBarChart(canvas: HTMLElement, renderInfo: RenderInfo) { } export function renderErrorMessage(canvas: HTMLElement, errorMessage: string) { + // Remove graph not completed + let graph = d3.select(canvas).select("#svg").remove(); + let svg = d3 .select(canvas) .append("div") From 9c2f232341eab6aa18ab8e36c6f3b2494f34d3a6 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Thu, 3 Jun 2021 21:40:59 +0800 Subject: [PATCH 047/402] Fix rendering error messages for the bullet value --- examples/TestBullet.md | 4 ++-- src/bullet.ts | 8 ++++---- src/expr.ts | 10 ++++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/examples/TestBullet.md b/examples/TestBullet.md index d4eca5d..733e1f6 100644 --- a/examples/TestBullet.md +++ b/examples/TestBullet.md @@ -12,7 +12,7 @@ bullet: orientation: horizontal range: 10, 20, 40 rangeColor: darkgray, silver, lightgray - value: "{{lastBreaks}}" + value: "{{currentBreaks}}" valueUnit: times valueColor: '#69b3a2' showMarker: true @@ -33,7 +33,7 @@ bullet: range: 30, 60, 100 rangeColor: darkgray, silver, lightgray value: "{{sum}}" - valueUnit: % + valueUnit: times valueColor: steelblue showMarker: true markerValue: 80 diff --git a/src/bullet.ts b/src/bullet.ts index 864df72..b370e6d 100644 --- a/src/bullet.ts +++ b/src/bullet.ts @@ -446,11 +446,11 @@ function renderBar( let bulletInfo = renderInfo.bullet; if (!bulletInfo) return; - let strActualValue = bulletInfo.value; - strActualValue = expr.resolveTemplate(strActualValue, renderInfo); - let actualValue = parseFloat(strActualValue); + let retActualValue = bulletInfo.value; + retActualValue = expr.resolveTemplate(retActualValue, renderInfo); + let actualValue = parseFloat(retActualValue); if (Number.isNaN(actualValue)) { - errorMessage = "Invalid paramter 'actualValue'"; + errorMessage = "Invalid input value: " + retActualValue; return errorMessage; } let valueColor = bulletInfo.valueColor; diff --git a/src/expr.ts b/src/expr.ts index 61d0c11..43969f6 100644 --- a/src/expr.ts +++ b/src/expr.ts @@ -43,7 +43,7 @@ let fnSet = { return d3.sum(dataset.getValues()); }, count: function (renderInfo: RenderInfo, datasetId: number) { - return "Deprecated template variable: count"; + return "deprecated template variable 'count'"; }, // number of occurrences of a target in a dataset numTargets: function (renderInfo: RenderInfo, datasetId: number) { @@ -51,7 +51,7 @@ let fnSet = { return dataset.getNumTargets(); }, days: function (renderInfo: RenderInfo, datasetId: number) { - return "Deprecated template variable: days"; + return "deprecated template variable 'days'"; }, numDays: function (renderInfo: RenderInfo, datasetId: number) { let dataset = renderInfo.datasets.getDatasetById(datasetId); @@ -204,8 +204,8 @@ let fnSet = { } return maxBreaksEnd?.format(renderInfo.dateFormat); }, - lastStreak: function(renderInfo: RenderInfo, datasetId: number) { - return "Deprecated template variable: lastStreak"; + lastStreak: function (renderInfo: RenderInfo, datasetId: number) { + return "deprecated template variable 'lastStreak'"; }, currentStreak: function (renderInfo: RenderInfo, datasetId: number) { let currentStreak = 0; @@ -465,6 +465,8 @@ export function resolveTemplate(template: string, renderInfo: RenderInfo) { } else { strResult = result.toFixed(2); } + } else if (typeof result === "string") { + strResult = result; } replaceMap[strReplaceRegex] = strResult; From ae81893fa323ef4a66228ff3a26cbeac33d2d945 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Sat, 5 Jun 2021 16:19:52 +0800 Subject: [PATCH 048/402] Strip # character if searchTarget for 'tag' startsWidth it --- src/collecting.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/collecting.ts b/src/collecting.ts index c6b1f79..956fc0d 100644 --- a/src/collecting.ts +++ b/src/collecting.ts @@ -408,6 +408,9 @@ export function collectDataFromInlineTag( if (query.getParentTarget()) { tagName = query.getParentTarget(); // use parent tag name for multiple values } + if (tagName.length > 1 && tagName.startsWith("#")) { + tagName = tagName.substring(1); + } let strHashtagRegex = "(^|\\s)#" + tagName + From 2c7eeb0d03e9f62bcc3460250f241796218405b1 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Sat, 5 Jun 2021 16:20:34 +0800 Subject: [PATCH 049/402] Update examples --- examples/HabitTracker.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/HabitTracker.md b/examples/HabitTracker.md index 7566007..b517eed 100644 --- a/examples/HabitTracker.md +++ b/examples/HabitTracker.md @@ -41,7 +41,7 @@ searchType: tag searchTarget: meditation folder: diary summary: - template: "Longest Streak: {{maxStreak}} day(s)\nLongest Breaks: {{maxBreaks}} day(s)\nLast streak: {{lastStreak}} day(s)" + template: "Longest Streak: {{maxStreak}} day(s)\nLongest Breaks: {{maxBreaks}} day(s)\nLast streak: {{currentStreak}} day(s)" ``` ### CleanUp @@ -51,7 +51,7 @@ searchTarget: clean-up folder: diary endDate: 2021-01-31 summary: - template: "Last Break: {{lastBreaks}} day(s)" + template: "Last Break: {{currentBreaks}} day(s)" ``` ### Work log From 5067a049366fce046f3702e178802dee69a3f732 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Sat, 5 Jun 2021 16:21:05 +0800 Subject: [PATCH 050/402] Update documents --- README.md | 189 ++------------------------------------ docs/Commands.md | 5 +- docs/Concepts.md | 20 ++++ docs/Examples.md | 36 ++++++++ docs/InputParameters.md | 164 +++++++++++++++++++-------------- docs/Installation.md | 11 +++ docs/Questions.md | 47 ++++++++++ docs/ReleaseNotes.md | 77 ++++++++++++++++ docs/RoadMap.md | 37 ++++++++ docs/Settings.md | 10 ++ docs/TargetEvaluation.md | 120 +++++++++++++++--------- docs/TemplateVariables.md | 64 +++++++------ docs/YAML.md | 65 +++++++++++++ 13 files changed, 526 insertions(+), 319 deletions(-) create mode 100644 docs/Concepts.md create mode 100644 docs/Examples.md create mode 100644 docs/Installation.md create mode 100644 docs/Questions.md create mode 100644 docs/ReleaseNotes.md create mode 100644 docs/RoadMap.md create mode 100644 docs/Settings.md create mode 100644 docs/YAML.md diff --git a/README.md b/README.md index c9c3b86..e373ea0 100644 --- a/README.md +++ b/README.md @@ -3,43 +3,9 @@ -This is an [Obsidian](https://obsidian.md/) plugin that helps you do tracking in daily notes and represent the data comprehensively. Here are examples showing what you can track: +This is an [Obsidian](https://obsidian.md/) plugin that helps you do tracking in notes [over dates](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/Questions.md) and represent the collected data comprehensively. -| From↓ | Track Targets↓ | by Tracker↓ | Get (O)ccurrence/(V)alue | -|:--------|:-------|:---------|:--:| -| content | #meditation | searchType: tag
searchTarget: meditation | O | -| frontmatter | ---
tags: meditation
--- | searchType: tag
searchTarget: meditation | O | -| content | #weight:60.5kg | searchType: tag
searchTarget: weight | V | -| content | #finance/bank1/transfer:100USD | searchType: tag
searchTarget: finance/bank1/transfer | V | -| content | #finance/bank1/transfer:100USD
#finance/bank1/income:80USD
#finance/bank1/outcome:-120USD | searchType: tag
searchTarget: finance/bank1 | V | -| content | #blood-pressure:180/120 | searchType: tag
searchTarget: blood-pressure[0], blood-pressure[1] | V | -| content | dvTarget:: 20.5 | searchType: dvField
searchTarget: dvTarget | V | -| content | dvTarget:: 20.5/30.5 | searchType: dvField
searchTarget: dvTarget[0], dvTarget[1] | V | -| content | dvTarget:: 20.5, 30.5 | searchType: dvField
searchTarget: dvTarget[0], dvTarget[1]
separator: 'comma' | V | -| frontmatter | ---
mood: 10
--- | searchType: frontmatter
searchTarget: mood | V | -| frontmatter | ---
bp: 184.4/118.8
--- | searchType: frontmatter
searchTarget: bp[0], bp[1] | V | -| frontmatter | ---
bp: 184.4, 118.8
--- | searchType: frontmatter
searchTarget: bp[0], bp[1]
separator: 'comma' | V | -| frontmatter | ---
bp: [184.4, 118.8]
--- | searchType: frontmatter
searchTarget: bp[0], bp[1] | V | -| content | [[journal]] | searchType: wiki
searchTarget: journal | O | -| content | ⭐ | searchType: text
searchTarget: ⭐ | O | -| content | love | searchType: text
searchTarget: love | O | -| content | test@gmail.com
test@hotmail.com | searchType: text
serchTarget: '.+\\@.+\\..+' | O | -| content | #weightlifting: 50 | searchType: text
searchTarget: 'weightlifting: (?\[\\-]?[0-9]+[\\.][0-9]+\|[\\-]?[0-9]+)' | V | -| content | I walked 10000 steps today. | searchType: text
searchTarget: 'walked\\s+(?\[0-9]+)\\s+steps' | V | -| content | myvalues 1/2/3 | searchType: text
searchTarget: 'myvalues\\s+(?\[0-9]+)/([0-9]+)/([0-9]+), myvalues\\s+([0-9]+)/(?\[0-9]+)/([0-9]+), myvalues\\s+([0-9]+)/([0-9]+)/(?\[0-9]+)' | V | -| content | { a table filled with dates and values }
[example table](https://github.com/pyrochlore/obsidian-tracker/blob/master/examples/data/Tables.md) | searchType: table
searchTarget: filePath[0][0], filePath[0][1] | V | -| content | { a table filled with dates and values }
[example table](https://github.com/pyrochlore/obsidian-tracker/blob/master/examples/data/Tables.md) | searchType: table
searchTarget: filePath[1][0], filePath[1][1][0], filePath[1][1][1] | V | - -## Installation -### Install from Obsidian Settings Panel -1. From Settings Panel (the icon at the bottom left corner) -> Options -> Community plugins, set 'safe mode' to off. -2. In the 'Community plugins' section, click 'Browse' and find the obsidian-tracker plugin by the name 'Tracker'. -3. Press the button 'Install' and wait for the completion of the install. -4. In the section 'Installed plugins', find and enable the plugin just installed. -5. Enjoy tracking. - -### Manual Installation -Download the latest [release](https://github.com/pyrochlore/obsidian-tracker/releases). Extract and put the three files (main.js, manifest.json, styles.css) to folder '{{obsidian_vault}}/.obsidian/plugins/obsidian-tracker'. +[Here](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/Examples.md) is a simplified table of examples showing what you can track. ## Usage 1. Have some targets you want to track in daily notes. Take a look at [Target Evaluation](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/TargetEvaluation.md) for what you can track and how targets are evaluated as numbers. @@ -51,150 +17,13 @@ Download the latest [release](https://github.com/pyrochlore/obsidian-tracker/rel For more use cases, please open the [examples](https://github.com/pyrochlore/obsidian-tracker/tree/master/examples) folder in obsidian with this plugin installed and enabled. -## Concepts -This plugin was designed to read code blocks in [YAML format](https://en.wikipedia.org/wiki/YAML). The key-value pairs in the code blocks tell the plugin what data to collect and how to represent the result. - -[Here](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/InputParameters.md) are all the parameters (key-value pairs) defined in this plugin. They are used for collecting data, evaluating targets, data preprocessing, and rendering output. - -### Collecting Data -Providing parameters '**searchType**' and '**searchTarget**' is the minimum requirement for a successful data collection. The value of the key '**searchType**' can be '**tag**', '**frontmatter**', '**wiki**', '**dvField**', '**table**', or '**text**' and the cooresponding '**searchTarget**' should be provided according to the specified type. - -#### Multiple Targets -From version 1.3, you can provide multiple search targets by entering an array of targets separated by a comma. Each of the targets will be identified then the corresponding value will be evaluated and form a dataset indexed by the order in the array (zero-based indexing). - -``` -searchTarget: target0, target1, target2, ...... -searchType: type0, type1, type2, ..... -line: - lineColor: red, blue, yello -``` -Above is an example of multiple target searching. Multiple targets are provided and separated by a comma. If they have a different searchType, provide the same number of types in the same order. In this case, the second search target 'target1' with index 1 has type 'type1'. Other parameters that accept multiple values (e.g. lineColor) can also be provided and will be applied to the corresponding dataset. - -#### Multiple Values -Multiple values under a target (value tuple) separated by a slash, e.g. #bloodpressure:180/120mmHg, are also got supported in version 1.3.0. To identify a specific value as a target, use an accessor with bracket notation where the value in the bracket is the index by the order of values. In this case, they are bloodpressure[0] and bloodpressure[1]. You can find the example of this [here](https://github.com/pyrochlore/obsidian-tracker/blob/master/examples/BloodPressureTracker.md). You can also use a custom separator by using the parameter '**separator**'. - -### Target Evaluation -Depends on the '**searchType**' and the '**searchTarget**' you provided, the evaluation of a target would be different. Simply speaking, you can track the occurrences of a target or the value attached/embedded in it. - -To see the detail about the target evaluation, please check the document [Target Evaluation](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/TargetEvaluation.md). - -### Rendering Output -Currently, obsidian-tracker provides three kinds of rendering output, the default one 'line' for a line chart, 'bar' for a bar chart, and 'summary' for a text block. - -For 'line' or 'bar' output, the plugin will generate a customizable chart. For 'summary' output, a text block based on your '**template**' parameter will be generated. You can also use [pre-defined template variables](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/TemplateVariables.md) in the template. - -Description for the chart and summary related paremeters can be found [here](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/InputParameters.md#parameters-for-common-chart) and [here](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/InputParameters.md#parameters-for-summary) respectively. - -### Plugin Settings -You can set the default folder location and date format in the plugin's settings panel. You can also override them by key '**folder**' and '**dateFormat**' in the code block respectively. - -| Setting Item | Description | Default | -|:--------|:-------|:---------| -| Default folder location | The folder your daily notes reside | Root of the vault | -| Default date format | The date format of your daily note title | 'YYYY-MM-DD' | - -For more information about the dateFormat settings, check the [TestDateFormats example](https://github.com/pyrochlore/obsidian-tracker/blob/master/examples/TestDateFormats.md) and [moment.js string format](https://momentjs.com/docs/#/parsing/string-format/). - -## Release Notes -### v1.5.1 -- Fixed labels not shown in light theme -- Enhanced error handling for searchType 'table' - -### v1.5.0 -- New searchType 'table', searching records from a given table -- New searchType 'dvField', searching the inline fields used with Dataview plugin -- Enhance multiple values extraction - - Allow using multiple values in searchType 'text' - - Allow using array values in searchType 'frontmatter' - - Allow using multiple values in searchType 'dvField' - - Allow using multiple values in searchType 'table' - - Allow using custom separator for multiple values extraction -- Improved performance -- Reduced package size - -### v1.4.1 -- Enhanced error handling - -### v1.4.0 -- Add a new parameter (fixedScale) for the scaling of the output chart -- Add a new parameter (fitPanelWidth) to enable/disable the auto-scaling of the output chart -- Add a new parameter (margin) to help to position the chart -- Tested in Obsidian mobile app on iPhone and iPad -- Fixed bugs - -### v1.3.0 -- Support reading and rendering multiple targets -- Support reading and rendering multiple values (a tuple of values) under a target -- New output type 'bar', rendering bar chart -- Add a legend for the chart output -- Fixed bugs - -### v1.2.1 -- Fixed files with the specified dateFormat are not recognized -- Restored the plugin's settings panel for dateFormat and folder - -### v1.2.0 -- Enable using regular expression in text searching -- New search type 'frontmatter', searching for key-value pairs in the front matter -- New search type 'wiki', searching for wiki links -- Reduced package size - -### v1.1.0 -- New output type 'summary' -- Add commands help create Tracker code blocks -- Relaxed the regex for searching tags, allowing tags embedded in sentences -- Fixed issues - -### v1.0.2 -- Fixed the searching of nested tag in frontmatter -- Reduced the package size by using the module from Obsidian - -### v1.0.1 -- Remove dependencies to Node.js modules -- Add example markdown files - -### v1.0.0 -First version released at 2021-03-23 - -- Track simple tags, value-attached tags, and texts using code blocks -- Represent the tracked data in a customizable line chart -- Allow tracking in-line tags and tags in frontmatter -- Allow tracking nested tags - -## Road Map -- Data Collecting - - [x] Support tracking key-value pairs in frontmatter - - [x] Support searching text using regular expression - - [x] Support multiple targets and multiple values. - - [x] Add a parameter xDataset to identify targets to be used as x values - - [ ] Allow tracking date and time values - - [x] Get data from a table - - [x] Collect data from dataview plugin's inline fields - - [ ] Allow manual data input, includes template varialbes embeded in - - [ ] New search type 'codeblock' -- Output Type and Graph - - [x] New output type 'summary', analyzes the input data and represents it using a user-defined text template. - - [x] New output type 'bar', renders bar chart. - - [x] New output type 'table', lists the search result in a formatted table. - - [ ] New output type 'heatmap', works like Github calendar heatmap. - - [x] Add parameters for adjusting the size of the graph. - - [ ] Multiple outputs from one code block - - [ ] Support graphs showing the correlation between sets of data. - - [ ] Add a 'targetDataSet' key to graph, allow the graph drawing selected dataset. - - [ ] Evaluate template parameters by predefined function and arithmetics. -- Helper - - [x] Add Commands help create Tracker blocks. - - [ ] Add an 'Explode' button to the rendered blocks, it will replace the code block with the rendered result. - - [ ] Add a helper panel for adding frequently used tracking targets to article. -- Data Processing - - [ ] Allow arithmetics operation on dataset and generate new datasets - - [ ] Add data post-process function, e.g. 'moving average' -- Performance - - [ ] Use PixiJS to do rendering - -And more ... - -P.S. Features may not be implemented in the order above. +## More Detail You May Want to Know +- [Installation](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/Installation.md): Install the plugin from Obsidian or install it manually. +- [Concepts](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/Concepts.md): Explain how this plugin works and what to setup +- [Plugin Settings](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/Settings.md) +- [Release Notes](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/ReleaseNotes.md) +- [Road Map](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/RoadMap.md) +- [Frequently Asked Questions](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/Questions.md) ## Support - If you like this plugin or want to support further development, you can [Buy Me a Coffee](https://www.buymeacoffee.com/pyrochlore). diff --git a/docs/Commands.md b/docs/Commands.md index 9efc36a..0fbb182 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -1,6 +1,5 @@ # Commands -To smooth the process of making trackers, obsidian-tracker provides three commands, "Add Line Chart Tracker", "Add Bar Chart Tracker", and "Add Summary Tracker; Just type Ctrl/Cmd+P to activate the command palette, then type "Tracker" to search the commands. -After a command is executed, a code block will be added to the next line below your cursor. The added code block will contain the most frequently used keys. To see the complete list of input parameters and description, please check [this document](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/InputParameters.md). +To smooth the process of making trackers, obsidian-tracker provides three commands, "Add Line Chart Tracker", "Add Bar Chart Tracker", and "Add Summary Tracker; Just type Ctrl/Cmd+P to activate the command palette, then type "Tracker" to search these commands. -Now you can fill the key-value pairs to fit your requirement and then press the preview button to see the wonderful tracking result from your data. \ No newline at end of file +After a command is executed, a code block will be added to the next line below your cursor position for you. The added code block will contain the most frequently used keys. To see the complete list of input parameters and description, please check [this document](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/InputParameters.md). \ No newline at end of file diff --git a/docs/Concepts.md b/docs/Concepts.md new file mode 100644 index 0000000..aad2975 --- /dev/null +++ b/docs/Concepts.md @@ -0,0 +1,20 @@ +# Concepts + +This plugin was designed to read code blocks in [YAML format](https://en.wikipedia.org/wiki/YAML). The key-value pairs in the code blocks tell the plugin what data to collect and how to represent the result. + +[Here](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/InputParameters.md) are all the parameters (key-value pairs) defined in this plugin. They are used for collecting data, evaluating targets, data preprocessing, and rendering output. + +### Collecting Data +Providing parameters `searchType` and `searchTarget` is the minimum requirement for a successful data collection. `searchType` can be `tag`, `frontmatter`, `wiki`, `dvField`, `table`, `fileMeta`,or `text`. Then the cooresponding `searchTarget` should be provided according to the specified type. + +### Target Evaluation +Depends on the `searchType` and the `searchTarget` you provided, the evaluation of a target would be different. Simply speaking, you can track the occurrences of a target or the value attached/embedded in it. + +To see the detail about the target evaluation, please check the document [Target Evaluation](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/TargetEvaluation.md). + +### Rendering Output +Currently, obsidian-tracker provides three kinds of rendering output, the default one 'line' for a line chart, 'bar' for a bar chart, and 'summary' for a text block. + +For 'line' or 'bar' output, the plugin will generate a customizable chart. For 'summary' output, a text block based on your '**template**' parameter will be generated. You can also use [pre-defined template variables](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/TemplateVariables.md) in the template. + +Description for the chart and summary related paremeters can be found [here](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/InputParameters.md#parameters-for-common-chart) and [here](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/InputParameters.md#parameters-for-summary) respectively. \ No newline at end of file diff --git a/docs/Examples.md b/docs/Examples.md new file mode 100644 index 0000000..8f9be44 --- /dev/null +++ b/docs/Examples.md @@ -0,0 +1,36 @@ +# Examples + +The following table lists use cases with information of target location (Location), target to track (Target to Track), simplified tracker block (Tracker), and what kind of data you will get ((((O)ccurence/(V)alue). Various kinds of full examples can also be found in the following section after the table. + +## Table of Use Cases + +| Location | Target to Track | Tracker | Get (O)ccurrence/(V)alue | +|:--------|:-------|:---------|:--:| +| content | #meditation | searchType: tag
searchTarget: meditation | O | +| frontmatter | ---
tags: meditation
--- | searchType: tag
searchTarget: meditation | O | +| content | #weight:60.5kg | searchType: tag
searchTarget: weight | V | +| content | #finance/bank1/transfer:100USD | searchType: tag
searchTarget: finance/bank1/transfer | V | +| content | #finance/bank1/transfer:100USD
#finance/bank1/income:80USD
#finance/bank1/outcome:-120USD | searchType: tag
searchTarget: finance/bank1 | V | +| content | #blood-pressure:180/120 | searchType: tag
searchTarget: blood-pressure[0], blood-pressure[1] | V | +| content | dvTarget:: 20.5 | searchType: dvField
searchTarget: dvTarget | V | +| content | dvTarget:: 20.5/30.5 | searchType: dvField
searchTarget: dvTarget[0], dvTarget[1] | V | +| content | dvTarget:: 20.5, 30.5 | searchType: dvField
searchTarget: dvTarget[0], dvTarget[1]
separator: 'comma' | V | +| frontmatter | ---
mood: 10
--- | searchType: frontmatter
searchTarget: mood | V | +| frontmatter | ---
bp: 184.4/118.8
--- | searchType: frontmatter
searchTarget: bp[0], bp[1] | V | +| frontmatter | ---
bp: 184.4, 118.8
--- | searchType: frontmatter
searchTarget: bp[0], bp[1]
separator: 'comma' | V | +| frontmatter | ---
bp: [184.4, 118.8]
--- | searchType: frontmatter
searchTarget: bp[0], bp[1] | V | +| frontmatter | ---
clock-in: 10:45
clock-out: 20:51
--- | searchType: frontmatter
searchTarget: clock-in, clock-out | V | +| content | [[journal]] | searchType: wiki
searchTarget: journal | O | +| content | ⭐ | searchType: text
searchTarget: ⭐ | O | +| content | love | searchType: text
searchTarget: love | O | +| content | test@gmail.com
test@hotmail.com | searchType: text
serchTarget: '.+\\@.+\\..+' | O | +| content | #weightlifting: 50 | searchType: text
searchTarget: 'weightlifting: (?\[\\-]?[0-9]+[\\.][0-9]+\|[\\-]?[0-9]+)' | V | +| content | I walked 10000 steps today. | searchType: text
searchTarget: 'walked\\s+(?\[0-9]+)\\s+steps' | V | +| content | myvalues 1/2/3 | searchType: text
searchTarget: 'myvalues\\s+(?\[0-9]+)/([0-9]+)/([0-9]+), myvalues\\s+([0-9]+)/(?\[0-9]+)/([0-9]+), myvalues\\s+([0-9]+)/([0-9]+)/(?\[0-9]+)' | V | +| table content | { a table filled with dates and values }
[example table](https://github.com/pyrochlore/obsidian-tracker/blob/master/examples/data/Tables.md) | searchType: table
searchTarget: filePath[0][0], filePath[0][1] | V | +| talbe content | { a table filled with dates and values }
[example table](https://github.com/pyrochlore/obsidian-tracker/blob/master/examples/data/Tables.md) | searchType: table
searchTarget: filePath[1][0], filePath[1][1][0], filePath[1][1][1] | V | +| file meta | meta data from files (size, cDate, mDate) | searchType: fileMeta
searchTarget: size | V | + +## Full examples + +Full tracker code blocks can be found [here](https://github.com/pyrochlore/obsidian-tracker/tree/master/examples) and the corresponding notes can be found under folder 'diary' and 'data'. \ No newline at end of file diff --git a/docs/InputParameters.md b/docs/InputParameters.md index 6f55faa..e8c33bd 100644 --- a/docs/InputParameters.md +++ b/docs/InputParameters.md @@ -1,88 +1,116 @@ # Input Parameters -Obsidian-tracker parses key-value pairs in YAML format as input parameters. The following sections show a complete list of all the input parameters. +Obsidian-tracker parses key-value pairs in your code block in YAML format and uses them as input parameters. The minimum requirements for parameters are `searchType`, `searchTarget` and one output parameter (`line`, `bar`, `frontmatter`, `fileMeta`, `wiki`, `table`, or `text`). -Notice that some of the parameters can accept one value for each target, thus the maximum number of values of the parameters equals the number of targets (NT). If the number of values is less than the number of targets, the plugin will use the previously provided value or use the default value if nothing is provided. +## Array Input -Also, some y-axis related parameters like yMin, yMax, and yAxisLabel accept one value for each y-axis (left and right). If you only use one axis, or the settings for the two axes are the same, only one value is required. If you need the two axes to have different settings, assign two values to them. The first one will be used for the left axis and the second one for the right axis. +Some of the parameters can accept more than one value for each target, thus the maximum number of values of the parameter equals the number of targets (NT). If the number of values is less than the number of targets, the plugin will use the previously provided value or use the default value if nothing is provided. -## Root Parameters -These key-value pairs are placed under the YAML root. +Also, some y-axis related parameters for chart (`line` or `bar`), like `yMin`, `yMax`, and `yAxisLabel` accept one value for each y-axis (`left` and `right`). If you only use one axis, or the values for the two axes are the same, only one value is required. If you need the two axes to have different values, assign two values to them. The first one will be used for the left axis and the second one for the right axis. + +To enter array of values, Use YAML array (e.g. ['value1', 'value2', 'value3']) or simply values separated by comma (e.g. value1, value2, value3). The second method is a syntax surgar of Tracker to simplify the inputs. If YAML special characters are included, be sure to wrap the whole values by single quotes (e.g. 'value1, value2, value3'). Please also check [this](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/TargetEvaluation.md) for more information about YAML in Tracker. + +## List of Parameters + +### Root Parameters + +These key-value pairs are placed under the code block root. | Key | Description | Number of Values | Default | |:--------|:-------|:-----------:|:------| -| **searchType** | The type of searchTarget (tag\|frontmatter\|wiki\|text) | 1~NT | Must provided | -| **searchTarget** | The target to search
[[detail](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/TargetEvaluation.md)] | NT (Number of Targets) | Must provided | -| **folder** | The folder path of your daily notes | 1 | Root of this vault | -| **dateFormat** | The date format of your file names | 1 | 'YYYY-MM-DD' | -| **dateFormatPrefix** | A prefix before your dateFormat | 1 | '' | -| **dateFormatSuffix** | A suffix after your dateFormat | 1 | '' | -| **startDate** | The start date of collected dataset | 1 | Min date found | -| **endDate** | The end date of collected dataset | 1 | Max date found | -| **datasetName** | The name of the dataset collected from the search target | 1~NT | untitled | -| **separator** | The character to separate multiple values in the search target | 1~NT | '/' | -| **xDataset** | The id of the searchTarget to be used for searching x values for this target | 1~NT | -1 (use filename as xValue) | -| **constValue** | The constant value of a target if no value attached | 1~NT | 1.0 | -| **ignoreAttachedValue** | Use a constant value even if the target has a value attached on (true\|false) | 1~NT | false | -| **ignoreZeroValue** | Treat zero value as missing value (true\|false) | 1~NT | false | -| **accum** | Accumulatively sum the values over time (true\|false) | 1~NT | false | -| **penalty** | The value to use if the search target is missing on the day | 1~NT | | -| **fixedScale** | The scaling factor apply to the chart | 1 | 1.0 | -| **fitPanelWidth** | Auto-fit the width of the chart to the container | 1 | false | -| **margin** | The four margin (top|right|bottom|left) of the graph | 1~4 | 10 | -| **line** | A container key for parameters related to line-chart | | | -| **bar** | A container key for parameters related to bar-chart | | | -| **summary** | A container key for parameters related to summary output | | | - -## Parameters for Common Chart -These key-value pairs should be placed under the key '**line**' or '**bar**'. +| `searchType` | The type of `searchTarget` (tag\|frontmatter\|wiki\|text\|dvField\|table\|filemeta) | 1~NT | Must be provided | +| `searchTarget` | The target to search
[[detail](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/TargetEvaluation.md)] | NT (Number of Targets) | Must be provided | +| `folder` | The root path of notes to search | 1 | Root of this vault | +| `dateFormat` | The date format you are using | 1 | 'YYYY-MM-DD' | +| `dateFormatPrefix` | The prefix before your dateFormat | 1 | '' | +| `dateFormatSuffix` | The suffix after your dateFormat | 1 | '' | +| `startDate` | The start date to collect data | 1 | Min date found | +| `endDate` | The end date of to collect data | 1 | Max date found | +| `datasetName` | The name of the dataset for a search target` | 1~NT | untitled | +| `separator` | The character to separate multiple values appearing in the search target | 1~NT | '/' | +| `xDataset` | The `searchTarget` of this index will be used as xDataset | 1~NT | -1 (use filename as xDataset) | +| `constValue` | The constant value of a target if no value attached | 1~NT | 1.0 | +| `ignoreAttachedValue` | Use a constant value even if the target has a value attached on (true\|false) | 1~NT | false | +| `ignoreZeroValue` | Treat zero value as missing (true\|false) | 1~NT | false | +| `accum` | Accumulatively sum the values over time (true\|false) | 1~NT | false | +| `penalty` | The value to use if the search target is missing on the day | 1~NT | | +| `valueShift` | The amount to shift the collected values | 1~NT | 0 | +| `valueType` | Not implemented yet | 1~NT | | +| `fixedScale` | The scaling factor apply to the chart | 1 | 1.0 | +| `fitPanelWidth` | Auto-fit the width of the chart to the container | 1 | false | +| `margin` | The four margin (top|right|bottom|left) of the graph | 1~4 | 10 | +| `line` | A container key for parameters related to the line chart | | | +| `bar` | A container key for parameters related to the bar chart | | | +| `summary` | A container key for parameters related to the summary output | | | +| `bullet` | A container key for parameters related to the bullet chart | + +### Parameters for Common Chart +These key-value pairs should be placed under the key `line` or `bar`. | Key | Description | Number of Values | Default | |:--------|:-------|:-----------:|:------| -| **title** | The title of the chart | 1 | '' | -| **xAxisLabel** | X axis label | 1 | 'Date' | -| **xAxisColor** | X axis color | 1 | 'white'('black'*) | -| **xAxisLabelColor** | X axis label color | 1 | 'white'('black'*) | -| **yAxisLabel** | Y axis label | 1~2 | 'Value' | -| **yAxisColor** | Y axis color | 1~2 | 'white'('black'*) | -| **yAxisLabelColor** | The color of labels | 1~2 | 'white'('black'*) | -| **yAxisUnit** | The unit displayed aside y axis label | 1~2 | '' | -| **yMin** | The minimum value shown on Y axis | 1~2 |Minimum Y value found | -| **yMax** | The maximum value shown on Y axis | 1~2 | Maximum Y value found | -| **allowInspectData** | Show data value when mouse hovered (true\|false) | 1 | true | -| **showLegend** | Show legend (true\|false) | 1 | false | -| **legendPosition** | Legend position (top\|bottom\|left\|right) | 1 | bottom | -| **legendOrientation** | Legend Orientation (vertical\|horizontal) | 1 | horizontal for bottom and top
vertical for left and right | -| **legendBgColor** | Legend Background Color | 1 | none | -| **legendBorderColor** | Legend Border Color | 1 | white | - -## Parameters for Line Chart -These key-value pairs should be placed under the key '**line**'. +| `title` | The title of the chart | 1 | '' | +| `xAxisLabel` | The label of X axis | 1 | 'Date' | +| `xAxisColor` | The color of X axis | 1 | 'white'('black'*) | +| `xAxisLabelColor` | The color of X axis label | 1 | 'white'('black'*) | +| `yAxisLabel` | The label of Y axis | 1~2 | 'Value' | +| `yAxisColor` | The color of Y axis | 1~2 | 'white'('black'*) | +| `yAxisLabelColor` | The color of Y axis label | 1~2 | 'white'('black'*) | +| `yAxisUnit` | The unit displayed aside Y axis label | 1~2 | '' | +| `yMin` | The minimum value on Y axis | 1~2 |Minimum Y value found | +| `yMax` | The maximum value on Y axis | 1~2 | Maximum Y value found | +| `reverseYAxis` | Flip the Y Axis or not (true\|false) | 1~2 | false | +| `allowInspectData` | Show data value when mouse hovered (true\|false) | 1 | true | +| `showLegend` | Show legend (true\|false) | 1 | false | +| `legendPosition` | Legend position (top\|bottom\|left\|right) | 1 | bottom | +| `legendOrientation` | Legend orientation (vertical\|horizontal) | 1 | horizontal for bottom and top
vertical for left and right | +| `legendBgColor` | Legend background color | 1 | none | +| `legendBorderColor` | Legend border color | 1 | white | + +### Parameters for Line Chart +These key-value pairs should be placed under the key `line`. | Key | Description | Number of Values | Default | |:--------|:-------|:-----------:|:------| -| **lineColor** | The color of line | 1~NT | 'white'('black'*) | -| **lineWidth** | The width of line| 1~NT | 1.5 | -| **showLine** |Show/hide line (true\|false) | 1~NT | true | -| **showPoint** | Show/hide data point (true\|false) | 1~NT | true | -| **pointColor** | The color of data point | 1~NT | #69b3a2 | -| **pointBorderColor** | The border color of data point | 1~NT | #69b3a2 | -| **pointBorderWidth** | The border width of data point | 1~NT | 0 | -| **pointSize** | The size of data point | 1~NT | 3 | -| **fillGap** | Connect points over missing data (true\|false) | 1~NT | false | -| **yAxisLocation** | The corresponding y-axis for a specific dataset (left\|right) | 1~NT | left | - -## Parameters for Bar Chart -These key-value pairs should be placed under the key '**bar**'. +| `lineColor` | The color of the lines in chart | 1~NT | 'white'('black'*) | +| `lineWidth` | The width of the lines in chart | 1~NT | 1.5 | +| `showLine` | Show lines (true\|false) | 1~NT | true | +| `showPoint` | Show data points (true\|false) | 1~NT | true | +| `pointColor` | The color of data points | 1~NT | #69b3a2 | +| `pointBorderColor` | The border color of data points | 1~NT | #69b3a2 | +| `pointBorderWidth` | The border width of data points | 1~NT | 0 | +| `pointSize` | The size of data points | 1~NT | 3 | +| `fillGap` | Connect points over missing data (true\|false) | 1~NT | false | +| `yAxisLocation` | The corresponding Y axis for a specific dataset (left\|right) | 1~NT | left | + +### Parameters for Bar Chart +These key-value pairs should be placed under the key `bar`. + +| Key | Description | Number of Values | Default | +|:--------|:-------|:-----------:|:------| +| `barColor` | The color of bars in chart | 1~NT | #69b3a2 | +| `yAxisLocation` | The corresponding y-axis for a specific dataset (left\|right) | 1~NT | left | + +### Parameters for Summary +These key-value pairs should be placed under the key `summary`. | Key | Description | Number of Values | Default | |:--------|:-------|:-----------:|:------| -| **barColor** | 'white'('black'*) | 1~NT | #69b3a2 | -| **yAxisLocation** | The corresponding y-axis for a specific dataset (left\|right) | 1~NT | left | +| `template` | Text template (you may embed [template variables](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/TemplateVariables.md) | 1 | '' | +| `style` | CSS style applied the rendered text block | 1 | '' | -## Parameters for Summary -These key-value pairs should be placed under the key '**summary**'. +### Parameters for Bullet Chart +These key-value pairs should be placed under the key `bullet`. | Key | Description | Number of Values | Default | |:--------|:-------|:-----------:|:------| -| **template** | Text template (you may embed [template variables](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/TemplateVariables.md) | 1 | '' | -| **style** | CSS style applied the rendered text block | 1 | '' | \ No newline at end of file +| `title` | The title of the chart | 1 | '' | +| `dataset` | The index of the dataset of your interest | 1 | 0 | +| `orientation` | Bar orientation (horizontal\|vertical) | 1 | 'horizontal' | +| `value` | The actual value of interest
(you may embed [template variables](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/TemplateVariables.md) | 1 | '' | +| `valueUnit` | The unit of value displayed aside | 1 | '' | +| `valueColor` | The color of the value bar | 1 | '#69b3a2' | +| `range` | The ranges of color bands in background | N | [] | +| `rangeColor` | The color of range bands | N | [] | +| `showMarker` | Show marker or not (true\|false) | 1 | true | +| `markerValue` | The value of the markder | 1 | 0 | +| `markerColor` | The color of the marker | 1 | 'black' | diff --git a/docs/Installation.md b/docs/Installation.md new file mode 100644 index 0000000..9d7736e --- /dev/null +++ b/docs/Installation.md @@ -0,0 +1,11 @@ +# Installation + +## Install from the Obsidian Settings Panel +1. From Settings Panel (the icon at the bottom left corner) -> Options -> Community plugins, set 'safe mode' to off. +2. In the 'Community plugins' section, click 'Browse' and find the obsidian-tracker plugin by the name 'Tracker'. +3. Press the button 'Install' and wait for the completion of the install. +4. In the section 'Installed plugins', find and enable the plugin just installed. +5. Enjoy tracking. + +## Manual Installation +Download the latest [release](https://github.com/pyrochlore/obsidian-tracker/releases). Extract and put the three files (main.js, manifest.json, styles.css) to folder '{{obsidian_vault}}/.obsidian/plugins/obsidian-tracker'. \ No newline at end of file diff --git a/docs/Questions.md b/docs/Questions.md new file mode 100644 index 0000000..285c8e5 --- /dev/null +++ b/docs/Questions.md @@ -0,0 +1,47 @@ +# Frequently Asked Questions + +- Does Tracker only track data in daily notes (notes named by dates)? + + No. From version 1.6.0, you can use collect date data from file meta data with search type `fileMeta` and use that dataset as `xDataset`. So any notes can be included. + +- Does Tracker only track data over dates? + + Yes. The data type of x values should be in the form of date for now. This constrain might be relaxed in future. + +- Why my line chart looks broken at some points? + + The plugin only connects adjacent points (by date) by default. To force it connecting points over missing data, set the parameter `fillGap` under `line` to true. + +- Why the plugin shows 'No notes found under the given search condition'? + + There are few possibilities for this error messages. + 1. No files in the given folder + 2. No files or x data values matched the dateFormat you gave in the given folder + 3. No files in the date range you gave (from startDate to endDate) + +- Why the plugin shows 'Error parsing YAML'? + + There are syntax errors in your code block. Please check [this document](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/YAML.md) for common mistakes. + +- Why no data (points or lines) is shown in my chart? + + That means no matched data found in your notes. Please check `searchType` and `searchTarget` in your notes and the document for [target evaluation](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/TargetEvaluation.md). + + For example, if you were doing tag search (`searchType` input is `tag`), an additional space between the colon and the value will leads to data missing. If the space in between is a must, you can use `text` search with regular expression instead of `tag`. Here is an example. + + To Track '#tagName: 10' in daily notes. Use +``` +searchType: text +searchTarget: 'tagName:\s(?[0-9]+)' +...... +``` + More cases can be found [here](https://github.com/pyrochlore/obsidian-tracker/blob/master/examples/TrackUsingRegex.md) + +--- + +Please also check [these trackers](https://github.com/pyrochlore/obsidian-tracker/blob/master/examples/ErrorMessages.md) for more cases leading to error messages. + +--- + +Still have problems?? You might encounter bugs. +Welcome to leave an issue [here](https://github.com/pyrochlore/obsidian-tracker/issues). \ No newline at end of file diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md new file mode 100644 index 0000000..6e3b31c --- /dev/null +++ b/docs/ReleaseNotes.md @@ -0,0 +1,77 @@ +# Release Notes + +### v1.6.0 +- Add a new input type 'file', getting meta data from a file +- Add a new output type 'bullet', rendering a bullet chart +- Enhancement + - Accept tracking time values + - Allow tracking nested values from front matter + - Allow using dataset with date values as xDataset + - Add more template variables + - Allow parsing date in wiki brackets +- Fixed bugs + +### v1.5.1 +- Fixed labels not shown in light theme +- Enhanced error handling for searchType 'table' + +### v1.5.0 +- New searchType 'table', searching records from a given table +- New searchType 'dvField', searching the inline fields used with Dataview plugin +- Enhance multiple values extraction + - Allow using multiple values in searchType 'text' + - Allow using array values in searchType 'frontmatter' + - Allow using multiple values in searchType 'dvField' + - Allow using multiple values in searchType 'table' + - Allow using custom separator for multiple values extraction +- Improved performance +- Reduced package size + +### v1.4.1 +- Enhanced error handling + +### v1.4.0 +- Add a new parameter (fixedScale) for the scaling of the output chart +- Add a new parameter (fitPanelWidth) to enable/disable the auto-scaling of the output chart +- Add a new parameter (margin) to help to position the chart +- Tested in Obsidian mobile app on iPhone and iPad +- Fixed bugs + +### v1.3.0 +- Support reading and rendering multiple targets +- Support reading and rendering multiple values (a tuple of values) under a target +- New output type 'bar', rendering a bar chart +- Add a legend for the chart output +- Fixed bugs + +### v1.2.1 +- Fixed files with the specified dateFormat are not recognized +- Restored the plugin's settings panel for dateFormat and folder + +### v1.2.0 +- Enable using regular expression in text searching +- New search type 'frontmatter', searching for key-value pairs in the front matter +- New search type 'wiki', searching for wiki links +- Reduced package size + +### v1.1.0 +- New output type 'summary' +- Add commands help create Tracker code blocks +- Relaxed the regex for searching tags, allowing tags embedded in sentences +- Fixed issues + +### v1.0.2 +- Fixed the searching of nested tag in frontmatter +- Reduced the package size by using the module from Obsidian + +### v1.0.1 +- Remove dependencies to Node.js modules +- Add example markdown files + +### v1.0.0 +First version released at 2021-03-23 + +- Track simple tags, value-attached tags, and texts using code blocks +- Represent the tracked data in a customizable line chart +- Allow tracking in-line tags and tags in frontmatter +- Allow tracking nested tags \ No newline at end of file diff --git a/docs/RoadMap.md b/docs/RoadMap.md new file mode 100644 index 0000000..9b62485 --- /dev/null +++ b/docs/RoadMap.md @@ -0,0 +1,37 @@ +# Road Map + +- Data Collecting + - [x] Support tracking key-value pairs in frontmatter + - [x] Support searching text using regular expression + - [x] Support multiple targets and multiple values. + - [x] Add a parameter xDataset to identify targets to be used as x values + - [x] Allow tracking time values + - [x] Get data from a table + - [x] Collect data from dataview plugin's inline fields + - [x] Collect meta information from file + - [ ] Allow manual data input (x and y values) + - [ ] Allow forced value types + - [ ] Allow using non-date x values +- Output Type and Graph + - [x] New output type 'summary', analyzes the input data and represents it using a user-defined text template. + - [x] New output type 'bar', rendering a bar chart. + - [x] New output type 'bullet', rendering + - [ ] New output type for calendar view, including a monthly view or a heatmap. + - [x] Add parameters for adjusting the size of the graph. + - [ ] Multiple outputs from one code block + - [ ] Support graphs showing the correlation between sets of data. + - [ ] Allow a graph drawing selected dataset. + - [ ] Evaluate template variables by arithmetics and predefined functions. +- Helper + - [x] Add Commands help create Tracker blocks. + - [ ] Add an 'Explode' button to the rendered blocks, it will replace the code block with the rendered result. + - [ ] Add a helper panel for adding frequently used tracking targets to article. +- Data Processing + - [ ] Allow arithmetics operation on dataset and generate new datasets + - [ ] Add data post-process function, e.g. 'moving average' +- Performance + - [ ] Use PixiJS to do rendering + +And more ... + +P.S. Features may not be implemented in the order above. \ No newline at end of file diff --git a/docs/Settings.md b/docs/Settings.md new file mode 100644 index 0000000..3fc596d --- /dev/null +++ b/docs/Settings.md @@ -0,0 +1,10 @@ +# Plugin Settings + +The default folder to search and date format can be defined in the plugin's settings panel. If `folder` and `dateFormat` are not assigned in your code blocks, these default values will be applied automatically. + +| Setting Item | Description | Default | +|:--------|:-------|:---------| +| Default folder location | The folder to search | Root of the vault | +| Default date format | The date format you are using | 'YYYY-MM-DD' | + +For more information about the dateFormat setting, please also check the [TestDateFormats example](https://github.com/pyrochlore/obsidian-tracker/blob/master/examples/TestDateFormats.md) and [moment.js string format](https://momentjs.com/docs/#/parsing/string-format/). \ No newline at end of file diff --git a/docs/TargetEvaluation.md b/docs/TargetEvaluation.md index 806bee5..c878ff4 100644 --- a/docs/TargetEvaluation.md +++ b/docs/TargetEvaluation.md @@ -1,78 +1,114 @@ # Target Evaluation -According to the [input parameters](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/InputParameters.md) you provided, the search target will be counted or evaluated as a value. Obsidian-tracker supports four kinds of searchType: 'tag', 'frontmatter', 'wiki', and 'text', dealing with different types of searching condition. +According to the [input parameters](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/InputParameters.md) you provided, the search targets will be counted or evaluated as a value. Obsidian-tracker supports seven kinds of `searchType`: `tag`, `frontmatter`, `wiki`, `text`, `table`, `dvField`, and `fileMeta`, dealing with different types of searching condition. -## tag +## Multiple Targets +You can provide multiple search targets by entering an array of targets separated by a comma. Each of the targets will be identified according your search condition then the corresponding value will be evaluated and form a dataset indexed by the order in the array (zero-based indexing). -Simple tags in the format of '*#tagName*' in the file content are evaluated as a constant value (default value 1.0). You can override the value by assigning the key '**constValue**' in the code block. Use the tag name (the name after #) as the value of key '**searchTarget**' or use quoted tag ('**#tagName**') to make it work. +``` +searchTarget: target0, target1, target2, ...... +searchType: type0, type1, type2, ..... +datasetName: dataset0, dataset1, dataset2, ...... +line: + lineColor: red, blue, yellow +``` + +Above is an example of multiple target searching. Multiple targets are provided and separated by a comma. If they have a different searchType, provide the same number of types in the same order. In this case, the second search target 'target1' with index 1 has type 'type1'. Other parameters that accept multiple values (e.g. lineColor) can also be provided and will be applied to the corresponding dataset. + +## Multiple Values + +Multiple values under a target (value tuple) separated by a slash, e.g. #bloodpressure:180/120mmHg, are also got supported in version 1.3.0. To identify a specific value as a target, use an accessor with bracket notation where the value in the bracket is the index by the order of values. In this case, they are bloodpressure[0] and bloodpressure[1]. You can find the example of this [here](https://github.com/pyrochlore/obsidian-tracker/blob/master/examples/BloodPressureTracker.md). You can also use a custom separator by using the parameter `separator`. + +## Search Target in Detail + +## searchType: tag + +Simple tags in the format of '*#tagName*' in the file content are evaluated as a constant value (default value 1.0). You can override the value by assigning the key `constValue` in the code block. Use the tag name (the name after #) as the value of key `searchTarget` or use quoted tag (`#tagName`) to make it work. For tags in frontmatter (e.g. tags: meditation), it works like simple tags and will be evaluated as a constant value. For example, -
+ \-\-\-
tags: tagName1, tagName2
+......
\-\-\-
-Set '**searchTarget**' to tagName1 or tagName2 will make the plugin do his work. -In your content, a value can be attached to the tag in the format of '*#tagName:value*'. Note the value should be appended right after your tag and an extra colon without spaces. If a value is attached this way, the obsidian-tracker will automatically use the provided value instead of the constant one. +Set `searchTarget` to tagName1 or tagName2 will make the plugin do its work. + +In your content, a value can be attached to the tag in the format of '*#tagName:value*'. Note the value should be appended right after your tag and an extra colon **without spaces**. If a value is attached this way, the obsidian-tracker will automatically use the provided value instead of the constant one. Nested tags with values attached could be useful for tracking children's data separately and also still see the overall merged data using parent tags. -If you don't want value-attached tags in your content, you can use the '**frontmatter**' as your searchType. +If you don't want value-attached tags in your content, you can also put data in front matter and use `frontmatter` as your searchType. + +## searchType: frontmatter -## frontmatter This search type is used to query the key-value pairs in the front matter. If you don't want these values been seen in your article, the front matter would be the best place to record. For example, -
+ \-\-\-
mood: 10
+......
\-\-\-
-## wiki +## searchType: wiki This search type helps you count wiki links in articles. For example, [[A]] [[B|Link to B]] -## text -searchType 'text' is the most powerful one among all. If you simply provide text like 'love', the number of occurrences of tags will be counted. You can provide a regular expression to search for a very complicated target by wrapping it in single quotes. If you want to retrieve a value from it, use the group name in the expression. To see more detail, see [this case](https://github.com/pyrochlore/obsidian-tracker/blob/master/examples/TrackUsingRegex.md). +## searchType: text +searchType `text` is the most powerful one among all. If you simply provide text like 'love', the number of occurrences of tags will be counted. You can provide a regular expression to search for a very complicated target by wrapping it in single quotes. If you want to retrieve a value from it, use the group name in the expression. To see more detail, see [this case](https://github.com/pyrochlore/obsidian-tracker/blob/master/examples/TrackUsingRegex.md). Multiple values in text search can be achieved by separate regex by comma and wrap them all in single quotes as follows: -
-\`\`\` tracker
-searchTarget: 'regex1, regex2'
-\`\`\`
-## dvField -From version 1.5.0, the plugin supports retrieving inline fields used with the dataview plugin. To get "targetName:: value" in your article, try the following tracker settings. -
-\`\`\` tracker
-searchType: dvField
-searchTarget: targetName
-......
-\`\`\`
+``` +searchTarget: 'regex1, regex2' +...... +``` + +## searchType: dvField + +Tracker supports retrieving inline fields used with the dataview plugin. To get "targetName:: value" in your article, try the following tracker settings. + +``` +searchType: dvField +searchTarget: targetName +...... +``` If you have multiple values in field, like "targetName:: 123 @ 456", use the following tracker settings.
-\`\`\` tracker
-searchType: dvField
-searchTarget: targetName[0], targetName[1]
-separator: '@'
-......
-\`\`\`
+``` +searchType: dvField +searchTarget: targetName[0], targetName[1] +separator: '@' +...... +``` More dvField example can be found [here](https://github.com/pyrochlore/obsidian-tracker/blob/master/examples/TestMultipleTargesMultipleValues.md#multiple-values-in-dvfield-dataview-inline-field). -## table -This search type is a special one because it does not search over files in the specified folder. Instead, it looks into a given file, finds the specified table, and retrieves data from specified columns. Here is an example, -
-\`\`\` tracker
-searchType: table
-searchTarget: data/Tables[0][0], data/Tables[0][1], data/Tables[0][2]
-xDataset: 0
-line:
-    yAxisLocation: none, left, right
-    lineColor: none, yellow, red
-    showLegend: true
-\`\`\`
+## searchType: table + +This search type is very much different from others because it does not search over files in the specified folder. Instead, it looks into a given file, finds the specified table, and retrieves data from specified columns. Here is an example, + +``` +searchType: table +searchTarget: data/Tables[0][0], data/Tables[0][1], data/Tables[0][2] +xDataset: +line: + yAxisLocation: none, left, right + lineColor: none, yellow, red + showLegend: true +``` In this case, "data/Tables" is the path of the file of interest. The number in the first brackets after the path ([0]) is the index of the table of interest in the file, starts from 0. And the number in the second brackets is the index of the column containing target data. If there are multiple values in table cells, you can provide a third index to identify them. More table examples can be found [here](https://github.com/pyrochlore/obsidian-tracker/blob/master/examples/TestTable.md). + +## searchType: fileMeta + +With this search type, you can retrieve infomation of files. Currently, three kinds of data you can get. + +- cDate: creation date of a file +- mDate: last modification date of a file +- size: file size in bytes + +`cData` and `mDate` can be used as X dataset and `size` can be used as Y dataset. \ No newline at end of file diff --git a/docs/TemplateVariables.md b/docs/TemplateVariables.md index b8df91c..df6bdf3 100644 --- a/docs/TemplateVariables.md +++ b/docs/TemplateVariables.md @@ -1,32 +1,44 @@ # Template Variables -When you choose 'summary' as the output type (by placing a key 'summary'), you can embed template variables into the 'template' parameter. If you do so, the plugin will do calculations based on the collected dataset refereed, and render the output with variables replaced by calculated results. +Currently, there are two places you can use template variables. First is the `template` parameter under output type `summary`. The second one is the `value` parameter under output type `bullet`. For each template variable, the plugin will do calculations based on the collected dataset refereed, and render the output with variables replaced by calculated results. -The following table shows all the predefined template variables available for now. The "N" in the table could be the ID (the order of a target in '**searchTarget**' parameter starting from zero) or the name (specified by the parameter '**datasetName**') of a dataset. Examples could be found [here](https://github.com/pyrochlore/obsidian-tracker/blob/master/examples/BloodPressureTracker.md). - +The following table shows all the template variables available for now. The "N" in the table is the ID (the order of a target in `searchTarget` parameter starting from zero) or the name (specified by the parameter `datasetName`) of a dataset. + +Examples using template variables could be found [here](https://github.com/pyrochlore/obsidian-tracker/blob/master/examples/BloodPressureTracker.md). + +## List of Template Variables + +| Template variable | Description | +|:------------------|:-----------| +| {{min}}
{{min(Dataset(N))}} | Minimum value of first dataset | +| {{max}}
{{max(Dataset(N))}} | Maximum value of first dataset | +| {{startDate}}
{{startDate(Dataset(N))}} | | +| {{endDate}}
{{endDate(Dataset(N))}} | | +| {{sum}}
{{sum(Dataset(N))}} | Summation of values of first dataset | +| {{numTargets}}
{{numTargets(Dataset(N))}} | Total counts of not-null values of specified dataset| +| {{numDays}}
{{numDays(Dataset(N))}} | Days from startDate to endDate of first dataset | +| {{numDaysHavingData}}
{{numDaysHavingData(Dataset(N))}} | | +| {{maxStreak}}
{{maxStreak(Dataset(N))}} | Maximum streak between missing data of first dataset | +| {{maxStreakStart}}
{{maxStreakStart(Dataset(N))}} | | +| {{maxStreakEnd}}
{{maxStreakEnd(Dataset(N))}} | | +| {{maxBreaks}}
{{maxBreaks(Dataset(N))}} | Maximum break from missing data of first dataset | +| {{maxBreaksStart}}
{{maxBreaksStart(Dataset(N))}} | Maximum break from missing data of specified dataset | +| {{maxBreaksEnd}}
{{maxBreaksEnd(Dataset(N))}} | Maximum break from missing data of specified dataset | +| {{currentStreak}}
{{currentStreak(Dataset(N))}} | Last streak of first dataset | +| {{currentStreakStart}}
{{currentStreakStart(Dataset(N))}} | Last streak of first dataset | +| {{currentStreakEnd}}
{{currentStreakEnd(Dataset(N))}} | Last streak of first dataset | +| {{currentBreaks}}
{{currentBreaks(Dataset(N))}} | Last breaks of first dataset | +| {{currentBreaksStart}}
{{currentBreaksStart(Dataset(N))}} | Last breaks of first dataset | +| {{currentBreaksEnd}}
{{currentBreaksEnd(Dataset(N))}} | Last breaks of first dataset | +| {{average}}
{{average(Dataset(N))}} | Average value of first dataset | +| {{median}}
{{median(Dataset(N))}} | Median value of first dataset | +| {{variance}}
{{variance(Dataset(N))}} | Variance value of first dataset | + +## List of Deprecated Template Variables | Template variable | Description | |:------------------|:-----------| -| {{min}} | Minimum value of first dataset | -| {{min(Dataset(N))}} | Minimum value of specified dataset | -| {{max}} | Maximum value of first dataset | -| {{max(Dataset(N))}} | Maximum value of specified dataset | -| {{sum}} | Summation of values of first dataset | -| {{sum(Dataset(N))}} | Summation of values of specified dataset | -| {{count}} | Total counts of not-null values of first dataset | -| {{count(Dataset(N))}} | Total counts of not-null values of specified dataset| -| {{days}} | Days from startDate to endDate of first dataset | -| {{days(Dataset(N))}} | Days from startDate to endDate of specified dataset | -| {{maxStreak}} | Maximum streak between missing data of first dataset | -| {{maxStreak(Dataset(N))}} | Maximum streak between missing data of specified dataset | -| {{maxBreak}} | Maximum break from missing data of first dataset | -| {{maxBreak(Dataset(N))}} | Maximum break from missing data of specified dataset | -| {{lastStreak}} | Last streak of first dataset | -| {{lastStreak(Dataset(N))}} | Last streak of specified dataset | -| {{average}} | Average value of first dataset | -| {{average(Dataset(N))}} | Average value of specified dataset | -| {{median}} | Median value of first dataset | -| {{median(Dataset(N))}} | Median value of specified dataset | -| {{variance}} | Variance value of first dataset | -| {{variance(Dataset(N))}} | Variance value of specified dataset | +| {{count}}
{{count(Dataset(N))}} | Use numTargets instead | +| {{days}}
{{days(Dataset(N))}} | Use numDays instead | +| {{lastStreak}}
{{lastStreak(Dataset(N))}} | Use currentSteak instead | -Notice when doing calculations like sum or average, the missing values are ignored. You can set those missing values to zero by setting the value of key 'penalty' to zero. Moreover, if the key 'ignoreZeroValue' is assigned true, zero value will be ignored too. You can check these parameters [here](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/InputParameters.md) \ No newline at end of file +Notice when doing calculations like sum or average, the missing values are ignored. You can set those missing values to zero by setting the value of key `penalty` to zero. Moreover, if the key `ignoreZeroValue` is assigned true, zero value will be ignored too. You can check these parameters [here](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/InputParameters.md) \ No newline at end of file diff --git a/docs/YAML.md b/docs/YAML.md new file mode 100644 index 0000000..77ef250 --- /dev/null +++ b/docs/YAML.md @@ -0,0 +1,65 @@ +# YAML Related Issues + +## Special characters + +YAML special characters (e.g. #, [, ], ...) are not allowed for input values in code blocks. You need to wrap the whole input up by single quotes. + +### Example 1 - Tag Search +If we have a bunch of tags (say #tagName) to track. +This will leads to 'Error parsing YAML' because of the # charactter. +``` +searchType: tag +searchTarget: #tagName +``` + +Instead, wrapping the input by single quotes will do the work. +``` +searchType: tag +searchTarget: '#tagName' +``` + +or simply +``` +searchType: tag +searchTarget: tagName +``` + +### Example 2 - Hex Color Codes + +Hex color codes must be wrapped in single quotes. +``` +searchType: tag +searchTarget: weight +line: + lineColor: '#F08080' +``` + +Wrap the whole input by single quotes. +``` +searchType: tag +searchTarget: weight, push-up +line: + lineColor: '#F08080, #008080' +``` + +### Example 3 - Text Search with Regex + +If we want to track value-attached tags not matched the standard format of Tracker, for example, weightlifting: 20.5kg. We can use search type `text` with regular expression in `searchTarget`. +``` +searchType: text +searchTarget: 'weightlifting:\s(?[\-]?[0-9]+[\.][0-9]+|[\-]?[0-9]+)' +``` +The whole input should be wrapped in single quotes because of the special characters. + +## Array input +Tracker accepts two types of array input. + +- YAML Array + +A YAML array input can be represented as: ['value1', 'value2', 'value3']. + +- Value Separated by Comma + +Use values separated by comma: value1, value2, value3 +Or single quoted string: 'value1, value2, value3' + From e941cf118782e8eb1aad47f1bb68b6c84d2a4032 Mon Sep 17 00:00:00 2001 From: pyrochlore Date: Sat, 5 Jun 2021 17:16:50 +0800 Subject: [PATCH 051/402] Update screenshot --- README.md | 2 +- docs/images/screenshot_v1.6.png | Bin 0 -> 184423 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 docs/images/screenshot_v1.6.png diff --git a/README.md b/README.md index e373ea0..a764bbc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Obsidian Tracker Plugin ![GitHub release](https://img.shields.io/github/v/release/pyrochlore/obsidian-tracker) - + This is an [Obsidian](https://obsidian.md/) plugin that helps you do tracking in notes [over dates](https://github.com/pyrochlore/obsidian-tracker/blob/master/docs/Questions.md) and represent the collected data comprehensively. diff --git a/docs/images/screenshot_v1.6.png b/docs/images/screenshot_v1.6.png new file mode 100644 index 0000000000000000000000000000000000000000..1d2d55fb22c6ae23c1968644c8c1aed60c5e2ca4 GIT binary patch literal 184423 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

n>(f&qBJ!`c=mv>-cuAym_+- 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