- Script and theme files available in this Github Repo
- Code is hobby for me, not my day job. I apologies for any inefficiencies in the script… Please share any feedback or improvements opportunities!
Overview
I had a need to quickly report the sums and counts of specific @tags(attribute). I decided to draft this script that do just that. It simply output the data in a project appended to the outline root. I have not extensively tested this but work well enough for my needs up to this point. I hope this script can be useful for others as well!
I wanted it to be flexible enough in case my reporting requirements were to change on the fly or to easily report stuff I would care for in the future.
- Ability to assign the name of the output “project”
- Create as many reporting “project” as required
- The “projects” get refreshed or created if does not exist on script run
- Default output labels and/or Custom output labels
- Ease of use → familiar function parameters. ex: tag syntax as “@tag(attribute)” and search queries
- Only 4 functions that cover a wide range of “reporting”:
- tagCounter()
- tagAttributeCounter()
- sumTagAsNumber()
- sumTagAsPercentage()
Output Example
Description and Functions Definition
Reporting script that output count of @tags or @tags(attribute), and/or sum of @tags(attribute) in appended project(s).
/**
* FUNCTION tagCounter();
* Count all @tags in a given search 'path' without a specific attribute.
* It then append results as number to a given 'project'.
*
* PARAMETERS:
* 'tag' as String (with leading @)
* 'tagPath' as String (in an TaskPaper eval/search format => '@type = project and (@status = active)//@type = note')
* 'outputProject' as TaskPaper Project Item
* 'label' as String (pass '' to use default label)
*/
function tagCounter(tag, tagPath, outputProject, label);
/**
* FUNCTION tagAttributeCounter();
* Count all @tags in a given search 'path' with a specific attribute. ex. @tag(attribute)
* It then append results as number to a given 'project'.
*
* PARAMETERS:
* 'tag' as String (with leading @)
* 'tagAttribute' as String (ex. 'active' to count @tag(active))
* 'tagPath' as String (in an TaskPaper eval/search format => '@type = project and (@status = active)//@type = note')
* 'outputProject' as TaskPaper Project Item
* 'label' as String (pass '' to use default label)
*
* TODO: Allow multiple tag attributes as Array parameter
*/
function tagAttributeCounter(tag, tagAttribute, tagPath, outputProject, label);
/**
* FUNCTION sumTagAsNumber();
* Sum all the number attributes for a given @tag and in a given search 'path'.
* It then append results as number to a given 'project'.
*
* PARAMETERS:
* 'tag' as String (with leading @)
* 'tagPath' as String (in an TaskPaper eval/search format => '@type = project and (@status = active)//@type = note')
* 'outputProject' as TaskPaper Project Item
* 'label' as String (pass '' to use default label)
*/
function sumTagAsNumber(tag, tagPath, outputProject, label);
/**
* FUNCTION sumTagAsPercentage();
* Sum all the number attributes for a given @tag and in a given search 'path'.
* It then append results as percentage to a given 'project'.
*
* PARAMETERS:
* 'tag' as String (with leading @)
* 'tagPath' as String (in an TaskPaper eval/search format => '@type = project and (@status = active)//@type = note')
* 'outputProject' as TaskPaper Project Item
* 'label' as String (pass '' to use default label)
*/
function sumTagAsPercentage(tag, tagPath, outputProject, label);
Stylesheet Scope
Ensure to use one of these themes OR ensure to append the following style scope to your [theme].less
files. I recommend the use of a monospace font on the @stats
tag to enhance the visual consumption of the output as shown in the example.
item[data-stats] {
font-size: @user-font-size;
font-family: hack; // or any other monospace font available on your machine
color: @text-color;
font-style: normal;
> run[content] {
text-strikethrough: none;
color: @text-color;
}
> run[tag] {
text-strikethrough: none;
color: fade(@text-color, 65%);
}
> run[tagvalue] {
color: @text-color;
}
}
The Script
Start creating and defining your reporting outputs after line 260 of the script file.
/**
* _statsReport script
*
* Reporting helper script output count of @tags and/or sum of @tags(attribute) in appended project(s)
* Start creating and defining your reporting outputs after line 260
*/
function TaskPaperContextScript(editor, options) {
var outline = editor.outline;
// group all the changes together into a single change and make it a single "undo" action
outline.groupUndoAndChanges(function () {
/**
* FUNCTION labelPadding();
* Add padding between the label and output for the amount of 'padding' passed in
*
* PARAMETERS:
* 'label' as String (label string)
* 'padding' as Integer
*/
var defaultPadding = 39;
function labelPadding(label, padding) {
var pad = ' ',
delimiter = '|';
altPadding = padding;
// Handle case where label length exceed the requested/default padding value
while (altPadding - label.length < 5) {
altPadding = altPadding + 15;
}
// Add padding spaces after the label
for (var i = (altPadding - label.length) - 1; i >= 0; i--) {
pad += ' ';
}
// Return full label + padding + output delimiter
// Add @stats tag (prevent counting them as stats and for styling purposes)
return '@stats ' + label + pad + delimiter + ' ';
}
/**
* FUNCTION sumTagAsPercentage();
* Sum all the number attributes for a given @tag and in a given search 'path'.
* It then append results as percentage to a given 'project'.
*
* PARAMETERS:
* 'tag' as String (with leading @)
* 'tagPath' as String (in an TaskPaper eval/search format => '@type = project and (@status = active)//@type = note')
* 'outputProject' as TaskPaper Project Item
* 'label' as String (pass '' to use default label)
*/
function sumTagAsPercentage(tag, tagPath, outputProject, label) {
// All the @tag items in outline search path
var tagArray = outline.evaluateItemPath(tagPath);
// Hold the sum of all the tags in path
var sumTag = 0;
// Hold defaul output label
var defaultLabel = '> Sum of ';
// Hold output item
var tagStat;
// Hold output item
var tagName = tag.substring(1);
// Labels & Padding
var defaultLabel = labelPadding('> Sum of ' + tag + ' as percentage', defaultPadding),
paramLabel = labelPadding(label, defaultPadding);
// Sum all values of the tag in path
tagArray.forEach(function(item) {
var num = item.getAttribute('data-' + tagName, Number);
// Exclude stats items from counter
var itm = item.hasAttribute('data-stats');
// chek if 'num' is not a number or inexistant
if (!itm && !isNaN(num) && num != null) {
sumTag = sumTag + num;
}
});
// Create item as @type = note + Label
// if default label
if (label == '')
{
tagStat = outline.createItem(defaultLabel + (Math.round(sumTag * 100)) + '%');
}
// if parameter label
else
{
tagStat = outline.createItem(paramLabel + (Math.round(sumTag * 100)) + '%');
}
// Append sum to outputProject as percentage
outputProject.appendChildren(tagStat);
}
/**
* FUNCTION sumTagAsNumber();
* Sum all the number attributes for a given @tag and in a given search 'path'.
* It then append results as number to a given 'project'.
*
* PARAMETERS:
* 'tag' as String (with leading @)
* 'tagPath' as String (in an TaskPaper eval/search format => '@type = project and (@status = active)//@type = note')
* 'outputProject' as TaskPaper Project Item
* 'label' as String (pass '' to use default label)
*/
function sumTagAsNumber(tag, tagPath, outputProject, label) {
var tagArray = outline.evaluateItemPath(tagPath),
sumTag = 0,
tagName = tag.substring(1),
tagStat;
// Labels & Padding
var defaultLabel = labelPadding('> Sum of ' + tag + ' as number', defaultPadding),
paramLabel = labelPadding(label, defaultPadding);
tagArray.forEach(function(item) {
var num = item.getAttribute('data-' + tagName, Number),
itm = item.hasAttribute('data-stats');
if (!itm && !isNaN(num) && num != null) {
sumTag = sumTag + num;
}
});
if (label == '') {
tagStat = outline.createItem(defaultLabel + sumTag);
} else {
tagStat = outline.createItem(paramLabel + sumTag);
}
// Append sum to outputProject as number
outputProject.appendChildren(tagStat);
}
/**
* FUNCTION tagCounter();
* Count all @tags in a given search 'path' without a specific attribute.
* It then append results as number to a given 'project'.
*
* PARAMETERS:
* 'tag' as String (with leading @)
* 'tagPath' as String (in an TaskPaper eval/search format => '@type = project and (@status = active)//@type = note')
* 'outputProject' as TaskPaper Project Item
* 'label' as String (pass '' to use default label)
*/
function tagCounter(tag, tagPath, outputProject, label) {
var tagArray = outline.evaluateItemPath(tagPath),
countTag = 0,
tagStat;
// Labels & Padding
var defaultLabel = labelPadding('> Count of ' + tag, defaultPadding),
paramLabel = labelPadding(label, defaultPadding);
tagArray.forEach(function(item) {
var itm = item.hasAttribute('data-stats');
if (!itm) {
countTag++;
}
});
if (label == '') {
tagStat = outline.createItem(defaultLabel + countTag);
} else {
tagStat = outline.createItem(paramLabel + countTag);
}
// Append count to outputProject as number
outputProject.appendChildren(tagStat);
}
/**
* FUNCTION tagAttributeCounter();
* Count all @tags in a given search 'path' with a specific attribute. ex. @tag(attribute)
* It then append results as number to a given 'project'.
*
* PARAMETERS:
* 'tag' as String (with leading @)
* 'tagAttribute' as String (ex. 'active' to count @tag(active))
* 'tagPath' as String (in an TaskPaper eval/search format => '@type = project and (@status = active)//@type = note')
* 'outputProject' as TaskPaper Project Item
* 'label' as String (pass '' to use default label)
*
* TODO: Allow multiple tag attributes as Array parameter
*/
function tagAttributeCounter(tag, tagAttribute, tagPath, outputProject, label) {
var tagArray = outline.evaluateItemPath(tagPath),
countTag = 0,
tagName = tag.substring(1),
tagStat;
// Labels & Padding
var attributeOutput = tagAttribute == '' ? '()' : '(' + tagAttribute + ')',
defaultLabel = labelPadding('> Count of ' + tag + attributeOutput, defaultPadding),
paramLabel = labelPadding(label, defaultPadding);
tagArray.forEach(function(item) {
// If item has a @tag attribute
var itmAttribute = item.getAttribute('data-' + tagName);
// // DEBUG
// var itmAttTest = outline.createItem('>> Attribute of ' + tag + ' = [' + tagAttribute + '] match? [' + itmAttribute + ']'); // create item as @type = note
// itmAttTest.setAttribute('data-stats', '');
// outputProject.appendChildren(itmAttTest);
var itm = item.hasAttribute('data-stats');
if (!itm && itmAttribute == tagAttribute) {
countTag++;
}
});
if (label == '') {
tagStat = outline.createItem(defaultLabel + countTag);
} else {
tagStat = outline.createItem(paramLabel + countTag);
}
// Append count to outputProject as number
outputProject.appendChildren(tagStat);
}
/**
* FUNCTION createOutputProject();
* Create or Reset a Taskpaper project at the end of the Taskpaper outline
*
* PARAMETERS:
* 'pjcName' as String (existing or new project name)
*/
function createOutputProject(pjcName) {
// Reset/Create 'output:' project
var outputProjectName = pjcName + ':',
outputPath = '@type = project and ' + outputProjectName,
outputProjectArray = outline.evaluateItemPath(outputPath),
outputProjectItem = outline.createItem(outputProjectName); // create item as @type = project
// Remove the 'output:' project from the outline if it exist
if (outputProjectArray.length != 0) {
outputProjectArray[0].removeFromParent();
}
// Add @section tag (optional)
outputProjectItem.setAttribute('data-section', '');
// Append a new `STATS:` project to outline root
outline.root.appendChildren(outputProjectItem);
outputProjectArray = outline.evaluateItemPath(outputPath);
return outputProjectArray[0];
}
/************************************************************************************
START CREATING AND DEFINING YOUR REPORTING OUTPUTS HERE
************************************************************************************/
// Create as many "stats" project sections as you wish
var statsProject1 = createOutputProject('DEMO - Sum Example Stats');
var statsProject2 = createOutputProject('DEMO - Counter Example Stats');
// Example that output the sum of all @capacity tags with number attribute
// and output as percentage ex. @tag(0.1) => 10%
sumTagAsPercentage('@capacity', '@type = note and @capacity', statsProject1, '');
// Example that output the sum of all @capacity tags with number attribute
// and output as number ex. @tag(1) => 1
sumTagAsNumber('@capacity', '@type = note and @capacity', statsProject1, '');
// Simple Counter Example without attribute (count all @tag regardless of attribute)
tagCounter('@capacity', '@type = note and @capacity', statsProject2, '');
// Advanced Counter Example without attribute (count all @tag without attribute)
tagAttributeCounter('@capacity', '', '@type = note and @capacity', statsProject2, '');
// Advanced Counter Example with attribute (count all @tag with specific attribute)
tagAttributeCounter('@capacity', '0.1', '@type = note and @capacity', statsProject2, '');
// Default Label Example
tagAttributeCounter('@status', 'active', '@type = project and @status', statsProject2, '');
// Parameter Label Example
tagAttributeCounter('@status', 'active', '@type = project and @status', statsProject2, '> Active Projects');
// Extra Long Label Parameter Example
tagAttributeCounter('@status', 'active', '@type = project and @status', statsProject2, '> Active Projects for the current month');
// My own reporting stuff I will actually use
var statsProject = createOutputProject('STATS REPORT');
// - Sum Total Capacty
sumTagAsPercentage('@capacity', '@type = note and @capacity', statsProject, '> Capacity Allocation');
// - Count Active Projects
tagAttributeCounter('@status', 'active', '@type = project and @status', statsProject, '> Active Projects');
// - Count Projects on Hold
tagAttributeCounter('@status', 'hold', '@type = project and @status', statsProject, '> Projects on Hold');
// - Count Completed Project Accomplishments
tagCounter('@accomplishment', '@type = project and (@status = cancelled or @status = delivered or @status = completed)//@accomplishment', statsProject, '> Accomplishments');
}); // end outline.groupUndoAndChanges
} // end TaskPaperContextScript
var string = Application("TaskPaper").documents[0].evaluate({
script: TaskPaperContextScript.toString()
});