From 70c23a19d3ce4d7e944168e7115b12d2a9825875 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 9 Sep 2015 00:24:53 -0400 Subject: [PATCH 1/9] Initial commit to support scheduled statistics. --- mayan/apps/documents/apps.py | 11 ++---- mayan/apps/documents/statistics.py | 26 ++++++++----- mayan/apps/statistics/admin.py | 12 ++++++ mayan/apps/statistics/apps.py | 33 ++++++++++++++++ mayan/apps/statistics/classes.py | 39 ++++++++++++++----- mayan/apps/statistics/literals.py | 3 ++ .../statistics/migrations/0001_initial.py | 27 +++++++++++++ mayan/apps/statistics/migrations/__init__.py | 0 mayan/apps/statistics/models.py | 30 ++++++++++++++ mayan/apps/statistics/tasks.py | 20 ++++++++++ mayan/apps/statistics/urls.py | 2 +- 11 files changed, 176 insertions(+), 27 deletions(-) create mode 100644 mayan/apps/statistics/admin.py create mode 100644 mayan/apps/statistics/literals.py create mode 100644 mayan/apps/statistics/migrations/0001_initial.py create mode 100644 mayan/apps/statistics/migrations/__init__.py create mode 100644 mayan/apps/statistics/models.py create mode 100644 mayan/apps/statistics/tasks.py diff --git a/mayan/apps/documents/apps.py b/mayan/apps/documents/apps.py index 39e9dcbafb..623a557f2b 100644 --- a/mayan/apps/documents/apps.py +++ b/mayan/apps/documents/apps.py @@ -72,7 +72,7 @@ from .permissions import ( permission_document_view ) from .settings import setting_thumbnail_size -from .statistics import DocumentStatistics, DocumentUsageStatistics +from .statistics import document_page_count_per_month from .widgets import document_thumbnail @@ -351,12 +351,9 @@ class DocumentsApp(MayanAppConfig): namespace = StatisticNamespace(name='documents', label=_('Documents')) namespace.add_statistic( - DocumentStatistics( - name='document_stats', label=_('Document tendencies') - ) - ) - namespace.add_statistic(DocumentUsageStatistics( - name='document_usage', label=_('Document usage')) + slug='document-page-count-per-month', + label=_('Document page count per month'), + func=document_page_count_per_month ) post_initial_setup.connect( diff --git a/mayan/apps/documents/statistics.py b/mayan/apps/documents/statistics.py index 63f0bfba0e..4ca0a641ea 100644 --- a/mayan/apps/documents/statistics.py +++ b/mayan/apps/documents/statistics.py @@ -40,15 +40,23 @@ def storage_count(path='.'): return total_count, total_size -class DocumentStatistics(Statistic): - def get_results(self): - results = [] +def document_page_count_per_month(): + return ( + ( + {'August, 2015': 1244}, + {'September, 2015': 3123}, + ), + ) - results.extend( - [ - _('Document types: %d') % DocumentType.objects.count(), - ] - ) + + #def get_results(self): + # return ( + ## ( + # {_('Document types'): DocumentType.objects.count()}, + # ), + # ) + +""" document_stats = DocumentVersion.objects.annotate( page_count=Count('pages') ).aggregate(Min('page_count'), Max('page_count'), Avg('page_count')) @@ -67,7 +75,7 @@ class DocumentStatistics(Statistic): ) return results - +""" class DocumentUsageStatistics(Statistic): def get_results(self): diff --git a/mayan/apps/statistics/admin.py b/mayan/apps/statistics/admin.py new file mode 100644 index 0000000000..054a35ce00 --- /dev/null +++ b/mayan/apps/statistics/admin.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals + +from django.contrib import admin + +from .models import StatisticResult + + +@admin.register(StatisticResult) +class StatisticResultAdmin(admin.ModelAdmin): + list_display = ( + 'slug', 'datetime', 'serialize_data' + ) diff --git a/mayan/apps/statistics/apps.py b/mayan/apps/statistics/apps.py index ac4f37595d..5d051fac0e 100644 --- a/mayan/apps/statistics/apps.py +++ b/mayan/apps/statistics/apps.py @@ -1,7 +1,12 @@ from __future__ import unicode_literals +from datetime import timedelta + +from kombu import Exchange, Queue + from django.utils.translation import ugettext_lazy as _ +from mayan.celery import app from common import MayanAppConfig, menu_object, menu_secondary, menu_tools from .classes import Statistic, StatisticNamespace @@ -9,6 +14,8 @@ from .links import ( link_execute, link_namespace_details, link_namespace_list, link_statistics ) +from .literals import STATISTICS_REFRESH_INTERVAL +from .tasks import task_check_statistics # NOQA - Force registration of task class StatisticsApp(MayanAppConfig): @@ -18,6 +25,32 @@ class StatisticsApp(MayanAppConfig): def ready(self): super(StatisticsApp, self).ready() + app.conf.CELERYBEAT_SCHEDULE.update( + { + 'statistics.task_check_statistics': { + 'task': 'statistics.tasks.task_check_statistics', + 'schedule': timedelta(seconds=STATISTICS_REFRESH_INTERVAL), + }, + } + ) + + app.conf.CELERY_QUEUES.extend( + ( + Queue( + 'statistics', Exchange('statistics'), + routing_key='statistics', delivery_mode=1 + ), + ) + ) + + app.conf.CELERY_ROUTES.update( + { + 'statistics.tasks.task_check_statistics': { + 'queue': 'statistics' + }, + } + ) + menu_object.bind_links(links=(link_execute,), sources=(Statistic,)) menu_object.bind_links( links=(link_namespace_details,), sources=(StatisticNamespace,) diff --git a/mayan/apps/statistics/classes.py b/mayan/apps/statistics/classes.py index 329f6843fd..cfd2a3b5e7 100644 --- a/mayan/apps/statistics/classes.py +++ b/mayan/apps/statistics/classes.py @@ -1,3 +1,8 @@ +from __future__ import unicode_literals + +from .models import StatisticResult + + class StatisticNamespace(object): _registry = {} @@ -18,9 +23,10 @@ class StatisticNamespace(object): def __unicode__(self): return unicode(self.label) - def add_statistic(self, statistic): - self._statistics.append(statistic) + def add_statistic(self, *args, **kwargs): + statistic = Statistic(*args, **kwargs) statistic.namespace = self + self._statistics.append(statistic) @property def id(self): @@ -39,20 +45,33 @@ class Statistic(object): return cls._registry.values() @classmethod - def get(cls, name): - return cls._registry[name] + def get(cls, slug): + return cls._registry[slug] - def __init__(self, name, label): - self.name = name + def __init__(self, slug, label, func): + self.slug = slug self.label = label - self.__class__._registry[name] = self + self.func = func + self.__class__._registry[slug] = self def __unicode__(self): return unicode(self.label) - def get_results(self, *args, **kwargs): - return NotImplementedError + def execute(self): + self.store_results(results=self.func()) @property def id(self): - return self.name + return self.slug + + def store_results(self, results): + StatisticResult.objects.filter(slug=self.slug).delete() + + statistic_result = StatisticResult.objects.create(slug=self.slug) + statistic_result.store_data(data=results) + + def get_results(self): + try: + return StatisticResult.objects.get(slug=self.slug).get_data() + except StatisticResultDoesNotExist: + return ((),) diff --git a/mayan/apps/statistics/literals.py b/mayan/apps/statistics/literals.py new file mode 100644 index 0000000000..1e19e26d4d --- /dev/null +++ b/mayan/apps/statistics/literals.py @@ -0,0 +1,3 @@ +from __future__ import unicode_literals + +STATISTICS_REFRESH_INTERVAL = 60 * 60 # Every hour diff --git a/mayan/apps/statistics/migrations/0001_initial.py b/mayan/apps/statistics/migrations/0001_initial.py new file mode 100644 index 0000000000..5c3d726f81 --- /dev/null +++ b/mayan/apps/statistics/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='StatisticResult', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('slug', models.SlugField(verbose_name='Slug')), + ('datetime', models.DateTimeField(auto_now=True, verbose_name='Date time')), + ('serialize_data', models.TextField(verbose_name='Data', blank=True)), + ], + options={ + 'verbose_name': 'Statistics result', + 'verbose_name_plural': 'Statistics results', + }, + bases=(models.Model,), + ), + ] diff --git a/mayan/apps/statistics/migrations/__init__.py b/mayan/apps/statistics/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mayan/apps/statistics/models.py b/mayan/apps/statistics/models.py new file mode 100644 index 0000000000..d2adf6516e --- /dev/null +++ b/mayan/apps/statistics/models.py @@ -0,0 +1,30 @@ +from __future__ import unicode_literals + +import json + +from django.db import models +from django.utils.encoding import python_2_unicode_compatible +from django.utils.translation import ugettext_lazy as _ + + +@python_2_unicode_compatible +class StatisticResult(models.Model): + slug = models.SlugField(verbose_name=_('Slug')) + datetime = models.DateTimeField( + auto_now=True, verbose_name=_('Date time') + ) + serialize_data = models.TextField(blank=True, verbose_name=_('Data')) + + def get_data(self): + return json.loads(self.serialize_data) + + def store_data(self, data): + self.serialize_data = json.dumps(data) + self.save() + + def __str__(self): + return self.slug + + class Meta: + verbose_name = _('Statistics result') + verbose_name_plural = _('Statistics results') diff --git a/mayan/apps/statistics/tasks.py b/mayan/apps/statistics/tasks.py new file mode 100644 index 0000000000..7013bcf45a --- /dev/null +++ b/mayan/apps/statistics/tasks.py @@ -0,0 +1,20 @@ +from __future__ import unicode_literals + +import logging + +from mayan.celery import app + +from .classes import StatisticNamespace + +logger = logging.getLogger(__name__) + + +@app.task(ignore_result=True) +def task_check_statistics(): + logger.info('Executing') + + for namespace in StatisticNamespace.get_all(): + for statistic in namespace.statistics: + statistic.execute() + + logger.info('Finshed') diff --git a/mayan/apps/statistics/urls.py b/mayan/apps/statistics/urls.py index a952799240..c535da3652 100644 --- a/mayan/apps/statistics/urls.py +++ b/mayan/apps/statistics/urls.py @@ -12,7 +12,7 @@ urlpatterns = patterns( NamespaceDetailView.as_view(), name='namespace_details' ), url( - r'^(?P\w+)/execute/$', StatisticExecute.as_view(), + r'^(?P[\w,-]+)/view/$', StatisticExecute.as_view(), name='statistic_execute' ), ) From 5846743d6e72aff3e73098342dfb15fd6017ae8f Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 9 Sep 2015 04:36:09 -0400 Subject: [PATCH 2/9] Finish conversion of statistic to periodic statistics. Add ChartJS support. Add Line chart renderer. --- mayan/apps/documents/apps.py | 26 +- mayan/apps/documents/statistics.py | 73 +- mayan/apps/statistics/classes.py | 77 +- mayan/apps/statistics/links.py | 4 +- mayan/apps/statistics/literals.py | 2 +- .../packages/Chart.js-1.0.2/.gitignore | 7 + .../packages/Chart.js-1.0.2/.travis.yml | 13 + .../packages/Chart.js-1.0.2/CONTRIBUTING.md | 55 + .../packages/Chart.js-1.0.2/Chart.js | 3477 +++++++++++++++++ .../packages/Chart.js-1.0.2/Chart.min.js | 11 + .../packages/Chart.js-1.0.2/LICENSE.md | 7 + .../packages/Chart.js-1.0.2/README.md | 20 + .../packages/Chart.js-1.0.2/bower.json | 11 + .../Chart.js-1.0.2/docs/00-Getting-Started.md | 203 + .../Chart.js-1.0.2/docs/01-Line-Chart.md | 166 + .../Chart.js-1.0.2/docs/02-Bar-Chart.md | 149 + .../Chart.js-1.0.2/docs/03-Radar-Chart.md | 177 + .../docs/04-Polar-Area-Chart.md | 172 + .../docs/05-Pie-Doughnut-Chart.md | 158 + .../Chart.js-1.0.2/docs/06-Advanced.md | 185 + .../packages/Chart.js-1.0.2/docs/07-Notes.md | 42 + .../packages/Chart.js-1.0.2/gulpfile.js | 137 + .../packages/Chart.js-1.0.2/package.json | 28 + .../packages/Chart.js-1.0.2/samples/bar.html | 45 + .../Chart.js-1.0.2/samples/doughnut.html | 67 + .../samples/line-customTooltips.html | 129 + .../packages/Chart.js-1.0.2/samples/line.html | 54 + .../samples/pie-customTooltips.html | 156 + .../packages/Chart.js-1.0.2/samples/pie.html | 58 + .../Chart.js-1.0.2/samples/polar-area.html | 60 + .../Chart.js-1.0.2/samples/radar.html | 53 + .../packages/Chart.js-1.0.2/src/Chart.Bar.js | 302 ++ .../packages/Chart.js-1.0.2/src/Chart.Core.js | 2021 ++++++++++ .../Chart.js-1.0.2/src/Chart.Doughnut.js | 184 + .../packages/Chart.js-1.0.2/src/Chart.Line.js | 374 ++ .../Chart.js-1.0.2/src/Chart.PolarArea.js | 250 ++ .../Chart.js-1.0.2/src/Chart.Radar.js | 343 ++ .../statistics/backends/chartjs/line.html | 35 + mayan/apps/statistics/urls.py | 6 +- mayan/apps/statistics/views.py | 29 +- requirements/common.txt | 1 + 41 files changed, 9308 insertions(+), 59 deletions(-) create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/.gitignore create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/.travis.yml create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/CONTRIBUTING.md create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/Chart.js create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/Chart.min.js create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/LICENSE.md create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/README.md create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/bower.json create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/00-Getting-Started.md create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/01-Line-Chart.md create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/02-Bar-Chart.md create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/03-Radar-Chart.md create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/04-Polar-Area-Chart.md create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/05-Pie-Doughnut-Chart.md create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/06-Advanced.md create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/07-Notes.md create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/gulpfile.js create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/package.json create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/bar.html create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/doughnut.html create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/line-customTooltips.html create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/line.html create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/pie-customTooltips.html create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/pie.html create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/polar-area.html create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/radar.html create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.Bar.js create mode 100755 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.Core.js create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.Doughnut.js create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.Line.js create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.PolarArea.js create mode 100644 mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.Radar.js create mode 100644 mayan/apps/statistics/templates/statistics/backends/chartjs/line.html diff --git a/mayan/apps/documents/apps.py b/mayan/apps/documents/apps.py index 623a557f2b..982a5491ca 100644 --- a/mayan/apps/documents/apps.py +++ b/mayan/apps/documents/apps.py @@ -29,7 +29,7 @@ from events.permissions import permission_events_view from mayan.celery import app from navigation import SourceColumn from rest_api.classes import APIEndPoint -from statistics.classes import StatisticNamespace +from statistics.classes import StatisticNamespace, CharJSLine from .handlers import create_default_document_type from .links import ( @@ -72,7 +72,10 @@ from .permissions import ( permission_document_view ) from .settings import setting_thumbnail_size -from .statistics import document_page_count_per_month +from .statistics import ( + new_documents_per_month, new_document_pages_per_month, + new_document_versions_per_month +) from .widgets import document_thumbnail @@ -351,9 +354,22 @@ class DocumentsApp(MayanAppConfig): namespace = StatisticNamespace(name='documents', label=_('Documents')) namespace.add_statistic( - slug='document-page-count-per-month', - label=_('Document page count per month'), - func=document_page_count_per_month + slug='new-documents-per-month', + label=_('New documents per month'), + func=new_documents_per_month, + renderer=CharJSLine + ) + namespace.add_statistic( + slug='new-document-versions-per-month', + label=_('New document versions per month'), + func=new_document_versions_per_month, + renderer=CharJSLine + ) + namespace.add_statistic( + slug='new-document-pages-per-month', + label=_('New document pages per month'), + func=new_document_pages_per_month, + renderer=CharJSLine ) post_initial_setup.connect( diff --git a/mayan/apps/documents/statistics.py b/mayan/apps/documents/statistics.py index 4ca0a641ea..848288f68a 100644 --- a/mayan/apps/documents/statistics.py +++ b/mayan/apps/documents/statistics.py @@ -1,7 +1,13 @@ from __future__ import absolute_import, unicode_literals +import datetime + +import qsstats + from django.db.models import Avg, Count, Max, Min from django.template.defaultfilters import filesizeformat +from django.utils import formats +from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ from statistics.classes import Statistic @@ -40,41 +46,45 @@ def storage_count(path='.'): return total_count, total_size -def document_page_count_per_month(): - return ( - ( - {'August, 2015': 1244}, - {'September, 2015': 3123}, - ), - ) +def new_documents_per_month(): + qss = qsstats.QuerySetStats(Document.passthrough.all(), 'date_added') + + today = datetime.date.today() + this_year = datetime.date(year=today.year, month=1, day=1) + + return { + 'series': { + 'Document': map(lambda x: {x[0].month: x[1]}, qss.time_series(start=this_year, end=today, interval='months')) + } + } - #def get_results(self): - # return ( - ## ( - # {_('Document types'): DocumentType.objects.count()}, - # ), - # ) +def new_document_versions_per_month(): + qss = qsstats.QuerySetStats(DocumentVersion.objects.all(), 'document__date_added') + + today = datetime.date.today() + this_year = datetime.date(year=today.year, month=1, day=1) + + return { + 'series': { + 'Document': map(lambda x: {x[0].month: x[1]}, qss.time_series(start=this_year, end=today, interval='months')) + } + } + + +def new_document_pages_per_month(): + qss = qsstats.QuerySetStats(DocumentPage.objects.all(), 'document_version__document__date_added') + + today = datetime.date.today() + this_year = datetime.date(year=today.year, month=1, day=1) + + return { + 'series': { + 'Document': map(lambda x: {x[0].month: x[1]}, qss.time_series(start=this_year, end=today, interval='months')) + } + } -""" - document_stats = DocumentVersion.objects.annotate( - page_count=Count('pages') - ).aggregate(Min('page_count'), Max('page_count'), Avg('page_count')) - results.extend( - [ - _( - 'Minimum amount of pages per document: %d' - ) % (document_stats['page_count__min'] or 0), - _( - 'Maximum amount of pages per document: %d' - ) % (document_stats['page_count__max'] or 0), - _( - 'Average amount of pages per document: %f' - ) % (document_stats['page_count__avg'] or 0), - ] - ) - return results """ class DocumentUsageStatistics(Statistic): @@ -111,3 +121,4 @@ class DocumentUsageStatistics(Statistic): ) return results +""" diff --git a/mayan/apps/statistics/classes.py b/mayan/apps/statistics/classes.py index cfd2a3b5e7..c6d03b3940 100644 --- a/mayan/apps/statistics/classes.py +++ b/mayan/apps/statistics/classes.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import json + from .models import StatisticResult @@ -48,10 +50,11 @@ class Statistic(object): def get(cls, slug): return cls._registry[slug] - def __init__(self, slug, label, func): + def __init__(self, slug, label, func, renderer): self.slug = slug self.label = label self.func = func + self.renderer = renderer self.__class__._registry[slug] = self def __unicode__(self): @@ -73,5 +76,73 @@ class Statistic(object): def get_results(self): try: return StatisticResult.objects.get(slug=self.slug).get_data() - except StatisticResultDoesNotExist: - return ((),) + except StatisticResult.DoesNotExist: + return {'series': {}} + + def get_chart_data(self): + return self.renderer(data=self.get_results()).get_chart_data() + + +class ChartRenderer(object): + def __init__(self, data): + self.data = data + + def get_chart_data(self): + raise NotImplementedError + + +class CharJSLine(ChartRenderer): + template_name = 'statistics/backends/chartjs/line.html' + + dataset_palette = ( + { + 'fillColor': "rgba(220,220,220,0.2)", + 'strokeColor': "rgba(220,220,220,1)", + 'pointColor': "rgba(220,220,220,1)", + 'pointStrokeColor': "#fff", + 'pointHighlightFill': "#fff", + 'pointHighlightStroke': "rgba(220,220,220,1)", + }, + { + 'fillColor': "rgba(151,187,205,0.2)", + 'strokeColor': "rgba(151,187,205,1)", + 'pointColor': "rgba(151,187,205,1)", + 'pointStrokeColor': "#fff", + 'pointHighlightFill': "#fff", + 'pointHighlightStroke': "rgba(151,187,205,1)", + } + ) + + def get_chart_data(self): + labels = [] + datasets = [] + + for count, serie in enumerate(self.data['series'].items()): + series_name, series_data = serie + dataset_labels = [] + dataset_values = [] + + for data_point in series_data: + dataset_labels.extend(data_point.keys()) + dataset_values.extend(data_point.values()) + + labels = dataset_labels + dataset = { + 'label': series_name, + 'data': dataset_values, + } + dataset.update( + CharJSLine.dataset_palette[ + count % len(CharJSLine.dataset_palette) + ] + ) + + datasets.append(dataset) + + data = { + 'labels': labels, + 'datasets': datasets, + + } + + return json.dumps(data) diff --git a/mayan/apps/statistics/links.py b/mayan/apps/statistics/links.py index 7add5c86c1..6a55576cd2 100644 --- a/mayan/apps/statistics/links.py +++ b/mayan/apps/statistics/links.py @@ -8,8 +8,8 @@ from .permissions import permission_statistics_view link_execute = Link( - permissions=(permission_statistics_view,), text=_('Execute'), - view='statistics:statistic_execute', args='resolved_object.id' + permissions=(permission_statistics_view,), text=_('View'), + view='statistics:statistic_detail', args='resolved_object.id' ) link_namespace_details = Link( permissions=(permission_statistics_view,), text=_('Namespace details'), diff --git a/mayan/apps/statistics/literals.py b/mayan/apps/statistics/literals.py index 1e19e26d4d..85ab08bbef 100644 --- a/mayan/apps/statistics/literals.py +++ b/mayan/apps/statistics/literals.py @@ -1,3 +1,3 @@ from __future__ import unicode_literals -STATISTICS_REFRESH_INTERVAL = 60 * 60 # Every hour +STATISTICS_REFRESH_INTERVAL = 60 * 60 * 24 # Every 12 hours diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/.gitignore b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/.gitignore new file mode 100644 index 0000000000..3969d0aade --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/.gitignore @@ -0,0 +1,7 @@ + +.DS_Store + +node_modules/* +custom/* + +docs/index.md diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/.travis.yml b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/.travis.yml new file mode 100644 index 0000000000..c66ea5fb77 --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/.travis.yml @@ -0,0 +1,13 @@ +language: node_js +node_js: + - "0.11" + - "0.10" + +before_script: + - npm install + +script: + - gulp test + +notifications: + slack: chartjs:pcfCZR6ugg5TEcaLtmIfQYuA diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/CONTRIBUTING.md b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/CONTRIBUTING.md new file mode 100644 index 0000000000..714bdd2a73 --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/CONTRIBUTING.md @@ -0,0 +1,55 @@ +Contributing to Chart.js +======================== + +Contributions to Chart.js are welcome and encouraged, but please have a look through the guidelines in this document before raising an issue, or writing code for the project. + + +Using issues +------------ + +The [issue tracker](https://github.com/nnnick/Chart.js/issues) is the preferred channel for reporting bugs, requesting new features and submitting pull requests. + +If you're suggesting a new chart type, please take a look at [writing new chart types](https://github.com/nnnick/Chart.js/blob/master/docs/06-Advanced.md#writing-new-chart-types) in the documentation, and some of the [community extensions](https://github.com/nnnick/Chart.js/blob/master/docs/06-Advanced.md#community-extensions) that have been created already. + +To keep the library lightweight for everyone, it's unlikely we'll add many more chart types to the core of Chart.js, but issues are a good medium to design and spec out how new chart types could work and look. + +Please do not use issues for support requests. For help using Chart.js, please take a look at the [`chartjs`](http://stackoverflow.com/questions/tagged/chartjs) tag on Stack Overflow. + + +Reporting bugs +-------------- + +Well structured, detailed bug reports are hugely valuable for the project. + +Guidlines for reporting bugs: + + - Check the issue search to see if it has already been reported + - Isolate the problem to a simple test case + - Provide a demonstration of the problem on [JS Bin](http://jsbin.com) or similar + +Please provide any additional details associated with the bug, if it's browser or screen density specific, or only happens with a certain configuration or data. + + +Pull requests +------------- + +Clear, concise pull requests are excellent at continuing the project's community driven growth. But please review [these guidelines](https://github.com/blog/1943-how-to-write-the-perfect-pull-request) and the guidelines below before starting work on the project. + +Guidlines: + + - Please create an issue first: + - For bugs, we can discuss the fixing approach + - For enhancements, we can discuss if it is within the project scope and avoid duplicate effort + - Please make changes to the files in [`/src`](https://github.com/nnnick/Chart.js/tree/master/src), not `Chart.js` or `Chart.min.js` in the repo root directory, this avoids merge conflicts + - Tabs for indentation, not spaces please + - If adding new functionality, please also update the relevant `.md` file in [`/docs`](https://github.com/nnnick/Chart.js/tree/master/docs) + - Please make your commits in logical sections with clear commit messages + +Joining the Project +------------- + - Active committers and contributors are invited to introduce yourself and request commit access to this project. Please send an email to hello@nickdownie.com or file an issue. + +License +------- + +By contributing your code, you agree to license your contribution under the [MIT license](https://github.com/nnnick/Chart.js/blob/master/LICENSE.md). diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/Chart.js b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/Chart.js new file mode 100644 index 0000000000..c264262ba7 --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/Chart.js @@ -0,0 +1,3477 @@ +/*! + * Chart.js + * http://chartjs.org/ + * Version: 1.0.2 + * + * Copyright 2015 Nick Downie + * Released under the MIT license + * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md + */ + + +(function(){ + + "use strict"; + + //Declare root variable - window in the browser, global on the server + var root = this, + previous = root.Chart; + + //Occupy the global variable of Chart, and create a simple base class + var Chart = function(context){ + var chart = this; + this.canvas = context.canvas; + + this.ctx = context; + + //Variables global to the chart + var computeDimension = function(element,dimension) + { + if (element['offset'+dimension]) + { + return element['offset'+dimension]; + } + else + { + return document.defaultView.getComputedStyle(element).getPropertyValue(dimension); + } + } + + var width = this.width = computeDimension(context.canvas,'Width'); + var height = this.height = computeDimension(context.canvas,'Height'); + + // Firefox requires this to work correctly + context.canvas.width = width; + context.canvas.height = height; + + var width = this.width = context.canvas.width; + var height = this.height = context.canvas.height; + this.aspectRatio = this.width / this.height; + //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. + helpers.retinaScale(this); + + return this; + }; + //Globally expose the defaults to allow for user updating/changing + Chart.defaults = { + global: { + // Boolean - Whether to animate the chart + animation: true, + + // Number - Number of animation steps + animationSteps: 60, + + // String - Animation easing effect + animationEasing: "easeOutQuart", + + // Boolean - If we should show the scale at all + showScale: true, + + // Boolean - If we want to override with a hard coded scale + scaleOverride: false, + + // ** Required if scaleOverride is true ** + // Number - The number of steps in a hard coded scale + scaleSteps: null, + // Number - The value jump in the hard coded scale + scaleStepWidth: null, + // Number - The scale starting value + scaleStartValue: null, + + // String - Colour of the scale line + scaleLineColor: "rgba(0,0,0,.1)", + + // Number - Pixel width of the scale line + scaleLineWidth: 1, + + // Boolean - Whether to show labels on the scale + scaleShowLabels: true, + + // Interpolated JS string - can access value + scaleLabel: "<%=value%>", + + // Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there + scaleIntegersOnly: true, + + // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value + scaleBeginAtZero: false, + + // String - Scale label font declaration for the scale label + scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Scale label font size in pixels + scaleFontSize: 12, + + // String - Scale label font weight style + scaleFontStyle: "normal", + + // String - Scale label font colour + scaleFontColor: "#666", + + // Boolean - whether or not the chart should be responsive and resize when the browser does. + responsive: false, + + // Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container + maintainAspectRatio: true, + + // Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove + showTooltips: true, + + // Boolean - Determines whether to draw built-in tooltip or call custom tooltip function + customTooltips: false, + + // Array - Array of string names to attach tooltip events + tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"], + + // String - Tooltip background colour + tooltipFillColor: "rgba(0,0,0,0.8)", + + // String - Tooltip label font declaration for the scale label + tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Tooltip label font size in pixels + tooltipFontSize: 14, + + // String - Tooltip font weight style + tooltipFontStyle: "normal", + + // String - Tooltip label font colour + tooltipFontColor: "#fff", + + // String - Tooltip title font declaration for the scale label + tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Tooltip title font size in pixels + tooltipTitleFontSize: 14, + + // String - Tooltip title font weight style + tooltipTitleFontStyle: "bold", + + // String - Tooltip title font colour + tooltipTitleFontColor: "#fff", + + // Number - pixel width of padding around tooltip text + tooltipYPadding: 6, + + // Number - pixel width of padding around tooltip text + tooltipXPadding: 6, + + // Number - Size of the caret on the tooltip + tooltipCaretSize: 8, + + // Number - Pixel radius of the tooltip border + tooltipCornerRadius: 6, + + // Number - Pixel offset from point x to tooltip edge + tooltipXOffset: 10, + + // String - Template string for single tooltips + tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>", + + // String - Template string for single tooltips + multiTooltipTemplate: "<%= value %>", + + // String - Colour behind the legend colour block + multiTooltipKeyBackground: '#fff', + + // Function - Will fire on animation progression. + onAnimationProgress: function(){}, + + // Function - Will fire on animation completion. + onAnimationComplete: function(){} + + } + }; + + //Create a dictionary of chart types, to allow for extension of existing types + Chart.types = {}; + + //Global Chart helpers object for utility methods and classes + var helpers = Chart.helpers = {}; + + //-- Basic js utility methods + var each = helpers.each = function(loopable,callback,self){ + var additionalArgs = Array.prototype.slice.call(arguments, 3); + // Check to see if null or undefined firstly. + if (loopable){ + if (loopable.length === +loopable.length){ + var i; + for (i=0; i= 0; i--) { + var currentItem = arrayToSearch[i]; + if (filterCallback(currentItem)){ + return currentItem; + } + } + }, + inherits = helpers.inherits = function(extensions){ + //Basic javascript inheritance based on the model created in Backbone.js + var parent = this; + var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); }; + + var Surrogate = function(){ this.constructor = ChartElement;}; + Surrogate.prototype = parent.prototype; + ChartElement.prototype = new Surrogate(); + + ChartElement.extend = inherits; + + if (extensions) extend(ChartElement.prototype, extensions); + + ChartElement.__super__ = parent.prototype; + + return ChartElement; + }, + noop = helpers.noop = function(){}, + uid = helpers.uid = (function(){ + var id=0; + return function(){ + return "chart-" + id++; + }; + })(), + warn = helpers.warn = function(str){ + //Method for warning of errors + if (window.console && typeof window.console.warn == "function") console.warn(str); + }, + amd = helpers.amd = (typeof define == 'function' && define.amd), + //-- Math methods + isNumber = helpers.isNumber = function(n){ + return !isNaN(parseFloat(n)) && isFinite(n); + }, + max = helpers.max = function(array){ + return Math.max.apply( Math, array ); + }, + min = helpers.min = function(array){ + return Math.min.apply( Math, array ); + }, + cap = helpers.cap = function(valueToCap,maxValue,minValue){ + if(isNumber(maxValue)) { + if( valueToCap > maxValue ) { + return maxValue; + } + } + else if(isNumber(minValue)){ + if ( valueToCap < minValue ){ + return minValue; + } + } + return valueToCap; + }, + getDecimalPlaces = helpers.getDecimalPlaces = function(num){ + if (num%1!==0 && isNumber(num)){ + return num.toString().split(".")[1].length; + } + else { + return 0; + } + }, + toRadians = helpers.radians = function(degrees){ + return degrees * (Math.PI/180); + }, + // Gets the angle from vertical upright to the point about a centre. + getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){ + var distanceFromXCenter = anglePoint.x - centrePoint.x, + distanceFromYCenter = anglePoint.y - centrePoint.y, + radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); + + + var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter); + + //If the segment is in the top left quadrant, we need to add another rotation to the angle + if (distanceFromXCenter < 0 && distanceFromYCenter < 0){ + angle += Math.PI*2; + } + + return { + angle: angle, + distance: radialDistanceFromCenter + }; + }, + aliasPixel = helpers.aliasPixel = function(pixelWidth){ + return (pixelWidth % 2 === 0) ? 0 : 0.5; + }, + splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){ + //Props to Rob Spencer at scaled innovation for his post on splining between points + //http://scaledinnovation.com/analytics/splines/aboutSplines.html + var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)), + d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)), + fa=t*d01/(d01+d12),// scaling factor for triangle Ta + fb=t*d12/(d01+d12); + return { + inner : { + x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x), + y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y) + }, + outer : { + x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x), + y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y) + } + }; + }, + calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){ + return Math.floor(Math.log(val) / Math.LN10); + }, + calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){ + + //Set a minimum step of two - a point at the top of the graph, and a point at the base + var minSteps = 2, + maxSteps = Math.floor(drawingSize/(textSize * 1.5)), + skipFitting = (minSteps >= maxSteps); + + var maxValue = max(valuesArray), + minValue = min(valuesArray); + + // We need some degree of seperation here to calculate the scales if all the values are the same + // Adding/minusing 0.5 will give us a range of 1. + if (maxValue === minValue){ + maxValue += 0.5; + // So we don't end up with a graph with a negative start value if we've said always start from zero + if (minValue >= 0.5 && !startFromZero){ + minValue -= 0.5; + } + else{ + // Make up a whole number above the values + maxValue += 0.5; + } + } + + var valueRange = Math.abs(maxValue - minValue), + rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange), + graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), + graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), + graphRange = graphMax - graphMin, + stepValue = Math.pow(10, rangeOrderOfMagnitude), + numberOfSteps = Math.round(graphRange / stepValue); + + //If we have more space on the graph we'll use it to give more definition to the data + while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) { + if(numberOfSteps > maxSteps){ + stepValue *=2; + numberOfSteps = Math.round(graphRange/stepValue); + // Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps. + if (numberOfSteps % 1 !== 0){ + skipFitting = true; + } + } + //We can fit in double the amount of scale points on the scale + else{ + //If user has declared ints only, and the step value isn't a decimal + if (integersOnly && rangeOrderOfMagnitude >= 0){ + //If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float + if(stepValue/2 % 1 === 0){ + stepValue /=2; + numberOfSteps = Math.round(graphRange/stepValue); + } + //If it would make it a float break out of the loop + else{ + break; + } + } + //If the scale doesn't have to be an int, make the scale more granular anyway. + else{ + stepValue /=2; + numberOfSteps = Math.round(graphRange/stepValue); + } + + } + } + + if (skipFitting){ + numberOfSteps = minSteps; + stepValue = graphRange / numberOfSteps; + } + + return { + steps : numberOfSteps, + stepValue : stepValue, + min : graphMin, + max : graphMin + (numberOfSteps * stepValue) + }; + + }, + /* jshint ignore:start */ + // Blows up jshint errors based on the new Function constructor + //Templating methods + //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/ + template = helpers.template = function(templateString, valuesObject){ + + // If templateString is function rather than string-template - call the function for valuesObject + + if(templateString instanceof Function){ + return templateString(valuesObject); + } + + var cache = {}; + 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) ? + cache[str] = cache[str] : + + // Generate a reusable function that will serve as a template + // generator (and which will be cached). + new Function("obj", + "var p=[],print=function(){p.push.apply(p,arguments);};" + + + // Introduce the data as local variables using with(){} + "with(obj){p.push('" + + + // Convert the template into pure JavaScript + str + .replace(/[\r\t\n]/g, " ") + .split("<%").join("\t") + .replace(/((^|%>)[^\t]*)'/g, "$1\r") + .replace(/\t=(.*?)%>/g, "',$1,'") + .split("\t").join("');") + .split("%>").join("p.push('") + .split("\r").join("\\'") + + "');}return p.join('');" + ); + + // Provide some basic currying to the user + return data ? fn( data ) : fn; + } + return tmpl(templateString,valuesObject); + }, + /* jshint ignore:end */ + generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){ + var labelsArray = new Array(numberOfSteps); + if (labelTemplateString){ + each(labelsArray,function(val,index){ + labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))}); + }); + } + return labelsArray; + }, + //--Animation methods + //Easing functions adapted from Robert Penner's easing equations + //http://www.robertpenner.com/easing/ + easingEffects = helpers.easingEffects = { + linear: function (t) { + return t; + }, + easeInQuad: function (t) { + return t * t; + }, + easeOutQuad: function (t) { + return -1 * t * (t - 2); + }, + easeInOutQuad: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t; + return -1 / 2 * ((--t) * (t - 2) - 1); + }, + easeInCubic: function (t) { + return t * t * t; + }, + easeOutCubic: function (t) { + return 1 * ((t = t / 1 - 1) * t * t + 1); + }, + easeInOutCubic: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t; + return 1 / 2 * ((t -= 2) * t * t + 2); + }, + easeInQuart: function (t) { + return t * t * t * t; + }, + easeOutQuart: function (t) { + return -1 * ((t = t / 1 - 1) * t * t * t - 1); + }, + easeInOutQuart: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t; + return -1 / 2 * ((t -= 2) * t * t * t - 2); + }, + easeInQuint: function (t) { + return 1 * (t /= 1) * t * t * t * t; + }, + easeOutQuint: function (t) { + return 1 * ((t = t / 1 - 1) * t * t * t * t + 1); + }, + easeInOutQuint: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t * t; + return 1 / 2 * ((t -= 2) * t * t * t * t + 2); + }, + easeInSine: function (t) { + return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1; + }, + easeOutSine: function (t) { + return 1 * Math.sin(t / 1 * (Math.PI / 2)); + }, + easeInOutSine: function (t) { + return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1); + }, + easeInExpo: function (t) { + return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1)); + }, + easeOutExpo: function (t) { + return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1); + }, + easeInOutExpo: function (t) { + if (t === 0) return 0; + if (t === 1) return 1; + if ((t /= 1 / 2) < 1) return 1 / 2 * Math.pow(2, 10 * (t - 1)); + return 1 / 2 * (-Math.pow(2, -10 * --t) + 2); + }, + easeInCirc: function (t) { + if (t >= 1) return t; + return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1); + }, + easeOutCirc: function (t) { + return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t); + }, + easeInOutCirc: function (t) { + if ((t /= 1 / 2) < 1) return -1 / 2 * (Math.sqrt(1 - t * t) - 1); + return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1); + }, + easeInElastic: function (t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) return 0; + if ((t /= 1) == 1) return 1; + if (!p) p = 1 * 0.3; + if (a < Math.abs(1)) { + a = 1; + s = p / 4; + } else s = p / (2 * Math.PI) * Math.asin(1 / a); + return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); + }, + easeOutElastic: function (t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) return 0; + if ((t /= 1) == 1) return 1; + if (!p) p = 1 * 0.3; + if (a < Math.abs(1)) { + a = 1; + s = p / 4; + } else s = p / (2 * Math.PI) * Math.asin(1 / a); + return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1; + }, + easeInOutElastic: function (t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) return 0; + if ((t /= 1 / 2) == 2) return 1; + if (!p) p = 1 * (0.3 * 1.5); + if (a < Math.abs(1)) { + a = 1; + s = p / 4; + } else s = p / (2 * Math.PI) * Math.asin(1 / a); + if (t < 1) return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); + return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1; + }, + easeInBack: function (t) { + var s = 1.70158; + return 1 * (t /= 1) * t * ((s + 1) * t - s); + }, + easeOutBack: function (t) { + var s = 1.70158; + return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1); + }, + easeInOutBack: function (t) { + var s = 1.70158; + if ((t /= 1 / 2) < 1) return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)); + return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2); + }, + easeInBounce: function (t) { + return 1 - easingEffects.easeOutBounce(1 - t); + }, + easeOutBounce: function (t) { + if ((t /= 1) < (1 / 2.75)) { + return 1 * (7.5625 * t * t); + } else if (t < (2 / 2.75)) { + return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75); + } else if (t < (2.5 / 2.75)) { + return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375); + } else { + return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375); + } + }, + easeInOutBounce: function (t) { + if (t < 1 / 2) return easingEffects.easeInBounce(t * 2) * 0.5; + return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5; + } + }, + //Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ + requestAnimFrame = helpers.requestAnimFrame = (function(){ + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback) { + return window.setTimeout(callback, 1000 / 60); + }; + })(), + cancelAnimFrame = helpers.cancelAnimFrame = (function(){ + return window.cancelAnimationFrame || + window.webkitCancelAnimationFrame || + window.mozCancelAnimationFrame || + window.oCancelAnimationFrame || + window.msCancelAnimationFrame || + function(callback) { + return window.clearTimeout(callback, 1000 / 60); + }; + })(), + animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){ + + var currentStep = 0, + easingFunction = easingEffects[easingString] || easingEffects.linear; + + var animationFrame = function(){ + currentStep++; + var stepDecimal = currentStep/totalSteps; + var easeDecimal = easingFunction(stepDecimal); + + callback.call(chartInstance,easeDecimal,stepDecimal, currentStep); + onProgress.call(chartInstance,easeDecimal,stepDecimal); + if (currentStep < totalSteps){ + chartInstance.animationFrame = requestAnimFrame(animationFrame); + } else{ + onComplete.apply(chartInstance); + } + }; + requestAnimFrame(animationFrame); + }, + //-- DOM methods + getRelativePosition = helpers.getRelativePosition = function(evt){ + var mouseX, mouseY; + var e = evt.originalEvent || evt, + canvas = evt.currentTarget || evt.srcElement, + boundingRect = canvas.getBoundingClientRect(); + + if (e.touches){ + mouseX = e.touches[0].clientX - boundingRect.left; + mouseY = e.touches[0].clientY - boundingRect.top; + + } + else{ + mouseX = e.clientX - boundingRect.left; + mouseY = e.clientY - boundingRect.top; + } + + return { + x : mouseX, + y : mouseY + }; + + }, + addEvent = helpers.addEvent = function(node,eventType,method){ + if (node.addEventListener){ + node.addEventListener(eventType,method); + } else if (node.attachEvent){ + node.attachEvent("on"+eventType, method); + } else { + node["on"+eventType] = method; + } + }, + removeEvent = helpers.removeEvent = function(node, eventType, handler){ + if (node.removeEventListener){ + node.removeEventListener(eventType, handler, false); + } else if (node.detachEvent){ + node.detachEvent("on"+eventType,handler); + } else{ + node["on" + eventType] = noop; + } + }, + bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){ + // Create the events object if it's not already present + if (!chartInstance.events) chartInstance.events = {}; + + each(arrayOfEvents,function(eventName){ + chartInstance.events[eventName] = function(){ + handler.apply(chartInstance, arguments); + }; + addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]); + }); + }, + unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) { + each(arrayOfEvents, function(handler,eventName){ + removeEvent(chartInstance.chart.canvas, eventName, handler); + }); + }, + getMaximumWidth = helpers.getMaximumWidth = function(domNode){ + var container = domNode.parentNode; + // TODO = check cross browser stuff with this. + return container.clientWidth; + }, + getMaximumHeight = helpers.getMaximumHeight = function(domNode){ + var container = domNode.parentNode; + // TODO = check cross browser stuff with this. + return container.clientHeight; + }, + getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support + retinaScale = helpers.retinaScale = function(chart){ + var ctx = chart.ctx, + width = chart.canvas.width, + height = chart.canvas.height; + + if (window.devicePixelRatio) { + ctx.canvas.style.width = width + "px"; + ctx.canvas.style.height = height + "px"; + ctx.canvas.height = height * window.devicePixelRatio; + ctx.canvas.width = width * window.devicePixelRatio; + ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + } + }, + //-- Canvas methods + clear = helpers.clear = function(chart){ + chart.ctx.clearRect(0,0,chart.width,chart.height); + }, + fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){ + return fontStyle + " " + pixelSize+"px " + fontFamily; + }, + longestText = helpers.longestText = function(ctx,font,arrayOfStrings){ + ctx.font = font; + var longest = 0; + each(arrayOfStrings,function(string){ + var textWidth = ctx.measureText(string).width; + longest = (textWidth > longest) ? textWidth : longest; + }); + return longest; + }, + drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){ + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); + }; + + + //Store a reference to each instance - allowing us to globally resize chart instances on window resize. + //Destroy method on the chart will remove the instance of the chart from this reference. + Chart.instances = {}; + + Chart.Type = function(data,options,chart){ + this.options = options; + this.chart = chart; + this.id = uid(); + //Add the chart instance to the global namespace + Chart.instances[this.id] = this; + + // Initialize is always called when a chart type is created + // By default it is a no op, but it should be extended + if (options.responsive){ + this.resize(); + } + this.initialize.call(this,data); + }; + + //Core methods that'll be a part of every chart type + extend(Chart.Type.prototype,{ + initialize : function(){return this;}, + clear : function(){ + clear(this.chart); + return this; + }, + stop : function(){ + // Stops any current animation loop occuring + cancelAnimFrame(this.animationFrame); + return this; + }, + resize : function(callback){ + this.stop(); + var canvas = this.chart.canvas, + newWidth = getMaximumWidth(this.chart.canvas), + newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas); + + canvas.width = this.chart.width = newWidth; + canvas.height = this.chart.height = newHeight; + + retinaScale(this.chart); + + if (typeof callback === "function"){ + callback.apply(this, Array.prototype.slice.call(arguments, 1)); + } + return this; + }, + reflow : noop, + render : function(reflow){ + if (reflow){ + this.reflow(); + } + if (this.options.animation && !reflow){ + helpers.animationLoop( + this.draw, + this.options.animationSteps, + this.options.animationEasing, + this.options.onAnimationProgress, + this.options.onAnimationComplete, + this + ); + } + else{ + this.draw(); + this.options.onAnimationComplete.call(this); + } + return this; + }, + generateLegend : function(){ + return template(this.options.legendTemplate,this); + }, + destroy : function(){ + this.clear(); + unbindEvents(this, this.events); + var canvas = this.chart.canvas; + + // Reset canvas height/width attributes starts a fresh with the canvas context + canvas.width = this.chart.width; + canvas.height = this.chart.height; + + // < IE9 doesn't support removeProperty + if (canvas.style.removeProperty) { + canvas.style.removeProperty('width'); + canvas.style.removeProperty('height'); + } else { + canvas.style.removeAttribute('width'); + canvas.style.removeAttribute('height'); + } + + delete Chart.instances[this.id]; + }, + showTooltip : function(ChartElements, forceRedraw){ + // Only redraw the chart if we've actually changed what we're hovering on. + if (typeof this.activeElements === 'undefined') this.activeElements = []; + + var isChanged = (function(Elements){ + var changed = false; + + if (Elements.length !== this.activeElements.length){ + changed = true; + return changed; + } + + each(Elements, function(element, index){ + if (element !== this.activeElements[index]){ + changed = true; + } + }, this); + return changed; + }).call(this, ChartElements); + + if (!isChanged && !forceRedraw){ + return; + } + else{ + this.activeElements = ChartElements; + } + this.draw(); + if(this.options.customTooltips){ + this.options.customTooltips(false); + } + if (ChartElements.length > 0){ + // If we have multiple datasets, show a MultiTooltip for all of the data points at that index + if (this.datasets && this.datasets.length > 1) { + var dataArray, + dataIndex; + + for (var i = this.datasets.length - 1; i >= 0; i--) { + dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments; + dataIndex = indexOf(dataArray, ChartElements[0]); + if (dataIndex !== -1){ + break; + } + } + var tooltipLabels = [], + tooltipColors = [], + medianPosition = (function(index) { + + // Get all the points at that particular index + var Elements = [], + dataCollection, + xPositions = [], + yPositions = [], + xMax, + yMax, + xMin, + yMin; + helpers.each(this.datasets, function(dataset){ + dataCollection = dataset.points || dataset.bars || dataset.segments; + if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){ + Elements.push(dataCollection[dataIndex]); + } + }); + + helpers.each(Elements, function(element) { + xPositions.push(element.x); + yPositions.push(element.y); + + + //Include any colour information about the element + tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element)); + tooltipColors.push({ + fill: element._saved.fillColor || element.fillColor, + stroke: element._saved.strokeColor || element.strokeColor + }); + + }, this); + + yMin = min(yPositions); + yMax = max(yPositions); + + xMin = min(xPositions); + xMax = max(xPositions); + + return { + x: (xMin > this.chart.width/2) ? xMin : xMax, + y: (yMin + yMax)/2 + }; + }).call(this, dataIndex); + + new Chart.MultiTooltip({ + x: medianPosition.x, + y: medianPosition.y, + xPadding: this.options.tooltipXPadding, + yPadding: this.options.tooltipYPadding, + xOffset: this.options.tooltipXOffset, + fillColor: this.options.tooltipFillColor, + textColor: this.options.tooltipFontColor, + fontFamily: this.options.tooltipFontFamily, + fontStyle: this.options.tooltipFontStyle, + fontSize: this.options.tooltipFontSize, + titleTextColor: this.options.tooltipTitleFontColor, + titleFontFamily: this.options.tooltipTitleFontFamily, + titleFontStyle: this.options.tooltipTitleFontStyle, + titleFontSize: this.options.tooltipTitleFontSize, + cornerRadius: this.options.tooltipCornerRadius, + labels: tooltipLabels, + legendColors: tooltipColors, + legendColorBackground : this.options.multiTooltipKeyBackground, + title: ChartElements[0].label, + chart: this.chart, + ctx: this.chart.ctx, + custom: this.options.customTooltips + }).draw(); + + } else { + each(ChartElements, function(Element) { + var tooltipPosition = Element.tooltipPosition(); + new Chart.Tooltip({ + x: Math.round(tooltipPosition.x), + y: Math.round(tooltipPosition.y), + xPadding: this.options.tooltipXPadding, + yPadding: this.options.tooltipYPadding, + fillColor: this.options.tooltipFillColor, + textColor: this.options.tooltipFontColor, + fontFamily: this.options.tooltipFontFamily, + fontStyle: this.options.tooltipFontStyle, + fontSize: this.options.tooltipFontSize, + caretHeight: this.options.tooltipCaretSize, + cornerRadius: this.options.tooltipCornerRadius, + text: template(this.options.tooltipTemplate, Element), + chart: this.chart, + custom: this.options.customTooltips + }).draw(); + }, this); + } + } + return this; + }, + toBase64Image : function(){ + return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments); + } + }); + + Chart.Type.extend = function(extensions){ + + var parent = this; + + var ChartType = function(){ + return parent.apply(this,arguments); + }; + + //Copy the prototype object of the this class + ChartType.prototype = clone(parent.prototype); + //Now overwrite some of the properties in the base class with the new extensions + extend(ChartType.prototype, extensions); + + ChartType.extend = Chart.Type.extend; + + if (extensions.name || parent.prototype.name){ + + var chartName = extensions.name || parent.prototype.name; + //Assign any potential default values of the new chart type + + //If none are defined, we'll use a clone of the chart type this is being extended from. + //I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart + //doesn't define some defaults of their own. + + var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {}; + + Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults); + + Chart.types[chartName] = ChartType; + + //Register this new chart type in the Chart prototype + Chart.prototype[chartName] = function(data,options){ + var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {}); + return new ChartType(data,config,this); + }; + } else{ + warn("Name not provided for this chart, so it hasn't been registered"); + } + return parent; + }; + + Chart.Element = function(configuration){ + extend(this,configuration); + this.initialize.apply(this,arguments); + this.save(); + }; + extend(Chart.Element.prototype,{ + initialize : function(){}, + restore : function(props){ + if (!props){ + extend(this,this._saved); + } else { + each(props,function(key){ + this[key] = this._saved[key]; + },this); + } + return this; + }, + save : function(){ + this._saved = clone(this); + delete this._saved._saved; + return this; + }, + update : function(newProps){ + each(newProps,function(value,key){ + this._saved[key] = this[key]; + this[key] = value; + },this); + return this; + }, + transition : function(props,ease){ + each(props,function(value,key){ + this[key] = ((value - this._saved[key]) * ease) + this._saved[key]; + },this); + return this; + }, + tooltipPosition : function(){ + return { + x : this.x, + y : this.y + }; + }, + hasValue: function(){ + return isNumber(this.value); + } + }); + + Chart.Element.extend = inherits; + + + Chart.Point = Chart.Element.extend({ + display: true, + inRange: function(chartX,chartY){ + var hitDetectionRange = this.hitDetectionRadius + this.radius; + return ((Math.pow(chartX-this.x, 2)+Math.pow(chartY-this.y, 2)) < Math.pow(hitDetectionRange,2)); + }, + draw : function(){ + if (this.display){ + var ctx = this.ctx; + ctx.beginPath(); + + ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2); + ctx.closePath(); + + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + ctx.fillStyle = this.fillColor; + + ctx.fill(); + ctx.stroke(); + } + + + //Quick debug for bezier curve splining + //Highlights control points and the line between them. + //Handy for dev - stripped in the min version. + + // ctx.save(); + // ctx.fillStyle = "black"; + // ctx.strokeStyle = "black" + // ctx.beginPath(); + // ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2); + // ctx.fill(); + + // ctx.beginPath(); + // ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2); + // ctx.fill(); + + // ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y); + // ctx.lineTo(this.x, this.y); + // ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y); + // ctx.stroke(); + + // ctx.restore(); + + + + } + }); + + Chart.Arc = Chart.Element.extend({ + inRange : function(chartX,chartY){ + + var pointRelativePosition = helpers.getAngleFromPoint(this, { + x: chartX, + y: chartY + }); + + //Check if within the range of the open/close angle + var betweenAngles = (pointRelativePosition.angle >= this.startAngle && pointRelativePosition.angle <= this.endAngle), + withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius); + + return (betweenAngles && withinRadius); + //Ensure within the outside of the arc centre, but inside arc outer + }, + tooltipPosition : function(){ + var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2), + rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius; + return { + x : this.x + (Math.cos(centreAngle) * rangeFromCentre), + y : this.y + (Math.sin(centreAngle) * rangeFromCentre) + }; + }, + draw : function(animationPercent){ + + var easingDecimal = animationPercent || 1; + + var ctx = this.ctx; + + ctx.beginPath(); + + ctx.arc(this.x, this.y, this.outerRadius, this.startAngle, this.endAngle); + + ctx.arc(this.x, this.y, this.innerRadius, this.endAngle, this.startAngle, true); + + ctx.closePath(); + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + ctx.fillStyle = this.fillColor; + + ctx.fill(); + ctx.lineJoin = 'bevel'; + + if (this.showStroke){ + ctx.stroke(); + } + } + }); + + Chart.Rectangle = Chart.Element.extend({ + draw : function(){ + var ctx = this.ctx, + halfWidth = this.width/2, + leftX = this.x - halfWidth, + rightX = this.x + halfWidth, + top = this.base - (this.base - this.y), + halfStroke = this.strokeWidth / 2; + + // Canvas doesn't allow us to stroke inside the width so we can + // adjust the sizes to fit if we're setting a stroke on the line + if (this.showStroke){ + leftX += halfStroke; + rightX -= halfStroke; + top += halfStroke; + } + + ctx.beginPath(); + + ctx.fillStyle = this.fillColor; + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + // It'd be nice to keep this class totally generic to any rectangle + // and simply specify which border to miss out. + ctx.moveTo(leftX, this.base); + ctx.lineTo(leftX, top); + ctx.lineTo(rightX, top); + ctx.lineTo(rightX, this.base); + ctx.fill(); + if (this.showStroke){ + ctx.stroke(); + } + }, + height : function(){ + return this.base - this.y; + }, + inRange : function(chartX,chartY){ + return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base); + } + }); + + Chart.Tooltip = Chart.Element.extend({ + draw : function(){ + + var ctx = this.chart.ctx; + + ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); + + this.xAlign = "center"; + this.yAlign = "above"; + + //Distance between the actual element.y position and the start of the tooltip caret + var caretPadding = this.caretPadding = 2; + + var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding, + tooltipRectHeight = this.fontSize + 2*this.yPadding, + tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding; + + if (this.x + tooltipWidth/2 >this.chart.width){ + this.xAlign = "left"; + } else if (this.x - tooltipWidth/2 < 0){ + this.xAlign = "right"; + } + + if (this.y - tooltipHeight < 0){ + this.yAlign = "below"; + } + + + var tooltipX = this.x - tooltipWidth/2, + tooltipY = this.y - tooltipHeight; + + ctx.fillStyle = this.fillColor; + + // Custom Tooltips + if(this.custom){ + this.custom(this); + } + else{ + switch(this.yAlign) + { + case "above": + //Draw a caret above the x/y + ctx.beginPath(); + ctx.moveTo(this.x,this.y - caretPadding); + ctx.lineTo(this.x + this.caretHeight, this.y - (caretPadding + this.caretHeight)); + ctx.lineTo(this.x - this.caretHeight, this.y - (caretPadding + this.caretHeight)); + ctx.closePath(); + ctx.fill(); + break; + case "below": + tooltipY = this.y + caretPadding + this.caretHeight; + //Draw a caret below the x/y + ctx.beginPath(); + ctx.moveTo(this.x, this.y + caretPadding); + ctx.lineTo(this.x + this.caretHeight, this.y + caretPadding + this.caretHeight); + ctx.lineTo(this.x - this.caretHeight, this.y + caretPadding + this.caretHeight); + ctx.closePath(); + ctx.fill(); + break; + } + + switch(this.xAlign) + { + case "left": + tooltipX = this.x - tooltipWidth + (this.cornerRadius + this.caretHeight); + break; + case "right": + tooltipX = this.x - (this.cornerRadius + this.caretHeight); + break; + } + + drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius); + + ctx.fill(); + + ctx.fillStyle = this.textColor; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2); + } + } + }); + + Chart.MultiTooltip = Chart.Element.extend({ + initialize : function(){ + this.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); + + this.titleFont = fontString(this.titleFontSize,this.titleFontStyle,this.titleFontFamily); + + this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleFontSize *1.5; + + this.ctx.font = this.titleFont; + + var titleWidth = this.ctx.measureText(this.title).width, + //Label has a legend square as well so account for this. + labelWidth = longestText(this.ctx,this.font,this.labels) + this.fontSize + 3, + longestTextWidth = max([labelWidth,titleWidth]); + + this.width = longestTextWidth + (this.xPadding*2); + + + var halfHeight = this.height/2; + + //Check to ensure the height will fit on the canvas + if (this.y - halfHeight < 0 ){ + this.y = halfHeight; + } else if (this.y + halfHeight > this.chart.height){ + this.y = this.chart.height - halfHeight; + } + + //Decide whether to align left or right based on position on canvas + if (this.x > this.chart.width/2){ + this.x -= this.xOffset + this.width; + } else { + this.x += this.xOffset; + } + + + }, + getLineHeight : function(index){ + var baseLineHeight = this.y - (this.height/2) + this.yPadding, + afterTitleIndex = index-1; + + //If the index is zero, we're getting the title + if (index === 0){ + return baseLineHeight + this.titleFontSize/2; + } else{ + return baseLineHeight + ((this.fontSize*1.5*afterTitleIndex) + this.fontSize/2) + this.titleFontSize * 1.5; + } + + }, + draw : function(){ + // Custom Tooltips + if(this.custom){ + this.custom(this); + } + else{ + drawRoundedRectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerRadius); + var ctx = this.ctx; + ctx.fillStyle = this.fillColor; + ctx.fill(); + ctx.closePath(); + + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.fillStyle = this.titleTextColor; + ctx.font = this.titleFont; + + ctx.fillText(this.title,this.x + this.xPadding, this.getLineHeight(0)); + + ctx.font = this.font; + helpers.each(this.labels,function(label,index){ + ctx.fillStyle = this.textColor; + ctx.fillText(label,this.x + this.xPadding + this.fontSize + 3, this.getLineHeight(index + 1)); + + //A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas) + //ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + //Instead we'll make a white filled block to put the legendColour palette over. + + ctx.fillStyle = this.legendColorBackground; + ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + + ctx.fillStyle = this.legendColors[index].fill; + ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + + + },this); + } + } + }); + + Chart.Scale = Chart.Element.extend({ + initialize : function(){ + this.fit(); + }, + buildYLabels : function(){ + this.yLabels = []; + + var stepDecimalPlaces = getDecimalPlaces(this.stepValue); + + for (var i=0; i<=this.steps; i++){ + this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); + } + this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx,this.font,this.yLabels) : 0; + }, + addXLabel : function(label){ + this.xLabels.push(label); + this.valuesCount++; + this.fit(); + }, + removeXLabel : function(){ + this.xLabels.shift(); + this.valuesCount--; + this.fit(); + }, + // Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use + fit: function(){ + // First we need the width of the yLabels, assuming the xLabels aren't rotated + + // To do that we need the base line at the top and base of the chart, assuming there is no x label rotation + this.startPoint = (this.display) ? this.fontSize : 0; + this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels + + // Apply padding settings to the start and end point. + this.startPoint += this.padding; + this.endPoint -= this.padding; + + // Cache the starting height, so can determine if we need to recalculate the scale yAxis + var cachedHeight = this.endPoint - this.startPoint, + cachedYLabelWidth; + + // Build the current yLabels so we have an idea of what size they'll be to start + /* + * This sets what is returned from calculateScaleRange as static properties of this class: + * + this.steps; + this.stepValue; + this.min; + this.max; + * + */ + this.calculateYRange(cachedHeight); + + // With these properties set we can now build the array of yLabels + // and also the width of the largest yLabel + this.buildYLabels(); + + this.calculateXLabelRotation(); + + while((cachedHeight > this.endPoint - this.startPoint)){ + cachedHeight = this.endPoint - this.startPoint; + cachedYLabelWidth = this.yLabelWidth; + + this.calculateYRange(cachedHeight); + this.buildYLabels(); + + // Only go through the xLabel loop again if the yLabel width has changed + if (cachedYLabelWidth < this.yLabelWidth){ + this.calculateXLabelRotation(); + } + } + + }, + calculateXLabelRotation : function(){ + //Get the width of each grid by calculating the difference + //between x offsets between 0 and 1. + + this.ctx.font = this.font; + + var firstWidth = this.ctx.measureText(this.xLabels[0]).width, + lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width, + firstRotated, + lastRotated; + + + this.xScalePaddingRight = lastWidth/2 + 3; + this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth + 10) ? firstWidth/2 : this.yLabelWidth + 10; + + this.xLabelRotation = 0; + if (this.display){ + var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels), + cosRotation, + firstRotatedWidth; + this.xLabelWidth = originalLabelWidth; + //Allow 3 pixels x2 padding either side for label readability + var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6; + + //Max label rotate should be 90 - also act as a loop counter + while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){ + cosRotation = Math.cos(toRadians(this.xLabelRotation)); + + firstRotated = cosRotation * firstWidth; + lastRotated = cosRotation * lastWidth; + + // We're right aligning the text now. + if (firstRotated + this.fontSize / 2 > this.yLabelWidth + 8){ + this.xScalePaddingLeft = firstRotated + this.fontSize / 2; + } + this.xScalePaddingRight = this.fontSize/2; + + + this.xLabelRotation++; + this.xLabelWidth = cosRotation * originalLabelWidth; + + } + if (this.xLabelRotation > 0){ + this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3; + } + } + else{ + this.xLabelWidth = 0; + this.xScalePaddingRight = this.padding; + this.xScalePaddingLeft = this.padding; + } + + }, + // Needs to be overidden in each Chart type + // Otherwise we need to pass all the data into the scale class + calculateYRange: noop, + drawingArea: function(){ + return this.startPoint - this.endPoint; + }, + calculateY : function(value){ + var scalingFactor = this.drawingArea() / (this.min - this.max); + return this.endPoint - (scalingFactor * (value - this.min)); + }, + calculateX : function(index){ + var isRotated = (this.xLabelRotation > 0), + // innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding, + innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight), + valueWidth = innerWidth/Math.max((this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), 1), + valueOffset = (valueWidth * index) + this.xScalePaddingLeft; + + if (this.offsetGridLines){ + valueOffset += (valueWidth/2); + } + + return Math.round(valueOffset); + }, + update : function(newProps){ + helpers.extend(this, newProps); + this.fit(); + }, + draw : function(){ + var ctx = this.ctx, + yLabelGap = (this.endPoint - this.startPoint) / this.steps, + xStart = Math.round(this.xScalePaddingLeft); + if (this.display){ + ctx.fillStyle = this.textColor; + ctx.font = this.font; + each(this.yLabels,function(labelString,index){ + var yLabelCenter = this.endPoint - (yLabelGap * index), + linePositionY = Math.round(yLabelCenter), + drawHorizontalLine = this.showHorizontalLines; + + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + if (this.showLabels){ + ctx.fillText(labelString,xStart - 10,yLabelCenter); + } + + // This is X axis, so draw it + if (index === 0 && !drawHorizontalLine){ + drawHorizontalLine = true; + } + + if (drawHorizontalLine){ + ctx.beginPath(); + } + + if (index > 0){ + // This is a grid line in the centre, so drop that + ctx.lineWidth = this.gridLineWidth; + ctx.strokeStyle = this.gridLineColor; + } else { + // This is the first line on the scale + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + } + + linePositionY += helpers.aliasPixel(ctx.lineWidth); + + if(drawHorizontalLine){ + ctx.moveTo(xStart, linePositionY); + ctx.lineTo(this.width, linePositionY); + ctx.stroke(); + ctx.closePath(); + } + + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + ctx.beginPath(); + ctx.moveTo(xStart - 5, linePositionY); + ctx.lineTo(xStart, linePositionY); + ctx.stroke(); + ctx.closePath(); + + },this); + + each(this.xLabels,function(label,index){ + var xPos = this.calculateX(index) + aliasPixel(this.lineWidth), + // Check to see if line/bar here and decide where to place the line + linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth), + isRotated = (this.xLabelRotation > 0), + drawVerticalLine = this.showVerticalLines; + + // This is Y axis, so draw it + if (index === 0 && !drawVerticalLine){ + drawVerticalLine = true; + } + + if (drawVerticalLine){ + ctx.beginPath(); + } + + if (index > 0){ + // This is a grid line in the centre, so drop that + ctx.lineWidth = this.gridLineWidth; + ctx.strokeStyle = this.gridLineColor; + } else { + // This is the first line on the scale + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + } + + if (drawVerticalLine){ + ctx.moveTo(linePos,this.endPoint); + ctx.lineTo(linePos,this.startPoint - 3); + ctx.stroke(); + ctx.closePath(); + } + + + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + + + // Small lines at the bottom of the base grid line + ctx.beginPath(); + ctx.moveTo(linePos,this.endPoint); + ctx.lineTo(linePos,this.endPoint + 5); + ctx.stroke(); + ctx.closePath(); + + ctx.save(); + ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8); + ctx.rotate(toRadians(this.xLabelRotation)*-1); + ctx.font = this.font; + ctx.textAlign = (isRotated) ? "right" : "center"; + ctx.textBaseline = (isRotated) ? "middle" : "top"; + ctx.fillText(label, 0, 0); + ctx.restore(); + },this); + + } + } + + }); + + Chart.RadialScale = Chart.Element.extend({ + initialize: function(){ + this.size = min([this.height, this.width]); + this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); + }, + calculateCenterOffset: function(value){ + // Take into account half font size + the yPadding of the top value + var scalingFactor = this.drawingArea / (this.max - this.min); + + return (value - this.min) * scalingFactor; + }, + update : function(){ + if (!this.lineArc){ + this.setScaleSize(); + } else { + this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); + } + this.buildYLabels(); + }, + buildYLabels: function(){ + this.yLabels = []; + + var stepDecimalPlaces = getDecimalPlaces(this.stepValue); + + for (var i=0; i<=this.steps; i++){ + this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); + } + }, + getCircumference : function(){ + return ((Math.PI*2) / this.valuesCount); + }, + setScaleSize: function(){ + /* + * Right, this is really confusing and there is a lot of maths going on here + * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 + * + * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif + * + * Solution: + * + * We assume the radius of the polygon is half the size of the canvas at first + * at each index we check if the text overlaps. + * + * Where it does, we store that angle and that index. + * + * After finding the largest index and angle we calculate how much we need to remove + * from the shape radius to move the point inwards by that x. + * + * We average the left and right distances to get the maximum shape radius that can fit in the box + * along with labels. + * + * Once we have that, we can find the centre point for the chart, by taking the x text protrusion + * on each side, removing that from the size, halving it and adding the left x protrusion width. + * + * This will mean we have a shape fitted to the canvas, as large as it can be with the labels + * and position it in the most space efficient manner + * + * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif + */ + + + // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. + // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points + var largestPossibleRadius = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]), + pointPosition, + i, + textWidth, + halfTextWidth, + furthestRight = this.width, + furthestRightIndex, + furthestRightAngle, + furthestLeft = 0, + furthestLeftIndex, + furthestLeftAngle, + xProtrusionLeft, + xProtrusionRight, + radiusReductionRight, + radiusReductionLeft, + maxWidthRadius; + this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); + for (i=0;i furthestRight) { + furthestRight = pointPosition.x + halfTextWidth; + furthestRightIndex = i; + } + if (pointPosition.x - halfTextWidth < furthestLeft) { + furthestLeft = pointPosition.x - halfTextWidth; + furthestLeftIndex = i; + } + } + else if (i < this.valuesCount/2) { + // Less than half the values means we'll left align the text + if (pointPosition.x + textWidth > furthestRight) { + furthestRight = pointPosition.x + textWidth; + furthestRightIndex = i; + } + } + else if (i > this.valuesCount/2){ + // More than half the values means we'll right align the text + if (pointPosition.x - textWidth < furthestLeft) { + furthestLeft = pointPosition.x - textWidth; + furthestLeftIndex = i; + } + } + } + + xProtrusionLeft = furthestLeft; + + xProtrusionRight = Math.ceil(furthestRight - this.width); + + furthestRightAngle = this.getIndexAngle(furthestRightIndex); + + furthestLeftAngle = this.getIndexAngle(furthestLeftIndex); + + radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI/2); + + radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI/2); + + // Ensure we actually need to reduce the size of the chart + radiusReductionRight = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0; + radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; + + this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2; + + //this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2]) + this.setCenterPoint(radiusReductionLeft, radiusReductionRight); + + }, + setCenterPoint: function(leftMovement, rightMovement){ + + var maxRight = this.width - rightMovement - this.drawingArea, + maxLeft = leftMovement + this.drawingArea; + + this.xCenter = (maxLeft + maxRight)/2; + // Always vertically in the centre as the text height doesn't change + this.yCenter = (this.height/2); + }, + + getIndexAngle : function(index){ + var angleMultiplier = (Math.PI * 2) / this.valuesCount; + // Start from the top instead of right, so remove a quarter of the circle + + return index * angleMultiplier - (Math.PI/2); + }, + getPointPosition : function(index, distanceFromCenter){ + var thisAngle = this.getIndexAngle(index); + return { + x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter, + y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter + }; + }, + draw: function(){ + if (this.display){ + var ctx = this.ctx; + each(this.yLabels, function(label, index){ + // Don't draw a centre value + if (index > 0){ + var yCenterOffset = index * (this.drawingArea/this.steps), + yHeight = this.yCenter - yCenterOffset, + pointPosition; + + // Draw circular lines around the scale + if (this.lineWidth > 0){ + ctx.strokeStyle = this.lineColor; + ctx.lineWidth = this.lineWidth; + + if(this.lineArc){ + ctx.beginPath(); + ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2); + ctx.closePath(); + ctx.stroke(); + } else{ + ctx.beginPath(); + for (var i=0;i= 0; i--) { + if (this.angleLineWidth > 0){ + var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max)); + ctx.beginPath(); + ctx.moveTo(this.xCenter, this.yCenter); + ctx.lineTo(outerPosition.x, outerPosition.y); + ctx.stroke(); + ctx.closePath(); + } + // Extra 3px out for some label spacing + var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5); + ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); + ctx.fillStyle = this.pointLabelFontColor; + + var labelsCount = this.labels.length, + halfLabelsCount = this.labels.length/2, + quarterLabelsCount = halfLabelsCount/2, + upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount), + exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount); + if (i === 0){ + ctx.textAlign = 'center'; + } else if(i === halfLabelsCount){ + ctx.textAlign = 'center'; + } else if (i < halfLabelsCount){ + ctx.textAlign = 'left'; + } else { + ctx.textAlign = 'right'; + } + + // Set the correct text baseline based on outer positioning + if (exactQuarter){ + ctx.textBaseline = 'middle'; + } else if (upperHalf){ + ctx.textBaseline = 'bottom'; + } else { + ctx.textBaseline = 'top'; + } + + ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y); + } + } + } + } + }); + + // Attach global event to resize each chart instance when the browser resizes + helpers.addEvent(window, "resize", (function(){ + // Basic debounce of resize function so it doesn't hurt performance when resizing browser. + var timeout; + return function(){ + clearTimeout(timeout); + timeout = setTimeout(function(){ + each(Chart.instances,function(instance){ + // If the responsive flag is set in the chart instance config + // Cascade the resize event down to the chart. + if (instance.options.responsive){ + instance.resize(instance.render, true); + } + }); + }, 50); + }; + })()); + + + if (amd) { + define(function(){ + return Chart; + }); + } else if (typeof module === 'object' && module.exports) { + module.exports = Chart; + } + + root.Chart = Chart; + + Chart.noConflict = function(){ + root.Chart = previous; + return Chart; + }; + +}).call(this); + +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + + var defaultConfig = { + //Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value + scaleBeginAtZero : true, + + //Boolean - Whether grid lines are shown across the chart + scaleShowGridLines : true, + + //String - Colour of the grid lines + scaleGridLineColor : "rgba(0,0,0,.05)", + + //Number - Width of the grid lines + scaleGridLineWidth : 1, + + //Boolean - Whether to show horizontal lines (except X axis) + scaleShowHorizontalLines: true, + + //Boolean - Whether to show vertical lines (except Y axis) + scaleShowVerticalLines: true, + + //Boolean - If there is a stroke on each bar + barShowStroke : true, + + //Number - Pixel width of the bar stroke + barStrokeWidth : 2, + + //Number - Spacing between each of the X value sets + barValueSpacing : 5, + + //Number - Spacing between data sets within X values + barDatasetSpacing : 1, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" + + }; + + + Chart.Type.extend({ + name: "Bar", + defaults : defaultConfig, + initialize: function(data){ + + //Expose options as a scope variable here so we can access it in the ScaleClass + var options = this.options; + + this.ScaleClass = Chart.Scale.extend({ + offsetGridLines : true, + calculateBarX : function(datasetCount, datasetIndex, barIndex){ + //Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar + var xWidth = this.calculateBaseWidth(), + xAbsolute = this.calculateX(barIndex) - (xWidth/2), + barWidth = this.calculateBarWidth(datasetCount); + + return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2; + }, + calculateBaseWidth : function(){ + return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing); + }, + calculateBarWidth : function(datasetCount){ + //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset + var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing); + + return (baseWidth / datasetCount); + } + }); + + this.datasets = []; + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : []; + + this.eachBars(function(bar){ + bar.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activeBars, function(activeBar){ + activeBar.fillColor = activeBar.highlightFill; + activeBar.strokeColor = activeBar.highlightStroke; + }); + this.showTooltip(activeBars); + }); + } + + //Declare the extension of the default point, to cater for the options passed in to the constructor + this.BarClass = Chart.Rectangle.extend({ + strokeWidth : this.options.barStrokeWidth, + showStroke : this.options.barShowStroke, + ctx : this.chart.ctx + }); + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset,datasetIndex){ + + var datasetObject = { + label : dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + bars : [] + }; + + this.datasets.push(datasetObject); + + helpers.each(dataset.data,function(dataPoint,index){ + //Add a new point for each piece of data, passing any required data to draw. + datasetObject.bars.push(new this.BarClass({ + value : dataPoint, + label : data.labels[index], + datasetLabel: dataset.label, + strokeColor : dataset.strokeColor, + fillColor : dataset.fillColor, + highlightFill : dataset.highlightFill || dataset.fillColor, + highlightStroke : dataset.highlightStroke || dataset.strokeColor + })); + },this); + + },this); + + this.buildScale(data.labels); + + this.BarClass.prototype.base = this.scale.endPoint; + + this.eachBars(function(bar, index, datasetIndex){ + helpers.extend(bar, { + width : this.scale.calculateBarWidth(this.datasets.length), + x: this.scale.calculateBarX(this.datasets.length, datasetIndex, index), + y: this.scale.endPoint + }); + bar.save(); + }, this); + + this.render(); + }, + update : function(){ + this.scale.update(); + // Reset any highlight colours before updating. + helpers.each(this.activeElements, function(activeElement){ + activeElement.restore(['fillColor', 'strokeColor']); + }); + + this.eachBars(function(bar){ + bar.save(); + }); + this.render(); + }, + eachBars : function(callback){ + helpers.each(this.datasets,function(dataset, datasetIndex){ + helpers.each(dataset.bars, callback, this, datasetIndex); + },this); + }, + getBarsAtEvent : function(e){ + var barsArray = [], + eventPosition = helpers.getRelativePosition(e), + datasetIterator = function(dataset){ + barsArray.push(dataset.bars[barIndex]); + }, + barIndex; + + for (var datasetIndex = 0; datasetIndex < this.datasets.length; datasetIndex++) { + for (barIndex = 0; barIndex < this.datasets[datasetIndex].bars.length; barIndex++) { + if (this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){ + helpers.each(this.datasets, datasetIterator); + return barsArray; + } + } + } + + return barsArray; + }, + buildScale : function(labels){ + var self = this; + + var dataTotal = function(){ + var values = []; + self.eachBars(function(bar){ + values.push(bar.value); + }); + return values; + }; + + var scaleOptions = { + templateString : this.options.scaleLabel, + height : this.chart.height, + width : this.chart.width, + ctx : this.chart.ctx, + textColor : this.options.scaleFontColor, + fontSize : this.options.scaleFontSize, + fontStyle : this.options.scaleFontStyle, + fontFamily : this.options.scaleFontFamily, + valuesCount : labels.length, + beginAtZero : this.options.scaleBeginAtZero, + integersOnly : this.options.scaleIntegersOnly, + calculateYRange: function(currentHeight){ + var updatedRanges = helpers.calculateScaleRange( + dataTotal(), + currentHeight, + this.fontSize, + this.beginAtZero, + this.integersOnly + ); + helpers.extend(this, updatedRanges); + }, + xLabels : labels, + font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), + lineWidth : this.options.scaleLineWidth, + lineColor : this.options.scaleLineColor, + showHorizontalLines : this.options.scaleShowHorizontalLines, + showVerticalLines : this.options.scaleShowVerticalLines, + gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, + gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", + padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0, + showLabels : this.options.scaleShowLabels, + display : this.options.showScale + }; + + if (this.options.scaleOverride){ + helpers.extend(scaleOptions, { + calculateYRange: helpers.noop, + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + }); + } + + this.scale = new this.ScaleClass(scaleOptions); + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + helpers.each(valuesArray,function(value,datasetIndex){ + //Add a new point for each piece of data, passing any required data to draw. + this.datasets[datasetIndex].bars.push(new this.BarClass({ + value : value, + label : label, + x: this.scale.calculateBarX(this.datasets.length, datasetIndex, this.scale.valuesCount+1), + y: this.scale.endPoint, + width : this.scale.calculateBarWidth(this.datasets.length), + base : this.scale.endPoint, + strokeColor : this.datasets[datasetIndex].strokeColor, + fillColor : this.datasets[datasetIndex].fillColor + })); + },this); + + this.scale.addXLabel(label); + //Then re-render the chart. + this.update(); + }, + removeData : function(){ + this.scale.removeXLabel(); + //Then re-render the chart. + helpers.each(this.datasets,function(dataset){ + dataset.bars.shift(); + },this); + this.update(); + }, + reflow : function(){ + helpers.extend(this.BarClass.prototype,{ + y: this.scale.endPoint, + base : this.scale.endPoint + }); + var newScaleProps = helpers.extend({ + height : this.chart.height, + width : this.chart.width + }); + this.scale.update(newScaleProps); + }, + draw : function(ease){ + var easingDecimal = ease || 1; + this.clear(); + + var ctx = this.chart.ctx; + + this.scale.draw(easingDecimal); + + //Draw all the bars for each dataset + helpers.each(this.datasets,function(dataset,datasetIndex){ + helpers.each(dataset.bars,function(bar,index){ + if (bar.hasValue()){ + bar.base = this.scale.endPoint; + //Transition then draw + bar.transition({ + x : this.scale.calculateBarX(this.datasets.length, datasetIndex, index), + y : this.scale.calculateY(bar.value), + width : this.scale.calculateBarWidth(this.datasets.length) + }, easingDecimal).draw(); + } + },this); + + },this); + } + }); + + +}).call(this); + +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + //Cache a local reference to Chart.helpers + helpers = Chart.helpers; + + var defaultConfig = { + //Boolean - Whether we should show a stroke on each segment + segmentShowStroke : true, + + //String - The colour of each segment stroke + segmentStrokeColor : "#fff", + + //Number - The width of each segment stroke + segmentStrokeWidth : 2, + + //The percentage of the chart that we cut out of the middle. + percentageInnerCutout : 50, + + //Number - Amount of animation steps + animationSteps : 100, + + //String - Animation easing effect + animationEasing : "easeOutBounce", + + //Boolean - Whether we animate the rotation of the Doughnut + animateRotate : true, + + //Boolean - Whether we animate scaling the Doughnut from the centre + animateScale : false, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" + + }; + + + Chart.Type.extend({ + //Passing in a name registers this chart in the Chart namespace + name: "Doughnut", + //Providing a defaults will also register the deafults in the chart namespace + defaults : defaultConfig, + //Initialize is fired when the chart is initialized - Data is passed in as a parameter + //Config is automatically merged by the core of Chart.js, and is available at this.options + initialize: function(data){ + + //Declare segments as a static property to prevent inheriting across the Chart type prototype + this.segments = []; + this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; + + this.SegmentArc = Chart.Arc.extend({ + ctx : this.chart.ctx, + x : this.chart.width/2, + y : this.chart.height/2 + }); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; + + helpers.each(this.segments,function(segment){ + segment.restore(["fillColor"]); + }); + helpers.each(activeSegments,function(activeSegment){ + activeSegment.fillColor = activeSegment.highlightColor; + }); + this.showTooltip(activeSegments); + }); + } + this.calculateTotal(data); + + helpers.each(data,function(datapoint, index){ + this.addData(datapoint, index, true); + },this); + + this.render(); + }, + getSegmentsAtEvent : function(e){ + var segmentsArray = []; + + var location = helpers.getRelativePosition(e); + + helpers.each(this.segments,function(segment){ + if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); + },this); + return segmentsArray; + }, + addData : function(segment, atIndex, silent){ + var index = atIndex || this.segments.length; + this.segments.splice(index, 0, new this.SegmentArc({ + value : segment.value, + outerRadius : (this.options.animateScale) ? 0 : this.outerRadius, + innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout, + fillColor : segment.color, + highlightColor : segment.highlight || segment.color, + showStroke : this.options.segmentShowStroke, + strokeWidth : this.options.segmentStrokeWidth, + strokeColor : this.options.segmentStrokeColor, + startAngle : Math.PI * 1.5, + circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value), + label : segment.label + })); + if (!silent){ + this.reflow(); + this.update(); + } + }, + calculateCircumference : function(value){ + return (Math.PI*2)*(Math.abs(value) / this.total); + }, + calculateTotal : function(data){ + this.total = 0; + helpers.each(data,function(segment){ + this.total += Math.abs(segment.value); + },this); + }, + update : function(){ + this.calculateTotal(this.segments); + + // Reset any highlight colours before updating. + helpers.each(this.activeElements, function(activeElement){ + activeElement.restore(['fillColor']); + }); + + helpers.each(this.segments,function(segment){ + segment.save(); + }); + this.render(); + }, + + removeData: function(atIndex){ + var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; + this.segments.splice(indexToDelete, 1); + this.reflow(); + this.update(); + }, + + reflow : function(){ + helpers.extend(this.SegmentArc.prototype,{ + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; + helpers.each(this.segments, function(segment){ + segment.update({ + outerRadius : this.outerRadius, + innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout + }); + }, this); + }, + draw : function(easeDecimal){ + var animDecimal = (easeDecimal) ? easeDecimal : 1; + this.clear(); + helpers.each(this.segments,function(segment,index){ + segment.transition({ + circumference : this.calculateCircumference(segment.value), + outerRadius : this.outerRadius, + innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout + },animDecimal); + + segment.endAngle = segment.startAngle + segment.circumference; + + segment.draw(); + if (index === 0){ + segment.startAngle = Math.PI * 1.5; + } + //Check to see if it's the last segment, if not get the next and update the start angle + if (index < this.segments.length-1){ + this.segments[index+1].startAngle = segment.endAngle; + } + },this); + + } + }); + + Chart.types.Doughnut.extend({ + name : "Pie", + defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0}) + }); + +}).call(this); +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + var defaultConfig = { + + ///Boolean - Whether grid lines are shown across the chart + scaleShowGridLines : true, + + //String - Colour of the grid lines + scaleGridLineColor : "rgba(0,0,0,.05)", + + //Number - Width of the grid lines + scaleGridLineWidth : 1, + + //Boolean - Whether to show horizontal lines (except X axis) + scaleShowHorizontalLines: true, + + //Boolean - Whether to show vertical lines (except Y axis) + scaleShowVerticalLines: true, + + //Boolean - Whether the line is curved between points + bezierCurve : true, + + //Number - Tension of the bezier curve between points + bezierCurveTension : 0.4, + + //Boolean - Whether to show a dot for each point + pointDot : true, + + //Number - Radius of each point dot in pixels + pointDotRadius : 4, + + //Number - Pixel width of point dot stroke + pointDotStrokeWidth : 1, + + //Number - amount extra to add to the radius to cater for hit detection outside the drawn point + pointHitDetectionRadius : 20, + + //Boolean - Whether to show a stroke for datasets + datasetStroke : true, + + //Number - Pixel width of dataset stroke + datasetStrokeWidth : 2, + + //Boolean - Whether to fill the dataset with a colour + datasetFill : true, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" + + }; + + + Chart.Type.extend({ + name: "Line", + defaults : defaultConfig, + initialize: function(data){ + //Declare the extension of the default point, to cater for the options passed in to the constructor + this.PointClass = Chart.Point.extend({ + strokeWidth : this.options.pointDotStrokeWidth, + radius : this.options.pointDotRadius, + display: this.options.pointDot, + hitDetectionRadius : this.options.pointHitDetectionRadius, + ctx : this.chart.ctx, + inRange : function(mouseX){ + return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2)); + } + }); + + this.datasets = []; + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; + this.eachPoints(function(point){ + point.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activePoints, function(activePoint){ + activePoint.fillColor = activePoint.highlightFill; + activePoint.strokeColor = activePoint.highlightStroke; + }); + this.showTooltip(activePoints); + }); + } + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset){ + + var datasetObject = { + label : dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + pointColor : dataset.pointColor, + pointStrokeColor : dataset.pointStrokeColor, + points : [] + }; + + this.datasets.push(datasetObject); + + + helpers.each(dataset.data,function(dataPoint,index){ + //Add a new point for each piece of data, passing any required data to draw. + datasetObject.points.push(new this.PointClass({ + value : dataPoint, + label : data.labels[index], + datasetLabel: dataset.label, + strokeColor : dataset.pointStrokeColor, + fillColor : dataset.pointColor, + highlightFill : dataset.pointHighlightFill || dataset.pointColor, + highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor + })); + },this); + + this.buildScale(data.labels); + + + this.eachPoints(function(point, index){ + helpers.extend(point, { + x: this.scale.calculateX(index), + y: this.scale.endPoint + }); + point.save(); + }, this); + + },this); + + + this.render(); + }, + update : function(){ + this.scale.update(); + // Reset any highlight colours before updating. + helpers.each(this.activeElements, function(activeElement){ + activeElement.restore(['fillColor', 'strokeColor']); + }); + this.eachPoints(function(point){ + point.save(); + }); + this.render(); + }, + eachPoints : function(callback){ + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,callback,this); + },this); + }, + getPointsAtEvent : function(e){ + var pointsArray = [], + eventPosition = helpers.getRelativePosition(e); + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,function(point){ + if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point); + }); + },this); + return pointsArray; + }, + buildScale : function(labels){ + var self = this; + + var dataTotal = function(){ + var values = []; + self.eachPoints(function(point){ + values.push(point.value); + }); + + return values; + }; + + var scaleOptions = { + templateString : this.options.scaleLabel, + height : this.chart.height, + width : this.chart.width, + ctx : this.chart.ctx, + textColor : this.options.scaleFontColor, + fontSize : this.options.scaleFontSize, + fontStyle : this.options.scaleFontStyle, + fontFamily : this.options.scaleFontFamily, + valuesCount : labels.length, + beginAtZero : this.options.scaleBeginAtZero, + integersOnly : this.options.scaleIntegersOnly, + calculateYRange : function(currentHeight){ + var updatedRanges = helpers.calculateScaleRange( + dataTotal(), + currentHeight, + this.fontSize, + this.beginAtZero, + this.integersOnly + ); + helpers.extend(this, updatedRanges); + }, + xLabels : labels, + font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), + lineWidth : this.options.scaleLineWidth, + lineColor : this.options.scaleLineColor, + showHorizontalLines : this.options.scaleShowHorizontalLines, + showVerticalLines : this.options.scaleShowVerticalLines, + gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, + gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", + padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth, + showLabels : this.options.scaleShowLabels, + display : this.options.showScale + }; + + if (this.options.scaleOverride){ + helpers.extend(scaleOptions, { + calculateYRange: helpers.noop, + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + }); + } + + + this.scale = new Chart.Scale(scaleOptions); + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + + helpers.each(valuesArray,function(value,datasetIndex){ + //Add a new point for each piece of data, passing any required data to draw. + this.datasets[datasetIndex].points.push(new this.PointClass({ + value : value, + label : label, + x: this.scale.calculateX(this.scale.valuesCount+1), + y: this.scale.endPoint, + strokeColor : this.datasets[datasetIndex].pointStrokeColor, + fillColor : this.datasets[datasetIndex].pointColor + })); + },this); + + this.scale.addXLabel(label); + //Then re-render the chart. + this.update(); + }, + removeData : function(){ + this.scale.removeXLabel(); + //Then re-render the chart. + helpers.each(this.datasets,function(dataset){ + dataset.points.shift(); + },this); + this.update(); + }, + reflow : function(){ + var newScaleProps = helpers.extend({ + height : this.chart.height, + width : this.chart.width + }); + this.scale.update(newScaleProps); + }, + draw : function(ease){ + var easingDecimal = ease || 1; + this.clear(); + + var ctx = this.chart.ctx; + + // Some helper methods for getting the next/prev points + var hasValue = function(item){ + return item.value !== null; + }, + nextPoint = function(point, collection, index){ + return helpers.findNextWhere(collection, hasValue, index) || point; + }, + previousPoint = function(point, collection, index){ + return helpers.findPreviousWhere(collection, hasValue, index) || point; + }; + + this.scale.draw(easingDecimal); + + + helpers.each(this.datasets,function(dataset){ + var pointsWithValues = helpers.where(dataset.points, hasValue); + + //Transition each point first so that the line and point drawing isn't out of sync + //We can use this extra loop to calculate the control points of this dataset also in this loop + + helpers.each(dataset.points, function(point, index){ + if (point.hasValue()){ + point.transition({ + y : this.scale.calculateY(point.value), + x : this.scale.calculateX(index) + }, easingDecimal); + } + },this); + + + // Control points need to be calculated in a seperate loop, because we need to know the current x/y of the point + // This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed + if (this.options.bezierCurve){ + helpers.each(pointsWithValues, function(point, index){ + var tension = (index > 0 && index < pointsWithValues.length - 1) ? this.options.bezierCurveTension : 0; + point.controlPoints = helpers.splineCurve( + previousPoint(point, pointsWithValues, index), + point, + nextPoint(point, pointsWithValues, index), + tension + ); + + // Prevent the bezier going outside of the bounds of the graph + + // Cap puter bezier handles to the upper/lower scale bounds + if (point.controlPoints.outer.y > this.scale.endPoint){ + point.controlPoints.outer.y = this.scale.endPoint; + } + else if (point.controlPoints.outer.y < this.scale.startPoint){ + point.controlPoints.outer.y = this.scale.startPoint; + } + + // Cap inner bezier handles to the upper/lower scale bounds + if (point.controlPoints.inner.y > this.scale.endPoint){ + point.controlPoints.inner.y = this.scale.endPoint; + } + else if (point.controlPoints.inner.y < this.scale.startPoint){ + point.controlPoints.inner.y = this.scale.startPoint; + } + },this); + } + + + //Draw the line between all the points + ctx.lineWidth = this.options.datasetStrokeWidth; + ctx.strokeStyle = dataset.strokeColor; + ctx.beginPath(); + + helpers.each(pointsWithValues, function(point, index){ + if (index === 0){ + ctx.moveTo(point.x, point.y); + } + else{ + if(this.options.bezierCurve){ + var previous = previousPoint(point, pointsWithValues, index); + + ctx.bezierCurveTo( + previous.controlPoints.outer.x, + previous.controlPoints.outer.y, + point.controlPoints.inner.x, + point.controlPoints.inner.y, + point.x, + point.y + ); + } + else{ + ctx.lineTo(point.x,point.y); + } + } + }, this); + + ctx.stroke(); + + if (this.options.datasetFill && pointsWithValues.length > 0){ + //Round off the line by going to the base of the chart, back to the start, then fill. + ctx.lineTo(pointsWithValues[pointsWithValues.length - 1].x, this.scale.endPoint); + ctx.lineTo(pointsWithValues[0].x, this.scale.endPoint); + ctx.fillStyle = dataset.fillColor; + ctx.closePath(); + ctx.fill(); + } + + //Now draw the points over the line + //A little inefficient double looping, but better than the line + //lagging behind the point positions + helpers.each(pointsWithValues,function(point){ + point.draw(); + }); + },this); + } + }); + + +}).call(this); + +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + //Cache a local reference to Chart.helpers + helpers = Chart.helpers; + + var defaultConfig = { + //Boolean - Show a backdrop to the scale label + scaleShowLabelBackdrop : true, + + //String - The colour of the label backdrop + scaleBackdropColor : "rgba(255,255,255,0.75)", + + // Boolean - Whether the scale should begin at zero + scaleBeginAtZero : true, + + //Number - The backdrop padding above & below the label in pixels + scaleBackdropPaddingY : 2, + + //Number - The backdrop padding to the side of the label in pixels + scaleBackdropPaddingX : 2, + + //Boolean - Show line for each value in the scale + scaleShowLine : true, + + //Boolean - Stroke a line around each segment in the chart + segmentShowStroke : true, + + //String - The colour of the stroke on each segement. + segmentStrokeColor : "#fff", + + //Number - The width of the stroke value in pixels + segmentStrokeWidth : 2, + + //Number - Amount of animation steps + animationSteps : 100, + + //String - Animation easing effect. + animationEasing : "easeOutBounce", + + //Boolean - Whether to animate the rotation of the chart + animateRotate : true, + + //Boolean - Whether to animate scaling the chart from the centre + animateScale : false, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" + }; + + + Chart.Type.extend({ + //Passing in a name registers this chart in the Chart namespace + name: "PolarArea", + //Providing a defaults will also register the deafults in the chart namespace + defaults : defaultConfig, + //Initialize is fired when the chart is initialized - Data is passed in as a parameter + //Config is automatically merged by the core of Chart.js, and is available at this.options + initialize: function(data){ + this.segments = []; + //Declare segment class as a chart instance specific class, so it can share props for this instance + this.SegmentArc = Chart.Arc.extend({ + showStroke : this.options.segmentShowStroke, + strokeWidth : this.options.segmentStrokeWidth, + strokeColor : this.options.segmentStrokeColor, + ctx : this.chart.ctx, + innerRadius : 0, + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.scale = new Chart.RadialScale({ + display: this.options.showScale, + fontStyle: this.options.scaleFontStyle, + fontSize: this.options.scaleFontSize, + fontFamily: this.options.scaleFontFamily, + fontColor: this.options.scaleFontColor, + showLabels: this.options.scaleShowLabels, + showLabelBackdrop: this.options.scaleShowLabelBackdrop, + backdropColor: this.options.scaleBackdropColor, + backdropPaddingY : this.options.scaleBackdropPaddingY, + backdropPaddingX: this.options.scaleBackdropPaddingX, + lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, + lineColor: this.options.scaleLineColor, + lineArc: true, + width: this.chart.width, + height: this.chart.height, + xCenter: this.chart.width/2, + yCenter: this.chart.height/2, + ctx : this.chart.ctx, + templateString: this.options.scaleLabel, + valuesCount: data.length + }); + + this.updateScaleRange(data); + + this.scale.update(); + + helpers.each(data,function(segment,index){ + this.addData(segment,index,true); + },this); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; + helpers.each(this.segments,function(segment){ + segment.restore(["fillColor"]); + }); + helpers.each(activeSegments,function(activeSegment){ + activeSegment.fillColor = activeSegment.highlightColor; + }); + this.showTooltip(activeSegments); + }); + } + + this.render(); + }, + getSegmentsAtEvent : function(e){ + var segmentsArray = []; + + var location = helpers.getRelativePosition(e); + + helpers.each(this.segments,function(segment){ + if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); + },this); + return segmentsArray; + }, + addData : function(segment, atIndex, silent){ + var index = atIndex || this.segments.length; + + this.segments.splice(index, 0, new this.SegmentArc({ + fillColor: segment.color, + highlightColor: segment.highlight || segment.color, + label: segment.label, + value: segment.value, + outerRadius: (this.options.animateScale) ? 0 : this.scale.calculateCenterOffset(segment.value), + circumference: (this.options.animateRotate) ? 0 : this.scale.getCircumference(), + startAngle: Math.PI * 1.5 + })); + if (!silent){ + this.reflow(); + this.update(); + } + }, + removeData: function(atIndex){ + var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; + this.segments.splice(indexToDelete, 1); + this.reflow(); + this.update(); + }, + calculateTotal: function(data){ + this.total = 0; + helpers.each(data,function(segment){ + this.total += segment.value; + },this); + this.scale.valuesCount = this.segments.length; + }, + updateScaleRange: function(datapoints){ + var valuesArray = []; + helpers.each(datapoints,function(segment){ + valuesArray.push(segment.value); + }); + + var scaleSizes = (this.options.scaleOverride) ? + { + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + } : + helpers.calculateScaleRange( + valuesArray, + helpers.min([this.chart.width, this.chart.height])/2, + this.options.scaleFontSize, + this.options.scaleBeginAtZero, + this.options.scaleIntegersOnly + ); + + helpers.extend( + this.scale, + scaleSizes, + { + size: helpers.min([this.chart.width, this.chart.height]), + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + } + ); + + }, + update : function(){ + this.calculateTotal(this.segments); + + helpers.each(this.segments,function(segment){ + segment.save(); + }); + + this.reflow(); + this.render(); + }, + reflow : function(){ + helpers.extend(this.SegmentArc.prototype,{ + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.updateScaleRange(this.segments); + this.scale.update(); + + helpers.extend(this.scale,{ + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + }); + + helpers.each(this.segments, function(segment){ + segment.update({ + outerRadius : this.scale.calculateCenterOffset(segment.value) + }); + }, this); + + }, + draw : function(ease){ + var easingDecimal = ease || 1; + //Clear & draw the canvas + this.clear(); + helpers.each(this.segments,function(segment, index){ + segment.transition({ + circumference : this.scale.getCircumference(), + outerRadius : this.scale.calculateCenterOffset(segment.value) + },easingDecimal); + + segment.endAngle = segment.startAngle + segment.circumference; + + // If we've removed the first segment we need to set the first one to + // start at the top. + if (index === 0){ + segment.startAngle = Math.PI * 1.5; + } + + //Check to see if it's the last segment, if not get the next and update the start angle + if (index < this.segments.length - 1){ + this.segments[index+1].startAngle = segment.endAngle; + } + segment.draw(); + }, this); + this.scale.draw(); + } + }); + +}).call(this); +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + + + Chart.Type.extend({ + name: "Radar", + defaults:{ + //Boolean - Whether to show lines for each scale point + scaleShowLine : true, + + //Boolean - Whether we show the angle lines out of the radar + angleShowLineOut : true, + + //Boolean - Whether to show labels on the scale + scaleShowLabels : false, + + // Boolean - Whether the scale should begin at zero + scaleBeginAtZero : true, + + //String - Colour of the angle line + angleLineColor : "rgba(0,0,0,.1)", + + //Number - Pixel width of the angle line + angleLineWidth : 1, + + //String - Point label font declaration + pointLabelFontFamily : "'Arial'", + + //String - Point label font weight + pointLabelFontStyle : "normal", + + //Number - Point label font size in pixels + pointLabelFontSize : 10, + + //String - Point label font colour + pointLabelFontColor : "#666", + + //Boolean - Whether to show a dot for each point + pointDot : true, + + //Number - Radius of each point dot in pixels + pointDotRadius : 3, + + //Number - Pixel width of point dot stroke + pointDotStrokeWidth : 1, + + //Number - amount extra to add to the radius to cater for hit detection outside the drawn point + pointHitDetectionRadius : 20, + + //Boolean - Whether to show a stroke for datasets + datasetStroke : true, + + //Number - Pixel width of dataset stroke + datasetStrokeWidth : 2, + + //Boolean - Whether to fill the dataset with a colour + datasetFill : true, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" + + }, + + initialize: function(data){ + this.PointClass = Chart.Point.extend({ + strokeWidth : this.options.pointDotStrokeWidth, + radius : this.options.pointDotRadius, + display: this.options.pointDot, + hitDetectionRadius : this.options.pointHitDetectionRadius, + ctx : this.chart.ctx + }); + + this.datasets = []; + + this.buildScale(data); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; + + this.eachPoints(function(point){ + point.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activePointsCollection, function(activePoint){ + activePoint.fillColor = activePoint.highlightFill; + activePoint.strokeColor = activePoint.highlightStroke; + }); + + this.showTooltip(activePointsCollection); + }); + } + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset){ + + var datasetObject = { + label: dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + pointColor : dataset.pointColor, + pointStrokeColor : dataset.pointStrokeColor, + points : [] + }; + + this.datasets.push(datasetObject); + + helpers.each(dataset.data,function(dataPoint,index){ + //Add a new point for each piece of data, passing any required data to draw. + var pointPosition; + if (!this.scale.animation){ + pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(dataPoint)); + } + datasetObject.points.push(new this.PointClass({ + value : dataPoint, + label : data.labels[index], + datasetLabel: dataset.label, + x: (this.options.animation) ? this.scale.xCenter : pointPosition.x, + y: (this.options.animation) ? this.scale.yCenter : pointPosition.y, + strokeColor : dataset.pointStrokeColor, + fillColor : dataset.pointColor, + highlightFill : dataset.pointHighlightFill || dataset.pointColor, + highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor + })); + },this); + + },this); + + this.render(); + }, + eachPoints : function(callback){ + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,callback,this); + },this); + }, + + getPointsAtEvent : function(evt){ + var mousePosition = helpers.getRelativePosition(evt), + fromCenter = helpers.getAngleFromPoint({ + x: this.scale.xCenter, + y: this.scale.yCenter + }, mousePosition); + + var anglePerIndex = (Math.PI * 2) /this.scale.valuesCount, + pointIndex = Math.round((fromCenter.angle - Math.PI * 1.5) / anglePerIndex), + activePointsCollection = []; + + // If we're at the top, make the pointIndex 0 to get the first of the array. + if (pointIndex >= this.scale.valuesCount || pointIndex < 0){ + pointIndex = 0; + } + + if (fromCenter.distance <= this.scale.drawingArea){ + helpers.each(this.datasets, function(dataset){ + activePointsCollection.push(dataset.points[pointIndex]); + }); + } + + return activePointsCollection; + }, + + buildScale : function(data){ + this.scale = new Chart.RadialScale({ + display: this.options.showScale, + fontStyle: this.options.scaleFontStyle, + fontSize: this.options.scaleFontSize, + fontFamily: this.options.scaleFontFamily, + fontColor: this.options.scaleFontColor, + showLabels: this.options.scaleShowLabels, + showLabelBackdrop: this.options.scaleShowLabelBackdrop, + backdropColor: this.options.scaleBackdropColor, + backdropPaddingY : this.options.scaleBackdropPaddingY, + backdropPaddingX: this.options.scaleBackdropPaddingX, + lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, + lineColor: this.options.scaleLineColor, + angleLineColor : this.options.angleLineColor, + angleLineWidth : (this.options.angleShowLineOut) ? this.options.angleLineWidth : 0, + // Point labels at the edge of each line + pointLabelFontColor : this.options.pointLabelFontColor, + pointLabelFontSize : this.options.pointLabelFontSize, + pointLabelFontFamily : this.options.pointLabelFontFamily, + pointLabelFontStyle : this.options.pointLabelFontStyle, + height : this.chart.height, + width: this.chart.width, + xCenter: this.chart.width/2, + yCenter: this.chart.height/2, + ctx : this.chart.ctx, + templateString: this.options.scaleLabel, + labels: data.labels, + valuesCount: data.datasets[0].data.length + }); + + this.scale.setScaleSize(); + this.updateScaleRange(data.datasets); + this.scale.buildYLabels(); + }, + updateScaleRange: function(datasets){ + var valuesArray = (function(){ + var totalDataArray = []; + helpers.each(datasets,function(dataset){ + if (dataset.data){ + totalDataArray = totalDataArray.concat(dataset.data); + } + else { + helpers.each(dataset.points, function(point){ + totalDataArray.push(point.value); + }); + } + }); + return totalDataArray; + })(); + + + var scaleSizes = (this.options.scaleOverride) ? + { + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + } : + helpers.calculateScaleRange( + valuesArray, + helpers.min([this.chart.width, this.chart.height])/2, + this.options.scaleFontSize, + this.options.scaleBeginAtZero, + this.options.scaleIntegersOnly + ); + + helpers.extend( + this.scale, + scaleSizes + ); + + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + this.scale.valuesCount++; + helpers.each(valuesArray,function(value,datasetIndex){ + var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value)); + this.datasets[datasetIndex].points.push(new this.PointClass({ + value : value, + label : label, + x: pointPosition.x, + y: pointPosition.y, + strokeColor : this.datasets[datasetIndex].pointStrokeColor, + fillColor : this.datasets[datasetIndex].pointColor + })); + },this); + + this.scale.labels.push(label); + + this.reflow(); + + this.update(); + }, + removeData : function(){ + this.scale.valuesCount--; + this.scale.labels.shift(); + helpers.each(this.datasets,function(dataset){ + dataset.points.shift(); + },this); + this.reflow(); + this.update(); + }, + update : function(){ + this.eachPoints(function(point){ + point.save(); + }); + this.reflow(); + this.render(); + }, + reflow: function(){ + helpers.extend(this.scale, { + width : this.chart.width, + height: this.chart.height, + size : helpers.min([this.chart.width, this.chart.height]), + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + }); + this.updateScaleRange(this.datasets); + this.scale.setScaleSize(); + this.scale.buildYLabels(); + }, + draw : function(ease){ + var easeDecimal = ease || 1, + ctx = this.chart.ctx; + this.clear(); + this.scale.draw(); + + helpers.each(this.datasets,function(dataset){ + + //Transition each point first so that the line and point drawing isn't out of sync + helpers.each(dataset.points,function(point,index){ + if (point.hasValue()){ + point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal); + } + },this); + + + + //Draw the line between all the points + ctx.lineWidth = this.options.datasetStrokeWidth; + ctx.strokeStyle = dataset.strokeColor; + ctx.beginPath(); + helpers.each(dataset.points,function(point,index){ + if (index === 0){ + ctx.moveTo(point.x,point.y); + } + else{ + ctx.lineTo(point.x,point.y); + } + },this); + ctx.closePath(); + ctx.stroke(); + + ctx.fillStyle = dataset.fillColor; + ctx.fill(); + + //Now draw the points over the line + //A little inefficient double looping, but better than the line + //lagging behind the point positions + helpers.each(dataset.points,function(point){ + if (point.hasValue()){ + point.draw(); + } + }); + + },this); + + } + + }); + + + + + +}).call(this); \ No newline at end of file diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/Chart.min.js b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/Chart.min.js new file mode 100644 index 0000000000..3a0a2c8734 --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/Chart.min.js @@ -0,0 +1,11 @@ +/*! + * Chart.js + * http://chartjs.org/ + * Version: 1.0.2 + * + * Copyright 2015 Nick Downie + * Released under the MIT license + * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md + */ +(function(){"use strict";var t=this,i=t.Chart,e=function(t){this.canvas=t.canvas,this.ctx=t;var i=function(t,i){return t["offset"+i]?t["offset"+i]:document.defaultView.getComputedStyle(t).getPropertyValue(i)},e=this.width=i(t.canvas,"Width"),n=this.height=i(t.canvas,"Height");t.canvas.width=e,t.canvas.height=n;var e=this.width=t.canvas.width,n=this.height=t.canvas.height;return this.aspectRatio=this.width/this.height,s.retinaScale(this),this};e.defaults={global:{animation:!0,animationSteps:60,animationEasing:"easeOutQuart",showScale:!0,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleIntegersOnly:!0,scaleBeginAtZero:!1,scaleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",responsive:!1,maintainAspectRatio:!0,showTooltips:!0,customTooltips:!1,tooltipEvents:["mousemove","touchstart","touchmove","mouseout"],tooltipFillColor:"rgba(0,0,0,0.8)",tooltipFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipFontSize:14,tooltipFontStyle:"normal",tooltipFontColor:"#fff",tooltipTitleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipTitleFontSize:14,tooltipTitleFontStyle:"bold",tooltipTitleFontColor:"#fff",tooltipYPadding:6,tooltipXPadding:6,tooltipCaretSize:8,tooltipCornerRadius:6,tooltipXOffset:10,tooltipTemplate:"<%if (label){%><%=label%>: <%}%><%= value %>",multiTooltipTemplate:"<%= value %>",multiTooltipKeyBackground:"#fff",onAnimationProgress:function(){},onAnimationComplete:function(){}}},e.types={};var s=e.helpers={},n=s.each=function(t,i,e){var s=Array.prototype.slice.call(arguments,3);if(t)if(t.length===+t.length){var n;for(n=0;n=0;s--){var n=t[s];if(i(n))return n}},s.inherits=function(t){var i=this,e=t&&t.hasOwnProperty("constructor")?t.constructor:function(){return i.apply(this,arguments)},s=function(){this.constructor=e};return s.prototype=i.prototype,e.prototype=new s,e.extend=r,t&&a(e.prototype,t),e.__super__=i.prototype,e}),c=s.noop=function(){},u=s.uid=function(){var t=0;return function(){return"chart-"+t++}}(),d=s.warn=function(t){window.console&&"function"==typeof window.console.warn&&console.warn(t)},p=s.amd="function"==typeof define&&define.amd,f=s.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},g=s.max=function(t){return Math.max.apply(Math,t)},m=s.min=function(t){return Math.min.apply(Math,t)},v=(s.cap=function(t,i,e){if(f(i)){if(t>i)return i}else if(f(e)&&e>t)return e;return t},s.getDecimalPlaces=function(t){return t%1!==0&&f(t)?t.toString().split(".")[1].length:0}),S=s.radians=function(t){return t*(Math.PI/180)},x=(s.getAngleFromPoint=function(t,i){var e=i.x-t.x,s=i.y-t.y,n=Math.sqrt(e*e+s*s),o=2*Math.PI+Math.atan2(s,e);return 0>e&&0>s&&(o+=2*Math.PI),{angle:o,distance:n}},s.aliasPixel=function(t){return t%2===0?0:.5}),y=(s.splineCurve=function(t,i,e,s){var n=Math.sqrt(Math.pow(i.x-t.x,2)+Math.pow(i.y-t.y,2)),o=Math.sqrt(Math.pow(e.x-i.x,2)+Math.pow(e.y-i.y,2)),a=s*n/(n+o),h=s*o/(n+o);return{inner:{x:i.x-a*(e.x-t.x),y:i.y-a*(e.y-t.y)},outer:{x:i.x+h*(e.x-t.x),y:i.y+h*(e.y-t.y)}}},s.calculateOrderOfMagnitude=function(t){return Math.floor(Math.log(t)/Math.LN10)}),C=(s.calculateScaleRange=function(t,i,e,s,n){var o=2,a=Math.floor(i/(1.5*e)),h=o>=a,l=g(t),r=m(t);l===r&&(l+=.5,r>=.5&&!s?r-=.5:l+=.5);for(var c=Math.abs(l-r),u=y(c),d=Math.ceil(l/(1*Math.pow(10,u)))*Math.pow(10,u),p=s?0:Math.floor(r/(1*Math.pow(10,u)))*Math.pow(10,u),f=d-p,v=Math.pow(10,u),S=Math.round(f/v);(S>a||a>2*S)&&!h;)if(S>a)v*=2,S=Math.round(f/v),S%1!==0&&(h=!0);else if(n&&u>=0){if(v/2%1!==0)break;v/=2,S=Math.round(f/v)}else v/=2,S=Math.round(f/v);return h&&(S=o,v=f/S),{steps:S,stepValue:v,min:p,max:p+S*v}},s.template=function(t,i){function e(t,i){var e=/\W/.test(t)?new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+t.replace(/[\r\t\n]/g," ").split("<%").join(" ").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split(" ").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');"):s[t]=s[t];return i?e(i):e}if(t instanceof Function)return t(i);var s={};return e(t,i)}),w=(s.generateLabels=function(t,i,e,s){var o=new Array(i);return labelTemplateString&&n(o,function(i,n){o[n]=C(t,{value:e+s*(n+1)})}),o},s.easingEffects={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-1*t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-0.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return 1*((t=t/1-1)*t*t+1)},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-1*((t=t/1-1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-0.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return 1*(t/=1)*t*t*t*t},easeOutQuint:function(t){return 1*((t=t/1-1)*t*t*t*t+1)},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return-1*Math.cos(t/1*(Math.PI/2))+1},easeOutSine:function(t){return 1*Math.sin(t/1*(Math.PI/2))},easeInOutSine:function(t){return-0.5*(Math.cos(Math.PI*t/1)-1)},easeInExpo:function(t){return 0===t?1:1*Math.pow(2,10*(t/1-1))},easeOutExpo:function(t){return 1===t?1:1*(-Math.pow(2,-10*t/1)+1)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(-Math.pow(2,-10*--t)+2)},easeInCirc:function(t){return t>=1?t:-1*(Math.sqrt(1-(t/=1)*t)-1)},easeOutCirc:function(t){return 1*Math.sqrt(1-(t=t/1-1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-0.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),st?-.5*s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e):s*Math.pow(2,-10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)*.5+1)},easeInBack:function(t){var i=1.70158;return 1*(t/=1)*t*((i+1)*t-i)},easeOutBack:function(t){var i=1.70158;return 1*((t=t/1-1)*t*((i+1)*t+i)+1)},easeInOutBack:function(t){var i=1.70158;return(t/=.5)<1?.5*t*t*(((i*=1.525)+1)*t-i):.5*((t-=2)*t*(((i*=1.525)+1)*t+i)+2)},easeInBounce:function(t){return 1-w.easeOutBounce(1-t)},easeOutBounce:function(t){return(t/=1)<1/2.75?7.5625*t*t:2/2.75>t?1*(7.5625*(t-=1.5/2.75)*t+.75):2.5/2.75>t?1*(7.5625*(t-=2.25/2.75)*t+.9375):1*(7.5625*(t-=2.625/2.75)*t+.984375)},easeInOutBounce:function(t){return.5>t?.5*w.easeInBounce(2*t):.5*w.easeOutBounce(2*t-1)+.5}}),b=s.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)}}(),P=s.cancelAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||window.oCancelAnimationFrame||window.msCancelAnimationFrame||function(t){return window.clearTimeout(t,1e3/60)}}(),L=(s.animationLoop=function(t,i,e,s,n,o){var a=0,h=w[e]||w.linear,l=function(){a++;var e=a/i,r=h(e);t.call(o,r,e,a),s.call(o,r,e),i>a?o.animationFrame=b(l):n.apply(o)};b(l)},s.getRelativePosition=function(t){var i,e,s=t.originalEvent||t,n=t.currentTarget||t.srcElement,o=n.getBoundingClientRect();return s.touches?(i=s.touches[0].clientX-o.left,e=s.touches[0].clientY-o.top):(i=s.clientX-o.left,e=s.clientY-o.top),{x:i,y:e}},s.addEvent=function(t,i,e){t.addEventListener?t.addEventListener(i,e):t.attachEvent?t.attachEvent("on"+i,e):t["on"+i]=e}),k=s.removeEvent=function(t,i,e){t.removeEventListener?t.removeEventListener(i,e,!1):t.detachEvent?t.detachEvent("on"+i,e):t["on"+i]=c},F=(s.bindEvents=function(t,i,e){t.events||(t.events={}),n(i,function(i){t.events[i]=function(){e.apply(t,arguments)},L(t.chart.canvas,i,t.events[i])})},s.unbindEvents=function(t,i){n(i,function(i,e){k(t.chart.canvas,e,i)})}),R=s.getMaximumWidth=function(t){var i=t.parentNode;return i.clientWidth},T=s.getMaximumHeight=function(t){var i=t.parentNode;return i.clientHeight},A=(s.getMaximumSize=s.getMaximumWidth,s.retinaScale=function(t){var i=t.ctx,e=t.canvas.width,s=t.canvas.height;window.devicePixelRatio&&(i.canvas.style.width=e+"px",i.canvas.style.height=s+"px",i.canvas.height=s*window.devicePixelRatio,i.canvas.width=e*window.devicePixelRatio,i.scale(window.devicePixelRatio,window.devicePixelRatio))}),M=s.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},W=s.fontString=function(t,i,e){return i+" "+t+"px "+e},z=s.longestText=function(t,i,e){t.font=i;var s=0;return n(e,function(i){var e=t.measureText(i).width;s=e>s?e:s}),s},B=s.drawRoundedRectangle=function(t,i,e,s,n,o){t.beginPath(),t.moveTo(i+o,e),t.lineTo(i+s-o,e),t.quadraticCurveTo(i+s,e,i+s,e+o),t.lineTo(i+s,e+n-o),t.quadraticCurveTo(i+s,e+n,i+s-o,e+n),t.lineTo(i+o,e+n),t.quadraticCurveTo(i,e+n,i,e+n-o),t.lineTo(i,e+o),t.quadraticCurveTo(i,e,i+o,e),t.closePath()};e.instances={},e.Type=function(t,i,s){this.options=i,this.chart=s,this.id=u(),e.instances[this.id]=this,i.responsive&&this.resize(),this.initialize.call(this,t)},a(e.Type.prototype,{initialize:function(){return this},clear:function(){return M(this.chart),this},stop:function(){return P(this.animationFrame),this},resize:function(t){this.stop();var i=this.chart.canvas,e=R(this.chart.canvas),s=this.options.maintainAspectRatio?e/this.chart.aspectRatio:T(this.chart.canvas);return i.width=this.chart.width=e,i.height=this.chart.height=s,A(this.chart),"function"==typeof t&&t.apply(this,Array.prototype.slice.call(arguments,1)),this},reflow:c,render:function(t){return t&&this.reflow(),this.options.animation&&!t?s.animationLoop(this.draw,this.options.animationSteps,this.options.animationEasing,this.options.onAnimationProgress,this.options.onAnimationComplete,this):(this.draw(),this.options.onAnimationComplete.call(this)),this},generateLegend:function(){return C(this.options.legendTemplate,this)},destroy:function(){this.clear(),F(this,this.events);var t=this.chart.canvas;t.width=this.chart.width,t.height=this.chart.height,t.style.removeProperty?(t.style.removeProperty("width"),t.style.removeProperty("height")):(t.style.removeAttribute("width"),t.style.removeAttribute("height")),delete e.instances[this.id]},showTooltip:function(t,i){"undefined"==typeof this.activeElements&&(this.activeElements=[]);var o=function(t){var i=!1;return t.length!==this.activeElements.length?i=!0:(n(t,function(t,e){t!==this.activeElements[e]&&(i=!0)},this),i)}.call(this,t);if(o||i){if(this.activeElements=t,this.draw(),this.options.customTooltips&&this.options.customTooltips(!1),t.length>0)if(this.datasets&&this.datasets.length>1){for(var a,h,r=this.datasets.length-1;r>=0&&(a=this.datasets[r].points||this.datasets[r].bars||this.datasets[r].segments,h=l(a,t[0]),-1===h);r--);var c=[],u=[],d=function(){var t,i,e,n,o,a=[],l=[],r=[];return s.each(this.datasets,function(i){t=i.points||i.bars||i.segments,t[h]&&t[h].hasValue()&&a.push(t[h])}),s.each(a,function(t){l.push(t.x),r.push(t.y),c.push(s.template(this.options.multiTooltipTemplate,t)),u.push({fill:t._saved.fillColor||t.fillColor,stroke:t._saved.strokeColor||t.strokeColor})},this),o=m(r),e=g(r),n=m(l),i=g(l),{x:n>this.chart.width/2?n:i,y:(o+e)/2}}.call(this,h);new e.MultiTooltip({x:d.x,y:d.y,xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,xOffset:this.options.tooltipXOffset,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,titleTextColor:this.options.tooltipTitleFontColor,titleFontFamily:this.options.tooltipTitleFontFamily,titleFontStyle:this.options.tooltipTitleFontStyle,titleFontSize:this.options.tooltipTitleFontSize,cornerRadius:this.options.tooltipCornerRadius,labels:c,legendColors:u,legendColorBackground:this.options.multiTooltipKeyBackground,title:t[0].label,chart:this.chart,ctx:this.chart.ctx,custom:this.options.customTooltips}).draw()}else n(t,function(t){var i=t.tooltipPosition();new e.Tooltip({x:Math.round(i.x),y:Math.round(i.y),xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,caretHeight:this.options.tooltipCaretSize,cornerRadius:this.options.tooltipCornerRadius,text:C(this.options.tooltipTemplate,t),chart:this.chart,custom:this.options.customTooltips}).draw()},this);return this}},toBase64Image:function(){return this.chart.canvas.toDataURL.apply(this.chart.canvas,arguments)}}),e.Type.extend=function(t){var i=this,s=function(){return i.apply(this,arguments)};if(s.prototype=o(i.prototype),a(s.prototype,t),s.extend=e.Type.extend,t.name||i.prototype.name){var n=t.name||i.prototype.name,l=e.defaults[i.prototype.name]?o(e.defaults[i.prototype.name]):{};e.defaults[n]=a(l,t.defaults),e.types[n]=s,e.prototype[n]=function(t,i){var o=h(e.defaults.global,e.defaults[n],i||{});return new s(t,o,this)}}else d("Name not provided for this chart, so it hasn't been registered");return i},e.Element=function(t){a(this,t),this.initialize.apply(this,arguments),this.save()},a(e.Element.prototype,{initialize:function(){},restore:function(t){return t?n(t,function(t){this[t]=this._saved[t]},this):a(this,this._saved),this},save:function(){return this._saved=o(this),delete this._saved._saved,this},update:function(t){return n(t,function(t,i){this._saved[i]=this[i],this[i]=t},this),this},transition:function(t,i){return n(t,function(t,e){this[e]=(t-this._saved[e])*i+this._saved[e]},this),this},tooltipPosition:function(){return{x:this.x,y:this.y}},hasValue:function(){return f(this.value)}}),e.Element.extend=r,e.Point=e.Element.extend({display:!0,inRange:function(t,i){var e=this.hitDetectionRadius+this.radius;return Math.pow(t-this.x,2)+Math.pow(i-this.y,2)=this.startAngle&&e.angle<=this.endAngle,o=e.distance>=this.innerRadius&&e.distance<=this.outerRadius;return n&&o},tooltipPosition:function(){var t=this.startAngle+(this.endAngle-this.startAngle)/2,i=(this.outerRadius-this.innerRadius)/2+this.innerRadius;return{x:this.x+Math.cos(t)*i,y:this.y+Math.sin(t)*i}},draw:function(t){var i=this.ctx;i.beginPath(),i.arc(this.x,this.y,this.outerRadius,this.startAngle,this.endAngle),i.arc(this.x,this.y,this.innerRadius,this.endAngle,this.startAngle,!0),i.closePath(),i.strokeStyle=this.strokeColor,i.lineWidth=this.strokeWidth,i.fillStyle=this.fillColor,i.fill(),i.lineJoin="bevel",this.showStroke&&i.stroke()}}),e.Rectangle=e.Element.extend({draw:function(){var t=this.ctx,i=this.width/2,e=this.x-i,s=this.x+i,n=this.base-(this.base-this.y),o=this.strokeWidth/2;this.showStroke&&(e+=o,s-=o,n+=o),t.beginPath(),t.fillStyle=this.fillColor,t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.moveTo(e,this.base),t.lineTo(e,n),t.lineTo(s,n),t.lineTo(s,this.base),t.fill(),this.showStroke&&t.stroke()},height:function(){return this.base-this.y},inRange:function(t,i){return t>=this.x-this.width/2&&t<=this.x+this.width/2&&i>=this.y&&i<=this.base}}),e.Tooltip=e.Element.extend({draw:function(){var t=this.chart.ctx;t.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.xAlign="center",this.yAlign="above";var i=this.caretPadding=2,e=t.measureText(this.text).width+2*this.xPadding,s=this.fontSize+2*this.yPadding,n=s+this.caretHeight+i;this.x+e/2>this.chart.width?this.xAlign="left":this.x-e/2<0&&(this.xAlign="right"),this.y-n<0&&(this.yAlign="below");var o=this.x-e/2,a=this.y-n;if(t.fillStyle=this.fillColor,this.custom)this.custom(this);else{switch(this.yAlign){case"above":t.beginPath(),t.moveTo(this.x,this.y-i),t.lineTo(this.x+this.caretHeight,this.y-(i+this.caretHeight)),t.lineTo(this.x-this.caretHeight,this.y-(i+this.caretHeight)),t.closePath(),t.fill();break;case"below":a=this.y+i+this.caretHeight,t.beginPath(),t.moveTo(this.x,this.y+i),t.lineTo(this.x+this.caretHeight,this.y+i+this.caretHeight),t.lineTo(this.x-this.caretHeight,this.y+i+this.caretHeight),t.closePath(),t.fill()}switch(this.xAlign){case"left":o=this.x-e+(this.cornerRadius+this.caretHeight);break;case"right":o=this.x-(this.cornerRadius+this.caretHeight)}B(t,o,a,e,s,this.cornerRadius),t.fill(),t.fillStyle=this.textColor,t.textAlign="center",t.textBaseline="middle",t.fillText(this.text,o+e/2,a+s/2)}}}),e.MultiTooltip=e.Element.extend({initialize:function(){this.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.titleFont=W(this.titleFontSize,this.titleFontStyle,this.titleFontFamily),this.height=this.labels.length*this.fontSize+(this.labels.length-1)*(this.fontSize/2)+2*this.yPadding+1.5*this.titleFontSize,this.ctx.font=this.titleFont;var t=this.ctx.measureText(this.title).width,i=z(this.ctx,this.font,this.labels)+this.fontSize+3,e=g([i,t]);this.width=e+2*this.xPadding;var s=this.height/2;this.y-s<0?this.y=s:this.y+s>this.chart.height&&(this.y=this.chart.height-s),this.x>this.chart.width/2?this.x-=this.xOffset+this.width:this.x+=this.xOffset},getLineHeight:function(t){var i=this.y-this.height/2+this.yPadding,e=t-1;return 0===t?i+this.titleFontSize/2:i+(1.5*this.fontSize*e+this.fontSize/2)+1.5*this.titleFontSize},draw:function(){if(this.custom)this.custom(this);else{B(this.ctx,this.x,this.y-this.height/2,this.width,this.height,this.cornerRadius);var t=this.ctx;t.fillStyle=this.fillColor,t.fill(),t.closePath(),t.textAlign="left",t.textBaseline="middle",t.fillStyle=this.titleTextColor,t.font=this.titleFont,t.fillText(this.title,this.x+this.xPadding,this.getLineHeight(0)),t.font=this.font,s.each(this.labels,function(i,e){t.fillStyle=this.textColor,t.fillText(i,this.x+this.xPadding+this.fontSize+3,this.getLineHeight(e+1)),t.fillStyle=this.legendColorBackground,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize),t.fillStyle=this.legendColors[e].fill,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize)},this)}}}),e.Scale=e.Element.extend({initialize:function(){this.fit()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}));this.yLabelWidth=this.display&&this.showLabels?z(this.ctx,this.font,this.yLabels):0},addXLabel:function(t){this.xLabels.push(t),this.valuesCount++,this.fit()},removeXLabel:function(){this.xLabels.shift(),this.valuesCount--,this.fit()},fit:function(){this.startPoint=this.display?this.fontSize:0,this.endPoint=this.display?this.height-1.5*this.fontSize-5:this.height,this.startPoint+=this.padding,this.endPoint-=this.padding;var t,i=this.endPoint-this.startPoint;for(this.calculateYRange(i),this.buildYLabels(),this.calculateXLabelRotation();i>this.endPoint-this.startPoint;)i=this.endPoint-this.startPoint,t=this.yLabelWidth,this.calculateYRange(i),this.buildYLabels(),tthis.yLabelWidth+10?e/2:this.yLabelWidth+10,this.xLabelRotation=0,this.display){var n,o=z(this.ctx,this.font,this.xLabels);this.xLabelWidth=o;for(var a=Math.floor(this.calculateX(1)-this.calculateX(0))-6;this.xLabelWidth>a&&0===this.xLabelRotation||this.xLabelWidth>a&&this.xLabelRotation<=90&&this.xLabelRotation>0;)n=Math.cos(S(this.xLabelRotation)),t=n*e,i=n*s,t+this.fontSize/2>this.yLabelWidth+8&&(this.xScalePaddingLeft=t+this.fontSize/2),this.xScalePaddingRight=this.fontSize/2,this.xLabelRotation++,this.xLabelWidth=n*o;this.xLabelRotation>0&&(this.endPoint-=Math.sin(S(this.xLabelRotation))*o+3)}else this.xLabelWidth=0,this.xScalePaddingRight=this.padding,this.xScalePaddingLeft=this.padding},calculateYRange:c,drawingArea:function(){return this.startPoint-this.endPoint},calculateY:function(t){var i=this.drawingArea()/(this.min-this.max);return this.endPoint-i*(t-this.min)},calculateX:function(t){var i=(this.xLabelRotation>0,this.width-(this.xScalePaddingLeft+this.xScalePaddingRight)),e=i/Math.max(this.valuesCount-(this.offsetGridLines?0:1),1),s=e*t+this.xScalePaddingLeft;return this.offsetGridLines&&(s+=e/2),Math.round(s)},update:function(t){s.extend(this,t),this.fit()},draw:function(){var t=this.ctx,i=(this.endPoint-this.startPoint)/this.steps,e=Math.round(this.xScalePaddingLeft);this.display&&(t.fillStyle=this.textColor,t.font=this.font,n(this.yLabels,function(n,o){var a=this.endPoint-i*o,h=Math.round(a),l=this.showHorizontalLines;t.textAlign="right",t.textBaseline="middle",this.showLabels&&t.fillText(n,e-10,a),0!==o||l||(l=!0),l&&t.beginPath(),o>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),h+=s.aliasPixel(t.lineWidth),l&&(t.moveTo(e,h),t.lineTo(this.width,h),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(e-5,h),t.lineTo(e,h),t.stroke(),t.closePath()},this),n(this.xLabels,function(i,e){var s=this.calculateX(e)+x(this.lineWidth),n=this.calculateX(e-(this.offsetGridLines?.5:0))+x(this.lineWidth),o=this.xLabelRotation>0,a=this.showVerticalLines;0!==e||a||(a=!0),a&&t.beginPath(),e>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),a&&(t.moveTo(n,this.endPoint),t.lineTo(n,this.startPoint-3),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(n,this.endPoint),t.lineTo(n,this.endPoint+5),t.stroke(),t.closePath(),t.save(),t.translate(s,o?this.endPoint+12:this.endPoint+8),t.rotate(-1*S(this.xLabelRotation)),t.font=this.font,t.textAlign=o?"right":"center",t.textBaseline=o?"middle":"top",t.fillText(i,0,0),t.restore()},this))}}),e.RadialScale=e.Element.extend({initialize:function(){this.size=m([this.height,this.width]),this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2},calculateCenterOffset:function(t){var i=this.drawingArea/(this.max-this.min);return(t-this.min)*i},update:function(){this.lineArc?this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2:this.setScaleSize(),this.buildYLabels()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}))},getCircumference:function(){return 2*Math.PI/this.valuesCount},setScaleSize:function(){var t,i,e,s,n,o,a,h,l,r,c,u,d=m([this.height/2-this.pointLabelFontSize-5,this.width/2]),p=this.width,g=0;for(this.ctx.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),i=0;ip&&(p=t.x+s,n=i),t.x-sp&&(p=t.x+e,n=i):i>this.valuesCount/2&&t.x-e0){var s,n=e*(this.drawingArea/this.steps),o=this.yCenter-n;if(this.lineWidth>0)if(t.strokeStyle=this.lineColor,t.lineWidth=this.lineWidth,this.lineArc)t.beginPath(),t.arc(this.xCenter,this.yCenter,n,0,2*Math.PI),t.closePath(),t.stroke();else{t.beginPath();for(var a=0;a=0;i--){if(this.angleLineWidth>0){var e=this.getPointPosition(i,this.calculateCenterOffset(this.max));t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(e.x,e.y),t.stroke(),t.closePath()}var s=this.getPointPosition(i,this.calculateCenterOffset(this.max)+5);t.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),t.fillStyle=this.pointLabelFontColor;var o=this.labels.length,a=this.labels.length/2,h=a/2,l=h>i||i>o-h,r=i===h||i===o-h;t.textAlign=0===i?"center":i===a?"center":a>i?"left":"right",t.textBaseline=r?"middle":l?"bottom":"top",t.fillText(this.labels[i],s.x,s.y)}}}}}),s.addEvent(window,"resize",function(){var t;return function(){clearTimeout(t),t=setTimeout(function(){n(e.instances,function(t){t.options.responsive&&t.resize(t.render,!0)})},50)}}()),p?define(function(){return e}):"object"==typeof module&&module.exports&&(module.exports=e),t.Chart=e,e.noConflict=function(){return t.Chart=i,e}}).call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleBeginAtZero:!0,scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,scaleShowHorizontalLines:!0,scaleShowVerticalLines:!0,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,legendTemplate:'
    <% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
'};i.Type.extend({name:"Bar",defaults:s,initialize:function(t){var s=this.options;this.ScaleClass=i.Scale.extend({offsetGridLines:!0,calculateBarX:function(t,i,e){var n=this.calculateBaseWidth(),o=this.calculateX(e)-n/2,a=this.calculateBarWidth(t);return o+a*i+i*s.barDatasetSpacing+a/2},calculateBaseWidth:function(){return this.calculateX(1)-this.calculateX(0)-2*s.barValueSpacing},calculateBarWidth:function(t){var i=this.calculateBaseWidth()-(t-1)*s.barDatasetSpacing;return i/t}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getBarsAtEvent(t):[];this.eachBars(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),this.BarClass=i.Rectangle.extend({strokeWidth:this.options.barStrokeWidth,showStroke:this.options.barShowStroke,ctx:this.chart.ctx}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,bars:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.bars.push(new this.BarClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.strokeColor,fillColor:i.fillColor,highlightFill:i.highlightFill||i.fillColor,highlightStroke:i.highlightStroke||i.strokeColor}))},this)},this),this.buildScale(t.labels),this.BarClass.prototype.base=this.scale.endPoint,this.eachBars(function(t,i,s){e.extend(t,{width:this.scale.calculateBarWidth(this.datasets.length),x:this.scale.calculateBarX(this.datasets.length,s,i),y:this.scale.endPoint}),t.save()},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachBars(function(t){t.save()}),this.render()},eachBars:function(t){e.each(this.datasets,function(i,s){e.each(i.bars,t,this,s)},this)},getBarsAtEvent:function(t){for(var i,s=[],n=e.getRelativePosition(t),o=function(t){s.push(t.bars[i])},a=0;a<% for (var i=0; i
  • <%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>'};i.Type.extend({name:"Doughnut",defaults:s,initialize:function(t){this.segments=[],this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,this.SegmentArc=i.Arc.extend({ctx:this.chart.ctx,x:this.chart.width/2,y:this.chart.height/2}),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.calculateTotal(t),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({value:t.value,outerRadius:this.options.animateScale?0:this.outerRadius,innerRadius:this.options.animateScale?0:this.outerRadius/100*this.options.percentageInnerCutout,fillColor:t.color,highlightColor:t.highlight||t.color,showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,startAngle:1.5*Math.PI,circumference:this.options.animateRotate?0:this.calculateCircumference(t.value),label:t.label})),e||(this.reflow(),this.update())},calculateCircumference:function(t){return 2*Math.PI*(Math.abs(t)/this.total)},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=Math.abs(t.value)},this)},update:function(){this.calculateTotal(this.segments),e.each(this.activeElements,function(t){t.restore(["fillColor"])}),e.each(this.segments,function(t){t.save()}),this.render()},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,e.each(this.segments,function(t){t.update({outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout})},this)},draw:function(t){var i=t?t:1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.calculateCircumference(t.value),outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout},i),t.endAngle=t.startAngle+t.circumference,t.draw(),0===e&&(t.startAngle=1.5*Math.PI),e<% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>'};i.Type.extend({name:"Line",defaults:s,initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx,inRange:function(t){return Math.pow(t-this.x,2)0&&ithis.scale.endPoint?t.controlPoints.outer.y=this.scale.endPoint:t.controlPoints.outer.ythis.scale.endPoint?t.controlPoints.inner.y=this.scale.endPoint:t.controlPoints.inner.y0&&(s.lineTo(h[h.length-1].x,this.scale.endPoint),s.lineTo(h[0].x,this.scale.endPoint),s.fillStyle=t.fillColor,s.closePath(),s.fill()),e.each(h,function(t){t.draw()})},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBeginAtZero:!0,scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,scaleShowLine:!0,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'
      <% for (var i=0; i
    • <%if(segments[i].label){%><%=segments[i].label%><%}%>
    • <%}%>
    '};i.Type.extend({name:"PolarArea",defaults:s,initialize:function(t){this.segments=[],this.SegmentArc=i.Arc.extend({showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,ctx:this.chart.ctx,innerRadius:0,x:this.chart.width/2,y:this.chart.height/2}),this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,lineArc:!0,width:this.chart.width,height:this.chart.height,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,valuesCount:t.length}),this.updateScaleRange(t),this.scale.update(),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({fillColor:t.color,highlightColor:t.highlight||t.color,label:t.label,value:t.value,outerRadius:this.options.animateScale?0:this.scale.calculateCenterOffset(t.value),circumference:this.options.animateRotate?0:this.scale.getCircumference(),startAngle:1.5*Math.PI})),e||(this.reflow(),this.update())},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=t.value},this),this.scale.valuesCount=this.segments.length},updateScaleRange:function(t){var i=[];e.each(t,function(t){i.push(t.value)});var s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s,{size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2})},update:function(){this.calculateTotal(this.segments),e.each(this.segments,function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.updateScaleRange(this.segments),this.scale.update(),e.extend(this.scale,{xCenter:this.chart.width/2,yCenter:this.chart.height/2}),e.each(this.segments,function(t){t.update({outerRadius:this.scale.calculateCenterOffset(t.value)})},this)},draw:function(t){var i=t||1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.scale.getCircumference(),outerRadius:this.scale.calculateCenterOffset(t.value)},i),t.endAngle=t.startAngle+t.circumference,0===e&&(t.startAngle=1.5*Math.PI),e<% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>'},initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx}),this.datasets=[],this.buildScale(t),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){var o;this.scale.animation||(o=this.scale.getPointPosition(n,this.scale.calculateCenterOffset(e))),s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,x:this.options.animation?this.scale.xCenter:o.x,y:this.options.animation?this.scale.yCenter:o.y,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this)},this),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=e.getRelativePosition(t),s=e.getAngleFromPoint({x:this.scale.xCenter,y:this.scale.yCenter},i),n=2*Math.PI/this.scale.valuesCount,o=Math.round((s.angle-1.5*Math.PI)/n),a=[];return(o>=this.scale.valuesCount||0>o)&&(o=0),s.distance<=this.scale.drawingArea&&e.each(this.datasets,function(t){a.push(t.points[o])}),a},buildScale:function(t){this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,angleLineColor:this.options.angleLineColor,angleLineWidth:this.options.angleShowLineOut?this.options.angleLineWidth:0,pointLabelFontColor:this.options.pointLabelFontColor,pointLabelFontSize:this.options.pointLabelFontSize,pointLabelFontFamily:this.options.pointLabelFontFamily,pointLabelFontStyle:this.options.pointLabelFontStyle,height:this.chart.height,width:this.chart.width,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,labels:t.labels,valuesCount:t.datasets[0].data.length}),this.scale.setScaleSize(),this.updateScaleRange(t.datasets),this.scale.buildYLabels()},updateScaleRange:function(t){var i=function(){var i=[];return e.each(t,function(t){t.data?i=i.concat(t.data):e.each(t.points,function(t){i.push(t.value)})}),i}(),s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s)},addData:function(t,i){this.scale.valuesCount++,e.each(t,function(t,e){var s=this.scale.getPointPosition(this.scale.valuesCount,this.scale.calculateCenterOffset(t));this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:s.x,y:s.y,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.labels.push(i),this.reflow(),this.update()},removeData:function(){this.scale.valuesCount--,this.scale.labels.shift(),e.each(this.datasets,function(t){t.points.shift()},this),this.reflow(),this.update()},update:function(){this.eachPoints(function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.scale,{width:this.chart.width,height:this.chart.height,size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2}),this.updateScaleRange(this.datasets),this.scale.setScaleSize(),this.scale.buildYLabels()},draw:function(t){var i=t||1,s=this.chart.ctx;this.clear(),this.scale.draw(),e.each(this.datasets,function(t){e.each(t.points,function(t,e){t.hasValue()&&t.transition(this.scale.getPointPosition(e,this.scale.calculateCenterOffset(t.value)),i)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(t.points,function(t,i){0===i?s.moveTo(t.x,t.y):s.lineTo(t.x,t.y)},this),s.closePath(),s.stroke(),s.fillStyle=t.fillColor,s.fill(),e.each(t.points,function(t){t.hasValue()&&t.draw()})},this)}})}.call(this); \ No newline at end of file diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/LICENSE.md b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/LICENSE.md new file mode 100644 index 0000000000..e10bc0ff1f --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/LICENSE.md @@ -0,0 +1,7 @@ +Copyright (c) 2013-2015 Nick Downie + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/README.md b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/README.md new file mode 100644 index 0000000000..7c4fa8bf6d --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/README.md @@ -0,0 +1,20 @@ +# Chart.js + +[![Build Status](https://travis-ci.org/nnnick/Chart.js.svg?branch=master)](https://travis-ci.org/nnnick/Chart.js) [![Code Climate](https://codeclimate.com/github/nnnick/Chart.js/badges/gpa.svg)](https://codeclimate.com/github/nnnick/Chart.js) + + +*Simple HTML5 Charts using the canvas element* [chartjs.org](http://www.chartjs.org) + +## Documentation + +You can find documentation at [chartjs.org/docs](http://www.chartjs.org/docs/). The markdown files that build the site are available under `/docs`. Please note - in some of the json examples of configuration you might notice some liquid tags - this is just for the generating the site html, please disregard. + +## Bugs, issues and contributing + +Before submitting an issue or a pull request to the project, please take a moment to look over the [contributing guidelines](https://github.com/nnnick/Chart.js/blob/master/CONTRIBUTING.md) first. + +For support using Chart.js, please post questions with the [`chartjs` tag on Stack Overflow](http://stackoverflow.com/questions/tagged/chartjs). + +## License + +Chart.js is available under the [MIT license](http://opensource.org/licenses/MIT). diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/bower.json b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/bower.json new file mode 100644 index 0000000000..f26f81eef8 --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/bower.json @@ -0,0 +1,11 @@ +{ + "name": "Chart.js", + "version": "1.0.2", + "description": "Simple HTML5 Charts using the canvas element", + "homepage": "https://github.com/nnnick/Chart.js", + "author": "nnnick", + "main": [ + "Chart.js" + ], + "dependencies": {} +} \ No newline at end of file diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/00-Getting-Started.md b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/00-Getting-Started.md new file mode 100644 index 0000000000..273b10f2e9 --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/00-Getting-Started.md @@ -0,0 +1,203 @@ +--- +title: Getting started +anchor: getting-started +--- + +###Include Chart.js + +First we need to include the Chart.js library on the page. The library occupies a global variable of `Chart`. + +```html + +``` + +Alternatively, if you're using an AMD loader for JavaScript modules, that is also supported in the Chart.js core. Please note: the library will still occupy a global variable of `Chart`, even if it detects `define` and `define.amd`. If this is a problem, you can call `noConflict` to restore the global Chart variable to it's previous owner. + +```javascript +// Using requirejs +require(['path/to/Chartjs'], function(Chart){ + // Use Chart.js as normal here. + + // Chart.noConflict restores the Chart global variable to it's previous owner + // The function returns what was previously Chart, allowing you to reassign. + var Chartjs = Chart.noConflict(); + +}); +``` + +You can also grab Chart.js using bower: + +```bash +bower install Chart.js --save +``` + +###Creating a chart + +To create a chart, we need to instantiate the `Chart` class. To do this, we need to pass in the 2d context of where we want to draw the chart. Here's an example. + +```html + +``` + +```javascript +// Get the context of the canvas element we want to select +var ctx = document.getElementById("myChart").getContext("2d"); +var myNewChart = new Chart(ctx).PolarArea(data); +``` + +We can also get the context of our canvas with jQuery. To do this, we need to get the DOM node out of the jQuery collection, and call the `getContext("2d")` method on that. + +```javascript +// Get context with jQuery - using jQuery's .get() method. +var ctx = $("#myChart").get(0).getContext("2d"); +// This will get the first returned node in the jQuery collection. +var myNewChart = new Chart(ctx); +``` + +After we've instantiated the Chart class on the canvas we want to draw on, Chart.js will handle the scaling for retina displays. + +With the Chart class set up, we can go on to create one of the charts Chart.js has available. In the example below, we would be drawing a Polar area chart. + +```javascript +new Chart(ctx).PolarArea(data, options); +``` + +We call a method of the name of the chart we want to create. We pass in the data for that chart type, and the options for that chart as parameters. Chart.js will merge the global defaults with chart type specific defaults, then merge any options passed in as a second argument after data. + +###Global chart configuration + +This concept was introduced in Chart.js 1.0 to keep configuration DRY, and allow for changing options globally across chart types, avoiding the need to specify options for each instance, or the default for a particular chart type. + +```javascript +Chart.defaults.global = { + // Boolean - Whether to animate the chart + animation: true, + + // Number - Number of animation steps + animationSteps: 60, + + // String - Animation easing effect + animationEasing: "easeOutQuart", + + // Boolean - If we should show the scale at all + showScale: true, + + // Boolean - If we want to override with a hard coded scale + scaleOverride: false, + + // ** Required if scaleOverride is true ** + // Number - The number of steps in a hard coded scale + scaleSteps: null, + // Number - The value jump in the hard coded scale + scaleStepWidth: null, + // Number - The scale starting value + scaleStartValue: null, + + // String - Colour of the scale line + scaleLineColor: "rgba(0,0,0,.1)", + + // Number - Pixel width of the scale line + scaleLineWidth: 1, + + // Boolean - Whether to show labels on the scale + scaleShowLabels: true, + + // Interpolated JS string - can access value + scaleLabel: "<%=value%>", + + // Boolean - Whether the scale should stick to integers, not floats even if drawing space is there + scaleIntegersOnly: true, + + // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value + scaleBeginAtZero: false, + + // String - Scale label font declaration for the scale label + scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Scale label font size in pixels + scaleFontSize: 12, + + // String - Scale label font weight style + scaleFontStyle: "normal", + + // String - Scale label font colour + scaleFontColor: "#666", + + // Boolean - whether or not the chart should be responsive and resize when the browser does. + responsive: false, + + // Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container + maintainAspectRatio: true, + + // Boolean - Determines whether to draw tooltips on the canvas or not + showTooltips: true, + + // Function - Determines whether to execute the customTooltips function instead of drawing the built in tooltips (See [Advanced - External Tooltips](#advanced-usage-custom-tooltips)) + customTooltips: false, + + // Array - Array of string names to attach tooltip events + tooltipEvents: ["mousemove", "touchstart", "touchmove"], + + // String - Tooltip background colour + tooltipFillColor: "rgba(0,0,0,0.8)", + + // String - Tooltip label font declaration for the scale label + tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Tooltip label font size in pixels + tooltipFontSize: 14, + + // String - Tooltip font weight style + tooltipFontStyle: "normal", + + // String - Tooltip label font colour + tooltipFontColor: "#fff", + + // String - Tooltip title font declaration for the scale label + tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Tooltip title font size in pixels + tooltipTitleFontSize: 14, + + // String - Tooltip title font weight style + tooltipTitleFontStyle: "bold", + + // String - Tooltip title font colour + tooltipTitleFontColor: "#fff", + + // Number - pixel width of padding around tooltip text + tooltipYPadding: 6, + + // Number - pixel width of padding around tooltip text + tooltipXPadding: 6, + + // Number - Size of the caret on the tooltip + tooltipCaretSize: 8, + + // Number - Pixel radius of the tooltip border + tooltipCornerRadius: 6, + + // Number - Pixel offset from point x to tooltip edge + tooltipXOffset: 10, + {% raw %} + // String - Template string for single tooltips + tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>", + {% endraw %} + // String - Template string for multiple tooltips + multiTooltipTemplate: "<%= value %>", + + // Function - Will fire on animation progression. + onAnimationProgress: function(){}, + + // Function - Will fire on animation completion. + onAnimationComplete: function(){} +} +``` + +If for example, you wanted all charts created to be responsive, and resize when the browser window does, the following setting can be changed: + +```javascript +Chart.defaults.global.responsive = true; +``` + +Now, every time we create a chart, `options.responsive` will be `true`. diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/01-Line-Chart.md b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/01-Line-Chart.md new file mode 100644 index 0000000000..1a5ae84c4a --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/01-Line-Chart.md @@ -0,0 +1,166 @@ +--- +title: Line Chart +anchor: line-chart +--- +###Introduction +A line chart is a way of plotting data points on a line. + +Often, it is used to show trend data, and the comparison of two data sets. + +
    + +
    + +###Example usage +```javascript +var myLineChart = new Chart(ctx).Line(data, options); +``` +###Data structure + +```javascript +var data = { + labels: ["January", "February", "March", "April", "May", "June", "July"], + datasets: [ + { + label: "My First dataset", + fillColor: "rgba(220,220,220,0.2)", + strokeColor: "rgba(220,220,220,1)", + pointColor: "rgba(220,220,220,1)", + pointStrokeColor: "#fff", + pointHighlightFill: "#fff", + pointHighlightStroke: "rgba(220,220,220,1)", + data: [65, 59, 80, 81, 56, 55, 40] + }, + { + label: "My Second dataset", + fillColor: "rgba(151,187,205,0.2)", + strokeColor: "rgba(151,187,205,1)", + pointColor: "rgba(151,187,205,1)", + pointStrokeColor: "#fff", + pointHighlightFill: "#fff", + pointHighlightStroke: "rgba(151,187,205,1)", + data: [28, 48, 40, 19, 86, 27, 90] + } + ] +}; +``` + +The line chart requires an array of labels for each of the data points. This is shown on the X axis. +The data for line charts is broken up into an array of datasets. Each dataset has a colour for the fill, a colour for the line and colours for the points and strokes of the points. These colours are strings just like CSS. You can use RGBA, RGB, HEX or HSL notation. + +The label key on each dataset is optional, and can be used when generating a scale for the chart. + +### Chart options + +These are the customisation options specific to Line charts. These options are merged with the [global chart configuration options](#getting-started-global-chart-configuration), and form the options of the chart. + +```javascript +{ + + ///Boolean - Whether grid lines are shown across the chart + scaleShowGridLines : true, + + //String - Colour of the grid lines + scaleGridLineColor : "rgba(0,0,0,.05)", + + //Number - Width of the grid lines + scaleGridLineWidth : 1, + + //Boolean - Whether to show horizontal lines (except X axis) + scaleShowHorizontalLines: true, + + //Boolean - Whether to show vertical lines (except Y axis) + scaleShowVerticalLines: true, + + //Boolean - Whether the line is curved between points + bezierCurve : true, + + //Number - Tension of the bezier curve between points + bezierCurveTension : 0.4, + + //Boolean - Whether to show a dot for each point + pointDot : true, + + //Number - Radius of each point dot in pixels + pointDotRadius : 4, + + //Number - Pixel width of point dot stroke + pointDotStrokeWidth : 1, + + //Number - amount extra to add to the radius to cater for hit detection outside the drawn point + pointHitDetectionRadius : 20, + + //Boolean - Whether to show a stroke for datasets + datasetStroke : true, + + //Number - Pixel width of dataset stroke + datasetStrokeWidth : 2, + + //Boolean - Whether to fill the dataset with a colour + datasetFill : true, + {% raw %} + //String - A legend template + legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
    • <%}%>
    " + {% endraw %} +}; +``` + +You can override these for your `Chart` instance by passing a second argument into the `Line` method as an object with the keys you want to override. + +For example, we could have a line chart without bezier curves between points by doing the following: + +```javascript +new Chart(ctx).Line(data, { + bezierCurve: false +}); +// This will create a chart with all of the default options, merged from the global config, +// and the Line chart defaults, but this particular instance will have `bezierCurve` set to false. +``` + +We can also change these defaults values for each Line type that is created, this object is available at `Chart.defaults.Line`. + + +### Prototype methods + +#### .getPointsAtEvent( event ) + +Calling `getPointsAtEvent(event)` on your Chart instance passing an argument of an event, or jQuery event, will return the point elements that are at that the same position of that event. + +```javascript +canvas.onclick = function(evt){ + var activePoints = myLineChart.getPointsAtEvent(evt); + // => activePoints is an array of points on the canvas that are at the same position as the click event. +}; +``` + +This functionality may be useful for implementing DOM based tooltips, or triggering custom behaviour in your application. + +#### .update( ) + +Calling `update()` on your Chart instance will re-render the chart with any updated values, allowing you to edit the value of multiple existing points, then render those in one animated render loop. + +```javascript +myLineChart.datasets[0].points[2].value = 50; +// Would update the first dataset's value of 'March' to be 50 +myLineChart.update(); +// Calling update now animates the position of March from 90 to 50. +``` + +#### .addData( valuesArray, label ) + +Calling `addData(valuesArray, label)` on your Chart instance passing an array of values for each dataset, along with a label for those points. + +```javascript +// The values array passed into addData should be one for each dataset in the chart +myLineChart.addData([40, 60], "August"); +// This new data will now animate at the end of the chart. +``` + +#### .removeData( ) + +Calling `removeData()` on your Chart instance will remove the first value for all datasets on the chart. + +```javascript +myLineChart.removeData(); +// The chart will remove the first point and animate other points into place +``` diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/02-Bar-Chart.md b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/02-Bar-Chart.md new file mode 100644 index 0000000000..cc23f38b73 --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/02-Bar-Chart.md @@ -0,0 +1,149 @@ +--- +title: Bar Chart +anchor: bar-chart +--- + +### Introduction +A bar chart is a way of showing data as bars. + +It is sometimes used to show trend data, and the comparison of multiple data sets side by side. + +
    + +
    + +### Example usage +```javascript +var myBarChart = new Chart(ctx).Bar(data, options); +``` + +### Data structure + +```javascript +var data = { + labels: ["January", "February", "March", "April", "May", "June", "July"], + datasets: [ + { + label: "My First dataset", + fillColor: "rgba(220,220,220,0.5)", + strokeColor: "rgba(220,220,220,0.8)", + highlightFill: "rgba(220,220,220,0.75)", + highlightStroke: "rgba(220,220,220,1)", + data: [65, 59, 80, 81, 56, 55, 40] + }, + { + label: "My Second dataset", + fillColor: "rgba(151,187,205,0.5)", + strokeColor: "rgba(151,187,205,0.8)", + highlightFill: "rgba(151,187,205,0.75)", + highlightStroke: "rgba(151,187,205,1)", + data: [28, 48, 40, 19, 86, 27, 90] + } + ] +}; +``` +The bar chart has the a very similar data structure to the line chart, and has an array of datasets, each with colours and an array of data. Again, colours are in CSS format. +We have an array of labels too for display. In the example, we are showing the same data as the previous line chart example. + +The label key on each dataset is optional, and can be used when generating a scale for the chart. + +### Chart Options + +These are the customisation options specific to Bar charts. These options are merged with the [global chart configuration options](#getting-started-global-chart-configuration), and form the options of the chart. + +```javascript +{ + //Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value + scaleBeginAtZero : true, + + //Boolean - Whether grid lines are shown across the chart + scaleShowGridLines : true, + + //String - Colour of the grid lines + scaleGridLineColor : "rgba(0,0,0,.05)", + + //Number - Width of the grid lines + scaleGridLineWidth : 1, + + //Boolean - Whether to show horizontal lines (except X axis) + scaleShowHorizontalLines: true, + + //Boolean - Whether to show vertical lines (except Y axis) + scaleShowVerticalLines: true, + + //Boolean - If there is a stroke on each bar + barShowStroke : true, + + //Number - Pixel width of the bar stroke + barStrokeWidth : 2, + + //Number - Spacing between each of the X value sets + barValueSpacing : 5, + + //Number - Spacing between data sets within X values + barDatasetSpacing : 1, + {% raw %} + //String - A legend template + legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
    • <%}%>
    " + {% endraw %} +} +``` + +You can override these for your `Chart` instance by passing a second argument into the `Bar` method as an object with the keys you want to override. + +For example, we could have a bar chart without a stroke on each bar by doing the following: + +```javascript +new Chart(ctx).Bar(data, { + barShowStroke: false +}); +// This will create a chart with all of the default options, merged from the global config, +// and the Bar chart defaults but this particular instance will have `barShowStroke` set to false. +``` + +We can also change these defaults values for each Bar type that is created, this object is available at `Chart.defaults.Bar`. + +### Prototype methods + +#### .getBarsAtEvent( event ) + +Calling `getBarsAtEvent(event)` on your Chart instance passing an argument of an event, or jQuery event, will return the bar elements that are at that the same position of that event. + +```javascript +canvas.onclick = function(evt){ + var activeBars = myBarChart.getBarsAtEvent(evt); + // => activeBars is an array of bars on the canvas that are at the same position as the click event. +}; +``` + +This functionality may be useful for implementing DOM based tooltips, or triggering custom behaviour in your application. + +#### .update( ) + +Calling `update()` on your Chart instance will re-render the chart with any updated values, allowing you to edit the value of multiple existing points, then render those in one animated render loop. + +```javascript +myBarChart.datasets[0].bars[2].value = 50; +// Would update the first dataset's value of 'March' to be 50 +myBarChart.update(); +// Calling update now animates the position of March from 90 to 50. +``` + +#### .addData( valuesArray, label ) + +Calling `addData(valuesArray, label)` on your Chart instance passing an array of values for each dataset, along with a label for those bars. + +```javascript +// The values array passed into addData should be one for each dataset in the chart +myBarChart.addData([40, 60], "August"); +// The new data will now animate at the end of the chart. +``` + +#### .removeData( ) + +Calling `removeData()` on your Chart instance will remove the first value for all datasets on the chart. + +```javascript +myBarChart.removeData(); +// The chart will now animate and remove the first bar +``` diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/03-Radar-Chart.md b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/03-Radar-Chart.md new file mode 100644 index 0000000000..03dcf6e83b --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/03-Radar-Chart.md @@ -0,0 +1,177 @@ +--- +title: Radar Chart +anchor: radar-chart +--- + +###Introduction +A radar chart is a way of showing multiple data points and the variation between them. + +They are often useful for comparing the points of two or more different data sets. + +
    + +
    + +###Example usage + +```javascript +var myRadarChart = new Chart(ctx).Radar(data, options); +``` + +###Data structure +```javascript +var data = { + labels: ["Eating", "Drinking", "Sleeping", "Designing", "Coding", "Cycling", "Running"], + datasets: [ + { + label: "My First dataset", + fillColor: "rgba(220,220,220,0.2)", + strokeColor: "rgba(220,220,220,1)", + pointColor: "rgba(220,220,220,1)", + pointStrokeColor: "#fff", + pointHighlightFill: "#fff", + pointHighlightStroke: "rgba(220,220,220,1)", + data: [65, 59, 90, 81, 56, 55, 40] + }, + { + label: "My Second dataset", + fillColor: "rgba(151,187,205,0.2)", + strokeColor: "rgba(151,187,205,1)", + pointColor: "rgba(151,187,205,1)", + pointStrokeColor: "#fff", + pointHighlightFill: "#fff", + pointHighlightStroke: "rgba(151,187,205,1)", + data: [28, 48, 40, 19, 96, 27, 100] + } + ] +}; +``` +For a radar chart, to provide context of what each point means, we include an array of strings that show around each point in the chart. +For the radar chart data, we have an array of datasets. Each of these is an object, with a fill colour, a stroke colour, a colour for the fill of each point, and a colour for the stroke of each point. We also have an array of data values. + +The label key on each dataset is optional, and can be used when generating a scale for the chart. + +### Chart options + +These are the customisation options specific to Radar charts. These options are merged with the [global chart configuration options](#getting-started-global-chart-configuration), and form the options of the chart. + + +```javascript +{ + //Boolean - Whether to show lines for each scale point + scaleShowLine : true, + + //Boolean - Whether we show the angle lines out of the radar + angleShowLineOut : true, + + //Boolean - Whether to show labels on the scale + scaleShowLabels : false, + + // Boolean - Whether the scale should begin at zero + scaleBeginAtZero : true, + + //String - Colour of the angle line + angleLineColor : "rgba(0,0,0,.1)", + + //Number - Pixel width of the angle line + angleLineWidth : 1, + + //String - Point label font declaration + pointLabelFontFamily : "'Arial'", + + //String - Point label font weight + pointLabelFontStyle : "normal", + + //Number - Point label font size in pixels + pointLabelFontSize : 10, + + //String - Point label font colour + pointLabelFontColor : "#666", + + //Boolean - Whether to show a dot for each point + pointDot : true, + + //Number - Radius of each point dot in pixels + pointDotRadius : 3, + + //Number - Pixel width of point dot stroke + pointDotStrokeWidth : 1, + + //Number - amount extra to add to the radius to cater for hit detection outside the drawn point + pointHitDetectionRadius : 20, + + //Boolean - Whether to show a stroke for datasets + datasetStroke : true, + + //Number - Pixel width of dataset stroke + datasetStrokeWidth : 2, + + //Boolean - Whether to fill the dataset with a colour + datasetFill : true, + {% raw %} + //String - A legend template + legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
    • <%}%>
    " + {% endraw %} +} +``` + + +You can override these for your `Chart` instance by passing a second argument into the `Radar` method as an object with the keys you want to override. + +For example, we could have a radar chart without a point for each on piece of data by doing the following: + +```javascript +new Chart(ctx).Radar(data, { + pointDot: false +}); +// This will create a chart with all of the default options, merged from the global config, +// and the Bar chart defaults but this particular instance will have `pointDot` set to false. +``` + +We can also change these defaults values for each Radar type that is created, this object is available at `Chart.defaults.Radar`. + + +### Prototype methods + +#### .getPointsAtEvent( event ) + +Calling `getPointsAtEvent(event)` on your Chart instance passing an argument of an event, or jQuery event, will return the point elements that are at that the same position of that event. + +```javascript +canvas.onclick = function(evt){ + var activePoints = myRadarChart.getPointsAtEvent(evt); + // => activePoints is an array of points on the canvas that are at the same position as the click event. +}; +``` + +This functionality may be useful for implementing DOM based tooltips, or triggering custom behaviour in your application. + +#### .update( ) + +Calling `update()` on your Chart instance will re-render the chart with any updated values, allowing you to edit the value of multiple existing points, then render those in one animated render loop. + +```javascript +myRadarChart.datasets[0].points[2].value = 50; +// Would update the first dataset's value of 'Sleeping' to be 50 +myRadarChart.update(); +// Calling update now animates the position of Sleeping from 90 to 50. +``` + +#### .addData( valuesArray, label ) + +Calling `addData(valuesArray, label)` on your Chart instance passing an array of values for each dataset, along with a label for those points. + +```javascript +// The values array passed into addData should be one for each dataset in the chart +myRadarChart.addData([40, 60], "Dancing"); +// The new data will now animate at the end of the chart. +``` + +#### .removeData( ) + +Calling `removeData()` on your Chart instance will remove the first value for all datasets on the chart. + +```javascript +myRadarChart.removeData(); +// Other points will now animate to their correct positions. +``` \ No newline at end of file diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/04-Polar-Area-Chart.md b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/04-Polar-Area-Chart.md new file mode 100644 index 0000000000..658da54fcd --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/04-Polar-Area-Chart.md @@ -0,0 +1,172 @@ +--- +title: Polar Area Chart +anchor: polar-area-chart +--- +### Introduction +Polar area charts are similar to pie charts, but each segment has the same angle - the radius of the segment differs depending on the value. + +This type of chart is often useful when we want to show a comparison data similar to a pie chart, but also show a scale of values for context. + +
    + +
    + +### Example usage + +```javascript +new Chart(ctx).PolarArea(data, options); +``` + +### Data structure + +```javascript +var data = [ + { + value: 300, + color:"#F7464A", + highlight: "#FF5A5E", + label: "Red" + }, + { + value: 50, + color: "#46BFBD", + highlight: "#5AD3D1", + label: "Green" + }, + { + value: 100, + color: "#FDB45C", + highlight: "#FFC870", + label: "Yellow" + }, + { + value: 40, + color: "#949FB1", + highlight: "#A8B3C5", + label: "Grey" + }, + { + value: 120, + color: "#4D5360", + highlight: "#616774", + label: "Dark Grey" + } + +]; +``` +As you can see, for the chart data you pass in an array of objects, with a value and a colour. The value attribute should be a number, while the color attribute should be a string. Similar to CSS, for this string you can use HEX notation, RGB, RGBA or HSL. + +### Chart options + +These are the customisation options specific to Polar Area charts. These options are merged with the [global chart configuration options](#getting-started-global-chart-configuration), and form the options of the chart. + +```javascript +{ + //Boolean - Show a backdrop to the scale label + scaleShowLabelBackdrop : true, + + //String - The colour of the label backdrop + scaleBackdropColor : "rgba(255,255,255,0.75)", + + // Boolean - Whether the scale should begin at zero + scaleBeginAtZero : true, + + //Number - The backdrop padding above & below the label in pixels + scaleBackdropPaddingY : 2, + + //Number - The backdrop padding to the side of the label in pixels + scaleBackdropPaddingX : 2, + + //Boolean - Show line for each value in the scale + scaleShowLine : true, + + //Boolean - Stroke a line around each segment in the chart + segmentShowStroke : true, + + //String - The colour of the stroke on each segement. + segmentStrokeColor : "#fff", + + //Number - The width of the stroke value in pixels + segmentStrokeWidth : 2, + + //Number - Amount of animation steps + animationSteps : 100, + + //String - Animation easing effect. + animationEasing : "easeOutBounce", + + //Boolean - Whether to animate the rotation of the chart + animateRotate : true, + + //Boolean - Whether to animate scaling the chart from the centre + animateScale : false, + {% raw %} + //String - A legend template + legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
    • <%}%>
    " + {% endraw %} +} +``` + +You can override these for your `Chart` instance by passing a second argument into the `PolarArea` method as an object with the keys you want to override. + +For example, we could have a polar area chart with a black stroke on each segment like so: + +```javascript +new Chart(ctx).PolarArea(data, { + segmentStrokeColor: "#000000" +}); +// This will create a chart with all of the default options, merged from the global config, +// and the PolarArea chart defaults but this particular instance will have `segmentStrokeColor` set to `"#000000"`. +``` + +We can also change these defaults values for each PolarArea type that is created, this object is available at `Chart.defaults.PolarArea`. + +### Prototype methods + +#### .getSegmentsAtEvent( event ) + +Calling `getSegmentsAtEvent(event)` on your Chart instance passing an argument of an event, or jQuery event, will return the segment elements that are at that the same position of that event. + +```javascript +canvas.onclick = function(evt){ + var activePoints = myPolarAreaChart.getSegmentsAtEvent(evt); + // => activePoints is an array of segments on the canvas that are at the same position as the click event. +}; +``` + +This functionality may be useful for implementing DOM based tooltips, or triggering custom behaviour in your application. + +#### .update( ) + +Calling `update()` on your Chart instance will re-render the chart with any updated values, allowing you to edit the value of multiple existing points, then render those in one animated render loop. + +```javascript +myPolarAreaChart.segments[1].value = 10; +// Would update the first dataset's value of 'Green' to be 10 +myPolarAreaChart.update(); +// Calling update now animates the position of Green from 50 to 10. +``` + +#### .addData( segmentData, index ) + +Calling `addData(segmentData, index)` on your Chart instance passing an object in the same format as in the constructor. There is an option second argument of 'index', this determines at what index the new segment should be inserted into the chart. + +```javascript +// An object in the same format as the original data source +myPolarAreaChart.addData({ + value: 130, + color: "#B48EAD", + highlight: "#C69CBE", + label: "Purple" +}); +// The new segment will now animate in. +``` + +#### .removeData( index ) + +Calling `removeData(index)` on your Chart instance will remove segment at that particular index. If none is provided, it will default to the last segment. + +```javascript +myPolarAreaChart.removeData(); +// Other segments will update to fill the empty space left. +``` \ No newline at end of file diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/05-Pie-Doughnut-Chart.md b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/05-Pie-Doughnut-Chart.md new file mode 100644 index 0000000000..dd9de6f404 --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/05-Pie-Doughnut-Chart.md @@ -0,0 +1,158 @@ +--- +title: Pie & Doughnut Charts +anchor: doughnut-pie-chart +--- +###Introduction +Pie and doughnut charts are probably the most commonly used chart there are. They are divided into segments, the arc of each segment shows the proportional value of each piece of data. + +They are excellent at showing the relational proportions between data. + +Pie and doughnut charts are effectively the same class in Chart.js, but have one different default value - their `percentageInnerCutout`. This equates what percentage of the inner should be cut out. This defaults to `0` for pie charts, and `50` for doughnuts. + +They are also registered under two aliases in the `Chart` core. Other than their different default value, and different alias, they are exactly the same. + +
    + +
    + +
    + +
    + + +### Example usage + +```javascript +// For a pie chart +var myPieChart = new Chart(ctx[0]).Pie(data,options); + +// And for a doughnut chart +var myDoughnutChart = new Chart(ctx[1]).Doughnut(data,options); +``` + +### Data structure + +```javascript +var data = [ + { + value: 300, + color:"#F7464A", + highlight: "#FF5A5E", + label: "Red" + }, + { + value: 50, + color: "#46BFBD", + highlight: "#5AD3D1", + label: "Green" + }, + { + value: 100, + color: "#FDB45C", + highlight: "#FFC870", + label: "Yellow" + } +] +``` + +For a pie chart, you must pass in an array of objects with a value and a color property. The value attribute should be a number, Chart.js will total all of the numbers and calculate the relative proportion of each. The color attribute should be a string. Similar to CSS, for this string you can use HEX notation, RGB, RGBA or HSL. + +### Chart options + +These are the customisation options specific to Pie & Doughnut charts. These options are merged with the [global chart configuration options](#getting-started-global-chart-configuration), and form the options of the chart. + +```javascript +{ + //Boolean - Whether we should show a stroke on each segment + segmentShowStroke : true, + + //String - The colour of each segment stroke + segmentStrokeColor : "#fff", + + //Number - The width of each segment stroke + segmentStrokeWidth : 2, + + //Number - The percentage of the chart that we cut out of the middle + percentageInnerCutout : 50, // This is 0 for Pie charts + + //Number - Amount of animation steps + animationSteps : 100, + + //String - Animation easing effect + animationEasing : "easeOutBounce", + + //Boolean - Whether we animate the rotation of the Doughnut + animateRotate : true, + + //Boolean - Whether we animate scaling the Doughnut from the centre + animateScale : false, + {% raw %} + //String - A legend template + legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
    • <%}%>
    " + {% endraw %} +} +``` +You can override these for your `Chart` instance by passing a second argument into the `Doughnut` method as an object with the keys you want to override. + +For example, we could have a doughnut chart that animates by scaling out from the centre like so: + +```javascript +new Chart(ctx).Doughnut(data, { + animateScale: true +}); +// This will create a chart with all of the default options, merged from the global config, +// and the Doughnut chart defaults but this particular instance will have `animateScale` set to `true`. +``` + +We can also change these default values for each Doughnut type that is created, this object is available at `Chart.defaults.Doughnut`. Pie charts also have a clone of these defaults available to change at `Chart.defaults.Pie`, with the only difference being `percentageInnerCutout` being set to 0. + +### Prototype methods + +#### .getSegmentsAtEvent( event ) + +Calling `getSegmentsAtEvent(event)` on your Chart instance passing an argument of an event, or jQuery event, will return the segment elements that are at the same position of that event. + +```javascript +canvas.onclick = function(evt){ + var activePoints = myDoughnutChart.getSegmentsAtEvent(evt); + // => activePoints is an array of segments on the canvas that are at the same position as the click event. +}; +``` + +This functionality may be useful for implementing DOM based tooltips, or triggering custom behaviour in your application. + +#### .update( ) + +Calling `update()` on your Chart instance will re-render the chart with any updated values, allowing you to edit the value of multiple existing points, then render those in one animated render loop. + +```javascript +myDoughnutChart.segments[1].value = 10; +// Would update the first dataset's value of 'Green' to be 10 +myDoughnutChart.update(); +// Calling update now animates the circumference of the segment 'Green' from 50 to 10. +// and transitions other segment widths +``` + +#### .addData( segmentData, index ) + +Calling `addData(segmentData, index)` on your Chart instance passing an object in the same format as in the constructor. There is an optional second argument of 'index', this determines at what index the new segment should be inserted into the chart. + +```javascript +// An object in the same format as the original data source +myDoughnutChart.addData({ + value: 130, + color: "#B48EAD", + highlight: "#C69CBE", + label: "Purple" +}); +// The new segment will now animate in. +``` + +#### .removeData( index ) + +Calling `removeData(index)` on your Chart instance will remove segment at that particular index. If none is provided, it will default to the last segment. + +```javascript +myDoughnutChart.removeData(); +// Other segments will update to fill the empty space left. +``` diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/06-Advanced.md b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/06-Advanced.md new file mode 100644 index 0000000000..f0495bfbba --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/06-Advanced.md @@ -0,0 +1,185 @@ +--- +title: Advanced usage +anchor: advanced-usage +--- + + +### Prototype methods + +For each chart, there are a set of global prototype methods on the shared `ChartType` which you may find useful. These are available on all charts created with Chart.js, but for the examples, let's use a line chart we've made. + +```javascript +// For example: +var myLineChart = new Chart(ctx).Line(data); +``` + +#### .clear() + +Will clear the chart canvas. Used extensively internally between animation frames, but you might find it useful. + +```javascript +// Will clear the canvas that myLineChart is drawn on +myLineChart.clear(); +// => returns 'this' for chainability +``` + +#### .stop() + +Use this to stop any current animation loop. This will pause the chart during any current animation frame. Call `.render()` to re-animate. + +```javascript +// Stops the charts animation loop at its current frame +myLineChart.stop(); +// => returns 'this' for chainability +``` + +#### .resize() + +Use this to manually resize the canvas element. This is run each time the browser is resized, but you can call this method manually if you change the size of the canvas nodes container element. + +```javascript +// Resizes & redraws to fill its container element +myLineChart.resize(); +// => returns 'this' for chainability +``` + +#### .destroy() + +Use this to destroy any chart instances that are created. This will clean up any references stored to the chart object within Chart.js, along with any associated event listeners attached by Chart.js. + +```javascript +// Destroys a specific chart instance +myLineChart.destroy(); +``` + +#### .toBase64Image() + +This returns a base 64 encoded string of the chart in it's current state. + +```javascript +myLineChart.toBase64Image(); +// => returns png data url of the image on the canvas +``` + +#### .generateLegend() + +Returns an HTML string of a legend for that chart. The template for this legend is at `legendTemplate` in the chart options. + +```javascript +myLineChart.generateLegend(); +// => returns HTML string of a legend for this chart +``` + +### External Tooltips + +You can enable custom tooltips in the global or chart configuration like so: + +```javascript +var myPieChart = new Chart(ctx).Pie(data, { + customTooltips: function(tooltip) { + + // tooltip will be false if tooltip is not visible or should be hidden + if (!tooltip) { + return; + } + + // Otherwise, tooltip will be an object with all tooltip properties like: + + // tooltip.caretHeight + // tooltip.caretPadding + // tooltip.chart + // tooltip.cornerRadius + // tooltip.fillColor + // tooltip.font... + // tooltip.text + // tooltip.x + // tooltip.y + // etc... + + }; +}); +``` + +See files `sample/pie-customTooltips.html` and `sample/line-customTooltips.html` for examples on how to get started. + + +### Writing new chart types + +Chart.js 1.0 has been rewritten to provide a platform for developers to create their own custom chart types, and be able to share and utilise them through the Chart.js API. + +The format is relatively simple, there are a set of utility helper methods under `Chart.helpers`, including things such as looping over collections, requesting animation frames, and easing equations. + +On top of this, there are also some simple base classes of Chart elements, these all extend from `Chart.Element`, and include things such as points, bars and scales. + +```javascript +Chart.Type.extend({ + // Passing in a name registers this chart in the Chart namespace + name: "Scatter", + // Providing a defaults will also register the deafults in the chart namespace + defaults : { + options: "Here", + available: "at this.options" + }, + // Initialize is fired when the chart is initialized - Data is passed in as a parameter + // Config is automatically merged by the core of Chart.js, and is available at this.options + initialize: function(data){ + this.chart.ctx // The drawing context for this chart + this.chart.canvas // the canvas node for this chart + }, + // Used to draw something on the canvas + draw: function() { + } +}); + +// Now we can create a new instance of our chart, using the Chart.js API +new Chart(ctx).Scatter(data); +// initialize is now run +``` + +### Extending existing chart types + +We can also extend existing chart types, and expose them to the API in the same way. Let's say for example, we might want to run some more code when we initialize every Line chart. + +```javascript +// Notice now we're extending the particular Line chart type, rather than the base class. +Chart.types.Line.extend({ + // Passing in a name registers this chart in the Chart namespace in the same way + name: "LineAlt", + initialize: function(data){ + console.log('My Line chart extension'); + Chart.types.Line.prototype.initialize.apply(this, arguments); + } +}); + +// Creates a line chart in the same way +new Chart(ctx).LineAlt(data); +// but this logs 'My Line chart extension' in the console. +``` + +### Community extensions + +- Stacked Bar Chart by @Regaddi +- Error bars (bar and line charts) by @CAYdenberg + +### Creating custom builds + +Chart.js uses gulp to build the library into a single JavaScript file. We can use this same build script with custom parameters in order to build a custom version. + +Firstly, we need to ensure development dependencies are installed. With node and npm installed, after cloning the Chart.js repo to a local directory, and navigating to that directory in the command line, we can run the following: + +```bash +npm install +npm install -g gulp +``` + +This will install the local development dependencies for Chart.js, along with a CLI for the JavaScript task runner gulp. + +Now, we can run the `gulp build` task, and pass in a comma seperated list of types as an argument to build a custom version of Chart.js with only specified chart types. + +Here we will create a version of Chart.js with only Line, Radar and Bar charts included: + +```bash +gulp build --types=Line,Radar,Bar +``` + +This will output to the `/custom` directory, and write two files, Chart.js, and Chart.min.js with only those chart types included. diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/07-Notes.md b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/07-Notes.md new file mode 100644 index 0000000000..8ba5a59dd7 --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/docs/07-Notes.md @@ -0,0 +1,42 @@ +--- +title: Notes +anchor: notes +--- + +### Browser support +Browser support for the canvas element is available in all modern & major mobile browsers (caniuse.com/canvas). + +For IE8 & below, I would recommend using the polyfill ExplorerCanvas - available at https://code.google.com/p/explorercanvas/. It falls back to Internet explorer's format VML when canvas support is not available. Example use: + +```html + + + +``` + +Usually I would recommend feature detection to choose whether or not to load a polyfill, rather than IE conditional comments, however in this case, VML is a Microsoft proprietary format, so it will only work in IE. + +Some important points to note in my experience using ExplorerCanvas as a fallback. + +- Initialise charts on load rather than DOMContentReady when using the library, as sometimes a race condition will occur, and it will result in an error when trying to get the 2d context of a canvas. +- New VML DOM elements are being created for each animation frame and there is no hardware acceleration. As a result animation is usually slow and jerky, with flashing text. It is a good idea to dynamically turn off animation based on canvas support. I recommend using the excellent Modernizr to do this. +- When declaring fonts, the library explorercanvas requires the font name to be in single quotes inside the string. For example, instead of your scaleFontFamily property being simply "Arial", explorercanvas support, use "'Arial'" instead. Chart.js does this for default values. + +### Bugs & issues + +Please report these on the GitHub page - at github.com/nnnick/Chart.js. If you could include a link to a simple jsbin or similar to demonstrate the issue, that'd be really helpful. + + +### Contributing +New contributions to the library are welcome, just a couple of guidelines: + +- Tabs for indentation, not spaces please. +- Please ensure you're changing the individual files in `/src`, not the concatenated output in the `Chart.js` file in the root of the repo. +- Please check that your code will pass `jshint` code standards, `gulp jshint` will run this for you. +- Please keep pull requests concise, and document new functionality in the relevant `.md` file. +- Consider whether your changes are useful for all users, or if creating a Chart.js extension would be more appropriate. + +### License +Chart.js is open source and available under the MIT license. \ No newline at end of file diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/gulpfile.js b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/gulpfile.js new file mode 100644 index 0000000000..e8fe1c7115 --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/gulpfile.js @@ -0,0 +1,137 @@ +var gulp = require('gulp'), + concat = require('gulp-concat'), + uglify = require('gulp-uglify'), + util = require('gulp-util'), + jshint = require('gulp-jshint'), + size = require('gulp-size'), + connect = require('gulp-connect'), + replace = require('gulp-replace'), + htmlv = require('gulp-html-validator'), + inquirer = require('inquirer'), + semver = require('semver'), + exec = require('child_process').exec, + fs = require('fs'), + package = require('./package.json'), + bower = require('./bower.json'); + +var srcDir = './src/'; +/* + * Usage : gulp build --types=Bar,Line,Doughnut + * Output: - A built Chart.js file with Core and types Bar, Line and Doughnut concatenated together + * - A minified version of this code, in Chart.min.js + */ + +gulp.task('build', function(){ + + // Default to all of the chart types, with Chart.Core first + var srcFiles = [FileName('Core')], + isCustom = !!(util.env.types), + outputDir = (isCustom) ? 'custom' : '.'; + if (isCustom){ + util.env.types.split(',').forEach(function(type){ return srcFiles.push(FileName(type))}); + } + else{ + // Seems gulp-concat remove duplicates - nice! + // So we can use this to sort out dependency order - aka include Core first! + srcFiles.push(srcDir+'*'); + } + + return gulp.src(srcFiles) + .pipe(concat('Chart.js')) + .pipe(replace('{{ version }}', package.version)) + .pipe(gulp.dest(outputDir)) + .pipe(uglify({preserveComments:'some'})) + .pipe(concat('Chart.min.js')) + .pipe(gulp.dest(outputDir)); + + function FileName(moduleName){ + return srcDir+'Chart.'+moduleName+'.js'; + }; +}); + +/* + * Usage : gulp bump + * Prompts: Version increment to bump + * Output: - New version number written into package.json & bower.json + */ + +gulp.task('bump', function(complete){ + util.log('Current version:', util.colors.cyan(package.version)); + var choices = ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease'].map(function(versionType){ + return versionType + ' (v' + semver.inc(package.version, versionType) + ')'; + }); + inquirer.prompt({ + type: 'list', + name: 'version', + message: 'What version update would you like?', + choices: choices + }, function(res){ + var increment = res.version.split(' ')[0], + newVersion = semver.inc(package.version, increment); + + // Set the new versions into the bower/package object + package.version = newVersion; + bower.version = newVersion; + + // Write these to their own files, then build the output + fs.writeFileSync('package.json', JSON.stringify(package, null, 2)); + fs.writeFileSync('bower.json', JSON.stringify(bower, null, 2)); + + complete(); + }); +}); + +gulp.task('release', ['build'], function(){ + exec('git tag -a v' + package.version); +}); + +gulp.task('jshint', function(){ + return gulp.src(srcDir + '*.js') + .pipe(jshint()) + .pipe(jshint.reporter('default')); +}); + +gulp.task('valid', function(){ + return gulp.src('samples/*.html') + .pipe(htmlv()); +}); + +gulp.task('library-size', function(){ + return gulp.src('Chart.min.js') + .pipe(size({ + gzip: true + })); +}); + +gulp.task('module-sizes', function(){ + return gulp.src(srcDir + '*.js') + .pipe(uglify({preserveComments:'some'})) + .pipe(size({ + showFiles: true, + gzip: true + })) +}); + +gulp.task('watch', function(){ + gulp.watch('./src/*', ['build']); +}); + +gulp.task('test', ['jshint', 'valid']); + +gulp.task('size', ['library-size', 'module-sizes']); + +gulp.task('default', ['build', 'watch']); + +gulp.task('server', function(){ + connect.server({ + port: 8000 + }); +}); + +// Convenience task for opening the project straight from the command line +gulp.task('_open', function(){ + exec('open http://localhost:8000'); + exec('subl .'); +}); + +gulp.task('dev', ['server', 'default']); diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/package.json b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/package.json new file mode 100644 index 0000000000..9ac4a0e71b --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/package.json @@ -0,0 +1,28 @@ +{ + "name": "chart.js", + "homepage": "http://www.chartjs.org", + "description": "Simple HTML5 charts using the canvas element.", + "version": "1.0.2", + "main": "Chart.js", + "repository": { + "type": "git", + "url": "https://github.com/nnnick/Chart.js.git" + }, + "dependences": {}, + "devDependencies": { + "gulp": "3.5.x", + "gulp-concat": "~2.1.x", + "gulp-connect": "~2.0.5", + "gulp-jshint": "~1.5.1", + "gulp-replace": "^0.4.0", + "gulp-size": "~0.4.0", + "gulp-uglify": "~0.2.x", + "gulp-util": "~2.2.x", + "gulp-html-validator": "^0.0.2", + "inquirer": "^0.5.1", + "semver": "^3.0.1" + }, + "spm": { + "main": "Chart.js" + } +} \ No newline at end of file diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/bar.html b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/bar.html new file mode 100644 index 0000000000..5bf4b5bae3 --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/bar.html @@ -0,0 +1,45 @@ + + + + Bar Chart + + + +
    + +
    + + + + + diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/doughnut.html b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/doughnut.html new file mode 100644 index 0000000000..fdf7539a89 --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/doughnut.html @@ -0,0 +1,67 @@ + + + + Doughnut Chart + + + + +
    + +
    + + + + + diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/line-customTooltips.html b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/line-customTooltips.html new file mode 100644 index 0000000000..4dc46e1abc --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/line-customTooltips.html @@ -0,0 +1,129 @@ + + + + + Line Chart with Custom Tooltips + + + + + + + +
    + +
    +
    + +
    + +
    + + + + + + diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/line.html b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/line.html new file mode 100644 index 0000000000..ccd0dad965 --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/line.html @@ -0,0 +1,54 @@ + + + + Line Chart + + + +
    +
    + +
    +
    + + + + + diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/pie-customTooltips.html b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/pie-customTooltips.html new file mode 100644 index 0000000000..732317de46 --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/pie-customTooltips.html @@ -0,0 +1,156 @@ + + + + + Pie Chart with Custom Tooltips + + + + + + + +
    + +
    +
    + +
    + +
    + + + + + + diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/pie.html b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/pie.html new file mode 100644 index 0000000000..255a499761 --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/pie.html @@ -0,0 +1,58 @@ + + + + Pie Chart + + + +
    + +
    + + + + + diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/polar-area.html b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/polar-area.html new file mode 100644 index 0000000000..d3d3f01b47 --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/polar-area.html @@ -0,0 +1,60 @@ + + + + Polar Area Chart + + + +
    + +
    + + + + + diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/radar.html b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/radar.html new file mode 100644 index 0000000000..6a04f879c0 --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/samples/radar.html @@ -0,0 +1,53 @@ + + + + Radar Chart + + + + + +
    + +
    + + + + + diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.Bar.js b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.Bar.js new file mode 100644 index 0000000000..87fb26ddc4 --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.Bar.js @@ -0,0 +1,302 @@ +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + + var defaultConfig = { + //Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value + scaleBeginAtZero : true, + + //Boolean - Whether grid lines are shown across the chart + scaleShowGridLines : true, + + //String - Colour of the grid lines + scaleGridLineColor : "rgba(0,0,0,.05)", + + //Number - Width of the grid lines + scaleGridLineWidth : 1, + + //Boolean - Whether to show horizontal lines (except X axis) + scaleShowHorizontalLines: true, + + //Boolean - Whether to show vertical lines (except Y axis) + scaleShowVerticalLines: true, + + //Boolean - If there is a stroke on each bar + barShowStroke : true, + + //Number - Pixel width of the bar stroke + barStrokeWidth : 2, + + //Number - Spacing between each of the X value sets + barValueSpacing : 5, + + //Number - Spacing between data sets within X values + barDatasetSpacing : 1, + + //String - A legend template + legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
    • <%}%>
    " + + }; + + + Chart.Type.extend({ + name: "Bar", + defaults : defaultConfig, + initialize: function(data){ + + //Expose options as a scope variable here so we can access it in the ScaleClass + var options = this.options; + + this.ScaleClass = Chart.Scale.extend({ + offsetGridLines : true, + calculateBarX : function(datasetCount, datasetIndex, barIndex){ + //Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar + var xWidth = this.calculateBaseWidth(), + xAbsolute = this.calculateX(barIndex) - (xWidth/2), + barWidth = this.calculateBarWidth(datasetCount); + + return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2; + }, + calculateBaseWidth : function(){ + return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing); + }, + calculateBarWidth : function(datasetCount){ + //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset + var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing); + + return (baseWidth / datasetCount); + } + }); + + this.datasets = []; + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : []; + + this.eachBars(function(bar){ + bar.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activeBars, function(activeBar){ + activeBar.fillColor = activeBar.highlightFill; + activeBar.strokeColor = activeBar.highlightStroke; + }); + this.showTooltip(activeBars); + }); + } + + //Declare the extension of the default point, to cater for the options passed in to the constructor + this.BarClass = Chart.Rectangle.extend({ + strokeWidth : this.options.barStrokeWidth, + showStroke : this.options.barShowStroke, + ctx : this.chart.ctx + }); + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset,datasetIndex){ + + var datasetObject = { + label : dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + bars : [] + }; + + this.datasets.push(datasetObject); + + helpers.each(dataset.data,function(dataPoint,index){ + //Add a new point for each piece of data, passing any required data to draw. + datasetObject.bars.push(new this.BarClass({ + value : dataPoint, + label : data.labels[index], + datasetLabel: dataset.label, + strokeColor : dataset.strokeColor, + fillColor : dataset.fillColor, + highlightFill : dataset.highlightFill || dataset.fillColor, + highlightStroke : dataset.highlightStroke || dataset.strokeColor + })); + },this); + + },this); + + this.buildScale(data.labels); + + this.BarClass.prototype.base = this.scale.endPoint; + + this.eachBars(function(bar, index, datasetIndex){ + helpers.extend(bar, { + width : this.scale.calculateBarWidth(this.datasets.length), + x: this.scale.calculateBarX(this.datasets.length, datasetIndex, index), + y: this.scale.endPoint + }); + bar.save(); + }, this); + + this.render(); + }, + update : function(){ + this.scale.update(); + // Reset any highlight colours before updating. + helpers.each(this.activeElements, function(activeElement){ + activeElement.restore(['fillColor', 'strokeColor']); + }); + + this.eachBars(function(bar){ + bar.save(); + }); + this.render(); + }, + eachBars : function(callback){ + helpers.each(this.datasets,function(dataset, datasetIndex){ + helpers.each(dataset.bars, callback, this, datasetIndex); + },this); + }, + getBarsAtEvent : function(e){ + var barsArray = [], + eventPosition = helpers.getRelativePosition(e), + datasetIterator = function(dataset){ + barsArray.push(dataset.bars[barIndex]); + }, + barIndex; + + for (var datasetIndex = 0; datasetIndex < this.datasets.length; datasetIndex++) { + for (barIndex = 0; barIndex < this.datasets[datasetIndex].bars.length; barIndex++) { + if (this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){ + helpers.each(this.datasets, datasetIterator); + return barsArray; + } + } + } + + return barsArray; + }, + buildScale : function(labels){ + var self = this; + + var dataTotal = function(){ + var values = []; + self.eachBars(function(bar){ + values.push(bar.value); + }); + return values; + }; + + var scaleOptions = { + templateString : this.options.scaleLabel, + height : this.chart.height, + width : this.chart.width, + ctx : this.chart.ctx, + textColor : this.options.scaleFontColor, + fontSize : this.options.scaleFontSize, + fontStyle : this.options.scaleFontStyle, + fontFamily : this.options.scaleFontFamily, + valuesCount : labels.length, + beginAtZero : this.options.scaleBeginAtZero, + integersOnly : this.options.scaleIntegersOnly, + calculateYRange: function(currentHeight){ + var updatedRanges = helpers.calculateScaleRange( + dataTotal(), + currentHeight, + this.fontSize, + this.beginAtZero, + this.integersOnly + ); + helpers.extend(this, updatedRanges); + }, + xLabels : labels, + font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), + lineWidth : this.options.scaleLineWidth, + lineColor : this.options.scaleLineColor, + showHorizontalLines : this.options.scaleShowHorizontalLines, + showVerticalLines : this.options.scaleShowVerticalLines, + gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, + gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", + padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0, + showLabels : this.options.scaleShowLabels, + display : this.options.showScale + }; + + if (this.options.scaleOverride){ + helpers.extend(scaleOptions, { + calculateYRange: helpers.noop, + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + }); + } + + this.scale = new this.ScaleClass(scaleOptions); + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + helpers.each(valuesArray,function(value,datasetIndex){ + //Add a new point for each piece of data, passing any required data to draw. + this.datasets[datasetIndex].bars.push(new this.BarClass({ + value : value, + label : label, + x: this.scale.calculateBarX(this.datasets.length, datasetIndex, this.scale.valuesCount+1), + y: this.scale.endPoint, + width : this.scale.calculateBarWidth(this.datasets.length), + base : this.scale.endPoint, + strokeColor : this.datasets[datasetIndex].strokeColor, + fillColor : this.datasets[datasetIndex].fillColor + })); + },this); + + this.scale.addXLabel(label); + //Then re-render the chart. + this.update(); + }, + removeData : function(){ + this.scale.removeXLabel(); + //Then re-render the chart. + helpers.each(this.datasets,function(dataset){ + dataset.bars.shift(); + },this); + this.update(); + }, + reflow : function(){ + helpers.extend(this.BarClass.prototype,{ + y: this.scale.endPoint, + base : this.scale.endPoint + }); + var newScaleProps = helpers.extend({ + height : this.chart.height, + width : this.chart.width + }); + this.scale.update(newScaleProps); + }, + draw : function(ease){ + var easingDecimal = ease || 1; + this.clear(); + + var ctx = this.chart.ctx; + + this.scale.draw(easingDecimal); + + //Draw all the bars for each dataset + helpers.each(this.datasets,function(dataset,datasetIndex){ + helpers.each(dataset.bars,function(bar,index){ + if (bar.hasValue()){ + bar.base = this.scale.endPoint; + //Transition then draw + bar.transition({ + x : this.scale.calculateBarX(this.datasets.length, datasetIndex, index), + y : this.scale.calculateY(bar.value), + width : this.scale.calculateBarWidth(this.datasets.length) + }, easingDecimal).draw(); + } + },this); + + },this); + } + }); + + +}).call(this); diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.Core.js b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.Core.js new file mode 100755 index 0000000000..5dccd2ef9b --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.Core.js @@ -0,0 +1,2021 @@ +/*! + * Chart.js + * http://chartjs.org/ + * Version: {{ version }} + * + * Copyright 2015 Nick Downie + * Released under the MIT license + * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md + */ + + +(function(){ + + "use strict"; + + //Declare root variable - window in the browser, global on the server + var root = this, + previous = root.Chart; + + //Occupy the global variable of Chart, and create a simple base class + var Chart = function(context){ + var chart = this; + this.canvas = context.canvas; + + this.ctx = context; + + //Variables global to the chart + var computeDimension = function(element,dimension) + { + if (element['offset'+dimension]) + { + return element['offset'+dimension]; + } + else + { + return document.defaultView.getComputedStyle(element).getPropertyValue(dimension); + } + } + + var width = this.width = computeDimension(context.canvas,'Width'); + var height = this.height = computeDimension(context.canvas,'Height'); + + // Firefox requires this to work correctly + context.canvas.width = width; + context.canvas.height = height; + + var width = this.width = context.canvas.width; + var height = this.height = context.canvas.height; + this.aspectRatio = this.width / this.height; + //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. + helpers.retinaScale(this); + + return this; + }; + //Globally expose the defaults to allow for user updating/changing + Chart.defaults = { + global: { + // Boolean - Whether to animate the chart + animation: true, + + // Number - Number of animation steps + animationSteps: 60, + + // String - Animation easing effect + animationEasing: "easeOutQuart", + + // Boolean - If we should show the scale at all + showScale: true, + + // Boolean - If we want to override with a hard coded scale + scaleOverride: false, + + // ** Required if scaleOverride is true ** + // Number - The number of steps in a hard coded scale + scaleSteps: null, + // Number - The value jump in the hard coded scale + scaleStepWidth: null, + // Number - The scale starting value + scaleStartValue: null, + + // String - Colour of the scale line + scaleLineColor: "rgba(0,0,0,.1)", + + // Number - Pixel width of the scale line + scaleLineWidth: 1, + + // Boolean - Whether to show labels on the scale + scaleShowLabels: true, + + // Interpolated JS string - can access value + scaleLabel: "<%=value%>", + + // Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there + scaleIntegersOnly: true, + + // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value + scaleBeginAtZero: false, + + // String - Scale label font declaration for the scale label + scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Scale label font size in pixels + scaleFontSize: 12, + + // String - Scale label font weight style + scaleFontStyle: "normal", + + // String - Scale label font colour + scaleFontColor: "#666", + + // Boolean - whether or not the chart should be responsive and resize when the browser does. + responsive: false, + + // Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container + maintainAspectRatio: true, + + // Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove + showTooltips: true, + + // Boolean - Determines whether to draw built-in tooltip or call custom tooltip function + customTooltips: false, + + // Array - Array of string names to attach tooltip events + tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"], + + // String - Tooltip background colour + tooltipFillColor: "rgba(0,0,0,0.8)", + + // String - Tooltip label font declaration for the scale label + tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Tooltip label font size in pixels + tooltipFontSize: 14, + + // String - Tooltip font weight style + tooltipFontStyle: "normal", + + // String - Tooltip label font colour + tooltipFontColor: "#fff", + + // String - Tooltip title font declaration for the scale label + tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Tooltip title font size in pixels + tooltipTitleFontSize: 14, + + // String - Tooltip title font weight style + tooltipTitleFontStyle: "bold", + + // String - Tooltip title font colour + tooltipTitleFontColor: "#fff", + + // Number - pixel width of padding around tooltip text + tooltipYPadding: 6, + + // Number - pixel width of padding around tooltip text + tooltipXPadding: 6, + + // Number - Size of the caret on the tooltip + tooltipCaretSize: 8, + + // Number - Pixel radius of the tooltip border + tooltipCornerRadius: 6, + + // Number - Pixel offset from point x to tooltip edge + tooltipXOffset: 10, + + // String - Template string for single tooltips + tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>", + + // String - Template string for single tooltips + multiTooltipTemplate: "<%= value %>", + + // String - Colour behind the legend colour block + multiTooltipKeyBackground: '#fff', + + // Function - Will fire on animation progression. + onAnimationProgress: function(){}, + + // Function - Will fire on animation completion. + onAnimationComplete: function(){} + + } + }; + + //Create a dictionary of chart types, to allow for extension of existing types + Chart.types = {}; + + //Global Chart helpers object for utility methods and classes + var helpers = Chart.helpers = {}; + + //-- Basic js utility methods + var each = helpers.each = function(loopable,callback,self){ + var additionalArgs = Array.prototype.slice.call(arguments, 3); + // Check to see if null or undefined firstly. + if (loopable){ + if (loopable.length === +loopable.length){ + var i; + for (i=0; i= 0; i--) { + var currentItem = arrayToSearch[i]; + if (filterCallback(currentItem)){ + return currentItem; + } + } + }, + inherits = helpers.inherits = function(extensions){ + //Basic javascript inheritance based on the model created in Backbone.js + var parent = this; + var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); }; + + var Surrogate = function(){ this.constructor = ChartElement;}; + Surrogate.prototype = parent.prototype; + ChartElement.prototype = new Surrogate(); + + ChartElement.extend = inherits; + + if (extensions) extend(ChartElement.prototype, extensions); + + ChartElement.__super__ = parent.prototype; + + return ChartElement; + }, + noop = helpers.noop = function(){}, + uid = helpers.uid = (function(){ + var id=0; + return function(){ + return "chart-" + id++; + }; + })(), + warn = helpers.warn = function(str){ + //Method for warning of errors + if (window.console && typeof window.console.warn == "function") console.warn(str); + }, + amd = helpers.amd = (typeof define == 'function' && define.amd), + //-- Math methods + isNumber = helpers.isNumber = function(n){ + return !isNaN(parseFloat(n)) && isFinite(n); + }, + max = helpers.max = function(array){ + return Math.max.apply( Math, array ); + }, + min = helpers.min = function(array){ + return Math.min.apply( Math, array ); + }, + cap = helpers.cap = function(valueToCap,maxValue,minValue){ + if(isNumber(maxValue)) { + if( valueToCap > maxValue ) { + return maxValue; + } + } + else if(isNumber(minValue)){ + if ( valueToCap < minValue ){ + return minValue; + } + } + return valueToCap; + }, + getDecimalPlaces = helpers.getDecimalPlaces = function(num){ + if (num%1!==0 && isNumber(num)){ + return num.toString().split(".")[1].length; + } + else { + return 0; + } + }, + toRadians = helpers.radians = function(degrees){ + return degrees * (Math.PI/180); + }, + // Gets the angle from vertical upright to the point about a centre. + getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){ + var distanceFromXCenter = anglePoint.x - centrePoint.x, + distanceFromYCenter = anglePoint.y - centrePoint.y, + radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); + + + var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter); + + //If the segment is in the top left quadrant, we need to add another rotation to the angle + if (distanceFromXCenter < 0 && distanceFromYCenter < 0){ + angle += Math.PI*2; + } + + return { + angle: angle, + distance: radialDistanceFromCenter + }; + }, + aliasPixel = helpers.aliasPixel = function(pixelWidth){ + return (pixelWidth % 2 === 0) ? 0 : 0.5; + }, + splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){ + //Props to Rob Spencer at scaled innovation for his post on splining between points + //http://scaledinnovation.com/analytics/splines/aboutSplines.html + var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)), + d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)), + fa=t*d01/(d01+d12),// scaling factor for triangle Ta + fb=t*d12/(d01+d12); + return { + inner : { + x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x), + y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y) + }, + outer : { + x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x), + y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y) + } + }; + }, + calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){ + return Math.floor(Math.log(val) / Math.LN10); + }, + calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){ + + //Set a minimum step of two - a point at the top of the graph, and a point at the base + var minSteps = 2, + maxSteps = Math.floor(drawingSize/(textSize * 1.5)), + skipFitting = (minSteps >= maxSteps); + + var maxValue = max(valuesArray), + minValue = min(valuesArray); + + // We need some degree of seperation here to calculate the scales if all the values are the same + // Adding/minusing 0.5 will give us a range of 1. + if (maxValue === minValue){ + maxValue += 0.5; + // So we don't end up with a graph with a negative start value if we've said always start from zero + if (minValue >= 0.5 && !startFromZero){ + minValue -= 0.5; + } + else{ + // Make up a whole number above the values + maxValue += 0.5; + } + } + + var valueRange = Math.abs(maxValue - minValue), + rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange), + graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), + graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), + graphRange = graphMax - graphMin, + stepValue = Math.pow(10, rangeOrderOfMagnitude), + numberOfSteps = Math.round(graphRange / stepValue); + + //If we have more space on the graph we'll use it to give more definition to the data + while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) { + if(numberOfSteps > maxSteps){ + stepValue *=2; + numberOfSteps = Math.round(graphRange/stepValue); + // Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps. + if (numberOfSteps % 1 !== 0){ + skipFitting = true; + } + } + //We can fit in double the amount of scale points on the scale + else{ + //If user has declared ints only, and the step value isn't a decimal + if (integersOnly && rangeOrderOfMagnitude >= 0){ + //If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float + if(stepValue/2 % 1 === 0){ + stepValue /=2; + numberOfSteps = Math.round(graphRange/stepValue); + } + //If it would make it a float break out of the loop + else{ + break; + } + } + //If the scale doesn't have to be an int, make the scale more granular anyway. + else{ + stepValue /=2; + numberOfSteps = Math.round(graphRange/stepValue); + } + + } + } + + if (skipFitting){ + numberOfSteps = minSteps; + stepValue = graphRange / numberOfSteps; + } + + return { + steps : numberOfSteps, + stepValue : stepValue, + min : graphMin, + max : graphMin + (numberOfSteps * stepValue) + }; + + }, + /* jshint ignore:start */ + // Blows up jshint errors based on the new Function constructor + //Templating methods + //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/ + template = helpers.template = function(templateString, valuesObject){ + + // If templateString is function rather than string-template - call the function for valuesObject + + if(templateString instanceof Function){ + return templateString(valuesObject); + } + + var cache = {}; + 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) ? + cache[str] = cache[str] : + + // Generate a reusable function that will serve as a template + // generator (and which will be cached). + new Function("obj", + "var p=[],print=function(){p.push.apply(p,arguments);};" + + + // Introduce the data as local variables using with(){} + "with(obj){p.push('" + + + // Convert the template into pure JavaScript + str + .replace(/[\r\t\n]/g, " ") + .split("<%").join("\t") + .replace(/((^|%>)[^\t]*)'/g, "$1\r") + .replace(/\t=(.*?)%>/g, "',$1,'") + .split("\t").join("');") + .split("%>").join("p.push('") + .split("\r").join("\\'") + + "');}return p.join('');" + ); + + // Provide some basic currying to the user + return data ? fn( data ) : fn; + } + return tmpl(templateString,valuesObject); + }, + /* jshint ignore:end */ + generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){ + var labelsArray = new Array(numberOfSteps); + if (labelTemplateString){ + each(labelsArray,function(val,index){ + labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))}); + }); + } + return labelsArray; + }, + //--Animation methods + //Easing functions adapted from Robert Penner's easing equations + //http://www.robertpenner.com/easing/ + easingEffects = helpers.easingEffects = { + linear: function (t) { + return t; + }, + easeInQuad: function (t) { + return t * t; + }, + easeOutQuad: function (t) { + return -1 * t * (t - 2); + }, + easeInOutQuad: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t; + return -1 / 2 * ((--t) * (t - 2) - 1); + }, + easeInCubic: function (t) { + return t * t * t; + }, + easeOutCubic: function (t) { + return 1 * ((t = t / 1 - 1) * t * t + 1); + }, + easeInOutCubic: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t; + return 1 / 2 * ((t -= 2) * t * t + 2); + }, + easeInQuart: function (t) { + return t * t * t * t; + }, + easeOutQuart: function (t) { + return -1 * ((t = t / 1 - 1) * t * t * t - 1); + }, + easeInOutQuart: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t; + return -1 / 2 * ((t -= 2) * t * t * t - 2); + }, + easeInQuint: function (t) { + return 1 * (t /= 1) * t * t * t * t; + }, + easeOutQuint: function (t) { + return 1 * ((t = t / 1 - 1) * t * t * t * t + 1); + }, + easeInOutQuint: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t * t; + return 1 / 2 * ((t -= 2) * t * t * t * t + 2); + }, + easeInSine: function (t) { + return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1; + }, + easeOutSine: function (t) { + return 1 * Math.sin(t / 1 * (Math.PI / 2)); + }, + easeInOutSine: function (t) { + return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1); + }, + easeInExpo: function (t) { + return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1)); + }, + easeOutExpo: function (t) { + return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1); + }, + easeInOutExpo: function (t) { + if (t === 0) return 0; + if (t === 1) return 1; + if ((t /= 1 / 2) < 1) return 1 / 2 * Math.pow(2, 10 * (t - 1)); + return 1 / 2 * (-Math.pow(2, -10 * --t) + 2); + }, + easeInCirc: function (t) { + if (t >= 1) return t; + return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1); + }, + easeOutCirc: function (t) { + return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t); + }, + easeInOutCirc: function (t) { + if ((t /= 1 / 2) < 1) return -1 / 2 * (Math.sqrt(1 - t * t) - 1); + return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1); + }, + easeInElastic: function (t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) return 0; + if ((t /= 1) == 1) return 1; + if (!p) p = 1 * 0.3; + if (a < Math.abs(1)) { + a = 1; + s = p / 4; + } else s = p / (2 * Math.PI) * Math.asin(1 / a); + return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); + }, + easeOutElastic: function (t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) return 0; + if ((t /= 1) == 1) return 1; + if (!p) p = 1 * 0.3; + if (a < Math.abs(1)) { + a = 1; + s = p / 4; + } else s = p / (2 * Math.PI) * Math.asin(1 / a); + return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1; + }, + easeInOutElastic: function (t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) return 0; + if ((t /= 1 / 2) == 2) return 1; + if (!p) p = 1 * (0.3 * 1.5); + if (a < Math.abs(1)) { + a = 1; + s = p / 4; + } else s = p / (2 * Math.PI) * Math.asin(1 / a); + if (t < 1) return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); + return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1; + }, + easeInBack: function (t) { + var s = 1.70158; + return 1 * (t /= 1) * t * ((s + 1) * t - s); + }, + easeOutBack: function (t) { + var s = 1.70158; + return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1); + }, + easeInOutBack: function (t) { + var s = 1.70158; + if ((t /= 1 / 2) < 1) return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)); + return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2); + }, + easeInBounce: function (t) { + return 1 - easingEffects.easeOutBounce(1 - t); + }, + easeOutBounce: function (t) { + if ((t /= 1) < (1 / 2.75)) { + return 1 * (7.5625 * t * t); + } else if (t < (2 / 2.75)) { + return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75); + } else if (t < (2.5 / 2.75)) { + return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375); + } else { + return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375); + } + }, + easeInOutBounce: function (t) { + if (t < 1 / 2) return easingEffects.easeInBounce(t * 2) * 0.5; + return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5; + } + }, + //Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ + requestAnimFrame = helpers.requestAnimFrame = (function(){ + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback) { + return window.setTimeout(callback, 1000 / 60); + }; + })(), + cancelAnimFrame = helpers.cancelAnimFrame = (function(){ + return window.cancelAnimationFrame || + window.webkitCancelAnimationFrame || + window.mozCancelAnimationFrame || + window.oCancelAnimationFrame || + window.msCancelAnimationFrame || + function(callback) { + return window.clearTimeout(callback, 1000 / 60); + }; + })(), + animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){ + + var currentStep = 0, + easingFunction = easingEffects[easingString] || easingEffects.linear; + + var animationFrame = function(){ + currentStep++; + var stepDecimal = currentStep/totalSteps; + var easeDecimal = easingFunction(stepDecimal); + + callback.call(chartInstance,easeDecimal,stepDecimal, currentStep); + onProgress.call(chartInstance,easeDecimal,stepDecimal); + if (currentStep < totalSteps){ + chartInstance.animationFrame = requestAnimFrame(animationFrame); + } else{ + onComplete.apply(chartInstance); + } + }; + requestAnimFrame(animationFrame); + }, + //-- DOM methods + getRelativePosition = helpers.getRelativePosition = function(evt){ + var mouseX, mouseY; + var e = evt.originalEvent || evt, + canvas = evt.currentTarget || evt.srcElement, + boundingRect = canvas.getBoundingClientRect(); + + if (e.touches){ + mouseX = e.touches[0].clientX - boundingRect.left; + mouseY = e.touches[0].clientY - boundingRect.top; + + } + else{ + mouseX = e.clientX - boundingRect.left; + mouseY = e.clientY - boundingRect.top; + } + + return { + x : mouseX, + y : mouseY + }; + + }, + addEvent = helpers.addEvent = function(node,eventType,method){ + if (node.addEventListener){ + node.addEventListener(eventType,method); + } else if (node.attachEvent){ + node.attachEvent("on"+eventType, method); + } else { + node["on"+eventType] = method; + } + }, + removeEvent = helpers.removeEvent = function(node, eventType, handler){ + if (node.removeEventListener){ + node.removeEventListener(eventType, handler, false); + } else if (node.detachEvent){ + node.detachEvent("on"+eventType,handler); + } else{ + node["on" + eventType] = noop; + } + }, + bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){ + // Create the events object if it's not already present + if (!chartInstance.events) chartInstance.events = {}; + + each(arrayOfEvents,function(eventName){ + chartInstance.events[eventName] = function(){ + handler.apply(chartInstance, arguments); + }; + addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]); + }); + }, + unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) { + each(arrayOfEvents, function(handler,eventName){ + removeEvent(chartInstance.chart.canvas, eventName, handler); + }); + }, + getMaximumWidth = helpers.getMaximumWidth = function(domNode){ + var container = domNode.parentNode; + // TODO = check cross browser stuff with this. + return container.clientWidth; + }, + getMaximumHeight = helpers.getMaximumHeight = function(domNode){ + var container = domNode.parentNode; + // TODO = check cross browser stuff with this. + return container.clientHeight; + }, + getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support + retinaScale = helpers.retinaScale = function(chart){ + var ctx = chart.ctx, + width = chart.canvas.width, + height = chart.canvas.height; + + if (window.devicePixelRatio) { + ctx.canvas.style.width = width + "px"; + ctx.canvas.style.height = height + "px"; + ctx.canvas.height = height * window.devicePixelRatio; + ctx.canvas.width = width * window.devicePixelRatio; + ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + } + }, + //-- Canvas methods + clear = helpers.clear = function(chart){ + chart.ctx.clearRect(0,0,chart.width,chart.height); + }, + fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){ + return fontStyle + " " + pixelSize+"px " + fontFamily; + }, + longestText = helpers.longestText = function(ctx,font,arrayOfStrings){ + ctx.font = font; + var longest = 0; + each(arrayOfStrings,function(string){ + var textWidth = ctx.measureText(string).width; + longest = (textWidth > longest) ? textWidth : longest; + }); + return longest; + }, + drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){ + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); + }; + + + //Store a reference to each instance - allowing us to globally resize chart instances on window resize. + //Destroy method on the chart will remove the instance of the chart from this reference. + Chart.instances = {}; + + Chart.Type = function(data,options,chart){ + this.options = options; + this.chart = chart; + this.id = uid(); + //Add the chart instance to the global namespace + Chart.instances[this.id] = this; + + // Initialize is always called when a chart type is created + // By default it is a no op, but it should be extended + if (options.responsive){ + this.resize(); + } + this.initialize.call(this,data); + }; + + //Core methods that'll be a part of every chart type + extend(Chart.Type.prototype,{ + initialize : function(){return this;}, + clear : function(){ + clear(this.chart); + return this; + }, + stop : function(){ + // Stops any current animation loop occuring + cancelAnimFrame(this.animationFrame); + return this; + }, + resize : function(callback){ + this.stop(); + var canvas = this.chart.canvas, + newWidth = getMaximumWidth(this.chart.canvas), + newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas); + + canvas.width = this.chart.width = newWidth; + canvas.height = this.chart.height = newHeight; + + retinaScale(this.chart); + + if (typeof callback === "function"){ + callback.apply(this, Array.prototype.slice.call(arguments, 1)); + } + return this; + }, + reflow : noop, + render : function(reflow){ + if (reflow){ + this.reflow(); + } + if (this.options.animation && !reflow){ + helpers.animationLoop( + this.draw, + this.options.animationSteps, + this.options.animationEasing, + this.options.onAnimationProgress, + this.options.onAnimationComplete, + this + ); + } + else{ + this.draw(); + this.options.onAnimationComplete.call(this); + } + return this; + }, + generateLegend : function(){ + return template(this.options.legendTemplate,this); + }, + destroy : function(){ + this.clear(); + unbindEvents(this, this.events); + var canvas = this.chart.canvas; + + // Reset canvas height/width attributes starts a fresh with the canvas context + canvas.width = this.chart.width; + canvas.height = this.chart.height; + + // < IE9 doesn't support removeProperty + if (canvas.style.removeProperty) { + canvas.style.removeProperty('width'); + canvas.style.removeProperty('height'); + } else { + canvas.style.removeAttribute('width'); + canvas.style.removeAttribute('height'); + } + + delete Chart.instances[this.id]; + }, + showTooltip : function(ChartElements, forceRedraw){ + // Only redraw the chart if we've actually changed what we're hovering on. + if (typeof this.activeElements === 'undefined') this.activeElements = []; + + var isChanged = (function(Elements){ + var changed = false; + + if (Elements.length !== this.activeElements.length){ + changed = true; + return changed; + } + + each(Elements, function(element, index){ + if (element !== this.activeElements[index]){ + changed = true; + } + }, this); + return changed; + }).call(this, ChartElements); + + if (!isChanged && !forceRedraw){ + return; + } + else{ + this.activeElements = ChartElements; + } + this.draw(); + if(this.options.customTooltips){ + this.options.customTooltips(false); + } + if (ChartElements.length > 0){ + // If we have multiple datasets, show a MultiTooltip for all of the data points at that index + if (this.datasets && this.datasets.length > 1) { + var dataArray, + dataIndex; + + for (var i = this.datasets.length - 1; i >= 0; i--) { + dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments; + dataIndex = indexOf(dataArray, ChartElements[0]); + if (dataIndex !== -1){ + break; + } + } + var tooltipLabels = [], + tooltipColors = [], + medianPosition = (function(index) { + + // Get all the points at that particular index + var Elements = [], + dataCollection, + xPositions = [], + yPositions = [], + xMax, + yMax, + xMin, + yMin; + helpers.each(this.datasets, function(dataset){ + dataCollection = dataset.points || dataset.bars || dataset.segments; + if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){ + Elements.push(dataCollection[dataIndex]); + } + }); + + helpers.each(Elements, function(element) { + xPositions.push(element.x); + yPositions.push(element.y); + + + //Include any colour information about the element + tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element)); + tooltipColors.push({ + fill: element._saved.fillColor || element.fillColor, + stroke: element._saved.strokeColor || element.strokeColor + }); + + }, this); + + yMin = min(yPositions); + yMax = max(yPositions); + + xMin = min(xPositions); + xMax = max(xPositions); + + return { + x: (xMin > this.chart.width/2) ? xMin : xMax, + y: (yMin + yMax)/2 + }; + }).call(this, dataIndex); + + new Chart.MultiTooltip({ + x: medianPosition.x, + y: medianPosition.y, + xPadding: this.options.tooltipXPadding, + yPadding: this.options.tooltipYPadding, + xOffset: this.options.tooltipXOffset, + fillColor: this.options.tooltipFillColor, + textColor: this.options.tooltipFontColor, + fontFamily: this.options.tooltipFontFamily, + fontStyle: this.options.tooltipFontStyle, + fontSize: this.options.tooltipFontSize, + titleTextColor: this.options.tooltipTitleFontColor, + titleFontFamily: this.options.tooltipTitleFontFamily, + titleFontStyle: this.options.tooltipTitleFontStyle, + titleFontSize: this.options.tooltipTitleFontSize, + cornerRadius: this.options.tooltipCornerRadius, + labels: tooltipLabels, + legendColors: tooltipColors, + legendColorBackground : this.options.multiTooltipKeyBackground, + title: ChartElements[0].label, + chart: this.chart, + ctx: this.chart.ctx, + custom: this.options.customTooltips + }).draw(); + + } else { + each(ChartElements, function(Element) { + var tooltipPosition = Element.tooltipPosition(); + new Chart.Tooltip({ + x: Math.round(tooltipPosition.x), + y: Math.round(tooltipPosition.y), + xPadding: this.options.tooltipXPadding, + yPadding: this.options.tooltipYPadding, + fillColor: this.options.tooltipFillColor, + textColor: this.options.tooltipFontColor, + fontFamily: this.options.tooltipFontFamily, + fontStyle: this.options.tooltipFontStyle, + fontSize: this.options.tooltipFontSize, + caretHeight: this.options.tooltipCaretSize, + cornerRadius: this.options.tooltipCornerRadius, + text: template(this.options.tooltipTemplate, Element), + chart: this.chart, + custom: this.options.customTooltips + }).draw(); + }, this); + } + } + return this; + }, + toBase64Image : function(){ + return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments); + } + }); + + Chart.Type.extend = function(extensions){ + + var parent = this; + + var ChartType = function(){ + return parent.apply(this,arguments); + }; + + //Copy the prototype object of the this class + ChartType.prototype = clone(parent.prototype); + //Now overwrite some of the properties in the base class with the new extensions + extend(ChartType.prototype, extensions); + + ChartType.extend = Chart.Type.extend; + + if (extensions.name || parent.prototype.name){ + + var chartName = extensions.name || parent.prototype.name; + //Assign any potential default values of the new chart type + + //If none are defined, we'll use a clone of the chart type this is being extended from. + //I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart + //doesn't define some defaults of their own. + + var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {}; + + Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults); + + Chart.types[chartName] = ChartType; + + //Register this new chart type in the Chart prototype + Chart.prototype[chartName] = function(data,options){ + var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {}); + return new ChartType(data,config,this); + }; + } else{ + warn("Name not provided for this chart, so it hasn't been registered"); + } + return parent; + }; + + Chart.Element = function(configuration){ + extend(this,configuration); + this.initialize.apply(this,arguments); + this.save(); + }; + extend(Chart.Element.prototype,{ + initialize : function(){}, + restore : function(props){ + if (!props){ + extend(this,this._saved); + } else { + each(props,function(key){ + this[key] = this._saved[key]; + },this); + } + return this; + }, + save : function(){ + this._saved = clone(this); + delete this._saved._saved; + return this; + }, + update : function(newProps){ + each(newProps,function(value,key){ + this._saved[key] = this[key]; + this[key] = value; + },this); + return this; + }, + transition : function(props,ease){ + each(props,function(value,key){ + this[key] = ((value - this._saved[key]) * ease) + this._saved[key]; + },this); + return this; + }, + tooltipPosition : function(){ + return { + x : this.x, + y : this.y + }; + }, + hasValue: function(){ + return isNumber(this.value); + } + }); + + Chart.Element.extend = inherits; + + + Chart.Point = Chart.Element.extend({ + display: true, + inRange: function(chartX,chartY){ + var hitDetectionRange = this.hitDetectionRadius + this.radius; + return ((Math.pow(chartX-this.x, 2)+Math.pow(chartY-this.y, 2)) < Math.pow(hitDetectionRange,2)); + }, + draw : function(){ + if (this.display){ + var ctx = this.ctx; + ctx.beginPath(); + + ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2); + ctx.closePath(); + + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + ctx.fillStyle = this.fillColor; + + ctx.fill(); + ctx.stroke(); + } + + + //Quick debug for bezier curve splining + //Highlights control points and the line between them. + //Handy for dev - stripped in the min version. + + // ctx.save(); + // ctx.fillStyle = "black"; + // ctx.strokeStyle = "black" + // ctx.beginPath(); + // ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2); + // ctx.fill(); + + // ctx.beginPath(); + // ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2); + // ctx.fill(); + + // ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y); + // ctx.lineTo(this.x, this.y); + // ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y); + // ctx.stroke(); + + // ctx.restore(); + + + + } + }); + + Chart.Arc = Chart.Element.extend({ + inRange : function(chartX,chartY){ + + var pointRelativePosition = helpers.getAngleFromPoint(this, { + x: chartX, + y: chartY + }); + + //Check if within the range of the open/close angle + var betweenAngles = (pointRelativePosition.angle >= this.startAngle && pointRelativePosition.angle <= this.endAngle), + withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius); + + return (betweenAngles && withinRadius); + //Ensure within the outside of the arc centre, but inside arc outer + }, + tooltipPosition : function(){ + var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2), + rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius; + return { + x : this.x + (Math.cos(centreAngle) * rangeFromCentre), + y : this.y + (Math.sin(centreAngle) * rangeFromCentre) + }; + }, + draw : function(animationPercent){ + + var easingDecimal = animationPercent || 1; + + var ctx = this.ctx; + + ctx.beginPath(); + + ctx.arc(this.x, this.y, this.outerRadius, this.startAngle, this.endAngle); + + ctx.arc(this.x, this.y, this.innerRadius, this.endAngle, this.startAngle, true); + + ctx.closePath(); + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + ctx.fillStyle = this.fillColor; + + ctx.fill(); + ctx.lineJoin = 'bevel'; + + if (this.showStroke){ + ctx.stroke(); + } + } + }); + + Chart.Rectangle = Chart.Element.extend({ + draw : function(){ + var ctx = this.ctx, + halfWidth = this.width/2, + leftX = this.x - halfWidth, + rightX = this.x + halfWidth, + top = this.base - (this.base - this.y), + halfStroke = this.strokeWidth / 2; + + // Canvas doesn't allow us to stroke inside the width so we can + // adjust the sizes to fit if we're setting a stroke on the line + if (this.showStroke){ + leftX += halfStroke; + rightX -= halfStroke; + top += halfStroke; + } + + ctx.beginPath(); + + ctx.fillStyle = this.fillColor; + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + // It'd be nice to keep this class totally generic to any rectangle + // and simply specify which border to miss out. + ctx.moveTo(leftX, this.base); + ctx.lineTo(leftX, top); + ctx.lineTo(rightX, top); + ctx.lineTo(rightX, this.base); + ctx.fill(); + if (this.showStroke){ + ctx.stroke(); + } + }, + height : function(){ + return this.base - this.y; + }, + inRange : function(chartX,chartY){ + return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base); + } + }); + + Chart.Tooltip = Chart.Element.extend({ + draw : function(){ + + var ctx = this.chart.ctx; + + ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); + + this.xAlign = "center"; + this.yAlign = "above"; + + //Distance between the actual element.y position and the start of the tooltip caret + var caretPadding = this.caretPadding = 2; + + var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding, + tooltipRectHeight = this.fontSize + 2*this.yPadding, + tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding; + + if (this.x + tooltipWidth/2 >this.chart.width){ + this.xAlign = "left"; + } else if (this.x - tooltipWidth/2 < 0){ + this.xAlign = "right"; + } + + if (this.y - tooltipHeight < 0){ + this.yAlign = "below"; + } + + + var tooltipX = this.x - tooltipWidth/2, + tooltipY = this.y - tooltipHeight; + + ctx.fillStyle = this.fillColor; + + // Custom Tooltips + if(this.custom){ + this.custom(this); + } + else{ + switch(this.yAlign) + { + case "above": + //Draw a caret above the x/y + ctx.beginPath(); + ctx.moveTo(this.x,this.y - caretPadding); + ctx.lineTo(this.x + this.caretHeight, this.y - (caretPadding + this.caretHeight)); + ctx.lineTo(this.x - this.caretHeight, this.y - (caretPadding + this.caretHeight)); + ctx.closePath(); + ctx.fill(); + break; + case "below": + tooltipY = this.y + caretPadding + this.caretHeight; + //Draw a caret below the x/y + ctx.beginPath(); + ctx.moveTo(this.x, this.y + caretPadding); + ctx.lineTo(this.x + this.caretHeight, this.y + caretPadding + this.caretHeight); + ctx.lineTo(this.x - this.caretHeight, this.y + caretPadding + this.caretHeight); + ctx.closePath(); + ctx.fill(); + break; + } + + switch(this.xAlign) + { + case "left": + tooltipX = this.x - tooltipWidth + (this.cornerRadius + this.caretHeight); + break; + case "right": + tooltipX = this.x - (this.cornerRadius + this.caretHeight); + break; + } + + drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius); + + ctx.fill(); + + ctx.fillStyle = this.textColor; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2); + } + } + }); + + Chart.MultiTooltip = Chart.Element.extend({ + initialize : function(){ + this.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); + + this.titleFont = fontString(this.titleFontSize,this.titleFontStyle,this.titleFontFamily); + + this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleFontSize *1.5; + + this.ctx.font = this.titleFont; + + var titleWidth = this.ctx.measureText(this.title).width, + //Label has a legend square as well so account for this. + labelWidth = longestText(this.ctx,this.font,this.labels) + this.fontSize + 3, + longestTextWidth = max([labelWidth,titleWidth]); + + this.width = longestTextWidth + (this.xPadding*2); + + + var halfHeight = this.height/2; + + //Check to ensure the height will fit on the canvas + if (this.y - halfHeight < 0 ){ + this.y = halfHeight; + } else if (this.y + halfHeight > this.chart.height){ + this.y = this.chart.height - halfHeight; + } + + //Decide whether to align left or right based on position on canvas + if (this.x > this.chart.width/2){ + this.x -= this.xOffset + this.width; + } else { + this.x += this.xOffset; + } + + + }, + getLineHeight : function(index){ + var baseLineHeight = this.y - (this.height/2) + this.yPadding, + afterTitleIndex = index-1; + + //If the index is zero, we're getting the title + if (index === 0){ + return baseLineHeight + this.titleFontSize/2; + } else{ + return baseLineHeight + ((this.fontSize*1.5*afterTitleIndex) + this.fontSize/2) + this.titleFontSize * 1.5; + } + + }, + draw : function(){ + // Custom Tooltips + if(this.custom){ + this.custom(this); + } + else{ + drawRoundedRectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerRadius); + var ctx = this.ctx; + ctx.fillStyle = this.fillColor; + ctx.fill(); + ctx.closePath(); + + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.fillStyle = this.titleTextColor; + ctx.font = this.titleFont; + + ctx.fillText(this.title,this.x + this.xPadding, this.getLineHeight(0)); + + ctx.font = this.font; + helpers.each(this.labels,function(label,index){ + ctx.fillStyle = this.textColor; + ctx.fillText(label,this.x + this.xPadding + this.fontSize + 3, this.getLineHeight(index + 1)); + + //A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas) + //ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + //Instead we'll make a white filled block to put the legendColour palette over. + + ctx.fillStyle = this.legendColorBackground; + ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + + ctx.fillStyle = this.legendColors[index].fill; + ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + + + },this); + } + } + }); + + Chart.Scale = Chart.Element.extend({ + initialize : function(){ + this.fit(); + }, + buildYLabels : function(){ + this.yLabels = []; + + var stepDecimalPlaces = getDecimalPlaces(this.stepValue); + + for (var i=0; i<=this.steps; i++){ + this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); + } + this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx,this.font,this.yLabels) : 0; + }, + addXLabel : function(label){ + this.xLabels.push(label); + this.valuesCount++; + this.fit(); + }, + removeXLabel : function(){ + this.xLabels.shift(); + this.valuesCount--; + this.fit(); + }, + // Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use + fit: function(){ + // First we need the width of the yLabels, assuming the xLabels aren't rotated + + // To do that we need the base line at the top and base of the chart, assuming there is no x label rotation + this.startPoint = (this.display) ? this.fontSize : 0; + this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels + + // Apply padding settings to the start and end point. + this.startPoint += this.padding; + this.endPoint -= this.padding; + + // Cache the starting height, so can determine if we need to recalculate the scale yAxis + var cachedHeight = this.endPoint - this.startPoint, + cachedYLabelWidth; + + // Build the current yLabels so we have an idea of what size they'll be to start + /* + * This sets what is returned from calculateScaleRange as static properties of this class: + * + this.steps; + this.stepValue; + this.min; + this.max; + * + */ + this.calculateYRange(cachedHeight); + + // With these properties set we can now build the array of yLabels + // and also the width of the largest yLabel + this.buildYLabels(); + + this.calculateXLabelRotation(); + + while((cachedHeight > this.endPoint - this.startPoint)){ + cachedHeight = this.endPoint - this.startPoint; + cachedYLabelWidth = this.yLabelWidth; + + this.calculateYRange(cachedHeight); + this.buildYLabels(); + + // Only go through the xLabel loop again if the yLabel width has changed + if (cachedYLabelWidth < this.yLabelWidth){ + this.calculateXLabelRotation(); + } + } + + }, + calculateXLabelRotation : function(){ + //Get the width of each grid by calculating the difference + //between x offsets between 0 and 1. + + this.ctx.font = this.font; + + var firstWidth = this.ctx.measureText(this.xLabels[0]).width, + lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width, + firstRotated, + lastRotated; + + + this.xScalePaddingRight = lastWidth/2 + 3; + this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth + 10) ? firstWidth/2 : this.yLabelWidth + 10; + + this.xLabelRotation = 0; + if (this.display){ + var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels), + cosRotation, + firstRotatedWidth; + this.xLabelWidth = originalLabelWidth; + //Allow 3 pixels x2 padding either side for label readability + var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6; + + //Max label rotate should be 90 - also act as a loop counter + while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){ + cosRotation = Math.cos(toRadians(this.xLabelRotation)); + + firstRotated = cosRotation * firstWidth; + lastRotated = cosRotation * lastWidth; + + // We're right aligning the text now. + if (firstRotated + this.fontSize / 2 > this.yLabelWidth + 8){ + this.xScalePaddingLeft = firstRotated + this.fontSize / 2; + } + this.xScalePaddingRight = this.fontSize/2; + + + this.xLabelRotation++; + this.xLabelWidth = cosRotation * originalLabelWidth; + + } + if (this.xLabelRotation > 0){ + this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3; + } + } + else{ + this.xLabelWidth = 0; + this.xScalePaddingRight = this.padding; + this.xScalePaddingLeft = this.padding; + } + + }, + // Needs to be overidden in each Chart type + // Otherwise we need to pass all the data into the scale class + calculateYRange: noop, + drawingArea: function(){ + return this.startPoint - this.endPoint; + }, + calculateY : function(value){ + var scalingFactor = this.drawingArea() / (this.min - this.max); + return this.endPoint - (scalingFactor * (value - this.min)); + }, + calculateX : function(index){ + var isRotated = (this.xLabelRotation > 0), + // innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding, + innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight), + valueWidth = innerWidth/Math.max((this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), 1), + valueOffset = (valueWidth * index) + this.xScalePaddingLeft; + + if (this.offsetGridLines){ + valueOffset += (valueWidth/2); + } + + return Math.round(valueOffset); + }, + update : function(newProps){ + helpers.extend(this, newProps); + this.fit(); + }, + draw : function(){ + var ctx = this.ctx, + yLabelGap = (this.endPoint - this.startPoint) / this.steps, + xStart = Math.round(this.xScalePaddingLeft); + if (this.display){ + ctx.fillStyle = this.textColor; + ctx.font = this.font; + each(this.yLabels,function(labelString,index){ + var yLabelCenter = this.endPoint - (yLabelGap * index), + linePositionY = Math.round(yLabelCenter), + drawHorizontalLine = this.showHorizontalLines; + + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + if (this.showLabels){ + ctx.fillText(labelString,xStart - 10,yLabelCenter); + } + + // This is X axis, so draw it + if (index === 0 && !drawHorizontalLine){ + drawHorizontalLine = true; + } + + if (drawHorizontalLine){ + ctx.beginPath(); + } + + if (index > 0){ + // This is a grid line in the centre, so drop that + ctx.lineWidth = this.gridLineWidth; + ctx.strokeStyle = this.gridLineColor; + } else { + // This is the first line on the scale + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + } + + linePositionY += helpers.aliasPixel(ctx.lineWidth); + + if(drawHorizontalLine){ + ctx.moveTo(xStart, linePositionY); + ctx.lineTo(this.width, linePositionY); + ctx.stroke(); + ctx.closePath(); + } + + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + ctx.beginPath(); + ctx.moveTo(xStart - 5, linePositionY); + ctx.lineTo(xStart, linePositionY); + ctx.stroke(); + ctx.closePath(); + + },this); + + each(this.xLabels,function(label,index){ + var xPos = this.calculateX(index) + aliasPixel(this.lineWidth), + // Check to see if line/bar here and decide where to place the line + linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth), + isRotated = (this.xLabelRotation > 0), + drawVerticalLine = this.showVerticalLines; + + // This is Y axis, so draw it + if (index === 0 && !drawVerticalLine){ + drawVerticalLine = true; + } + + if (drawVerticalLine){ + ctx.beginPath(); + } + + if (index > 0){ + // This is a grid line in the centre, so drop that + ctx.lineWidth = this.gridLineWidth; + ctx.strokeStyle = this.gridLineColor; + } else { + // This is the first line on the scale + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + } + + if (drawVerticalLine){ + ctx.moveTo(linePos,this.endPoint); + ctx.lineTo(linePos,this.startPoint - 3); + ctx.stroke(); + ctx.closePath(); + } + + + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + + + // Small lines at the bottom of the base grid line + ctx.beginPath(); + ctx.moveTo(linePos,this.endPoint); + ctx.lineTo(linePos,this.endPoint + 5); + ctx.stroke(); + ctx.closePath(); + + ctx.save(); + ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8); + ctx.rotate(toRadians(this.xLabelRotation)*-1); + ctx.font = this.font; + ctx.textAlign = (isRotated) ? "right" : "center"; + ctx.textBaseline = (isRotated) ? "middle" : "top"; + ctx.fillText(label, 0, 0); + ctx.restore(); + },this); + + } + } + + }); + + Chart.RadialScale = Chart.Element.extend({ + initialize: function(){ + this.size = min([this.height, this.width]); + this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); + }, + calculateCenterOffset: function(value){ + // Take into account half font size + the yPadding of the top value + var scalingFactor = this.drawingArea / (this.max - this.min); + + return (value - this.min) * scalingFactor; + }, + update : function(){ + if (!this.lineArc){ + this.setScaleSize(); + } else { + this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); + } + this.buildYLabels(); + }, + buildYLabels: function(){ + this.yLabels = []; + + var stepDecimalPlaces = getDecimalPlaces(this.stepValue); + + for (var i=0; i<=this.steps; i++){ + this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); + } + }, + getCircumference : function(){ + return ((Math.PI*2) / this.valuesCount); + }, + setScaleSize: function(){ + /* + * Right, this is really confusing and there is a lot of maths going on here + * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 + * + * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif + * + * Solution: + * + * We assume the radius of the polygon is half the size of the canvas at first + * at each index we check if the text overlaps. + * + * Where it does, we store that angle and that index. + * + * After finding the largest index and angle we calculate how much we need to remove + * from the shape radius to move the point inwards by that x. + * + * We average the left and right distances to get the maximum shape radius that can fit in the box + * along with labels. + * + * Once we have that, we can find the centre point for the chart, by taking the x text protrusion + * on each side, removing that from the size, halving it and adding the left x protrusion width. + * + * This will mean we have a shape fitted to the canvas, as large as it can be with the labels + * and position it in the most space efficient manner + * + * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif + */ + + + // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. + // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points + var largestPossibleRadius = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]), + pointPosition, + i, + textWidth, + halfTextWidth, + furthestRight = this.width, + furthestRightIndex, + furthestRightAngle, + furthestLeft = 0, + furthestLeftIndex, + furthestLeftAngle, + xProtrusionLeft, + xProtrusionRight, + radiusReductionRight, + radiusReductionLeft, + maxWidthRadius; + this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); + for (i=0;i furthestRight) { + furthestRight = pointPosition.x + halfTextWidth; + furthestRightIndex = i; + } + if (pointPosition.x - halfTextWidth < furthestLeft) { + furthestLeft = pointPosition.x - halfTextWidth; + furthestLeftIndex = i; + } + } + else if (i < this.valuesCount/2) { + // Less than half the values means we'll left align the text + if (pointPosition.x + textWidth > furthestRight) { + furthestRight = pointPosition.x + textWidth; + furthestRightIndex = i; + } + } + else if (i > this.valuesCount/2){ + // More than half the values means we'll right align the text + if (pointPosition.x - textWidth < furthestLeft) { + furthestLeft = pointPosition.x - textWidth; + furthestLeftIndex = i; + } + } + } + + xProtrusionLeft = furthestLeft; + + xProtrusionRight = Math.ceil(furthestRight - this.width); + + furthestRightAngle = this.getIndexAngle(furthestRightIndex); + + furthestLeftAngle = this.getIndexAngle(furthestLeftIndex); + + radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI/2); + + radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI/2); + + // Ensure we actually need to reduce the size of the chart + radiusReductionRight = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0; + radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; + + this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2; + + //this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2]) + this.setCenterPoint(radiusReductionLeft, radiusReductionRight); + + }, + setCenterPoint: function(leftMovement, rightMovement){ + + var maxRight = this.width - rightMovement - this.drawingArea, + maxLeft = leftMovement + this.drawingArea; + + this.xCenter = (maxLeft + maxRight)/2; + // Always vertically in the centre as the text height doesn't change + this.yCenter = (this.height/2); + }, + + getIndexAngle : function(index){ + var angleMultiplier = (Math.PI * 2) / this.valuesCount; + // Start from the top instead of right, so remove a quarter of the circle + + return index * angleMultiplier - (Math.PI/2); + }, + getPointPosition : function(index, distanceFromCenter){ + var thisAngle = this.getIndexAngle(index); + return { + x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter, + y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter + }; + }, + draw: function(){ + if (this.display){ + var ctx = this.ctx; + each(this.yLabels, function(label, index){ + // Don't draw a centre value + if (index > 0){ + var yCenterOffset = index * (this.drawingArea/this.steps), + yHeight = this.yCenter - yCenterOffset, + pointPosition; + + // Draw circular lines around the scale + if (this.lineWidth > 0){ + ctx.strokeStyle = this.lineColor; + ctx.lineWidth = this.lineWidth; + + if(this.lineArc){ + ctx.beginPath(); + ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2); + ctx.closePath(); + ctx.stroke(); + } else{ + ctx.beginPath(); + for (var i=0;i= 0; i--) { + if (this.angleLineWidth > 0){ + var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max)); + ctx.beginPath(); + ctx.moveTo(this.xCenter, this.yCenter); + ctx.lineTo(outerPosition.x, outerPosition.y); + ctx.stroke(); + ctx.closePath(); + } + // Extra 3px out for some label spacing + var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5); + ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); + ctx.fillStyle = this.pointLabelFontColor; + + var labelsCount = this.labels.length, + halfLabelsCount = this.labels.length/2, + quarterLabelsCount = halfLabelsCount/2, + upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount), + exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount); + if (i === 0){ + ctx.textAlign = 'center'; + } else if(i === halfLabelsCount){ + ctx.textAlign = 'center'; + } else if (i < halfLabelsCount){ + ctx.textAlign = 'left'; + } else { + ctx.textAlign = 'right'; + } + + // Set the correct text baseline based on outer positioning + if (exactQuarter){ + ctx.textBaseline = 'middle'; + } else if (upperHalf){ + ctx.textBaseline = 'bottom'; + } else { + ctx.textBaseline = 'top'; + } + + ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y); + } + } + } + } + }); + + // Attach global event to resize each chart instance when the browser resizes + helpers.addEvent(window, "resize", (function(){ + // Basic debounce of resize function so it doesn't hurt performance when resizing browser. + var timeout; + return function(){ + clearTimeout(timeout); + timeout = setTimeout(function(){ + each(Chart.instances,function(instance){ + // If the responsive flag is set in the chart instance config + // Cascade the resize event down to the chart. + if (instance.options.responsive){ + instance.resize(instance.render, true); + } + }); + }, 50); + }; + })()); + + + if (amd) { + define(function(){ + return Chart; + }); + } else if (typeof module === 'object' && module.exports) { + module.exports = Chart; + } + + root.Chart = Chart; + + Chart.noConflict = function(){ + root.Chart = previous; + return Chart; + }; + +}).call(this); diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.Doughnut.js b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.Doughnut.js new file mode 100644 index 0000000000..a892b0efc6 --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.Doughnut.js @@ -0,0 +1,184 @@ +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + //Cache a local reference to Chart.helpers + helpers = Chart.helpers; + + var defaultConfig = { + //Boolean - Whether we should show a stroke on each segment + segmentShowStroke : true, + + //String - The colour of each segment stroke + segmentStrokeColor : "#fff", + + //Number - The width of each segment stroke + segmentStrokeWidth : 2, + + //The percentage of the chart that we cut out of the middle. + percentageInnerCutout : 50, + + //Number - Amount of animation steps + animationSteps : 100, + + //String - Animation easing effect + animationEasing : "easeOutBounce", + + //Boolean - Whether we animate the rotation of the Doughnut + animateRotate : true, + + //Boolean - Whether we animate scaling the Doughnut from the centre + animateScale : false, + + //String - A legend template + legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
    • <%}%>
    " + + }; + + + Chart.Type.extend({ + //Passing in a name registers this chart in the Chart namespace + name: "Doughnut", + //Providing a defaults will also register the deafults in the chart namespace + defaults : defaultConfig, + //Initialize is fired when the chart is initialized - Data is passed in as a parameter + //Config is automatically merged by the core of Chart.js, and is available at this.options + initialize: function(data){ + + //Declare segments as a static property to prevent inheriting across the Chart type prototype + this.segments = []; + this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; + + this.SegmentArc = Chart.Arc.extend({ + ctx : this.chart.ctx, + x : this.chart.width/2, + y : this.chart.height/2 + }); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; + + helpers.each(this.segments,function(segment){ + segment.restore(["fillColor"]); + }); + helpers.each(activeSegments,function(activeSegment){ + activeSegment.fillColor = activeSegment.highlightColor; + }); + this.showTooltip(activeSegments); + }); + } + this.calculateTotal(data); + + helpers.each(data,function(datapoint, index){ + this.addData(datapoint, index, true); + },this); + + this.render(); + }, + getSegmentsAtEvent : function(e){ + var segmentsArray = []; + + var location = helpers.getRelativePosition(e); + + helpers.each(this.segments,function(segment){ + if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); + },this); + return segmentsArray; + }, + addData : function(segment, atIndex, silent){ + var index = atIndex || this.segments.length; + this.segments.splice(index, 0, new this.SegmentArc({ + value : segment.value, + outerRadius : (this.options.animateScale) ? 0 : this.outerRadius, + innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout, + fillColor : segment.color, + highlightColor : segment.highlight || segment.color, + showStroke : this.options.segmentShowStroke, + strokeWidth : this.options.segmentStrokeWidth, + strokeColor : this.options.segmentStrokeColor, + startAngle : Math.PI * 1.5, + circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value), + label : segment.label + })); + if (!silent){ + this.reflow(); + this.update(); + } + }, + calculateCircumference : function(value){ + return (Math.PI*2)*(Math.abs(value) / this.total); + }, + calculateTotal : function(data){ + this.total = 0; + helpers.each(data,function(segment){ + this.total += Math.abs(segment.value); + },this); + }, + update : function(){ + this.calculateTotal(this.segments); + + // Reset any highlight colours before updating. + helpers.each(this.activeElements, function(activeElement){ + activeElement.restore(['fillColor']); + }); + + helpers.each(this.segments,function(segment){ + segment.save(); + }); + this.render(); + }, + + removeData: function(atIndex){ + var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; + this.segments.splice(indexToDelete, 1); + this.reflow(); + this.update(); + }, + + reflow : function(){ + helpers.extend(this.SegmentArc.prototype,{ + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; + helpers.each(this.segments, function(segment){ + segment.update({ + outerRadius : this.outerRadius, + innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout + }); + }, this); + }, + draw : function(easeDecimal){ + var animDecimal = (easeDecimal) ? easeDecimal : 1; + this.clear(); + helpers.each(this.segments,function(segment,index){ + segment.transition({ + circumference : this.calculateCircumference(segment.value), + outerRadius : this.outerRadius, + innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout + },animDecimal); + + segment.endAngle = segment.startAngle + segment.circumference; + + segment.draw(); + if (index === 0){ + segment.startAngle = Math.PI * 1.5; + } + //Check to see if it's the last segment, if not get the next and update the start angle + if (index < this.segments.length-1){ + this.segments[index+1].startAngle = segment.endAngle; + } + },this); + + } + }); + + Chart.types.Doughnut.extend({ + name : "Pie", + defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0}) + }); + +}).call(this); \ No newline at end of file diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.Line.js b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.Line.js new file mode 100644 index 0000000000..34ad85b4d0 --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.Line.js @@ -0,0 +1,374 @@ +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + var defaultConfig = { + + ///Boolean - Whether grid lines are shown across the chart + scaleShowGridLines : true, + + //String - Colour of the grid lines + scaleGridLineColor : "rgba(0,0,0,.05)", + + //Number - Width of the grid lines + scaleGridLineWidth : 1, + + //Boolean - Whether to show horizontal lines (except X axis) + scaleShowHorizontalLines: true, + + //Boolean - Whether to show vertical lines (except Y axis) + scaleShowVerticalLines: true, + + //Boolean - Whether the line is curved between points + bezierCurve : true, + + //Number - Tension of the bezier curve between points + bezierCurveTension : 0.4, + + //Boolean - Whether to show a dot for each point + pointDot : true, + + //Number - Radius of each point dot in pixels + pointDotRadius : 4, + + //Number - Pixel width of point dot stroke + pointDotStrokeWidth : 1, + + //Number - amount extra to add to the radius to cater for hit detection outside the drawn point + pointHitDetectionRadius : 20, + + //Boolean - Whether to show a stroke for datasets + datasetStroke : true, + + //Number - Pixel width of dataset stroke + datasetStrokeWidth : 2, + + //Boolean - Whether to fill the dataset with a colour + datasetFill : true, + + //String - A legend template + legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
    • <%}%>
    " + + }; + + + Chart.Type.extend({ + name: "Line", + defaults : defaultConfig, + initialize: function(data){ + //Declare the extension of the default point, to cater for the options passed in to the constructor + this.PointClass = Chart.Point.extend({ + strokeWidth : this.options.pointDotStrokeWidth, + radius : this.options.pointDotRadius, + display: this.options.pointDot, + hitDetectionRadius : this.options.pointHitDetectionRadius, + ctx : this.chart.ctx, + inRange : function(mouseX){ + return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2)); + } + }); + + this.datasets = []; + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; + this.eachPoints(function(point){ + point.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activePoints, function(activePoint){ + activePoint.fillColor = activePoint.highlightFill; + activePoint.strokeColor = activePoint.highlightStroke; + }); + this.showTooltip(activePoints); + }); + } + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset){ + + var datasetObject = { + label : dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + pointColor : dataset.pointColor, + pointStrokeColor : dataset.pointStrokeColor, + points : [] + }; + + this.datasets.push(datasetObject); + + + helpers.each(dataset.data,function(dataPoint,index){ + //Add a new point for each piece of data, passing any required data to draw. + datasetObject.points.push(new this.PointClass({ + value : dataPoint, + label : data.labels[index], + datasetLabel: dataset.label, + strokeColor : dataset.pointStrokeColor, + fillColor : dataset.pointColor, + highlightFill : dataset.pointHighlightFill || dataset.pointColor, + highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor + })); + },this); + + this.buildScale(data.labels); + + + this.eachPoints(function(point, index){ + helpers.extend(point, { + x: this.scale.calculateX(index), + y: this.scale.endPoint + }); + point.save(); + }, this); + + },this); + + + this.render(); + }, + update : function(){ + this.scale.update(); + // Reset any highlight colours before updating. + helpers.each(this.activeElements, function(activeElement){ + activeElement.restore(['fillColor', 'strokeColor']); + }); + this.eachPoints(function(point){ + point.save(); + }); + this.render(); + }, + eachPoints : function(callback){ + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,callback,this); + },this); + }, + getPointsAtEvent : function(e){ + var pointsArray = [], + eventPosition = helpers.getRelativePosition(e); + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,function(point){ + if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point); + }); + },this); + return pointsArray; + }, + buildScale : function(labels){ + var self = this; + + var dataTotal = function(){ + var values = []; + self.eachPoints(function(point){ + values.push(point.value); + }); + + return values; + }; + + var scaleOptions = { + templateString : this.options.scaleLabel, + height : this.chart.height, + width : this.chart.width, + ctx : this.chart.ctx, + textColor : this.options.scaleFontColor, + fontSize : this.options.scaleFontSize, + fontStyle : this.options.scaleFontStyle, + fontFamily : this.options.scaleFontFamily, + valuesCount : labels.length, + beginAtZero : this.options.scaleBeginAtZero, + integersOnly : this.options.scaleIntegersOnly, + calculateYRange : function(currentHeight){ + var updatedRanges = helpers.calculateScaleRange( + dataTotal(), + currentHeight, + this.fontSize, + this.beginAtZero, + this.integersOnly + ); + helpers.extend(this, updatedRanges); + }, + xLabels : labels, + font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), + lineWidth : this.options.scaleLineWidth, + lineColor : this.options.scaleLineColor, + showHorizontalLines : this.options.scaleShowHorizontalLines, + showVerticalLines : this.options.scaleShowVerticalLines, + gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, + gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", + padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth, + showLabels : this.options.scaleShowLabels, + display : this.options.showScale + }; + + if (this.options.scaleOverride){ + helpers.extend(scaleOptions, { + calculateYRange: helpers.noop, + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + }); + } + + + this.scale = new Chart.Scale(scaleOptions); + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + + helpers.each(valuesArray,function(value,datasetIndex){ + //Add a new point for each piece of data, passing any required data to draw. + this.datasets[datasetIndex].points.push(new this.PointClass({ + value : value, + label : label, + x: this.scale.calculateX(this.scale.valuesCount+1), + y: this.scale.endPoint, + strokeColor : this.datasets[datasetIndex].pointStrokeColor, + fillColor : this.datasets[datasetIndex].pointColor + })); + },this); + + this.scale.addXLabel(label); + //Then re-render the chart. + this.update(); + }, + removeData : function(){ + this.scale.removeXLabel(); + //Then re-render the chart. + helpers.each(this.datasets,function(dataset){ + dataset.points.shift(); + },this); + this.update(); + }, + reflow : function(){ + var newScaleProps = helpers.extend({ + height : this.chart.height, + width : this.chart.width + }); + this.scale.update(newScaleProps); + }, + draw : function(ease){ + var easingDecimal = ease || 1; + this.clear(); + + var ctx = this.chart.ctx; + + // Some helper methods for getting the next/prev points + var hasValue = function(item){ + return item.value !== null; + }, + nextPoint = function(point, collection, index){ + return helpers.findNextWhere(collection, hasValue, index) || point; + }, + previousPoint = function(point, collection, index){ + return helpers.findPreviousWhere(collection, hasValue, index) || point; + }; + + this.scale.draw(easingDecimal); + + + helpers.each(this.datasets,function(dataset){ + var pointsWithValues = helpers.where(dataset.points, hasValue); + + //Transition each point first so that the line and point drawing isn't out of sync + //We can use this extra loop to calculate the control points of this dataset also in this loop + + helpers.each(dataset.points, function(point, index){ + if (point.hasValue()){ + point.transition({ + y : this.scale.calculateY(point.value), + x : this.scale.calculateX(index) + }, easingDecimal); + } + },this); + + + // Control points need to be calculated in a seperate loop, because we need to know the current x/y of the point + // This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed + if (this.options.bezierCurve){ + helpers.each(pointsWithValues, function(point, index){ + var tension = (index > 0 && index < pointsWithValues.length - 1) ? this.options.bezierCurveTension : 0; + point.controlPoints = helpers.splineCurve( + previousPoint(point, pointsWithValues, index), + point, + nextPoint(point, pointsWithValues, index), + tension + ); + + // Prevent the bezier going outside of the bounds of the graph + + // Cap puter bezier handles to the upper/lower scale bounds + if (point.controlPoints.outer.y > this.scale.endPoint){ + point.controlPoints.outer.y = this.scale.endPoint; + } + else if (point.controlPoints.outer.y < this.scale.startPoint){ + point.controlPoints.outer.y = this.scale.startPoint; + } + + // Cap inner bezier handles to the upper/lower scale bounds + if (point.controlPoints.inner.y > this.scale.endPoint){ + point.controlPoints.inner.y = this.scale.endPoint; + } + else if (point.controlPoints.inner.y < this.scale.startPoint){ + point.controlPoints.inner.y = this.scale.startPoint; + } + },this); + } + + + //Draw the line between all the points + ctx.lineWidth = this.options.datasetStrokeWidth; + ctx.strokeStyle = dataset.strokeColor; + ctx.beginPath(); + + helpers.each(pointsWithValues, function(point, index){ + if (index === 0){ + ctx.moveTo(point.x, point.y); + } + else{ + if(this.options.bezierCurve){ + var previous = previousPoint(point, pointsWithValues, index); + + ctx.bezierCurveTo( + previous.controlPoints.outer.x, + previous.controlPoints.outer.y, + point.controlPoints.inner.x, + point.controlPoints.inner.y, + point.x, + point.y + ); + } + else{ + ctx.lineTo(point.x,point.y); + } + } + }, this); + + ctx.stroke(); + + if (this.options.datasetFill && pointsWithValues.length > 0){ + //Round off the line by going to the base of the chart, back to the start, then fill. + ctx.lineTo(pointsWithValues[pointsWithValues.length - 1].x, this.scale.endPoint); + ctx.lineTo(pointsWithValues[0].x, this.scale.endPoint); + ctx.fillStyle = dataset.fillColor; + ctx.closePath(); + ctx.fill(); + } + + //Now draw the points over the line + //A little inefficient double looping, but better than the line + //lagging behind the point positions + helpers.each(pointsWithValues,function(point){ + point.draw(); + }); + },this); + } + }); + + +}).call(this); diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.PolarArea.js b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.PolarArea.js new file mode 100644 index 0000000000..8f2b9c52cf --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.PolarArea.js @@ -0,0 +1,250 @@ +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + //Cache a local reference to Chart.helpers + helpers = Chart.helpers; + + var defaultConfig = { + //Boolean - Show a backdrop to the scale label + scaleShowLabelBackdrop : true, + + //String - The colour of the label backdrop + scaleBackdropColor : "rgba(255,255,255,0.75)", + + // Boolean - Whether the scale should begin at zero + scaleBeginAtZero : true, + + //Number - The backdrop padding above & below the label in pixels + scaleBackdropPaddingY : 2, + + //Number - The backdrop padding to the side of the label in pixels + scaleBackdropPaddingX : 2, + + //Boolean - Show line for each value in the scale + scaleShowLine : true, + + //Boolean - Stroke a line around each segment in the chart + segmentShowStroke : true, + + //String - The colour of the stroke on each segement. + segmentStrokeColor : "#fff", + + //Number - The width of the stroke value in pixels + segmentStrokeWidth : 2, + + //Number - Amount of animation steps + animationSteps : 100, + + //String - Animation easing effect. + animationEasing : "easeOutBounce", + + //Boolean - Whether to animate the rotation of the chart + animateRotate : true, + + //Boolean - Whether to animate scaling the chart from the centre + animateScale : false, + + //String - A legend template + legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
    • <%}%>
    " + }; + + + Chart.Type.extend({ + //Passing in a name registers this chart in the Chart namespace + name: "PolarArea", + //Providing a defaults will also register the deafults in the chart namespace + defaults : defaultConfig, + //Initialize is fired when the chart is initialized - Data is passed in as a parameter + //Config is automatically merged by the core of Chart.js, and is available at this.options + initialize: function(data){ + this.segments = []; + //Declare segment class as a chart instance specific class, so it can share props for this instance + this.SegmentArc = Chart.Arc.extend({ + showStroke : this.options.segmentShowStroke, + strokeWidth : this.options.segmentStrokeWidth, + strokeColor : this.options.segmentStrokeColor, + ctx : this.chart.ctx, + innerRadius : 0, + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.scale = new Chart.RadialScale({ + display: this.options.showScale, + fontStyle: this.options.scaleFontStyle, + fontSize: this.options.scaleFontSize, + fontFamily: this.options.scaleFontFamily, + fontColor: this.options.scaleFontColor, + showLabels: this.options.scaleShowLabels, + showLabelBackdrop: this.options.scaleShowLabelBackdrop, + backdropColor: this.options.scaleBackdropColor, + backdropPaddingY : this.options.scaleBackdropPaddingY, + backdropPaddingX: this.options.scaleBackdropPaddingX, + lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, + lineColor: this.options.scaleLineColor, + lineArc: true, + width: this.chart.width, + height: this.chart.height, + xCenter: this.chart.width/2, + yCenter: this.chart.height/2, + ctx : this.chart.ctx, + templateString: this.options.scaleLabel, + valuesCount: data.length + }); + + this.updateScaleRange(data); + + this.scale.update(); + + helpers.each(data,function(segment,index){ + this.addData(segment,index,true); + },this); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; + helpers.each(this.segments,function(segment){ + segment.restore(["fillColor"]); + }); + helpers.each(activeSegments,function(activeSegment){ + activeSegment.fillColor = activeSegment.highlightColor; + }); + this.showTooltip(activeSegments); + }); + } + + this.render(); + }, + getSegmentsAtEvent : function(e){ + var segmentsArray = []; + + var location = helpers.getRelativePosition(e); + + helpers.each(this.segments,function(segment){ + if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); + },this); + return segmentsArray; + }, + addData : function(segment, atIndex, silent){ + var index = atIndex || this.segments.length; + + this.segments.splice(index, 0, new this.SegmentArc({ + fillColor: segment.color, + highlightColor: segment.highlight || segment.color, + label: segment.label, + value: segment.value, + outerRadius: (this.options.animateScale) ? 0 : this.scale.calculateCenterOffset(segment.value), + circumference: (this.options.animateRotate) ? 0 : this.scale.getCircumference(), + startAngle: Math.PI * 1.5 + })); + if (!silent){ + this.reflow(); + this.update(); + } + }, + removeData: function(atIndex){ + var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; + this.segments.splice(indexToDelete, 1); + this.reflow(); + this.update(); + }, + calculateTotal: function(data){ + this.total = 0; + helpers.each(data,function(segment){ + this.total += segment.value; + },this); + this.scale.valuesCount = this.segments.length; + }, + updateScaleRange: function(datapoints){ + var valuesArray = []; + helpers.each(datapoints,function(segment){ + valuesArray.push(segment.value); + }); + + var scaleSizes = (this.options.scaleOverride) ? + { + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + } : + helpers.calculateScaleRange( + valuesArray, + helpers.min([this.chart.width, this.chart.height])/2, + this.options.scaleFontSize, + this.options.scaleBeginAtZero, + this.options.scaleIntegersOnly + ); + + helpers.extend( + this.scale, + scaleSizes, + { + size: helpers.min([this.chart.width, this.chart.height]), + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + } + ); + + }, + update : function(){ + this.calculateTotal(this.segments); + + helpers.each(this.segments,function(segment){ + segment.save(); + }); + + this.reflow(); + this.render(); + }, + reflow : function(){ + helpers.extend(this.SegmentArc.prototype,{ + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.updateScaleRange(this.segments); + this.scale.update(); + + helpers.extend(this.scale,{ + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + }); + + helpers.each(this.segments, function(segment){ + segment.update({ + outerRadius : this.scale.calculateCenterOffset(segment.value) + }); + }, this); + + }, + draw : function(ease){ + var easingDecimal = ease || 1; + //Clear & draw the canvas + this.clear(); + helpers.each(this.segments,function(segment, index){ + segment.transition({ + circumference : this.scale.getCircumference(), + outerRadius : this.scale.calculateCenterOffset(segment.value) + },easingDecimal); + + segment.endAngle = segment.startAngle + segment.circumference; + + // If we've removed the first segment we need to set the first one to + // start at the top. + if (index === 0){ + segment.startAngle = Math.PI * 1.5; + } + + //Check to see if it's the last segment, if not get the next and update the start angle + if (index < this.segments.length - 1){ + this.segments[index+1].startAngle = segment.endAngle; + } + segment.draw(); + }, this); + this.scale.draw(); + } + }); + +}).call(this); \ No newline at end of file diff --git a/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.Radar.js b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.Radar.js new file mode 100644 index 0000000000..134fd2d021 --- /dev/null +++ b/mayan/apps/statistics/static/statistics/packages/Chart.js-1.0.2/src/Chart.Radar.js @@ -0,0 +1,343 @@ +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + + + Chart.Type.extend({ + name: "Radar", + defaults:{ + //Boolean - Whether to show lines for each scale point + scaleShowLine : true, + + //Boolean - Whether we show the angle lines out of the radar + angleShowLineOut : true, + + //Boolean - Whether to show labels on the scale + scaleShowLabels : false, + + // Boolean - Whether the scale should begin at zero + scaleBeginAtZero : true, + + //String - Colour of the angle line + angleLineColor : "rgba(0,0,0,.1)", + + //Number - Pixel width of the angle line + angleLineWidth : 1, + + //String - Point label font declaration + pointLabelFontFamily : "'Arial'", + + //String - Point label font weight + pointLabelFontStyle : "normal", + + //Number - Point label font size in pixels + pointLabelFontSize : 10, + + //String - Point label font colour + pointLabelFontColor : "#666", + + //Boolean - Whether to show a dot for each point + pointDot : true, + + //Number - Radius of each point dot in pixels + pointDotRadius : 3, + + //Number - Pixel width of point dot stroke + pointDotStrokeWidth : 1, + + //Number - amount extra to add to the radius to cater for hit detection outside the drawn point + pointHitDetectionRadius : 20, + + //Boolean - Whether to show a stroke for datasets + datasetStroke : true, + + //Number - Pixel width of dataset stroke + datasetStrokeWidth : 2, + + //Boolean - Whether to fill the dataset with a colour + datasetFill : true, + + //String - A legend template + legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
    • <%}%>
    " + + }, + + initialize: function(data){ + this.PointClass = Chart.Point.extend({ + strokeWidth : this.options.pointDotStrokeWidth, + radius : this.options.pointDotRadius, + display: this.options.pointDot, + hitDetectionRadius : this.options.pointHitDetectionRadius, + ctx : this.chart.ctx + }); + + this.datasets = []; + + this.buildScale(data); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; + + this.eachPoints(function(point){ + point.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activePointsCollection, function(activePoint){ + activePoint.fillColor = activePoint.highlightFill; + activePoint.strokeColor = activePoint.highlightStroke; + }); + + this.showTooltip(activePointsCollection); + }); + } + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset){ + + var datasetObject = { + label: dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + pointColor : dataset.pointColor, + pointStrokeColor : dataset.pointStrokeColor, + points : [] + }; + + this.datasets.push(datasetObject); + + helpers.each(dataset.data,function(dataPoint,index){ + //Add a new point for each piece of data, passing any required data to draw. + var pointPosition; + if (!this.scale.animation){ + pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(dataPoint)); + } + datasetObject.points.push(new this.PointClass({ + value : dataPoint, + label : data.labels[index], + datasetLabel: dataset.label, + x: (this.options.animation) ? this.scale.xCenter : pointPosition.x, + y: (this.options.animation) ? this.scale.yCenter : pointPosition.y, + strokeColor : dataset.pointStrokeColor, + fillColor : dataset.pointColor, + highlightFill : dataset.pointHighlightFill || dataset.pointColor, + highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor + })); + },this); + + },this); + + this.render(); + }, + eachPoints : function(callback){ + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,callback,this); + },this); + }, + + getPointsAtEvent : function(evt){ + var mousePosition = helpers.getRelativePosition(evt), + fromCenter = helpers.getAngleFromPoint({ + x: this.scale.xCenter, + y: this.scale.yCenter + }, mousePosition); + + var anglePerIndex = (Math.PI * 2) /this.scale.valuesCount, + pointIndex = Math.round((fromCenter.angle - Math.PI * 1.5) / anglePerIndex), + activePointsCollection = []; + + // If we're at the top, make the pointIndex 0 to get the first of the array. + if (pointIndex >= this.scale.valuesCount || pointIndex < 0){ + pointIndex = 0; + } + + if (fromCenter.distance <= this.scale.drawingArea){ + helpers.each(this.datasets, function(dataset){ + activePointsCollection.push(dataset.points[pointIndex]); + }); + } + + return activePointsCollection; + }, + + buildScale : function(data){ + this.scale = new Chart.RadialScale({ + display: this.options.showScale, + fontStyle: this.options.scaleFontStyle, + fontSize: this.options.scaleFontSize, + fontFamily: this.options.scaleFontFamily, + fontColor: this.options.scaleFontColor, + showLabels: this.options.scaleShowLabels, + showLabelBackdrop: this.options.scaleShowLabelBackdrop, + backdropColor: this.options.scaleBackdropColor, + backdropPaddingY : this.options.scaleBackdropPaddingY, + backdropPaddingX: this.options.scaleBackdropPaddingX, + lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, + lineColor: this.options.scaleLineColor, + angleLineColor : this.options.angleLineColor, + angleLineWidth : (this.options.angleShowLineOut) ? this.options.angleLineWidth : 0, + // Point labels at the edge of each line + pointLabelFontColor : this.options.pointLabelFontColor, + pointLabelFontSize : this.options.pointLabelFontSize, + pointLabelFontFamily : this.options.pointLabelFontFamily, + pointLabelFontStyle : this.options.pointLabelFontStyle, + height : this.chart.height, + width: this.chart.width, + xCenter: this.chart.width/2, + yCenter: this.chart.height/2, + ctx : this.chart.ctx, + templateString: this.options.scaleLabel, + labels: data.labels, + valuesCount: data.datasets[0].data.length + }); + + this.scale.setScaleSize(); + this.updateScaleRange(data.datasets); + this.scale.buildYLabels(); + }, + updateScaleRange: function(datasets){ + var valuesArray = (function(){ + var totalDataArray = []; + helpers.each(datasets,function(dataset){ + if (dataset.data){ + totalDataArray = totalDataArray.concat(dataset.data); + } + else { + helpers.each(dataset.points, function(point){ + totalDataArray.push(point.value); + }); + } + }); + return totalDataArray; + })(); + + + var scaleSizes = (this.options.scaleOverride) ? + { + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + } : + helpers.calculateScaleRange( + valuesArray, + helpers.min([this.chart.width, this.chart.height])/2, + this.options.scaleFontSize, + this.options.scaleBeginAtZero, + this.options.scaleIntegersOnly + ); + + helpers.extend( + this.scale, + scaleSizes + ); + + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + this.scale.valuesCount++; + helpers.each(valuesArray,function(value,datasetIndex){ + var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value)); + this.datasets[datasetIndex].points.push(new this.PointClass({ + value : value, + label : label, + x: pointPosition.x, + y: pointPosition.y, + strokeColor : this.datasets[datasetIndex].pointStrokeColor, + fillColor : this.datasets[datasetIndex].pointColor + })); + },this); + + this.scale.labels.push(label); + + this.reflow(); + + this.update(); + }, + removeData : function(){ + this.scale.valuesCount--; + this.scale.labels.shift(); + helpers.each(this.datasets,function(dataset){ + dataset.points.shift(); + },this); + this.reflow(); + this.update(); + }, + update : function(){ + this.eachPoints(function(point){ + point.save(); + }); + this.reflow(); + this.render(); + }, + reflow: function(){ + helpers.extend(this.scale, { + width : this.chart.width, + height: this.chart.height, + size : helpers.min([this.chart.width, this.chart.height]), + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + }); + this.updateScaleRange(this.datasets); + this.scale.setScaleSize(); + this.scale.buildYLabels(); + }, + draw : function(ease){ + var easeDecimal = ease || 1, + ctx = this.chart.ctx; + this.clear(); + this.scale.draw(); + + helpers.each(this.datasets,function(dataset){ + + //Transition each point first so that the line and point drawing isn't out of sync + helpers.each(dataset.points,function(point,index){ + if (point.hasValue()){ + point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal); + } + },this); + + + + //Draw the line between all the points + ctx.lineWidth = this.options.datasetStrokeWidth; + ctx.strokeStyle = dataset.strokeColor; + ctx.beginPath(); + helpers.each(dataset.points,function(point,index){ + if (index === 0){ + ctx.moveTo(point.x,point.y); + } + else{ + ctx.lineTo(point.x,point.y); + } + },this); + ctx.closePath(); + ctx.stroke(); + + ctx.fillStyle = dataset.fillColor; + ctx.fill(); + + //Now draw the points over the line + //A little inefficient double looping, but better than the line + //lagging behind the point positions + helpers.each(dataset.points,function(point){ + if (point.hasValue()){ + point.draw(); + } + }); + + },this); + + } + + }); + + + + + +}).call(this); \ No newline at end of file diff --git a/mayan/apps/statistics/templates/statistics/backends/chartjs/line.html b/mayan/apps/statistics/templates/statistics/backends/chartjs/line.html new file mode 100644 index 0000000000..a664849542 --- /dev/null +++ b/mayan/apps/statistics/templates/statistics/backends/chartjs/line.html @@ -0,0 +1,35 @@ +{% extends 'appearance/base.html' %} + +{% load i18n %} +{% load static %} + +{% load navigation_tags %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +

    + {{ title }} +

    +
    + +
    +
    + {% if no_data %} + {% trans 'No data available yet' %} + {% endif %} + +
    +
    +{% endblock %} + +{% block javascript %} + + +{% endblock javascript %} diff --git a/mayan/apps/statistics/urls.py b/mayan/apps/statistics/urls.py index c535da3652..9c8355792f 100644 --- a/mayan/apps/statistics/urls.py +++ b/mayan/apps/statistics/urls.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url -from .views import NamespaceDetailView, NamespaceListView, StatisticExecute +from .views import NamespaceDetailView, NamespaceListView, StatisticDetailView urlpatterns = patterns( 'statistics.views', @@ -12,7 +12,7 @@ urlpatterns = patterns( NamespaceDetailView.as_view(), name='namespace_details' ), url( - r'^(?P[\w,-]+)/view/$', StatisticExecute.as_view(), - name='statistic_execute' + r'^(?P[\w-]+)/view/$', StatisticDetailView.as_view(), + name='statistic_detail' ), ) diff --git a/mayan/apps/statistics/views.py b/mayan/apps/statistics/views.py index a1b56441b6..f109b11d7d 100644 --- a/mayan/apps/statistics/views.py +++ b/mayan/apps/statistics/views.py @@ -1,9 +1,11 @@ from __future__ import unicode_literals +import json + from django.http import Http404 from django.utils.translation import ugettext_lazy as _ -from common.views import SingleObjectListView +from common.views import SingleObjectDetailView, SingleObjectListView from .classes import Statistic, StatisticNamespace from .permissions import permission_statistics_view @@ -22,7 +24,6 @@ class NamespaceListView(SingleObjectListView): class NamespaceDetailView(SingleObjectListView): - template_name = 'appearance/generic_list.html' view_permission = permission_statistics_view def get_extra_context(self): @@ -39,24 +40,24 @@ class NamespaceDetailView(SingleObjectListView): return self.get_namespace().statistics -class StatisticExecute(SingleObjectListView): - template_name = 'appearance/generic_list.html' +class StatisticDetailView(SingleObjectDetailView): view_permission = permission_statistics_view def get_extra_context(self): return { - 'hide_link': True, - 'namespace': self.get_statictic().namespace, + 'chart_data': self.get_object().get_chart_data(), + 'namespace': self.get_object().namespace, 'navigation_object_list': ('namespace', 'object'), - 'object': self.get_statictic(), - 'title': _('Results for: %s') % self.get_statictic(), + 'no_data': not self.get_object().get_results()['series'], + 'object': self.get_object(), + 'title': _('Results for: %s') % self.get_object(), } - def get_queryset(self): - return self.get_statictic().get_results() - - def get_statictic(self): + def get_object(self): try: - return Statistic.get(self.kwargs['statistic_id']) + return Statistic.get(self.kwargs['slug']) except KeyError: - raise Http404(_('Statistic "%s" not found.') % self.kwargs['statistic_id']) + raise Http404(_('Statistic "%s" not found.') % self.kwargs['slug']) + + def get_template_names(self): + return (self.get_object().renderer.template_name,) diff --git a/requirements/common.txt b/requirements/common.txt index 282e23d4e8..fd8dd98578 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -16,6 +16,7 @@ django-filetransfers==0.1.0 django-pure-pagination==0.2.1 django-model-utils==2.2 django-mptt==0.7.4 +django-qsstats-magic==0.7.2 django-rest-swagger==0.3.3 django-suit==0.2.13 django-widget-tweaks==1.3 From 7687618ea62837f6f813987037c8ff2e5975d151 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 11 Sep 2015 08:28:16 -0400 Subject: [PATCH 3/9] Invert series palette. --- mayan/apps/statistics/classes.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/mayan/apps/statistics/classes.py b/mayan/apps/statistics/classes.py index c6d03b3940..6d634ceab8 100644 --- a/mayan/apps/statistics/classes.py +++ b/mayan/apps/statistics/classes.py @@ -95,14 +95,6 @@ class CharJSLine(ChartRenderer): template_name = 'statistics/backends/chartjs/line.html' dataset_palette = ( - { - 'fillColor': "rgba(220,220,220,0.2)", - 'strokeColor': "rgba(220,220,220,1)", - 'pointColor': "rgba(220,220,220,1)", - 'pointStrokeColor': "#fff", - 'pointHighlightFill': "#fff", - 'pointHighlightStroke': "rgba(220,220,220,1)", - }, { 'fillColor': "rgba(151,187,205,0.2)", 'strokeColor': "rgba(151,187,205,1)", @@ -110,6 +102,14 @@ class CharJSLine(ChartRenderer): 'pointStrokeColor': "#fff", 'pointHighlightFill': "#fff", 'pointHighlightStroke': "rgba(151,187,205,1)", + }, + { + 'fillColor': "rgba(220,220,220,0.2)", + 'strokeColor': "rgba(220,220,220,1)", + 'pointColor': "rgba(220,220,220,1)", + 'pointStrokeColor': "#fff", + 'pointHighlightFill': "#fff", + 'pointHighlightStroke': "rgba(220,220,220,1)", } ) From ca6f6c3402724612f25285909cbb78f601430583 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 11 Sep 2015 09:47:56 -0400 Subject: [PATCH 4/9] Document update. Add new deployment chapter. --- docs/conf.py | 4 +- docs/index.rst | 1 + docs/releases/0.12.1.rst | 24 ++-- docs/releases/0.12.2.rst | 8 +- docs/releases/0.12.rst | 10 +- docs/releases/2.0.rst | 251 ++++++++++++++++++++--------------- docs/topics/deploying.rst | 28 ++++ docs/topics/development.rst | 33 +++-- docs/topics/installation.rst | 39 ++---- 9 files changed, 224 insertions(+), 174 deletions(-) create mode 100644 docs/topics/deploying.rst diff --git a/docs/conf.py b/docs/conf.py index cf135cc173..74283db540 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -240,9 +240,9 @@ html_theme_options = { 'description': mayan.__description__, 'github_user': 'mayan-edms', 'github_repo': 'mayan-edms', - 'travis_button': True, + 'travis_button': False, 'gratipay_user': 'rosarior', - 'github_banner': True, + 'github_banner': False, } html_logo = '_static/logo_pyramid_only.png' diff --git a/docs/index.rst b/docs/index.rst index fe768174b2..2eba239b7f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -25,6 +25,7 @@ account. Features Installation + Deploying Getting started Release notes and upgrading Concepts diff --git a/docs/releases/0.12.1.rst b/docs/releases/0.12.1.rst index 75379ea8d7..cce015965f 100644 --- a/docs/releases/0.12.1.rst +++ b/docs/releases/0.12.1.rst @@ -13,7 +13,7 @@ While bug fixes and minor feature were the focus for this release, some bigger changes were included because of their importance. The parsing of documents saw a complete rewrite being now class based and allows for more than one parser per mimetype with sequencial fallback. This provides the -best text extraction on deployments where users have control over the +best text extraction on deployments where users have control over the installation and basic extraction when deploying on the cloud or other environments where users don't have the ability to install OS level binaries. @@ -24,7 +24,7 @@ What's new in Mayan EDMS v0.12.1 Fabric file (fabfile) ~~~~~~~~~~~~~~~~~~~~~ -A Fabric file is included to help users not very familiar with Ubuntu, +A Fabric file is included to help users not very familiar with Ubuntu, Python and Django install **Mayan EDMS**, or for system administrators looking to automate the install whether in local or remote systems. At the moment the fabfile will install **Mayan EDMS** in the same configurations @@ -36,10 +36,10 @@ the fabfile as more are tested. Documentation update ~~~~~~~~~~~~~~~~~~~~ The installation instructions were updated to include the installation of -the libpng-dev and libjpeg-dev libraries as well as the installation of +the libpng-dev and libjpeg-dev libraries as well as the installation of the poppler-utils package. An additional step to help users test their new installation of **Mayan EDMS** was also added. - + Translations ~~~~~~~~~~~~ The Italian translation has been synchronized with the source files at @@ -50,30 +50,30 @@ Usability improvements The index instance view now feature the same multi document action buttons (Submit to OCR, delete, download, etc) as the mail and recent document views. - + Better office document conversion ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A new method of converting office documents has been implemented, this new method doesn't require the use of the command line utility ``UNOCONV``. If this new method proves to work better than previous solutions the use of ``UNOCONV`` may be deprecated in the future. The conversion method -adds just one new configuration option: :setting:`CONVERTER_LIBREOFFICE_PATH` +adds just one new configuration option: `CONVERTER_LIBREOFFICE_PATH` which defaults to '/usr/bin/libreoffice'. Better PDF text parsing ~~~~~~~~~~~~~~~~~~~~~~~ Brian E. submitted a patch to use the Poppler package pdftotext utility to extract text from PDF files. This is now the default method Mayan EDMS -will execute to try to extract text from a PDF and failing that will +will execute to try to extract text from a PDF and failing that will fallback to the previous method. This change add a new configuration -option: :setting:`OCR_PDFTOTEXT_PATH` to specify the location of the ``pdftotext`` +option: `OCR_PDFTOTEXT_PATH` to specify the location of the ``pdftotext`` executable, it defaults to '/usr/bin/pdftotext'. Be sure to install the ``poppler-utils`` os package to take advantage of this new parser. Changed defaults ~~~~~~~~~~~~~~~~ -The OCR queue is now active by default when first created during the -``syncdb`` phase and the :setting:`OCR_AUTOMATIC_OCR` option now defaults +The OCR queue is now active by default when first created during the +``syncdb`` phase and the `OCR_AUTOMATIC_OCR` option now defaults to ``True``. These two changes are made to reduce the steps required for new users to start enjoying the benefits of automatic text extraction from uploaded documents without having to read the documentation and have a more @@ -95,7 +95,7 @@ Install the ``poppler-utils`` package: * Ubuntu, Debian:: $ apt-get install -y poppler-utils - + * Fedora:: $ yum install -y poppler-utils @@ -112,5 +112,5 @@ Bugs fixed * Issue #25 "Office document conversion error" Stuff removed -============= +============= * None diff --git a/docs/releases/0.12.2.rst b/docs/releases/0.12.2.rst index e8e4c83f35..b0bcc4aa45 100644 --- a/docs/releases/0.12.2.rst +++ b/docs/releases/0.12.2.rst @@ -27,10 +27,10 @@ Smarter auto admin creation **Mayan EDMS** creates a administrator user during the database creation phase to reduce the amount of steps required for a functional install. The creation of this account is controlled by the configuration -option :setting:`COMMON_AUTO_CREATE_ADMIN`, the username of the account is -specified with the configuration option :setting:`COMMON_AUTO_ADMIN_USERNAME` -and the password of this account by the option :setting:`COMMON_AUTO_ADMIN_PASSWORD`. -Previously the :setting:`COMMON_AUTO_ADMIN_PASSWORD` defaulted to 'admin' which +option `COMMON_AUTO_CREATE_ADMIN`, the username of the account is +specified with the configuration option `COMMON_AUTO_ADMIN_USERNAME` +and the password of this account by the option `COMMON_AUTO_ADMIN_PASSWORD`. +Previously the `COMMON_AUTO_ADMIN_PASSWORD` defaulted to 'admin' which created an administrator account of username 'admin' with a password of 'admin'. The new default is to randomize an initial password and show this password at the login screen until the administrator password is changed. diff --git a/docs/releases/0.12.rst b/docs/releases/0.12.rst index 67f4f20f1e..d46b3cb0a4 100644 --- a/docs/releases/0.12.rst +++ b/docs/releases/0.12.rst @@ -60,7 +60,7 @@ ACL support Anonymous user support ~~~~~~~~~~~~~~~~~~~~~~ Anonymous user support is a two tier function, first is the addition of -the :setting:`COMMON_ALLOW_ANONYMOUS_ACCESS` configuration option that +the `COMMON_ALLOW_ANONYMOUS_ACCESS` configuration option that allows non authenticated user to browse all the pages of a **Mayan EDMS** installation. The second part of this support is the ability to assign permissions or individual access to objects to anonymous users. @@ -88,7 +88,7 @@ the download of several documents in a single compressed file. Customizable GPG home directory ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Addition of the :setting:`SIGNATURES_GPG_HOME` configuration option to let +Addition of the `SIGNATURES_GPG_HOME` configuration option to let administrators set **Mayan EDMS**'s GPG instance home directory, used to store keyrings and other GPG configuration files. @@ -116,7 +116,7 @@ These populated trees can also be mirrored on the physical filesystem and shared using Samba or another filesharing server giving users a structured view of the documents contained within **Mayan EDMS** from the ``Indexes`` tab or from a mirrored index shared via the network. A new configuration option -has been added, :setting:`DOCUMENT_INDEXING_FILESYSTEM_SERVING`, which maps +has been added, `DOCUMENT_INDEXING_FILESYSTEM_SERVING`, which maps the index internal name with the physical directory where such index will be mirrored on disk. @@ -226,6 +226,6 @@ Stuff removed * Configuration options removed: * OCR_CACHE_URI - * DOCUMENT_INDEXING_FILESYSTEM_FILESERVING_PATH - Use the newest :setting:`DOCUMENT_INDEXING_FILESYSTEM_SERVING` - * DOCUMENT_INDEXING_FILESYSTEM_FILESERVING_ENABLE - Use the newest :setting:`DOCUMENT_INDEXING_FILESYSTEM_SERVING` + * DOCUMENT_INDEXING_FILESYSTEM_FILESERVING_PATH - Use the newest `DOCUMENT_INDEXING_FILESYSTEM_SERVING` + * DOCUMENT_INDEXING_FILESYSTEM_FILESERVING_ENABLE - Use the newest `DOCUMENT_INDEXING_FILESYSTEM_SERVING` diff --git a/docs/releases/2.0.rst b/docs/releases/2.0.rst index df9c54e898..bc1fd94690 100644 --- a/docs/releases/2.0.rst +++ b/docs/releases/2.0.rst @@ -12,179 +12,216 @@ What's new in Mayan EDMS v2.0 Update to Django 1.7 -------------------- -The biggest change of this release comes in the form of support for Django 1.7. **Mayan EDMS** makes use of several new features -of Django 1.7 like: migrations, app config and transaction handling. The version of Django supported in this version is 1.7.10. -With the move to Django 1.7, support for South migrations and Python 2.6 is removed. The switch to Django 1.7's app config -means that the startup order of app should not longer have any relevance, cause any import or startup -problems. +The biggest change of this release comes in the form of support for Django 1.7. +**Mayan EDMS** makes use of several new features of Django 1.7 like: migrations, +app config and transaction handling. The version of Django supported in this +version is 1.7.10. With the move to Django 1.7, support for South migrations +and Python 2.6 is removed. The switch to Django 1.7's app config means that +the startup order of app should not longer have any relevance, cause any import +or startup problems. Frontend UI ----------- -The frontend UI HTML has been refactored to use Bootstrap. Along with this update a lot of legacy HTML and CSS was removed, greatly -simplifying the existing template and allowing the removal of some. +The frontend UI HTML has been refactored to use Bootstrap. Along with this +update a lot of legacy HTML and CSS was removed, greatly simplifying the +existing template and allowing the removal of some. Theming and rebranding ---------------------- -All the presentation logic and markup has been moved into it's own app, the 'appearance' app. -All modifications required to customize the entire look of the **Mayan EDMS** can now be done in a single app. Very little markup -remains in the other apps, and it's usually because of necesity, namely the widgets.py modules. +All the presentation logic and markup has been moved into it's own app, the +'appearance' app. All modifications required to customize the entire look of +the **Mayan EDMS** can now be done in a single app. Very little markup remains +in the other apps, and it's usually because of necesity, namely the widgets.py +modules. Improved page navigation interface ---------------------------------- -Previously the document page interface used a fancybox windows leaving the current document in the background. -This UI workflow as been improved and the document page navigation behaves like the rest of the document -views. +Previously the document page interface used a fancybox windows leaving the +current document in the background. This UI workflow as been improved and the +document page navigation behaves like the rest of the document views. Menu reorganization ------------------- -To improve user experience, the main menu has been restructured based on function usage, moving seldom used buttons inside -other views. +To improve user experience, the main menu has been restructured based on +function usage, moving seldom used buttons inside other views. Removal of famfam icon set -------------------------- -The previously used icon set and icon display code was removed and a new system that favor font icon was added. +The previously used icon set and icon display code was removed and a new +system that favor font icon was added. Document preview generation --------------------------- -The image conversion system was refactored from the ground up and uses a much smarted caching system. The document -image cache has it's own Django file storage driver and no longer default to the system /tmp directory. -By moving the document image cache to a Django file storage, the cache doesn't need to reside -in the same filesystem or even computer serving the document images. This change also allows nodes in a -clutered install to share the document image cache. +The image conversion system was refactored from the ground up and uses a much +smarted caching system. The document image cache has it's own Django file +storage driver and no longer default to the system /tmp directory. By moving +the document image cache to a Django file storage, the cache doesn't need to +reside in the same filesystem or even computer serving the document images. +This change also allows nodes in a clustered install to share the document +image cache. Document submission for OCR changed to POST ------------------------------------------- -Previously submitting a document for OCR could be done with a GET request to the corresponding URL. -This design decision allowed for fast user experience but caused massive document submissions -when sites were scanned by web spiders. The new workflow is to submit documents to the -OCR queue only on POST request. +Previously submitting a document for OCR could be done with a GET request to +the corresponding URL. This design decision allowed for fast user experience +but caused massive document submissions when sites were scanned by web spiders. +The new workflow is to submit documents to the OCR queue only on POST request. New YAML based settings system ------------------------------ -The first phase of the new distributed settings system has landed in this version. -This first change causes settings to be serialized to YAML. This also means that it is -not possible to pass functions or custom classes as values to settings. -Setting that related to a class or function, now specify the path to those classes or -functions and they are imported dynamically at runtime. -Example: DOCUMENTS_STORAGE_BACKEND = 'storage.backends.filebasedstorage.FileBasedStorage' +The first phase of the new distributed settings system has landed in this +version. This first change causes settings to be serialized to YAML. This also +means that it is not possible to pass functions or custom classes as values to +settings. Setting that related to a class or function, now specify the path to +those classes or functions and they are imported dynamically at runtime. +Example:: + + DOCUMENTS_STORAGE_BACKEND = 'storage.backends.filebasedstorage.FileBasedStorage' Removal of auto admin creation ------------------------------ -The auto admin user creation code used during new installs has been removed and it -is its own reusable Django app. The app is available at https://pypi.python.org/pypi/django-autoadmin +The auto admin user creation code used during new installs has been removed and +it is its own reusable Django app. The app is available at +https://pypi.python.org/pypi/django-autoadmin Removal of dependencies ----------------------- -Through optimizations and code reduction several Python libraries and Django app are no longer -required. These are: South, GitPython, django-pagination, psutil, python-hkp, sendfile and slate. +Through optimizations and code reduction several Python libraries and Django +app are no longer required. These are: + +* South +* GitPython +* django-pagination +* psutil +* python-hkp +* sendfile +* slate ACL system refactor ------------------- -The Access Control System has been greatly simplified and optimized. The logistics to grant and revoke permissions -are now as follows: Only Roles can hold permissions, groups and user can no longer on their own be granted a permission. -Groups are now only organizational units that hold users and Roles are collections of groups. User are just a -profile and authentication information object. So to grant a permission or access to a document to a user, grant -those permissions to a new or existing role, add the desired user to a group and add that group to the role to -which you granted the permission. When thinking about granting permissions think of it this way: -Permissions -> Roles -> Groups -> User -Permissions for a document -> Roles -> Groups -> User -Permissions for a type of document -> Roles -> Groups -> User +The Access Control System has been greatly simplified and optimized. The +logistics to grant and revoke permissions are now as follows: Only Roles can +hold permissions, groups and user can no longer on their own be granted a +permission. Groups are now only organizational units that hold users and Roles +are collections of groups. User are just a profile and authentication +information object. So to grant a permission or access to a document to a user, +grant those permissions to a new or existing role, add the desired user to a +group and add that group to the role to which you granted the permission. When +thinking about granting permissions think of it this way: + +**Permissions -> Roles -> Groups -> User** + +**Permissions for a document -> Roles -> Groups -> User** + +**Permissions for a type of document -> Roles -> Groups -> User** + Object access control inheritance --------------------------------- -A frequently asked feature is the ability to change the access control of a group of documents. This -feature has been implemented in the form of object access control inheritance. This means that -if you grant a permission to a role for a document type, that role will inherit that permission -for all document that are later created of that type. If you revoke a permission from a role -for a document type, that role loses that permission for all documents of that type. With this new -system changing the access control of individual documents should be an edge case. This new ability of -modifying the access control of document types is the new recommended method. +A frequently asked feature is the ability to change the access control of a +group of documents. This feature has been implemented in the form of object +access control inheritance. This means that if you grant a permission to a role +for a document type, that role will inherit that permission for all document +that are later created of that type. If you revoke a permission from a role for +a document type, that role loses that permission for all documents of that type. +With this new system changing the access control of individual documents +should be an edge case. This new ability of modifying the access control of +document types is the new recommended method. Removal of anonymous user support --------------------------------- -Allowing anonymous users access to your document repository is no longer support. Admininstrator -wanting to make a group of documents public are encouraged to create an user, group and role for -that purpose. +Allowing anonymous users access to your document repository is no longer +support. Admininstrator wanting to make a group of documents public are +encouraged to create an user, group and role for that purpose. Metadata validators refactor ---------------------------- -The metadata validator have been splitted into: Validators and Parsers. Validators will just -check that the input value conforms to certain specification, raising a validation error is not -and blocking the user from submitting data. The Parsers will transform user input and store the -result as the metadata value. +The metadata validator have been splitted into: Validators and Parsers. +Validators will just check that the input value conforms to certain +specification, raising a validation error is not and blocking the user from +submitting data. The Parsers will transform user input and store the result as +the metadata value. Trash can support ----------------- -To avoid accidental data loss, documents are not deleted but moved to a virtual trash can. From -that trash can documents can them be deleted permanently. The deletion document documents and -the moving of documents to the trash can are governed by two different permissions. +To avoid accidental data loss, documents are not deleted but moved to a virtual +trash can. From that trash can documents can them be deleted permanently. The +deletion document documents and the moving of documents to the trash can are +governed by two different permissions. Retention policies ------------------ Support for retention policies was added and is control on a document type basis. -Two aspects can be controlled: the time at which documents will be automatically moved to the -trash can and the time after which documents in the trash can will be automatically -deleted. By default all new document types created will have a retention policy that doesn't -move documents to the trash can and that permanently deletes documents in the trash can after -30 days. +Two aspects can be controlled: the time at which documents will be +automatically moved to the trash can and the time after which documents in the +trash can will be automatically deleted. By default all new document types +created will have a retention policy that doesn't move documents to the trash +can and that permanently deletes documents in the trash can after 30 days. Support to share an index as a FUSE filesystem ---------------------------------------------- -Index mirror has been added after being removed several version ago. This time mirroring works -by creating a FUSE filesystem that is then mounted anywhere in the filesystem. The -previous implementation used symbolic links that while fast, required constant modification -to keep in sync with the indexes structure and only worked when the document storage and -the index mirror resided in the same physical computer or node. This new implementation -allowing mirroring of indexes even across a network or if the document storage is not a -traditional filesystem but a remote object store. Since this new FUSE mirroring uses -direct read access to the database caching is provided and is controlled by the -MIRRORING_DOCUMENT_CACHE_LOOKUP_TIMEOUT and MIRRORING_NODE_CACHE_LOOKUP_TIMEOUT setting -options. Both setting have a default of 10 seconds. +Index mirror has been added after being removed several version ago. This time +mirroring works by creating a FUSE filesystem that is then mounted anywhere in +the filesystem. The previous implementation used symbolic links that while +fast, required constant modification to keep in sync with the indexes structure +and only worked when the document storage and the index mirror resided in the +same physical computer or node. This new implementation allowing mirroring of +indexes even across a network or if the document storage is not a traditional +filesystem but a remote object store. Since this new FUSE mirroring uses direct +read access to the database caching is provided and is controlled by the +**MIRRORING_DOCUMENT_CACHE_LOOKUP_TIMEOUT** and **MIRRORING_NODE_CACHE_LOOKUP_TIMEOUT** +setting options. Both setting have a default of 10 seconds. Clickable preview images titles ------------------------------- -To shorten the amount of click required to access a document, document preview image titles -are clickable and will take the user straight to the document view. +To shorten the amount of click required to access a document, document preview +image titles are clickable and will take the user straight to the document view. Removal of eval --------------- -Use of Python's eval statement has been completely removed. Metadata type defaults, lookup fields, -smart links and indexes templates now use Django's own template language. +Use of Python's eval statement has been completely removed. Metadata type +defaults, lookup fields, smart links and indexes templates now use Django's +own template language. Smarter OCR ----------- -Document OCR workflow has been improved to try to parse text for each document page and in -failing to parse text will only perform OCR on that specific page, returning to the parsing -behavior for the next page. This allowing proper text extraction of documents containing both, -embedded text and images. +Document OCR workflow has been improved to try to parse text for each document +page and in failing to parse text will only perform OCR on that specific page, +returning to the parsing behavior for the next page. This allowing proper text +extraction of documents containing both, embedded text and images. Failure tolerance ----------------- -Previous versions made use of transactions to prevent data loss in the event of an unexpected -error. This release improves on that approach by also reacting to infrastructure failures. -**Mayan EDMS** can now recover without any or minimal data loss from critical events such as loss -of connectivity to the database manager. This changes allow installation of using database managers -that do not provide guaranteed concurrency such as SQLite, to scale to thousand of documents. -While this configuration is still not recommended, **Mayan EDMS** will now work and scale much better -in environments where parts of the infrastructure cannot be changed (such as the database manager). +Previous versions made use of transactions to prevent data loss in the event of +an unexpected error. This release improves on that approach by also reacting +to infrastructure failures. **Mayan EDMS** can now recover without any or +minimal data loss from critical events such as loss of connectivity to the +database manager. This changes allow installation of using database managers +that do not provide guaranteed concurrency such as SQLite, to scale to thousand +of documents. While this configuration is still not recommended, **Mayan EDMS** +will now work and scale much better in environments where parts of the +infrastructure cannot be changed (such as the database manager). -For more information about this change read the blog post: http://blog.robertorosario.com/testing-django-project-infrastructure-failure-tolerance/ +For more information about this change read the blog post: +http://blog.robertorosario.com/testing-django-project-infrastructure-failure-tolerance/ -As a result of this work a new Django app called Django-sabot was created that gives Django projects the -ability to create unit tests for infrastructure failure tolerance: https://pypi.python.org/pypi/django-sabot +As a result of this work a new Django app called Django-sabot was created that +gives Django projects the ability to create unit tests for infrastructure +failure tolerance: https://pypi.python.org/pypi/django-sabot RGB tags -------- -Previously tags could only choose from a predertermined number of color. This release changes -that and tags be of any color. Tags now store the color selected in HTML RGB format. Existing tags -are automatically converted to this new scheme. +Previously tags could only choose from a predertermined number of color. This +release changes that and tags be of any color. Tags now store the color +selected in HTML RGB format. Existing tags are automatically converted to this +new scheme. Default document type and default document source ------------------------------------------------- -After installation a default document type and document source are created, this means -that users can start uploading documents as soon as **Mayan EDMS** is installed without -having to do any configuration setting changes. The default document type and default document -source are both called 'Default'. +After installation a default document type and document source are created, +this means that users can start uploading documents as soon as **Mayan EDMS** +is installed without having to do any configuration setting changes. The +default document type and default document source are both called 'Default'. Other changes ------------- @@ -232,7 +269,7 @@ Other changes * Reduction of text strings. Upgrading from a previous version -================================= +--------------------------------- Using PIP ~~~~~~~~~ @@ -251,7 +288,8 @@ If you installed Mayan EDMS by cloning the Git repository issue the commands:: $ git reset --hard HEAD $ git pull -otherwise download the compressed archived and uncompress it overriding the existing installation. +otherwise download the compressed archived and uncompress it overriding the +existing installation. Next upgrade/add the new requirements:: @@ -293,6 +331,7 @@ Remove unused dependencies:: $ pip uninstall python-hkp $ pip uninstall django-sendfile $ pip uninstall django-pagination + $ pip uninstall slate The upgrade procedure is now complete. @@ -300,13 +339,14 @@ The upgrade procedure is now complete. Backward incompatible changes ============================= -* Document and sources transformations +* Current document and document sources transformations will be lost during upgrade. +* Permissions and Access Controls granted to users and/or groups will be lost during upgrade. Bugs fixed or issues closed =========================== * `issue #33 `_ Update to Django 1.7 -* `issue #59 `_ New bootstrap based UI * `issue #60 `_ Backport class based navigation code from the unstable branch * `issue #62 `_ Simplify and reduce code in templates * `issue #67 `_ Python 3 compatibility: Update models __unicode__ methdo to __str__ methods (using Django's six library) @@ -317,4 +357,3 @@ Bugs fixed or issues closed .. _PyPI: https://pypi.python.org/pypi/mayan-edms/ - diff --git a/docs/topics/deploying.rst b/docs/topics/deploying.rst new file mode 100644 index 0000000000..53a2f0459f --- /dev/null +++ b/docs/topics/deploying.rst @@ -0,0 +1,28 @@ +========= +Deploying +========= + +Like other Django based projects **Mayan EDMS** can be deployed in a wide variety +of ways. The method provided below is only provided as a bare minimum example. +These instructions asume you installed **Mayan EDMS** as mentioned in the +Installation chapter. + +Install the system dependencies: + + sudo apt-get install nginx supervisor redis-server + +Switch and activate the `virtualenv` where you installed **Mayan EDMS**. Install +the Python client for redis: + + pip install redis + +Install the Python application server gunicorn: + + pip install gunicorn + +Update the settings/local.py file: + + BROKER_URL = 'redis://127.0.0.1:6379/0' + CELERY_ALWAYS_EAGER = False + CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/0' + diff --git a/docs/topics/development.rst b/docs/topics/development.rst index e45495179d..c5f8094443 100644 --- a/docs/topics/development.rst +++ b/docs/topics/development.rst @@ -6,11 +6,11 @@ Development **Mayan EDMS** is under active development, and contributions are welcome. If you have a feature request, suggestion or bug report, please open a new -issue on the `GitHub issue tracker`_. To submit patches, please send a pull -request on GitHub_. Make sure to add yourself to the :ref:`contributors` file. +issue on the `GitLab issue tracker`_. To submit patches, please send a pull +request on GitLab_. Make sure to add yourself to the :ref:`contributors` file. -.. _GitHub: https://github.com/mayan-edms/mayan-edms/ -.. _`GitHub issue tracker`: https://github.com/mayan-edms/mayan-edms/issues +.. _GitLab: https://gitlab.com/mayan-edms/mayan-edms/ +.. _`GitLab issue tracker`: https://gitlab.com/mayan-edms/mayan-edms/issues Project philosophies @@ -286,9 +286,9 @@ Source Control **Mayan EDMS** source is controlled with Git_. -The project is publicly accessible, hosted and can be cloned from **GitHub** using:: +The project is publicly accessible, hosted and can be cloned from **GitLab** using:: - $ git clone git://github.com/mayan-edms/mayan-edms.git + $ git clone git://gitlab.com/mayan-edms/mayan-edms.git Git branch structure @@ -306,21 +306,20 @@ Git branch structure Released versions. -Each release is tagged and available for download on the Downloads_ section of the **Mayan EDMS** repository on GitHub_. +Each release is tagged separately. -When submitting patches, please place your code in its own ``feature/`` branch prior to opening a pull request on GitHub_. +When submitting patches, please place your code in its own ``feature/`` branch prior to opening a Merge Request on GitLab_. .. _Git: http://git-scm.org .. _`Successful Git Branching Model`: http://nvie.com/posts/a-successful-git-branching-model/ .. _git-flow: https://github.com/nvie/gitflow -.. _Downloads: https://github.com/mayan-edms/mayan-edms/archives/master Steps to deploy a development version ------------------------------------- .. code-block:: bash - $ git clone https://github.com/mayan-edms/mayan-edms.git + $ git clone https://gitlab.com/mayan-edms/mayan-edms.git $ cd mayan-edms $ git checkout development $ virtualenv venv @@ -372,14 +371,12 @@ Then on a separate console launch a celery worker from the same provisioned Vagr Contributing changes -------------------- -Once your have created and committed some new code or feature, submit a Pull Request. -Be sure to merge with mayan-edms/development before doing a pull request so that patches -apply as cleanly as possible. If there are no conflicts, Pull Requests can be merged -directly from Github otherwise a manual command line merge has to be done and -your patches might take longer to get merged. - -For more information on how to create Pull Request read: https://help.github.com/articles/using-pull-requests -or the quick version: https://help.github.com/articles/creating-a-pull-request +Once your have created and committed some new code or feature, submit a Pull +Request. Be sure to merge with the development branch before doing a Pull +Request so that patches apply as cleanly as possible. If there are no conflicts, +Merge Requests can be merged directly from the website UI otherwise a manual +command line merge has to be done and your patches might take longer to get +merged. Debugging diff --git a/docs/topics/installation.rst b/docs/topics/installation.rst index da025e1f0e..4a377fa049 100644 --- a/docs/topics/installation.rst +++ b/docs/topics/installation.rst @@ -26,7 +26,8 @@ Initialize a ``virtualenv`` to deploy the project: $ pip install mayan-edms By default **Mayan EDMS** will create a single file SQLite_ database, which makes -it very easy to start using **Mayan EDMS**. Populate the database with the project's schema doing: +it very easy to start using **Mayan EDMS**. Populate the database with the +project's schema doing: .. code-block:: bash @@ -34,13 +35,15 @@ it very easy to start using **Mayan EDMS**. Populate the database with the proje $ mayan-edms.py runserver Point your browser to http://127.0.0.1:8000. If everything was installed -correctly you should see the login screen and panel showing a randomly generated admin password. +correctly you should see the login screen and panel showing a randomly generated +admin password. Mac OSX ------- -**Mayan EDMS** is dependant on a number of binary packages and the recommended way is -to use a package manager such as `MacPorts `_ or `Homebrew `. +**Mayan EDMS** is dependant on a number of binary packages and the recommended +way is to use a package manager such as `MacPorts `_ +or `Homebrew `. Use MacPorts to install binary dependencies @@ -52,13 +55,6 @@ With MacPorts installed run the command: sudo port install python-dev gcc tesseract-ocr unpaper python-virtualenv ghostscript libjpeg-dev libpng-dev poppler-utils -**Mayan EDMS** can use either Pillow, ImageMagick or GraphicsMagick as the backend to -convert images so in addition you can install GraphicsMagick ... - -.. code-block:: bash - - sudo port install graphicsmagick - Set the Binary paths ******************** @@ -68,19 +64,12 @@ to /usr/bin/ with ... .. code-block:: bash - sudo ln -s /opt/local/bin/tesseract /usr/bin/tesseract && \ - sudo ln -s /opt/local/bin/identify /usr/bin/identify && \ - sudo ln -s /opt/local/bin/gs /usr/bin/gs + sudo ln -s /opt/local/bin/tesseract /usr/bin/tesseract ... alternatively set the paths in the ``settings/locals.py`` .. code-block:: python - # document converters - CONVERTER_IM_CONVERT_PATH = '/opt/local/bin/convert' - IM_IDENTIFY_PATH = '/opt/local/bin/identify' - GRAPHICS_BACKEND = 'converter.backends.graphicsmagick.GraphicsMagick' - GM_PATH = '/opt/local/bin/gm' LIBREOFFICE_PATH = '/Applications/LibreOffice.app/Contents/MacOS/soffice' Or Use Homebrew @@ -90,7 +79,7 @@ With Homebrew installed run the command: .. code-block:: bash - brew install python gcc tesseract unpaper poppler libpng graphicsmagick postgresql + brew install python gcc tesseract unpaper poppler libpng postgresql Set the Binary paths ******************** @@ -102,20 +91,14 @@ to /usr/bin/ with ... .. code-block:: bash sudo ln -s /usr/local/bin/tesseract /usr/bin/tesseract && \ - sudo ln -s /usr/local/bin/identify /usr/bin/identify && \ sudo ln -s /usr/local/bin/unpaper /usr/bin/unpaper && \ sudo ln -s /usr/local/bin/pdftotext /usr/bin/pdftotext && \ - sudo ln -s /usr/local/bin/gs /usr/bin/gs && \ + sudo ln -s /usr/local/bin/gs /usr/bin/gs ... alternatively set the paths in the ``settings/locals.py`` .. code-block:: python - # document converters - CONVERTER_IM_CONVERT_PATH = '/usr/local/bin/convert' - IM_IDENTIFY_PATH = '/usr/local/bin/identify' - GRAPHICS_BACKEND = 'converter.backends.graphicsmagick.GraphicsMagick' - GM_PATH = '/usr/local/bin/gm' LIBREOFFICE_PATH = '/Applications/LibreOffice.app/Contents/MacOS/soffice' Testing the install @@ -145,6 +128,8 @@ After making sure everything is running correctly, stop the `runserver` command. Deploy **Mayan EDMS** using the webserver of your preference. For more information on deployment instructions and examples, checkout Django's official documentation on the topic https://docs.djangoproject.com/en/1.6/howto/deployment/ +For a simple production deployment setup follow the instructions in the +Deploying chapter Other database managers From ceed96401b02b9927687cfab81173125bde52c01 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 11 Sep 2015 22:51:39 -0400 Subject: [PATCH 5/9] Add app creation chapter to documentation. --- docs/index.rst | 1 + docs/topics/app_creation.rst | 143 +++++++++++++++++++++++++++++++++ docs/topics/development.rst | 149 ++++++----------------------------- 3 files changed, 170 insertions(+), 123 deletions(-) create mode 100644 docs/topics/app_creation.rst diff --git a/docs/index.rst b/docs/index.rst index 2eba239b7f..128a2227f0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -30,6 +30,7 @@ account. Release notes and upgrading Concepts Development + App creation Translations Contributors Licensing diff --git a/docs/topics/app_creation.rst b/docs/topics/app_creation.rst new file mode 100644 index 0000000000..288d56c87b --- /dev/null +++ b/docs/topics/app_creation.rst @@ -0,0 +1,143 @@ +============ +App creation +============ + +**Mayan EDMS** apps are essentially Django app with some extra code to register +navigation, permissions and other relationships. + + +App modules +=========== + +- __init__.py + + Should be empty if possible. No initialization code should be here, use the + ready() method of the MayanAppConfig class in the apps.py module. + +- admin.py + + Standard Django app module to define how models are to be presented in the + admin interface. + +- api_views.py + + REST API views go here. **Mayan EDMS** uses Django REST Framework API view + classes. + +- apps.py + + Contains the MayanAppConfig subclass as required by Django 1.7 and up. This + is a place to define the app name and translatable verbose name as well as + code to be execute when the modules of the app are ready. + +- classes.py + + Hold python classes to be used internally or externally. Any class defined by + the app that is not a model. + +- events.py + + Define event class instances that are later commited to a log by custom + code. + +- exceptions.py + + Custom exceptions defined by the app. + +- fields.py + + Place any custom form field classed you define here. + +- forms.py + + Standard Django app module that hold custom form classes. + +- handlers.py + + Contains the signal handlers, functions that will process a given signal + emited from this or other apps. Connect the handler functions to the + corresponding signal in the ready() method of the MayanAppConfig subclass in + apps.py + +- links.py + + Defines the links to be used by the app. Import only from the navigation app + and the local permissions.py file. + +- literals.py + + Stores magic numbers, module choices (if static), settings defaults, and + constants. Should contain all capital case variables. Must not import from + any other module. + +- managers.py + + Standard Django app module that hold custom model managers. These act as + model class method to performs actions in a series of model instances or + utilitarian actions on external models instances. + +- models.py + + Standard Django app module that defines ORM persistent data schema. + +- permissions.py + + Defines the permissions to be used to validate user access by links and views. + Imports only from the permissions app. Link or view conditions such as + testing for is_staff or is_super_user flag are defined in this same module. + +- runtime.py + + Use this module when you need the same instance of a class for the entire app. + This module acts as a shared memory space for the other modules of the app or + other apps. + +- serializers.py + + Hold Django REST Framework serializers used by the api_views.py module. + +- settings.py + + Define the config settings instances that the app will use. + +- signals.py + + Any custom defined signal goes here. + +- statistics.py + + Provides functions that will compute any sort of statistical information on + the app’s data. + +- tasks.py + + Code to be execute in the background or as an out-of-process action. + +- tests/ directory + + Hold test modules. There should be one test_*.py module for each aspect being + tested, examples: test_api.py, test_views.py, test_parsers.py, test_permissions.py + Any shared constant data used by the tests should be added to tests/literals.py + +- utils.py + + Holds utilitarian code that doesn't fit on any other app module or that is + used by several modules in the app. Anything used internally by the app that + is not a class or a literal (should be as little as possible) + +- widgets.py + + HTML widgets go here. This should be the only place with presentation + directives in the app (aside the templates). + + +Views +===== + +The module common.generics provides custom generic class based views to be used. +The basic views used to create, edit, view and delete objects in **Mayan EDMS** +are: SingleObjectCreateView, SingleObjectDetailView, SingleObjectEditView, +and SingleObjectListView + +These views handle aspects relating to view permissions, object permissions, +post action redirection and template context generation. diff --git a/docs/topics/development.rst b/docs/topics/development.rst index c5f8094443..8dac37e0bd 100644 --- a/docs/topics/development.rst +++ b/docs/topics/development.rst @@ -16,7 +16,8 @@ request on GitLab_. Make sure to add yourself to the :ref:`contributors` file. Project philosophies -------------------- -How to think about **Mayan EDMS** when doing changes or adding new features, why things are the way they are in **Mayan EDMS**. +How to think about **Mayan EDMS** when doing changes or adding new features, +why things are the way they are in **Mayan EDMS**. - Functionality must be as market/sector independent as possible, code for the 95% of use cases. - Each user must be able to configure and customize it to their needs after install. @@ -93,7 +94,9 @@ Example: ) from .models import Index, IndexInstanceNode, DocumentRenameCount -All local app module imports are in relative form, local app module name is to be referenced as little as possible, unless required by a specific feature, trick, restriction, ie: Runtime modification of the module's attributes. +All local app module imports are in relative form, local app module name is to +be referenced as little as possible, unless required by a specific feature, +trick, restriction, ie: Runtime modification of the module's attributes. Incorrect: @@ -113,12 +116,18 @@ Correct: Dependencies ~~~~~~~~~~~~ -**Mayan EDMS** apps follow a hierarchical model of dependency. Apps import from their parents or siblings, never from their children. Think plugins. A parent app must never assume anything about a possible existing child app. The documents app and the Document model are the basic entities they must never import anything else. The common and main apps are the base apps. +**Mayan EDMS** apps follow a hierarchical model of dependency. Apps import from +their parents or siblings, never from their children. Think plugins. A parent +app must never assume anything about a possible existing child app. The +documents app and the Document model are the basic entities they must never +import anything else. The common and main apps are the base apps. Variables ~~~~~~~~~ -Naming of variables should follow a Major to Minor convention, usually including the purpose of the variable as the first piece of the name, using underscores as spaces. camelCase is not used in **Mayan EDMS**. +Naming of variables should follow a Major to Minor convention, usually +including the purpose of the variable as the first piece of the name, using +underscores as spaces. camelCase is not used in **Mayan EDMS**. Examples: @@ -158,128 +167,20 @@ Quotation character used in **Mayan EDMS** for strings is the single quote. Doub General ~~~~~~~ - -Code should appear in their modules in alphabetic order or in their order of importance if it makes more sense for the specific application. -This makes visual scanning easier on modules with a large number of imports, views or classes. -Class methods that return a value should be prepended with a ``get_`` to differentiate from an object’s properties. -When a variable refers to a file it should be named as follows: +Code should appear in their modules in alphabetic order or in their order of +importance if it makes more sense for the specific application. This makes +visual scanning easier on modules with a large number of imports, views or +classes. Class methods that return a value should be prepended with a +``get_`` to differentiate from an object’s properties. When a variable refers +to a file it should be named as follows: - filename: The file’s name and extension only. - filepath: The entire path to the file including the filename. - path: A path to a directory. Flash messages should end with a period as applicable for the language. -Only exception is when the tail of the message contains an exceptions message as passed directly from the exception object. - -App anatomy -~~~~~~~~~~~ - -- __init__.py - - - Should be empty if possible. No initialization code should be here, use the ready() method of the AppConfig class in the apps.py module. - -- api.py - - - File to hold functions that are meant to be used by external apps. - - Interfaces meant to be used by other apps that are not models or classes. - -- apps.py - - - Contains the AppConfig subclass as required by Django 1.7 and up. - -- classes.py - - - Hold python classes to be used internally or externally. - - Any class defined by the app that is not a model. - -- diagnostics.py - - - Define functions that will return the state of the data of an app. - - Does not fixes the problems only finds them. - -- events.py - - - Define history type events. - -- exceptions.py - - - Exceptions defined by the app. - -- fields.py - - - Place any custom form field classed you define here. - -- handlers.py - - - Contains the signal handlers, functions that will process a given signal emited from this or other apps. - -- links.py - - - Defines the links to be used by the app. - - Import only from the navigation app and the local icons.py file. - -- literals.py - - - Stores magic numbers, module choices (if static), settings defaults, and constants. - - Should contain all capital case variables. - - Must not import from any other module. - -- maintenance.py - - - Hold functions that the user may run periodically to fix errors in the app’s data. - -- permissions.py - - - Defines the permissions to be used by links and views to validate access. - - Imports only from permissions app. - - Link or view conditions such as testing for staff or super admin status are defined in the same file. - -- runtime.py - - - Use this module when you need the same instance of a class for the entire app. This app is acts as a shared memory space for the modules of the apps or other apps. - -- settings.py - - - Define the config settings that the app will use here. - -- signals.py - - - Any custom defined signal goes here. - -- statistics.py - - - Provides functions that will computer any sort of statistical information on the app’s data. - -- tasks.py - - - Code to be execute as in the background or a as an process-of-process action. - -- utils.py - - - Hold utilitarian code that doesn't fit on any other app file or that is used by several files in the app. - - Anything used internally by the app that is not a class or a literal (should be as little as possible) - -- widgets.py - - - HTML widgets go here. This should be the only place with presentation directives in the app (aside the templates). - -Views behavior -~~~~~~~~~~~~~~ - -- Delete views: - - - Redirect to object list view if one object is deleted. - - Redirect to previous view if many are deleted. - - Previous view equals: - - - previous variable in POST or - - previous variable in GET or - - request.META.HTTP_REFERER or - - object list view or - - 'home' view - - fallback to ‘/’ - - if previous equal same view then previous should equal object list view or ‘/’ - +Only exception is when the tail of the message contains an exceptions message +as passed directly from the exception object. Source Control -------------- @@ -294,7 +195,7 @@ The project is publicly accessible, hosted and can be cloned from **GitLab** usi Git branch structure -------------------- -**Mayan EDMS** follows the model layout by Vincent Driessen in his `Successful Git Branching Model`_ blog post. Git-flow_ is a great tool for managing the repository in this way. +**Mayan EDMS** follows simplified model layout by Vincent Driessen in his `Successful Git Branching Model`_ blog post. Git-flow_ is a great tool for managing the repository in this way. ``develop`` The "next release" branch, likely unstable. @@ -308,7 +209,8 @@ Git branch structure Each release is tagged separately. -When submitting patches, please place your code in its own ``feature/`` branch prior to opening a Merge Request on GitLab_. +When submitting patches, please place your code in its own ``feature/`` branch +prior to opening a Merge Request on GitLab_. .. _Git: http://git-scm.org .. _`Successful Git Branching Model`: http://nvie.com/posts/a-successful-git-branching-model/ @@ -331,7 +233,8 @@ Steps to deploy a development version Setting up a development version using Vagrant ---------------------------------------------- -Make sure you have Vagrant and a provider properly installed as per https://docs.vagrantup.com/v2/installation/index.html +Make sure you have Vagrant and a provider properly installed as per +https://docs.vagrantup.com/v2/installation/index.html Start and provision a machine using: From c1619c93e08fc9b42f83b72cdc862a9591f0276d Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 14 Sep 2015 21:57:55 -0400 Subject: [PATCH 6/9] Finish statistics subsystem refactor. --- docs/releases/2.0.rst | 1 + mayan/apps/documents/apps.py | 35 ++++- mayan/apps/documents/statistics.py | 140 +++++++++--------- mayan/apps/statistics/apps.py | 31 ++-- mayan/apps/statistics/classes.py | 88 ++++++++--- mayan/apps/statistics/links.py | 8 +- mayan/apps/statistics/literals.py | 3 - mayan/apps/statistics/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/purgestatistics.py | 12 ++ mayan/apps/statistics/tasks.py | 8 +- mayan/apps/statistics/urls.py | 11 +- mayan/apps/statistics/views.py | 36 ++++- 13 files changed, 243 insertions(+), 130 deletions(-) delete mode 100644 mayan/apps/statistics/literals.py create mode 100644 mayan/apps/statistics/management/__init__.py create mode 100644 mayan/apps/statistics/management/commands/__init__.py create mode 100644 mayan/apps/statistics/management/commands/purgestatistics.py diff --git a/docs/releases/2.0.rst b/docs/releases/2.0.rst index df9c54e898..e1dda20c3a 100644 --- a/docs/releases/2.0.rst +++ b/docs/releases/2.0.rst @@ -230,6 +230,7 @@ Other changes * setting_view_permission added to the smart settings app. It is no longer required to be a super admin or staff user to see the setting values. * Removal of the CombinedSource class. * Reduction of text strings. +* Statistics refactor; purgestatistics command Upgrading from a previous version ================================= diff --git a/mayan/apps/documents/apps.py b/mayan/apps/documents/apps.py index 982a5491ca..6b8eba3205 100644 --- a/mayan/apps/documents/apps.py +++ b/mayan/apps/documents/apps.py @@ -74,7 +74,8 @@ from .permissions import ( from .settings import setting_thumbnail_size from .statistics import ( new_documents_per_month, new_document_pages_per_month, - new_document_versions_per_month + new_document_versions_per_month, total_document_per_month, + total_document_page_per_month, total_document_version_per_month ) from .widgets import document_thumbnail @@ -352,24 +353,48 @@ class DocumentsApp(MayanAppConfig): links=(link_transformation_list,), sources=(DocumentPage,) ) - namespace = StatisticNamespace(name='documents', label=_('Documents')) + namespace = StatisticNamespace(slug='documents', label=_('Documents')) namespace.add_statistic( slug='new-documents-per-month', label=_('New documents per month'), func=new_documents_per_month, - renderer=CharJSLine + renderer=CharJSLine, + minute='0' ) namespace.add_statistic( slug='new-document-versions-per-month', label=_('New document versions per month'), func=new_document_versions_per_month, - renderer=CharJSLine + renderer=CharJSLine, + minute='0' ) namespace.add_statistic( slug='new-document-pages-per-month', label=_('New document pages per month'), func=new_document_pages_per_month, - renderer=CharJSLine + renderer=CharJSLine, + minute='0' + ) + namespace.add_statistic( + slug='total-documents-at-each-month', + label=_('Total documents at each month'), + func=total_document_per_month, + renderer=CharJSLine, + minute='0' + ) + namespace.add_statistic( + slug='total-document-versions-at-each-month', + label=_('Total document versions at each month'), + func=total_document_version_per_month, + renderer=CharJSLine, + minute='0' + ) + namespace.add_statistic( + slug='total-document-pages-at-each-month', + label=_('Total document pages at each month'), + func=total_document_page_per_month, + renderer=CharJSLine, + minute='0' ) post_initial_setup.connect( diff --git a/mayan/apps/documents/statistics.py b/mayan/apps/documents/statistics.py index 848288f68a..b838784e7a 100644 --- a/mayan/apps/documents/statistics.py +++ b/mayan/apps/documents/statistics.py @@ -5,45 +5,11 @@ import datetime import qsstats from django.db.models import Avg, Count, Max, Min -from django.template.defaultfilters import filesizeformat -from django.utils import formats -from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ from statistics.classes import Statistic from .models import Document, DocumentType, DocumentPage, DocumentVersion -from .runtime import storage_backend - - -def get_used_size(path, file_list): - total_size = 0 - for filename in file_list: - try: - total_size += storage_backend.size( - storage_backend.separator.join([path, filename]) - ) - except OSError: - pass - - return total_size - - -def storage_count(path='.'): - try: - directories, files = storage_backend.listdir(path) - except OSError: - return 0, 0 - else: - total_count = len(files) - total_size = get_used_size(path, files) - - for directory in directories: - file_count, files_size = storage_count(directory) - total_count += file_count - total_size += files_size - - return total_count, total_size def new_documents_per_month(): @@ -54,7 +20,7 @@ def new_documents_per_month(): return { 'series': { - 'Document': map(lambda x: {x[0].month: x[1]}, qss.time_series(start=this_year, end=today, interval='months')) + 'Documents': map(lambda x: {x[0].month: x[1]}, qss.time_series(start=this_year, end=today, interval='months')) } } @@ -67,7 +33,7 @@ def new_document_versions_per_month(): return { 'series': { - 'Document': map(lambda x: {x[0].month: x[1]}, qss.time_series(start=this_year, end=today, interval='months')) + 'Versions': map(lambda x: {x[0].month: x[1]}, qss.time_series(start=this_year, end=today, interval='months')) } } @@ -80,45 +46,83 @@ def new_document_pages_per_month(): return { 'series': { - 'Document': map(lambda x: {x[0].month: x[1]}, qss.time_series(start=this_year, end=today, interval='months')) + 'Pages': map(lambda x: {x[0].month: x[1]}, qss.time_series(start=this_year, end=today, interval='months')) + } + } + +def total_document_per_month(): + qss = qsstats.QuerySetStats(Document.objects.all(), 'date_added') + this_year = datetime.date.today().year + + result = [] + + for month in range(1, datetime.date.today().month + 1): + next_month = month + 1 + print month + + if next_month == 12: + next_month = 1 + year = this_year + 1 + else: + next_month = month + 1 + year = this_year + + result.append({month: qss.until(datetime.date(year, next_month, 1))}) + + return { + 'series': { + 'Documents': result } } -""" +def total_document_version_per_month(): + qss = qsstats.QuerySetStats(DocumentVersion.objects.all(), 'document__date_added') + this_year = datetime.date.today().year -class DocumentUsageStatistics(Statistic): - def get_results(self): - results = [] + result = [] - total_db_documents = Document.objects.only('pk',).count() + for month in range(1, datetime.date.today().month + 1): + next_month = month + 1 + print month - results.extend( - [ - _('Documents in database: %d') % total_db_documents, - ] - ) + if next_month == 12: + next_month = 1 + year = this_year + 1 + else: + next_month = month + 1 + year = this_year - try: - total_storage_documents, storage_used_space = storage_count() - results.append( - _('Documents in storage: %d') % total_storage_documents - ) - results.append( - _( - 'Space used in storage: %s' - ) % filesizeformat(storage_used_space) - ) - except NotImplementedError: - pass + result.append({month: qss.until(datetime.date(year, next_month, 1))}) - results.extend( - [ - _( - 'Document pages in database: %d' - ) % DocumentPage.objects.only('pk',).count(), - ] - ) + return { + 'series': { + 'Versions': result + } + } - return results -""" + +def total_document_page_per_month(): + qss = qsstats.QuerySetStats(DocumentPage.objects.all(), 'document_version__document__date_added') + this_year = datetime.date.today().year + + result = [] + + for month in range(1, datetime.date.today().month + 1): + next_month = month + 1 + print month + + if next_month == 12: + next_month = 1 + year = this_year + 1 + else: + next_month = month + 1 + year = this_year + + result.append({month: qss.until(datetime.date(year, next_month, 1))}) + + return { + 'series': { + 'Pages': result + } + } diff --git a/mayan/apps/statistics/apps.py b/mayan/apps/statistics/apps.py index 5d051fac0e..92c4ea886d 100644 --- a/mayan/apps/statistics/apps.py +++ b/mayan/apps/statistics/apps.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals -from datetime import timedelta - +from celery.schedules import crontab from kombu import Exchange, Queue from django.utils.translation import ugettext_lazy as _ @@ -9,13 +8,14 @@ from django.utils.translation import ugettext_lazy as _ from mayan.celery import app from common import MayanAppConfig, menu_object, menu_secondary, menu_tools +from navigation import SourceColumn + from .classes import Statistic, StatisticNamespace from .links import ( link_execute, link_namespace_details, link_namespace_list, - link_statistics + link_statistics, link_view ) -from .literals import STATISTICS_REFRESH_INTERVAL -from .tasks import task_check_statistics # NOQA - Force registration of task +from .tasks import task_execute_statistic # NOQA - Force registration of task class StatisticsApp(MayanAppConfig): @@ -25,13 +25,10 @@ class StatisticsApp(MayanAppConfig): def ready(self): super(StatisticsApp, self).ready() - app.conf.CELERYBEAT_SCHEDULE.update( - { - 'statistics.task_check_statistics': { - 'task': 'statistics.tasks.task_check_statistics', - 'schedule': timedelta(seconds=STATISTICS_REFRESH_INTERVAL), - }, - } + SourceColumn( + source=Statistic, + label=_('Schedule'), + attribute='schedule', ) app.conf.CELERY_QUEUES.extend( @@ -43,15 +40,7 @@ class StatisticsApp(MayanAppConfig): ) ) - app.conf.CELERY_ROUTES.update( - { - 'statistics.tasks.task_check_statistics': { - 'queue': 'statistics' - }, - } - ) - - menu_object.bind_links(links=(link_execute,), sources=(Statistic,)) + menu_object.bind_links(links=(link_execute, link_view), sources=(Statistic,)) menu_object.bind_links( links=(link_namespace_details,), sources=(StatisticNamespace,) ) diff --git a/mayan/apps/statistics/classes.py b/mayan/apps/statistics/classes.py index 6d634ceab8..333b288d48 100644 --- a/mayan/apps/statistics/classes.py +++ b/mayan/apps/statistics/classes.py @@ -2,6 +2,11 @@ from __future__ import unicode_literals import json +from celery.schedules import crontab +from djcelery.models import PeriodicTask + +from mayan.celery import app + from .models import StatisticResult @@ -13,14 +18,14 @@ class StatisticNamespace(object): return cls._registry.values() @classmethod - def get(cls, name): - return cls._registry[name] + def get(cls, slug): + return cls._registry[slug] - def __init__(self, name, label): - self.name = name + def __init__(self, slug, label): + self.slug = slug self.label = label self._statistics = [] - self.__class__._registry[name] = self + self.__class__._registry[slug] = self def __unicode__(self): return unicode(self.label) @@ -30,10 +35,6 @@ class StatisticNamespace(object): statistic.namespace = self self._statistics.append(statistic) - @property - def id(self): - return self.name - @property def statistics(self): return self._statistics @@ -42,6 +43,22 @@ class StatisticNamespace(object): class Statistic(object): _registry = {} + @staticmethod + def purge_schedules(): + queryset = PeriodicTask.objects.filter(name__startswith='statistics.').exclude(name__in=Statistic.get_task_names()) + + for periodic_task in queryset: + crontab_instance = periodic_task.crontab + periodic_task.delete() + + if crontab_instance and not crontab_instance.periodictask_set.all(): + # Only delete the interval if nobody else is using it + crontab_instance.delete() + + StatisticResult.objects.filter( + slug__in=queryset.values_list('name', flat=True) + ).delete() + @classmethod def get_all(cls): return cls._registry.values() @@ -50,11 +67,39 @@ class Statistic(object): def get(cls, slug): return cls._registry[slug] - def __init__(self, slug, label, func, renderer): + @classmethod + def get_task_names(cls): + return [task.get_task_name() for task in cls.get_all()] + + def __init__(self, slug, label, func, renderer, minute='*', hour='*', day_of_week='*', day_of_month='*', month_of_year='*'): self.slug = slug self.label = label self.func = func self.renderer = renderer + + self.schedule = crontab( + minute=minute, hour=hour, day_of_week=day_of_week, + day_of_month=day_of_month, month_of_year=month_of_year, + ) + + app.conf.CELERYBEAT_SCHEDULE.update( + { + self.get_task_name(): { + 'task': 'statistics.tasks.task_execute_statistic', + 'schedule': self.schedule, + 'args': (self.slug,) + }, + } + ) + + app.conf.CELERY_ROUTES.update( + { + self.get_task_name(): { + 'queue': 'statistics' + }, + } + ) + self.__class__._registry[slug] = self def __unicode__(self): @@ -63,14 +108,13 @@ class Statistic(object): def execute(self): self.store_results(results=self.func()) - @property - def id(self): - return self.slug + def get_task_name(self): + return 'statistics.task_execute_statistic_{}'.format(self.slug) def store_results(self, results): StatisticResult.objects.filter(slug=self.slug).delete() - statistic_result = StatisticResult.objects.create(slug=self.slug) + statistic_result, created = StatisticResult.objects.get_or_create(slug=self.slug) statistic_result.store_data(data=results) def get_results(self): @@ -95,6 +139,14 @@ class CharJSLine(ChartRenderer): template_name = 'statistics/backends/chartjs/line.html' dataset_palette = ( + { + 'fillColor': "rgba(220,220,220,0.2)", + 'strokeColor': "rgba(220,220,220,1)", + 'pointColor': "rgba(220,220,220,1)", + 'pointStrokeColor': "#fff", + 'pointHighlightFill': "#fff", + 'pointHighlightStroke': "rgba(220,220,220,1)", + }, { 'fillColor': "rgba(151,187,205,0.2)", 'strokeColor': "rgba(151,187,205,1)", @@ -103,14 +155,6 @@ class CharJSLine(ChartRenderer): 'pointHighlightFill': "#fff", 'pointHighlightStroke': "rgba(151,187,205,1)", }, - { - 'fillColor': "rgba(220,220,220,0.2)", - 'strokeColor': "rgba(220,220,220,1)", - 'pointColor': "rgba(220,220,220,1)", - 'pointStrokeColor': "#fff", - 'pointHighlightFill': "#fff", - 'pointHighlightStroke': "rgba(220,220,220,1)", - } ) def get_chart_data(self): diff --git a/mayan/apps/statistics/links.py b/mayan/apps/statistics/links.py index 6a55576cd2..88e6370188 100644 --- a/mayan/apps/statistics/links.py +++ b/mayan/apps/statistics/links.py @@ -8,12 +8,16 @@ from .permissions import permission_statistics_view link_execute = Link( + permissions=(permission_statistics_view,), text=_('Queue'), + view='statistics:statistic_queue', args='resolved_object.slug' +) +link_view = Link( permissions=(permission_statistics_view,), text=_('View'), - view='statistics:statistic_detail', args='resolved_object.id' + view='statistics:statistic_detail', args='resolved_object.slug' ) link_namespace_details = Link( permissions=(permission_statistics_view,), text=_('Namespace details'), - view='statistics:namespace_details', args='resolved_object.id' + view='statistics:namespace_details', args='resolved_object.slug' ) link_namespace_list = Link( permissions=(permission_statistics_view,), text=_('Namespace list'), diff --git a/mayan/apps/statistics/literals.py b/mayan/apps/statistics/literals.py deleted file mode 100644 index 85ab08bbef..0000000000 --- a/mayan/apps/statistics/literals.py +++ /dev/null @@ -1,3 +0,0 @@ -from __future__ import unicode_literals - -STATISTICS_REFRESH_INTERVAL = 60 * 60 * 24 # Every 12 hours diff --git a/mayan/apps/statistics/management/__init__.py b/mayan/apps/statistics/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mayan/apps/statistics/management/commands/__init__.py b/mayan/apps/statistics/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mayan/apps/statistics/management/commands/purgestatistics.py b/mayan/apps/statistics/management/commands/purgestatistics.py new file mode 100644 index 0000000000..f3dd67ba13 --- /dev/null +++ b/mayan/apps/statistics/management/commands/purgestatistics.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals + +from django.core.management.base import BaseCommand + +from ...classes import Statistic + + +class Command(BaseCommand): + help = 'Remove obsolete statistics scheduled and results from the database' + + def handle(self, *args, **options): + Statistic.purge_schedules() diff --git a/mayan/apps/statistics/tasks.py b/mayan/apps/statistics/tasks.py index 7013bcf45a..21af51caf2 100644 --- a/mayan/apps/statistics/tasks.py +++ b/mayan/apps/statistics/tasks.py @@ -4,17 +4,15 @@ import logging from mayan.celery import app -from .classes import StatisticNamespace +from .classes import Statistic logger = logging.getLogger(__name__) @app.task(ignore_result=True) -def task_check_statistics(): +def task_execute_statistic(slug): logger.info('Executing') - for namespace in StatisticNamespace.get_all(): - for statistic in namespace.statistics: - statistic.execute() + Statistic.get(slug=slug).execute() logger.info('Finshed') diff --git a/mayan/apps/statistics/urls.py b/mayan/apps/statistics/urls.py index 9c8355792f..3f568d6df4 100644 --- a/mayan/apps/statistics/urls.py +++ b/mayan/apps/statistics/urls.py @@ -2,17 +2,24 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url -from .views import NamespaceDetailView, NamespaceListView, StatisticDetailView +from .views import ( + NamespaceDetailView, NamespaceListView, StatisticDetailView, + StatisticQueueView +) urlpatterns = patterns( 'statistics.views', url(r'^$', NamespaceListView.as_view(), name='namespace_list'), url( - r'^namespace/(?P\w+)/details/$', + r'^namespace/(?P[\w-]+)/details/$', NamespaceDetailView.as_view(), name='namespace_details' ), url( r'^(?P[\w-]+)/view/$', StatisticDetailView.as_view(), name='statistic_detail' ), + url( + r'^(?P[\w-]+)/queue/$', StatisticQueueView.as_view(), + name='statistic_queue' + ), ) diff --git a/mayan/apps/statistics/views.py b/mayan/apps/statistics/views.py index f109b11d7d..223308d6b5 100644 --- a/mayan/apps/statistics/views.py +++ b/mayan/apps/statistics/views.py @@ -2,13 +2,18 @@ from __future__ import unicode_literals import json +from django.core.urlresolvers import reverse from django.http import Http404 +from django.shortcuts import HttpResponseRedirect from django.utils.translation import ugettext_lazy as _ -from common.views import SingleObjectDetailView, SingleObjectListView +from common.generics import ( + ConfirmView, SingleObjectDetailView, SingleObjectListView +) from .classes import Statistic, StatisticNamespace from .permissions import permission_statistics_view +from .tasks import task_execute_statistic class NamespaceListView(SingleObjectListView): @@ -34,7 +39,7 @@ class NamespaceDetailView(SingleObjectListView): } def get_namespace(self): - return StatisticNamespace.get(self.kwargs['namespace_id']) + return StatisticNamespace.get(self.kwargs['slug']) def get_queryset(self): return self.get_namespace().statistics @@ -61,3 +66,30 @@ class StatisticDetailView(SingleObjectDetailView): def get_template_names(self): return (self.get_object().renderer.template_name,) + + +class StatisticQueueView(ConfirmView): + view_permission = permission_statistics_view + + def get_extra_context(self): + return { + 'namespace': self.get_object().namespace, + 'object': self.get_object(), + 'title': _('Queue statistic "%s" to be updated?') % self.get_object(), + } + + def get_object(self): + try: + return Statistic.get(self.kwargs['slug']) + except KeyError: + raise Http404(_('Statistic "%s" not found.') % self.kwargs['slug']) + + def get_post_action_redirect(self): + return reverse( + 'statistics:namespace_details', + args=(self.get_object().namespace.slug,) + ) + + def post(self, request, *args, **kwargs): + task_execute_statistic.delay(slug=self.get_object().slug) + return HttpResponseRedirect(self.get_post_action_redirect()) From a598aef66294ce3c4808501db58ac3a24638301c Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 14 Sep 2015 23:42:04 -0400 Subject: [PATCH 7/9] Add tag get_absolute_url method. --- mayan/apps/tags/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mayan/apps/tags/models.py b/mayan/apps/tags/models.py index 1a872db9af..163ce9171b 100644 --- a/mayan/apps/tags/models.py +++ b/mayan/apps/tags/models.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, unicode_literals +from django.core.urlresolvers import reverse from django.db import models from django.core.exceptions import PermissionDenied from django.utils.encoding import python_2_unicode_compatible @@ -30,6 +31,9 @@ class Tag(models.Model): def __str__(self): return self.label + def get_absolute_url(self): + return reverse('tags:tag_tagged_item_list', args=(str(self.pk),)) + def get_document_count(self, user): queryset = self.documents From d6a3111d6954cf881e1b707e4ed6e98e5074ab6b Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 15 Sep 2015 02:01:56 -0400 Subject: [PATCH 8/9] Make sure all images are converted to RGB before processing. --- mayan/apps/converter/backends/python.py | 2 +- mayan/apps/converter/classes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mayan/apps/converter/backends/python.py b/mayan/apps/converter/backends/python.py index 0b32d82827..e57c653394 100644 --- a/mayan/apps/converter/backends/python.py +++ b/mayan/apps/converter/backends/python.py @@ -93,7 +93,7 @@ class Python(ConverterBase): file_object.seek(0) else: try: - image = Image.open(self.file_object) + image = Image.open(self.file_object).convert('RGB') except IOError as exception: error_message = _('Exception determining PDF page count; %s') % exception logger.error(error_message) diff --git a/mayan/apps/converter/classes.py b/mayan/apps/converter/classes.py index 6d282d910e..41b8cb8653 100644 --- a/mayan/apps/converter/classes.py +++ b/mayan/apps/converter/classes.py @@ -163,7 +163,7 @@ class ConverterBase(object): self.file_object.seek(0) try: - self.image = Image.open(self.file_object) + self.image = Image.open(self.file_object).convert('RGB') except IOError: # Cannot identify image file self.image = self.convert(page_number=page_number) From e1a04d98380b59569bbdc726c67d666bbd796dd0 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 15 Sep 2015 02:02:38 -0400 Subject: [PATCH 9/9] Convert document trash view to CBV. Improve ConfirmView based CBVs. --- mayan/apps/checkouts/views.py | 27 ++--- mayan/apps/common/generics.py | 4 + mayan/apps/common/mixins.py | 23 +++- mayan/apps/documents/urls.py | 15 +-- mayan/apps/documents/views.py | 188 +++++++++++++++------------------ mayan/apps/ocr/urls.py | 4 +- mayan/apps/ocr/views.py | 97 ++++++++--------- mayan/apps/statistics/views.py | 3 +- 8 files changed, 184 insertions(+), 177 deletions(-) diff --git a/mayan/apps/checkouts/views.py b/mayan/apps/checkouts/views.py index ff4e91b3e4..6f46046568 100644 --- a/mayan/apps/checkouts/views.py +++ b/mayan/apps/checkouts/views.py @@ -77,9 +77,6 @@ class CheckoutDocumentView(SingleObjectCreateView): class CheckoutListView(DocumentListView): - def get_document_queryset(self): - return DocumentCheckout.objects.checked_out_documents() - extra_context = { 'title': _('Documents checked out'), 'hide_links': True, @@ -105,6 +102,9 @@ class CheckoutListView(DocumentListView): ), } + def get_document_queryset(self): + return DocumentCheckout.objects.checked_out_documents() + def checkout_info(request, document_pk): document = get_object_or_404(Document, pk=document_pk) @@ -156,9 +156,6 @@ def checkout_info(request, document_pk): class DocumentCheckinView(ConfirmView): - def get_object(self): - return get_object_or_404(Document, pk=self.kwargs['pk']) - def get_extra_context(self): document = self.get_object() @@ -176,8 +173,14 @@ class DocumentCheckinView(ConfirmView): return context - def object_action(self, request, obj): - document = obj + def get_object(self): + return get_object_or_404(Document, pk=self.kwargs['pk']) + + def get_post_action_redirect(self): + return reverse('checkouts:checkout_info', args=(self.get_object().pk,)) + + def view_action(self, request): + document = self.get_object() if document.checkout_info().user == request.user: try: @@ -210,11 +213,3 @@ class DocumentCheckinView(ConfirmView): messages.success( request, _('Document "%s" checked in successfully.') % document ) - - def get_post_action_redirect(self): - return reverse('checkouts:checkout_info', args=(self.get_object().pk,)) - - def post(self, request, *args, **kwargs): - self.object_action(request=request, obj=self.get_object()) - - return HttpResponseRedirect(self.get_success_url()) diff --git a/mayan/apps/common/generics.py b/mayan/apps/common/generics.py index 5564c34b52..b6b1b545eb 100644 --- a/mayan/apps/common/generics.py +++ b/mayan/apps/common/generics.py @@ -169,6 +169,10 @@ class AssignRemoveView(ExtraContextMixin, ViewPermissionCheckMixin, ObjectPermis class ConfirmView(ObjectListPermissionFilterMixin, ViewPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, TemplateView): template_name = 'appearance/generic_confirm.html' + def post(self, request, *args, **kwargs): + self.view_action() + return HttpResponseRedirect(self.get_success_url()) + class MultiFormView(FormView): prefixes = {} diff --git a/mayan/apps/common/mixins.py b/mayan/apps/common/mixins.py index cb02ade704..f70e0a52c4 100644 --- a/mayan/apps/common/mixins.py +++ b/mayan/apps/common/mixins.py @@ -1,10 +1,12 @@ from __future__ import unicode_literals from django.conf import settings +from django.contrib import messages from django.core.exceptions import PermissionDenied from django.core.urlresolvers import reverse from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 +from django.utils.translation import ungettext from acls.models import AccessControlList from permissions import Permission @@ -42,14 +44,33 @@ class ExtraContextMixin(object): class MultipleInstanceActionMixin(object): model = None + success_message = 'Operation performed on %(count)d object' + success_message_plural = 'Operation performed on %(count)d objects' + + def get_success_message(self, count): + return ungettext( + self.success_message, + self.success_message_plural, + count + ) % { + 'count': count, + } def post(self, request, *args, **kwargs): + count = 0 for pk in request.GET.get('id_list', '').split(','): document = get_object_or_404(self.model, pk=pk) try: - self.object_action(request=request, instance=document) + self.object_action(instance=document) except PermissionDenied: pass + else: + count += 1 + + messages.success( + self.request, + self.get_success_message(count=count) + ) return HttpResponseRedirect(self.get_success_url()) diff --git a/mayan/apps/documents/urls.py b/mayan/apps/documents/urls.py index 0ec00637ba..af9d4af2ca 100644 --- a/mayan/apps/documents/urls.py +++ b/mayan/apps/documents/urls.py @@ -13,10 +13,11 @@ from .api_views import ( ) from .settings import setting_print_size, setting_display_size from .views import ( - DeletedDocumentDeleteView, DeletedDocumentListView, DocumentEditView, - DocumentListView, DocumentManyDeleteView, DocumentManyRestoreView, + DeletedDocumentDeleteView, DeletedDocumentDeleteManyView, + DeletedDocumentListView, DocumentEditView, DocumentListView, DocumentPageView, DocumentPageListView, DocumentPreviewView, - DocumentRestoreView, DocumentTypeCreateView, DocumentTypeDeleteView, + DocumentRestoreView, DocumentRestoreManyView, DocumentTrashView, + DocumentTrashManyView, DocumentTypeCreateView, DocumentTypeDeleteView, DocumentTypeDocumentListView, DocumentTypeFilenameDeleteView, DocumentTypeFilenameEditView, DocumentTypeFilenameListView, DocumentTypeListView, DocumentTypeEditView, DocumentVersionListView, @@ -48,7 +49,7 @@ urlpatterns = patterns( name='document_restore' ), url( - r'^multiple/restore/$', DocumentManyRestoreView.as_view(), + r'^multiple/restore/$', DocumentRestoreManyView.as_view(), name='document_multiple_restore' ), url( @@ -56,7 +57,7 @@ urlpatterns = patterns( name='document_delete' ), url( - r'^multiple/delete/$', DocumentManyDeleteView.as_view(), + r'^multiple/delete/$', DeletedDocumentDeleteManyView.as_view(), name='document_multiple_delete' ), url( @@ -68,11 +69,11 @@ urlpatterns = patterns( name='document_multiple_document_type_edit' ), url( - r'^(?P\d+)/trash/$', 'document_trash', + r'^(?P\d+)/trash/$', DocumentTrashView.as_view(), name='document_trash' ), url( - r'^multiple/trash/$', 'document_multiple_trash', + r'^multiple/trash/$', DocumentTrashManyView.as_view(), name='document_multiple_trash' ), url( diff --git a/mayan/apps/documents/views.py b/mayan/apps/documents/views.py index 09db919f43..20a7fd1a68 100644 --- a/mayan/apps/documents/views.py +++ b/mayan/apps/documents/views.py @@ -112,30 +112,39 @@ class DeletedDocumentDeleteView(ConfirmView): 'title': _('Delete the selected document?') } - def object_action(self, request, instance): + def object_action(self, instance): source_document = get_object_or_404( Document.passthrough, pk=instance.pk ) try: Permission.check_permissions( - request.user, (permission_document_delete,) + self.request.user, (permission_document_delete,) ) except PermissionDenied: AccessControlList.objects.check_access( - permission_document_delete, request.user, source_document + permission_document_delete, self.request.user, source_document ) instance.delete() - messages.success(request, _('Document: %(document)s deleted.') % { - 'document': instance} + + def view_action(self): + instance = get_object_or_404(DeletedDocument, pk=self.kwargs['pk']) + self.object_action(instance=instance) + messages.success( + self.request, _('Document: %(document)s deleted.') % { + 'document': instance + } ) - def post(self, request, *args, **kwargs): - document = get_object_or_404(DeletedDocument, pk=self.kwargs['pk']) - self.object_action(request=request, instance=document) - return HttpResponseRedirect(self.get_success_url()) +class DeletedDocumentDeleteManyView(MultipleInstanceActionMixin, DeletedDocumentDeleteView): + extra_context = { + 'title': _('Delete the selected documents?') + } + model = DeletedDocument + success_message = '%(count)d document deleted.' + success_message_plural = '%(count)d documents deleted.' class DocumentEditView(SingleObjectEditView): @@ -172,46 +181,41 @@ class DocumentRestoreView(ConfirmView): 'title': _('Restore the selected document?') } - def object_action(self, request, instance): + def object_action(self, instance): source_document = get_object_or_404( Document.passthrough, pk=instance.pk ) try: Permission.check_permissions( - request.user, (permission_document_restore,) + self.request.user, (permission_document_restore,) ) except PermissionDenied: AccessControlList.objects.check_access( - permission_document_restore, request.user, source_document + permission_document_restore, self.request.user, source_document ) instance.restore() + + def view_action(self): + instance = get_object_or_404(DeletedDocument, pk=self.kwargs['pk']) + + self.object_action(instance=instance) + messages.success( - request, _('Document: %(document)s restored.') % { + self.request, _('Document: %(document)s restored.') % { 'document': instance } ) - def post(self, request, *args, **kwargs): - document = get_object_or_404(DeletedDocument, pk=self.kwargs['pk']) - self.object_action(request=request, instance=document) - return HttpResponseRedirect(self.get_success_url()) - - -class DocumentManyDeleteView(MultipleInstanceActionMixin, DeletedDocumentDeleteView): - extra_context = { - 'title': _('Delete the selected documents?') - } - model = DeletedDocument - - -class DocumentManyRestoreView(MultipleInstanceActionMixin, DocumentRestoreView): +class DocumentRestoreManyView(MultipleInstanceActionMixin, DocumentRestoreView): extra_context = { 'title': _('Restore the selected documents?') } model = DeletedDocument + success_message = '%(count)d document restored.' + success_message_plural = '%(count)d documents restored.' class DocumentPageListView(SingleObjectListView): @@ -309,6 +313,54 @@ class DocumentPreviewView(SingleObjectDetailView): } +class DocumentTrashView(ConfirmView): + def get_extra_context(self): + return { + 'object': self.get_object(), + 'title': _('Move "%s" to the trash?') % self.get_object() + } + + def get_object(self): + return get_object_or_404(Document, pk=self.kwargs['pk']) + + def get_post_action_redirect(self): + return reverse('documents:document_list_recent') + + def object_action(self, instance): + try: + Permission.check_permissions( + self.request.user, (permission_document_trash,) + ) + except PermissionDenied: + AccessControlList.objects.check_access( + permission_document_trash, self.request.user, instance + ) + + instance.delete() + + def view_action(self): + instance = self.get_object() + + self.object_action(instance=instance) + + messages.success( + self.request, _('Document: %(document)s moved to trash successfully.') % { + 'document': instance + } + ) + + +class DocumentTrashManyView(MultipleInstanceActionMixin, DocumentTrashView): + model = Document + success_message = '%(count)d document moved to the trash.' + success_message_plural = '%(count)d documents moved to the trash.' + + def get_extra_context(self): + return { + 'title': _('Move the selected documents to the trash?') + } + + class DocumentTypeDocumentListView(DocumentListView): def get_document_type(self): return get_object_or_404(DocumentType, pk=self.kwargs['pk']) @@ -405,12 +457,6 @@ class DocumentTypeFilenameEditView(SingleObjectEditView): model = DocumentTypeFilename view_permission = permission_document_type_edit - def get_post_action_redirect(self): - return reverse( - 'documents:document_type_filename_list', - args=(self.get_object().document_type.pk,) - ) - def get_extra_context(self): document_type_filename = self.get_object() @@ -427,6 +473,12 @@ class DocumentTypeFilenameEditView(SingleObjectEditView): }, } + def get_post_action_redirect(self): + return reverse( + 'documents:document_type_filename_list', + args=(self.get_object().document_type.pk,) + ) + class DocumentTypeFilenameDeleteView(SingleObjectDeleteView): model = DocumentTypeFilename @@ -553,13 +605,11 @@ class EmptyTrashCanView(ConfirmView): 'documents:document_list_deleted' ) - def post(self, request, *args, **kwargs): + def view_action(self): for deleted_document in DeletedDocument.objects.all(): deleted_document.delete() - messages.success(request, _('Trash emptied successfully')) - - return HttpResponseRedirect(self.get_success_url()) + messages.success(self.request, _('Trash emptied successfully')) class RecentDocumentListView(DocumentListView): @@ -572,70 +622,6 @@ class RecentDocumentListView(DocumentListView): return RecentDocument.objects.get_for_user(self.request.user) -def document_trash(request, document_id=None, document_id_list=None): - post_action_redirect = None - - if document_id: - documents = [get_object_or_404(Document, pk=document_id)] - post_action_redirect = reverse('documents:document_list_recent') - elif document_id_list: - documents = [ - get_object_or_404(Document, pk=document_id) for document_id in document_id_list.split(',') - ] - else: - messages.error(request, _('Must provide at least one document.')) - return HttpResponseRedirect( - request.META.get( - 'HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL) - ) - ) - - try: - Permission.check_permissions(request.user, (permission_document_trash,)) - except PermissionDenied: - documents = AccessControlList.objects.filter_by_access( - permission_document_trash, request.user, documents - ) - - previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL)))) - next = request.POST.get('next', request.GET.get('next', post_action_redirect if post_action_redirect else request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL)))) - - if request.method == 'POST': - for document in documents: - try: - document.delete() - messages.success(request, _('Document moved to trash successfully.')) - except Exception as exception: - messages.error(request, _('Document: %(document)s error moving to trash: %(error)s') % { - 'document': document, 'error': exception - }) - - return HttpResponseRedirect(next) - - context = { - 'delete_view': True, - 'previous': previous, - 'next': next, - 'title': ungettext( - 'Move the selected document to the trash ?', - 'Move the selected documents to the trash ?', - len(documents) - ) - } - - if len(documents) == 1: - context['object'] = documents[0] - - return render_to_response('appearance/generic_confirm.html', context, - context_instance=RequestContext(request)) - - -def document_multiple_trash(request): - return document_trash( - request, document_id_list=request.GET.get('id_list', []) - ) - - def document_document_type_edit(request, document_id=None, document_id_list=None): post_action_redirect = None diff --git a/mayan/apps/ocr/urls.py b/mayan/apps/ocr/urls.py index 55a0fd3711..760300307d 100644 --- a/mayan/apps/ocr/urls.py +++ b/mayan/apps/ocr/urls.py @@ -4,7 +4,7 @@ from django.conf.urls import patterns, url from .api_views import DocumentVersionOCRView from .views import ( - DocumentSubmitView, DocumentAllSubmitView, DocumentManySubmitView, + DocumentAllSubmitView, DocumentSubmitView, DocumentSubmitManyView, DocumentTypeSettingsEditView, EntryListView ) @@ -23,7 +23,7 @@ urlpatterns = patterns( name='document_submit_all' ), url( - r'^document/multiple/submit/$', DocumentManySubmitView.as_view(), + r'^document/multiple/submit/$', DocumentSubmitManyView.as_view(), name='document_submit_multiple' ), url( diff --git a/mayan/apps/ocr/views.py b/mayan/apps/ocr/views.py index 2337581c93..54f45f1e09 100644 --- a/mayan/apps/ocr/views.py +++ b/mayan/apps/ocr/views.py @@ -13,6 +13,7 @@ from acls.models import AccessControlList from common.generics import ( ConfirmView, SingleObjectEditView, SingleObjectListView ) +from common.mixins import MultipleInstanceActionMixin from documents.models import Document, DocumentType, DocumentVersion from permissions import Permission @@ -24,68 +25,68 @@ from .permissions import ( ) -class DocumentSubmitView(ConfirmView): - def get_extra_context(self): - return { - 'object': Document.objects.get(pk=self.kwargs['pk']), - 'title': _('Submit the selected document for OCR?') - } - - def object_action(self, request, obj): - document = obj - - try: - Permission.check_permissions( - request.user, (permission_ocr_document,) - ) - except PermissionDenied: - AccessControlList.objects.check_access( - permission_ocr_document, request.user, document - ) - - document.submit_for_ocr() - messages.success( - request, - _('Document: %(document)s was added to the OCR queue.') % { - 'document': document - } - ) - - def post(self, request, *args, **kwargs): - document = get_object_or_404(Document, pk=self.kwargs['pk']) - self.object_action(request=request, obj=document) - - return HttpResponseRedirect(self.get_success_url()) - - class DocumentAllSubmitView(ConfirmView): extra_context = {'title': _('Submit all documents for OCR?')} - def post(self, request, *args, **kwargs): + def get_post_action_redirect(self): + return reverse('common:tools_list') + + def view_action(self): count = 0 for document in Document.objects.all(): document.submit_for_ocr() count += 1 messages.success( - request, _('%d documents added to the OCR queue.') % count + self.request, _('%d documents added to the OCR queue.') % count ) - return HttpResponseRedirect(self.get_success_url()) + +class DocumentSubmitView(ConfirmView): + def get_extra_context(self): + return { + 'object': self.get_object(), + 'title': _('Submit "%s" to the OCR queue?') % self.get_object() + } + + def get_object(self): + return Document.objects.get(pk=self.kwargs['pk']) + + def object_action(self, instance): + try: + Permission.check_permissions( + self.request.user, (permission_ocr_document,) + ) + except PermissionDenied: + AccessControlList.objects.check_access( + permission_ocr_document, self.request.user, instance + ) + + instance.submit_for_ocr() + + def view_action(self): + instance = self.get_object() + + self.object_action(instance=instance) + + messages.success( + self.request, + _('Document: %(document)s was added to the OCR queue.') % { + 'document': instance + } + ) -class DocumentManySubmitView(DocumentSubmitView): - extra_context = {'title': _('Submit the selected documents for OCR?')} +class DocumentSubmitManyView(MultipleInstanceActionMixin, DocumentSubmitView): + model = Document + success_message = '%(count)d document submitted to the OCR queue.' + success_message_plural = '%(count)d documents submitted to the OCR queue.' - def post(self, request, *args, **kwargs): - for pk in request.GET.get('id_list', '').split(','): - document = get_object_or_404(Document, pk=pk) - try: - self.object_action(request=request, obj=document) - except PermissionDenied: - pass - - return HttpResponseRedirect(self.get_success_url()) + def get_extra_context(self): + # Override the base class method + return { + 'title': _('Submit the selected documents to the OCR queue?') + } class DocumentTypeSettingsEditView(SingleObjectEditView): diff --git a/mayan/apps/statistics/views.py b/mayan/apps/statistics/views.py index 223308d6b5..845dba5a32 100644 --- a/mayan/apps/statistics/views.py +++ b/mayan/apps/statistics/views.py @@ -90,6 +90,5 @@ class StatisticQueueView(ConfirmView): args=(self.get_object().namespace.slug,) ) - def post(self, request, *args, **kwargs): + def view_action(self): task_execute_statistic.delay(slug=self.get_object().slug) - return HttpResponseRedirect(self.get_post_action_redirect())