Replace nvd3 with Chart.js

nvd3 and its python/django wrappers appear to be no longer actively
maintained, and at least the wrappers were a bit clunky to use. Looking
around for a suitable replacement, Chart.js seems capable, has no
additional dependencies and is fairly simple to use. As a bonus we get
to drop a few Python dependencies from our list.

Signed-off-by: Paul Eggleton <paul.eggleton@linux.intel.com>
This commit is contained in:
Paul Eggleton 2018-10-11 14:53:55 +13:00
parent e591d1820a
commit 631116a1c4
14 changed files with 14531 additions and 24356 deletions

4
README
View File

@ -361,9 +361,7 @@ Bundled jQuery is redistributed under the MIT license.
Bundled uitablefilter.js is redistributed under the MIT license.
Bundled nv.d3.js is redistributed under the Apache License 2.0.
Bundled d3.js is redistributed under the BSD License.
Bundled Chart.js is redistributed under the MIT license.
All other content is copyright (C) 2013-2018 Intel Corporation and
licensed under the MIT license (unless otherwise noted) - see

View File

@ -154,7 +154,6 @@ INSTALLED_APPS = (
'captcha',
'rest_framework',
'corsheaders',
'django_nvd3'
)
REST_FRAMEWORK = {

View File

@ -1,645 +0,0 @@
/********************
* HTML CSS
*/
.chartWrap {
margin: 0;
padding: 0;
overflow: hidden;
}
/********************
* TOOLTIP CSS
*/
.nvtooltip {
position: absolute;
background-color: rgba(255,255,255,1);
padding: 10px;
border: 1px solid #ddd;
z-index: 10000;
font-family: Arial;
font-size: 13px;
transition: opacity 500ms linear;
-moz-transition: opacity 500ms linear;
-webkit-transition: opacity 500ms linear;
transition-delay: 500ms;
-moz-transition-delay: 500ms;
-webkit-transition-delay: 500ms;
-moz-box-shadow: 4px 4px 8px rgba(0,0,0,.5);
-webkit-box-shadow: 4px 4px 8px rgba(0,0,0,.5);
box-shadow: 4px 4px 8px rgba(0,0,0,.5);
-moz-border-radius: 10px;
border-radius: 10px;
pointer-events: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.nvtooltip h3 {
margin: 0;
padding: 0;
text-align: center;
}
.nvtooltip p {
margin: 0;
padding: 0;
text-align: center;
}
.nvtooltip span {
display: inline-block;
margin: 2px 0;
}
.nvtooltip-pending-removal {
position: absolute;
pointer-events: none;
}
/********************
* SVG CSS
*/
svg {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
/* Trying to get SVG to act like a greedy block in all browsers */
display: block;
width:100%;
height:100%;
}
svg text {
font: normal 12px sans-serif;
}
svg .title {
font: bold 14px Arial;
}
.nvd3 .nv-background {
fill: white;
fill-opacity: 0;
/*
pointer-events: none;
*/
}
.nvd3.nv-noData {
font-size: 18px;
font-weight: bolf;
}
/**********
* Brush
*/
.nv-brush .extent {
fill-opacity: .125;
shape-rendering: crispEdges;
}
/**********
* Legend
*/
.nvd3 .nv-legend .nv-series {
}
.nvd3 .nv-legend .disabled circle {
fill-opacity: 0;
}
/**********
* Axes
*/
.nvd3 .nv-axis path {
fill: none;
stroke: #000;
stroke-opacity: .75;
shape-rendering: crispEdges;
}
.nvd3 .nv-axis path.domain {
stroke-opacity: .75;
}
.nvd3 .nv-axis.nv-x path.domain {
stroke-opacity: 0;
}
.nvd3 .nv-axis line {
fill: none;
stroke: #000;
stroke-opacity: .25;
shape-rendering: crispEdges;
}
.nvd3 .nv-axis line.zero {
stroke-opacity: .75;
}
.nvd3 .nv-axis .nv-axisMaxMin text {
font-weight: bold;
}
.nvd3 .x .nv-axis .nv-axisMaxMin text,
.nvd3 .x2 .nv-axis .nv-axisMaxMin text,
.nvd3 .x3 .nv-axis .nv-axisMaxMin text {
text-anchor: middle
}
/**********
* Brush
*/
.nv-brush .resize path {
fill: #eee;
stroke: #666;
}
/**********
* Bars
*/
.nvd3 .nv-bars .negative rect {
zfill: brown;
}
.nvd3 .nv-bars rect {
zfill: steelblue;
fill-opacity: .75;
transition: fill-opacity 250ms linear;
-moz-transition: fill-opacity 250ms linear;
-webkit-transition: fill-opacity 250ms linear;
}
.nvd3 .nv-bars rect:hover {
fill-opacity: 1;
}
.nvd3 .nv-bars .hover rect {
fill: lightblue;
}
.nvd3 .nv-bars text {
fill: rgba(0,0,0,0);
}
.nvd3 .nv-bars .hover text {
fill: rgba(0,0,0,1);
}
.nvd3 .nv-x.nv-axis text {
transform: rotate(90);
}
/**********
* Bars
*/
.nvd3 .nv-multibar .nv-groups rect,
.nvd3 .nv-multibarHorizontal .nv-groups rect,
.nvd3 .nv-discretebar .nv-groups rect {
stroke-opacity: 0;
transition: fill-opacity 250ms linear;
-moz-transition: fill-opacity 250ms linear;
-webkit-transition: fill-opacity 250ms linear;
}
.nvd3 .nv-multibar .nv-groups rect:hover,
.nvd3 .nv-multibarHorizontal .nv-groups rect:hover,
.nvd3 .nv-discretebar .nv-groups rect:hover {
fill-opacity: 1;
}
.nvd3 .nv-discretebar .nv-groups text,
.nvd3 .nv-multibarHorizontal .nv-groups text {
font-weight: bold;
fill: rgba(0,0,0,1);
stroke: rgba(0,0,0,0);
}
/***********
* Pie Chart
*/
.nvd3.nv-pie path {
stroke-opacity: 0;
transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
-moz-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
-webkit-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
}
.nvd3.nv-pie .nv-slice text {
stroke: #000;
stroke-width: 0;
}
.nvd3.nv-pie path {
stroke: #fff;
stroke-width: 1px;
stroke-opacity: 1;
}
.nvd3.nv-pie .hover path {
fill-opacity: .7;
/*
stroke-width: 6px;
stroke-opacity: 1;
*/
}
.nvd3.nv-pie .nv-label rect {
fill-opacity: 0;
stroke-opacity: 0;
}
/**********
* Lines
*/
.nvd3 .nv-groups path.nv-line {
fill: none;
stroke-width: 2.5px;
stroke-linecap: round;
shape-rendering: geometricPrecision;
/*
transition: stroke-width 250ms linear;
-moz-transition: stroke-width 250ms linear;
-webkit-transition: stroke-width 250ms linear;
transition-delay: 250ms
-moz-transition-delay: 250ms;
-webkit-transition-delay: 250ms;
*/
}
.nvd3 .nv-groups path.nv-area {
stroke: none;
stroke-linecap: round;
shape-rendering: geometricPrecision;
/*
stroke-width: 2.5px;
transition: stroke-width 250ms linear;
-moz-transition: stroke-width 250ms linear;
-webkit-transition: stroke-width 250ms linear;
transition-delay: 250ms
-moz-transition-delay: 250ms;
-webkit-transition-delay: 250ms;
*/
}
.nvd3 .nv-line.hover path {
stroke-width: 6px;
}
/*
.nvd3.scatter .groups .point {
fill-opacity: 0.1;
stroke-opacity: 0.1;
}
*/
.nvd3.nv-line .nvd3.nv-scatter .nv-groups .nv-point {
fill-opacity: 0;
stroke-opacity: 0;
}
.nvd3.nv-scatter.nv-single-point .nv-groups .nv-point {
fill-opacity: .5 !important;
stroke-opacity: .5 !important;
}
.nvd3 .nv-groups .nv-point {
transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
-moz-transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
-webkit-transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
}
.nvd3.nv-scatter .nv-groups .nv-point.hover,
.nvd3 .nv-groups .nv-point.hover {
stroke-width: 20px;
fill-opacity: .5 !important;
stroke-opacity: .5 !important;
}
.nvd3 .nv-point-paths path {
stroke: #aaa;
stroke-opacity: 0;
fill: #eee;
fill-opacity: 0;
}
.nvd3 .nv-indexLine {
cursor: ew-resize;
}
/**********
* Distribution
*/
.nvd3 .nv-distribution {
pointer-events: none;
}
/**********
* Scatter
*/
.nvd3 .nv-groups .nv-point {
pointer-events: none;
}
.nvd3 .nv-groups .nv-point.hover {
stroke-width: 20px;
stroke-opacity: .5;
}
.nvd3 .nv-scatter .nv-point.hover {
fill-opacity: 1;
}
/*
.nv-group.hover .nv-point {
fill-opacity: 1;
}
*/
/**********
* Stacked Area
*/
.nvd3.nv-stackedarea path.nv-area {
fill-opacity: .7;
/*
stroke-opacity: .65;
fill-opacity: 1;
*/
stroke-opacity: 0;
transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
-moz-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
-webkit-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
/*
transition-delay: 500ms;
-moz-transition-delay: 500ms;
-webkit-transition-delay: 500ms;
*/
}
.nvd3.nv-stackedarea path.nv-area.hover {
fill-opacity: .9;
/*
stroke-opacity: .85;
*/
}
/*
.d3stackedarea .groups path {
stroke-opacity: 0;
}
*/
.nvd3.nv-stackedarea .nv-groups .nv-point {
stroke-opacity: 0;
fill-opacity: 0;
}
.nvd3.nv-stackedarea .nv-groups .nv-point.hover {
stroke-width: 20px;
stroke-opacity: .75;
fill-opacity: 1;
}
/**********
* Line Plus Bar
*/
.nvd3.nv-linePlusBar .nv-bar rect {
fill-opacity: .75;
}
.nvd3.nv-linePlusBar .nv-bar rect:hover {
fill-opacity: 1;
}
/**********
* Bullet
*/
.nvd3.nv-bullet { font: 10px sans-serif; }
.nvd3.nv-bullet rect { fill-opacity: .6; }
.nvd3.nv-bullet rect:hover { fill-opacity: 1; }
.nvd3.nv-bullet .nv-marker { stroke: #000; stroke-width: 2px; }
.nvd3.nv-bullet .nv-markerTriangle { stroke: #000; fill: #fff; stroke-width: 1.5px; }
.nvd3.nv-bullet .nv-tick line { stroke: #666; stroke-width: .5px; }
.nvd3.nv-bullet .nv-range.nv-s0 { fill: #eee; }
.nvd3.nv-bullet .nv-range.nv-s1 { fill: #ddd; }
.nvd3.nv-bullet .nv-range.nv-s2 { fill: #ccc; }
.nvd3.nv-bullet .nv-title { font-size: 14px; font-weight: bold; }
.nvd3.nv-bullet .nv-subtitle { fill: #999; }
/**********
* Sparkline
*/
.nvd3.nv-sparkline path {
fill: none;
}
.nvd3.nv-sparklineplus g.nv-hoverValue {
pointer-events: none;
}
.nvd3.nv-sparklineplus .nv-hoverValue line {
stroke: #f44;
stroke-width: 1.5px;
}
.nvd3.nv-sparklineplus,
.nvd3.nv-sparklineplus g {
pointer-events: all;
}
.nvd3 .nv-hoverArea {
fill-opacity: 0;
stroke-opacity: 0;
}
.nvd3.nv-sparklineplus .nv-xValue,
.nvd3.nv-sparklineplus .nv-yValue {
/*
stroke: #666;
*/
stroke-width: 0;
font-size: .8em;
font-weight: normal;
}
.nvd3.nv-sparklineplus .nv-yValue {
stroke: #f66;
}
.nvd3.nv-sparklineplus .nv-maxValue {
stroke: #2ca02c;
fill: #2ca02c;
}
.nvd3.nv-sparklineplus .nv-minValue {
stroke: #d62728;
fill: #d62728;
}
.nvd3.nv-sparklineplus .nv-currentValue {
stroke: #444;
fill: #444;
}
/**********
* historical stock
*/
.nvd3.nv-ohlcBar .nv-ticks .nv-tick {
stroke-width: 2px;
}
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.hover {
stroke-width: 4px;
}
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.positive {
stroke: #2ca02c;
}
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.negative {
stroke: #d62728;
}
.nvd3.nv-historicalStockChart .nv-axis .nv-axislabel {
font-weight: bold;
}
.nvd3.nv-historicalStockChart .nv-dragTarget {
fill-opacity: 0;
stroke: none;
cursor: move;
}
.nvd3 .nv-brush .extent {
/*
cursor: ew-resize !important;
*/
fill-opacity: 0 !important;
}
.nvd3 .nv-brushBackground rect {
stroke: #000;
stroke-width: .4;
fill: #fff;
fill-opacity: .7;
}
/**********
* Indented Tree
*/
/**
* TODO: the following 3 selectors are based on classes used in the example. I should either make them standard and leave them here, or move to a CSS file not included in the library
*/
.nvd3.nv-indentedtree .name {
margin-left: 5px;
}
.nvd3.nv-indentedtree .clickable {
color: #08C;
cursor: pointer;
}
.nvd3.nv-indentedtree span.clickable:hover {
color: #005580;
text-decoration: underline;
}
.nvd3.nv-indentedtree .nv-childrenCount {
display: inline-block;
margin-left: 5px;
}
.nvd3.nv-indentedtree .nv-treeicon {
cursor: pointer;
/*
cursor: n-resize;
*/
}
.nvd3.nv-indentedtree .nv-treeicon.nv-folded {
cursor: pointer;
/*
cursor: s-resize;
*/
}

14384
layerindex/static/js/Chart.js vendored Normal file

File diff suppressed because it is too large Load Diff

10
layerindex/static/js/Chart.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1258,18 +1258,13 @@ class ClassicRecipeStatsView(TemplateView):
statuses = []
status_counts = {}
for choice, desc in ClassicRecipe.COVER_STATUS_CHOICES:
statuses.append(desc)
status_counts[desc] = recipes.filter(cover_status=choice).count()
count = recipes.filter(cover_status=choice).count()
if count > 0:
statuses.append(desc)
status_counts[desc] = count
statuses = sorted(statuses, key=lambda status: status_counts[status], reverse=True)
chartdata = {'x': statuses, 'y': [status_counts[k] for k in statuses]}
context['charttype_status'] = 'pieChart'
context['chartdata_status'] = chartdata
context['extra_status'] = {
'x_is_date': False,
'x_axis_format': '',
'tag_script_js': True,
'jquery_on_ready': False,
}
context['chart_status_labels'] = statuses
context['chart_status_values'] = [status_counts[status] for status in statuses]
# *** Categories chart ***
categories = ['obsoletedir', 'nonworkingdir']
uniquevals = recipes.exclude(classic_category='').values_list('classic_category', flat=True).distinct()
@ -1296,15 +1291,8 @@ class ClassicRecipeStatsView(TemplateView):
# Eliminate categories with zero count
categories = [cat for cat in categories if catcounts[cat] > 0]
categories = sorted(categories, key=lambda cat: catcounts[cat], reverse=True)
chartdata_category = {'x': categories, 'y': [catcounts[k] for k in categories]}
context['charttype_category'] = 'pieChart'
context['chartdata_category'] = chartdata_category
context['extra_category'] = {
'x_is_date': False,
'x_axis_format': '',
'tag_script_js': True,
'jquery_on_ready': False,
}
context['chart_category_labels'] = categories
context['chart_category_values'] = [catcounts[k] for k in categories]
return context

View File

@ -6,7 +6,6 @@ celery==4.1.0
confusable-homoglyphs==3.0.0
Django>1.11.0,<1.12
django-cors-headers==1.1.0
django-nvd3==0.9.7
django-ranged-response==0.2.0
django-registration==2.4.1
django-reversion==2.0.13
@ -15,16 +14,11 @@ django-simple-captcha==0.5.6
djangorestframework==3.8.2
gitdb==0.6.4
GitPython==2.1.8
Jinja2==2.10
kombu==4.1.0
MarkupSafe==1.0
mysqlclient==1.3.12
Pillow==5.1.0
python-nvd3==0.14.2
python-slugify==1.1.4
pytz==2018.4
six==1.11.0
smmap==0.9.0
smmap2==2.0.3
Unidecode==0.4.19
vine==1.1.4

View File

@ -912,15 +912,8 @@ class MaintenanceStatsView(TemplateView):
status_counts['Unknown'] = self.milestone_statistics['unknown']
statuses = sorted(statuses, key=lambda status: status_counts[status], reverse=True)
chartdata = {'x': statuses, 'y': [status_counts[k] for k in statuses]}
context['charttype_status'] = 'discreteBarChart'
context['chartdata_status'] = chartdata
context['extra_status'] = {
'x_is_date': False,
'x_axis_format': '',
'tag_script_js': True,
'jquery_on_ready': False,
}
context['chart_upstream_status_labels'] = statuses
context['chart_upstream_status_values'] = [status_counts[k] for k in statuses]
# *** Patch status chart ***
patch_statuses = []
@ -934,15 +927,8 @@ class MaintenanceStatsView(TemplateView):
patch_status_counts[desc] = patch_status_counts.get(desc, 0) + patches.filter(status=choice).count()
patch_statuses = sorted(patch_statuses, key=lambda status: patch_status_counts[status], reverse=True)
chartdata = {'x': patch_statuses, 'y': [patch_status_counts[k] for k in patch_statuses]}
context['charttype_patchstatus'] = 'discreteBarChart'
context['chartdata_patchstatus'] = chartdata
context['extra_patchstatus'] = {
'x_is_date': False,
'x_axis_format': '',
'tag_script_js': True,
'jquery_on_ready': False,
}
context['chart_patch_status_labels'] = patch_statuses
context['chart_patch_status_values'] = [patch_status_counts[k] for k in patch_statuses]
return context

View File

@ -154,7 +154,6 @@ INSTALLED_APPS = (
'captcha',
'rest_framework',
'corsheaders',
'django_nvd3'
)
REST_FRAMEWORK = {

View File

@ -24,13 +24,12 @@
<li><a href="http://github.com/etianen/django-reversion">django-reversion</a></li>
<li><a href="http://pypi.python.org/pypi/django-reversion-compare/">django-reversion-compare</a></li>
<li><a href="http://github.com/mbi/django-simple-captcha">django-simple-captcha</a></li>
<li><a href="http://github.com/areski/django-nvd3">django-nvd3</a></li>
</ul>
</li>
<li><a href="https://getbootstrap.com/">Bootstrap</a> (3.x)</li>
<li><a href="http://glyphicons.com/">Glyphicons</a> (via Bootstrap)</li>
<li><a href="http://jquery.com/">jQuery</a></li>
<li><a href="http://nvd3.org/">NVD3</a></li>
<li><a href="http://www.chartjs.org/">Chart.js</a></li>
<li><a href="http://www.openembedded.org/wiki/BitBake_%28user%29">BitBake</a></li>
</ul>

View File

@ -1,6 +1,5 @@
{% extends "layerindex/classic_base.html" %}
{% load i18n %}
{% load nvd3_tags %}
{% load staticfiles %}
{% comment %}
@ -38,10 +37,16 @@
{% endif %}
<h3>Comparison status</h3>
{% include_container "chart_status" 400 600 %}
<div>
<canvas id="chart_status">
</canvas>
</div>
<h3>Unavailable recipes by category</h3>
{% include_container "chart_category" 400 600 %}
<div>
<canvas id="chart_category">
</canvas>
</div>
</div>
@ -50,11 +55,56 @@
{% endblock %}
{% block scripts %}
<script src="{% static "js/Chart.js" %}"></script>
<script>
$(document).ready(function() {
chartColors = {
red: 'rgb(255, 99, 132)',
yellow: 'rgb(255, 205, 86)',
green: 'rgb(75, 192, 192)',
green2: 'rgb(51, 204, 51)',
blue: 'rgb(54, 162, 235)',
purple: 'rgb(153, 102, 255)',
pink: 'rgb(255, 51, 153)',
grey: 'rgb(201, 203, 207)',
};
chartColorsArray = Object.values(chartColors);
<link media="all" href="{% static "css/nv.d3.css" %}" type="text/css" rel="stylesheet" />
<script src="{% static "js/d3.js" %}" type="text/javascript"></script>
<script src="{% static "js/nv.d3.js" %}" type="text/javascript"></script>
// --- Status chart ---
var config = {
type: 'pie',
data: {
datasets: [{
data: {{ chart_status_values }},
backgroundColor: chartColorsArray,
}],
labels: {{ chart_status_labels|safe }}
},
options: {
responsive: true
}
};
{% load_chart charttype_status chartdata_status "chart_status" extra_status %}
{% load_chart charttype_category chartdata_category "chart_category" extra_category %}
var ctx = document.getElementById('chart_status').getContext('2d');
window.myPie = new Chart(ctx, config);
// --- Categories chart ---
var config = {
type: 'pie',
data: {
datasets: [{
data: {{ chart_category_values }},
backgroundColor: chartColorsArray,
}],
labels: {{ chart_category_labels|safe }}
},
options: {
responsive: true
}
};
var ctx = document.getElementById('chart_category').getContext('2d');
new Chart(ctx, config);
});
</script>
{% endblock %}

View File

@ -1,6 +1,5 @@
{% extends "rrs/base_toplevel.html" %}
{% load i18n %}
{% load nvd3_tags %}
{% load staticfiles %}
{% comment %}
@ -26,10 +25,16 @@
<h2>Maintenance statistics - {{ maintplan_name }} {{ release_name }} {{ milestone_name }}</h2>
<h3>Upstream status (milestone)</h3>
{% include_container "chart_status" 400 500 %}
<div style="width: 75%;">
<canvas id="chart_upstream_status">
</canvas>
</div>
<h3>Patch status (current)</h3>
{% include_container "chart_patchstatus" 400 700 %}
<div style="width: 75%;">
<canvas id="chart_patch_status">
</canvas>
</div>
</div>
@ -39,10 +44,62 @@
{% block scripts %}
<link media="all" href="{% static "css/nv.d3.css" %}" type="text/css" rel="stylesheet" />
<script src="{% static "js/d3.js" %}" type="text/javascript"></script>
<script src="{% static "js/nv.d3.js" %}" type="text/javascript"></script>
<script src="{% static "js/Chart.js" %}"></script>
<script>
$(document).ready(function() {
chartColors = {
red: 'rgb(255, 99, 132)',
yellow: 'rgb(255, 205, 86)',
green: 'rgb(75, 192, 192)',
green2: 'rgb(51, 204, 51)',
blue: 'rgb(54, 162, 235)',
purple: 'rgb(153, 102, 255)',
pink: 'rgb(255, 51, 153)',
grey: 'rgb(201, 203, 207)',
};
chartColorsArray = Object.values(chartColors);
{% load_chart charttype_status chartdata_status "chart_status" extra_status %}
{% load_chart charttype_patchstatus chartdata_patchstatus "chart_patchstatus" extra_patchstatus %}
// --- Status chart ---
var config = {
type: 'bar',
data: {
datasets: [{
data: {{ chart_upstream_status_values }},
backgroundColor: chartColorsArray,
}],
labels: {{ chart_upstream_status_labels|safe }}
},
options: {
responsive: true,
legend: {
display: false,
},
}
};
var ctx = document.getElementById('chart_upstream_status').getContext('2d');
window.myPie = new Chart(ctx, config);
// --- Patch status chart ---
var config = {
type: 'bar',
data: {
datasets: [{
data: {{ chart_patch_status_values }},
backgroundColor: chartColorsArray,
}],
labels: {{ chart_patch_status_labels|safe }}
},
options: {
responsive: true,
legend: {
display: false,
},
}
};
var ctx = document.getElementById('chart_patch_status').getContext('2d');
new Chart(ctx, config);
});
</script>
{% endblock %}