Skip to main content

Team progress tracker

The code for this example can be used in the Markdown visualization to create a progress chart for multiple individuals on the same timeline. If any individuals exceed 100% of their goal, the progress line will extend to the highest value.

A few caveats:

  • It uses some HTML and CSS that do not render in PDFs or PNGs. Only use this if you don't need it delivered.
  • It currently requires all team members have the same goal value. You could adjust the math if you need to do varying target values.
  • This version adds a crown to any team member that exceeds the target.

Setup

These charts use the Mustache iterator syntax to draw a circle on the progress tracker for each row in the results.

The following table explains each field used in the example, including the calculation formulas.

 

ColNameDescription or formulaNotes
ASales RepName used in tooltip
BQuotatarget valueMust be the same for each individual
CTo datecurrent valuemust be > 0 but can exceed target
D% to quota=C1 / B1% to goal - can be > 100%
Eis over quota=IF(D1 >= 1, "over", "")used to add a crown if user is over quota
FMax dollars=MAX(MAX(C:C), B1)needed for calculating positions if anybody is over quota
GMax %=MAX(MAX(D:D), 1)used for presentation in the tooltip
HSomeone over quota=IF(G1 > 1, "over", "")flag to change range of tracker if anybody is over quota
IInitialsNot used in this example but could be used instead of picture
JImageused as background of the circle

Example code

<style>
article.race-tracker {
--padding-inline: 44px;
--track-width: 2px;
--track-color: var(--color-border2);
--rep-diam: 56px;
--rep-half: calc(var(--rep-diam) / 2);
--rep-quarter: calc(var(--rep-half) / 2);
--track-height: calc(2.25 * var(--rep-diam));
--zero: calc(var(--rep-diam) / 2); /* 1/2 diam */
--quota: {{result.0.reps_to_quota.quota.raw}};
--max: {{result.0.calc_2.raw}};

width: 100%;
padding: 0 var(--padding-inline);
position: relative;
}
ol.track {
list-style: none;
margin: 0;
display: flex;
width: 100%;
height: var(--track-height);
position: relative;

& li.track-line {
position: absolute;
background: var(--track-color);
height: var(--track-width);
width: 100%;
top: calc(50% - var(--track-width) / 2);
left: 0;
z-index: 0;
}

& li.tick {
--marker-width: 5ch;
--half: calc(var(--marker-width) / 2);
--mark: attr(data-value type(<number>));
--quota-x-mark: calc(var(--quota) * var(--mark));
--position: calc(var(--quota-x-mark) / var(--max) * 100%);

position: absolute;
width: 5ch;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
height: 100%;
top: 0;
left: calc(var(--position) - var(--half));
font-size: var(--font-xxs);

& span.line {
width: var(--track-width);
background: var(--track-color);
height: var(--rep-half);
display: inline-blcok;
position: absolute;
top: calc(var(--rep-half)* 1.75);
}
}
& li.tick.alt-max {
visibility: hidden;
}

&.over li.tick.alt-max {
visibility: visible;
}
}
ol.tracker {
list-style: none;
margin: 0;
display: flex;
width: calc(100% - 2 * var(--padding-inline));
height: var(--rep-diam);
position: absolute;
top: calc(var(--rep-half) * 1.25);
left: var(--padding-inline);

& li.rep {
--todate: attr(data-todate type(<number>));
--position: calc(var(--todate) / var(--max) * 100%);
width: var(--rep-diam);
height: var(--rep-diam);
display: flex;
align-items: center;
justify-content: center;
border-radius: 500px;
border: 1px solid var(--color-text4);
box-shadow: var(--elevation1);
position: absolute;
left: calc(var(--position) - var(--zero));
background-color: var(--color-text1);
background-size: var(--rep-diam);

& .crown {
position: absolute;
top: calc(var(--rep-half) * -0.75);
font-size: var(--font-xxl);
display: none;
}

& ul.info-bits {
position: absolute;
bottom: 0;
left: calc(-60px + var(--size4));
top: -60px;
list-style: none;
margin: 0;
padding: var(--size1) var(--size2);
width: 120px;
border-radius: 3px;
height: min-content;
background: var(--color-surface-invert);
color: var(--color-text-inverse);
box-shadow: var(--elevation3);
display: none;

& li {
white-space: nowrap;
font-size: var(--font-xxs);
line-height: var(--line-height-xxs);
}
}
}
& li.rep:hover {
box-shadow: var(--elevation3);
z-index: 2;

& ul.info-bits {
display: block;
}
}
& li.rep.over {
.crown {
display: block;
}
}
}
</style>
<article class="race-tracker">
<ol class="track {{result.0.calc_4.value_static}}">
<li class="track-line"></li>
<li class="tick t0" data-value="0"><span class="line"></span><span class="value">0%</span></li>
<li class="tick t25" data-value="0.25"><span class="line"></span><span class="value">25%</span></li>
<li class="tick t50" data-value="0.5"><span class="line"></span><span class="value">50%</span></li>
<li class="tick t75" data-value="0.75"><span class="line"></span><span class="value">75%</span></li>
<li class="tick t100" data-value="1"><span class="line"></span><span class="value">100%</span></li>
<li class="tick alt-max" data-value="{{result._first.calc_3.raw}}"><span class="line"></span><span class="value">{{result._first.calc_3.value_static}}</span></li>
</ol>
<ol class="tracker">
{{#result}}
<li class="rep {{calc_5.raw}}" data-todate="{{reps_to_quota.to_date.raw}}" style="background-image: url('{{reps_to_quota.image.raw}}')"><span class="crown">👑</span>
<ul class="info-bits">
<li>{{reps_to_quota.sales_rep.value_static}}</li>
<li>{{reps_to_quota.to_date.value_static}} to date</li>
<li>{{calc_1.value_static}} to quota</li>
</ul>
</li>
{{/result}}
</ol>
</article>