Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.


Page properties
hiddentrue





UI Text Box
sizemedium
typeinfo

This chart displays the status categories from all Stories under an Epic grouped by a custom value.

The status categories are also grouped on the y axis by Epic and will be displayed as individual donut charts within the table.

The number of tickets within the status category Done will be displayed as percentage values in the center of each donut.

In the following example, the Stories are grouped by Priority.

Button Hyperlink
iconapprove
titleDownload Scripted Chart Bundle (Jira < 8)
typeprimary
urlhttps://apps.decadis.net/download/attachments/1817374/Story%20status%20category%20grouped%20by%20Epic%20and%20custom%20value.json?api=v2

Button Hyperlink
iconapprove
titleDownload Scripted Chart Bundle (Jira >= 8)
typeprimary
urlhttps://apps.decadis.net/download/attachments/1817374/Story%20status%20category%20grouped%20by%20Epic%20and%20custom%20value%20Jira%208.json?api=v2

Chart preview

Parameters

NameTypeDefault
JQLJQL autocomplete
Epic Link Field IDText
GroupByGroup By Picker


UI Expand
titleLayout Script


Code Block
languagejs
themeMidnight
titleJavaScript Template
linenumberstrue
const $container = $('#chart');
 
 
const $table = $('<table />').addClass('table table-bordered').css('width', 'auto').appendTo($container);
const $thead = $('<thead />').appendTo($table);
const $tbody = $('<tbody />').appendTo($table);
 
const data = chartData.data;
const statusCategories = chartData.statusCategories;
 
 
// create header row
const $tr = $('<tr />').css('background', '#f5f5f5').append('<td />').appendTo($thead);
// create header columns
$.each(data[Object.keys(data)[0]], function (groupKey) {
    $('<th />').addClass('nowrap text-center').text(chartData.groups[groupKey]).appendTo($tr);
});
 
// set color pattern
const colors = {
    'done': '#8cc152',
    'indeterminate': '#ffce54',
    'new': '#ed5556'
};
 
/**
 * Calculate sum
 *
 * @param obj
 * @returns {number}
 */
const sum = function (obj) {
 
    let keys = Object.keys(obj);
 
    if(keys.length){
        return keys.reduce(function (sum, key) {
            return sum + parseFloat(obj[key]);
        }, 0);
    }
 
    return 0;
}
 
/**
 * Create donut status chart
 *
 * @param status
 * @returns {void|*|jQuery}
 */
const getStatusChart = function (status) {
    let $element = $('<div />').addClass('text-center status-chart-wrap');
    let $chart = $('<div />').appendTo($element);
 
    let total = sum(status);
 
    // return empty element, if status has no data
    if (total === 0) {
        return $element;
    }
 
    // calculate percents
    let title = Math.floor((status.done / total) * 100) + '%';
 
    // timeout is necessary, becaouse we should create the chart after the dom element are created
    setTimeout(function () {
        c3.generate({
            bindto: $chart[0],
            data: {
                colors: colors,
                columns: [
                    ['done', status['done'] || 0],
                    ['indeterminate', status['indeterminate'] || 0],
                    ['new', status['new'] || 0]
                ],
                names: statusCategories,
                type: 'donut'
            },
            size: {
                height: 90,
                width: 90
            },
            legend: {
                show: false
            },
            donut: {
                title: title,
                label: {
                    show: false
                },
                width: 13
            }
        });
    }, 50);
 
    $('<small />').text('Total: '+total).appendTo($element);
 
    return $element;
}
 
$.each(data, function (epicKey, groups) {
 
    // create table row
    let $tr = $('<tr />').appendTo($tbody);
 
    // create first column label
    let $strong = $('<a />').attr({
        href : chartData.contextPath +'/browse/'+ epicKey
    }).text(chartData.epicData[epicKey] +" ["+epicKey + "]");
 
    // create first column
    $('<td />').append($strong).appendTo($tr);
 
    // create charts columns
    $.each(groups, function (groupName, status) {
        $('<td />').addClass('text-center').append(getStatusChart(status)).appendTo($tr);
    });
});



UI Expand
titleData Script


Code Block
languagejs
themeMidnight
titleGroovy Script for Jira < 8
linenumberstrue
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.config.StatusCategoryManager
import com.atlassian.jira.issue.CustomFieldManager
import com.atlassian.jira.issue.DocumentIssueImpl
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.fields.CustomField
import com.atlassian.jira.issue.link.IssueLink
import com.atlassian.jira.issue.search.SearchResults
import com.atlassian.jira.issue.search.providers.LuceneSearchProvider
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.rest.api.util.ErrorCollection
import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.query.Query
import com.decadis.jira.xchart.api.GroupValueExtractor
import com.decadis.jira.xchart.api.ValueExtractor
import com.decadis.jira.xchart.calculating.ValueExtractorBuilder
import com.decadis.jira.xchart.grouping.GroupValueExtractorFactory
import org.apache.commons.lang.StringUtils
import org.apache.lucene.document.Document
 
 
import java.lang.reflect.Field
 
/**
 * Get issues on JQL
 *
 * @param jqlString
 *
 * @return
 */
static List<Issue> getIssues(final String jqlString) {
    final LuceneSearchProvider luceneSearchProvider = ComponentAccessor.getComponent(LuceneSearchProvider.class)
    final JqlQueryParser jqlQueryParser = ComponentAccessor.getComponent(JqlQueryParser.class)
 
    Query query = jqlQueryParser.parseQuery(jqlString)
    SearchResults searchResults = luceneSearchProvider.searchOverrideSecurity(query, null, PagerFilter.getUnlimitedFilter(), null)
 
    return searchResults.getIssues()
}
 
/**
 *
 * @param issue
 * @param groupValueExtractor
 *
 * @return
 */
static Map<String, String> getGroups(final Issue issue, final GroupValueExtractor<?> groupValueExtractor) {
    final Map<String, String> result = new TreeMap<>()
 
    try{
        Field documentField = DocumentIssueImpl.class.getDeclaredField("document")
        documentField.setAccessible(true)
        Document document = (Document) documentField.get(issue)
 
        for (String value : groupValueExtractor.getGroups(document)) {
            if (StringUtils.isNotBlank(value)) {
                result.put(value, groupValueExtractor.getResolvedValue(value, issue))
            }
        }
    }
    catch (Exception e){
 
    }
 
    return result
}
 
/**
 * fill all empty values with empty map
 *
 * @param data
 * @param groups
 *
 * @return
 */
static void fillMissingValues(final Map<String, Map<String, Map<String, Integer>>> data, final Map<String, String> groups)
{
    for (Map.Entry<String, Map<String, Map<String, Integer>>> epics : data.entrySet()){
        Map<String, Map<String, Integer>> group = epics.getValue()
 
        for(String key : groups.keySet())
        {
            if(!group.containsKey(key)){
                group.put(key, new TreeMap<>())
            }
        }
    }
}
 
 
final StatusCategoryManager statusCategoryManager = ComponentAccessor.getComponent(StatusCategoryManager.class)
 
// create group value extractor
final GroupValueExtractor<?> groupValueExtractor = GroupValueExtractorFactory.Create(GroupBy, "");
 
Map<String, Object> result = new HashMap<>()
Map<String, String> groups = new TreeMap<>()
Map<String, String> epicData = new TreeMap<>()
Map<String, Integer> statusMap = new TreeMap<>()
Map<String, String> statusCategoriesMeta = new HashMap<>()
Map<String, Map<String, Map<String, Integer>>> data = new HashMap<>()
 
// get all epic issues
List<Issue> epics = getIssues(JQL);
 
// mapping status category values
statusCategoryManager.getStatusCategories().each {
    if (!"undefined".equalsIgnoreCase(it.getKey())){
        statusMap.put(it.getKey(), 0)
        statusCategoriesMeta.put(it.getKey(), it.getTranslatedName())
    }
}
 
for (Issue epic : epics)
{
    Map<String, Map<String, Integer>> groupData = new TreeMap<>()
 
    for (Issue story : getIssues("cf["+ Epic_Link_FieldID +"] = " + epic.getKey()))
    {
        Map<String, String> groupsTemp = getGroups(story, groupValueExtractor)
 
        groups.putAll(groupsTemp)
 
        for(String groupName : groupsTemp.keySet())
        {
            Map<String, Integer> statusMapTemp = groupData.get(groupName)
 
            if(statusMapTemp == null)
            {
                statusMapTemp = new TreeMap<>(statusMap)
            }
 
            String statusCategoryKey = story.getStatus().getStatusCategory().getKey()
 
            if(statusCategoryKey != null)
            {
                statusMapTemp.put(statusCategoryKey, ++statusMapTemp.get(statusCategoryKey))
            }
 
            groupData.put(groupName, statusMapTemp)
        }
    }
 
    data.put(epic.getKey(), groupData)
    epicData.put(epic.getKey(), epic.getSummary())
}
 
fillMissingValues(data, groups)
 
result.put("groups", groups)
result.put("data", data)
result.put("statusCategories", statusCategoriesMeta)
result.put("epicData", epicData)
result.put("contextPath", ComponentAccessor.getApplicationProperties().getString("jira.baseurl"))
 
return result


Code Block
languagejs
themeMidnight
titleGroovy script for Jira >= 8
linenumberstrue
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.config.StatusCategoryManager
import com.atlassian.jira.issue.CustomFieldManager
import com.atlassian.jira.issue.DocumentIssueImpl
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.fields.CustomField
import com.atlassian.jira.issue.link.IssueLink
import com.atlassian.jira.issue.search.SearchResults
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.rest.api.util.ErrorCollection
import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.query.Query
import com.decadis.jira.xchart.api.GroupValueExtractor
import com.decadis.jira.xchart.api.ValueExtractor
import com.decadis.jira.xchart.calculating.ValueExtractorBuilder
import com.decadis.jira.xchart.grouping.GroupValueExtractorFactory
import org.apache.commons.lang.StringUtils
import org.apache.lucene.document.Document
import com.atlassian.jira.bc.issue.search.SearchService;
  
import java.lang.reflect.Field
  
/**
 * Get issues on JQL
 *
 * @param jqlString
 *
 * @return
 */
 List<Issue> getIssues(final String jqlString) {
        SearchService serchService = ComponentAccessor.getComponentOfType(SearchService.class);
      SearchService.ParseResult parseResult =
    serchService.parseQuery(ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser(),
    jqlString);
            
        if (!parseResult.isValid())
        {
            throw new Exception("jql is not valid. jql was "+jqlString);
        }
            
        Query query = parseResult.getQuery();
          SearchResults sr =
    serchService.search(ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser(),
    query, PagerFilter.getUnlimitedFilter());
            return sr.getResults();
       
}
  
/**
 *
 * @param issue
 * @param groupValueExtractor
 *
 * @return
 */
static Map<String, String> getGroups(final Issue issue, final GroupValueExtractor<?> groupValueExtractor) {
    final Map<String, String> result = new TreeMap<>()
  
    try{
        Field documentField = DocumentIssueImpl.class.getDeclaredField("document")
        documentField.setAccessible(true)
        Document document = (Document) documentField.get(issue)
  
        for (String value : groupValueExtractor.getGroups(document)) {
            if (StringUtils.isNotBlank(value)) {
                result.put(value, groupValueExtractor.getResolvedValue(value, issue))
            }
        }
    }
    catch (Exception e){
  
    }
  
    return result
}
  
/**
 * fill all empty values with empty map
 *
 * @param data
 * @param groups
 *
 * @return
 */
static void fillMissingValues(final Map<String, Map<String, Map<String, Integer>>> data, final Map<String, String> groups)
{
    for (Map.Entry<String, Map<String, Map<String, Integer>>> epics : data.entrySet()){
        Map<String, Map<String, Integer>> group = epics.getValue()
  
        for(String key : groups.keySet())
        {
            if(!group.containsKey(key)){
                group.put(key, new TreeMap<>())
            }
        }
    }
}
  
  
final StatusCategoryManager statusCategoryManager = ComponentAccessor.getComponent(StatusCategoryManager.class)
  
// create group value extractor
final GroupValueExtractor<?> groupValueExtractor = GroupValueExtractorFactory.Create(GroupBy, "");
  
Map<String, Object> result = new HashMap<>()
Map<String, String> groups = new TreeMap<>()
Map<String, String> epicData = new TreeMap<>()
Map<String, Integer> statusMap = new TreeMap<>()
Map<String, String> statusCategoriesMeta = new HashMap<>()
Map<String, Map<String, Map<String, Integer>>> data = new HashMap<>()
  
// get all epic issues
List<Issue> epics = getIssues(JQL);
  
// mapping status category values
statusCategoryManager.getStatusCategories().each {
    if (!"undefined".equalsIgnoreCase(it.getKey())){
        statusMap.put(it.getKey(), 0)
        statusCategoriesMeta.put(it.getKey(), it.getTranslatedName())
    }
}
  
for (Issue epic : epics)
{
    Map<String, Map<String, Integer>> groupData = new TreeMap<>()
  
    for (Issue story : getIssues("cf["+ Epic_Link_FieldID +"] = " + epic.getKey()))
    {
        Map<String, String> groupsTemp = getGroups(story, groupValueExtractor)
  
        groups.putAll(groupsTemp)
  
        for(String groupName : groupsTemp.keySet())
        {
            Map<String, Integer> statusMapTemp = groupData.get(groupName)
  
            if(statusMapTemp == null)
            {
                statusMapTemp = new TreeMap<>(statusMap)
            }
  
            String statusCategoryKey = story.getStatus().getStatusCategory().getKey()
  
            if(statusCategoryKey != null)
            {
                statusMapTemp.put(statusCategoryKey, ++statusMapTemp.get(statusCategoryKey))
            }
  
            groupData.put(groupName, statusMapTemp)
        }
    }
  
    data.put(epic.getKey(), groupData)
    epicData.put(epic.getKey(), epic.getSummary())
}
  
fillMissingValues(data, groups)
  
result.put("groups", groups)
result.put("data", data)
result.put("statusCategories", statusCategoriesMeta)
result.put("epicData", epicData)
result.put("contextPath", ComponentAccessor.getApplicationProperties().getString("jira.baseurl"))
  
return result




Related examples

Page properties report
cqllabel = "chart_script_example" and space = currentSpace()


Excerpt Include
DECADIS:Contact support
DECADIS:Contact support
nopaneltrue