Merge branch 'charts2'

* charts2:
  Loosening stats rate limiter. Ready to launch.
  Patching charts to fix broken charts branch.
This commit is contained in:
Samuel Clay 2016-01-05 11:03:50 -08:00
commit 02909962d4
9 changed files with 781 additions and 73 deletions

1
.gitignore vendored
View file

@ -63,4 +63,3 @@ media/safari/NewsBlur.safariextz
# IDE files
clients/android/NewsBlur/.idea
.tm_properties

View file

@ -866,28 +866,28 @@ class Feed(models.Model):
map_f = """
function() {
var date = (this.story_date.getFullYear()) + "-" + (this.story_date.getMonth()+1);
emit(date, 1);
var hour = this.story_date.getHours();
var day = this.story_date.getDay();
emit(this.story_hash, {'month': date, 'hour': hour, 'day': day});
}
"""
reduce_f = """
function(key, values) {
var total = 0;
for (var i=0; i < values.length; i++) {
total += values[i];
}
return total;
return values;
}
"""
dates = {}
res = MStory.objects(story_feed_id=self.pk).map_reduce(map_f, reduce_f, output='inline')
for r in res:
dates[r.key] = r.value
year_found = re.findall(r"(\d{4})-\d{1,2}", r.key)
if year_found and len(year_found):
year = int(year_found[0])
if year < min_year and year > 2000:
min_year = year
dates = defaultdict(int)
hours = defaultdict(int)
days = defaultdict(int)
results = MStory.objects(story_feed_id=self.pk).map_reduce(map_f, reduce_f, output='inline')
for result in results:
dates[result.value['month']] += 1
hours[int(result.value['hour'])] += 1
days[int(result.value['day'])] += 1
year = int(re.findall(r"(\d{4})-\d{1,2}", result.value['month'])[0])
if year < min_year and year > 2000:
min_year = year
# Add on to existing months, always amending up, never down. (Current month
# is guaranteed to be accurate, since trim_feeds won't delete it until after
# a month. Hacker News can have 1,000+ and still be counted.)
@ -912,7 +912,7 @@ class Feed(models.Model):
total += dates.get(key, 0)
month_count += 1
original_story_count_history = self.data.story_count_history
self.data.story_count_history = json.encode(months)
self.data.story_count_history = json.encode({'months': months, 'hours': hours, 'days': days})
if self.data.story_count_history != original_story_count_history:
self.data.save(update_fields=['story_count_history'])

View file

@ -150,7 +150,7 @@ def feed_autocomplete(request):
else:
return feeds
@ratelimit(minutes=1, requests=10)
@ratelimit(minutes=1, requests=30)
@json.json_view
def load_feed_statistics(request, feed_id):
user = get_user(request)
@ -193,7 +193,13 @@ def load_feed_statistics(request, feed_id):
# Stories per month - average and month-by-month breakout
average_stories_per_month, story_count_history = feed.average_stories_per_month, feed.data.story_count_history
stats['average_stories_per_month'] = average_stories_per_month
stats['story_count_history'] = story_count_history and json.decode(story_count_history)
story_count_history = story_count_history and json.decode(story_count_history)
if story_count_history and isinstance(story_count_history, dict):
stats['story_count_history'] = story_count_history['months']
stats['story_days_history'] = story_count_history['days']
stats['story_hours_history'] = story_count_history['hours']
else:
stats['story_count_history'] = story_count_history
# Subscribers
stats['subscriber_count'] = feed.num_subscribers

View file

@ -133,6 +133,8 @@ class MSocialProfile(mongo.Document):
stories_last_month = mongo.IntField(default=0)
average_stories_per_month = mongo.IntField(default=0)
story_count_history = mongo.ListField()
story_days_history = mongo.DictField()
story_hours_history = mongo.DictField()
feed_classifier_counts = mongo.DictField()
favicon_color = mongo.StringField(max_length=6)
protected = mongo.BooleanField()
@ -690,23 +692,25 @@ class MSocialProfile(mongo.Document):
map_f = """
function() {
var date = (this.shared_date.getFullYear()) + "-" + (this.shared_date.getMonth()+1);
emit(date, 1);
var hour = this.shared_date.getHours();
var day = this.shared_date.getDay();
emit(this.story_hash, {'month': date, 'hour': hour, 'day': day});
}
"""
reduce_f = """
function(key, values) {
var total = 0;
for (var i=0; i < values.length; i++) {
total += values[i];
}
return total;
return values;
}
"""
dates = {}
res = MSharedStory.objects(user_id=self.user_id).map_reduce(map_f, reduce_f, output='inline')
for r in res:
dates[r.key] = r.value
year = int(re.findall(r"(\d{4})-\d{1,2}", r.key)[0])
dates = defaultdict(int)
hours = defaultdict(int)
days = defaultdict(int)
results = MSharedStory.objects(user_id=self.user_id).map_reduce(map_f, reduce_f, output='inline')
for result in results:
dates[result.value['month']] += 1
hours[str(int(result.value['hour']))] += 1
days[str(int(result.value['day']))] += 1
year = int(re.findall(r"(\d{4})-\d{1,2}", result.value['month'])[0])
if year < min_year:
min_year = year
@ -725,6 +729,8 @@ class MSocialProfile(mongo.Document):
month_count += 1
self.story_count_history = months
self.story_days_history = days
self.story_hours_history = hours
self.average_stories_per_month = total / max(1, month_count)
self.save()

View file

@ -1358,6 +1358,8 @@ def load_social_statistics(request, social_user_id, username=None):
# Stories per month - average and month-by-month breakout
stats['average_stories_per_month'] = social_profile.average_stories_per_month
stats['story_count_history'] = social_profile.story_count_history
stats['story_hours_history'] = social_profile.story_hours_history
stats['story_days_history'] = social_profile.story_days_history
# Subscribers
stats['subscriber_count'] = social_profile.follower_count

View file

@ -7512,8 +7512,8 @@ form.opml_import_form input {
overflow: hidden;
}
.NB-modal-statistics .NB-statistics-stat .NB-statistics-history-chart {
margin: 0px 24px;
.NB-modal-statistics .NB-statistics-stat .NB-statistics-history-count-chart {
margin: 12px 24px 18px;
width: 524px;
height: 180px;
-webkit-box-sizing: border-box;
@ -7521,6 +7521,35 @@ form.opml_import_form input {
box-sizing: border-box;
}
.NB-modal-statistics .NB-statistics-stat .NB-statistics-history-hours-chart {
margin: 12px 24px 18px;
width: 524px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.NB-modal-statistics .NB-statistics-stat .NB-statistics-history-days-chart {
margin: -32px 24px;
width: 524px;
height: 400px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.NB-modal-statistics .NB-statistics-stat .NB-statistics-history-hours-chart {
text-align: left;
color: #808080;
font-size: 10px;
}
.NB-modal-statistics .NB-statistics-history-chart-hours-row {
background-color: #97BBCD;
height: 50px;
}
.NB-modal-statistics .NB-statistics-history-chart-hours-row td {
width: 18px;
}
.NB-modal-statistics .NB-modal-loading {
margin: 6px 8px 0;
}

View file

@ -531,6 +531,21 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, {
});
},
animate_saved: function() {
var $status = $('.NB-exception-option-view .NB-exception-option-status', this.$modal);
$status.text('Saved').animate({
'opacity': 1
}, {
'queue': false,
'duration': 600,
'complete': function() {
_.delay(function() {
$status.animate({'opacity': 0}, {'queue': false, 'duration': 1000});
}, 300);
}
});
},
handle_change: function(elem, e) {
var self = this;

View file

@ -85,7 +85,9 @@ _.extend(NEWSBLUR.ReaderStatistics.prototype, {
$expires.html("");
}
setTimeout(function() {
self.make_charts(data);
self.make_chart_count(data);
self.make_chart_hours(data);
self.make_chart_days(data);
}, this.first_load ? 200 : 50);
setTimeout(function() {
@ -156,7 +158,19 @@ _.extend(NEWSBLUR.ReaderStatistics.prototype, {
$.make('div', { className: 'NB-statistics-history-stat' }, [
$.make('div', { className: 'NB-statistics-label' }, 'Stories per month')
]),
$.make('canvas', { id: 'NB-statistics-history-chart', className: 'NB-statistics-history-chart' })
$.make('canvas', { id: 'NB-statistics-history-count-chart', className: 'NB-statistics-history-count-chart' })
]),
$.make('div', { className: 'NB-statistics-stat NB-statistics-history'}, [
$.make('div', { className: 'NB-statistics-history-stat' }, [
$.make('div', { className: 'NB-statistics-label' }, 'Stories per day')
]),
$.make('canvas', { id: 'NB-statistics-history-days-chart', className: 'NB-statistics-history-days-chart' })
]),
$.make('div', { className: 'NB-statistics-stat NB-statistics-history'}, [
$.make('div', { className: 'NB-statistics-history-stat' }, [
$.make('div', { className: 'NB-statistics-label' }, 'Daily distribution of stories')
]),
$.make('div', { className: 'NB-statistics-history-hours-chart' })
]),
(data.classifier_counts && $.make('div', { className: 'NB-statistics-state NB-statistics-classifiers' }, [
this.make_classifier_count('tag', data.classifier_counts['tag']),
@ -263,7 +277,7 @@ _.extend(NEWSBLUR.ReaderStatistics.prototype, {
return $history;
},
make_charts: function(data) {
make_chart_count: function(data) {
var labels = _.map(data['story_count_history'], function(date) {
var date_matched = date[0].match(/(\d{4})-(\d{1,2})/);
var date = (new Date(parseInt(date_matched[1], 10), parseInt(date_matched[2],10)-1));
@ -291,7 +305,7 @@ _.extend(NEWSBLUR.ReaderStatistics.prototype, {
}
]
};
var $plot = $(".NB-statistics-history-chart");
var $plot = $(".NB-statistics-history-count-chart");
var width = $plot.width();
var height = $plot.height();
$plot.attr('width', width);
@ -301,6 +315,59 @@ _.extend(NEWSBLUR.ReaderStatistics.prototype, {
});
},
make_chart_hours: function(data) {
var max_count = _.max(data.story_hours_history);
var $chart = $.make('table', [
$.make('tr', { className: 'NB-statistics-history-chart-hours-row' }, [
_.map(_.range(24), function(hour) {
var count = data.story_hours_history[hour] || 0;
var opacity = 1 - (count * 1.0 / max_count);
return $.make('td', { style: "background-color: rgba(255, 255, 255, " + opacity + ");" });
})
]),
$.make('tr', { className: 'NB-statistics-history-chart-hours-text-row' }, [
_.compact(_.map(_.range(24), function(hour, count) {
var am = hour < 12;
if (hour == 0) hour = 12;
var hour_name = am ? (hour + "am") : ((hour > 12 ? hour - 12 : hour) + "pm");
if (hour % 3 == 0) {
return $.make('td', { colSpan: 3 }, hour_name);
}
}))
])
]);
$(".NB-statistics-history-hours-chart", this.$modal).html($chart);
},
make_chart_days: function(data) {
var labels = NEWSBLUR.utils.dayNames;
var values = _.map(_.range(7), function(day) {
return data['story_days_history'][day] || 0;
});
var points = {
labels: labels,
datasets: [
{
fillColor : "rgba(151,187,205,0.5)",
strokeColor : "rgba(151,187,205,1)",
pointColor : "rgba(151,187,205,1)",
pointStrokeColor : "#fff",
data : values
}
]
};
var $plot = $(".NB-statistics-history-days-chart");
var width = $plot.width();
var height = $plot.height();
$plot.attr('width', width);
$plot.attr('height', height);
var myLine = new Chart($plot.get(0).getContext("2d")).Radar(points, {
scaleShowLabelBackdrop: false
});
},
close_and_load_premium: function() {
this.close(function() {
NEWSBLUR.reader.open_feedchooser_modal();

View file

@ -1,5 +1,14 @@
/*!
* Chart.js
* http://chartjs.org/
*
* Copyright 2013 Nick Downie
* Released under the MIT license
* https://github.com/nnnick/Chart.js/blob/master/LICENSE.md
*/
//Define the global Chart Variable as a class.
var Chart = function(context){
window.Chart = function(context){
var chart = this;
@ -152,6 +161,127 @@ var Chart = function(context){
context.scale(window.devicePixelRatio, window.devicePixelRatio);
}
this.PolarArea = function(data,options){
chart.PolarArea.defaults = {
scaleOverlay : true,
scaleOverride : false,
scaleSteps : null,
scaleStepWidth : null,
scaleStartValue : null,
scaleShowLine : true,
scaleLineColor : "rgba(0,0,0,.1)",
scaleLineWidth : 1,
scaleShowLabels : true,
scaleLabel : "<%=value%>",
scaleFontFamily : "'Arial'",
scaleFontSize : 12,
scaleFontStyle : "normal",
scaleFontColor : "#666",
scaleShowLabelBackdrop : true,
scaleBackdropColor : "rgba(255,255,255,0.75)",
scaleBackdropPaddingY : 2,
scaleBackdropPaddingX : 2,
segmentShowStroke : true,
segmentStrokeColor : "#fff",
segmentStrokeWidth : 2,
animation : true,
animationSteps : 100,
animationEasing : "easeOutBounce",
animateRotate : true,
animateScale : false,
onAnimationComplete : null
};
var config = (options)? mergeChartConfig(chart.PolarArea.defaults,options) : chart.PolarArea.defaults;
return new PolarArea(data,config,context);
};
this.Radar = function(data,options){
chart.Radar.defaults = {
scaleOverlay : false,
scaleOverride : false,
scaleSteps : null,
scaleStepWidth : null,
scaleStartValue : null,
scaleShowLine : true,
scaleLineColor : "rgba(0,0,0,.1)",
scaleLineWidth : 1,
scaleShowLabels : false,
scaleLabel : "<%=value%>",
scaleFontFamily : "'Arial'",
scaleFontSize : 12,
scaleFontStyle : "normal",
scaleFontColor : "#666",
scaleShowLabelBackdrop : true,
scaleBackdropColor : "rgba(255,255,255,0.75)",
scaleBackdropPaddingY : 2,
scaleBackdropPaddingX : 2,
angleShowLineOut : true,
angleLineColor : "rgba(0,0,0,.1)",
angleLineWidth : 1,
pointLabelFontFamily : "'Arial'",
pointLabelFontStyle : "normal",
pointLabelFontSize : 12,
pointLabelFontColor : "#666",
pointDot : true,
pointDotRadius : 3,
pointDotStrokeWidth : 1,
datasetStroke : true,
datasetStrokeWidth : 2,
datasetFill : true,
animation : true,
animationSteps : 60,
animationEasing : "easeOutQuart",
onAnimationComplete : null
};
var config = (options)? mergeChartConfig(chart.Radar.defaults,options) : chart.Radar.defaults;
return new Radar(data,config,context);
};
this.Pie = function(data,options){
chart.Pie.defaults = {
segmentShowStroke : true,
segmentStrokeColor : "#fff",
segmentStrokeWidth : 2,
animation : true,
animationSteps : 100,
animationEasing : "easeOutBounce",
animateRotate : true,
animateScale : false,
onAnimationComplete : null
};
var config = (options)? mergeChartConfig(chart.Pie.defaults,options) : chart.Pie.defaults;
return new Pie(data,config,context);
};
this.Doughnut = function(data,options){
chart.Doughnut.defaults = {
segmentShowStroke : true,
segmentStrokeColor : "#fff",
segmentStrokeWidth : 2,
percentageInnerCutout : 50,
animation : true,
animationSteps : 100,
animationEasing : "easeOutBounce",
animateRotate : true,
animateScale : false,
onAnimationComplete : null
};
var config = (options)? mergeChartConfig(chart.Doughnut.defaults,options) : chart.Doughnut.defaults;
return new Doughnut(data,config,context);
};
this.Line = function(data,options){
chart.Line.defaults = {
@ -186,12 +316,476 @@ var Chart = function(context){
var config = (options) ? mergeChartConfig(chart.Line.defaults,options) : chart.Line.defaults;
return new Line(data,config,context);
};
}
this.Bar = function(data,options){
chart.Bar.defaults = {
scaleOverlay : false,
scaleOverride : false,
scaleSteps : null,
scaleStepWidth : null,
scaleStartValue : null,
scaleLineColor : "rgba(0,0,0,.1)",
scaleLineWidth : 1,
scaleShowLabels : true,
scaleLabel : "<%=value%>",
scaleFontFamily : "'Arial'",
scaleFontSize : 12,
scaleFontStyle : "normal",
scaleFontColor : "#666",
scaleShowGridLines : true,
scaleGridLineColor : "rgba(0,0,0,.05)",
scaleGridLineWidth : 1,
barShowStroke : true,
barStrokeWidth : 2,
barValueSpacing : 5,
barDatasetSpacing : 1,
animation : true,
animationSteps : 60,
animationEasing : "easeOutQuart",
onAnimationComplete : null
};
var config = (options) ? mergeChartConfig(chart.Bar.defaults,options) : chart.Bar.defaults;
return new Bar(data,config,context);
}
var clear = function(c){
c.clearRect(0, 0, width, height);
};
var PolarArea = function(data,config,ctx){
var maxSize, scaleHop, calculatedScale, labelHeight, scaleHeight, valueBounds, labelTemplateString;
calculateDrawingSizes();
valueBounds = getValueBounds();
labelTemplateString = (config.scaleShowLabels)? config.scaleLabel : null;
//Check and set the scale
if (!config.scaleOverride){
calculatedScale = calculateScale(scaleHeight,valueBounds.maxSteps,valueBounds.minSteps,valueBounds.maxValue,valueBounds.minValue,labelTemplateString);
}
else {
calculatedScale = {
steps : config.scaleSteps,
stepValue : config.scaleStepWidth,
graphMin : config.scaleStartValue,
labels : []
}
populateLabels(labelTemplateString, calculatedScale.labels,calculatedScale.steps,config.scaleStartValue,config.scaleStepWidth);
}
scaleHop = maxSize/(calculatedScale.steps);
//Wrap in an animation loop wrapper
animationLoop(config,drawScale,drawAllSegments,ctx);
function calculateDrawingSizes(){
maxSize = (Min([width,height])/2);
//Remove whatever is larger - the font size or line width.
maxSize -= Max([config.scaleFontSize*0.5,config.scaleLineWidth*0.5]);
labelHeight = config.scaleFontSize*2;
//If we're drawing the backdrop - add the Y padding to the label height and remove from drawing region.
if (config.scaleShowLabelBackdrop){
labelHeight += (2 * config.scaleBackdropPaddingY);
maxSize -= config.scaleBackdropPaddingY*1.5;
}
scaleHeight = maxSize;
//If the label height is less than 5, set it to 5 so we don't have lines on top of each other.
labelHeight = Default(labelHeight,5);
}
function drawScale(){
for (var i=0; i<calculatedScale.steps; i++){
//If the line object is there
if (config.scaleShowLine){
ctx.beginPath();
ctx.arc(width/2, height/2, scaleHop * (i + 1), 0, (Math.PI * 2), true);
ctx.strokeStyle = config.scaleLineColor;
ctx.lineWidth = config.scaleLineWidth;
ctx.stroke();
}
if (config.scaleShowLabels){
ctx.textAlign = "center";
ctx.font = config.scaleFontStyle + " " + config.scaleFontSize + "px " + config.scaleFontFamily;
var label = calculatedScale.labels[i];
//If the backdrop object is within the font object
if (config.scaleShowLabelBackdrop){
var textWidth = ctx.measureText(label).width;
ctx.fillStyle = config.scaleBackdropColor;
ctx.beginPath();
ctx.rect(
Math.round(width/2 - textWidth/2 - config.scaleBackdropPaddingX), //X
Math.round(height/2 - (scaleHop * (i + 1)) - config.scaleFontSize*0.5 - config.scaleBackdropPaddingY),//Y
Math.round(textWidth + (config.scaleBackdropPaddingX*2)), //Width
Math.round(config.scaleFontSize + (config.scaleBackdropPaddingY*2)) //Height
);
ctx.fill();
}
ctx.textBaseline = "middle";
ctx.fillStyle = config.scaleFontColor;
ctx.fillText(label,width/2,height/2 - (scaleHop * (i + 1)));
}
}
}
function drawAllSegments(animationDecimal){
var startAngle = -Math.PI/2,
angleStep = (Math.PI*2)/data.length,
scaleAnimation = 1,
rotateAnimation = 1;
if (config.animation) {
if (config.animateScale) {
scaleAnimation = animationDecimal;
}
if (config.animateRotate){
rotateAnimation = animationDecimal;
}
}
for (var i=0; i<data.length; i++){
ctx.beginPath();
ctx.arc(width/2,height/2,scaleAnimation * calculateOffset(data[i].value,calculatedScale,scaleHop),startAngle, startAngle + rotateAnimation*angleStep, false);
ctx.lineTo(width/2,height/2);
ctx.closePath();
ctx.fillStyle = data[i].color;
ctx.fill();
if(config.segmentShowStroke){
ctx.strokeStyle = config.segmentStrokeColor;
ctx.lineWidth = config.segmentStrokeWidth;
ctx.stroke();
}
startAngle += rotateAnimation*angleStep;
}
}
function getValueBounds() {
var upperValue = Number.MIN_VALUE;
var lowerValue = Number.MAX_VALUE;
for (var i=0; i<data.length; i++){
if (data[i].value > upperValue) {upperValue = data[i].value;}
if (data[i].value < lowerValue) {lowerValue = data[i].value;}
};
var maxSteps = Math.floor((scaleHeight / (labelHeight*0.66)));
var minSteps = Math.floor((scaleHeight / labelHeight*0.5));
return {
maxValue : upperValue,
minValue : lowerValue,
maxSteps : maxSteps,
minSteps : minSteps
};
}
}
var Radar = function (data,config,ctx) {
var maxSize, scaleHop, calculatedScale, labelHeight, scaleHeight, valueBounds, labelTemplateString;
//If no labels are defined set to an empty array, so referencing length for looping doesn't blow up.
if (!data.labels) data.labels = [];
calculateDrawingSizes();
var valueBounds = getValueBounds();
labelTemplateString = (config.scaleShowLabels)? config.scaleLabel : null;
//Check and set the scale
if (!config.scaleOverride){
calculatedScale = calculateScale(scaleHeight,valueBounds.maxSteps,valueBounds.minSteps,valueBounds.maxValue,valueBounds.minValue,labelTemplateString);
}
else {
calculatedScale = {
steps : config.scaleSteps,
stepValue : config.scaleStepWidth,
graphMin : config.scaleStartValue,
labels : []
}
populateLabels(labelTemplateString, calculatedScale.labels,calculatedScale.steps,config.scaleStartValue,config.scaleStepWidth);
}
scaleHop = maxSize/(calculatedScale.steps);
animationLoop(config,drawScale,drawAllDataPoints,ctx);
//Radar specific functions.
function drawAllDataPoints(animationDecimal){
var rotationDegree = (2*Math.PI)/data.datasets[0].data.length;
ctx.save();
//translate to the centre of the canvas.
ctx.translate(width/2,height/2);
//We accept multiple data sets for radar charts, so show loop through each set
for (var i=0; i<data.datasets.length; i++){
ctx.beginPath();
ctx.moveTo(0,animationDecimal*(-1*calculateOffset(data.datasets[i].data[0],calculatedScale,scaleHop)));
for (var j=1; j<data.datasets[i].data.length; j++){
ctx.rotate(rotationDegree);
ctx.lineTo(0,animationDecimal*(-1*calculateOffset(data.datasets[i].data[j],calculatedScale,scaleHop)));
}
ctx.closePath();
ctx.fillStyle = data.datasets[i].fillColor;
ctx.strokeStyle = data.datasets[i].strokeColor;
ctx.lineWidth = config.datasetStrokeWidth;
ctx.fill();
ctx.stroke();
if (config.pointDot){
ctx.fillStyle = data.datasets[i].pointColor;
ctx.strokeStyle = data.datasets[i].pointStrokeColor;
ctx.lineWidth = config.pointDotStrokeWidth;
for (var k=0; k<data.datasets[i].data.length; k++){
ctx.rotate(rotationDegree);
ctx.beginPath();
ctx.arc(0,animationDecimal*(-1*calculateOffset(data.datasets[i].data[k],calculatedScale,scaleHop)),config.pointDotRadius,2*Math.PI,false);
ctx.fill();
ctx.stroke();
}
}
ctx.rotate(rotationDegree);
}
ctx.restore();
}
function drawScale(){
var rotationDegree = (2*Math.PI)/data.datasets[0].data.length;
ctx.save();
ctx.translate(width / 2, height / 2);
if (config.angleShowLineOut){
ctx.strokeStyle = config.angleLineColor;
ctx.lineWidth = config.angleLineWidth;
for (var h=0; h<data.datasets[0].data.length; h++){
ctx.rotate(rotationDegree);
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(0,-maxSize);
ctx.stroke();
}
}
for (var i=0; i<calculatedScale.steps; i++){
ctx.beginPath();
if(config.scaleShowLine){
ctx.strokeStyle = config.scaleLineColor;
ctx.lineWidth = config.scaleLineWidth;
ctx.moveTo(0,-scaleHop * (i+1));
for (var j=0; j<data.datasets[0].data.length; j++){
ctx.rotate(rotationDegree);
ctx.lineTo(0,-scaleHop * (i+1));
}
ctx.closePath();
ctx.stroke();
}
if (config.scaleShowLabels){
ctx.textAlign = 'center';
ctx.font = config.scaleFontStyle + " " + config.scaleFontSize+"px " + config.scaleFontFamily;
ctx.textBaseline = "middle";
if (config.scaleShowLabelBackdrop){
var textWidth = ctx.measureText(calculatedScale.labels[i]).width;
ctx.fillStyle = config.scaleBackdropColor;
ctx.beginPath();
ctx.rect(
Math.round(- textWidth/2 - config.scaleBackdropPaddingX), //X
Math.round((-scaleHop * (i + 1)) - config.scaleFontSize*0.5 - config.scaleBackdropPaddingY),//Y
Math.round(textWidth + (config.scaleBackdropPaddingX*2)), //Width
Math.round(config.scaleFontSize + (config.scaleBackdropPaddingY*2)) //Height
);
ctx.fill();
}
ctx.fillStyle = config.scaleFontColor;
ctx.fillText(calculatedScale.labels[i],0,-scaleHop*(i+1));
}
}
for (var k=0; k<data.labels.length; k++){
ctx.font = config.pointLabelFontStyle + " " + config.pointLabelFontSize+"px " + config.pointLabelFontFamily;
ctx.fillStyle = config.pointLabelFontColor;
var opposite = Math.sin(rotationDegree*k) * (maxSize + config.pointLabelFontSize);
var adjacent = Math.cos(rotationDegree*k) * (maxSize + config.pointLabelFontSize);
if(rotationDegree*k == Math.PI || rotationDegree*k == 0){
ctx.textAlign = "center";
}
else if(rotationDegree*k > Math.PI){
ctx.textAlign = "right";
}
else{
ctx.textAlign = "left";
}
ctx.textBaseline = "middle";
ctx.fillText(data.labels[k],opposite,-adjacent);
}
ctx.restore();
};
function calculateDrawingSizes(){
maxSize = (Min([width,height])/2);
labelHeight = config.scaleFontSize*2;
var labelLength = 0;
for (var i=0; i<data.labels.length; i++){
ctx.font = config.pointLabelFontStyle + " " + config.pointLabelFontSize+"px " + config.pointLabelFontFamily;
var textMeasurement = ctx.measureText(data.labels[i]).width;
if(textMeasurement>labelLength) labelLength = textMeasurement;
}
//Figure out whats the largest - the height of the text or the width of what's there, and minus it from the maximum usable size.
maxSize -= Max([labelLength,((config.pointLabelFontSize/2)*1.5)]);
maxSize -= config.pointLabelFontSize;
maxSize = CapValue(maxSize, null, 0);
scaleHeight = maxSize;
//If the label height is less than 5, set it to 5 so we don't have lines on top of each other.
labelHeight = Default(labelHeight,5);
};
function getValueBounds() {
var upperValue = Number.MIN_VALUE;
var lowerValue = Number.MAX_VALUE;
for (var i=0; i<data.datasets.length; i++){
for (var j=0; j<data.datasets[i].data.length; j++){
if (data.datasets[i].data[j] > upperValue){upperValue = data.datasets[i].data[j]}
if (data.datasets[i].data[j] < lowerValue){lowerValue = data.datasets[i].data[j]}
}
}
var maxSteps = Math.floor((scaleHeight / (labelHeight*0.66)));
var minSteps = Math.floor((scaleHeight / labelHeight*0.5));
return {
maxValue : upperValue,
minValue : lowerValue,
maxSteps : maxSteps,
minSteps : minSteps
};
}
}
var Pie = function(data,config,ctx){
var segmentTotal = 0;
//In case we have a canvas that is not a square. Minus 5 pixels as padding round the edge.
var pieRadius = Min([height/2,width/2]) - 5;
for (var i=0; i<data.length; i++){
segmentTotal += data[i].value;
}
animationLoop(config,null,drawPieSegments,ctx);
function drawPieSegments (animationDecimal){
var cumulativeAngle = -Math.PI/2,
scaleAnimation = 1,
rotateAnimation = 1;
if (config.animation) {
if (config.animateScale) {
scaleAnimation = animationDecimal;
}
if (config.animateRotate){
rotateAnimation = animationDecimal;
}
}
for (var i=0; i<data.length; i++){
var segmentAngle = rotateAnimation * ((data[i].value/segmentTotal) * (Math.PI*2));
ctx.beginPath();
ctx.arc(width/2,height/2,scaleAnimation * pieRadius,cumulativeAngle,cumulativeAngle + segmentAngle);
ctx.lineTo(width/2,height/2);
ctx.closePath();
ctx.fillStyle = data[i].color;
ctx.fill();
if(config.segmentShowStroke){
ctx.lineWidth = config.segmentStrokeWidth;
ctx.strokeStyle = config.segmentStrokeColor;
ctx.stroke();
}
cumulativeAngle += segmentAngle;
}
}
}
var Doughnut = function(data,config,ctx){
var segmentTotal = 0;
//In case we have a canvas that is not a square. Minus 5 pixels as padding round the edge.
var doughnutRadius = Min([height/2,width/2]) - 5;
var cutoutRadius = doughnutRadius * (config.percentageInnerCutout/100);
for (var i=0; i<data.length; i++){
segmentTotal += data[i].value;
}
animationLoop(config,null,drawPieSegments,ctx);
function drawPieSegments (animationDecimal){
var cumulativeAngle = -Math.PI/2,
scaleAnimation = 1,
rotateAnimation = 1;
if (config.animation) {
if (config.animateScale) {
scaleAnimation = animationDecimal;
}
if (config.animateRotate){
rotateAnimation = animationDecimal;
}
}
for (var i=0; i<data.length; i++){
var segmentAngle = rotateAnimation * ((data[i].value/segmentTotal) * (Math.PI*2));
ctx.beginPath();
ctx.arc(width/2,height/2,scaleAnimation * doughnutRadius,cumulativeAngle,cumulativeAngle + segmentAngle,false);
ctx.arc(width/2,height/2,scaleAnimation * cutoutRadius,cumulativeAngle + segmentAngle,cumulativeAngle,true);
ctx.closePath();
ctx.fillStyle = data[i].color;
ctx.fill();
if(config.segmentShowStroke){
ctx.lineWidth = config.segmentStrokeWidth;
ctx.strokeStyle = config.segmentStrokeColor;
ctx.stroke();
}
cumulativeAngle += segmentAngle;
}
}
}
var Line = function(data,config,ctx){
var maxSize, scaleHop, calculatedScale, labelHeight, scaleHeight, valueBounds, labelTemplateString, valueHop,widestXLabel, xAxisLength,yAxisPosX,xAxisPosY, rotateLabels = 0;
@ -210,12 +804,8 @@ var Chart = function(context){
stepValue : config.scaleStepWidth,
graphMin : config.scaleStartValue,
labels : []
};
for (var i=0; i<calculatedScale.steps; i++){
if(labelTemplateString){
calculatedScale.labels.push(tmpl(labelTemplateString,{value:(config.scaleStartValue + (config.scaleStepWidth * i)).toFixed(getDecimalPlaces (config.scaleStepWidth))}));
}
}
populateLabels(labelTemplateString, calculatedScale.labels,calculatedScale.steps,config.scaleStartValue,config.scaleStepWidth);
}
scaleHop = Math.floor(scaleHeight/calculatedScale.steps);
@ -227,7 +817,7 @@ var Chart = function(context){
ctx.strokeStyle = data.datasets[i].strokeColor;
ctx.lineWidth = config.datasetStrokeWidth;
ctx.beginPath();
ctx.moveTo(yAxisPosX, xAxisPosY - animPc*(calculateOffset(data.datasets[i].data[0],calculatedScale,scaleHop)));
ctx.moveTo(yAxisPosX, xAxisPosY - animPc*(calculateOffset(data.datasets[i].data[0],calculatedScale,scaleHop)))
for (var j=1; j<data.datasets[i].data.length; j++){
if (config.bezierCurve){
@ -407,8 +997,8 @@ var Chart = function(context){
var lowerValue = Number.MAX_VALUE;
for (var i=0; i<data.datasets.length; i++){
for (var j=0; j<data.datasets[i].data.length; j++){
if ( data.datasets[i].data[j] > upperValue) { upperValue = data.datasets[i].data[j]; };
if ( data.datasets[i].data[j] < lowerValue) { lowerValue = data.datasets[i].data[j]; };
if ( data.datasets[i].data[j] > upperValue) { upperValue = data.datasets[i].data[j] };
if ( data.datasets[i].data[j] < lowerValue) { lowerValue = data.datasets[i].data[j] };
}
};
@ -426,7 +1016,7 @@ var Chart = function(context){
}
};
}
var Bar = function(data,config,ctx){
var maxSize, scaleHop, calculatedScale, labelHeight, scaleHeight, valueBounds, labelTemplateString, valueHop,widestXLabel, xAxisLength,yAxisPosX,xAxisPosY,barWidth, rotateLabels = 0;
@ -446,12 +1036,8 @@ var Chart = function(context){
stepValue : config.scaleStepWidth,
graphMin : config.scaleStartValue,
labels : []
};
for (var i=0; i<calculatedScale.steps; i++){
if(labelTemplateString){
calculatedScale.labels.push(tmpl(labelTemplateString,{value:(config.scaleStartValue + (config.scaleStepWidth * i)).toFixed(getDecimalPlaces (config.scaleStepWidth))}));
}
}
populateLabels(labelTemplateString, calculatedScale.labels,calculatedScale.steps,config.scaleStartValue,config.scaleStepWidth);
}
scaleHop = Math.floor(scaleHeight/calculatedScale.steps);
@ -615,8 +1201,8 @@ var Chart = function(context){
var lowerValue = Number.MAX_VALUE;
for (var i=0; i<data.datasets.length; i++){
for (var j=0; j<data.datasets[i].data.length; j++){
if ( data.datasets[i].data[j] > upperValue) { upperValue = data.datasets[i].data[j]; };
if ( data.datasets[i].data[j] < lowerValue) { lowerValue = data.datasets[i].data[j]; };
if ( data.datasets[i].data[j] > upperValue) { upperValue = data.datasets[i].data[j] };
if ( data.datasets[i].data[j] < lowerValue) { lowerValue = data.datasets[i].data[j] };
}
};
@ -632,7 +1218,7 @@ var Chart = function(context){
}
};
}
function calculateOffset(val,calculatedScale,scaleHop){
var outerValue = calculatedScale.steps * calculatedScale.stepValue;
@ -722,19 +1308,9 @@ var Chart = function(context){
numberOfSteps = Math.round(graphRange/stepValue);
}
};
//Create an array of all the labels by interpolating the string.
var labels = [];
if(labelTemplateString){
//Fix floating point errors by setting to fixed the on the same decimal as the stepValue.
for (var i=1; i<numberOfSteps+1; i++){
labels.push(tmpl(labelTemplateString,{value:(graphMin + (stepValue*i)).toFixed(getDecimalPlaces (stepValue))}));
}
}
populateLabels(labelTemplateString, labels, numberOfSteps, graphMin, stepValue);
return {
steps : numberOfSteps,
@ -742,7 +1318,7 @@ var Chart = function(context){
graphMin : graphMin,
labels : labels
};
}
function calculateOrderOfMagnitude(val){
return Math.floor(Math.log(val) / Math.LN10);
@ -750,6 +1326,16 @@ var Chart = function(context){
}
//Populate an array of all the labels by interpolating the string.
function populateLabels(labelTemplateString, labels, numberOfSteps, graphMin, stepValue) {
if (labelTemplateString) {
//Fix floating point errors by setting to fixed the on the same decimal as the stepValue.
for (var i = 1; i < numberOfSteps + 1; i++) {
labels.push(tmpl(labelTemplateString, {value: (graphMin + (stepValue * i)).toFixed(getDecimalPlaces(stepValue))}));
}
}
}
//Max value from array
function Max( array ){
@ -788,7 +1374,7 @@ var Chart = function(context){
function getDecimalPlaces (num){
var numberOfDecimalPlaces;
if (num%1!=0){
return num.toString().split(".")[1].length;
return num.toString().split(".")[1].length
}
else{
return 0;
@ -809,7 +1395,7 @@ var Chart = function(context){
function tmpl(str, data){
// Figure out if we're getting a template, or if we need to
// load the template - and be sure to cache the result.
var fn = !(/\W/.test(str)) ?
var fn = !/\W/.test(str) ?
cache[str] = cache[str] ||
tmpl(document.getElementById(str).innerHTML) :
@ -835,7 +1421,5 @@ var Chart = function(context){
// Provide some basic currying to the user
return data ? fn( data ) : fn;
};
};
}