Adds d3.js library and reworked quota infographics

The d3.js library is now added and the quota infographics
have been redone to use d3.

Added a reusable animated d3 pie chart that is now used to
display the quota infographics.

Reworked the progress bars (horizon.quota.js) so that project
quotas when creating a new Instance / Volume display a progress bar
in D3. Also changed progress bar for adding a floating ip so that
it shows that 1 will be used up on create just like adding a new volume.

Change-Id: I2930c3c80736e50d3f38c001aac1918e1932be5e
Implements: blueprint d3
This commit is contained in:
Bradley Jones 2013-05-06 16:53:11 -07:00
parent 09fc3f1fbb
commit ccb11b7099
11 changed files with 240 additions and 62 deletions

View File

@ -0,0 +1,91 @@
/*
Draw pie chart in d3.
To use, a div is required with the class .d3_pie_chart
and a data-used attribute in the div
that stores the percentage to fill the chart
Example:
<div class="d3_pie_chart"
data-used="{% widthratio current_val max_val 100 %}">
</div>
*/
horizon.d3_pie_chart = {
w: 100,
h: 100,
r: 45,
bkgrnd: "#F2F2F2",
frgrnd: "#4790B2",
init: function() {
var self = this;
// Pie Charts
var pie_chart_data = $(".d3_pie_chart");
self.chart = d3.selectAll(".d3_pie_chart");
for (var i = 0; i < pie_chart_data.length; i++) {
used = parseInt(pie_chart_data[i].dataset.used);
self.data = [{"percentage":used}, {"percentage":100 - used}];
self.pieChart(i);
}
},
// Draw a pie chart
pieChart: function(i) {
var self = this;
var vis = d3.select(self.chart[0][i]).append("svg:svg")
.attr("class", "chart")
.attr("width", self.w)
.attr("height", self.h)
.style("background-color", "white")
.append("g")
.attr("transform", "translate(" + (self.r + 2) + "," + (self.r + 2) + ")")
var arc = d3.svg.arc()
.outerRadius(self.r)
.innerRadius(0)
var pie = d3.layout.pie()
.sort(null)
.value(function(d){ return d.percentage; })
// Draw an empty pie chart
var piechart = vis.selectAll(".arc")
.data(pie([{"percentage":10}]))
.enter()
.append("path")
.attr("class","arc")
.attr("d", arc)
.style("fill", self.frgrnd)
.style("stroke", "#CCCCCC")
.style("stroke-width", 1)
.each(function(d) {return self.current = d;})
// Animate filling the pie chart
animate = function(data) {
var piechart = vis.selectAll(".arc")
.data(pie(data))
.enter()
.append("path")
.attr("class","arc")
.attr("d", arc)
.style("fill", self.bkgrnd)
.style("stroke", "#CCCCCC")
.style("stroke-width", 1)
.each(function(d) {return self.current = d;})
.transition()
.duration(500)
.attrTween("d", function(a) {
var tween = d3.interpolate(self.current, a);
self.current = tween(0);
return function(t) { return arc(tween(t)); }
})
}
animate(self.data)
}
}
horizon.addInitFunction(function () {
horizon.d3_pie_chart.init();
});

View File

@ -1,6 +1,6 @@
/* /*
Used for animating and displaying quota information on forms which use the Used for animating and displaying quota information on forms using
Bootstrap progress bars. Also used for displaying flavor details on modal- D3js progress bars. Also used for displaying flavor details on modal-
dialogs. dialogs.
Usage: Usage:
@ -8,7 +8,6 @@
DOM structure like this in your Django template: DOM structure like this in your Django template:
<div id="your_progress_bar_id" class="quota_bar"> <div id="your_progress_bar_id" class="quota_bar">
{% horizon_progress_bar total_number_used max_number_allowed %}
</div> </div>
With this progress bar, you then need to add some data- HTML attributes With this progress bar, you then need to add some data- HTML attributes
@ -67,6 +66,11 @@ horizon.Quota = {
return ('#' + $(elm).attr('data-progress-indicator-for')); return ('#' + $(elm).attr('data-progress-indicator-for'));
})); }));
// Draw the initial progress bars
this._initialCreation(this.user_value_progress_bars)
this._initialCreation(this.auto_value_progress_bars)
this._initialCreation(this.flavor_progress_bars)
this._initialAnimations(); this._initialAnimations();
this._attachInputHandlers(); this._attachInputHandlers();
}, },
@ -126,23 +130,6 @@ horizon.Quota = {
} }
}, },
// Updates a progress bar, taking care of exceeding quota display as well.
update: function(element, percentage_used, percentage_to_update) {
var update_width = percentage_to_update;
if(percentage_to_update + percentage_used > 100) {
update_width = 100 - percentage_used;
if(!element.hasClass('progress_bar_over')) {
element.addClass('progress_bar_over');
}
} else {
element.removeClass('progress_bar_over');
}
element.animate({width: parseInt(update_width, 10) + "%"}, 300);
},
/* /*
When a new flavor is selected, this takes care of updating the relevant When a new flavor is selected, this takes care of updating the relevant
progress bars associated with the flavor quota usage. progress bars associated with the flavor quota usage.
@ -176,13 +163,80 @@ horizon.Quota = {
updateUsageFor: function(progress_element, increment_by) { updateUsageFor: function(progress_element, increment_by) {
progress_element = $(progress_element); progress_element = $(progress_element);
var update_indicator = progress_element.find('.progress_bar_selected'); //var update_indicator = progress_element.find('.progress_bar_selected');
var quota_limit = parseInt(progress_element.attr('data-quota-limit'), 10); var quota_limit = parseInt(progress_element.attr('data-quota-limit'), 10);
var quota_used = parseInt(progress_element.attr('data-quota-used'), 10); var quota_used = parseInt(progress_element.attr('data-quota-used'), 10);
var percentage_to_update = ((increment_by / quota_limit) * 100); var percentage_to_update = ((increment_by / quota_limit) * 100);
var percentage_used = ((quota_used / quota_limit) * 100); var percentage_used = ((quota_used / quota_limit) * 100);
this.update(update_indicator, percentage_used, percentage_to_update); this.update($(progress_element).attr('id'), percentage_to_update);
},
// Create a new d3 bar and populate it with the current amount used
drawUsed: function(element, used) {
var w= "100%";
var h= 20;
var lvl_curve= 4;
var bkgrnd= "#F2F2F2";
var frgrnd= "grey";
// Horizontal Bars
var bar = d3.select("#"+element).append("svg:svg")
.attr("class", "chart")
.attr("width", w)
.attr("height", h)
.style("background-color", "white")
.append("g")
// background - unused resources
bar.append("rect")
.attr("y", 0)
.attr("width", w)
.attr("height", h)
.attr("rx", lvl_curve)
.attr("ry", lvl_curve)
.style("fill", bkgrnd)
.style("stroke", "#CCCCCC")
.style("stroke-width", 1)
// new resources
bar.append("rect")
.attr("y",0)
.attr("class", "newbar")
.attr("width", 0)
.attr("height", h)
.attr("rx", lvl_curve)
.attr("ry", lvl_curve)
.style("fill", "lightgreen")
// used resources
var used_bar = bar.insert("rect")
.attr("class", "usedbar")
.attr("y", 0)
.attr("id", "test")
.attr("width", 0)
.attr("height", h)
.attr("rx", lvl_curve)
.attr("ry", lvl_curve)
.style("fill", frgrnd)
.attr("d", used)
.transition()
.duration(500)
.attr("width", used + "%")
},
// Update the progress Bar
update: function(element, value) {
var already_used = parseInt(d3.select("#"+element).select(".usedbar").attr("d"))
d3.select("#"+element).select(".newbar")
.transition()
.duration(500)
.attr("width", (value + already_used) + "%")
.style("fill", function() {
if (value > (100 - already_used)) { return "red" }
else {return "lightgreen" }
});
}, },
/* /*
@ -242,5 +296,25 @@ horizon.Quota = {
scope.updateUsageFor(auto_progress, update_amount); scope.updateUsageFor(auto_progress, update_amount);
}); });
},
// Draw the initial d3 bars
_initialCreation: function(bars) {
// Draw the initial progress bars
var scope = this;
$(bars).each(function(index, element) {
var progress_element = $(element);
var quota_limit = parseInt(progress_element.attr('data-quota-limit'), 10);
var quota_used = parseInt(progress_element.attr('data-quota-used'), 10);
if (!isNaN(quota_limit) && !isNaN(quota_used)) {
var percentage_used = ((quota_used / quota_limit) * 100);
} else { // If NaN percentage_used is 0
var percentage_used = 0;
}
scope.drawUsed($(element).attr('id'), percentage_used);
});
} }
}; };

File diff suppressed because one or more lines are too long

View File

@ -16,6 +16,8 @@
<script src="{{ STATIC_URL }}horizon/lib/underscore/underscore-min.js" type="text/javascript" charset="utf-8"></script> <script src="{{ STATIC_URL }}horizon/lib/underscore/underscore-min.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ STATIC_URL }}horizon/lib/jquery/jquery-ui-1.9.2.custom.min.js" type="text/javascript" charset="utf-8"></script> <script src="{{ STATIC_URL }}horizon/lib/jquery/jquery-ui-1.9.2.custom.min.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ STATIC_URL }}horizon/lib/d3.v3.min.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ STATIC_URL }}bootstrap/js/bootstrap.min.js" type="text/javascript" charset="utf-8"></script> <script src="{{ STATIC_URL }}bootstrap/js/bootstrap.min.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ STATIC_URL }}horizon/lib/hogan-2.0.0.js" type="text/javascript" charset='utf-8'></script> <script src="{{ STATIC_URL }}horizon/lib/hogan-2.0.0.js" type="text/javascript" charset='utf-8'></script>
@ -34,6 +36,7 @@
<script src='{{ STATIC_URL }}horizon/js/horizon.utils.js' type='text/javascript' charset='utf-8'></script> <script src='{{ STATIC_URL }}horizon/js/horizon.utils.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.projects.js' type='text/javascript' charset='utf-8'></script> <script src='{{ STATIC_URL }}horizon/js/horizon.projects.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.networktopology.js' type='text/javascript' charset='utf-8'></script> <script src='{{ STATIC_URL }}horizon/js/horizon.networktopology.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.d3piechart.js' type='text/javascript' charset='utf-8'></script>
{% endcompress %} {% endcompress %}
{% comment %} Client-side Templates (These should *not* be inside the "compress" tag.) {% endcomment %} {% comment %} Client-side Templates (These should *not* be inside the "compress" tag.) {% endcomment %}

View File

@ -1,4 +0,0 @@
<div class="progress_bar progress">
<div class="progress_bar_fill bar progress-warning" data-width="{% widthratio current_val max_val 100 %}" style="width: {% widthratio current_val max_val 100 %}%"></div>
<div class="progress_bar_selected bar progress-success"></div>
</div>

View File

@ -2,20 +2,29 @@
<div class="quota-dynamic"> <div class="quota-dynamic">
<h3>{% trans "Quota Summary" %}</h3> <h3>{% trans "Quota Summary" %}</h3>
<strong>{% trans "Used" %}<span> {{ usage.quotas.instances.used|intcomma }} </span> {% trans "of" %} <span> {{ usage.quotas.instances.quota|intcomma }} </span>{% trans "Available Instances" %} </strong> <div class="d3_quota_bar">
{% horizon_progress_bar usage.quotas.instances.used usage.quotas.instances.quota %} <div class="d3_pie_chart" data-used="{% widthratio usage.quotas.instances.used usage.quotas.instances.quota 100 %}"></div>
<strong>{% trans "Available Instances" %} <br /> {% trans "Used" %}<span> {{ usage.quotas.instances.used|intcomma }} </span> {% trans "of" %} <span> {{ usage.quotas.instances.quota|intcomma }} </span></strong>
</div>
<strong>{% trans "Used" %} <span> {{ usage.quotas.cores.used|intcomma }} </span>{% trans "of" %}<span> {{ usage.quotas.cores.quota|intcomma }} </span>{% trans "Available vCPUs" %} </strong> <div class="d3_quota_bar">
{% horizon_progress_bar usage.quotas.cores.used usage.quotas.cores.quota %} <div class="d3_pie_chart" data-used="{% widthratio usage.quotas.cores.used usage.quotas.cores.quota 100 %}"></div>
<strong>{% trans "Available VCPUs" %} <br /> {% trans "Used" %} <span> {{ usage.quotas.cores.used|intcomma }} </span>{% trans "of" %}<span> {{ usage.quotas.cores.quota|intcomma }} </span></strong>
</div>
<strong>{% trans "Used" %} <span> {{ usage.quotas.ram.used|intcomma }} MB </span>{% trans "of" %}<span> {{ usage.quotas.ram.quota|intcomma }} MB </span>{% trans "Available RAM" %} </strong> <div class="d3_quota_bar">
{% horizon_progress_bar usage.quotas.ram.used usage.quotas.ram.quota %} <div class="d3_pie_chart" data-used="{% widthratio usage.quotas.ram.used usage.quotas.ram.quota 100 %}"></div>
<strong>{% trans "Available RAM" %} <br /> {% trans "Used" %} <span> {{ usage.quotas.ram.used|intcomma }} MB </span>{% trans "of" %}<span> {{ usage.quotas.ram.quota|intcomma }} MB </span></strong>
</div>
{% if usage.quotas.volumes %} {% if usage.quotas.volumes %}
<strong>{% trans "Used" %} <span> {{ usage.quotas.volumes.used|intcomma }} </span>{% trans "of" %}<span> {{ usage.quotas.volumes.quota|intcomma }} </span>{% trans "Available volumes" %} </strong> <div class="d3_quota_bar">
{% horizon_progress_bar usage.quotas.volumes.used usage.quotas.volumes.quota %} <div class="d3_pie_chart" data-used="{% widthratio usage.quotas.volumes.used usage.quotas.volumes.quota 100 %}"></div>
<strong>{% trans "Available Volumes" %} <br /> {% trans "Used" %} <span> {{ usage.quotas.volumes.used|intcomma }} </span>{% trans "of" %}<span> {{ usage.quotas.volumes.quota|intcomma }} </span></strong>
<strong>{% trans "Used" %} <span> {{ usage.quotas.gigabytes.used|intcomma }} GB </span>{% trans "of" %}<span> {{ usage.quotas.gigabytes.quota|intcomma }} GB </span>{% trans "Available volume storage" %} </strong> </div>
{% horizon_progress_bar usage.quotas.gigabytes.used usage.quotas.gigabytes.quota %} <div class="d3_quota_bar">
<div class="d3_pie_chart" data-used="{% widthratio usage.quotas.gigabytes.used usage.quotas.gigabytes.quota 100 %}"></div>
<strong>{% trans "Available Volume Storage" %} <br /> {% trans "Used" %} <span> {{ usage.quotas.gigabytes.used|intcomma }} GB </span>{% trans "of" %}<span> {{ usage.quotas.gigabytes.quota|intcomma }} GB </span></strong>
</div>
{% endif %} {% endif %}
</div> </div>

View File

@ -85,24 +85,6 @@ def horizon_dashboard_nav(context):
'request': context['request']} 'request': context['request']}
@register.inclusion_tag('horizon/common/_progress_bar.html')
def horizon_progress_bar(current_val, max_val):
""" Renders a progress bar based on parameters passed to the tag. The first
parameter is the current value and the second is the max value.
Example: ``{% progress_bar 25 50 %}``
This will generate a half-full progress bar.
The rendered progress bar will fill the area of its container. To constrain
the rendered size of the bar provide a container with appropriate width and
height styles.
"""
return {'current_val': current_val,
'max_val': max_val}
@register.filter @register.filter
def quota(val, units=None): def quota(val, units=None):
if val == float("inf"): if val == float("inf"):

View File

@ -14,7 +14,7 @@
{% include "horizon/common/_form_fields.html" %} {% include "horizon/common/_form_fields.html" %}
</fieldset> </fieldset>
</div> </div>
<div class="right"> <div class="right quota-dynamic">
<h3>{% trans "Description:" %}</h3> <h3>{% trans "Description:" %}</h3>
<p>{% trans "Allocate a floating IP from a given floating ip pool." %}</p> <p>{% trans "Allocate a floating IP from a given floating ip pool." %}</p>
@ -24,8 +24,18 @@
<p>{{ usages.floating_ips.available|quota }}</p> <p>{{ usages.floating_ips.available|quota }}</p>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
<div class="quota_bar">{% horizon_progress_bar usages.floating_ips.used usages.floating_ips.quota %}</div> <div id="floating_ip_progress" class="quota_bar" data-quota-used="{{ usages.floating_ips.used }}" data-quota-limit="{{ usages.floating_ips.quota }}" data-progress-indicator-step-by="1"></div>
</div> </div>
<script type="text/javascript" charset="utf-8">
if(typeof horizon.Quota !== 'undefined') {
horizon.Quota.init();
} else {
addHorizonLoadEvent(function() {
horizon.Quota.init();
});
}
</script>
{% endblock %} {% endblock %}
{% block modal-footer %} {% block modal-footer %}

View File

@ -22,7 +22,6 @@
<p>{{ usages.instances.available|quota|intcomma }}</p> <p>{{ usages.instances.available|quota|intcomma }}</p>
</div> </div>
<div id="quota_instances" class="quota_bar" data-progress-indicator-flavor data-quota-limit="{{ usages.instances.quota }}" data-quota-used="{{ usages.instances.used }}"> <div id="quota_instances" class="quota_bar" data-progress-indicator-flavor data-quota-limit="{{ usages.instances.quota }}" data-quota-used="{{ usages.instances.used }}">
{% horizon_progress_bar usages.instances.used usages.instances.quota %}
</div> </div>
<div class="quota_title clearfix"> <div class="quota_title clearfix">
@ -30,7 +29,6 @@
<p>{{ usages.cores.available|quota|intcomma }}</p> <p>{{ usages.cores.available|quota|intcomma }}</p>
</div> </div>
<div id="quota_vcpus" class="quota_bar" data-progress-indicator-flavor data-quota-limit="{{ usages.cores.quota }}" data-quota-used="{{ usages.cores.used }}"> <div id="quota_vcpus" class="quota_bar" data-progress-indicator-flavor data-quota-limit="{{ usages.cores.quota }}" data-quota-used="{{ usages.cores.used }}">
{% horizon_progress_bar usages.cores.used usages.cores.quota %}
</div> </div>
<div class="quota_title clearfix"> <div class="quota_title clearfix">
@ -38,7 +36,6 @@
<p>{{ usages.ram.available|quota:"MB"|intcomma }}</p> <p>{{ usages.ram.available|quota:"MB"|intcomma }}</p>
</div> </div>
<div id="quota_ram" data-progress-indicator-flavor data-quota-limit="{{ usages.ram.quota }}" data-quota-used="{{ usages.ram.used }}" class="quota_bar"> <div id="quota_ram" data-progress-indicator-flavor data-quota-limit="{{ usages.ram.quota }}" data-quota-used="{{ usages.ram.used }}" class="quota_bar">
{% horizon_progress_bar usages.ram.used usages.ram.quota %}
</div> </div>
</div> </div>

View File

@ -28,7 +28,6 @@
</div> </div>
<div id="quota_size" data-progress-indicator-for="id_size" data-quota-limit="{{ usages.gigabytes.quota }}" data-quota-used="{{ usages.gigabytes.used }}" class="quota_bar"> <div id="quota_size" data-progress-indicator-for="id_size" data-quota-limit="{{ usages.gigabytes.quota }}" data-quota-used="{{ usages.gigabytes.used }}" class="quota_bar">
{% horizon_progress_bar usages.gigabytes.used usages.gigabytes.quota %}
</div> </div>
<div class="quota_title clearfix"> <div class="quota_title clearfix">
@ -37,7 +36,6 @@
</div> </div>
<div id="quota_volumes" data-progress-indicator-step-by="1" data-quota-limit="{{ usages.volumes.quota }}" data-quota-used="{{ usages.volumes.used }}" class="quota_bar"> <div id="quota_volumes" data-progress-indicator-step-by="1" data-quota-limit="{{ usages.volumes.quota }}" data-quota-used="{{ usages.volumes.used }}" class="quota_bar">
{% horizon_progress_bar usages.volumes.used usages.volumes.quota %}
</div> </div>
</div> </div>

View File

@ -1196,6 +1196,19 @@ iframe {
background-color: red; background-color: red;
} }
.d3_quota_bar {
width: 20%;
margin-bottom: 8px;
margin-top: 10px;
float: left;
text-align: center;
}
.quota-dynamic {
overflow: hidden;
margin-bottom: 8px;
}
.quota_title { .quota_title {
color: #999; color: #999;
padding-bottom: 0; padding-bottom: 0;