Reporting SUM / COUNT of @tags and @tags(attribute) w/output appended as "project" to outline

  • 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()
});
1 Like

wow, this looks really interesting, taking it to another level! :wink:

You got me while editing the post! I’m glad to see it might be of interest for others.

@drootz Your script is awesome. Just wanted to say thank you. I find it super helpful to estimate effort when doing weekly planning.