diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
index 306e5fd5905..89a9a4694b6 100644
--- a/addons/mail/mail_thread.py
+++ b/addons/mail/mail_thread.py
@@ -772,6 +772,7 @@ class mail_thread(osv.AbstractModel):
"""
assert isinstance(message, Message), 'message must be an email.message.Message at this point'
fallback_model = model
+ bounce_alias = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.bounce.alias", context=context)
# Get email.message.Message variables for future processing
message_id = message.get('Message-Id')
@@ -780,6 +781,24 @@ class mail_thread(osv.AbstractModel):
references = decode_header(message, 'References')
in_reply_to = decode_header(message, 'In-Reply-To')
+ # 0. Verify whether this is a bounced email (wrong destination,...) -> use it to collect data, such as dead leads
+ if bounce_alias in email_to:
+ bounce_match = tools.bounce_re.search(email_to)
+ if bounce_match:
+ bounced_mail_id = bounce_match.group(1)
+ if self.pool['mail.mail'].exists(cr, uid, bounced_mail_id):
+ mail = self.pool['mail.mail'].browse(cr, uid, bounced_mail_id, context=context)
+ bounced_model = mail.model
+ bounced_thread_id = mail.res_id
+ else:
+ bounced_model = bounce_match.group(2)
+ bounced_thread_id = int(bounce_match.group(3)) if bounce_match.group(3) else 0
+ _logger.info('Routing mail from %s to %s with Message-Id %s: bounced mail from mail %s, model: %s, thread_id: %s',
+ email_from, email_to, message_id, bounced_mail_id, bounced_model, bounced_thread_id)
+ if bounced_model and bounced_model in self.pool and hasattr(self.pool[bounced_model], 'message_receive_bounce'):
+ self.pool[bounced_model].message_receive_bounce(cr, uid, [bounced_thread_id], mail_id=bounced_mail_id, context=context)
+ return []
+
# 1. Verify if this is a reply to an existing thread
thread_references = references or in_reply_to
ref_match = thread_references and tools.reference_re.search(thread_references)
@@ -1018,6 +1037,15 @@ class mail_thread(osv.AbstractModel):
self.write(cr, uid, ids, update_vals, context=context)
return True
+ def message_receive_bounce(self, cr, uid, ids, mail_id=None, context=None):
+ """Called by ``message_process`` when a bounce email (such as Undelivered
+ Mail Returned to Sender) is received for an existing thread. The default
+ behavior is to check is an integer ``message_bounce`` column exists.
+ If it is the case, its content is incremented. """
+ if self._all_columns.get('message_bounce'):
+ for obj in self.browse(cr, uid, ids, context=context):
+ self.write(cr, uid, [obj.id], {'message_bounce': obj.message_bounce + 1}, context=context)
+
def _message_extract_payload(self, message, save_original=False):
"""Extract body as HTML and attachments from the mail message"""
attachments = []
--
cgit v1.2.3
From 82999df03c4eff9107fa620bce45042c95a4b907 Mon Sep 17 00:00:00 2001
From: Xavier Morel
Date: Wed, 7 Aug 2013 13:21:59 +0200
Subject: [IMP] recent qunit will send moduleStart with no name if error
happens outside of a testing module
bzr revid: xmo@openerp.com-20130807112159-91jdzgl6g2fk966t
---
addons/web/tests/qunitsuite/suite.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/web/tests/qunitsuite/suite.py b/addons/web/tests/qunitsuite/suite.py
index 7f0a0cb4976..3aed7b133f1 100644
--- a/addons/web/tests/qunitsuite/suite.py
+++ b/addons/web/tests/qunitsuite/suite.py
@@ -91,7 +91,7 @@ class QUnitSuite(unittest.TestSuite):
return True
elif event_name == 'qunit.moduleStart':
- self._module = args[1].encode('utf-8')
+ self._module = args[1].encode('utf-8') if args[1] else ''
elif event_name == 'qunit.moduleStop':
self._test = None
self._module = None
--
cgit v1.2.3
From fd100054ccdb4cedcb2411bdf38a7388338ea814 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Wed, 7 Aug 2013 15:01:18 +0200
Subject: [FIX] mail_mail: fixed reply_to computation (before create was
leading to a wrong message_id to the embedded mail_message); mail_thread;
fixed private discussion not going through the route checking; improved
replying through mailgateway now incrementing the replied field of mail_mail.
bzr revid: tde@openerp.com-20130807130118-yggvmontssofxt0q
---
addons/mail/mail_mail.py | 11 +++++++++--
addons/mail/mail_thread.py | 18 ++++++++++++------
2 files changed, 21 insertions(+), 8 deletions(-)
diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py
index 5548608aeeb..130e63deefd 100644
--- a/addons/mail/mail_mail.py
+++ b/addons/mail/mail_mail.py
@@ -140,10 +140,17 @@ class mail_mail(osv.Model):
# notification field: if not set, set if mail comes from an existing mail.message
if 'notification' not in values and values.get('mail_message_id'):
values['notification'] = True
+ mail_id = super(mail_mail, self).create(cr, uid, values, context=context)
+
# reply_to: if not set, set with default values that require creation values
+ # but delegate after creation because of mail_message.message_id automatic
+ # creation using existence of reply_to
if not values.get('reply_to'):
- values['reply_to'] = self._get_reply_to(cr, uid, values, context=context)
- return super(mail_mail, self).create(cr, uid, values, context=context)
+ reply_to = self._get_reply_to(cr, uid, values, context=context)
+ if reply_to:
+ self.write(cr, uid, [mail_id], {'reply_to': reply_to}, context=context)
+
+ return mail_id
def unlink(self, cr, uid, ids, context=None):
# cascade-delete the parent message for all mails that are not created for a notification
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
index 89a9a4694b6..f03f1239bd8 100644
--- a/addons/mail/mail_thread.py
+++ b/addons/mail/mail_thread.py
@@ -706,7 +706,7 @@ class mail_thread(osv.AbstractModel):
return ()
# New Document: check model accepts the mailgateway
- if not thread_id and not hasattr(model_pool, 'message_new'):
+ if not thread_id and model and not hasattr(model_pool, 'message_new'):
if assert_model:
assert hasattr(model_pool, 'message_new'), 'Model %s does not accept document creation, crashing' % model
_warn('model %s does not accept document creation, skipping' % model)
@@ -817,16 +817,16 @@ class mail_thread(osv.AbstractModel):
# 2. Reply to a private message
if in_reply_to:
- message_ids = self.pool.get('mail.message').search(cr, uid, [
+ mail_message_ids = self.pool.get('mail.message').search(cr, uid, [
('message_id', '=', in_reply_to),
'!', ('message_id', 'ilike', 'reply_to')
], limit=1, context=context)
- if message_ids:
- message = self.pool.get('mail.message').browse(cr, uid, message_ids[0], context=context)
+ if mail_message_ids:
+ mail_message = self.pool.get('mail.message').browse(cr, uid, mail_message_ids[0], context=context)
_logger.info('Routing mail from %s to %s with Message-Id %s: direct reply to a private message: %s, custom_values: %s, uid: %s',
- email_from, email_to, message_id, message.id, custom_values, uid)
+ email_from, email_to, message_id, mail_message.id, custom_values, uid)
route = self.message_route_verify(cr, uid, message, message_dict,
- (message.model, message.res_id, custom_values, uid, None),
+ (mail_message.model, mail_message.res_id, custom_values, uid, None),
update_author=True, assert_model=True, create_fallback=True, context=context)
return route and [route] or []
@@ -1415,6 +1415,12 @@ class mail_thread(osv.AbstractModel):
parent_id = message_ids and message_ids[0] or False
# we want to set a parent: force to set the parent_id to the oldest ancestor, to avoid having more than 1 level of thread
elif parent_id:
+ # update original mail_mail if exists
+ if type == 'email':
+ mail_mail_ids = self.pool['mail.mail'].search(cr, SUPERUSER_ID, [('mail_message_id', '=', parent_id)], context=context)
+ for mail in self.pool['mail.mail'].browse(cr, SUPERUSER_ID, mail_mail_ids, context=context):
+ self.pool['mail.mail'].write(cr, SUPERUSER_ID, [mail.id], {'replied': mail.replied + 1}, context=context)
+
message_ids = mail_message.search(cr, SUPERUSER_ID, [('id', '=', parent_id), ('parent_id', '!=', False)], context=context)
# avoid loops when finding ancestors
processed_list = []
--
cgit v1.2.3
From 33fd9e01d6f50d7d6a3e379fd8544bb069a5d21d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Wed, 7 Aug 2013 15:03:34 +0200
Subject: [ADD] mass_mailing: added module for mass mailign campaigns. First
draft of new module : simple mass mailing campaign model, linked to emails,
compute some statistics mail.compose.message updated to the mass mailing
campaigns in mass mail mode
bzr revid: tde@openerp.com-20130807130334-nwd34fgsz4lc6lt1
---
addons/mass_mailing/__init__.py | 24 +++++++
addons/mass_mailing/__openerp__.py | 39 ++++++++++
addons/mass_mailing/mail_mail.py | 35 +++++++++
addons/mass_mailing/mail_mail_view.xml | 18 +++++
addons/mass_mailing/mass_mailing.py | 84 ++++++++++++++++++++++
addons/mass_mailing/mass_mailing_view.xml | 48 +++++++++++++
addons/mass_mailing/security/ir.model.access.csv | 3 +
addons/mass_mailing/wizard/__init__.py | 22 ++++++
addons/mass_mailing/wizard/mail_compose_message.py | 51 +++++++++++++
.../wizard/mail_compose_message_view.xml | 20 ++++++
10 files changed, 344 insertions(+)
create mode 100644 addons/mass_mailing/__init__.py
create mode 100644 addons/mass_mailing/__openerp__.py
create mode 100644 addons/mass_mailing/mail_mail.py
create mode 100644 addons/mass_mailing/mail_mail_view.xml
create mode 100644 addons/mass_mailing/mass_mailing.py
create mode 100644 addons/mass_mailing/mass_mailing_view.xml
create mode 100644 addons/mass_mailing/security/ir.model.access.csv
create mode 100644 addons/mass_mailing/wizard/__init__.py
create mode 100644 addons/mass_mailing/wizard/mail_compose_message.py
create mode 100644 addons/mass_mailing/wizard/mail_compose_message_view.xml
diff --git a/addons/mass_mailing/__init__.py b/addons/mass_mailing/__init__.py
new file mode 100644
index 00000000000..37f3850306a
--- /dev/null
+++ b/addons/mass_mailing/__init__.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013-today OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+import mass_mailing
+import mail_mail
+import wizard
diff --git a/addons/mass_mailing/__openerp__.py b/addons/mass_mailing/__openerp__.py
new file mode 100644
index 00000000000..e6fbb42e886
--- /dev/null
+++ b/addons/mass_mailing/__openerp__.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013-today OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+{
+ 'name': 'Mass Mailing Campaigns',
+ 'version': '1.0',
+ 'author': 'OpenERP',
+ 'website': 'http://www.openerp.com',
+ 'category': 'Marketing',
+ 'depends': ['mail', 'email_template'],
+ 'description': """TODO""",
+ 'data': [
+ 'mass_mailing_view.xml',
+ 'mail_mail_view.xml',
+ 'wizard/mail_compose_message_view.xml',
+ 'security/ir.model.access.csv',
+ ],
+ 'demo': [],
+ 'installable': True,
+ 'auto_install': False,
+}
diff --git a/addons/mass_mailing/mail_mail.py b/addons/mass_mailing/mail_mail.py
new file mode 100644
index 00000000000..d7f46446b56
--- /dev/null
+++ b/addons/mass_mailing/mail_mail.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013-today OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+from openerp.osv import osv, fields
+
+
+class MailMail(osv.Model):
+ """Add the mass mailing campaign data to mail"""
+ _name = 'mail.mail'
+ _inherit = ['mail.mail']
+
+ _columns = {
+ 'mass_mailing_campaign_id': fields.many2one(
+ 'mail.mass_mailing.campaign', 'Mass Mailing Campaign',
+ ondelete='set null',
+ ),
+ }
diff --git a/addons/mass_mailing/mail_mail_view.xml b/addons/mass_mailing/mail_mail_view.xml
new file mode 100644
index 00000000000..dd66aede66f
--- /dev/null
+++ b/addons/mass_mailing/mail_mail_view.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ mail.mail.form.mass_mailing
+ mail.mail
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/mass_mailing/mass_mailing.py b/addons/mass_mailing/mass_mailing.py
new file mode 100644
index 00000000000..91392759b0e
--- /dev/null
+++ b/addons/mass_mailing/mass_mailing.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013-today OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+from openerp.osv import osv, fields
+
+
+class MassMailingCampaign(osv.Model):
+ """Model of mass mailing campaigns.
+ """
+ _name = "mail.mass_mailing.campaign"
+ _description = 'Mass Mailing Campaign'
+
+ def _get_statistics(self, cr, uid, ids, name, arg, context=None):
+ """ Compute statistics of the mass mailing campaign """
+ results = dict.fromkeys(ids, False)
+ for campaign in self.browse(cr, uid, ids, context=context):
+ if not campaign.mail_ids:
+ results[campaign.id] = {
+ 'sent': 0,
+ 'opened_ratio': 0.0,
+ 'replied_ratio': 0.0,
+ 'bounce_ratio': 0.0,
+ }
+ continue
+ results[campaign.id] = {
+ 'sent': len(campaign.mail_ids),
+ 'opened_ratio': len([mail for mail in campaign.mail_ids if mail.opened]) * 1.0 / len(campaign.mail_ids),
+ 'replied_ratio': len([mail for mail in campaign.mail_ids if mail.replied]) * 1.0 / len(campaign.mail_ids),
+ 'bounce_ratio': 0.0,
+ }
+ return results
+
+ _columns = {
+ 'name': fields.char(
+ 'Campaign Name', required=True,
+ ),
+ 'template_id': fields.many2one(
+ 'email.template', 'Email Template',
+ ondelete='set null',
+ ),
+ 'mail_ids': fields.one2many(
+ 'mail.mail', 'mass_mailing_campaign_id',
+ 'Send Emails',
+ ),
+ # stat fields
+ 'sent': fields.function(
+ _get_statistics,
+ string='Sent Emails',
+ type='integer', multi='_get_statistics'
+ ),
+ 'opened_ratio': fields.function(
+ _get_statistics,
+ string='Opened Ratio',
+ type='float', multi='_get_statistics',
+ ),
+ 'replied_ratio': fields.function(
+ _get_statistics,
+ string='Replied Ratio',
+ type='float', multi='_get_statistics'
+ ),
+ 'bounce_ratio': fields.function(
+ _get_statistics,
+ string='Bounce Ratio',
+ type='float', multi='_get_statistics'
+ ),
+ }
diff --git a/addons/mass_mailing/mass_mailing_view.xml b/addons/mass_mailing/mass_mailing_view.xml
new file mode 100644
index 00000000000..bc28827343d
--- /dev/null
+++ b/addons/mass_mailing/mass_mailing_view.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+ mail.mass_mailing.campaign.tree
+ mail.mass_mailing.campaign
+ 10
+
+
+
+
+
+
+
+
+ mail.mass_mailing.campaign.form
+ mail.mass_mailing.campaign
+
+
+
+
+
+
+ Mass Mailing Campaigns
+ mail.mass_mailing.campaign
+ form
+ tree,form
+
+
+
+
+
+
+
diff --git a/addons/mass_mailing/security/ir.model.access.csv b/addons/mass_mailing/security/ir.model.access.csv
new file mode 100644
index 00000000000..af6eaa7bb06
--- /dev/null
+++ b/addons/mass_mailing/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_mass_mailing_campaign,mail.mass_mailing.campaign.template,model_mail_mass_mailing_campaign,,1,1,1,0
+access_mass_mailing_campaign_system,mail.mass_mailing.campaign.system,model_mail_mass_mailing_campaign,base.group_system,1,1,1,1
diff --git a/addons/mass_mailing/wizard/__init__.py b/addons/mass_mailing/wizard/__init__.py
new file mode 100644
index 00000000000..155849362cd
--- /dev/null
+++ b/addons/mass_mailing/wizard/__init__.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013-today OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+import mail_compose_message
diff --git a/addons/mass_mailing/wizard/mail_compose_message.py b/addons/mass_mailing/wizard/mail_compose_message.py
new file mode 100644
index 00000000000..2f74ef2c732
--- /dev/null
+++ b/addons/mass_mailing/wizard/mail_compose_message.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013-today OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+from openerp.osv import osv, fields
+
+
+class MailComposeMessage(osv.TransientModel):
+ """Add concept of mass mailing campaign to the mail.compose.message wizard
+ """
+ _inherit = 'mail.compose.message'
+
+ _columns = {
+ 'mass_mail_campaign_id': fields.many2one(
+ 'mail.mass_mailing.campaign', 'Mass mailing campaign'
+ ),
+ }
+
+ def onchange_mass_mail_campaign_id(self, cr, uid, ids, mass_mail_campaign_id, context=None):
+ values = {}
+ if mass_mail_campaign_id:
+ campaign = self.pool['mail.mass_mailing.campaign'].browse(cr, uid, mass_mail_campaign_id, context=context)
+ if campaign and campaign.template_id:
+ values['template_id'] = campaign.template_id.id
+ return {'value': values}
+
+ def render_message(self, cr, uid, wizard, res_id, context=None):
+ """ Override method that generated the mail content by adding the mass
+ mailing campaign, when doing pure email mass mailing. """
+ res = super(MailComposeMessage, self).render_message(cr, uid, wizard, res_id, context=context)
+ print res, wizard.mass_mail_campaign_id
+ if wizard.composition_mode == 'mass_mail' and wizard.mass_mail_campaign_id: # TODO: which kind of mass mailing ?
+ res['mass_mailing_campaign_id'] = wizard.mass_mail_campaign_id.id
+ return res
diff --git a/addons/mass_mailing/wizard/mail_compose_message_view.xml b/addons/mass_mailing/wizard/mail_compose_message_view.xml
new file mode 100644
index 00000000000..bc5f1a9eb6a
--- /dev/null
+++ b/addons/mass_mailing/wizard/mail_compose_message_view.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ mail.compose.message.form.mass_mailing
+ mail.compose.message
+
+
+
+
+
+
+
+
+
+
--
cgit v1.2.3
From 70d2a3859240498816682418ab173dd9af89c4ac Mon Sep 17 00:00:00 2001
From: "Mansi Kariya (OpenERP Trainee)"
Date: Mon, 12 Aug 2013 14:43:21 +0530
Subject: [IMP] Optimized code
bzr revid: mansi.mk.179@gmail.com-20130812091321-o6ql1csd9sq78vs7
---
addons/purchase/purchase_view.xml | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/addons/purchase/purchase_view.xml b/addons/purchase/purchase_view.xml
index 0463527461e..ab32eb0372f 100644
--- a/addons/purchase/purchase_view.xml
+++ b/addons/purchase/purchase_view.xml
@@ -33,14 +33,13 @@
-
+ action="product.product_pricelist_action_for_purchase" id="menu_product_pricelist_action2_purchase"
+ parent="menu_purchase_config_pricelist" sequence="1" groups="product.group_purchase_pricelist" />
+
+
+
+
+
+
+
+
--
cgit v1.2.3
From 3a67d7dc992963c27a993010ab355cc3b6d88a78 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Mon, 26 Aug 2013 11:56:00 +0200
Subject: [FIX] crm: fixed a bug introduced when merging trunk
bzr revid: tde@openerp.com-20130826095600-bgvdzah9nwb2vu0l
---
addons/crm/crm_lead_view.xml | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml
index acbec848da0..da3d91491b4 100644
--- a/addons/crm/crm_lead_view.xml
+++ b/addons/crm/crm_lead_view.xml
@@ -173,8 +173,7 @@
-
+ widget="selection"/>
--
cgit v1.2.3
From b35a44f66d947b03542b8eb3899fcd3adcf476ef Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Tue, 27 Aug 2013 15:30:58 +0200
Subject: [IMP] mail_mail, mail_message: various improvements to try to improve
message creation time Replaced some read by browse Moved get_reply_to from
mail_mail to mail_message Hint: specifying email_from, reply_to help to
enhance computation time
[REF] mail: cleaned some tests, renamed a file, moved mail_group tests into a dedicated file
bzr revid: tde@openerp.com-20130827133058-ko0g0ib0f0jihmdk
---
addons/email_template/tests/test_mail.py | 4 +-
addons/mail/mail_mail.py | 70 +----------
addons/mail/mail_message.py | 99 +++++++++++----
addons/mail/tests/__init__.py | 3 +-
addons/mail/tests/common.py | 134 +++++++++++++++++++++
addons/mail/tests/test_invite.py | 4 +-
addons/mail/tests/test_mail_base.py | 94 ---------------
addons/mail/tests/test_mail_features.py | 4 +-
addons/mail/tests/test_mail_gateway.py | 174 +--------------------------
addons/mail/tests/test_mail_group.py | 71 +++++++++++
addons/mail/tests/test_mail_message.py | 193 +++++++++++++++++++++---------
addons/mail/tests/test_message_read.py | 4 +-
addons/portal/tests/test_portal.py | 4 +-
addons/project/tests/test_project_base.py | 4 +-
14 files changed, 438 insertions(+), 424 deletions(-)
create mode 100644 addons/mail/tests/common.py
delete mode 100644 addons/mail/tests/test_mail_base.py
create mode 100644 addons/mail/tests/test_mail_group.py
diff --git a/addons/email_template/tests/test_mail.py b/addons/email_template/tests/test_mail.py
index d5feea9ebc4..adb550aa617 100644
--- a/addons/email_template/tests/test_mail.py
+++ b/addons/email_template/tests/test_mail.py
@@ -20,10 +20,10 @@
##############################################################################
import base64
-from openerp.addons.mail.tests.test_mail_base import TestMailBase
+from openerp.addons.mail.tests.common import TestMail
-class test_message_compose(TestMailBase):
+class test_message_compose(TestMail):
def setUp(self):
super(test_message_compose, self).setUp()
diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py
index f3cf5ce9e57..e5cb2b56eb3 100644
--- a/addons/mail/mail_mail.py
+++ b/addons/mail/mail_mail.py
@@ -82,74 +82,11 @@ class mail_mail(osv.Model):
context = dict(context, default_type=None)
return super(mail_mail, self).default_get(cr, uid, fields, context=context)
- def _get_reply_to(self, cr, uid, values, context=None):
- """ Return a specific reply_to: alias of the document through message_get_reply_to
- or take the email_from
- """
- # if value specified: directly return it
- if values.get('reply_to'):
- return values.get('reply_to')
- format_name = True # whether to use a 'Followers of Pigs catchall alias
- if not email_reply_to:
- catchall_alias = ir_config_parameter.get_param(cr, uid, "mail.catchall.alias", context=context)
- if catchall_domain and catchall_alias:
- email_reply_to = '%s@%s' % (catchall_alias, catchall_domain)
-
- # still no reply_to -> reply_to will be the email_from
- if not email_reply_to and email_from:
- email_reply_to = email_from
-
- # format 'Document name '
- if email_reply_to and model and res_id and format_name:
- emails = tools.email_split(email_reply_to)
- if emails:
- email_reply_to = emails[0]
- document_name = self.pool[model].name_get(cr, SUPERUSER_ID, [res_id], context=context)[0]
- if document_name:
- # sanitize document name
- sanitized_doc_name = re.sub(r'[^\w+.]+', '-', document_name[1])
- # generate reply to
- email_reply_to = _('"Followers of %s" <%s>') % (sanitized_doc_name, email_reply_to)
-
- return email_reply_to
-
def create(self, cr, uid, values, context=None):
# notification field: if not set, set if mail comes from an existing mail.message
if 'notification' not in values and values.get('mail_message_id'):
values['notification'] = True
- mail_id = super(mail_mail, self).create(cr, uid, values, context=context)
-
- # reply_to: if not set, set with default values that require creation values
- # but delegate after creation because of mail_message.message_id automatic
- # creation using existence of reply_to
- if not values.get('reply_to'):
- reply_to = self._get_reply_to(cr, uid, values, context=context)
- if reply_to:
- self.write(cr, uid, [mail_id], {'reply_to': reply_to}, context=context)
- return mail_id
+ return super(mail_mail, self).create(cr, uid, values, context=context)
def unlink(self, cr, uid, ids, context=None):
# cascade-delete the parent message for all mails that are not created for a notification
@@ -226,11 +163,6 @@ class mail_mail(osv.Model):
# mail_mail formatting, tools and send mechanism
#------------------------------------------------------
- # TODO in 8.0(+): maybe factorize this to enable in modules link generation
- # independently of mail_mail model
- # TODO in 8.0(+): factorize doc name sanitized and 'Followers of ...' formatting
- # because it begins to appear everywhere
-
def _get_partner_access_link(self, cr, uid, mail, partner=None, context=None):
""" Generate URLs for links in mails:
- partner is an user and has read access to the document: direct link to document with model, res_id
diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py
index bcb66433964..d63025b0912 100644
--- a/addons/mail/mail_message.py
+++ b/addons/mail/mail_message.py
@@ -20,6 +20,8 @@
##############################################################################
import logging
+import re
+
from openerp import tools
from email.header import decode_header
@@ -87,6 +89,7 @@ class mail_message(osv.Model):
def _get_record_name(self, cr, uid, ids, name, arg, context=None):
""" Return the related document name, using name_get. It is done using
SUPERUSER_ID, to be sure to have the record name correctly stored. """
+ # return dict.fromkeys(ids, False)
# TDE note: regroup by model/ids, to have less queries to perform
result = dict.fromkeys(ids, False)
for message in self.read(cr, uid, ids, ['model', 'res_id'], context=context):
@@ -206,11 +209,11 @@ class mail_message(osv.Model):
raise osv.except_osv(_('Invalid Action!'), _("Unable to send email, please configure the sender's email address or alias."))
def _get_default_author(self, cr, uid, context=None):
- return self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+ return self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
_defaults = {
'type': 'email',
- 'date': lambda *a: fields.datetime.now(),
+ 'date': fields.datetime.now(),
'author_id': lambda self, cr, uid, ctx=None: self._get_default_author(cr, uid, ctx),
'body': '',
'email_from': lambda self, cr, uid, ctx=None: self._get_default_from(cr, uid, ctx),
@@ -645,7 +648,8 @@ class mail_message(osv.Model):
elif not ids:
return ids
- pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'])['partner_id'][0]
+ # pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'])['partner_id'][0]
+ pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
author_ids, partner_ids, allowed_ids = set([]), set([]), set([])
model_ids = {}
@@ -705,7 +709,7 @@ class mail_message(osv.Model):
ids = [ids]
not_obj = self.pool.get('mail.notification')
fol_obj = self.pool.get('mail.followers')
- partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0]
+ partner_id = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=None).partner_id.id
# Read mail_message.ids to have their values
message_values = dict.fromkeys(ids)
@@ -774,17 +778,66 @@ class mail_message(osv.Model):
_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % \
(self._description, operation))
+ def _get_reply_to(self, cr, uid, values, context=None):
+ """ Return a specific reply_to: alias of the document through message_get_reply_to
+ or take the email_from
+ """
+ email_reply_to = None
+
+ ir_config_parameter = self.pool.get("ir.config_parameter")
+ catchall_domain = ir_config_parameter.get_param(cr, uid, "mail.catchall.domain", context=context)
+
+ # model, res_id, email_from: comes from values OR related message
+ model, res_id, email_from = values.get('model'), values.get('res_id'), values.get('email_from')
+
+ # if model and res_id: try to use ``message_get_reply_to`` that returns the document alias
+ if not email_reply_to and model and res_id and catchall_domain and hasattr(self.pool[model], 'message_get_reply_to'):
+ email_reply_to = self.pool[model].message_get_reply_to(cr, uid, [res_id], context=context)[0]
+ # no alias reply_to -> catchall alias
+ if not email_reply_to and catchall_domain:
+ catchall_alias = ir_config_parameter.get_param(cr, uid, "mail.catchall.alias", context=context)
+ if catchall_alias:
+ email_reply_to = '%s@%s' % (catchall_alias, catchall_domain)
+ # still no reply_to -> reply_to will be the email_from
+ if not email_reply_to and email_from:
+ email_reply_to = email_from
+
+ # format 'Document name '
+ if email_reply_to and model and res_id:
+ emails = tools.email_split(email_reply_to)
+ if emails:
+ email_reply_to = emails[0]
+ document_name = self.pool[model].name_get(cr, SUPERUSER_ID, [res_id], context=context)[0]
+ if document_name:
+ # sanitize document name
+ sanitized_doc_name = re.sub(r'[^\w+.]+', '-', document_name[1])
+ # generate reply to
+ email_reply_to = _('"Followers of %s" <%s>') % (sanitized_doc_name, email_reply_to)
+
+ return email_reply_to
+
+ def _get_message_id(self, cr, uid, values, context=None):
+ message_id = None
+ if not values.get('message_id') and values.get('reply_to'):
+ message_id = tools.generate_tracking_message_id('reply_to')
+ elif not values.get('message_id') and values.get('res_id') and values.get('model'):
+ message_id = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
+ elif not values.get('message_id'):
+ message_id = tools.generate_tracking_message_id('private')
+ return message_id
+
def create(self, cr, uid, values, context=None):
if context is None:
context = {}
default_starred = context.pop('default_starred', False)
- # generate message_id, to redirect answers to the right discussion thread
- if not values.get('message_id') and values.get('reply_to'):
- values['message_id'] = tools.generate_tracking_message_id('reply_to')
- elif not values.get('message_id') and values.get('res_id') and values.get('model'):
- values['message_id'] = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
- elif not values.get('message_id'):
- values['message_id'] = tools.generate_tracking_message_id('private')
+
+ if not values.get('email_from'): # needed to compute reply_to
+ values['email_from'] = self._get_default_from(cr, uid, context=context)
+ if not values.get('message_id'):
+ values['message_id'] = self._get_message_id(cr, uid, values, context=context)
+ if not values.get('reply_to'):
+ values['reply_to'] = self._get_reply_to(cr, uid, values, context=context)
+
newid = super(mail_message, self).create(cr, uid, values, context)
self._notify(cr, uid, newid, context=context,
force_send=context.get('mail_notify_force_send', True),
@@ -914,26 +967,28 @@ class mail_message(osv.Model):
if message.subtype_id and message.model and message.res_id:
fol_obj = self.pool.get("mail.followers")
# browse as SUPERUSER because rules could restrict the search results
- fol_ids = fol_obj.search(cr, SUPERUSER_ID, [
- ('res_model', '=', message.model),
- ('res_id', '=', message.res_id),
- ('subtype_ids', 'in', message.subtype_id.id)
+ fol_ids = fol_obj.search(
+ cr, SUPERUSER_ID, [
+ ('res_model', '=', message.model),
+ ('res_id', '=', message.res_id),
+ ('subtype_ids', 'in', message.subtype_id.id)
], context=context)
- partners_to_notify |= set(fo.partner_id for fo in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context))
+ partners_to_notify |= set(fo.partner_id.id for fo in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context))
# remove me from notified partners, unless the message is written on my own wall
if message.subtype_id and message.author_id and message.model == "res.partner" and message.res_id == message.author_id.id:
- partners_to_notify |= set([message.author_id])
+ partners_to_notify |= set([message.author_id.id])
elif message.author_id:
- partners_to_notify -= set([message.author_id])
+ partners_to_notify -= set([message.author_id.id])
# all partner_ids of the mail.message have to be notified regardless of the above (even the author if explicitly added!)
if message.partner_ids:
- partners_to_notify |= set(message.partner_ids)
+ partners_to_notify |= set([p.id for p in message.partner_ids])
# notify
- if partners_to_notify:
- notification_obj._notify(cr, uid, newid, partners_to_notify=[p.id for p in partners_to_notify], context=context,
- force_send=force_send, user_signature=user_signature)
+ notification_obj._notify(
+ cr, uid, newid, partners_to_notify=list(partners_to_notify), context=context,
+ force_send=force_send, user_signature=user_signature
+ )
message.refresh()
# An error appear when a user receive a notification without notifying
diff --git a/addons/mail/tests/__init__.py b/addons/mail/tests/__init__.py
index ff1080b0580..242beb60bf1 100644
--- a/addons/mail/tests/__init__.py
+++ b/addons/mail/tests/__init__.py
@@ -19,9 +19,10 @@
#
##############################################################################
-from . import test_mail_message, test_mail_features, test_mail_gateway, test_message_read, test_invite
+from . import test_mail_group, test_mail_message, test_mail_features, test_mail_gateway, test_message_read, test_invite
checks = [
+ test_mail_group,
test_mail_message,
test_mail_features,
test_mail_gateway,
diff --git a/addons/mail/tests/common.py b/addons/mail/tests/common.py
new file mode 100644
index 00000000000..68a329e043a
--- /dev/null
+++ b/addons/mail/tests/common.py
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Business Applications
+# Copyright (c) 2012-TODAY OpenERP S.A.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+from openerp.tests import common
+
+
+class TestMail(common.TransactionCase):
+
+ def _mock_smtp_gateway(self, *args, **kwargs):
+ return args[2]['Message-Id']
+
+ def _init_mock_build_email(self):
+ self._build_email_args_list = []
+ self._build_email_kwargs_list = []
+
+ def _mock_build_email(self, *args, **kwargs):
+ """ Mock build_email to be able to test its values. Store them into
+ some internal variable for latter processing. """
+ self._build_email_args_list.append(args)
+ self._build_email_kwargs_list.append(kwargs)
+ return self._build_email(*args, **kwargs)
+
+ def setUp(self):
+ super(TestMail, self).setUp()
+ cr, uid = self.cr, self.uid
+
+ # Install mock SMTP gateway
+ self._init_mock_build_email()
+ self._build_email = self.registry('ir.mail_server').build_email
+ self.registry('ir.mail_server').build_email = self._mock_build_email
+ self._send_email = self.registry('ir.mail_server').send_email
+ self.registry('ir.mail_server').send_email = self._mock_smtp_gateway
+
+ # Usefull models
+ self.ir_model = self.registry('ir.model')
+ self.ir_model_data = self.registry('ir.model.data')
+ self.ir_attachment = self.registry('ir.attachment')
+ self.mail_alias = self.registry('mail.alias')
+ self.mail_thread = self.registry('mail.thread')
+ self.mail_group = self.registry('mail.group')
+ self.mail_mail = self.registry('mail.mail')
+ self.mail_message = self.registry('mail.message')
+ self.mail_notification = self.registry('mail.notification')
+ self.mail_followers = self.registry('mail.followers')
+ self.mail_message_subtype = self.registry('mail.message.subtype')
+ self.res_users = self.registry('res.users')
+ self.res_partner = self.registry('res.partner')
+
+ # Find Employee group
+ group_employee_ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'base', 'group_user')
+ self.group_employee_id = group_employee_ref and group_employee_ref[1] or False
+
+ # Partner Data
+
+ # User Data: employee, noone
+ self.user_employee_id = self.res_users.create(cr, uid, {
+ 'name': 'Ernest Employee',
+ 'login': 'ernest',
+ 'alias_name': 'ernest',
+ 'email': 'e.e@example.com',
+ 'signature': '--\nErnest',
+ 'notification_email_send': 'comment',
+ 'groups_id': [(6, 0, [self.group_employee_id])]
+ }, {'no_reset_password': True})
+ self.user_noone_id = self.res_users.create(cr, uid, {
+ 'name': 'Noemie NoOne',
+ 'login': 'noemie',
+ 'alias_name': 'noemie',
+ 'email': 'n.n@example.com',
+ 'signature': '--\nNoemie',
+ 'notification_email_send': 'comment',
+ 'groups_id': [(6, 0, [])]
+ }, {'no_reset_password': True})
+
+ # Test users to use through the various tests
+ self.res_users.write(cr, uid, uid, {'name': 'Administrator'})
+ self.user_raoul_id = self.res_users.create(cr, uid, {
+ 'name': 'Raoul Grosbedon',
+ 'signature': 'SignRaoul',
+ 'email': 'raoul@raoul.fr',
+ 'login': 'raoul',
+ 'alias_name': 'raoul',
+ 'groups_id': [(6, 0, [self.group_employee_id])]
+ })
+ self.user_bert_id = self.res_users.create(cr, uid, {
+ 'name': 'Bert Tartignole',
+ 'signature': 'SignBert',
+ 'email': 'bert@bert.fr',
+ 'login': 'bert',
+ 'alias_name': 'bert',
+ 'groups_id': [(6, 0, [])]
+ })
+ self.user_raoul = self.res_users.browse(cr, uid, self.user_raoul_id)
+ self.user_bert = self.res_users.browse(cr, uid, self.user_bert_id)
+ self.user_admin = self.res_users.browse(cr, uid, uid)
+ self.partner_admin_id = self.user_admin.partner_id.id
+ self.partner_raoul_id = self.user_raoul.partner_id.id
+ self.partner_bert_id = self.user_bert.partner_id.id
+
+ # Test 'pigs' group to use through the various tests
+ self.group_pigs_id = self.mail_group.create(
+ cr, uid,
+ {'name': 'Pigs', 'description': 'Fans of Pigs, unite !', 'alias_name': 'group+pigs'},
+ {'mail_create_nolog': True}
+ )
+ self.group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
+ # Test mail.group: public to provide access to everyone
+ self.group_jobs_id = self.mail_group.create(cr, uid, {'name': 'Jobs', 'public': 'public'})
+ # Test mail.group: private to restrict access
+ self.group_priv_id = self.mail_group.create(cr, uid, {'name': 'Private', 'public': 'private'})
+
+ def tearDown(self):
+ # Remove mocks
+ self.registry('ir.mail_server').build_email = self._build_email
+ self.registry('ir.mail_server').send_email = self._send_email
+ super(TestMail, self).tearDown()
diff --git a/addons/mail/tests/test_invite.py b/addons/mail/tests/test_invite.py
index faa9fd55793..c4484d254bb 100644
--- a/addons/mail/tests/test_invite.py
+++ b/addons/mail/tests/test_invite.py
@@ -19,10 +19,10 @@
#
##############################################################################
-from openerp.addons.mail.tests.test_mail_base import TestMailBase
+from openerp.addons.mail.tests.common import TestMail
-class test_invite(TestMailBase):
+class test_invite(TestMail):
def test_00_basic_invite(self):
cr, uid = self.cr, self.uid
diff --git a/addons/mail/tests/test_mail_base.py b/addons/mail/tests/test_mail_base.py
deleted file mode 100644
index b0f9f72b6f7..00000000000
--- a/addons/mail/tests/test_mail_base.py
+++ /dev/null
@@ -1,94 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Business Applications
-# Copyright (c) 2012-TODAY OpenERP S.A.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
-#
-##############################################################################
-
-from openerp.tests import common
-
-
-class TestMailBase(common.TransactionCase):
-
- def _mock_smtp_gateway(self, *args, **kwargs):
- return args[2]['Message-Id']
-
- def _init_mock_build_email(self):
- self._build_email_args_list = []
- self._build_email_kwargs_list = []
-
- def _mock_build_email(self, *args, **kwargs):
- """ Mock build_email to be able to test its values. Store them into
- some internal variable for latter processing. """
- self._build_email_args_list.append(args)
- self._build_email_kwargs_list.append(kwargs)
- return self._build_email(*args, **kwargs)
-
- def setUp(self):
- super(TestMailBase, self).setUp()
- cr, uid = self.cr, self.uid
-
- # Install mock SMTP gateway
- self._init_mock_build_email()
- self._build_email = self.registry('ir.mail_server').build_email
- self.registry('ir.mail_server').build_email = self._mock_build_email
- self._send_email = self.registry('ir.mail_server').send_email
- self.registry('ir.mail_server').send_email = self._mock_smtp_gateway
-
- # Usefull models
- self.ir_model = self.registry('ir.model')
- self.ir_model_data = self.registry('ir.model.data')
- self.ir_attachment = self.registry('ir.attachment')
- self.mail_alias = self.registry('mail.alias')
- self.mail_thread = self.registry('mail.thread')
- self.mail_group = self.registry('mail.group')
- self.mail_mail = self.registry('mail.mail')
- self.mail_message = self.registry('mail.message')
- self.mail_notification = self.registry('mail.notification')
- self.mail_followers = self.registry('mail.followers')
- self.mail_message_subtype = self.registry('mail.message.subtype')
- self.res_users = self.registry('res.users')
- self.res_partner = self.registry('res.partner')
-
- # Find Employee group
- group_employee_ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'base', 'group_user')
- self.group_employee_id = group_employee_ref and group_employee_ref[1] or False
-
- # Test users to use through the various tests
- self.res_users.write(cr, uid, uid, {'name': 'Administrator'})
- self.user_raoul_id = self.res_users.create(cr, uid,
- {'name': 'Raoul Grosbedon', 'signature': 'SignRaoul', 'email': 'raoul@raoul.fr', 'login': 'raoul', 'alias_name': 'raoul', 'groups_id': [(6, 0, [self.group_employee_id])]})
- self.user_bert_id = self.res_users.create(cr, uid,
- {'name': 'Bert Tartignole', 'signature': 'SignBert', 'email': 'bert@bert.fr', 'login': 'bert', 'alias_name': 'bert', 'groups_id': [(6, 0, [])]})
- self.user_raoul = self.res_users.browse(cr, uid, self.user_raoul_id)
- self.user_bert = self.res_users.browse(cr, uid, self.user_bert_id)
- self.user_admin = self.res_users.browse(cr, uid, uid)
- self.partner_admin_id = self.user_admin.partner_id.id
- self.partner_raoul_id = self.user_raoul.partner_id.id
- self.partner_bert_id = self.user_bert.partner_id.id
-
- # Test 'pigs' group to use through the various tests
- self.group_pigs_id = self.mail_group.create(cr, uid,
- {'name': 'Pigs', 'description': 'Fans of Pigs, unite !', 'alias_name': 'group+pigs'},
- {'mail_create_nolog': True})
- self.group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
-
- def tearDown(self):
- # Remove mocks
- self.registry('ir.mail_server').build_email = self._build_email
- self.registry('ir.mail_server').send_email = self._send_email
- super(TestMailBase, self).tearDown()
diff --git a/addons/mail/tests/test_mail_features.py b/addons/mail/tests/test_mail_features.py
index e9563e19246..970f07f519e 100644
--- a/addons/mail/tests/test_mail_features.py
+++ b/addons/mail/tests/test_mail_features.py
@@ -21,12 +21,12 @@
from openerp.addons.mail.mail_mail import mail_mail
from openerp.addons.mail.mail_thread import mail_thread
-from openerp.addons.mail.tests.test_mail_base import TestMailBase
+from openerp.addons.mail.tests.common import TestMail
from openerp.tools import mute_logger, email_split
from openerp.tools.mail import html_sanitize
-class test_mail(TestMailBase):
+class test_mail(TestMail):
def test_000_alias_setup(self):
""" Test basic mail.alias setup works, before trying to use them for routing """
diff --git a/addons/mail/tests/test_mail_gateway.py b/addons/mail/tests/test_mail_gateway.py
index 3fd8ebd3101..b2a1c3250aa 100644
--- a/addons/mail/tests/test_mail_gateway.py
+++ b/addons/mail/tests/test_mail_gateway.py
@@ -19,7 +19,7 @@
#
##############################################################################
-from openerp.addons.mail.tests.test_mail_base import TestMailBase
+from openerp.addons.mail.tests.common import TestMail
from openerp.tools import mute_logger
MAIL_TEMPLATE = """Return-Path:
@@ -143,173 +143,9 @@ dGVzdAo=
--089e01536c4ed4d17204e49b8e96--"""
-class TestMailgateway(TestMailBase):
+class TestMailgateway(TestMail):
- def test_00_partner_find_from_email(self):
- """ Tests designed for partner fetch based on emails. """
- cr, uid, user_raoul, group_pigs = self.cr, self.uid, self.user_raoul, self.group_pigs
-
- # --------------------------------------------------
- # Data creation
- # --------------------------------------------------
- # 1 - Partner ARaoul
- p_a_id = self.res_partner.create(cr, uid, {'name': 'ARaoul', 'email': 'test@test.fr'})
-
- # --------------------------------------------------
- # CASE1: without object
- # --------------------------------------------------
-
- # Do: find partner with email -> first partner should be found
- partner_info = self.mail_thread.message_partner_info_from_emails(cr, uid, None, ['Maybe Raoul '], link_mail=False)[0]
- self.assertEqual(partner_info['full_name'], 'Maybe Raoul ',
- 'mail_thread: message_partner_info_from_emails did not handle email')
- self.assertEqual(partner_info['partner_id'], p_a_id,
- 'mail_thread: message_partner_info_from_emails wrong partner found')
-
- # Data: add some data about partners
- # 2 - User BRaoul
- p_b_id = self.res_partner.create(cr, uid, {'name': 'BRaoul', 'email': 'test@test.fr', 'user_ids': [(4, user_raoul.id)]})
-
- # Do: find partner with email -> first user should be found
- partner_info = self.mail_thread.message_partner_info_from_emails(cr, uid, None, ['Maybe Raoul '], link_mail=False)[0]
- self.assertEqual(partner_info['partner_id'], p_b_id,
- 'mail_thread: message_partner_info_from_emails wrong partner found')
-
- # --------------------------------------------------
- # CASE1: with object
- # --------------------------------------------------
-
- # Do: find partner in group where there is a follower with the email -> should be taken
- self.mail_group.message_subscribe(cr, uid, [group_pigs.id], [p_b_id])
- partner_info = self.mail_group.message_partner_info_from_emails(cr, uid, group_pigs.id, ['Maybe Raoul '], link_mail=False)[0]
- self.assertEqual(partner_info['partner_id'], p_b_id,
- 'mail_thread: message_partner_info_from_emails wrong partner found')
-
- def test_05_mail_message_mail_mail(self):
- """ Tests designed for testing email values based on mail.message, aliases, ... """
- cr, uid, user_raoul_id = self.cr, self.uid, self.user_raoul_id
-
- # Data: update + generic variables
- reply_to1 = '_reply_to1@example.com'
- reply_to2 = '_reply_to2@example.com'
- email_from1 = 'from@example.com'
- alias_domain = 'schlouby.fr'
- raoul_from = 'Raoul Grosbedon '
- raoul_from_alias = 'Raoul Grosbedon '
- raoul_reply = '"Followers of Pigs" '
- raoul_reply_alias = '"Followers of Pigs" '
- # Data: remove alias_domain to see emails with alias
- param_ids = self.registry('ir.config_parameter').search(cr, uid, [('key', '=', 'mail.catchall.domain')])
- self.registry('ir.config_parameter').unlink(cr, uid, param_ids)
-
- # Do: free message; specified values > default values
- msg_id = self.mail_message.create(cr, user_raoul_id, {'reply_to': reply_to1, 'email_from': email_from1})
- msg = self.mail_message.browse(cr, user_raoul_id, msg_id)
- # Test: message content
- self.assertIn('reply_to', msg.message_id,
- 'mail_message: message_id should be specific to a mail_message with a given reply_to')
- self.assertEqual(msg.reply_to, reply_to1,
- 'mail_message: incorrect reply_to: should come from values')
- self.assertEqual(msg.email_from, email_from1,
- 'mail_message: incorrect email_from: should come from values')
- # Do: create a mail_mail with the previous mail_message
- mail_id = self.mail_mail.create(cr, user_raoul_id, {'mail_message_id': msg_id, 'state': 'cancel'})
- mail = self.mail_mail.browse(cr, user_raoul_id, mail_id)
- # Test: mail_mail content
- self.assertEqual(mail.reply_to, reply_to1,
- 'mail_mail: incorrect reply_to: should come from mail.message')
- self.assertEqual(mail.email_from, email_from1,
- 'mail_mail: incorrect email_from: should come from mail.message')
- # Do: create a mail_mail with the previous mail_message + specified reply_to
- mail_id = self.mail_mail.create(cr, user_raoul_id, {'mail_message_id': msg_id, 'state': 'cancel', 'reply_to': reply_to2})
- mail = self.mail_mail.browse(cr, user_raoul_id, mail_id)
- # Test: mail_mail content
- self.assertEqual(mail.reply_to, reply_to2,
- 'mail_mail: incorrect reply_to: should come from values')
- self.assertEqual(mail.email_from, email_from1,
- 'mail_mail: incorrect email_from: should come from mail.message')
-
- # Do: mail_message attached to a document
- msg_id = self.mail_message.create(cr, user_raoul_id, {'model': 'mail.group', 'res_id': self.group_pigs_id})
- msg = self.mail_message.browse(cr, user_raoul_id, msg_id)
- # Test: message content
- self.assertIn('mail.group', msg.message_id,
- 'mail_message: message_id should contain model')
- self.assertIn('%s' % self.group_pigs_id, msg.message_id,
- 'mail_message: message_id should contain res_id')
- self.assertFalse(msg.reply_to,
- 'mail_message: incorrect reply_to: should not be generated if not specified')
- self.assertEqual(msg.email_from, raoul_from,
- 'mail_message: incorrect email_from: should be Raoul')
- # Do: create a mail_mail based on the previous mail_message
- mail_id = self.mail_mail.create(cr, user_raoul_id, {'mail_message_id': msg_id, 'state': 'cancel'})
- mail = self.mail_mail.browse(cr, user_raoul_id, mail_id)
- # Test: mail_mail content
- self.assertEqual(mail.reply_to, raoul_reply,
- 'mail_mail: incorrect reply_to: should be Raoul')
-
- # Data: set catchall domain
- self.registry('ir.config_parameter').set_param(cr, uid, 'mail.catchall.domain', alias_domain)
- self.registry('ir.config_parameter').unlink(cr, uid, self.registry('ir.config_parameter').search(cr, uid, [('key', '=', 'mail.catchall.alias')]))
-
- # Update message
- self.mail_message.write(cr, user_raoul_id, [msg_id], {'email_from': False, 'reply_to': False})
- msg.refresh()
- # Do: create a mail_mail based on the previous mail_message
- mail_id = self.mail_mail.create(cr, user_raoul_id, {'mail_message_id': msg_id, 'state': 'cancel'})
- mail = self.mail_mail.browse(cr, user_raoul_id, mail_id)
- # Test: mail_mail content
- self.assertEqual(mail.reply_to, raoul_reply_alias,
- 'mail_mail: incorrect reply_to: should be Pigs alias')
-
- # Update message: test alias on email_from
- msg_id = self.mail_message.create(cr, user_raoul_id, {})
- msg = self.mail_message.browse(cr, user_raoul_id, msg_id)
- # Do: create a mail_mail based on the previous mail_message
- mail_id = self.mail_mail.create(cr, user_raoul_id, {'mail_message_id': msg_id, 'state': 'cancel'})
- mail = self.mail_mail.browse(cr, user_raoul_id, mail_id)
- # Test: mail_mail content
- self.assertEqual(mail.reply_to, raoul_from_alias,
- 'mail_mail: incorrect reply_to: should be message email_from using Raoul alias')
-
- # Update message
- self.mail_message.write(cr, user_raoul_id, [msg_id], {'res_id': False, 'email_from': 'someone@schlouby.fr', 'reply_to': False})
- msg.refresh()
- # Do: create a mail_mail based on the previous mail_message
- mail_id = self.mail_mail.create(cr, user_raoul_id, {'mail_message_id': msg_id, 'state': 'cancel'})
- mail = self.mail_mail.browse(cr, user_raoul_id, mail_id)
- # Test: mail_mail content
- self.assertEqual(mail.reply_to, msg.email_from,
- 'mail_mail: incorrect reply_to: should be message email_from')
-
- # Data: set catchall alias
- self.registry('ir.config_parameter').set_param(self.cr, self.uid, 'mail.catchall.alias', 'gateway')
-
- # Update message
- self.mail_message.write(cr, uid, [msg_id], {'email_from': False, 'reply_to': False})
- msg.refresh()
- # Do: create a mail_mail based on the previous mail_message
- mail_id = self.mail_mail.create(cr, uid, {'mail_message_id': msg_id, 'state': 'cancel'})
- mail = self.mail_mail.browse(cr, uid, mail_id)
- # Test: mail_mail Content-Type
- self.assertEqual(mail.reply_to, 'gateway@schlouby.fr',
- 'mail_mail: reply_to should equal the catchall email alias')
-
- # Do: create a mail_mail
- mail_id = self.mail_mail.create(cr, uid, {'state': 'cancel'})
- mail = self.mail_mail.browse(cr, uid, mail_id)
- # Test: mail_mail content
- self.assertEqual(mail.reply_to, 'gateway@schlouby.fr',
- 'mail_mail: reply_to should equal the catchall email alias')
-
- # Do: create a mail_mail
- mail_id = self.mail_mail.create(cr, uid, {'state': 'cancel', 'reply_to': 'someone@example.com'})
- mail = self.mail_mail.browse(cr, uid, mail_id)
- # Test: mail_mail content
- self.assertEqual(mail.reply_to, 'someone@example.com',
- 'mail_mail: reply_to should equal the rpely_to given to create')
-
- def test_09_message_parse(self):
+ def test_00_message_parse(self):
""" Testing incoming emails parsing """
cr, uid = self.cr, self.uid
@@ -738,9 +574,7 @@ class TestMailgateway(TestMailBase):
'message_post: private discussion: incorrect notified recipients')
self.assertEqual(msg.model, False,
'message_post: private discussion: context key "thread_model" not correctly ignored when having no res_id')
- # Test: message reply_to and message-id
- self.assertFalse(msg.reply_to,
- 'message_post: private discussion: initial message should not have any reply_to specified')
+ # Test: message-id
self.assertIn('openerp-private', msg.message_id,
'message_post: private discussion: message-id should contain the private keyword')
diff --git a/addons/mail/tests/test_mail_group.py b/addons/mail/tests/test_mail_group.py
new file mode 100644
index 00000000000..c1512441b13
--- /dev/null
+++ b/addons/mail/tests/test_mail_group.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Business Applications
+# Copyright (c) 2012-TODAY OpenERP S.A.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+from openerp.addons.mail.tests.common import TestMail
+from openerp.osv.orm import except_orm
+from openerp.tools import mute_logger
+
+
+class TestMailGroup(TestMail):
+
+ @mute_logger('openerp.addons.base.ir.ir_model', 'openerp.osv.orm')
+ def test_00_mail_group_access_rights(self):
+ """ Testing mail_group access rights and basic mail_thread features """
+ cr, uid, user_noone_id, user_employee_id = self.cr, self.uid, self.user_noone_id, self.user_employee_id
+
+ # Do: Bert reads Jobs -> ok, public
+ self.mail_group.read(cr, user_noone_id, [self.group_jobs_id])
+ # Do: Bert read Pigs -> ko, restricted to employees
+ with self.assertRaises(except_orm):
+ self.mail_group.read(cr, user_noone_id, [self.group_pigs_id])
+ # Do: Raoul read Pigs -> ok, belong to employees
+ self.mail_group.read(cr, user_employee_id, [self.group_pigs_id])
+
+ # Do: Bert creates a group -> ko, no access rights
+ with self.assertRaises(except_orm):
+ self.mail_group.create(cr, user_noone_id, {'name': 'Test'})
+ # Do: Raoul creates a restricted group -> ok
+ new_group_id = self.mail_group.create(cr, user_employee_id, {'name': 'Test'})
+ # Do: Bert added in followers, read -> ok, in followers
+ self.mail_group.message_subscribe_users(cr, uid, [new_group_id], [user_noone_id])
+ self.mail_group.read(cr, user_noone_id, [new_group_id])
+
+ # Do: Raoul reads Priv -> ko, private
+ with self.assertRaises(except_orm):
+ self.mail_group.read(cr, user_employee_id, [self.group_priv_id])
+ # Do: Raoul added in follower, read -> ok, in followers
+ self.mail_group.message_subscribe_users(cr, uid, [self.group_priv_id], [user_employee_id])
+ self.mail_group.read(cr, user_employee_id, [self.group_priv_id])
+
+ # Do: Raoul write on Jobs -> ok
+ self.mail_group.write(cr, user_employee_id, [self.group_priv_id], {'name': 'modified'})
+ # Do: Bert cannot write on Private -> ko (read but no write)
+ with self.assertRaises(except_orm):
+ self.mail_group.write(cr, user_noone_id, [self.group_priv_id], {'name': 're-modified'})
+ # Test: Bert cannot unlink the group
+ with self.assertRaises(except_orm):
+ self.mail_group.unlink(cr, user_noone_id, [self.group_priv_id])
+ # Do: Raoul unlinks the group, there are no followers and messages left
+ self.mail_group.unlink(cr, user_employee_id, [self.group_priv_id])
+ fol_ids = self.mail_followers.search(cr, uid, [('res_model', '=', 'mail.group'), ('res_id', '=', self.group_priv_id)])
+ self.assertFalse(fol_ids, 'unlinked document should not have any followers left')
+ msg_ids = self.mail_message.search(cr, uid, [('model', '=', 'mail.group'), ('res_id', '=', self.group_priv_id)])
+ self.assertFalse(msg_ids, 'unlinked document should not have any followers left')
diff --git a/addons/mail/tests/test_mail_message.py b/addons/mail/tests/test_mail_message.py
index f422219000c..5dc5e9e247d 100644
--- a/addons/mail/tests/test_mail_message.py
+++ b/addons/mail/tests/test_mail_message.py
@@ -19,66 +19,147 @@
#
##############################################################################
-from openerp.addons.mail.tests.test_mail_base import TestMailBase
+from openerp.addons.mail.tests.common import TestMail
from openerp.osv.orm import except_orm
from openerp.tools import mute_logger
-class test_mail_access_rights(TestMailBase):
-
- def setUp(self):
- super(test_mail_access_rights, self).setUp()
- cr, uid = self.cr, self.uid
-
- # Test mail.group: public to provide access to everyone
- self.group_jobs_id = self.mail_group.create(cr, uid, {'name': 'Jobs', 'public': 'public'})
- # Test mail.group: private to restrict access
- self.group_priv_id = self.mail_group.create(cr, uid, {'name': 'Private', 'public': 'private'})
-
- @mute_logger('openerp.addons.base.ir.ir_model', 'openerp.osv.orm')
- def test_00_mail_group_access_rights(self):
- """ Testing mail_group access rights and basic mail_thread features """
- cr, uid, user_bert_id, user_raoul_id = self.cr, self.uid, self.user_bert_id, self.user_raoul_id
-
- # Do: Bert reads Jobs -> ok, public
- self.mail_group.read(cr, user_bert_id, [self.group_jobs_id])
- # Do: Bert read Pigs -> ko, restricted to employees
- self.assertRaises(except_orm, self.mail_group.read,
- cr, user_bert_id, [self.group_pigs_id])
- # Do: Raoul read Pigs -> ok, belong to employees
- self.mail_group.read(cr, user_raoul_id, [self.group_pigs_id])
-
- # Do: Bert creates a group -> ko, no access rights
- self.assertRaises(except_orm, self.mail_group.create,
- cr, user_bert_id, {'name': 'Test'})
- # Do: Raoul creates a restricted group -> ok
- new_group_id = self.mail_group.create(cr, user_raoul_id, {'name': 'Test'})
- # Do: Bert added in followers, read -> ok, in followers
- self.mail_group.message_subscribe_users(cr, uid, [new_group_id], [user_bert_id])
- self.mail_group.read(cr, user_bert_id, [new_group_id])
-
- # Do: Raoul reads Priv -> ko, private
- self.assertRaises(except_orm, self.mail_group.read,
- cr, user_raoul_id, [self.group_priv_id])
- # Do: Raoul added in follower, read -> ok, in followers
- self.mail_group.message_subscribe_users(cr, uid, [self.group_priv_id], [user_raoul_id])
- self.mail_group.read(cr, user_raoul_id, [self.group_priv_id])
-
- # Do: Raoul write on Jobs -> ok
- self.mail_group.write(cr, user_raoul_id, [self.group_priv_id], {'name': 'modified'})
- # Do: Bert cannot write on Private -> ko (read but no write)
- self.assertRaises(except_orm, self.mail_group.write,
- cr, user_bert_id, [self.group_priv_id], {'name': 're-modified'})
- # Test: Bert cannot unlink the group
- self.assertRaises(except_orm,
- self.mail_group.unlink,
- cr, user_bert_id, [self.group_priv_id])
- # Do: Raoul unlinks the group, there are no followers and messages left
- self.mail_group.unlink(cr, user_raoul_id, [self.group_priv_id])
- fol_ids = self.mail_followers.search(cr, uid, [('res_model', '=', 'mail.group'), ('res_id', '=', self.group_priv_id)])
- self.assertFalse(fol_ids, 'unlinked document should not have any followers left')
- msg_ids = self.mail_message.search(cr, uid, [('model', '=', 'mail.group'), ('res_id', '=', self.group_priv_id)])
- self.assertFalse(msg_ids, 'unlinked document should not have any followers left')
+class TestMailMail(TestMail):
+
+ def test_00_partner_find_from_email(self):
+ """ Tests designed for partner fetch based on emails. """
+ cr, uid, user_raoul, group_pigs = self.cr, self.uid, self.user_raoul, self.group_pigs
+
+ # --------------------------------------------------
+ # Data creation
+ # --------------------------------------------------
+ # 1 - Partner ARaoul
+ p_a_id = self.res_partner.create(cr, uid, {'name': 'ARaoul', 'email': 'test@test.fr'})
+
+ # --------------------------------------------------
+ # CASE1: without object
+ # --------------------------------------------------
+
+ # Do: find partner with email -> first partner should be found
+ partner_info = self.mail_thread.message_partner_info_from_emails(cr, uid, None, ['Maybe Raoul '], link_mail=False)[0]
+ self.assertEqual(partner_info['full_name'], 'Maybe Raoul ',
+ 'mail_thread: message_partner_info_from_emails did not handle email')
+ self.assertEqual(partner_info['partner_id'], p_a_id,
+ 'mail_thread: message_partner_info_from_emails wrong partner found')
+
+ # Data: add some data about partners
+ # 2 - User BRaoul
+ p_b_id = self.res_partner.create(cr, uid, {'name': 'BRaoul', 'email': 'test@test.fr', 'user_ids': [(4, user_raoul.id)]})
+
+ # Do: find partner with email -> first user should be found
+ partner_info = self.mail_thread.message_partner_info_from_emails(cr, uid, None, ['Maybe Raoul '], link_mail=False)[0]
+ self.assertEqual(partner_info['partner_id'], p_b_id,
+ 'mail_thread: message_partner_info_from_emails wrong partner found')
+
+ # --------------------------------------------------
+ # CASE1: with object
+ # --------------------------------------------------
+
+ # Do: find partner in group where there is a follower with the email -> should be taken
+ self.mail_group.message_subscribe(cr, uid, [group_pigs.id], [p_b_id])
+ partner_info = self.mail_group.message_partner_info_from_emails(cr, uid, group_pigs.id, ['Maybe Raoul '], link_mail=False)[0]
+ self.assertEqual(partner_info['partner_id'], p_b_id,
+ 'mail_thread: message_partner_info_from_emails wrong partner found')
+
+
+class TestMailMessage(TestMail):
+
+ def test_00_mail_message_values(self):
+ """ Tests designed for testing email values based on mail.message, aliases, ... """
+ cr, uid, user_raoul_id = self.cr, self.uid, self.user_raoul_id
+
+ # Data: update + generic variables
+ reply_to1 = '_reply_to1@example.com'
+ reply_to2 = '_reply_to2@example.com'
+ email_from1 = 'from@example.com'
+ alias_domain = 'schlouby.fr'
+ raoul_from = 'Raoul Grosbedon '
+ raoul_from_alias = 'Raoul Grosbedon '
+ raoul_reply = '"Followers of Pigs" '
+ raoul_reply_alias = '"Followers of Pigs" '
+
+ # --------------------------------------------------
+ # Case1: without alias_domain
+ # --------------------------------------------------
+ param_ids = self.registry('ir.config_parameter').search(cr, uid, [('key', '=', 'mail.catchall.domain')])
+ self.registry('ir.config_parameter').unlink(cr, uid, param_ids)
+
+ # Do: free message; specified values > default values
+ msg_id = self.mail_message.create(cr, user_raoul_id, {'reply_to': reply_to1, 'email_from': email_from1})
+ msg = self.mail_message.browse(cr, user_raoul_id, msg_id)
+ # Test: message content
+ self.assertIn('reply_to', msg.message_id,
+ 'mail_message: message_id should be specific to a mail_message with a given reply_to')
+ self.assertEqual(msg.reply_to, reply_to1,
+ 'mail_message: incorrect reply_to: should come from values')
+ self.assertEqual(msg.email_from, email_from1,
+ 'mail_message: incorrect email_from: should come from values')
+
+ # Do: create a mail_mail with the previous mail_message + specified reply_to
+ mail_id = self.mail_mail.create(cr, user_raoul_id, {'mail_message_id': msg_id, 'state': 'cancel', 'reply_to': reply_to2})
+ mail = self.mail_mail.browse(cr, user_raoul_id, mail_id)
+ # Test: mail_mail content
+ self.assertEqual(mail.reply_to, reply_to2,
+ 'mail_mail: incorrect reply_to: should come from values')
+ self.assertEqual(mail.email_from, email_from1,
+ 'mail_mail: incorrect email_from: should come from mail.message')
+
+ # Do: mail_message attached to a document
+ msg_id = self.mail_message.create(cr, user_raoul_id, {'model': 'mail.group', 'res_id': self.group_pigs_id})
+ msg = self.mail_message.browse(cr, user_raoul_id, msg_id)
+ # Test: message content
+ self.assertIn('mail.group', msg.message_id,
+ 'mail_message: message_id should contain model')
+ self.assertIn('%s' % self.group_pigs_id, msg.message_id,
+ 'mail_message: message_id should contain res_id')
+ self.assertEqual(msg.reply_to, raoul_reply,
+ 'mail_message: incorrect reply_to: should be Raoul')
+ self.assertEqual(msg.email_from, raoul_from,
+ 'mail_message: incorrect email_from: should be Raoul')
+
+ # --------------------------------------------------
+ # Case2: with alias_domain, without catchall alias
+ # --------------------------------------------------
+ self.registry('ir.config_parameter').set_param(cr, uid, 'mail.catchall.domain', alias_domain)
+ self.registry('ir.config_parameter').unlink(cr, uid, self.registry('ir.config_parameter').search(cr, uid, [('key', '=', 'mail.catchall.alias')]))
+
+ # Update message
+ msg_id = self.mail_message.create(cr, user_raoul_id, {'model': 'mail.group', 'res_id': self.group_pigs_id})
+ msg = self.mail_message.browse(cr, user_raoul_id, msg_id)
+ # Test: generated reply_to
+ self.assertEqual(msg.reply_to, raoul_reply_alias,
+ 'mail_mail: incorrect reply_to: should be Pigs alias')
+
+ # Update message: test alias on email_from
+ msg_id = self.mail_message.create(cr, user_raoul_id, {})
+ msg = self.mail_message.browse(cr, user_raoul_id, msg_id)
+ # Test: generated reply_to
+ self.assertEqual(msg.reply_to, raoul_from_alias,
+ 'mail_mail: incorrect reply_to: should be message email_from using Raoul alias')
+
+ # --------------------------------------------------
+ # Case2: with alias_domain and catchall alias
+ # --------------------------------------------------
+ self.registry('ir.config_parameter').set_param(self.cr, self.uid, 'mail.catchall.alias', 'gateway')
+
+ # Update message
+ msg_id = self.mail_message.create(cr, user_raoul_id, {})
+ msg = self.mail_message.browse(cr, user_raoul_id, msg_id)
+ # Test: generated reply_to
+ self.assertEqual(msg.reply_to, 'gateway@schlouby.fr',
+ 'mail_mail: reply_to should equal the catchall email alias')
+
+ # Do: create a mail_mail
+ mail_id = self.mail_mail.create(cr, uid, {'state': 'cancel', 'reply_to': 'someone@example.com'})
+ mail = self.mail_mail.browse(cr, uid, mail_id)
+ # Test: mail_mail content
+ self.assertEqual(mail.reply_to, 'someone@example.com',
+ 'mail_mail: reply_to should equal the rpely_to given to create')
@mute_logger('openerp.addons.base.ir.ir_model', 'openerp.osv.orm')
def test_10_mail_message_search_access_rights(self):
diff --git a/addons/mail/tests/test_message_read.py b/addons/mail/tests/test_message_read.py
index a4ff3685788..c02e9a32278 100644
--- a/addons/mail/tests/test_message_read.py
+++ b/addons/mail/tests/test_message_read.py
@@ -19,10 +19,10 @@
#
##############################################################################
-from openerp.addons.mail.tests.test_mail_base import TestMailBase
+from openerp.addons.mail.tests.common import TestMail
-class test_mail_access_rights(TestMailBase):
+class test_mail_access_rights(TestMail):
def test_00_message_read(self):
""" Tests for message_read and expandables. """
diff --git a/addons/portal/tests/test_portal.py b/addons/portal/tests/test_portal.py
index e2b66e65af6..4d301458365 100644
--- a/addons/portal/tests/test_portal.py
+++ b/addons/portal/tests/test_portal.py
@@ -19,12 +19,12 @@
#
##############################################################################
-from openerp.addons.mail.tests.test_mail_base import TestMailBase
+from openerp.addons.mail.tests.common import TestMail
from openerp.osv.orm import except_orm
from openerp.tools.misc import mute_logger
-class test_portal(TestMailBase):
+class test_portal(TestMail):
def setUp(self):
super(test_portal, self).setUp()
diff --git a/addons/project/tests/test_project_base.py b/addons/project/tests/test_project_base.py
index f82561766fe..70f8b59812f 100644
--- a/addons/project/tests/test_project_base.py
+++ b/addons/project/tests/test_project_base.py
@@ -19,10 +19,10 @@
#
##############################################################################
-from openerp.addons.mail.tests.test_mail_base import TestMailBase
+from openerp.addons.mail.tests.common import TestMail
-class TestProjectBase(TestMailBase):
+class TestProjectBase(TestMail):
def setUp(self):
super(TestProjectBase, self).setUp()
--
cgit v1.2.3
From 4779df339eb8d7790cb7c6ac61d32f4d1d193f43 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Tue, 27 Aug 2013 17:38:40 +0200
Subject: [REF] mail_followers: cleaned notify methods, lessening the number of
queries.
bzr revid: tde@openerp.com-20130827153840-fo9s2lc35fvld3fb
---
addons/mail/mail_followers.py | 120 +++++++++++++++++++++---------------------
addons/mail/mail_message.py | 1 -
2 files changed, 61 insertions(+), 60 deletions(-)
diff --git a/addons/mail/mail_followers.py b/addons/mail/mail_followers.py
index ac350810aa6..0059df90530 100644
--- a/addons/mail/mail_followers.py
+++ b/addons/mail/mail_followers.py
@@ -77,7 +77,7 @@ class mail_notification(osv.Model):
if not cr.fetchone():
cr.execute('CREATE INDEX mail_notification_partner_id_read_starred_message_id ON mail_notification (partner_id, read, starred, message_id)')
- def get_partners_to_notify(self, cr, uid, message, partners_to_notify=None, context=None):
+ def get_partners_to_email(self, cr, uid, ids, message, context=None):
""" Return the list of partners to notify, based on their preferences.
:param browse_record message: mail.message to notify
@@ -85,13 +85,10 @@ class mail_notification(osv.Model):
the notifications to process
"""
notify_pids = []
- for notification in message.notification_ids:
+ for notification in self.browse(cr, uid, ids, context=context):
if notification.read:
continue
partner = notification.partner_id
- # If partners_to_notify specified: restrict to them
- if partners_to_notify is not None and partner.id not in partners_to_notify:
- continue
# Do not send to partners without email address defined
if not partner.email:
continue
@@ -143,76 +140,81 @@ class mail_notification(osv.Model):
company = user.company_id.name
sent_by = _('Sent by %(company)s using %(openerp)s.')
signature_company = '%s' % (sent_by % {
- 'company': company,
- 'openerp': "OpenERP"
- })
+ 'company': company,
+ 'openerp': "OpenERP"
+ })
footer = tools.append_content_to_html(footer, signature_company, plaintext=False, container_tag='div')
return footer
- def _notify(self, cr, uid, msg_id, partners_to_notify=None, context=None,
- force_send=False, user_signature=True):
- """ Send by email the notification depending on the user preferences
+ def update_message_notification(self, cr, uid, ids, message_id, partner_ids, context=None):
+ existing_pids = set()
+ new_pids = set()
+ new_notif_ids = []
- :param list partners_to_notify: optional list of partner ids restricting
- the notifications to process
- :param bool force_send: if True, the generated mail.mail is
- immediately sent after being created, as if the scheduler
- was executed for this message only.
- :param bool user_signature: if True, the generated mail.mail body is
- the body of the related mail.message with the author's signature
- """
- if context is None:
- context = {}
- mail_message_obj = self.pool.get('mail.message')
-
- # optional list of partners to notify: subscribe them if not already done or update the notification
- if partners_to_notify:
- notifications_to_update = []
- notified_partners = []
- notif_ids = self.search(cr, SUPERUSER_ID, [('message_id', '=', msg_id), ('partner_id', 'in', partners_to_notify)], context=context)
- for notification in self.browse(cr, SUPERUSER_ID, notif_ids, context=context):
- notified_partners.append(notification.partner_id.id)
- notifications_to_update.append(notification.id)
- partners_to_notify = filter(lambda item: item not in notified_partners, partners_to_notify)
- if notifications_to_update:
- self.write(cr, SUPERUSER_ID, notifications_to_update, {'read': False}, context=context)
- mail_message_obj.write(cr, uid, msg_id, {'notified_partner_ids': [(4, id) for id in partners_to_notify]}, context=context)
+ for notification in self.browse(cr, uid, ids, context=context):
+ existing_pids.add(notification.partner_id.id)
- # mail_notify_noemail (do not send email) or no partner_ids: do not send, return
- if context.get('mail_notify_noemail'):
- return True
- # browse as SUPERUSER_ID because of access to res_partner not necessarily allowed
- msg = self.pool.get('mail.message').browse(cr, SUPERUSER_ID, msg_id, context=context)
- notify_partner_ids = self.get_partners_to_notify(cr, uid, msg, partners_to_notify=partners_to_notify, context=context)
- if not notify_partner_ids:
- return True
+ # update existing notifications
+ self.write(cr, uid, ids, {'read': False}, context=context)
- # add the context in the email
- # TDE FIXME: commented, to be improved in a future branch
- # quote_context = self.pool.get('mail.message').message_quote_context(cr, uid, msg_id, context=context)
+ # create new notifications
+ new_pids = set(partner_ids) - existing_pids
+ for new_pid in new_pids:
+ new_notif_ids.append(self.create(cr, uid, {'message_id': message_id, 'partner_id': new_pid, 'read': False}, context=context))
+ return new_notif_ids
- # add signature
- body_html = msg.body
- user_id = msg.author_id and msg.author_id.user_ids and msg.author_id.user_ids[0] and msg.author_id.user_ids[0].id or None
+ def _notify_email(self, cr, uid, ids, message_id, force_send=False, user_signature=True, context=None):
+ message = self.pool['mail.message'].browse(cr, SUPERUSER_ID, message_id, context=context)
+
+ # compute partners
+ email_pids = self.get_partners_to_email(cr, uid, ids, message, context=None)
+ if not email_pids:
+ return True
+
+ # compute email body (signature, company data)
+ body_html = message.body
+ user_id = message.author_id and message.author_id.user_ids and message.author_id.user_ids[0] and message.author_id.user_ids[0].id or None
if user_signature:
- signature_company = self.get_signature_footer(cr, uid, user_id, res_model=msg.model, res_id=msg.res_id, context=context)
+ signature_company = self.get_signature_footer(cr, uid, user_id, res_model=message.model, res_id=message.res_id, context=context)
body_html = tools.append_content_to_html(body_html, signature_company, plaintext=False, container_tag='div')
- references = False
- if msg.parent_id:
- references = msg.parent_id.message_id
+ # compute email references
+ references = message.parent_id.message_id if message.parent_id else False
+ # create email values
mail_values = {
- 'mail_message_id': msg.id,
+ 'mail_message_id': message.id,
'auto_delete': True,
'body_html': body_html,
- 'recipient_ids': [(4, id) for id in notify_partner_ids],
+ 'recipient_ids': [(4, id) for id in email_pids],
'references': references,
}
- mail_mail = self.pool.get('mail.mail')
- email_notif_id = mail_mail.create(cr, uid, mail_values, context=context)
-
+ email_notif_id = self.pool.get('mail.mail').create(cr, uid, mail_values, context=context)
if force_send:
- mail_mail.send(cr, uid, [email_notif_id], context=context)
+ self.pool.get('mail.mail').send(cr, uid, [email_notif_id], context=context)
return True
+
+ def _notify(self, cr, uid, message_id, partners_to_notify=None, context=None,
+ force_send=False, user_signature=True):
+ """ Send by email the notification depending on the user preferences
+
+ :param list partners_to_notify: optional list of partner ids restricting
+ the notifications to process
+ :param bool force_send: if True, the generated mail.mail is
+ immediately sent after being created, as if the scheduler
+ was executed for this message only.
+ :param bool user_signature: if True, the generated mail.mail body is
+ the body of the related mail.message with the author's signature
+ """
+ notif_ids = self.search(cr, SUPERUSER_ID, [('message_id', '=', message_id), ('partner_id', 'in', partners_to_notify)], context=context)
+
+ # update or create notifications
+ new_notif_ids = self.update_message_notification(cr, SUPERUSER_ID, notif_ids, message_id, partners_to_notify, context=context)
+
+ # mail_notify_noemail (do not send email) or no partner_ids: do not send, return
+ if context and context.get('mail_notify_noemail'):
+ return True
+
+ # browse as SUPERUSER_ID because of access to res_partner not necessarily allowed
+ self._notify_email(cr, SUPERUSER_ID, new_notif_ids, message_id, force_send, user_signature, context=context)
diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py
index d63025b0912..e3a1ae93342 100644
--- a/addons/mail/mail_message.py
+++ b/addons/mail/mail_message.py
@@ -89,7 +89,6 @@ class mail_message(osv.Model):
def _get_record_name(self, cr, uid, ids, name, arg, context=None):
""" Return the related document name, using name_get. It is done using
SUPERUSER_ID, to be sure to have the record name correctly stored. """
- # return dict.fromkeys(ids, False)
# TDE note: regroup by model/ids, to have less queries to perform
result = dict.fromkeys(ids, False)
for message in self.read(cr, uid, ids, ['model', 'res_id'], context=context):
--
cgit v1.2.3
From 0dfe3a2e677aad8a4c03e215a81787f299707d98 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Tue, 27 Aug 2013 18:18:01 +0200
Subject: [FIX] crm: fixed regression in merge_notify, due to a cleaning in
opportunities merging a few months ago
bzr revid: tde@openerp.com-20130827161801-d8i11n5uq7n4v9r8
---
addons/crm/crm_lead.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py
index 6e8e966f5b0..9d17526c19f 100644
--- a/addons/crm/crm_lead.py
+++ b/addons/crm/crm_lead.py
@@ -636,7 +636,7 @@ class crm_lead(format_address, osv.osv):
# Merge notifications about loss of information
opportunities = [highest]
opportunities.extend(opportunities_rest)
- self._merge_notify(cr, uid, highest, opportunities, context=context)
+ self._merge_notify(cr, uid, highest.id, opportunities, context=context)
# Check if the stage is in the stages of the sales team. If not, assign the stage with the lowest sequence
if merged_data.get('section_id'):
section_stage_ids = self.pool.get('crm.case.stage').search(cr, uid, [('section_ids', 'in', merged_data['section_id']), ('type', '=', merged_data.get('type'))], order='sequence', context=context)
--
cgit v1.2.3
From eaf07ae5c45be7ee3e55bf405556aa50d8a59091 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Wed, 28 Aug 2013 16:08:45 +0200
Subject: [FIX] mail_message: fixed reply_to and email_from computation in
create: take also False values, only undefined values trigger the default one
bzr revid: tde@openerp.com-20130828140845-uuv8ouoto4q7g96j
---
addons/mail/mail_message.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py
index e3a1ae93342..ef3b70bed13 100644
--- a/addons/mail/mail_message.py
+++ b/addons/mail/mail_message.py
@@ -91,6 +91,7 @@ class mail_message(osv.Model):
SUPERUSER_ID, to be sure to have the record name correctly stored. """
# TDE note: regroup by model/ids, to have less queries to perform
result = dict.fromkeys(ids, False)
+ # return result
for message in self.read(cr, uid, ids, ['model', 'res_id'], context=context):
if not message.get('model') or not message.get('res_id') or message['model'] not in self.pool:
continue
@@ -830,11 +831,11 @@ class mail_message(osv.Model):
context = {}
default_starred = context.pop('default_starred', False)
- if not values.get('email_from'): # needed to compute reply_to
+ if 'email_from' not in values: # needed to compute reply_to
values['email_from'] = self._get_default_from(cr, uid, context=context)
if not values.get('message_id'):
values['message_id'] = self._get_message_id(cr, uid, values, context=context)
- if not values.get('reply_to'):
+ if 'reply_to' not in values:
values['reply_to'] = self._get_reply_to(cr, uid, values, context=context)
newid = super(mail_message, self).create(cr, uid, values, context)
--
cgit v1.2.3
From 9ef46123a6c7c7dc1c136bbfcf9eb522cc4cf78c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Wed, 28 Aug 2013 16:09:29 +0200
Subject: [IMP] mail.compose.message, email.template: rendering is now done in
batch.
Also refactored send_mail in mail.compose.message in order to be able to override
mail values without having to intefere with the send_mail behavior.
bzr revid: tde@openerp.com-20130828140929-xe9hbmbo6jxgs9mh
---
addons/email_template/email_template.py | 207 ++++++++++++---------
.../email_template/wizard/mail_compose_message.py | 75 ++++----
addons/mail/wizard/mail_compose_message.py | 188 ++++++++++++-------
3 files changed, 282 insertions(+), 188 deletions(-)
diff --git a/addons/email_template/email_template.py b/addons/email_template/email_template.py
index 2e3405ed079..2e1c52d0ef9 100644
--- a/addons/email_template/email_template.py
+++ b/addons/email_template/email_template.py
@@ -67,7 +67,7 @@ class email_template(osv.osv):
_description = 'Email Templates'
_order = 'name'
- def render_template(self, cr, uid, template, model, res_id, context=None):
+ def render_template_batch(self, cr, uid, template, model, res_ids, context=None):
"""Render the given template text, replace mako expressions ``${expr}``
with the result of evaluating these expressions with
an evaluation context containing:
@@ -79,46 +79,60 @@ class email_template(osv.osv):
:param str template: the template text to render
:param str model: model name of the document record this mail is related to.
- :param int res_id: id of the document record this mail is related to.
+ :param int res_ids: list of ids of document records those mails are related to.
"""
- if not template:
- return u""
if context is None:
context = {}
+ results = dict.fromkeys(res_ids, u"")
+
+ # try to load the template
try:
- template = tools.ustr(template)
- record = None
- if res_id:
- record = self.pool[model].browse(cr, uid, res_id, context=context)
- user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
- variables = {
- 'object': record,
- 'user': user,
- 'ctx': context, # context kw would clash with mako internals
- }
- result = mako_template_env.from_string(template).render(variables)
- if result == u"False":
- result = u""
- return result
+ template = mako_template_env.from_string(tools.ustr(template))
except Exception:
- _logger.exception("failed to render mako template value %r", template)
- return u""
-
- def get_email_template(self, cr, uid, template_id=False, record_id=None, context=None):
+ _logger.exception("Failed to load template %r", template)
+ return results
+
+ # prepare template variables
+ user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
+ records = self.pool[model].browse(cr, uid, res_ids, context=context) or [None]
+ variables = {
+ 'user': user,
+ 'ctx': context, # context kw would clash with mako internals
+ }
+ for record in records:
+ res_id = record.id if record else None
+ variables['object'] = record
+ try:
+ render_result = template.render(variables)
+ except Exception:
+ _logger.exception("Failed to render template %r using values %r" % (template, variables))
+ render_result = u""
+ if render_result == u"False":
+ render_result = u""
+ results[res_id] = render_result
+ return results
+
+ def get_email_template_batch(self, cr, uid, template_id=False, res_ids=None, context=None):
if context is None:
context = {}
+ if res_ids is None:
+ res_ids = [None]
+ results = dict.fromkeys(res_ids, False)
+
if not template_id:
- return False
+ return results
template = self.browse(cr, uid, template_id, context)
- lang = self.render_template(cr, uid, template.lang, template.model, record_id, context)
- if lang:
- # Use translated template if necessary
- ctx = context.copy()
- ctx['lang'] = lang
- template = self.browse(cr, uid, template.id, ctx)
- else:
- template = self.browse(cr, uid, int(template_id), context)
- return template
+ langs = self.render_template_batch(cr, uid, template.lang, template.model, res_ids, context)
+ for res_id, lang in langs.iteritems():
+ if lang:
+ # Use translated template if necessary
+ ctx = context.copy()
+ ctx['lang'] = lang
+ template = self.browse(cr, uid, template.id, ctx)
+ else:
+ template = self.browse(cr, uid, int(template_id), context)
+ results[res_id] = template
+ return results
def onchange_model_id(self, cr, uid, ids, model_id, context=None):
mod_name = False
@@ -300,64 +314,75 @@ class email_template(osv.osv):
})
return {'value': result}
- def generate_email(self, cr, uid, template_id, res_id, context=None):
- """Generates an email from the template for given (model, res_id) pair.
-
- :param template_id: id of the template to render.
- :param res_id: id of the record to use for rendering the template (model
- is taken from template definition)
- :returns: a dict containing all relevant fields for creating a new
- mail.mail entry, with one extra key ``attachments``, in the
- format expected by :py:meth:`mail_thread.message_post`.
+ def generate_email_batch(self, cr, uid, template_id, res_ids, context=None, fields=None):
+ """Generates an email from the template for given the given model based on
+ records given by res_ids.
+
+ :param template_id: id of the template to render.
+ :param res_id: id of the record to use for rendering the template (model
+ is taken from template definition)
+ :returns: a dict containing all relevant fields for creating a new
+ mail.mail entry, with one extra key ``attachments``, in the
+ format expected by :py:meth:`mail_thread.message_post`.
"""
if context is None:
context = {}
+ if fields is None:
+ fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to']
+
report_xml_pool = self.pool.get('ir.actions.report.xml')
- template = self.get_email_template(cr, uid, template_id, res_id, context)
- values = {}
- for field in ['subject', 'body_html', 'email_from',
- 'email_to', 'partner_to', 'email_cc', 'reply_to']:
- values[field] = self.render_template(cr, uid, getattr(template, field),
- template.model, res_id, context=context) \
- or False
- if template.user_signature:
- signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature
- values['body_html'] = tools.append_content_to_html(values['body_html'], signature)
-
- if values['body_html']:
- values['body'] = tools.html_sanitize(values['body_html'])
-
- values.update(mail_server_id=template.mail_server_id.id or False,
- auto_delete=template.auto_delete,
- model=template.model,
- res_id=res_id or False)
-
- attachments = []
- # Add report in attachments
- if template.report_template:
- report_name = self.render_template(cr, uid, template.report_name, template.model, res_id, context=context)
- report_service = report_xml_pool.browse(cr, uid, template.report_template.id, context).report_name
- # Ensure report is rendered using template's language
- ctx = context.copy()
- if template.lang:
- ctx['lang'] = self.render_template(cr, uid, template.lang, template.model, res_id, context)
- result, format = openerp.report.render_report(cr, uid, [res_id], report_service, {'model': template.model}, ctx)
- result = base64.b64encode(result)
- if not report_name:
- report_name = 'report.' + report_service
- ext = "." + format
- if not report_name.endswith(ext):
- report_name += ext
- attachments.append((report_name, result))
-
- attachment_ids = []
- # Add template attachments
- for attach in template.attachment_ids:
- attachment_ids.append(attach.id)
-
- values['attachments'] = attachments
- values['attachment_ids'] = attachment_ids
- return values
+ res_ids_to_templates = self.get_email_template_batch(cr, uid, template_id, res_ids, context)
+
+ # templates: res_id -> template; template -> res_ids
+ templates_to_res_ids = {}
+ for res_id, template in res_ids_to_templates.iteritems():
+ templates_to_res_ids.setdefault(template, []).append(res_id)
+
+ results = dict()
+ for template, template_res_ids in templates_to_res_ids.iteritems():
+ # generate fields value for all res_ids linked to the current template
+ for field in ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to']:
+ generated_field_values = self.render_template_batch(cr, uid, getattr(template, field), template.model, template_res_ids, context=context)
+ for res_id, field_value in generated_field_values.iteritems():
+ results.setdefault(res_id, dict())[field] = field_value
+ # update values for all res_ids
+ for res_id in template_res_ids:
+ values = results[res_id]
+ if template.user_signature:
+ signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature
+ values['body_html'] = tools.append_content_to_html(values['body_html'], signature)
+ if values['body_html']:
+ values['body'] = tools.html_sanitize(values['body_html'])
+ values.update(
+ mail_server_id=template.mail_server_id.id or False,
+ auto_delete=template.auto_delete,
+ model=template.model,
+ res_id=res_id or False,
+ attachment_ids=[attach.id for attach in template.attachment_ids],
+ )
+
+ # Add report in attachments
+ if template.report_template:
+ for res_id in template_res_ids:
+ attachments = []
+ report_name = self.render_template(cr, uid, template.report_name, template.model, res_id, context=context)
+ report_service = report_xml_pool.browse(cr, uid, template.report_template.id, context).report_name
+ # Ensure report is rendered using template's language
+ ctx = context.copy()
+ if template.lang:
+ ctx['lang'] = self.render_template_batch(cr, uid, template.lang, template.model, res_id, context) # take 0 ?
+ result, format = openerp.report.render_report(cr, uid, [res_id], report_service, {'model': template.model}, ctx)
+ result = base64.b64encode(result)
+ if not report_name:
+ report_name = 'report.' + report_service
+ ext = "." + format
+ if not report_name.endswith(ext):
+ report_name += ext
+ attachments.append((report_name, result))
+
+ values['attachments'] = attachments
+
+ return results
def send_mail(self, cr, uid, template_id, res_id, force_send=False, raise_exception=False, context=None):
"""Generates a new mail message for the given template and record,
@@ -404,4 +429,14 @@ class email_template(osv.osv):
mail_mail.send(cr, uid, [msg_id], raise_exception=raise_exception, context=context)
return msg_id
+ # Compatibility method
+ def render_template(self, cr, uid, template, model, res_id, context=None):
+ return self.render_template_batch(cr, uid, template, model, [res_id], context)[res_id]
+
+ def get_email_template(self, cr, uid, template_id=False, record_id=None, context=None):
+ return self.get_email_template_batch(cr, uid, template_id, [record_id], context)[record_id]
+
+ def generate_email(self, cr, uid, template_id, res_id, context=None):
+ return self.generate_email_batch(cr, uid, template_id, [res_id], context)[res_id]
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/email_template/wizard/mail_compose_message.py b/addons/email_template/wizard/mail_compose_message.py
index 0aab765efbf..bae7a0d8f9c 100644
--- a/addons/email_template/wizard/mail_compose_message.py
+++ b/addons/email_template/wizard/mail_compose_message.py
@@ -62,6 +62,7 @@ class mail_compose_message(osv.TransientModel):
for wizard in self.browse(cr, uid, ids, context=context):
if wizard.template_id:
wizard_context['mail_notify_user_signature'] = False # template user_signature is added when generating body_html
+ wizard_context['mail_auto_delete'] = wizard.template_id.auto_delete # mass mailing: use template auto_delete value -> note, for emails mass mailing only
if not wizard.attachment_ids or wizard.composition_mode == 'mass_mail' or not wizard.template_id:
continue
new_attachment_ids = []
@@ -81,7 +82,7 @@ class mail_compose_message(osv.TransientModel):
template_values = self.pool.get('email.template').read(cr, uid, template_id, fields, context)
values = dict((field, template_values[field]) for field in fields if template_values.get(field))
elif template_id:
- values = self.generate_email_for_composer(cr, uid, template_id, res_id, context=context)
+ values = self.generate_email_for_composer_batch(cr, uid, template_id, [res_id], context=context)[res_id]
# transform attachments into attachment_ids; not attached to the document because this will
# be done further in the posting process, allowing to clean database if email not send
values['attachment_ids'] = values.pop('attachment_ids', [])
@@ -147,45 +148,55 @@ class mail_compose_message(osv.TransientModel):
partner_ids.append(int(partner_id))
return partner_ids
- def generate_email_for_composer(self, cr, uid, template_id, res_id, context=None):
+ def generate_email_for_composer_batch(self, cr, uid, template_id, res_ids, context=None):
""" Call email_template.generate_email(), get fields relevant for
mail.compose.message, transform email_cc and email_to into partner_ids """
- template_values = self.pool.get('email.template').generate_email(cr, uid, template_id, res_id, context=context)
# filter template values
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'attachments', 'mail_server_id']
- values = dict((field, template_values[field]) for field in fields if template_values.get(field))
- values['body'] = values.pop('body_html', '')
-
- # transform email_to, email_cc into partner_ids
- ctx = dict((k, v) for k, v in (context or {}).items() if not k.startswith('default_'))
- partner_ids = self._get_or_create_partners_from_values(cr, uid, values, context=ctx)
- # legacy template behavior: void values do not erase existing values and the
- # related key is removed from the values dict
- if partner_ids:
- values['partner_ids'] = list(partner_ids)
-
+ values = dict.fromkeys(res_ids, False)
+
+ template_values = self.pool.get('email.template').generate_email_batch(cr, uid, template_id, res_ids, context=context)
+ for res_id in res_ids:
+ res_id_values = dict((field, template_values[res_id][field]) for field in fields if template_values[res_id].get(field))
+ res_id_values['body'] = res_id_values.pop('body_html', '')
+
+ # transform email_to, email_cc into partner_ids
+ ctx = dict((k, v) for k, v in (context or {}).items() if not k.startswith('default_'))
+ partner_ids = self._get_or_create_partners_from_values(cr, uid, res_id_values, context=ctx)
+ # legacy template behavior: void values do not erase existing values and the
+ # related key is removed from the values dict
+ if partner_ids:
+ res_id_values['partner_ids'] = list(partner_ids)
+
+ values[res_id] = res_id_values
return values
- def render_message(self, cr, uid, wizard, res_id, context=None):
+ def render_message_batch(self, cr, uid, wizard, res_ids, context=None):
""" Override to handle templates. """
- # generate the composer email
+ # generate template-based values
if wizard.template_id:
- values = self.generate_email_for_composer(cr, uid, wizard.template_id.id, res_id, context=context)
+ template_values = self.generate_email_for_composer_batch(cr, uid, wizard.template_id.id, res_ids, context=context)
else:
- values = {}
- # remove attachments as they should not be rendered
- values.pop('attachment_ids', None)
- # get values to return
- email_dict = super(mail_compose_message, self).render_message(cr, uid, wizard, res_id, context)
- # those values are not managed; they are readonly
- email_dict.pop('email_to', None)
- email_dict.pop('email_cc', None)
- email_dict.pop('partner_to', None)
- # update template values by wizard values
- values.update(email_dict)
- return values
-
- def render_template(self, cr, uid, template, model, res_id, context=None):
- return self.pool.get('email.template').render_template(cr, uid, template, model, res_id, context=context)
+ template_values = dict.fromkeys(res_ids, dict())
+ # generate composer values
+ composer_values = super(mail_compose_message, self).render_message_batch(cr, uid, wizard, res_ids, context)
+
+ for res_id in res_ids:
+ # remove attachments from template values as they should not be rendered
+ template_values[res_id].pop('attachment_ids', None)
+ # remove some keys from composer that are readonly
+ composer_values[res_id].pop('email_to', None)
+ composer_values[res_id].pop('email_cc', None)
+ composer_values[res_id].pop('partner_to', None)
+ # update template values by composer values
+ template_values[res_id].update(composer_values[res_id])
+ return template_values
+
+ def render_template_batch(self, cr, uid, template, model, res_ids, context=None):
+ return self.pool.get('email.template').render_template_batch(cr, uid, template, model, res_ids, context=context)
+
+ # Compatibility methods
+ def generate_email_for_composer(self, cr, uid, template_id, res_id, context=None):
+ return self.generate_email_for_composer_batch(cr, uid, template_id, [res_id], context)[res_id]
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index 6ab1f0e81ff..34c3db4de9b 100644
--- a/addons/mail/wizard/mail_compose_message.py
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -233,7 +233,11 @@ class mail_compose_message(osv.TransientModel):
email(s), rendering any template patterns on the fly if needed. """
if context is None:
context = {}
- ir_attachment_obj = self.pool.get('ir.attachment')
+ # clean the context (hint: mass mailing sets some default values that
+ # could be wrongly interpreted by mail_mail)
+ context.pop('default_email_to', None)
+ context.pop('default_partner_ids', None)
+
active_ids = context.get('active_ids')
is_log = context.get('mail_compose_log', False)
@@ -252,43 +256,11 @@ class mail_compose_message(osv.TransientModel):
else:
res_ids = [wizard.res_id]
- for res_id in res_ids:
- # mail.message values, according to the wizard options
- post_values = {
- 'subject': wizard.subject,
- 'body': wizard.body,
- 'parent_id': wizard.parent_id and wizard.parent_id.id,
- 'partner_ids': [partner.id for partner in wizard.partner_ids],
- 'attachment_ids': [attach.id for attach in wizard.attachment_ids],
- }
- # mass mailing: render and override default values
- if mass_mail_mode and wizard.model:
- email_dict = self.render_message(cr, uid, wizard, res_id, context=context)
- post_values['partner_ids'] += email_dict.pop('partner_ids', [])
- post_values['attachments'] = email_dict.pop('attachments', [])
- attachment_ids = []
- for attach_id in post_values.pop('attachment_ids'):
- new_attach_id = ir_attachment_obj.copy(cr, uid, attach_id, {'res_model': self._name, 'res_id': wizard.id}, context=context)
- attachment_ids.append(new_attach_id)
- post_values['attachment_ids'] = attachment_ids
- # email_from: mass mailing only can specify another email_from
- if email_dict.get('email_from'):
- post_values['email_from'] = email_dict.pop('email_from')
- # replies redirection: mass mailing only
- if not wizard.same_thread:
- post_values['reply_to'] = email_dict.pop('reply_to')
- else:
- email_dict.pop('reply_to')
- post_values.update(email_dict)
- # clean the context (hint: mass mailing sets some default values that
- # could be wrongly interpreted by mail_mail)
- context.pop('default_email_to', None)
- context.pop('default_partner_ids', None)
- # post the message
+ all_mail_values = self.get_mail_values(cr, uid, wizard, res_ids, context=context)
+
+ for res_id, mail_values in all_mail_values.iteritems():
if mass_mail_mode and not wizard.post:
- post_values['body_html'] = post_values.get('body', '')
- post_values['recipient_ids'] = [(4, id) for id in post_values.pop('partner_ids', [])]
- self.pool.get('mail.mail').create(cr, uid, post_values, context=context)
+ self.pool.get('mail.mail').create(cr, uid, mail_values, context=context)
else:
subtype = 'mail.mt_comment'
if is_log: # log a note: subtype is False
@@ -297,46 +269,122 @@ class mail_compose_message(osv.TransientModel):
if not wizard.notify:
subtype = False
context = dict(context,
- mail_notify_force_send=False, # do not send emails directly but use the queue instead
- mail_create_nosubscribe=True) # add context key to avoid subscribing the author
- active_model_pool.message_post(cr, uid, [res_id], type='comment', subtype=subtype, context=context, **post_values)
+ mail_notify_force_send=False, # do not send emails directly but use the queue instead
+ mail_create_nosubscribe=True) # add context key to avoid subscribing the author
+ active_model_pool.message_post(cr, uid, [res_id], type='comment', subtype=subtype, context=context, **mail_values)
return {'type': 'ir.actions.act_window_close'}
- def render_message(self, cr, uid, wizard, res_id, context=None):
- """ Generate an email from the template for given (wizard.model, res_id)
- pair. This method is meant to be inherited by email_template that
- will produce a more complete dictionary. """
- return {
- 'subject': self.render_template(cr, uid, wizard.subject, wizard.model, res_id, context),
- 'body': self.render_template(cr, uid, wizard.body, wizard.model, res_id, context),
- 'email_from': self.render_template(cr, uid, wizard.email_from, wizard.model, res_id, context),
- 'reply_to': self.render_template(cr, uid, wizard.reply_to, wizard.model, res_id, context),
- }
-
- def render_template(self, cr, uid, template, model, res_id, context=None):
+ def get_mail_values(self, cr, uid, wizard, res_ids, context=None):
+ """Generate the values that will be used by send_mail to create mail_messages
+ or mail_mails. """
+ results = dict.fromkeys(res_ids, False)
+ mass_mail_mode = wizard.composition_mode == 'mass_mail'
+
+ # render all template-based value at once
+ if mass_mail_mode and wizard.model:
+ rendered_values = self.render_message_batch(cr, uid, wizard, res_ids, context=context)
+
+ for res_id in res_ids:
+ # static wizard (mail.message) values
+ mail_values = {
+ 'subject': wizard.subject,
+ 'body': wizard.body,
+ 'parent_id': wizard.parent_id and wizard.parent_id.id,
+ 'partner_ids': [partner.id for partner in wizard.partner_ids],
+ 'attachment_ids': [attach.id for attach in wizard.attachment_ids],
+ }
+ # mass mailing: rendering override wizard static values
+ if mass_mail_mode and wizard.model:
+ email_dict = rendered_values[res_id]
+ mail_values['partner_ids'] += email_dict.pop('partner_ids', [])
+ mail_values['attachments'] = email_dict.pop('attachments', [])
+ attachment_ids = []
+ for attach_id in mail_values.pop('attachment_ids'):
+ new_attach_id = self.pool.get('ir.attachment').copy(cr, uid, attach_id, {'res_model': self._name, 'res_id': wizard.id}, context=context)
+ attachment_ids.append(new_attach_id)
+ mail_values['attachment_ids'] = attachment_ids
+ # email_from: mass mailing only can specify another email_from
+ if email_dict.get('email_from'):
+ mail_values['email_from'] = email_dict.pop('email_from')
+ # replies redirection: mass mailing only
+ if not wizard.same_thread:
+ mail_values['reply_to'] = email_dict.pop('reply_to')
+ else:
+ email_dict.pop('reply_to')
+ mail_values.update(email_dict)
+ # mass mailing without post: mail_mail values
+ if mass_mail_mode and not wizard.post:
+ if 'mail_auto_delete' in context:
+ mail_values['auto_delete'] = context.get('mail_auto_delete')
+ mail_values['body_html'] = mail_values.get('body', '')
+ mail_values['recipient_ids'] = [(4, id) for id in mail_values.pop('partner_ids', [])]
+ results[res_id] = mail_values
+ return results
+
+ def render_message_batch(self, cr, uid, wizard, res_ids, context=None):
+ """Generate template-based values of wizard, for the document records given
+ by res_ids. This method is meant to be inherited by email_template that
+ will produce a more complete dictionary, using Jinja2 templates.
+
+ Each template is generated for all res_ids, allowing to parse the template
+ once, and render it multiple times. This is useful for mass mailing where
+ template rendering represent a significant part of the process.
+
+ :param browse wizard: current mail.compose.message browse record
+ :param list res_ids: list of record ids
+
+ :return dict results: for each res_id, the generated template values for
+ subject, body, email_from and reply_to
+ """
+ subjects = self.render_template_batch(cr, uid, wizard.subject, wizard.model, res_ids, context)
+ bodies = self.render_template_batch(cr, uid, wizard.body, wizard.model, res_ids, context)
+ emails_from = self.render_template_batch(cr, uid, wizard.email_from, wizard.model, res_ids, context)
+ replies_to = self.render_template_batch(cr, uid, wizard.reply_to, wizard.model, res_ids, context)
+
+ results = dict.fromkeys(res_ids, False)
+ for res_id in res_ids:
+ results[res_id] = {
+ 'subject': subjects[res_id],
+ 'body': bodies[res_id],
+ 'email_from': emails_from[res_id],
+ 'reply_to': replies_to[res_id],
+ }
+ return results
+
+ def render_template_batch(self, cr, uid, template, model, res_ids, context=None):
""" Render the given template text, replace mako-like expressions ``${expr}``
- with the result of evaluating these expressions with an evaluation context
- containing:
+ with the result of evaluating these expressions with an evaluation context
+ containing:
- * ``user``: browse_record of the current user
- * ``object``: browse_record of the document record this mail is
- related to
- * ``context``: the context passed to the mail composition wizard
+ * ``user``: browse_record of the current user
+ * ``object``: browse_record of the document record this mail is
+ related to
+ * ``context``: the context passed to the mail composition wizard
- :param str template: the template text to render
- :param str model: model name of the document record this mail is related to.
- :param int res_id: id of the document record this mail is related to.
+ :param str template: the template text to render
+ :param str model: model name of the document record this mail is related to
+ :param list res_ids: list of record ids
"""
if context is None:
context = {}
-
- def merge(match):
- exp = str(match.group()[2:-1]).strip()
- result = eval(exp, {
- 'user': self.pool.get('res.users').browse(cr, uid, uid, context=context),
- 'object': self.pool[model].browse(cr, uid, res_id, context=context),
- 'context': dict(context), # copy context to prevent side-effects of eval
+ results = dict.fromkeys(res_ids, False)
+
+ for res_id in res_ids:
+ def merge(match):
+ exp = str(match.group()[2:-1]).strip()
+ result = eval(exp, {
+ 'user': self.pool.get('res.users').browse(cr, uid, uid, context=context),
+ 'object': self.pool[model].browse(cr, uid, res_id, context=context),
+ 'context': dict(context), # copy context to prevent side-effects of eval
})
- return result and tools.ustr(result) or ''
- return template and EXPRESSION_PATTERN.sub(merge, template)
+ return result and tools.ustr(result) or ''
+ results[res_id] = template and EXPRESSION_PATTERN.sub(merge, template)
+ return results
+
+ # Compatibility methods
+ def render_template(self, cr, uid, template, model, res_id, context=None):
+ return self.render_template_batch(cr, uid, template, model, [res_id], context)[res_id]
+
+ def render_message(self, cr, uid, wizard, res_id, context=None):
+ return self.render_message_batch(cr, uid, wizard, [res_id], context)[res_id]
--
cgit v1.2.3
From 14a855f297a02765bf8ae1adc13bdb57750560f2 Mon Sep 17 00:00:00 2001
From: "Atul Patel (OpenERP)"
Date: Wed, 28 Aug 2013 20:31:05 +0530
Subject: [FIX]: Remove onchange from xml part.
bzr revid: atp@tinyerp.com-20130828150105-ylwdzki1viq9n8kl
---
addons/base_setup/res_config_view.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/base_setup/res_config_view.xml b/addons/base_setup/res_config_view.xml
index cc290f59156..8004d444ed9 100644
--- a/addons/base_setup/res_config_view.xml
+++ b/addons/base_setup/res_config_view.xml
@@ -94,7 +94,7 @@
-
+
--
cgit v1.2.3
From f70313103a082044d21e4b2290348bfe4d398bc3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Thu, 29 Aug 2013 12:09:32 +0200
Subject: [IMP] mail_message: cleaned search view
bzr revid: tde@openerp.com-20130829100932-xrh11khzghwxzyuf
---
addons/mail/mail_message_view.xml | 19 +++++--------------
1 file changed, 5 insertions(+), 14 deletions(-)
diff --git a/addons/mail/mail_message_view.xml b/addons/mail/mail_message_view.xml
index e4442e366e3..e2d1deb028b 100644
--- a/addons/mail/mail_message_view.xml
+++ b/addons/mail/mail_message_view.xml
@@ -56,7 +56,8 @@
25
-
+
+
@@ -66,23 +67,13 @@
-
-
-
-
-
+
+
+
--
cgit v1.2.3
From e0ceeb8214f7c821498569e6a76f423e37eea927 Mon Sep 17 00:00:00 2001
From: "Mehul Mehta (OpenERP)"
Date: Thu, 29 Aug 2013 21:52:42 +0800
Subject: [IMP]in general setting for select dynamic font and set a font for
company header and footer
bzr revid: mme@tinyerp.com-20130829135242-ov2b3v1yvbtvliv2
---
addons/base_setup/res_config.py | 26 ++++++++------------------
addons/base_setup/res_config_view.xml | 2 +-
2 files changed, 9 insertions(+), 19 deletions(-)
diff --git a/addons/base_setup/res_config.py b/addons/base_setup/res_config.py
index 753c063ed34..bbaa6713c82 100644
--- a/addons/base_setup/res_config.py
+++ b/addons/base_setup/res_config.py
@@ -21,24 +21,14 @@
from openerp.osv import fields, osv
import re
+import matplotlib.font_manager
-_select_font=[ ('DejaVu Sans',"DejaVu Sans"),
- ('DejaVu Sans Bold',"DejaVu Sans Bold"),
- ('DejaVu Sans Oblique',"DejaVu Sans Oblique"),
- ('DejaVu Sans BoldOblique',"DejaVu Sans BoldOblique"),
- ('Liberation Serif',"Liberation Serif"),
- ('Liberation Serif Bold',"Liberation Serif Bold"),
- ('Liberation Serif Italic',"Liberation Serif Italic"),
- ('Liberation Serif BoldItalic',"Liberation Serif BoldItalic"),
- ('Liberation Serif',"Liberation Serif"),
- ('Liberation Serif Bold',"Liberation Serif Bold"),
- ('Liberation Serif Italic',"Liberation Serif Italic"),
- ('Liberation Serif BoldItalic',"Liberation Serif BoldItalic"),
- ('FreeMono',"FreeMono"),
- ('FreeMono Bold',"FreeMono Bold"),
- ('FreeMono Oblique',"FreeMono Oblique"),
- ('FreeMono BoldOblique',"FreeMono BoldOblique"),
-]
+_lst_font=[]
+for i in matplotlib.font_manager.findSystemFonts(fontpaths=None, fontext='ttf'):
+ m=re.sub('(.*/)','', i)
+ n=m.strip('.ttf')
+ n=n.replace('-',' ')
+ _lst_font.append((n,n))
class base_config_settings(osv.osv_memory):
_name = 'base.config.settings'
@@ -57,7 +47,7 @@ class base_config_settings(osv.osv_memory):
'module_base_import': fields.boolean("Allow users to import data from CSV files"),
'module_google_drive': fields.boolean('Attach Google documents to any record',
help="""This installs the module google_docs."""),
- 'font': fields.selection(_select_font, "Select Font",help="Set your favorite font into company header"),
+ 'font': fields.selection(_lst_font, "Select Font",help="Set your favorite font into company header"),
}
def open_company(self, cr, uid, ids, context=None):
user = self.pool.get('res.users').browse(cr, uid, uid, context)
diff --git a/addons/base_setup/res_config_view.xml b/addons/base_setup/res_config_view.xml
index 8004d444ed9..e9be52c66d8 100644
--- a/addons/base_setup/res_config_view.xml
+++ b/addons/base_setup/res_config_view.xml
@@ -91,7 +91,7 @@
-
+
--
cgit v1.2.3
From 80da7ddd6029e4d29684c9b15ea79defe45f8d0f Mon Sep 17 00:00:00 2001
From: "Darshan Kalola (OpenERP)"
Date: Fri, 30 Aug 2013 15:15:12 +0530
Subject: [IMP]Add - before the the sentence This installs the module in help
bzr revid: dka@tinyerp.com-20130830094512-n3nqjtt1g0w8m94v
---
addons/account/res_config.py | 12 ++++++------
addons/base_setup/res_config.py | 6 +++---
addons/crm/res_config.py | 2 +-
addons/hr_recruitment/res_config.py | 2 +-
addons/knowledge/res_config.py | 6 +++---
addons/marketing/res_config.py | 6 +++---
addons/mrp/res_config.py | 12 ++++++------
addons/project/res_config.py | 12 ++++++------
addons/purchase/res_config.py | 6 +++---
addons/sale/res_config.py | 12 ++++++------
addons/sale_stock/res_config.py | 4 ++--
addons/stock/res_config.py | 6 +++---
12 files changed, 43 insertions(+), 43 deletions(-)
diff --git a/addons/account/res_config.py b/addons/account/res_config.py
index 1d79fa0ae11..9349818a47e 100644
--- a/addons/account/res_config.py
+++ b/addons/account/res_config.py
@@ -82,30 +82,30 @@ class account_config_settings(osv.osv_memory):
'module_account_check_writing': fields.boolean('Pay your suppliers by check',
help='This allows you to check writing and printing.\n'
- 'This installs the module account_check_writing.'),
+ '-This installs the module account_check_writing.'),
'module_account_accountant': fields.boolean('Full accounting features: journals, legal statements, chart of accounts, etc.',
help="""If you do not check this box, you will be able to do invoicing & payments, but not accounting (Journal Items, Chart of Accounts, ...)"""),
'module_account_asset': fields.boolean('Assets management',
help='This allows you to manage the assets owned by a company or a person.\n'
'It keeps track of the depreciation occurred on those assets, and creates account move for those depreciation lines.\n'
- 'This installs the module account_asset. If you do not check this box, you will be able to do invoicing & payments, '
+ '-This installs the module account_asset. If you do not check this box, you will be able to do invoicing & payments, '
'but not accounting (Journal Items, Chart of Accounts, ...)'),
'module_account_budget': fields.boolean('Budget management',
help='This allows accountants to manage analytic and crossovered budgets. '
'Once the master budgets and the budgets are defined, '
'the project managers can set the planned amount on each analytic account.\n'
- 'This installs the module account_budget.'),
+ '-This installs the module account_budget.'),
'module_account_payment': fields.boolean('Manage payment orders',
help='This allows you to create and manage your payment orders, with purposes to \n'
'* serve as base for an easy plug-in of various automated payment mechanisms, and \n'
'* provide a more efficient way to manage invoice payments.\n'
- 'This installs the module account_payment.' ),
+ '-This installs the module account_payment.' ),
'module_account_voucher': fields.boolean('Manage customer payments',
help='This includes all the basic requirements of voucher entries for bank, cash, sales, purchase, expense, contra, etc.\n'
- 'This installs the module account_voucher.'),
+ '-This installs the module account_voucher.'),
'module_account_followup': fields.boolean('Manage customer payment follow-ups',
help='This allows to automate letters for unpaid invoices, with multi-level recalls.\n'
- 'This installs the module account_followup.'),
+ '-This installs the module account_followup.'),
'group_proforma_invoices': fields.boolean('Allow pro-forma invoices',
implied_group='account.group_proforma_invoices',
help="Allows you to put invoices in pro-forma state."),
diff --git a/addons/base_setup/res_config.py b/addons/base_setup/res_config.py
index c7f6d4245ec..42e17d9c704 100644
--- a/addons/base_setup/res_config.py
+++ b/addons/base_setup/res_config.py
@@ -27,7 +27,7 @@ class base_config_settings(osv.osv_memory):
_columns = {
'module_multi_company': fields.boolean('Manage multiple companies',
help='Work in multi-company environments, with appropriate security access between companies.\n'
- 'This installs the module multi_company.'),
+ '-This installs the module multi_company.'),
'module_share': fields.boolean('Allow documents sharing',
help="""Share or embbed any screen of openerp."""),
'module_portal': fields.boolean('Activate the customer portal',
@@ -69,12 +69,12 @@ class sale_config_settings(osv.osv_memory):
'attach the selected mail as a .eml file in '
'the attachment of a selected record. You can create documents for CRM Lead, '
'Partner from the selected emails.\n'
- 'This installs the module plugin_thunderbird.'),
+ '-This installs the module plugin_thunderbird.'),
'module_plugin_outlook': fields.boolean('Enable Outlook plug-in',
help='The Outlook plugin allows you to select an object that you would like to add '
'to your email and its attachments from MS Outlook. You can select a partner, '
'or a lead object and archive a selected email into an OpenERP mail message with attachments.\n'
- 'This installs the module plugin_outlook.'),
+ '-This installs the module plugin_outlook.'),
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/crm/res_config.py b/addons/crm/res_config.py
index 622af04ef75..d572e50059a 100644
--- a/addons/crm/res_config.py
+++ b/addons/crm/res_config.py
@@ -59,7 +59,7 @@ class crm_configuration(osv.TransientModel):
help="""Allows you to trace and manage your activities for fund raising."""),
'module_crm_claim': fields.boolean("Manage Customer Claims",
help='Allows you to track your customers/suppliers claims and grievances.\n'
- 'This installs the module crm_claim.'),
+ '-This installs the module crm_claim.'),
'module_crm_helpdesk': fields.boolean("Manage Helpdesk and Support",
help="""Allows you to communicate with Customer, process Customer query, and provide better help and support. This installs the module crm_helpdesk."""),
'group_multi_salesteams': fields.boolean("Organize Sales activities into multiple Sales Teams",
diff --git a/addons/hr_recruitment/res_config.py b/addons/hr_recruitment/res_config.py
index a15ae541e7f..53fd01546ec 100644
--- a/addons/hr_recruitment/res_config.py
+++ b/addons/hr_recruitment/res_config.py
@@ -28,7 +28,7 @@ class hr_applicant_settings(osv.osv_memory):
_columns = {
'module_document_ftp': fields.boolean('Allow the automatic indexation of resumes',
help='Manage your CV\'s and motivation letter related to all applicants.\n'
- 'This installs the module document_ftp. This will install the knowledge management module in order to allow you to search using specific keywords through the content of all documents (PDF, .DOCx...)'),
+ '-This installs the module document_ftp. This will install the knowledge management module in order to allow you to search using specific keywords through the content of all documents (PDF, .DOCx...)'),
'fetchmail_applicants': fields.boolean('Create applicants from an incoming email account',
fetchmail_model='hr.applicant', fetchmail_name='Incoming HR Applications',
help ='Allow applicants to send their job application to an email address (jobs@mycompany.com), '
diff --git a/addons/knowledge/res_config.py b/addons/knowledge/res_config.py
index fd033772133..0800cd84856 100644
--- a/addons/knowledge/res_config.py
+++ b/addons/knowledge/res_config.py
@@ -30,13 +30,13 @@ class knowledge_config_settings(osv.osv_memory):
'module_document': fields.boolean('Manage documents',
help='This is a complete document management system, with: user authentication, '
'full document search (but pptx and docx are not supported), and a document dashboard.\n'
- 'This installs the module document.'),
+ '-This installs the module document.'),
'module_document_ftp': fields.boolean('Share repositories (FTP)',
help='Access your documents in OpenERP through an FTP interface.\n'
- 'This installs the module document_ftp.'),
+ '-This installs the module document_ftp.'),
'module_document_webdav': fields.boolean('Share repositories (WebDAV)',
help='Access your documents in OpenERP through WebDAV.\n'
- 'This installs the module document_webdav.'),
+ '-This installs the module document_webdav.'),
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/marketing/res_config.py b/addons/marketing/res_config.py
index 3d8eaf71fe6..b0a0679d68e 100644
--- a/addons/marketing/res_config.py
+++ b/addons/marketing/res_config.py
@@ -28,13 +28,13 @@ class marketing_config_settings(osv.osv_memory):
'module_marketing_campaign': fields.boolean('Marketing campaigns',
help='Provides leads automation through marketing campaigns. '
'Campaigns can in fact be defined on any resource, not just CRM leads.\n'
- 'This installs the module marketing_campaign.'),
+ '-This installs the module marketing_campaign.'),
'module_marketing_campaign_crm_demo': fields.boolean('Demo data for marketing campaigns',
help='Installs demo data like leads, campaigns and segments for Marketing Campaigns.\n'
- 'This installs the module marketing_campaign_crm_demo.'),
+ '-This installs the module marketing_campaign_crm_demo.'),
'module_crm_profiling': fields.boolean('Track customer profile to focus your campaigns',
help='Allows users to perform segmentation within partners.\n'
- 'This installs the module crm_profiling.'),
+ '-This installs the module crm_profiling.'),
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mrp/res_config.py b/addons/mrp/res_config.py
index 7f54999e15b..45a3ce41cf6 100644
--- a/addons/mrp/res_config.py
+++ b/addons/mrp/res_config.py
@@ -35,26 +35,26 @@ class mrp_config_settings(osv.osv_memory):
'* Warranty concept\n'
'* Repair quotation report\n'
'* Notes for the technician and for the final customer.\n'
- 'This installs the module mrp_repair.'),
+ '-This installs the module mrp_repair.'),
'module_mrp_operations': fields.boolean("Allow detailed planning of work order",
help='This allows to add state, date_start,date_stop in production order operation lines (in the "Work Centers" tab).\n'
- 'This installs the module mrp_operations.'),
+ '-This installs the module mrp_operations.'),
'module_mrp_byproduct': fields.boolean("Produce several products from one manufacturing order",
help='You can configure by-products in the bill of material.\n'
'Without this module: A + B + C -> D.\n'
'With this module: A + B + C -> D + E.\n'
- 'This installs the module mrp_byproduct.'),
+ '-This installs the module mrp_byproduct.'),
'module_mrp_jit': fields.boolean("Generate procurement in real time",
help='This allows Just In Time computation of procurement orders.\n'
'All procurement orders will be processed immediately, which could in some '
'cases entail a small performance impact.\n'
- 'This installs the module mrp_jit.'),
+ '-This installs the module mrp_jit.'),
'module_stock_no_autopicking': fields.boolean("Manage manual picking to fulfill manufacturing orders ",
help='This module allows an intermediate picking process to provide raw materials to production orders.\n'
'For example to manage production made by your suppliers (sub-contracting).\n'
'To achieve this, set the assembled product which is sub-contracted to "No Auto-Picking" '
'and put the location of the supplier in the routing of the assembly operation.\n'
- 'This installs the module stock_no_autopicking.'),
+ '-This installs the module stock_no_autopicking.'),
'group_mrp_routings': fields.boolean("Manage routings and work orders ",
implied_group='mrp.group_mrp_routings',
help='Routings allow you to create and manage the manufacturing operations that should be followed '
@@ -69,7 +69,7 @@ class mrp_config_settings(osv.osv_memory):
'* Manufacturer Product Name\n'
'* Manufacturer Product Code\n'
'* Product Attributes.\n'
- 'This installs the module product_manufacturer.'),
+ '-This installs the module product_manufacturer.'),
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/project/res_config.py b/addons/project/res_config.py
index a8b8af3b90b..78b05144208 100644
--- a/addons/project/res_config.py
+++ b/addons/project/res_config.py
@@ -31,27 +31,27 @@ class project_configuration(osv.osv_memory):
help ='This feature automatically creates project tasks from service products in sale orders. '
'More precisely, tasks are created for procurement lines with product of type \'Service\', '
'procurement method \'Make to Order\', and supply method \'Manufacture\'.\n'
- 'This installs the module project_mrp.'),
+ '-This installs the module project_mrp.'),
'module_pad': fields.boolean("Use integrated collaborative note pads on task",
help='Lets the company customize which Pad installation should be used to link to new pads '
'(for example: http://ietherpad.com/).\n'
- 'This installs the module pad.'),
+ '-This installs the module pad.'),
'module_project_timesheet': fields.boolean("Record timesheet lines per tasks",
help='This allows you to transfer the entries under tasks defined for Project Management to '
'the timesheet line entries for particular date and user, with the effect of creating, '
'editing and deleting either ways.\n'
- 'This installs the module project_timesheet.'),
+ '-This installs the module project_timesheet.'),
'module_project_long_term': fields.boolean("Manage resources planning on gantt view",
help='A long term project management module that tracks planning, scheduling, and resource allocation.\n'
- 'This installs the module project_long_term.'),
+ '-This installs the module project_long_term.'),
'module_project_issue': fields.boolean("Track issues and bugs",
help='Provides management of issues/bugs in projects.\n'
- 'This installs the module project_issue.'),
+ '-This installs the module project_issue.'),
'time_unit': fields.many2one('product.uom', 'Working time unit', required=True,
help="""This will set the unit of measure used in projects and tasks."""),
'module_project_issue_sheet': fields.boolean("Invoice working time on issues",
help='Provides timesheet support for the issues/bugs management in project.\n'
- 'This installs the module project_issue_sheet.'),
+ '-This installs the module project_issue_sheet.'),
'group_tasks_work_on_tasks': fields.boolean("Log work activities on tasks",
implied_group='project.group_tasks_work_on_tasks',
help="Allows you to compute work on tasks."),
diff --git a/addons/purchase/res_config.py b/addons/purchase/res_config.py
index 930ee5f9f29..82b07a839d2 100644
--- a/addons/purchase/res_config.py
+++ b/addons/purchase/res_config.py
@@ -48,14 +48,14 @@ class purchase_config_settings(osv.osv_memory):
'Supplier: don\'t forget to ask for an express delivery.'),
'module_purchase_double_validation': fields.boolean("Force two levels of approvals",
help='Provide a double validation mechanism for purchases exceeding minimum amount.\n'
- 'This installs the module purchase_double_validation.'),
+ '-This installs the module purchase_double_validation.'),
'module_purchase_requisition': fields.boolean("Manage purchase requisitions",
help='Purchase Requisitions are used when you want to request quotations from several suppliers for a given set of products.\n'
'You can configure per product if you directly do a Request for Quotation '
'to one supplier or if you want a purchase requisition to negotiate with several suppliers.'),
'module_purchase_analytic_plans': fields.boolean('Use multiple analytic accounts on purchase orders',
help ='Allows the user to maintain several analysis plans. These let you split lines on a purchase order between several accounts and analytic plans.\n'
- 'This installs the module purchase_analytic_plans.'),
+ '-This installs the module purchase_analytic_plans.'),
'group_analytic_account_for_purchases': fields.boolean('Analytic accounting for purchases',
implied_group='purchase.group_analytic_accounting',
help="Allows you to specify an analytic account on purchase orders."),
@@ -78,7 +78,7 @@ class account_config_settings(osv.osv_memory):
_columns = {
'module_purchase_analytic_plans': fields.boolean('Use multiple analytic accounts on orders',
help ='Allows the user to maintain several analysis plans. These let you split lines on a purchase order between several accounts and analytic plans.\n'
- 'This installs the module purchase_analytic_plans.'),
+ '-This installs the module purchase_analytic_plans.'),
'group_analytic_account_for_purchases': fields.boolean('Analytic accounting for purchases',
implied_group='purchase.group_analytic_accounting',
help="Allows you to specify an analytic account on purchase orders."),
diff --git a/addons/sale/res_config.py b/addons/sale/res_config.py
index fe81a7cc6d8..7c080d8168f 100644
--- a/addons/sale/res_config.py
+++ b/addons/sale/res_config.py
@@ -36,13 +36,13 @@ class sale_configuration(osv.osv_memory):
'timesheet': fields.boolean('Prepare invoices based on timesheets',
help = 'For modifying account analytic view to show important data to project manager of services companies.'
'You can also view the report of account analytic summary user-wise as well as month wise.\n'
- 'This installs the module account_analytic_analysis.'),
+ '-This installs the module account_analytic_analysis.'),
'module_account_analytic_analysis': fields.boolean('Use contracts management',
help = 'Allows to define your customer contracts conditions: invoicing '
'method (fixed price, on timesheet, advance invoice), the exact pricing '
'(650€/day for a developer), the duration (one year support contract).\n'
'You will be able to follow the progress of the contract and invoice automatically.\n'
- 'It installs the account_analytic_analysis module.'),
+ '-It installs the account_analytic_analysis module.'),
'time_unit': fields.many2one('product.uom', 'The default working time unit for services is'),
'group_sale_pricelist':fields.boolean("Use pricelists to adapt your price per customers",
implied_group='product.group_sale_pricelist',
@@ -64,20 +64,20 @@ Example: 10% for retailers, promotion of 5 EUR on this product, etc."""),
'module_sale_margin': fields.boolean("Display margins on sales orders",
help='This adds the \'Margin\' on sales order.\n'
'This gives the profitability by calculating the difference between the Unit Price and Cost Price.\n'
- 'This installs the module sale_margin.'),
+ '-This installs the module sale_margin.'),
'module_sale_journal': fields.boolean("Allow batch invoicing of delivery orders through journals",
help='Allows you to categorize your sales and deliveries (picking lists) between different journals, '
'and perform batch operations on journals.\n'
- 'This installs the module sale_journal.'),
+ '-This installs the module sale_journal.'),
'module_analytic_user_function': fields.boolean("One employee can have different roles per contract",
help='Allows you to define what is the default function of a specific user on a given account.\n'
'This is mostly used when a user encodes his timesheet. The values are retrieved and the fields are auto-filled. '
'But the possibility to change these values is still available.\n'
- 'This installs the module analytic_user_function.'),
+ '-This installs the module analytic_user_function.'),
'module_project': fields.boolean("Project"),
'module_sale_stock': fields.boolean("Trigger delivery orders automatically from sales orders",
help='Allows you to Make Quotation, Sale Order using different Order policy and Manage Related Stock.\n'
- 'This installs the module sale_stock.'),
+ '-This installs the module sale_stock.'),
}
def default_get(self, cr, uid, fields, context=None):
diff --git a/addons/sale_stock/res_config.py b/addons/sale_stock/res_config.py
index 76cfbcb6fd3..01871bf6f1d 100644
--- a/addons/sale_stock/res_config.py
+++ b/addons/sale_stock/res_config.py
@@ -36,7 +36,7 @@ class sale_configuration(osv.osv_memory):
help='Lets you transfer the entries under tasks defined for Project Management to '
'the Timesheet line entries for particular date and particular user with the effect of creating, editing and deleting either ways '
'and to automatically creates project tasks from procurement lines.\n'
- 'This installs the modules project_timesheet and project_mrp.'),
+ '-This installs the modules project_timesheet and project_mrp.'),
'default_order_policy': fields.selection(
[('manual', 'Invoice based on sales orders'), ('picking', 'Invoice based on deliveries')],
'The default invoicing method is', default_model='sale.order',
@@ -44,7 +44,7 @@ class sale_configuration(osv.osv_memory):
'module_delivery': fields.boolean('Allow adding shipping costs',
help ='Allows you to add delivery methods in sales orders and delivery orders.\n'
'You can define your own carrier and delivery grids for prices.\n'
- 'This installs the module delivery.'),
+ '-This installs the module delivery.'),
'default_picking_policy' : fields.boolean("Deliver all at once when all products are available.",
help = "Sales order by default will be configured to deliver all products at once instead of delivering each product when it is available. This may have an impact on the shipping price."),
'group_mrp_properties': fields.boolean('Product properties on order lines',
diff --git a/addons/stock/res_config.py b/addons/stock/res_config.py
index 4da23389214..1ce5d9d25fa 100644
--- a/addons/stock/res_config.py
+++ b/addons/stock/res_config.py
@@ -28,11 +28,11 @@ class stock_config_settings(osv.osv_memory):
_columns = {
'module_claim_from_delivery': fields.boolean("Allow claim on deliveries",
help='Adds a Claim link to the delivery order.\n'
- 'This installs the module claim_from_delivery.'),
+ '-This installs the module claim_from_delivery.'),
'module_stock_invoice_directly': fields.boolean("Create and open the invoice when the user finish a delivery order",
help='This allows to automatically launch the invoicing wizard if the delivery is '
'to be invoiced when you send or deliver goods.\n'
- 'This installs the module stock_invoice_directly.'),
+ '-This installs the module stock_invoice_directly.'),
'module_product_expiry': fields.boolean("Expiry date on serial numbers",
help="""Track different dates on products and serial numbers.
The following dates can be tracked:
@@ -45,7 +45,7 @@ This installs the module product_expiry."""),
help='Provide push and pull inventory flows. Typical uses of this feature are: '
'manage product manufacturing chains, manage default locations per product, '
'define routes within your warehouse according to business needs, etc.\n'
- 'This installs the module stock_location.'),
+ '-This installs the module stock_location.'),
'group_uom': fields.boolean("Manage different units of measure for products",
implied_group='product.group_uom',
help="""Allows you to select and maintain different units of measure for products."""),
--
cgit v1.2.3
From b3e6145e048b9eefebc6f3d297cecda54269fe0c Mon Sep 17 00:00:00 2001
From: "Darshan Kalola (OpenERP)"
Date: Fri, 30 Aug 2013 16:15:24 +0530
Subject: [IMP]improve help in setting/configuration/sales
bzr revid: dka@tinyerp.com-20130830104524-pbxm2p32n1widzqd
---
addons/crm/res_config.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/addons/crm/res_config.py b/addons/crm/res_config.py
index d572e50059a..c7ad5fb3d7e 100644
--- a/addons/crm/res_config.py
+++ b/addons/crm/res_config.py
@@ -61,7 +61,8 @@ class crm_configuration(osv.TransientModel):
help='Allows you to track your customers/suppliers claims and grievances.\n'
'-This installs the module crm_claim.'),
'module_crm_helpdesk': fields.boolean("Manage Helpdesk and Support",
- help="""Allows you to communicate with Customer, process Customer query, and provide better help and support. This installs the module crm_helpdesk."""),
+ help='Allows you to communicate with Customer, process Customer query, and provide better help and support.\n'
+ '-This installs the module crm_helpdesk.'),
'group_multi_salesteams': fields.boolean("Organize Sales activities into multiple Sales Teams",
implied_group='base.group_multi_salesteams',
help="""Allows you to use Sales Teams to manage your leads and opportunities."""),
--
cgit v1.2.3
From 4bc4547cb90f158d80ffb3275ae47fe2ead66192 Mon Sep 17 00:00:00 2001
From: "Bharat R. Devnani (OpenERP)"
Date: Fri, 30 Aug 2013 19:26:43 +0530
Subject: [ADD] added the functionalities mentioned on pad
bzr revid: bde@tinyerp.com-20130830135643-ehzm0bdtvq8lltdi
---
addons/account/account.py | 4 ++++
addons/account/account_installer.xml | 2 +-
addons/l10n_ar/l10n_ar_wizard.xml | 10 +++++++---
addons/l10n_at/l10n_chart_at_wizard.xml | 10 +++++++---
addons/l10n_be/wizard/account_wizard.xml | 12 +++++++++---
addons/l10n_bo/l10n_bo_wizard.xml | 10 +++++++---
addons/l10n_br/l10n_br_wizard.xml | 12 +++++++++---
addons/l10n_ca/l10n_ca_wizard.xml | 6 +++++-
addons/l10n_ch/wizard.xml | 6 ++++++
addons/l10n_cl/l10n_cl_wizard.xml | 7 ++++++-
addons/l10n_cn/l10n_chart_cn_wizard.xml | 7 ++++++-
addons/l10n_co/data/l10n_chart_mx_wizard.xml | 6 ++++++
addons/l10n_cr/l10n_wizard.xml | 7 ++++++-
addons/l10n_de/l10n_de_wizard.xml | 6 +++++-
addons/l10n_ec/l10n_chart_ec_wizard.xml | 7 ++++++-
addons/l10n_es/l10n_es_wizard.xml | 5 +++++
addons/l10n_et/l10n_et_wizard.xml | 6 ++++++
addons/l10n_fr/l10n_fr_wizard.xml | 5 +++++
addons/l10n_gr/l10n_gr_wizard.xml | 7 ++++++-
addons/l10n_gt/l10n_gt_base.xml | 4 ++++
addons/l10n_hn/l10n_hn_base.xml | 4 ++++
addons/l10n_hr/l10n_hr_wizard.xml | 6 ++++++
addons/l10n_in/l10n_in_wizard.xml | 5 +++++
addons/l10n_it/l10n_chart_it_generic.xml | 6 ++++++
addons/l10n_lu/l10n_lu_wizard.xml | 5 +++++
addons/l10n_ma/l10n_ma_tax.xml | 3 ++-
addons/l10n_ma/l10n_ma_wizard.xml | 5 +++++
addons/l10n_mx/data/l10n_chart_mx_wizard.xml | 12 +++++++++---
addons/l10n_nl/l10n_nl_wizard.xml | 7 ++++++-
addons/l10n_pa/l10n_pa_wizard.xml | 4 ++++
addons/l10n_pe/l10n_pe_wizard.xml | 4 ++++
addons/l10n_pl/l10n_chart_pl_wizard.xml | 5 +++++
addons/l10n_pt/l10n_chart_pt_wizard.xml | 5 +++++
addons/l10n_ro/l10n_chart_ro_wizard.xml | 5 +++++
addons/l10n_si/l10n_si_wizard.xml | 6 ++++++
addons/l10n_syscohada/l10n_syscohada_wizard.xml | 6 ++++++
addons/l10n_th/account_data.xml | 6 ++++++
addons/l10n_tr/l10n_tr_wizard.xml | 6 ++++++
addons/l10n_uk/l10n_uk_wizard.xml | 5 +++++
addons/l10n_us/__openerp__.py | 3 ++-
addons/l10n_us/l10n_us_wizard.xml | 11 ++++++++++-
addons/l10n_uy/l10n_uy_wizard.xml | 5 +++++
addons/l10n_ve/data/account_tax.xml | 18 ++----------------
addons/l10n_ve/data/l10n_chart_ve_wizard.xml | 5 +++++
addons/l10n_vn/l10n_vn_wizard.xml | 4 ++++
45 files changed, 244 insertions(+), 46 deletions(-)
diff --git a/addons/account/account.py b/addons/account/account.py
index 33b52b643ed..3a5e2f12cfc 100644
--- a/addons/account/account.py
+++ b/addons/account/account.py
@@ -3030,6 +3030,9 @@ class wizard_multi_charts_accounts(osv.osv_memory):
}
def onchange_company_id(self, cr, uid, ids, company_id, context=None):
+ if context.get('default_currency_id', False):
+ dummy, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', context.get('default_currency_id'))
+ return {'value': {'currency_id': view_id}}
currency_id = False
if company_id:
currency_id = self.pool.get('res.company').browse(cr, uid, company_id, context=context).currency_id.id
@@ -3067,6 +3070,7 @@ class wizard_multi_charts_accounts(osv.osv_memory):
res.update({'company_id': self.pool.get('res.users').browse(cr, uid, [uid], context=context)[0].company_id.id})
if 'currency_id' in fields:
company_id = res.get('company_id') or False
+
if company_id:
company_obj = self.pool.get('res.company')
country_id = company_obj.browse(cr, uid, company_id, context=context).country_id.id
diff --git a/addons/account/account_installer.xml b/addons/account/account_installer.xml
index b03babc63ac..79557b3f406 100644
--- a/addons/account/account_installer.xml
+++ b/addons/account/account_installer.xml
@@ -51,6 +51,6 @@
3automatic
-
+
diff --git a/addons/l10n_ar/l10n_ar_wizard.xml b/addons/l10n_ar/l10n_ar_wizard.xml
index 82e7671b4d3..f21b5cd8c11 100644
--- a/addons/l10n_ar/l10n_ar_wizard.xml
+++ b/addons/l10n_ar/l10n_ar_wizard.xml
@@ -2,9 +2,13 @@
-
- open
-
+
+ open
+
+
+
+ done
+
diff --git a/addons/l10n_at/l10n_chart_at_wizard.xml b/addons/l10n_at/l10n_chart_at_wizard.xml
index 19d27fb47c7..350ea995edc 100644
--- a/addons/l10n_at/l10n_chart_at_wizard.xml
+++ b/addons/l10n_at/l10n_chart_at_wizard.xml
@@ -1,9 +1,13 @@
-
- open
-
+
+ open
+
+
+
+ done
+
diff --git a/addons/l10n_be/wizard/account_wizard.xml b/addons/l10n_be/wizard/account_wizard.xml
index dde7a77516a..27785c1b6cf 100644
--- a/addons/l10n_be/wizard/account_wizard.xml
+++ b/addons/l10n_be/wizard/account_wizard.xml
@@ -1,7 +1,13 @@
-
- open
-
+
+
+ open
+
+
+
+ done
+
+
diff --git a/addons/l10n_bo/l10n_bo_wizard.xml b/addons/l10n_bo/l10n_bo_wizard.xml
index 4de61247b90..cc2f17058ff 100644
--- a/addons/l10n_bo/l10n_bo_wizard.xml
+++ b/addons/l10n_bo/l10n_bo_wizard.xml
@@ -2,9 +2,13 @@
-
- open
-
+
+ open
+
+
+
+ done
+
diff --git a/addons/l10n_br/l10n_br_wizard.xml b/addons/l10n_br/l10n_br_wizard.xml
index 748b8913660..5988a2ccd40 100644
--- a/addons/l10n_br/l10n_br_wizard.xml
+++ b/addons/l10n_br/l10n_br_wizard.xml
@@ -1,8 +1,14 @@
-
- open
-
+
+
+ open
+
+
+
+ done
+
+
\ No newline at end of file
diff --git a/addons/l10n_ca/l10n_ca_wizard.xml b/addons/l10n_ca/l10n_ca_wizard.xml
index 19d27fb47c7..007f2badc7a 100644
--- a/addons/l10n_ca/l10n_ca_wizard.xml
+++ b/addons/l10n_ca/l10n_ca_wizard.xml
@@ -4,6 +4,10 @@
open
-
+
+
+ done
+
+
diff --git a/addons/l10n_ch/wizard.xml b/addons/l10n_ch/wizard.xml
index dde7a77516a..8247cf51cc0 100644
--- a/addons/l10n_ch/wizard.xml
+++ b/addons/l10n_ch/wizard.xml
@@ -1,7 +1,13 @@
+
open
+
+
+ done
+
+
diff --git a/addons/l10n_cl/l10n_cl_wizard.xml b/addons/l10n_cl/l10n_cl_wizard.xml
index 704ac4daf38..f05a8d4d664 100644
--- a/addons/l10n_cl/l10n_cl_wizard.xml
+++ b/addons/l10n_cl/l10n_cl_wizard.xml
@@ -1,9 +1,14 @@
+
open
-
+
+
+ done
+
+
diff --git a/addons/l10n_cn/l10n_chart_cn_wizard.xml b/addons/l10n_cn/l10n_chart_cn_wizard.xml
index 52aaa88fdab..d93d83a37aa 100644
--- a/addons/l10n_cn/l10n_chart_cn_wizard.xml
+++ b/addons/l10n_cn/l10n_chart_cn_wizard.xml
@@ -1,8 +1,13 @@
+
open
-
+
+
+ done
+
+
diff --git a/addons/l10n_co/data/l10n_chart_mx_wizard.xml b/addons/l10n_co/data/l10n_chart_mx_wizard.xml
index e73c75c3506..c6f013cbdb3 100644
--- a/addons/l10n_co/data/l10n_chart_mx_wizard.xml
+++ b/addons/l10n_co/data/l10n_chart_mx_wizard.xml
@@ -1,8 +1,14 @@
+
open
+
+
+ done
+
+
diff --git a/addons/l10n_cr/l10n_wizard.xml b/addons/l10n_cr/l10n_wizard.xml
index 6919eb199b6..2199faa9366 100644
--- a/addons/l10n_cr/l10n_wizard.xml
+++ b/addons/l10n_cr/l10n_wizard.xml
@@ -1,8 +1,13 @@
+
open
-
+
+
+ done
+
+
diff --git a/addons/l10n_de/l10n_de_wizard.xml b/addons/l10n_de/l10n_de_wizard.xml
index 19d27fb47c7..007f2badc7a 100644
--- a/addons/l10n_de/l10n_de_wizard.xml
+++ b/addons/l10n_de/l10n_de_wizard.xml
@@ -4,6 +4,10 @@
open
-
+
+
+ done
+
+
diff --git a/addons/l10n_ec/l10n_chart_ec_wizard.xml b/addons/l10n_ec/l10n_chart_ec_wizard.xml
index 52aaa88fdab..71b98ed174b 100644
--- a/addons/l10n_ec/l10n_chart_ec_wizard.xml
+++ b/addons/l10n_ec/l10n_chart_ec_wizard.xml
@@ -1,8 +1,13 @@
+
open
-
+
+
+ done
+
+
diff --git a/addons/l10n_es/l10n_es_wizard.xml b/addons/l10n_es/l10n_es_wizard.xml
index 52aaa88fdab..22953449166 100644
--- a/addons/l10n_es/l10n_es_wizard.xml
+++ b/addons/l10n_es/l10n_es_wizard.xml
@@ -1,8 +1,13 @@
+
open
+
+ done
+
+
diff --git a/addons/l10n_et/l10n_et_wizard.xml b/addons/l10n_et/l10n_et_wizard.xml
index cbbb56b7989..1027cc5874f 100644
--- a/addons/l10n_et/l10n_et_wizard.xml
+++ b/addons/l10n_et/l10n_et_wizard.xml
@@ -1,8 +1,14 @@
+
open
+
+
+ done
+
+
diff --git a/addons/l10n_fr/l10n_fr_wizard.xml b/addons/l10n_fr/l10n_fr_wizard.xml
index 704ac4daf38..193de99eeb4 100644
--- a/addons/l10n_fr/l10n_fr_wizard.xml
+++ b/addons/l10n_fr/l10n_fr_wizard.xml
@@ -1,9 +1,14 @@
+
open
+
+ done
+
+
diff --git a/addons/l10n_gr/l10n_gr_wizard.xml b/addons/l10n_gr/l10n_gr_wizard.xml
index 52aaa88fdab..71b98ed174b 100644
--- a/addons/l10n_gr/l10n_gr_wizard.xml
+++ b/addons/l10n_gr/l10n_gr_wizard.xml
@@ -1,8 +1,13 @@
+
open
-
+
+
+ done
+
+
diff --git a/addons/l10n_gt/l10n_gt_base.xml b/addons/l10n_gt/l10n_gt_base.xml
index 8caf6ebe48c..ddce9748193 100644
--- a/addons/l10n_gt/l10n_gt_base.xml
+++ b/addons/l10n_gt/l10n_gt_base.xml
@@ -4,6 +4,10 @@
open
+
+
+ done
+
diff --git a/addons/l10n_hn/l10n_hn_base.xml b/addons/l10n_hn/l10n_hn_base.xml
index b8b85c3f2a7..63cf79a441f 100644
--- a/addons/l10n_hn/l10n_hn_base.xml
+++ b/addons/l10n_hn/l10n_hn_base.xml
@@ -5,6 +5,10 @@
open
+
+
+ done
+
diff --git a/addons/l10n_hr/l10n_hr_wizard.xml b/addons/l10n_hr/l10n_hr_wizard.xml
index b736f241607..25cc74eb4a6 100644
--- a/addons/l10n_hr/l10n_hr_wizard.xml
+++ b/addons/l10n_hr/l10n_hr_wizard.xml
@@ -1,8 +1,14 @@
+
open
+
+
+ done
+
+
diff --git a/addons/l10n_in/l10n_in_wizard.xml b/addons/l10n_in/l10n_in_wizard.xml
index 704ac4daf38..193de99eeb4 100644
--- a/addons/l10n_in/l10n_in_wizard.xml
+++ b/addons/l10n_in/l10n_in_wizard.xml
@@ -1,9 +1,14 @@
+
open
+
+ done
+
+
diff --git a/addons/l10n_it/l10n_chart_it_generic.xml b/addons/l10n_it/l10n_chart_it_generic.xml
index dde7a77516a..71b98ed174b 100644
--- a/addons/l10n_it/l10n_chart_it_generic.xml
+++ b/addons/l10n_it/l10n_chart_it_generic.xml
@@ -1,7 +1,13 @@
+
open
+
+
+ done
+
+
diff --git a/addons/l10n_lu/l10n_lu_wizard.xml b/addons/l10n_lu/l10n_lu_wizard.xml
index 52aaa88fdab..22953449166 100644
--- a/addons/l10n_lu/l10n_lu_wizard.xml
+++ b/addons/l10n_lu/l10n_lu_wizard.xml
@@ -1,8 +1,13 @@
+
open
+
+ done
+
+
diff --git a/addons/l10n_ma/l10n_ma_tax.xml b/addons/l10n_ma/l10n_ma_tax.xml
index 070d2d4fc9b..f296b22edca 100644
--- a/addons/l10n_ma/l10n_ma_tax.xml
+++ b/addons/l10n_ma/l10n_ma_tax.xml
@@ -742,7 +742,8 @@
-
+
+
diff --git a/addons/l10n_ma/l10n_ma_wizard.xml b/addons/l10n_ma/l10n_ma_wizard.xml
index 52aaa88fdab..22953449166 100644
--- a/addons/l10n_ma/l10n_ma_wizard.xml
+++ b/addons/l10n_ma/l10n_ma_wizard.xml
@@ -1,8 +1,13 @@
+
open
+
+ done
+
+
diff --git a/addons/l10n_mx/data/l10n_chart_mx_wizard.xml b/addons/l10n_mx/data/l10n_chart_mx_wizard.xml
index e73c75c3506..baf1a49fc54 100644
--- a/addons/l10n_mx/data/l10n_chart_mx_wizard.xml
+++ b/addons/l10n_mx/data/l10n_chart_mx_wizard.xml
@@ -1,8 +1,14 @@
-
- open
-
+
+
+ open
+
+
+
+ done
+
+
diff --git a/addons/l10n_nl/l10n_nl_wizard.xml b/addons/l10n_nl/l10n_nl_wizard.xml
index 52aaa88fdab..3b11bd90c90 100644
--- a/addons/l10n_nl/l10n_nl_wizard.xml
+++ b/addons/l10n_nl/l10n_nl_wizard.xml
@@ -1,8 +1,13 @@
+
open
-
+
+
+ done
+
+
diff --git a/addons/l10n_pa/l10n_pa_wizard.xml b/addons/l10n_pa/l10n_pa_wizard.xml
index 4de61247b90..8acb61d7edf 100644
--- a/addons/l10n_pa/l10n_pa_wizard.xml
+++ b/addons/l10n_pa/l10n_pa_wizard.xml
@@ -6,5 +6,9 @@
open
+
+ done
+
+
diff --git a/addons/l10n_pe/l10n_pe_wizard.xml b/addons/l10n_pe/l10n_pe_wizard.xml
index 82e7671b4d3..b18ee6e26ca 100644
--- a/addons/l10n_pe/l10n_pe_wizard.xml
+++ b/addons/l10n_pe/l10n_pe_wizard.xml
@@ -6,5 +6,9 @@
open
+
+ done
+
+
diff --git a/addons/l10n_pl/l10n_chart_pl_wizard.xml b/addons/l10n_pl/l10n_chart_pl_wizard.xml
index 52aaa88fdab..22953449166 100644
--- a/addons/l10n_pl/l10n_chart_pl_wizard.xml
+++ b/addons/l10n_pl/l10n_chart_pl_wizard.xml
@@ -1,8 +1,13 @@
+
open
+
+ done
+
+
diff --git a/addons/l10n_pt/l10n_chart_pt_wizard.xml b/addons/l10n_pt/l10n_chart_pt_wizard.xml
index 52aaa88fdab..22953449166 100644
--- a/addons/l10n_pt/l10n_chart_pt_wizard.xml
+++ b/addons/l10n_pt/l10n_chart_pt_wizard.xml
@@ -1,8 +1,13 @@
+
open
+
+ done
+
+
diff --git a/addons/l10n_ro/l10n_chart_ro_wizard.xml b/addons/l10n_ro/l10n_chart_ro_wizard.xml
index 52aaa88fdab..22953449166 100644
--- a/addons/l10n_ro/l10n_chart_ro_wizard.xml
+++ b/addons/l10n_ro/l10n_chart_ro_wizard.xml
@@ -1,8 +1,13 @@
+
open
+
+ done
+
+
diff --git a/addons/l10n_si/l10n_si_wizard.xml b/addons/l10n_si/l10n_si_wizard.xml
index 5a4b51a01e9..65e60217eb3 100644
--- a/addons/l10n_si/l10n_si_wizard.xml
+++ b/addons/l10n_si/l10n_si_wizard.xml
@@ -1,8 +1,14 @@
+
open
+
+
+ done
+
+
diff --git a/addons/l10n_syscohada/l10n_syscohada_wizard.xml b/addons/l10n_syscohada/l10n_syscohada_wizard.xml
index dde7a77516a..2f815a22ff6 100644
--- a/addons/l10n_syscohada/l10n_syscohada_wizard.xml
+++ b/addons/l10n_syscohada/l10n_syscohada_wizard.xml
@@ -1,7 +1,13 @@
+
open
+
+
+ done
+
+
diff --git a/addons/l10n_th/account_data.xml b/addons/l10n_th/account_data.xml
index 9ae3d9e3364..5df58bc3867 100644
--- a/addons/l10n_th/account_data.xml
+++ b/addons/l10n_th/account_data.xml
@@ -645,8 +645,14 @@
+
open
+
+
+ done
+
+
diff --git a/addons/l10n_tr/l10n_tr_wizard.xml b/addons/l10n_tr/l10n_tr_wizard.xml
index 66b9d37221b..a2c8c45c2de 100644
--- a/addons/l10n_tr/l10n_tr_wizard.xml
+++ b/addons/l10n_tr/l10n_tr_wizard.xml
@@ -1,8 +1,14 @@
+
open
+
+
+ done
+
+
diff --git a/addons/l10n_uk/l10n_uk_wizard.xml b/addons/l10n_uk/l10n_uk_wizard.xml
index 8a6f1a5a83c..b488b5b53f0 100644
--- a/addons/l10n_uk/l10n_uk_wizard.xml
+++ b/addons/l10n_uk/l10n_uk_wizard.xml
@@ -1,9 +1,14 @@
+
open
+
+ done
+
+
diff --git a/addons/l10n_us/__openerp__.py b/addons/l10n_us/__openerp__.py
index 90c0e6fd4ad..cc0de9350f2 100644
--- a/addons/l10n_us/__openerp__.py
+++ b/addons/l10n_us/__openerp__.py
@@ -36,7 +36,8 @@ United States - Chart of accounts.
'account_tax_code_template.xml',
'account_tax_template.xml',
'account_chart_template_after.xml',
- 'l10n_us_wizard.xml'
+ 'l10n_us_wizard.xml',
+ 'account_chart_template_set_accounts.xml'
],
'demo': [],
'test': [],
diff --git a/addons/l10n_us/l10n_us_wizard.xml b/addons/l10n_us/l10n_us_wizard.xml
index 52aaa88fdab..7d53a61910c 100644
--- a/addons/l10n_us/l10n_us_wizard.xml
+++ b/addons/l10n_us/l10n_us_wizard.xml
@@ -1,8 +1,17 @@
+
+
+ {'default_currency_id': "USD"}
+
+
open
-
+
+
+ done
+
+
diff --git a/addons/l10n_uy/l10n_uy_wizard.xml b/addons/l10n_uy/l10n_uy_wizard.xml
index 3dea57dc5c2..4a82f72ca23 100644
--- a/addons/l10n_uy/l10n_uy_wizard.xml
+++ b/addons/l10n_uy/l10n_uy_wizard.xml
@@ -4,5 +4,10 @@
open
+
+
+ done
+
+
diff --git a/addons/l10n_ve/data/account_tax.xml b/addons/l10n_ve/data/account_tax.xml
index d28b0d1d8f3..915761fc92a 100644
--- a/addons/l10n_ve/data/account_tax.xml
+++ b/addons/l10n_ve/data/account_tax.xml
@@ -1,12 +1,12 @@
-
+ Exento0.00000percent
- sale
+ all
@@ -14,7 +14,6 @@
-
IVA (12.0%) ventas
@@ -54,19 +53,6 @@
-
-
- Exento
- 0.00000
- percent
- purchase
-
-
-
-
-
-
- IVA (12.0%) compras
diff --git a/addons/l10n_ve/data/l10n_chart_ve_wizard.xml b/addons/l10n_ve/data/l10n_chart_ve_wizard.xml
index dde7a77516a..e772699014f 100644
--- a/addons/l10n_ve/data/l10n_chart_ve_wizard.xml
+++ b/addons/l10n_ve/data/l10n_chart_ve_wizard.xml
@@ -3,5 +3,10 @@
open
+
+
+ done
+
+
diff --git a/addons/l10n_vn/l10n_vn_wizard.xml b/addons/l10n_vn/l10n_vn_wizard.xml
index 368a3cbd8b3..f84b8d654e4 100755
--- a/addons/l10n_vn/l10n_vn_wizard.xml
+++ b/addons/l10n_vn/l10n_vn_wizard.xml
@@ -5,6 +5,10 @@
open
+
+
+ done
+
\ No newline at end of file
--
cgit v1.2.3
From 00711ec5bdd7df523196f3f5a2d712f47ccace9a Mon Sep 17 00:00:00 2001
From: "Turkesh Patel (Open ERP)"
Date: Mon, 2 Sep 2013 16:06:29 +0530
Subject: [IMP] set defalut cot as per last configuration wizard.
bzr revid: tpa@tinyerp.com-20130902103629-9d1joumo51b7w8qg
---
addons/account/account.py | 5 ++++-
addons/account/account_installer.xml | 2 +-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/addons/account/account.py b/addons/account/account.py
index 33b52b643ed..4773547df30 100644
--- a/addons/account/account.py
+++ b/addons/account/account.py
@@ -3076,7 +3076,10 @@ class wizard_multi_charts_accounts(osv.osv_memory):
ids = self.pool.get('account.chart.template').search(cr, uid, [('visible', '=', True)], context=context)
if ids:
if 'chart_template_id' in fields:
- res.update({'only_one_chart_template': len(ids) == 1, 'chart_template_id': ids[0]})
+ chart_id = ids[0]
+ if context.get("default_charts"):
+ chart_id = self.pool.get('ir.model.data').search_read(cr, uid, [('model','=','account.chart.template'),('module','=',context.get("default_charts"))], ['res_id'], context=context)[0]['res_id']
+ res.update({'only_one_chart_template': len(ids) == 1, 'chart_template_id': chart_id})
if 'sale_tax' in fields:
sale_tax_ids = tax_templ_obj.search(cr, uid, [("chart_template_id"
, "=", ids[0]), ('type_tax_use', 'in', ('sale','all'))], order="sequence")
diff --git a/addons/account/account_installer.xml b/addons/account/account_installer.xml
index b03babc63ac..8d1b25b299f 100644
--- a/addons/account/account_installer.xml
+++ b/addons/account/account_installer.xml
@@ -10,7 +10,7 @@
--
cgit v1.2.3
From 1b0ad7b55f013f94e39aac141d4f8edf0cd31847 Mon Sep 17 00:00:00 2001
From: "Turkesh Patel (Open ERP)"
Date: Mon, 2 Sep 2013 16:59:43 +0530
Subject: [IMP] l10n_us: improved code to set default recivable and payble
accounts.
bzr revid: tpa@tinyerp.com-20130902112943-cj3jbtn1bt5n15y2
---
addons/account/account.py | 1 +
addons/l10n_us/account_chart_template_after.xml | 2 ++
2 files changed, 3 insertions(+)
diff --git a/addons/account/account.py b/addons/account/account.py
index c298a15646b..45308a9e7d4 100644
--- a/addons/account/account.py
+++ b/addons/account/account.py
@@ -3030,6 +3030,7 @@ class wizard_multi_charts_accounts(osv.osv_memory):
}
def onchange_company_id(self, cr, uid, ids, company_id, context=None):
+ if context is None:context = {}
if context.get('default_currency_id', False):
dummy, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', context.get('default_currency_id'))
return {'value': {'currency_id': view_id}}
diff --git a/addons/l10n_us/account_chart_template_after.xml b/addons/l10n_us/account_chart_template_after.xml
index 5ca3a169aba..4dea930f09f 100644
--- a/addons/l10n_us/account_chart_template_after.xml
+++ b/addons/l10n_us/account_chart_template_after.xml
@@ -6,6 +6,8 @@
+
+
--
cgit v1.2.3
From 34862b9d247571606b11b751542751b9720d5627 Mon Sep 17 00:00:00 2001
From: "Turkesh Patel (Open ERP)"
Date: Mon, 2 Sep 2013 18:38:39 +0530
Subject: [ADD] added currency_id in account.chart.template and remove onchange
from company_id to set currency in account data configuration wizard and set
currency_in in l10n_us.
bzr revid: tpa@tinyerp.com-20130902130839-mpvu7nfzefmzh9kq
---
addons/account/account.py | 14 +++-----------
addons/account/account_view.xml | 2 +-
addons/l10n_us/account_chart_template.xml | 9 +++++++++
addons/l10n_us/l10n_us_wizard.xml | 8 ++------
4 files changed, 15 insertions(+), 18 deletions(-)
diff --git a/addons/account/account.py b/addons/account/account.py
index 45308a9e7d4..1a593e4ad59 100644
--- a/addons/account/account.py
+++ b/addons/account/account.py
@@ -2774,6 +2774,7 @@ class account_chart_template(osv.osv):
'parent_id': fields.many2one('account.chart.template', 'Parent Chart Template'),
'code_digits': fields.integer('# of Digits', required=True, help="No. of Digits to use for account code"),
'visible': fields.boolean('Can be Visible?', help="Set this to False if you don't want this template to be used actively in the wizard that generate Chart of Accounts from templates, this is useful when you want to generate accounts of this template only when loading its child template."),
+ 'currency_id': fields.many2one('res.currency', 'Currency'),
'complete_tax_set': fields.boolean('Complete Set of Taxes', help='This boolean helps you to choose if you want to propose to the user to encode the sale and purchase rates or choose from list of taxes. This last choice assumes that the set of tax defined on this template is complete'),
'account_root_id': fields.many2one('account.account.template', 'Root Account', domain=[('parent_id','=',False)]),
'tax_code_root_id': fields.many2one('account.tax.code.template', 'Root Tax Code', domain=[('parent_id','=',False)]),
@@ -3029,16 +3030,6 @@ class wizard_multi_charts_accounts(osv.osv_memory):
'complete_tax_set': fields.boolean('Complete Set of Taxes', help='This boolean helps you to choose if you want to propose to the user to encode the sales and purchase rates or use the usual m2o fields. This last choice assumes that the set of tax defined for the chosen template is complete'),
}
- def onchange_company_id(self, cr, uid, ids, company_id, context=None):
- if context is None:context = {}
- if context.get('default_currency_id', False):
- dummy, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', context.get('default_currency_id'))
- return {'value': {'currency_id': view_id}}
- currency_id = False
- if company_id:
- currency_id = self.pool.get('res.company').browse(cr, uid, company_id, context=context).currency_id.id
- return {'value': {'currency_id': currency_id}}
-
def onchange_tax_rate(self, cr, uid, ids, rate=False, context=None):
return {'value': {'purchase_tax_rate': rate or False}}
@@ -3048,7 +3039,8 @@ class wizard_multi_charts_accounts(osv.osv_memory):
res['value'] = {'complete_tax_set': False, 'sale_tax': False, 'purchase_tax': False}
if chart_template_id:
data = self.pool.get('account.chart.template').browse(cr, uid, chart_template_id, context=context)
- res['value'].update({'complete_tax_set': data.complete_tax_set})
+ currency_id = data.currency_id.id or self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
+ res['value'].update({'complete_tax_set': data.complete_tax_set, 'currency_id': currency_id})
if data.complete_tax_set:
# default tax is given by the lowest sequence. For same sequence we will take the latest created as it will be the case for tax created while isntalling the generic chart of account
sale_tax_ids = tax_templ_obj.search(cr, uid, [("chart_template_id"
diff --git a/addons/account/account_view.xml b/addons/account/account_view.xml
index 91c6d6608fd..cb53d7d8fdc 100644
--- a/addons/account/account_view.xml
+++ b/addons/account/account_view.xml
@@ -2120,7 +2120,7 @@
-
+
diff --git a/addons/l10n_us/account_chart_template.xml b/addons/l10n_us/account_chart_template.xml
index 0863680981f..704afbad152 100644
--- a/addons/l10n_us/account_chart_template.xml
+++ b/addons/l10n_us/account_chart_template.xml
@@ -6,6 +6,7 @@
Basic Chart of Account
+
@@ -13,45 +14,53 @@
+ Advertising
+ Agriculture
+ Construction Trades (Plumber, Electrician, HVAC, etc.)
+ Financial Services other than Accounting or Bookkeeping
+ General Service-Based Business
+ Legal Services
+ General Product-Based Business
+
diff --git a/addons/l10n_us/l10n_us_wizard.xml b/addons/l10n_us/l10n_us_wizard.xml
index 9373935b22c..19d27fb47c7 100644
--- a/addons/l10n_us/l10n_us_wizard.xml
+++ b/addons/l10n_us/l10n_us_wizard.xml
@@ -1,13 +1,9 @@
-
-
- {'default_currency_id': "USD"}
-
-
+
open
-
+
--
cgit v1.2.3
From 7acb2f3105d61ae668e6bd2042be789d965e2e10 Mon Sep 17 00:00:00 2001
From: "Mehul Mehta (OpenERP)"
Date: Mon, 2 Sep 2013 21:08:40 +0800
Subject: [IMP]view a dynaemic font in general setting for select font and set
a font for company header and footer
bzr revid: mme@tinyerp.com-20130902130840-pvtegpqlpxkip46k
---
addons/base_setup/res_config.py | 52 ++++++++++++++++++++++++++++++++++++-----
1 file changed, 46 insertions(+), 6 deletions(-)
diff --git a/addons/base_setup/res_config.py b/addons/base_setup/res_config.py
index bbaa6713c82..b9d3280e49e 100644
--- a/addons/base_setup/res_config.py
+++ b/addons/base_setup/res_config.py
@@ -21,14 +21,54 @@
from openerp.osv import fields, osv
import re
-import matplotlib.font_manager
+import os
+import platform
+from reportlab import rl_config
+from openerp.tools import config
_lst_font=[]
-for i in matplotlib.font_manager.findSystemFonts(fontpaths=None, fontext='ttf'):
- m=re.sub('(.*/)','', i)
- n=m.strip('.ttf')
- n=n.replace('-',' ')
- _lst_font.append((n,n))
+TTFSearchPath_Linux = [
+ '/usr/share/fonts/truetype', # SuSE
+ '/usr/share/fonts/dejavu', '/usr/share/fonts/liberation', # Fedora, RHEL
+ '/usr/share/fonts/truetype/*', # Ubuntu,
+ '/usr/share/fonts/TTF/*', # at Mandriva/Mageia
+ '/usr/share/fonts/TTF', # Arch Linux
+ ]
+
+TTFSearchPath_Windows = [
+ 'c:/winnt/fonts',
+ 'c:/windows/fonts'
+ ]
+
+TTFSearchPath_Darwin = [
+ '~/Library/Fonts',
+ '/Library/Fonts',
+ '/Network/Library/Fonts',
+ '/System/Library/Fonts',
+ ]
+
+TTFSearchPathMap = {
+ 'Darwin': TTFSearchPath_Darwin,
+ 'Windows': TTFSearchPath_Windows,
+ 'Linux': TTFSearchPath_Linux,
+}
+searchpath = []
+
+if config.get('fonts_search_path'):
+ searchpath += map(str.strip, config.get('fonts_search_path').split(','))
+
+local_platform = platform.system()
+if local_platform in TTFSearchPathMap:
+ searchpath += TTFSearchPathMap[local_platform]
+
+searchpath += rl_config.TTFSearchPath
+for dirglob in searchpath:
+ if os.path.isdir(dirglob):
+ for file in os.listdir('/'+dirglob):
+ if os.path.isfile('/'+dirglob+'/'+file):
+ font=file.strip('.ttf')
+ font=font.replace('-',' ')
+ _lst_font.append((font,font))
class base_config_settings(osv.osv_memory):
_name = 'base.config.settings'
--
cgit v1.2.3
From e7ef5b87909dc6acaedf8b0660b2ba87189e1d6c Mon Sep 17 00:00:00 2001
From: "Turkesh Patel (Open ERP)"
Date: Tue, 3 Sep 2013 11:07:15 +0530
Subject: [IMP] in order to set default chart which was last created set max of
ids.
bzr revid: tpa@tinyerp.com-20130903053715-zyduv0q0b81lz0vq
---
addons/account/account.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/addons/account/account.py b/addons/account/account.py
index 1a593e4ad59..9f9ee187d69 100644
--- a/addons/account/account.py
+++ b/addons/account/account.py
@@ -3072,7 +3072,8 @@ class wizard_multi_charts_accounts(osv.osv_memory):
ids = self.pool.get('account.chart.template').search(cr, uid, [('visible', '=', True)], context=context)
if ids:
if 'chart_template_id' in fields:
- chart_id = ids[0]
+ #in order to get set default chart which was last created set max of ids.
+ chart_id = max(ids)
if context.get("default_charts"):
chart_id = self.pool.get('ir.model.data').search_read(cr, uid, [('model','=','account.chart.template'),('module','=',context.get("default_charts"))], ['res_id'], context=context)[0]['res_id']
res.update({'only_one_chart_template': len(ids) == 1, 'chart_template_id': chart_id})
--
cgit v1.2.3
From 305e9f5cddf209db45e5bb5c6cfa7d7838b171f7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Tue, 3 Sep 2013 16:58:40 +0200
Subject: [ADD] web, web_kanban: moved sparkline and gauge widgets from crm and
sale_crm into web, because other modules will have to use it in kanban veiws.
bzr revid: tde@openerp.com-20130903145840-uxt0blhjcly1oz3o
---
addons/web/__openerp__.py | 2 +
.../lib/jquery.sparkline/jquery.sparkline.js | 3047 ++++++++++++++++++++
addons/web/static/lib/justgage/justgage.1.0.1.js | 946 ++++++
addons/web/static/lib/qweb/qweb2.js | 1 +
addons/web_kanban/static/src/js/kanban.js | 142 +
5 files changed, 4138 insertions(+)
create mode 100644 addons/web/static/lib/jquery.sparkline/jquery.sparkline.js
create mode 100644 addons/web/static/lib/justgage/justgage.1.0.1.js
diff --git a/addons/web/__openerp__.py b/addons/web/__openerp__.py
index 5c8d27e11cb..32a108c131d 100644
--- a/addons/web/__openerp__.py
+++ b/addons/web/__openerp__.py
@@ -32,9 +32,11 @@ This module provides the core of the OpenERP Web Client.
"static/lib/jquery.ui.notify/js/jquery.notify.js",
"static/lib/jquery.deferred-queue/jquery.deferred-queue.js",
"static/lib/jquery.scrollTo/jquery.scrollTo-min.js",
+ "static/lib/jquery.sparkline/jquery.sparkline.js",
"static/lib/jquery.tipsy/jquery.tipsy.js",
"static/lib/jquery.textext/jquery.textext.js",
"static/lib/jquery.timeago/jquery.timeago.js",
+ "static/lib/justgage/justgage.1.0.1.js",
"static/lib/qweb/qweb2.js",
"static/lib/underscore/underscore.js",
"static/lib/underscore.string/lib/underscore.string.js",
diff --git a/addons/web/static/lib/jquery.sparkline/jquery.sparkline.js b/addons/web/static/lib/jquery.sparkline/jquery.sparkline.js
new file mode 100644
index 00000000000..8069db45fe9
--- /dev/null
+++ b/addons/web/static/lib/jquery.sparkline/jquery.sparkline.js
@@ -0,0 +1,3047 @@
+/**
+*
+* jquery.sparkline.js
+*
+* v2.1.1
+* (c) Splunk, Inc
+* Contact: Gareth Watts (gareth@splunk.com)
+* http://omnipotent.net/jquery.sparkline/
+*
+* Generates inline sparkline charts from data supplied either to the method
+* or inline in HTML
+*
+* Compatible with Internet Explorer 6.0+ and modern browsers equipped with the canvas tag
+* (Firefox 2.0+, Safari, Opera, etc)
+*
+* License: New BSD License
+*
+* Copyright (c) 2012, Splunk Inc.
+* All rights reserved.
+*
+* Redistribution and use in source and binary forms, with or without modification,
+* are permitted provided that the following conditions are met:
+*
+* * Redistributions of source code must retain the above copyright notice,
+* this list of conditions and the following disclaimer.
+* * Redistributions in binary form must reproduce the above copyright notice,
+* this list of conditions and the following disclaimer in the documentation
+* and/or other materials provided with the distribution.
+* * Neither the name of Splunk Inc nor the names of its contributors may
+* be used to endorse or promote products derived from this software without
+* specific prior written permission.
+*
+* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
+* SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+* OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*
+*
+* Usage:
+* $(selector).sparkline(values, options)
+*
+* If values is undefined or set to 'html' then the data values are read from the specified tag:
+*
Sparkline: 1,4,6,6,8,5,3,5
+* $('.sparkline').sparkline();
+* There must be no spaces in the enclosed data set
+*
+* Otherwise values must be an array of numbers or null values
+*
Sparkline: This text replaced if the browser is compatible
+* $('#sparkline1').sparkline([1,4,6,6,8,5,3,5])
+* $('#sparkline2').sparkline([1,4,6,null,null,5,3,5])
+*
+* Values can also be specified in an HTML comment, or as a values attribute:
+*
Sparkline:
+*
Sparkline:
+* $('.sparkline').sparkline();
+*
+* For line charts, x values can also be specified:
+*
Sparkline: 1:1,2.7:4,3.4:6,5:6,6:8,8.7:5,9:3,10:5
+* $('#sparkline1').sparkline([ [1,1], [2.7,4], [3.4,6], [5,6], [6,8], [8.7,5], [9,3], [10,5] ])
+*
+* By default, options should be passed in as teh second argument to the sparkline function:
+* $('.sparkline').sparkline([1,2,3,4], {type: 'bar'})
+*
+* Options can also be set by passing them on the tag itself. This feature is disabled by default though
+* as there's a slight performance overhead:
+* $('.sparkline').sparkline([1,2,3,4], {enableTagOptions: true})
+*
Sparkline: loading
+* Prefix all options supplied as tag attribute with "spark" (configurable by setting tagOptionPrefix)
+*
+* Supported options:
+* lineColor - Color of the line used for the chart
+* fillColor - Color used to fill in the chart - Set to '' or false for a transparent chart
+* width - Width of the chart - Defaults to 3 times the number of values in pixels
+* height - Height of the chart - Defaults to the height of the containing element
+* chartRangeMin - Specify the minimum value to use for the Y range of the chart - Defaults to the minimum value supplied
+* chartRangeMax - Specify the maximum value to use for the Y range of the chart - Defaults to the maximum value supplied
+* chartRangeClip - Clip out of range values to the max/min specified by chartRangeMin and chartRangeMax
+* chartRangeMinX - Specify the minimum value to use for the X range of the chart - Defaults to the minimum value supplied
+* chartRangeMaxX - Specify the maximum value to use for the X range of the chart - Defaults to the maximum value supplied
+* composite - If true then don't erase any existing chart attached to the tag, but draw
+* another chart over the top - Note that width and height are ignored if an
+* existing chart is detected.
+* tagValuesAttribute - Name of tag attribute to check for data values - Defaults to 'values'
+* enableTagOptions - Whether to check tags for sparkline options
+* tagOptionPrefix - Prefix used for options supplied as tag attributes - Defaults to 'spark'
+* disableHiddenCheck - If set to true, then the plugin will assume that charts will never be drawn into a
+* hidden dom element, avoding a browser reflow
+* disableInteraction - If set to true then all mouseover/click interaction behaviour will be disabled,
+* making the plugin perform much like it did in 1.x
+* disableTooltips - If set to true then tooltips will be disabled - Defaults to false (tooltips enabled)
+* disableHighlight - If set to true then highlighting of selected chart elements on mouseover will be disabled
+* defaults to false (highlights enabled)
+* highlightLighten - Factor to lighten/darken highlighted chart values by - Defaults to 1.4 for a 40% increase
+* tooltipContainer - Specify which DOM element the tooltip should be rendered into - defaults to document.body
+* tooltipClassname - Optional CSS classname to apply to tooltips - If not specified then a default style will be applied
+* tooltipOffsetX - How many pixels away from the mouse pointer to render the tooltip on the X axis
+* tooltipOffsetY - How many pixels away from the mouse pointer to render the tooltip on the r axis
+* tooltipFormatter - Optional callback that allows you to override the HTML displayed in the tooltip
+* callback is given arguments of (sparkline, options, fields)
+* tooltipChartTitle - If specified then the tooltip uses the string specified by this setting as a title
+* tooltipFormat - A format string or SPFormat object (or an array thereof for multiple entries)
+* to control the format of the tooltip
+* tooltipPrefix - A string to prepend to each field displayed in a tooltip
+* tooltipSuffix - A string to append to each field displayed in a tooltip
+* tooltipSkipNull - If true then null values will not have a tooltip displayed (defaults to true)
+* tooltipValueLookups - An object or range map to map field values to tooltip strings
+* (eg. to map -1 to "Lost", 0 to "Draw", and 1 to "Win")
+* numberFormatter - Optional callback for formatting numbers in tooltips
+* numberDigitGroupSep - Character to use for group separator in numbers "1,234" - Defaults to ","
+* numberDecimalMark - Character to use for the decimal point when formatting numbers - Defaults to "."
+* numberDigitGroupCount - Number of digits between group separator - Defaults to 3
+*
+* There are 7 types of sparkline, selected by supplying a "type" option of 'line' (default),
+* 'bar', 'tristate', 'bullet', 'discrete', 'pie' or 'box'
+* line - Line chart. Options:
+* spotColor - Set to '' to not end each line in a circular spot
+* minSpotColor - If set, color of spot at minimum value
+* maxSpotColor - If set, color of spot at maximum value
+* spotRadius - Radius in pixels
+* lineWidth - Width of line in pixels
+* normalRangeMin
+* normalRangeMax - If set draws a filled horizontal bar between these two values marking the "normal"
+* or expected range of values
+* normalRangeColor - Color to use for the above bar
+* drawNormalOnTop - Draw the normal range above the chart fill color if true
+* defaultPixelsPerValue - Defaults to 3 pixels of width for each value in the chart
+* highlightSpotColor - The color to use for drawing a highlight spot on mouseover - Set to null to disable
+* highlightLineColor - The color to use for drawing a highlight line on mouseover - Set to null to disable
+* valueSpots - Specify which points to draw spots on, and in which color. Accepts a range map
+*
+* bar - Bar chart. Options:
+* barColor - Color of bars for postive values
+* negBarColor - Color of bars for negative values
+* zeroColor - Color of bars with zero values
+* nullColor - Color of bars with null values - Defaults to omitting the bar entirely
+* barWidth - Width of bars in pixels
+* colorMap - Optional mappnig of values to colors to override the *BarColor values above
+* can be an Array of values to control the color of individual bars or a range map
+* to specify colors for individual ranges of values
+* barSpacing - Gap between bars in pixels
+* zeroAxis - Centers the y-axis around zero if true
+*
+* tristate - Charts values of win (>0), lose (<0) or draw (=0)
+* posBarColor - Color of win values
+* negBarColor - Color of lose values
+* zeroBarColor - Color of draw values
+* barWidth - Width of bars in pixels
+* barSpacing - Gap between bars in pixels
+* colorMap - Optional mappnig of values to colors to override the *BarColor values above
+* can be an Array of values to control the color of individual bars or a range map
+* to specify colors for individual ranges of values
+*
+* discrete - Options:
+* lineHeight - Height of each line in pixels - Defaults to 30% of the graph height
+* thesholdValue - Values less than this value will be drawn using thresholdColor instead of lineColor
+* thresholdColor
+*
+* bullet - Values for bullet graphs msut be in the order: target, performance, range1, range2, range3, ...
+* options:
+* targetColor - The color of the vertical target marker
+* targetWidth - The width of the target marker in pixels
+* performanceColor - The color of the performance measure horizontal bar
+* rangeColors - Colors to use for each qualitative range background color
+*
+* pie - Pie chart. Options:
+* sliceColors - An array of colors to use for pie slices
+* offset - Angle in degrees to offset the first slice - Try -90 or +90
+* borderWidth - Width of border to draw around the pie chart, in pixels - Defaults to 0 (no border)
+* borderColor - Color to use for the pie chart border - Defaults to #000
+*
+* box - Box plot. Options:
+* raw - Set to true to supply pre-computed plot points as values
+* values should be: low_outlier, low_whisker, q1, median, q3, high_whisker, high_outlier
+* When set to false you can supply any number of values and the box plot will
+* be computed for you. Default is false.
+* showOutliers - Set to true (default) to display outliers as circles
+* outlierIQR - Interquartile range used to determine outliers. Default 1.5
+* boxLineColor - Outline color of the box
+* boxFillColor - Fill color for the box
+* whiskerColor - Line color used for whiskers
+* outlierLineColor - Outline color of outlier circles
+* outlierFillColor - Fill color of the outlier circles
+* spotRadius - Radius of outlier circles
+* medianColor - Line color of the median line
+* target - Draw a target cross hair at the supplied value (default undefined)
+*
+*
+*
+* Examples:
+* $('#sparkline1').sparkline(myvalues, { lineColor: '#f00', fillColor: false });
+* $('.barsparks').sparkline('html', { type:'bar', height:'40px', barWidth:5 });
+* $('#tristate').sparkline([1,1,-1,1,0,0,-1], { type:'tristate' }):
+* $('#discrete').sparkline([1,3,4,5,5,3,4,5], { type:'discrete' });
+* $('#bullet').sparkline([10,12,12,9,7], { type:'bullet' });
+* $('#pie').sparkline([1,1,2], { type:'pie' });
+*/
+
+/*jslint regexp: true, browser: true, jquery: true, white: true, nomen: false, plusplus: false, maxerr: 500, indent: 4 */
+
+(function(factory) {
+ if(typeof define === 'function' && define.amd) {
+ define(['jquery'], factory);
+ }
+ else {
+ factory(jQuery);
+ }
+}
+(function($) {
+ 'use strict';
+
+ var UNSET_OPTION = {},
+ getDefaults, createClass, SPFormat, clipval, quartile, normalizeValue, normalizeValues,
+ remove, isNumber, all, sum, addCSS, ensureArray, formatNumber, RangeMap,
+ MouseHandler, Tooltip, barHighlightMixin,
+ line, bar, tristate, discrete, bullet, pie, box, defaultStyles, initStyles,
+ VShape, VCanvas_base, VCanvas_canvas, VCanvas_vml, pending, shapeCount = 0;
+
+ /**
+ * Default configuration settings
+ */
+ getDefaults = function () {
+ return {
+ // Settings common to most/all chart types
+ common: {
+ type: 'line',
+ lineColor: '#00f',
+ fillColor: '#cdf',
+ defaultPixelsPerValue: 3,
+ width: 'auto',
+ height: 'auto',
+ composite: false,
+ tagValuesAttribute: 'values',
+ tagOptionsPrefix: 'spark',
+ enableTagOptions: false,
+ enableHighlight: true,
+ highlightLighten: 1.4,
+ tooltipSkipNull: true,
+ tooltipPrefix: '',
+ tooltipSuffix: '',
+ disableHiddenCheck: false,
+ numberFormatter: false,
+ numberDigitGroupCount: 3,
+ numberDigitGroupSep: ',',
+ numberDecimalMark: '.',
+ disableTooltips: false,
+ disableInteraction: false
+ },
+ // Defaults for line charts
+ line: {
+ spotColor: '#f80',
+ highlightSpotColor: '#5f5',
+ highlightLineColor: '#f22',
+ spotRadius: 1.5,
+ minSpotColor: '#f80',
+ maxSpotColor: '#f80',
+ lineWidth: 1,
+ normalRangeMin: undefined,
+ normalRangeMax: undefined,
+ normalRangeColor: '#ccc',
+ drawNormalOnTop: false,
+ chartRangeMin: undefined,
+ chartRangeMax: undefined,
+ chartRangeMinX: undefined,
+ chartRangeMaxX: undefined,
+ tooltipFormat: new SPFormat('● {{prefix}}{{y}}{{suffix}}')
+ },
+ // Defaults for bar charts
+ bar: {
+ barColor: '#3366cc',
+ negBarColor: '#f44',
+ stackedBarColor: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#66aa00',
+ '#dd4477', '#0099c6', '#990099'],
+ zeroColor: undefined,
+ nullColor: undefined,
+ zeroAxis: true,
+ barWidth: 4,
+ barSpacing: 1,
+ chartRangeMax: undefined,
+ chartRangeMin: undefined,
+ chartRangeClip: false,
+ colorMap: undefined,
+ tooltipFormat: new SPFormat('● {{prefix}}{{value}}{{suffix}}')
+ },
+ // Defaults for tristate charts
+ tristate: {
+ barWidth: 4,
+ barSpacing: 1,
+ posBarColor: '#6f6',
+ negBarColor: '#f44',
+ zeroBarColor: '#999',
+ colorMap: {},
+ tooltipFormat: new SPFormat('● {{value:map}}'),
+ tooltipValueLookups: { map: { '-1': 'Loss', '0': 'Draw', '1': 'Win' } }
+ },
+ // Defaults for discrete charts
+ discrete: {
+ lineHeight: 'auto',
+ thresholdColor: undefined,
+ thresholdValue: 0,
+ chartRangeMax: undefined,
+ chartRangeMin: undefined,
+ chartRangeClip: false,
+ tooltipFormat: new SPFormat('{{prefix}}{{value}}{{suffix}}')
+ },
+ // Defaults for bullet charts
+ bullet: {
+ targetColor: '#f33',
+ targetWidth: 3, // width of the target bar in pixels
+ performanceColor: '#33f',
+ rangeColors: ['#d3dafe', '#a8b6ff', '#7f94ff'],
+ base: undefined, // set this to a number to change the base start number
+ tooltipFormat: new SPFormat('{{fieldkey:fields}} - {{value}}'),
+ tooltipValueLookups: { fields: {r: 'Range', p: 'Performance', t: 'Target'} }
+ },
+ // Defaults for pie charts
+ pie: {
+ offset: 0,
+ sliceColors: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#66aa00',
+ '#dd4477', '#0099c6', '#990099'],
+ borderWidth: 0,
+ borderColor: '#000',
+ tooltipFormat: new SPFormat('● {{value}} ({{percent.1}}%)')
+ },
+ // Defaults for box plots
+ box: {
+ raw: false,
+ boxLineColor: '#000',
+ boxFillColor: '#cdf',
+ whiskerColor: '#000',
+ outlierLineColor: '#333',
+ outlierFillColor: '#fff',
+ medianColor: '#f00',
+ showOutliers: true,
+ outlierIQR: 1.5,
+ spotRadius: 1.5,
+ target: undefined,
+ targetColor: '#4a2',
+ chartRangeMax: undefined,
+ chartRangeMin: undefined,
+ tooltipFormat: new SPFormat('{{field:fields}}: {{value}}'),
+ tooltipFormatFieldlistKey: 'field',
+ tooltipValueLookups: { fields: { lq: 'Lower Quartile', med: 'Median',
+ uq: 'Upper Quartile', lo: 'Left Outlier', ro: 'Right Outlier',
+ lw: 'Left Whisker', rw: 'Right Whisker'} }
+ }
+ };
+ };
+
+ // You can have tooltips use a css class other than jqstooltip by specifying tooltipClassname
+ defaultStyles = '.jqstooltip { ' +
+ 'position: absolute;' +
+ 'left: 0px;' +
+ 'top: 0px;' +
+ 'visibility: hidden;' +
+ 'background: rgb(0, 0, 0) transparent;' +
+ 'background-color: rgba(0,0,0,0.6);' +
+ 'filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000);' +
+ '-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000)";' +
+ 'color: white;' +
+ 'font: 10px arial, san serif;' +
+ 'text-align: left;' +
+ 'white-space: nowrap;' +
+ 'padding: 5px;' +
+ 'border: 1px solid white;' +
+ 'z-index: 10000;' +
+ '}' +
+ '.jqsfield { ' +
+ 'color: white;' +
+ 'font: 10px arial, san serif;' +
+ 'text-align: left;' +
+ '}';
+
+ /**
+ * Utilities
+ */
+
+ createClass = function (/* [baseclass, [mixin, ...]], definition */) {
+ var Class, args;
+ Class = function () {
+ this.init.apply(this, arguments);
+ };
+ if (arguments.length > 1) {
+ if (arguments[0]) {
+ Class.prototype = $.extend(new arguments[0](), arguments[arguments.length - 1]);
+ Class._super = arguments[0].prototype;
+ } else {
+ Class.prototype = arguments[arguments.length - 1];
+ }
+ if (arguments.length > 2) {
+ args = Array.prototype.slice.call(arguments, 1, -1);
+ args.unshift(Class.prototype);
+ $.extend.apply($, args);
+ }
+ } else {
+ Class.prototype = arguments[0];
+ }
+ Class.prototype.cls = Class;
+ return Class;
+ };
+
+ /**
+ * Wraps a format string for tooltips
+ * {{x}}
+ * {{x.2}
+ * {{x:months}}
+ */
+ $.SPFormatClass = SPFormat = createClass({
+ fre: /\{\{([\w.]+?)(:(.+?))?\}\}/g,
+ precre: /(\w+)\.(\d+)/,
+
+ init: function (format, fclass) {
+ this.format = format;
+ this.fclass = fclass;
+ },
+
+ render: function (fieldset, lookups, options) {
+ var self = this,
+ fields = fieldset,
+ match, token, lookupkey, fieldvalue, prec;
+ return this.format.replace(this.fre, function () {
+ var lookup;
+ token = arguments[1];
+ lookupkey = arguments[3];
+ match = self.precre.exec(token);
+ if (match) {
+ prec = match[2];
+ token = match[1];
+ } else {
+ prec = false;
+ }
+ fieldvalue = fields[token];
+ if (fieldvalue === undefined) {
+ return '';
+ }
+ if (lookupkey && lookups && lookups[lookupkey]) {
+ lookup = lookups[lookupkey];
+ if (lookup.get) { // RangeMap
+ return lookups[lookupkey].get(fieldvalue) || fieldvalue;
+ } else {
+ return lookups[lookupkey][fieldvalue] || fieldvalue;
+ }
+ }
+ if (isNumber(fieldvalue)) {
+ if (options.get('numberFormatter')) {
+ fieldvalue = options.get('numberFormatter')(fieldvalue);
+ } else {
+ fieldvalue = formatNumber(fieldvalue, prec,
+ options.get('numberDigitGroupCount'),
+ options.get('numberDigitGroupSep'),
+ options.get('numberDecimalMark'));
+ }
+ }
+ return fieldvalue;
+ });
+ }
+ });
+
+ // convience method to avoid needing the new operator
+ $.spformat = function(format, fclass) {
+ return new SPFormat(format, fclass);
+ };
+
+ clipval = function (val, min, max) {
+ if (val < min) {
+ return min;
+ }
+ if (val > max) {
+ return max;
+ }
+ return val;
+ };
+
+ quartile = function (values, q) {
+ var vl;
+ if (q === 2) {
+ vl = Math.floor(values.length / 2);
+ return values.length % 2 ? values[vl] : (values[vl-1] + values[vl]) / 2;
+ } else {
+ if (values.length % 2 ) { // odd
+ vl = (values.length * q + q) / 4;
+ return vl % 1 ? (values[Math.floor(vl)] + values[Math.floor(vl) - 1]) / 2 : values[vl-1];
+ } else { //even
+ vl = (values.length * q + 2) / 4;
+ return vl % 1 ? (values[Math.floor(vl)] + values[Math.floor(vl) - 1]) / 2 : values[vl-1];
+
+ }
+ }
+ };
+
+ normalizeValue = function (val) {
+ var nf;
+ switch (val) {
+ case 'undefined':
+ val = undefined;
+ break;
+ case 'null':
+ val = null;
+ break;
+ case 'true':
+ val = true;
+ break;
+ case 'false':
+ val = false;
+ break;
+ default:
+ nf = parseFloat(val);
+ if (val == nf) {
+ val = nf;
+ }
+ }
+ return val;
+ };
+
+ normalizeValues = function (vals) {
+ var i, result = [];
+ for (i = vals.length; i--;) {
+ result[i] = normalizeValue(vals[i]);
+ }
+ return result;
+ };
+
+ remove = function (vals, filter) {
+ var i, vl, result = [];
+ for (i = 0, vl = vals.length; i < vl; i++) {
+ if (vals[i] !== filter) {
+ result.push(vals[i]);
+ }
+ }
+ return result;
+ };
+
+ isNumber = function (num) {
+ return !isNaN(parseFloat(num)) && isFinite(num);
+ };
+
+ formatNumber = function (num, prec, groupsize, groupsep, decsep) {
+ var p, i;
+ num = (prec === false ? parseFloat(num).toString() : num.toFixed(prec)).split('');
+ p = (p = $.inArray('.', num)) < 0 ? num.length : p;
+ if (p < num.length) {
+ num[p] = decsep;
+ }
+ for (i = p - groupsize; i > 0; i -= groupsize) {
+ num.splice(i, 0, groupsep);
+ }
+ return num.join('');
+ };
+
+ // determine if all values of an array match a value
+ // returns true if the array is empty
+ all = function (val, arr, ignoreNull) {
+ var i;
+ for (i = arr.length; i--; ) {
+ if (ignoreNull && arr[i] === null) continue;
+ if (arr[i] !== val) {
+ return false;
+ }
+ }
+ return true;
+ };
+
+ // sums the numeric values in an array, ignoring other values
+ sum = function (vals) {
+ var total = 0, i;
+ for (i = vals.length; i--;) {
+ total += typeof vals[i] === 'number' ? vals[i] : 0;
+ }
+ return total;
+ };
+
+ ensureArray = function (val) {
+ return $.isArray(val) ? val : [val];
+ };
+
+ // http://paulirish.com/2008/bookmarklet-inject-new-css-rules/
+ addCSS = function(css) {
+ var tag;
+ //if ('\v' == 'v') /* ie only */ {
+ if (document.createStyleSheet) {
+ document.createStyleSheet().cssText = css;
+ } else {
+ tag = document.createElement('style');
+ tag.type = 'text/css';
+ document.getElementsByTagName('head')[0].appendChild(tag);
+ tag[(typeof document.body.style.WebkitAppearance == 'string') /* webkit only */ ? 'innerText' : 'innerHTML'] = css;
+ }
+ };
+
+ // Provide a cross-browser interface to a few simple drawing primitives
+ $.fn.simpledraw = function (width, height, useExisting, interact) {
+ var target, mhandler;
+ if (useExisting && (target = this.data('_jqs_vcanvas'))) {
+ return target;
+ }
+ if (width === undefined) {
+ width = $(this).innerWidth();
+ }
+ if (height === undefined) {
+ height = $(this).innerHeight();
+ }
+ if ($.fn.sparkline.hasCanvas) {
+ target = new VCanvas_canvas(width, height, this, interact);
+ } else if ($.fn.sparkline.hasVML) {
+ target = new VCanvas_vml(width, height, this);
+ } else {
+ return false;
+ }
+ mhandler = $(this).data('_jqs_mhandler');
+ if (mhandler) {
+ mhandler.registerCanvas(target);
+ }
+ return target;
+ };
+
+ $.fn.cleardraw = function () {
+ var target = this.data('_jqs_vcanvas');
+ if (target) {
+ target.reset();
+ }
+ };
+
+ $.RangeMapClass = RangeMap = createClass({
+ init: function (map) {
+ var key, range, rangelist = [];
+ for (key in map) {
+ if (map.hasOwnProperty(key) && typeof key === 'string' && key.indexOf(':') > -1) {
+ range = key.split(':');
+ range[0] = range[0].length === 0 ? -Infinity : parseFloat(range[0]);
+ range[1] = range[1].length === 0 ? Infinity : parseFloat(range[1]);
+ range[2] = map[key];
+ rangelist.push(range);
+ }
+ }
+ this.map = map;
+ this.rangelist = rangelist || false;
+ },
+
+ get: function (value) {
+ var rangelist = this.rangelist,
+ i, range, result;
+ if ((result = this.map[value]) !== undefined) {
+ return result;
+ }
+ if (rangelist) {
+ for (i = rangelist.length; i--;) {
+ range = rangelist[i];
+ if (range[0] <= value && range[1] >= value) {
+ return range[2];
+ }
+ }
+ }
+ return undefined;
+ }
+ });
+
+ // Convenience function
+ $.range_map = function(map) {
+ return new RangeMap(map);
+ };
+
+ MouseHandler = createClass({
+ init: function (el, options) {
+ var $el = $(el);
+ this.$el = $el;
+ this.options = options;
+ this.currentPageX = 0;
+ this.currentPageY = 0;
+ this.el = el;
+ this.splist = [];
+ this.tooltip = null;
+ this.over = false;
+ this.displayTooltips = !options.get('disableTooltips');
+ this.highlightEnabled = !options.get('disableHighlight');
+ },
+
+ registerSparkline: function (sp) {
+ this.splist.push(sp);
+ if (this.over) {
+ this.updateDisplay();
+ }
+ },
+
+ registerCanvas: function (canvas) {
+ var $canvas = $(canvas.canvas);
+ this.canvas = canvas;
+ this.$canvas = $canvas;
+ $canvas.mouseenter($.proxy(this.mouseenter, this));
+ $canvas.mouseleave($.proxy(this.mouseleave, this));
+ $canvas.click($.proxy(this.mouseclick, this));
+ },
+
+ reset: function (removeTooltip) {
+ this.splist = [];
+ if (this.tooltip && removeTooltip) {
+ this.tooltip.remove();
+ this.tooltip = undefined;
+ }
+ },
+
+ mouseclick: function (e) {
+ var clickEvent = $.Event('sparklineClick');
+ clickEvent.originalEvent = e;
+ clickEvent.sparklines = this.splist;
+ this.$el.trigger(clickEvent);
+ },
+
+ mouseenter: function (e) {
+ $(document.body).unbind('mousemove.jqs');
+ $(document.body).bind('mousemove.jqs', $.proxy(this.mousemove, this));
+ this.over = true;
+ this.currentPageX = e.pageX;
+ this.currentPageY = e.pageY;
+ this.currentEl = e.target;
+ if (!this.tooltip && this.displayTooltips) {
+ this.tooltip = new Tooltip(this.options);
+ this.tooltip.updatePosition(e.pageX, e.pageY);
+ }
+ this.updateDisplay();
+ },
+
+ mouseleave: function () {
+ $(document.body).unbind('mousemove.jqs');
+ var splist = this.splist,
+ spcount = splist.length,
+ needsRefresh = false,
+ sp, i;
+ this.over = false;
+ this.currentEl = null;
+
+ if (this.tooltip) {
+ this.tooltip.remove();
+ this.tooltip = null;
+ }
+
+ for (i = 0; i < spcount; i++) {
+ sp = splist[i];
+ if (sp.clearRegionHighlight()) {
+ needsRefresh = true;
+ }
+ }
+
+ if (needsRefresh) {
+ this.canvas.render();
+ }
+ },
+
+ mousemove: function (e) {
+ this.currentPageX = e.pageX;
+ this.currentPageY = e.pageY;
+ this.currentEl = e.target;
+ if (this.tooltip) {
+ this.tooltip.updatePosition(e.pageX, e.pageY);
+ }
+ this.updateDisplay();
+ },
+
+ updateDisplay: function () {
+ var splist = this.splist,
+ spcount = splist.length,
+ needsRefresh = false,
+ offset = this.$canvas.offset(),
+ localX = this.currentPageX - offset.left,
+ localY = this.currentPageY - offset.top,
+ tooltiphtml, sp, i, result, changeEvent;
+ if (!this.over) {
+ return;
+ }
+ for (i = 0; i < spcount; i++) {
+ sp = splist[i];
+ result = sp.setRegionHighlight(this.currentEl, localX, localY);
+ if (result) {
+ needsRefresh = true;
+ }
+ }
+ if (needsRefresh) {
+ changeEvent = $.Event('sparklineRegionChange');
+ changeEvent.sparklines = this.splist;
+ this.$el.trigger(changeEvent);
+ if (this.tooltip) {
+ tooltiphtml = '';
+ for (i = 0; i < spcount; i++) {
+ sp = splist[i];
+ tooltiphtml += sp.getCurrentRegionTooltip();
+ }
+ this.tooltip.setContent(tooltiphtml);
+ }
+ if (!this.disableHighlight) {
+ this.canvas.render();
+ }
+ }
+ if (result === null) {
+ this.mouseleave();
+ }
+ }
+ });
+
+
+ Tooltip = createClass({
+ sizeStyle: 'position: static !important;' +
+ 'display: block !important;' +
+ 'visibility: hidden !important;' +
+ 'float: left !important;',
+
+ init: function (options) {
+ var tooltipClassname = options.get('tooltipClassname', 'jqstooltip'),
+ sizetipStyle = this.sizeStyle,
+ offset;
+ this.container = options.get('tooltipContainer') || document.body;
+ this.tooltipOffsetX = options.get('tooltipOffsetX', 10);
+ this.tooltipOffsetY = options.get('tooltipOffsetY', 12);
+ // remove any previous lingering tooltip
+ $('#jqssizetip').remove();
+ $('#jqstooltip').remove();
+ this.sizetip = $('', {
+ id: 'jqssizetip',
+ style: sizetipStyle,
+ 'class': tooltipClassname
+ });
+ this.tooltip = $('', {
+ id: 'jqstooltip',
+ 'class': tooltipClassname
+ }).appendTo(this.container);
+ // account for the container's location
+ offset = this.tooltip.offset();
+ this.offsetLeft = offset.left;
+ this.offsetTop = offset.top;
+ this.hidden = true;
+ $(window).unbind('resize.jqs scroll.jqs');
+ $(window).bind('resize.jqs scroll.jqs', $.proxy(this.updateWindowDims, this));
+ this.updateWindowDims();
+ },
+
+ updateWindowDims: function () {
+ this.scrollTop = $(window).scrollTop();
+ this.scrollLeft = $(window).scrollLeft();
+ this.scrollRight = this.scrollLeft + $(window).width();
+ this.updatePosition();
+ },
+
+ getSize: function (content) {
+ this.sizetip.html(content).appendTo(this.container);
+ this.width = this.sizetip.width() + 1;
+ this.height = this.sizetip.height();
+ this.sizetip.remove();
+ },
+
+ setContent: function (content) {
+ if (!content) {
+ this.tooltip.css('visibility', 'hidden');
+ this.hidden = true;
+ return;
+ }
+ this.getSize(content);
+ this.tooltip.html(content)
+ .css({
+ 'width': this.width,
+ 'height': this.height,
+ 'visibility': 'visible'
+ });
+ if (this.hidden) {
+ this.hidden = false;
+ this.updatePosition();
+ }
+ },
+
+ updatePosition: function (x, y) {
+ if (x === undefined) {
+ if (this.mousex === undefined) {
+ return;
+ }
+ x = this.mousex - this.offsetLeft;
+ y = this.mousey - this.offsetTop;
+
+ } else {
+ this.mousex = x = x - this.offsetLeft;
+ this.mousey = y = y - this.offsetTop;
+ }
+ if (!this.height || !this.width || this.hidden) {
+ return;
+ }
+
+ y -= this.height + this.tooltipOffsetY;
+ x += this.tooltipOffsetX;
+
+ if (y < this.scrollTop) {
+ y = this.scrollTop;
+ }
+ if (x < this.scrollLeft) {
+ x = this.scrollLeft;
+ } else if (x + this.width > this.scrollRight) {
+ x = this.scrollRight - this.width;
+ }
+
+ this.tooltip.css({
+ 'left': x,
+ 'top': y
+ });
+ },
+
+ remove: function () {
+ this.tooltip.remove();
+ this.sizetip.remove();
+ this.sizetip = this.tooltip = undefined;
+ $(window).unbind('resize.jqs scroll.jqs');
+ }
+ });
+
+ initStyles = function() {
+ addCSS(defaultStyles);
+ };
+
+ $(initStyles);
+
+ pending = [];
+ $.fn.sparkline = function (userValues, userOptions) {
+ return this.each(function () {
+ var options = new $.fn.sparkline.options(this, userOptions),
+ $this = $(this),
+ render, i;
+ render = function () {
+ var values, width, height, tmp, mhandler, sp, vals;
+ if (userValues === 'html' || userValues === undefined) {
+ vals = this.getAttribute(options.get('tagValuesAttribute'));
+ if (vals === undefined || vals === null) {
+ vals = $this.html();
+ }
+ values = vals.replace(/(^\s*\s*$)|\s+/g, '').split(',');
+ } else {
+ values = userValues;
+ }
+
+ width = options.get('width') === 'auto' ? values.length * options.get('defaultPixelsPerValue') : options.get('width');
+ if (options.get('height') === 'auto') {
+ if (!options.get('composite') || !$.data(this, '_jqs_vcanvas')) {
+ // must be a better way to get the line height
+ tmp = document.createElement('span');
+ tmp.innerHTML = 'a';
+ $this.html(tmp);
+ height = $(tmp).innerHeight() || $(tmp).height();
+ $(tmp).remove();
+ tmp = null;
+ }
+ } else {
+ height = options.get('height');
+ }
+
+ if (!options.get('disableInteraction')) {
+ mhandler = $.data(this, '_jqs_mhandler');
+ if (!mhandler) {
+ mhandler = new MouseHandler(this, options);
+ $.data(this, '_jqs_mhandler', mhandler);
+ } else if (!options.get('composite')) {
+ mhandler.reset();
+ }
+ } else {
+ mhandler = false;
+ }
+
+ if (options.get('composite') && !$.data(this, '_jqs_vcanvas')) {
+ if (!$.data(this, '_jqs_errnotify')) {
+ alert('Attempted to attach a composite sparkline to an element with no existing sparkline');
+ $.data(this, '_jqs_errnotify', true);
+ }
+ return;
+ }
+
+ sp = new $.fn.sparkline[options.get('type')](this, values, options, width, height);
+
+ sp.render();
+
+ if (mhandler) {
+ mhandler.registerSparkline(sp);
+ }
+ };
+ // jQuery 1.3.0 completely changed the meaning of :hidden :-/
+ if (($(this).html() && !options.get('disableHiddenCheck') && $(this).is(':hidden')) || ($.fn.jquery < '1.3.0' && $(this).parents().is(':hidden')) || !$(this).parents('body').length) {
+ if (!options.get('composite') && $.data(this, '_jqs_pending')) {
+ // remove any existing references to the element
+ for (i = pending.length; i; i--) {
+ if (pending[i - 1][0] == this) {
+ pending.splice(i - 1, 1);
+ }
+ }
+ }
+ pending.push([this, render]);
+ $.data(this, '_jqs_pending', true);
+ } else {
+ render.call(this);
+ }
+ });
+ };
+
+ $.fn.sparkline.defaults = getDefaults();
+
+
+ $.sparkline_display_visible = function () {
+ var el, i, pl;
+ var done = [];
+ for (i = 0, pl = pending.length; i < pl; i++) {
+ el = pending[i][0];
+ if ($(el).is(':visible') && !$(el).parents().is(':hidden')) {
+ pending[i][1].call(el);
+ $.data(pending[i][0], '_jqs_pending', false);
+ done.push(i);
+ } else if (!$(el).closest('html').length && !$.data(el, '_jqs_pending')) {
+ // element has been inserted and removed from the DOM
+ // If it was not yet inserted into the dom then the .data request
+ // will return true.
+ // removing from the dom causes the data to be removed.
+ $.data(pending[i][0], '_jqs_pending', false);
+ done.push(i);
+ }
+ }
+ for (i = done.length; i; i--) {
+ pending.splice(done[i - 1], 1);
+ }
+ };
+
+
+ /**
+ * User option handler
+ */
+ $.fn.sparkline.options = createClass({
+ init: function (tag, userOptions) {
+ var extendedOptions, defaults, base, tagOptionType;
+ this.userOptions = userOptions = userOptions || {};
+ this.tag = tag;
+ this.tagValCache = {};
+ defaults = $.fn.sparkline.defaults;
+ base = defaults.common;
+ this.tagOptionsPrefix = userOptions.enableTagOptions && (userOptions.tagOptionsPrefix || base.tagOptionsPrefix);
+
+ tagOptionType = this.getTagSetting('type');
+ if (tagOptionType === UNSET_OPTION) {
+ extendedOptions = defaults[userOptions.type || base.type];
+ } else {
+ extendedOptions = defaults[tagOptionType];
+ }
+ this.mergedOptions = $.extend({}, base, extendedOptions, userOptions);
+ },
+
+
+ getTagSetting: function (key) {
+ var prefix = this.tagOptionsPrefix,
+ val, i, pairs, keyval;
+ if (prefix === false || prefix === undefined) {
+ return UNSET_OPTION;
+ }
+ if (this.tagValCache.hasOwnProperty(key)) {
+ val = this.tagValCache.key;
+ } else {
+ val = this.tag.getAttribute(prefix + key);
+ if (val === undefined || val === null) {
+ val = UNSET_OPTION;
+ } else if (val.substr(0, 1) === '[') {
+ val = val.substr(1, val.length - 2).split(',');
+ for (i = val.length; i--;) {
+ val[i] = normalizeValue(val[i].replace(/(^\s*)|(\s*$)/g, ''));
+ }
+ } else if (val.substr(0, 1) === '{') {
+ pairs = val.substr(1, val.length - 2).split(',');
+ val = {};
+ for (i = pairs.length; i--;) {
+ keyval = pairs[i].split(':', 2);
+ val[keyval[0].replace(/(^\s*)|(\s*$)/g, '')] = normalizeValue(keyval[1].replace(/(^\s*)|(\s*$)/g, ''));
+ }
+ } else {
+ val = normalizeValue(val);
+ }
+ this.tagValCache.key = val;
+ }
+ return val;
+ },
+
+ get: function (key, defaultval) {
+ var tagOption = this.getTagSetting(key),
+ result;
+ if (tagOption !== UNSET_OPTION) {
+ return tagOption;
+ }
+ return (result = this.mergedOptions[key]) === undefined ? defaultval : result;
+ }
+ });
+
+
+ $.fn.sparkline._base = createClass({
+ disabled: false,
+
+ init: function (el, values, options, width, height) {
+ this.el = el;
+ this.$el = $(el);
+ this.values = values;
+ this.options = options;
+ this.width = width;
+ this.height = height;
+ this.currentRegion = undefined;
+ },
+
+ /**
+ * Setup the canvas
+ */
+ initTarget: function () {
+ var interactive = !this.options.get('disableInteraction');
+ if (!(this.target = this.$el.simpledraw(this.width, this.height, this.options.get('composite'), interactive))) {
+ this.disabled = true;
+ } else {
+ this.canvasWidth = this.target.pixelWidth;
+ this.canvasHeight = this.target.pixelHeight;
+ }
+ },
+
+ /**
+ * Actually render the chart to the canvas
+ */
+ render: function () {
+ if (this.disabled) {
+ this.el.innerHTML = '';
+ return false;
+ }
+ return true;
+ },
+
+ /**
+ * Return a region id for a given x/y co-ordinate
+ */
+ getRegion: function (x, y) {
+ },
+
+ /**
+ * Highlight an item based on the moused-over x,y co-ordinate
+ */
+ setRegionHighlight: function (el, x, y) {
+ var currentRegion = this.currentRegion,
+ highlightEnabled = !this.options.get('disableHighlight'),
+ newRegion;
+ if (x > this.canvasWidth || y > this.canvasHeight || x < 0 || y < 0) {
+ return null;
+ }
+ newRegion = this.getRegion(el, x, y);
+ if (currentRegion !== newRegion) {
+ if (currentRegion !== undefined && highlightEnabled) {
+ this.removeHighlight();
+ }
+ this.currentRegion = newRegion;
+ if (newRegion !== undefined && highlightEnabled) {
+ this.renderHighlight();
+ }
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Reset any currently highlighted item
+ */
+ clearRegionHighlight: function () {
+ if (this.currentRegion !== undefined) {
+ this.removeHighlight();
+ this.currentRegion = undefined;
+ return true;
+ }
+ return false;
+ },
+
+ renderHighlight: function () {
+ this.changeHighlight(true);
+ },
+
+ removeHighlight: function () {
+ this.changeHighlight(false);
+ },
+
+ changeHighlight: function (highlight) {},
+
+ /**
+ * Fetch the HTML to display as a tooltip
+ */
+ getCurrentRegionTooltip: function () {
+ var options = this.options,
+ header = '',
+ entries = [],
+ fields, formats, formatlen, fclass, text, i,
+ showFields, showFieldsKey, newFields, fv,
+ formatter, format, fieldlen, j;
+ if (this.currentRegion === undefined) {
+ return '';
+ }
+ fields = this.getCurrentRegionFields();
+ formatter = options.get('tooltipFormatter');
+ if (formatter) {
+ return formatter(this, options, fields);
+ }
+ if (options.get('tooltipChartTitle')) {
+ header += '
' + options.get('tooltipChartTitle') + '
\n';
+ }
+ formats = this.options.get('tooltipFormat');
+ if (!formats) {
+ return '';
+ }
+ if (!$.isArray(formats)) {
+ formats = [formats];
+ }
+ if (!$.isArray(fields)) {
+ fields = [fields];
+ }
+ showFields = this.options.get('tooltipFormatFieldlist');
+ showFieldsKey = this.options.get('tooltipFormatFieldlistKey');
+ if (showFields && showFieldsKey) {
+ // user-selected ordering of fields
+ newFields = [];
+ for (i = fields.length; i--;) {
+ fv = fields[i][showFieldsKey];
+ if ((j = $.inArray(fv, showFields)) != -1) {
+ newFields[j] = fields[i];
+ }
+ }
+ fields = newFields;
+ }
+ formatlen = formats.length;
+ fieldlen = fields.length;
+ for (i = 0; i < formatlen; i++) {
+ format = formats[i];
+ if (typeof format === 'string') {
+ format = new SPFormat(format);
+ }
+ fclass = format.fclass || 'jqsfield';
+ for (j = 0; j < fieldlen; j++) {
+ if (!fields[j].isNull || !options.get('tooltipSkipNull')) {
+ $.extend(fields[j], {
+ prefix: options.get('tooltipPrefix'),
+ suffix: options.get('tooltipSuffix')
+ });
+ text = format.render(fields[j], options.get('tooltipValueLookups'), options);
+ entries.push('
' + text + '
');
+ }
+ }
+ }
+ if (entries.length) {
+ return header + entries.join('\n');
+ }
+ return '';
+ },
+
+ getCurrentRegionFields: function () {},
+
+ calcHighlightColor: function (color, options) {
+ var highlightColor = options.get('highlightColor'),
+ lighten = options.get('highlightLighten'),
+ parse, mult, rgbnew, i;
+ if (highlightColor) {
+ return highlightColor;
+ }
+ if (lighten) {
+ // extract RGB values
+ parse = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(color) || /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(color);
+ if (parse) {
+ rgbnew = [];
+ mult = color.length === 4 ? 16 : 1;
+ for (i = 0; i < 3; i++) {
+ rgbnew[i] = clipval(Math.round(parseInt(parse[i + 1], 16) * mult * lighten), 0, 255);
+ }
+ return 'rgb(' + rgbnew.join(',') + ')';
+ }
+
+ }
+ return color;
+ }
+
+ });
+
+ barHighlightMixin = {
+ changeHighlight: function (highlight) {
+ var currentRegion = this.currentRegion,
+ target = this.target,
+ shapeids = this.regionShapes[currentRegion],
+ newShapes;
+ // will be null if the region value was null
+ if (shapeids) {
+ newShapes = this.renderRegion(currentRegion, highlight);
+ if ($.isArray(newShapes) || $.isArray(shapeids)) {
+ target.replaceWithShapes(shapeids, newShapes);
+ this.regionShapes[currentRegion] = $.map(newShapes, function (newShape) {
+ return newShape.id;
+ });
+ } else {
+ target.replaceWithShape(shapeids, newShapes);
+ this.regionShapes[currentRegion] = newShapes.id;
+ }
+ }
+ },
+
+ render: function () {
+ var values = this.values,
+ target = this.target,
+ regionShapes = this.regionShapes,
+ shapes, ids, i, j;
+
+ if (!this.cls._super.render.call(this)) {
+ return;
+ }
+ for (i = values.length; i--;) {
+ shapes = this.renderRegion(i);
+ if (shapes) {
+ if ($.isArray(shapes)) {
+ ids = [];
+ for (j = shapes.length; j--;) {
+ shapes[j].append();
+ ids.push(shapes[j].id);
+ }
+ regionShapes[i] = ids;
+ } else {
+ shapes.append();
+ regionShapes[i] = shapes.id; // store just the shapeid
+ }
+ } else {
+ // null value
+ regionShapes[i] = null;
+ }
+ }
+ target.render();
+ }
+ };
+
+ /**
+ * Line charts
+ */
+ $.fn.sparkline.line = line = createClass($.fn.sparkline._base, {
+ type: 'line',
+
+ init: function (el, values, options, width, height) {
+ line._super.init.call(this, el, values, options, width, height);
+ this.vertices = [];
+ this.regionMap = [];
+ this.xvalues = [];
+ this.yvalues = [];
+ this.yminmax = [];
+ this.hightlightSpotId = null;
+ this.lastShapeId = null;
+ this.initTarget();
+ },
+
+ getRegion: function (el, x, y) {
+ var i,
+ regionMap = this.regionMap; // maps regions to value positions
+ for (i = regionMap.length; i--;) {
+ if (regionMap[i] !== null && x >= regionMap[i][0] && x <= regionMap[i][1]) {
+ return regionMap[i][2];
+ }
+ }
+ return undefined;
+ },
+
+ getCurrentRegionFields: function () {
+ var currentRegion = this.currentRegion;
+ return {
+ isNull: this.yvalues[currentRegion] === null,
+ x: this.xvalues[currentRegion],
+ y: this.yvalues[currentRegion],
+ color: this.options.get('lineColor'),
+ fillColor: this.options.get('fillColor'),
+ offset: currentRegion
+ };
+ },
+
+ renderHighlight: function () {
+ var currentRegion = this.currentRegion,
+ target = this.target,
+ vertex = this.vertices[currentRegion],
+ options = this.options,
+ spotRadius = options.get('spotRadius'),
+ highlightSpotColor = options.get('highlightSpotColor'),
+ highlightLineColor = options.get('highlightLineColor'),
+ highlightSpot, highlightLine;
+
+ if (!vertex) {
+ return;
+ }
+ if (spotRadius && highlightSpotColor) {
+ highlightSpot = target.drawCircle(vertex[0], vertex[1],
+ spotRadius, undefined, highlightSpotColor);
+ this.highlightSpotId = highlightSpot.id;
+ target.insertAfterShape(this.lastShapeId, highlightSpot);
+ }
+ if (highlightLineColor) {
+ highlightLine = target.drawLine(vertex[0], this.canvasTop, vertex[0],
+ this.canvasTop + this.canvasHeight, highlightLineColor);
+ this.highlightLineId = highlightLine.id;
+ target.insertAfterShape(this.lastShapeId, highlightLine);
+ }
+ },
+
+ removeHighlight: function () {
+ var target = this.target;
+ if (this.highlightSpotId) {
+ target.removeShapeId(this.highlightSpotId);
+ this.highlightSpotId = null;
+ }
+ if (this.highlightLineId) {
+ target.removeShapeId(this.highlightLineId);
+ this.highlightLineId = null;
+ }
+ },
+
+ scanValues: function () {
+ var values = this.values,
+ valcount = values.length,
+ xvalues = this.xvalues,
+ yvalues = this.yvalues,
+ yminmax = this.yminmax,
+ i, val, isStr, isArray, sp;
+ for (i = 0; i < valcount; i++) {
+ val = values[i];
+ isStr = typeof(values[i]) === 'string';
+ isArray = typeof(values[i]) === 'object' && values[i] instanceof Array;
+ sp = isStr && values[i].split(':');
+ if (isStr && sp.length === 2) { // x:y
+ xvalues.push(Number(sp[0]));
+ yvalues.push(Number(sp[1]));
+ yminmax.push(Number(sp[1]));
+ } else if (isArray) {
+ xvalues.push(val[0]);
+ yvalues.push(val[1]);
+ yminmax.push(val[1]);
+ } else {
+ xvalues.push(i);
+ if (values[i] === null || values[i] === 'null') {
+ yvalues.push(null);
+ } else {
+ yvalues.push(Number(val));
+ yminmax.push(Number(val));
+ }
+ }
+ }
+ if (this.options.get('xvalues')) {
+ xvalues = this.options.get('xvalues');
+ }
+
+ this.maxy = this.maxyorg = Math.max.apply(Math, yminmax);
+ this.miny = this.minyorg = Math.min.apply(Math, yminmax);
+
+ this.maxx = Math.max.apply(Math, xvalues);
+ this.minx = Math.min.apply(Math, xvalues);
+
+ this.xvalues = xvalues;
+ this.yvalues = yvalues;
+ this.yminmax = yminmax;
+
+ },
+
+ processRangeOptions: function () {
+ var options = this.options,
+ normalRangeMin = options.get('normalRangeMin'),
+ normalRangeMax = options.get('normalRangeMax');
+
+ if (normalRangeMin !== undefined) {
+ if (normalRangeMin < this.miny) {
+ this.miny = normalRangeMin;
+ }
+ if (normalRangeMax > this.maxy) {
+ this.maxy = normalRangeMax;
+ }
+ }
+ if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < this.miny)) {
+ this.miny = options.get('chartRangeMin');
+ }
+ if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > this.maxy)) {
+ this.maxy = options.get('chartRangeMax');
+ }
+ if (options.get('chartRangeMinX') !== undefined && (options.get('chartRangeClipX') || options.get('chartRangeMinX') < this.minx)) {
+ this.minx = options.get('chartRangeMinX');
+ }
+ if (options.get('chartRangeMaxX') !== undefined && (options.get('chartRangeClipX') || options.get('chartRangeMaxX') > this.maxx)) {
+ this.maxx = options.get('chartRangeMaxX');
+ }
+
+ },
+
+ drawNormalRange: function (canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey) {
+ var normalRangeMin = this.options.get('normalRangeMin'),
+ normalRangeMax = this.options.get('normalRangeMax'),
+ ytop = canvasTop + Math.round(canvasHeight - (canvasHeight * ((normalRangeMax - this.miny) / rangey))),
+ height = Math.round((canvasHeight * (normalRangeMax - normalRangeMin)) / rangey);
+ this.target.drawRect(canvasLeft, ytop, canvasWidth, height, undefined, this.options.get('normalRangeColor')).append();
+ },
+
+ render: function () {
+ var options = this.options,
+ target = this.target,
+ canvasWidth = this.canvasWidth,
+ canvasHeight = this.canvasHeight,
+ vertices = this.vertices,
+ spotRadius = options.get('spotRadius'),
+ regionMap = this.regionMap,
+ rangex, rangey, yvallast,
+ canvasTop, canvasLeft,
+ vertex, path, paths, x, y, xnext, xpos, xposnext,
+ last, next, yvalcount, lineShapes, fillShapes, plen,
+ valueSpots, hlSpotsEnabled, color, xvalues, yvalues, i;
+
+ if (!line._super.render.call(this)) {
+ return;
+ }
+
+ this.scanValues();
+ this.processRangeOptions();
+
+ xvalues = this.xvalues;
+ yvalues = this.yvalues;
+
+ if (!this.yminmax.length || this.yvalues.length < 2) {
+ // empty or all null valuess
+ return;
+ }
+
+ canvasTop = canvasLeft = 0;
+
+ rangex = this.maxx - this.minx === 0 ? 1 : this.maxx - this.minx;
+ rangey = this.maxy - this.miny === 0 ? 1 : this.maxy - this.miny;
+ yvallast = this.yvalues.length - 1;
+
+ if (spotRadius && (canvasWidth < (spotRadius * 4) || canvasHeight < (spotRadius * 4))) {
+ spotRadius = 0;
+ }
+ if (spotRadius) {
+ // adjust the canvas size as required so that spots will fit
+ hlSpotsEnabled = options.get('highlightSpotColor') && !options.get('disableInteraction');
+ if (hlSpotsEnabled || options.get('minSpotColor') || (options.get('spotColor') && yvalues[yvallast] === this.miny)) {
+ canvasHeight -= Math.ceil(spotRadius);
+ }
+ if (hlSpotsEnabled || options.get('maxSpotColor') || (options.get('spotColor') && yvalues[yvallast] === this.maxy)) {
+ canvasHeight -= Math.ceil(spotRadius);
+ canvasTop += Math.ceil(spotRadius);
+ }
+ if (hlSpotsEnabled ||
+ ((options.get('minSpotColor') || options.get('maxSpotColor')) && (yvalues[0] === this.miny || yvalues[0] === this.maxy))) {
+ canvasLeft += Math.ceil(spotRadius);
+ canvasWidth -= Math.ceil(spotRadius);
+ }
+ if (hlSpotsEnabled || options.get('spotColor') ||
+ (options.get('minSpotColor') || options.get('maxSpotColor') &&
+ (yvalues[yvallast] === this.miny || yvalues[yvallast] === this.maxy))) {
+ canvasWidth -= Math.ceil(spotRadius);
+ }
+ }
+
+
+ canvasHeight--;
+
+ if (options.get('normalRangeMin') !== undefined && !options.get('drawNormalOnTop')) {
+ this.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey);
+ }
+
+ path = [];
+ paths = [path];
+ last = next = null;
+ yvalcount = yvalues.length;
+ for (i = 0; i < yvalcount; i++) {
+ x = xvalues[i];
+ xnext = xvalues[i + 1];
+ y = yvalues[i];
+ xpos = canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex));
+ xposnext = i < yvalcount - 1 ? canvasLeft + Math.round((xnext - this.minx) * (canvasWidth / rangex)) : canvasWidth;
+ next = xpos + ((xposnext - xpos) / 2);
+ regionMap[i] = [last || 0, next, i];
+ last = next;
+ if (y === null) {
+ if (i) {
+ if (yvalues[i - 1] !== null) {
+ path = [];
+ paths.push(path);
+ }
+ vertices.push(null);
+ }
+ } else {
+ if (y < this.miny) {
+ y = this.miny;
+ }
+ if (y > this.maxy) {
+ y = this.maxy;
+ }
+ if (!path.length) {
+ // previous value was null
+ path.push([xpos, canvasTop + canvasHeight]);
+ }
+ vertex = [xpos, canvasTop + Math.round(canvasHeight - (canvasHeight * ((y - this.miny) / rangey)))];
+ path.push(vertex);
+ vertices.push(vertex);
+ }
+ }
+
+ lineShapes = [];
+ fillShapes = [];
+ plen = paths.length;
+ for (i = 0; i < plen; i++) {
+ path = paths[i];
+ if (path.length) {
+ if (options.get('fillColor')) {
+ path.push([path[path.length - 1][0], (canvasTop + canvasHeight)]);
+ fillShapes.push(path.slice(0));
+ path.pop();
+ }
+ // if there's only a single point in this path, then we want to display it
+ // as a vertical line which means we keep path[0] as is
+ if (path.length > 2) {
+ // else we want the first value
+ path[0] = [path[0][0], path[1][1]];
+ }
+ lineShapes.push(path);
+ }
+ }
+
+ // draw the fill first, then optionally the normal range, then the line on top of that
+ plen = fillShapes.length;
+ for (i = 0; i < plen; i++) {
+ target.drawShape(fillShapes[i],
+ options.get('fillColor'), options.get('fillColor')).append();
+ }
+
+ if (options.get('normalRangeMin') !== undefined && options.get('drawNormalOnTop')) {
+ this.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey);
+ }
+
+ plen = lineShapes.length;
+ for (i = 0; i < plen; i++) {
+ target.drawShape(lineShapes[i], options.get('lineColor'), undefined,
+ options.get('lineWidth')).append();
+ }
+
+ if (spotRadius && options.get('valueSpots')) {
+ valueSpots = options.get('valueSpots');
+ if (valueSpots.get === undefined) {
+ valueSpots = new RangeMap(valueSpots);
+ }
+ for (i = 0; i < yvalcount; i++) {
+ color = valueSpots.get(yvalues[i]);
+ if (color) {
+ target.drawCircle(canvasLeft + Math.round((xvalues[i] - this.minx) * (canvasWidth / rangex)),
+ canvasTop + Math.round(canvasHeight - (canvasHeight * ((yvalues[i] - this.miny) / rangey))),
+ spotRadius, undefined,
+ color).append();
+ }
+ }
+
+ }
+ if (spotRadius && options.get('spotColor') && yvalues[yvallast] !== null) {
+ target.drawCircle(canvasLeft + Math.round((xvalues[xvalues.length - 1] - this.minx) * (canvasWidth / rangex)),
+ canvasTop + Math.round(canvasHeight - (canvasHeight * ((yvalues[yvallast] - this.miny) / rangey))),
+ spotRadius, undefined,
+ options.get('spotColor')).append();
+ }
+ if (this.maxy !== this.minyorg) {
+ if (spotRadius && options.get('minSpotColor')) {
+ x = xvalues[$.inArray(this.minyorg, yvalues)];
+ target.drawCircle(canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)),
+ canvasTop + Math.round(canvasHeight - (canvasHeight * ((this.minyorg - this.miny) / rangey))),
+ spotRadius, undefined,
+ options.get('minSpotColor')).append();
+ }
+ if (spotRadius && options.get('maxSpotColor')) {
+ x = xvalues[$.inArray(this.maxyorg, yvalues)];
+ target.drawCircle(canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)),
+ canvasTop + Math.round(canvasHeight - (canvasHeight * ((this.maxyorg - this.miny) / rangey))),
+ spotRadius, undefined,
+ options.get('maxSpotColor')).append();
+ }
+ }
+
+ this.lastShapeId = target.getLastShapeId();
+ this.canvasTop = canvasTop;
+ target.render();
+ }
+ });
+
+ /**
+ * Bar charts
+ */
+ $.fn.sparkline.bar = bar = createClass($.fn.sparkline._base, barHighlightMixin, {
+ type: 'bar',
+
+ init: function (el, values, options, width, height) {
+ var barWidth = parseInt(options.get('barWidth'), 10),
+ barSpacing = parseInt(options.get('barSpacing'), 10),
+ chartRangeMin = options.get('chartRangeMin'),
+ chartRangeMax = options.get('chartRangeMax'),
+ chartRangeClip = options.get('chartRangeClip'),
+ stackMin = Infinity,
+ stackMax = -Infinity,
+ isStackString, groupMin, groupMax, stackRanges,
+ numValues, i, vlen, range, zeroAxis, xaxisOffset, min, max, clipMin, clipMax,
+ stacked, vlist, j, slen, svals, val, yoffset, yMaxCalc, canvasHeightEf;
+ bar._super.init.call(this, el, values, options, width, height);
+
+ // scan values to determine whether to stack bars
+ for (i = 0, vlen = values.length; i < vlen; i++) {
+ val = values[i];
+ isStackString = typeof(val) === 'string' && val.indexOf(':') > -1;
+ if (isStackString || $.isArray(val)) {
+ stacked = true;
+ if (isStackString) {
+ val = values[i] = normalizeValues(val.split(':'));
+ }
+ val = remove(val, null); // min/max will treat null as zero
+ groupMin = Math.min.apply(Math, val);
+ groupMax = Math.max.apply(Math, val);
+ if (groupMin < stackMin) {
+ stackMin = groupMin;
+ }
+ if (groupMax > stackMax) {
+ stackMax = groupMax;
+ }
+ }
+ }
+
+ this.stacked = stacked;
+ this.regionShapes = {};
+ this.barWidth = barWidth;
+ this.barSpacing = barSpacing;
+ this.totalBarWidth = barWidth + barSpacing;
+ this.width = width = (values.length * barWidth) + ((values.length - 1) * barSpacing);
+
+ this.initTarget();
+
+ if (chartRangeClip) {
+ clipMin = chartRangeMin === undefined ? -Infinity : chartRangeMin;
+ clipMax = chartRangeMax === undefined ? Infinity : chartRangeMax;
+ }
+
+ numValues = [];
+ stackRanges = stacked ? [] : numValues;
+ var stackTotals = [];
+ var stackRangesNeg = [];
+ for (i = 0, vlen = values.length; i < vlen; i++) {
+ if (stacked) {
+ vlist = values[i];
+ values[i] = svals = [];
+ stackTotals[i] = 0;
+ stackRanges[i] = stackRangesNeg[i] = 0;
+ for (j = 0, slen = vlist.length; j < slen; j++) {
+ val = svals[j] = chartRangeClip ? clipval(vlist[j], clipMin, clipMax) : vlist[j];
+ if (val !== null) {
+ if (val > 0) {
+ stackTotals[i] += val;
+ }
+ if (stackMin < 0 && stackMax > 0) {
+ if (val < 0) {
+ stackRangesNeg[i] += Math.abs(val);
+ } else {
+ stackRanges[i] += val;
+ }
+ } else {
+ stackRanges[i] += Math.abs(val - (val < 0 ? stackMax : stackMin));
+ }
+ numValues.push(val);
+ }
+ }
+ } else {
+ val = chartRangeClip ? clipval(values[i], clipMin, clipMax) : values[i];
+ val = values[i] = normalizeValue(val);
+ if (val !== null) {
+ numValues.push(val);
+ }
+ }
+ }
+ this.max = max = Math.max.apply(Math, numValues);
+ this.min = min = Math.min.apply(Math, numValues);
+ this.stackMax = stackMax = stacked ? Math.max.apply(Math, stackTotals) : max;
+ this.stackMin = stackMin = stacked ? Math.min.apply(Math, numValues) : min;
+
+ if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < min)) {
+ min = options.get('chartRangeMin');
+ }
+ if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > max)) {
+ max = options.get('chartRangeMax');
+ }
+
+ this.zeroAxis = zeroAxis = options.get('zeroAxis', true);
+ if (min <= 0 && max >= 0 && zeroAxis) {
+ xaxisOffset = 0;
+ } else if (zeroAxis == false) {
+ xaxisOffset = min;
+ } else if (min > 0) {
+ xaxisOffset = min;
+ } else {
+ xaxisOffset = max;
+ }
+ this.xaxisOffset = xaxisOffset;
+
+ range = stacked ? (Math.max.apply(Math, stackRanges) + Math.max.apply(Math, stackRangesNeg)) : max - min;
+
+ // as we plot zero/min values a single pixel line, we add a pixel to all other
+ // values - Reduce the effective canvas size to suit
+ this.canvasHeightEf = (zeroAxis && min < 0) ? this.canvasHeight - 2 : this.canvasHeight - 1;
+
+ if (min < xaxisOffset) {
+ yMaxCalc = (stacked && max >= 0) ? stackMax : max;
+ yoffset = (yMaxCalc - xaxisOffset) / range * this.canvasHeight;
+ if (yoffset !== Math.ceil(yoffset)) {
+ this.canvasHeightEf -= 2;
+ yoffset = Math.ceil(yoffset);
+ }
+ } else {
+ yoffset = this.canvasHeight;
+ }
+ this.yoffset = yoffset;
+
+ if ($.isArray(options.get('colorMap'))) {
+ this.colorMapByIndex = options.get('colorMap');
+ this.colorMapByValue = null;
+ } else {
+ this.colorMapByIndex = null;
+ this.colorMapByValue = options.get('colorMap');
+ if (this.colorMapByValue && this.colorMapByValue.get === undefined) {
+ this.colorMapByValue = new RangeMap(this.colorMapByValue);
+ }
+ }
+
+ this.range = range;
+ },
+
+ getRegion: function (el, x, y) {
+ var result = Math.floor(x / this.totalBarWidth);
+ return (result < 0 || result >= this.values.length) ? undefined : result;
+ },
+
+ getCurrentRegionFields: function () {
+ var currentRegion = this.currentRegion,
+ values = ensureArray(this.values[currentRegion]),
+ result = [],
+ value, i;
+ for (i = values.length; i--;) {
+ value = values[i];
+ result.push({
+ isNull: value === null,
+ value: value,
+ color: this.calcColor(i, value, currentRegion),
+ offset: currentRegion
+ });
+ }
+ return result;
+ },
+
+ calcColor: function (stacknum, value, valuenum) {
+ var colorMapByIndex = this.colorMapByIndex,
+ colorMapByValue = this.colorMapByValue,
+ options = this.options,
+ color, newColor;
+ if (this.stacked) {
+ color = options.get('stackedBarColor');
+ } else {
+ color = (value < 0) ? options.get('negBarColor') : options.get('barColor');
+ }
+ if (value === 0 && options.get('zeroColor') !== undefined) {
+ color = options.get('zeroColor');
+ }
+ if (colorMapByValue && (newColor = colorMapByValue.get(value))) {
+ color = newColor;
+ } else if (colorMapByIndex && colorMapByIndex.length > valuenum) {
+ color = colorMapByIndex[valuenum];
+ }
+ return $.isArray(color) ? color[stacknum % color.length] : color;
+ },
+
+ /**
+ * Render bar(s) for a region
+ */
+ renderRegion: function (valuenum, highlight) {
+ var vals = this.values[valuenum],
+ options = this.options,
+ xaxisOffset = this.xaxisOffset,
+ result = [],
+ range = this.range,
+ stacked = this.stacked,
+ target = this.target,
+ x = valuenum * this.totalBarWidth,
+ canvasHeightEf = this.canvasHeightEf,
+ yoffset = this.yoffset,
+ y, height, color, isNull, yoffsetNeg, i, valcount, val, minPlotted, allMin;
+
+ vals = $.isArray(vals) ? vals : [vals];
+ valcount = vals.length;
+ val = vals[0];
+ isNull = all(null, vals);
+ allMin = all(xaxisOffset, vals, true);
+
+ if (isNull) {
+ if (options.get('nullColor')) {
+ color = highlight ? options.get('nullColor') : this.calcHighlightColor(options.get('nullColor'), options);
+ y = (yoffset > 0) ? yoffset - 1 : yoffset;
+ return target.drawRect(x, y, this.barWidth - 1, 0, color, color);
+ } else {
+ return undefined;
+ }
+ }
+ yoffsetNeg = yoffset;
+ for (i = 0; i < valcount; i++) {
+ val = vals[i];
+
+ if (stacked && val === xaxisOffset) {
+ if (!allMin || minPlotted) {
+ continue;
+ }
+ minPlotted = true;
+ }
+
+ if (range > 0) {
+ height = Math.floor(canvasHeightEf * ((Math.abs(val - xaxisOffset) / range))) + 1;
+ } else {
+ height = 1;
+ }
+ if (val < xaxisOffset || (val === xaxisOffset && yoffset === 0)) {
+ y = yoffsetNeg;
+ yoffsetNeg += height;
+ } else {
+ y = yoffset - height;
+ yoffset -= height;
+ }
+ color = this.calcColor(i, val, valuenum);
+ if (highlight) {
+ color = this.calcHighlightColor(color, options);
+ }
+ result.push(target.drawRect(x, y, this.barWidth - 1, height - 1, color, color));
+ }
+ if (result.length === 1) {
+ return result[0];
+ }
+ return result;
+ }
+ });
+
+ /**
+ * Tristate charts
+ */
+ $.fn.sparkline.tristate = tristate = createClass($.fn.sparkline._base, barHighlightMixin, {
+ type: 'tristate',
+
+ init: function (el, values, options, width, height) {
+ var barWidth = parseInt(options.get('barWidth'), 10),
+ barSpacing = parseInt(options.get('barSpacing'), 10);
+ tristate._super.init.call(this, el, values, options, width, height);
+
+ this.regionShapes = {};
+ this.barWidth = barWidth;
+ this.barSpacing = barSpacing;
+ this.totalBarWidth = barWidth + barSpacing;
+ this.values = $.map(values, Number);
+ this.width = width = (values.length * barWidth) + ((values.length - 1) * barSpacing);
+
+ if ($.isArray(options.get('colorMap'))) {
+ this.colorMapByIndex = options.get('colorMap');
+ this.colorMapByValue = null;
+ } else {
+ this.colorMapByIndex = null;
+ this.colorMapByValue = options.get('colorMap');
+ if (this.colorMapByValue && this.colorMapByValue.get === undefined) {
+ this.colorMapByValue = new RangeMap(this.colorMapByValue);
+ }
+ }
+ this.initTarget();
+ },
+
+ getRegion: function (el, x, y) {
+ return Math.floor(x / this.totalBarWidth);
+ },
+
+ getCurrentRegionFields: function () {
+ var currentRegion = this.currentRegion;
+ return {
+ isNull: this.values[currentRegion] === undefined,
+ value: this.values[currentRegion],
+ color: this.calcColor(this.values[currentRegion], currentRegion),
+ offset: currentRegion
+ };
+ },
+
+ calcColor: function (value, valuenum) {
+ var values = this.values,
+ options = this.options,
+ colorMapByIndex = this.colorMapByIndex,
+ colorMapByValue = this.colorMapByValue,
+ color, newColor;
+
+ if (colorMapByValue && (newColor = colorMapByValue.get(value))) {
+ color = newColor;
+ } else if (colorMapByIndex && colorMapByIndex.length > valuenum) {
+ color = colorMapByIndex[valuenum];
+ } else if (values[valuenum] < 0) {
+ color = options.get('negBarColor');
+ } else if (values[valuenum] > 0) {
+ color = options.get('posBarColor');
+ } else {
+ color = options.get('zeroBarColor');
+ }
+ return color;
+ },
+
+ renderRegion: function (valuenum, highlight) {
+ var values = this.values,
+ options = this.options,
+ target = this.target,
+ canvasHeight, height, halfHeight,
+ x, y, color;
+
+ canvasHeight = target.pixelHeight;
+ halfHeight = Math.round(canvasHeight / 2);
+
+ x = valuenum * this.totalBarWidth;
+ if (values[valuenum] < 0) {
+ y = halfHeight;
+ height = halfHeight - 1;
+ } else if (values[valuenum] > 0) {
+ y = 0;
+ height = halfHeight - 1;
+ } else {
+ y = halfHeight - 1;
+ height = 2;
+ }
+ color = this.calcColor(values[valuenum], valuenum);
+ if (color === null) {
+ return;
+ }
+ if (highlight) {
+ color = this.calcHighlightColor(color, options);
+ }
+ return target.drawRect(x, y, this.barWidth - 1, height - 1, color, color);
+ }
+ });
+
+ /**
+ * Discrete charts
+ */
+ $.fn.sparkline.discrete = discrete = createClass($.fn.sparkline._base, barHighlightMixin, {
+ type: 'discrete',
+
+ init: function (el, values, options, width, height) {
+ discrete._super.init.call(this, el, values, options, width, height);
+
+ this.regionShapes = {};
+ this.values = values = $.map(values, Number);
+ this.min = Math.min.apply(Math, values);
+ this.max = Math.max.apply(Math, values);
+ this.range = this.max - this.min;
+ this.width = width = options.get('width') === 'auto' ? values.length * 2 : this.width;
+ this.interval = Math.floor(width / values.length);
+ this.itemWidth = width / values.length;
+ if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < this.min)) {
+ this.min = options.get('chartRangeMin');
+ }
+ if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > this.max)) {
+ this.max = options.get('chartRangeMax');
+ }
+ this.initTarget();
+ if (this.target) {
+ this.lineHeight = options.get('lineHeight') === 'auto' ? Math.round(this.canvasHeight * 0.3) : options.get('lineHeight');
+ }
+ },
+
+ getRegion: function (el, x, y) {
+ return Math.floor(x / this.itemWidth);
+ },
+
+ getCurrentRegionFields: function () {
+ var currentRegion = this.currentRegion;
+ return {
+ isNull: this.values[currentRegion] === undefined,
+ value: this.values[currentRegion],
+ offset: currentRegion
+ };
+ },
+
+ renderRegion: function (valuenum, highlight) {
+ var values = this.values,
+ options = this.options,
+ min = this.min,
+ max = this.max,
+ range = this.range,
+ interval = this.interval,
+ target = this.target,
+ canvasHeight = this.canvasHeight,
+ lineHeight = this.lineHeight,
+ pheight = canvasHeight - lineHeight,
+ ytop, val, color, x;
+
+ val = clipval(values[valuenum], min, max);
+ x = valuenum * interval;
+ ytop = Math.round(pheight - pheight * ((val - min) / range));
+ color = (options.get('thresholdColor') && val < options.get('thresholdValue')) ? options.get('thresholdColor') : options.get('lineColor');
+ if (highlight) {
+ color = this.calcHighlightColor(color, options);
+ }
+ return target.drawLine(x, ytop, x, ytop + lineHeight, color);
+ }
+ });
+
+ /**
+ * Bullet charts
+ */
+ $.fn.sparkline.bullet = bullet = createClass($.fn.sparkline._base, {
+ type: 'bullet',
+
+ init: function (el, values, options, width, height) {
+ var min, max, vals;
+ bullet._super.init.call(this, el, values, options, width, height);
+
+ // values: target, performance, range1, range2, range3
+ this.values = values = normalizeValues(values);
+ // target or performance could be null
+ vals = values.slice();
+ vals[0] = vals[0] === null ? vals[2] : vals[0];
+ vals[1] = values[1] === null ? vals[2] : vals[1];
+ min = Math.min.apply(Math, values);
+ max = Math.max.apply(Math, values);
+ if (options.get('base') === undefined) {
+ min = min < 0 ? min : 0;
+ } else {
+ min = options.get('base');
+ }
+ this.min = min;
+ this.max = max;
+ this.range = max - min;
+ this.shapes = {};
+ this.valueShapes = {};
+ this.regiondata = {};
+ this.width = width = options.get('width') === 'auto' ? '4.0em' : width;
+ this.target = this.$el.simpledraw(width, height, options.get('composite'));
+ if (!values.length) {
+ this.disabled = true;
+ }
+ this.initTarget();
+ },
+
+ getRegion: function (el, x, y) {
+ var shapeid = this.target.getShapeAt(el, x, y);
+ return (shapeid !== undefined && this.shapes[shapeid] !== undefined) ? this.shapes[shapeid] : undefined;
+ },
+
+ getCurrentRegionFields: function () {
+ var currentRegion = this.currentRegion;
+ return {
+ fieldkey: currentRegion.substr(0, 1),
+ value: this.values[currentRegion.substr(1)],
+ region: currentRegion
+ };
+ },
+
+ changeHighlight: function (highlight) {
+ var currentRegion = this.currentRegion,
+ shapeid = this.valueShapes[currentRegion],
+ shape;
+ delete this.shapes[shapeid];
+ switch (currentRegion.substr(0, 1)) {
+ case 'r':
+ shape = this.renderRange(currentRegion.substr(1), highlight);
+ break;
+ case 'p':
+ shape = this.renderPerformance(highlight);
+ break;
+ case 't':
+ shape = this.renderTarget(highlight);
+ break;
+ }
+ this.valueShapes[currentRegion] = shape.id;
+ this.shapes[shape.id] = currentRegion;
+ this.target.replaceWithShape(shapeid, shape);
+ },
+
+ renderRange: function (rn, highlight) {
+ var rangeval = this.values[rn],
+ rangewidth = Math.round(this.canvasWidth * ((rangeval - this.min) / this.range)),
+ color = this.options.get('rangeColors')[rn - 2];
+ if (highlight) {
+ color = this.calcHighlightColor(color, this.options);
+ }
+ return this.target.drawRect(0, 0, rangewidth - 1, this.canvasHeight - 1, color, color);
+ },
+
+ renderPerformance: function (highlight) {
+ var perfval = this.values[1],
+ perfwidth = Math.round(this.canvasWidth * ((perfval - this.min) / this.range)),
+ color = this.options.get('performanceColor');
+ if (highlight) {
+ color = this.calcHighlightColor(color, this.options);
+ }
+ return this.target.drawRect(0, Math.round(this.canvasHeight * 0.3), perfwidth - 1,
+ Math.round(this.canvasHeight * 0.4) - 1, color, color);
+ },
+
+ renderTarget: function (highlight) {
+ var targetval = this.values[0],
+ x = Math.round(this.canvasWidth * ((targetval - this.min) / this.range) - (this.options.get('targetWidth') / 2)),
+ targettop = Math.round(this.canvasHeight * 0.10),
+ targetheight = this.canvasHeight - (targettop * 2),
+ color = this.options.get('targetColor');
+ if (highlight) {
+ color = this.calcHighlightColor(color, this.options);
+ }
+ return this.target.drawRect(x, targettop, this.options.get('targetWidth') - 1, targetheight - 1, color, color);
+ },
+
+ render: function () {
+ var vlen = this.values.length,
+ target = this.target,
+ i, shape;
+ if (!bullet._super.render.call(this)) {
+ return;
+ }
+ for (i = 2; i < vlen; i++) {
+ shape = this.renderRange(i).append();
+ this.shapes[shape.id] = 'r' + i;
+ this.valueShapes['r' + i] = shape.id;
+ }
+ if (this.values[1] !== null) {
+ shape = this.renderPerformance().append();
+ this.shapes[shape.id] = 'p1';
+ this.valueShapes.p1 = shape.id;
+ }
+ if (this.values[0] !== null) {
+ shape = this.renderTarget().append();
+ this.shapes[shape.id] = 't0';
+ this.valueShapes.t0 = shape.id;
+ }
+ target.render();
+ }
+ });
+
+ /**
+ * Pie charts
+ */
+ $.fn.sparkline.pie = pie = createClass($.fn.sparkline._base, {
+ type: 'pie',
+
+ init: function (el, values, options, width, height) {
+ var total = 0, i;
+
+ pie._super.init.call(this, el, values, options, width, height);
+
+ this.shapes = {}; // map shape ids to value offsets
+ this.valueShapes = {}; // maps value offsets to shape ids
+ this.values = values = $.map(values, Number);
+
+ if (options.get('width') === 'auto') {
+ this.width = this.height;
+ }
+
+ if (values.length > 0) {
+ for (i = values.length; i--;) {
+ total += values[i];
+ }
+ }
+ this.total = total;
+ this.initTarget();
+ this.radius = Math.floor(Math.min(this.canvasWidth, this.canvasHeight) / 2);
+ },
+
+ getRegion: function (el, x, y) {
+ var shapeid = this.target.getShapeAt(el, x, y);
+ return (shapeid !== undefined && this.shapes[shapeid] !== undefined) ? this.shapes[shapeid] : undefined;
+ },
+
+ getCurrentRegionFields: function () {
+ var currentRegion = this.currentRegion;
+ return {
+ isNull: this.values[currentRegion] === undefined,
+ value: this.values[currentRegion],
+ percent: this.values[currentRegion] / this.total * 100,
+ color: this.options.get('sliceColors')[currentRegion % this.options.get('sliceColors').length],
+ offset: currentRegion
+ };
+ },
+
+ changeHighlight: function (highlight) {
+ var currentRegion = this.currentRegion,
+ newslice = this.renderSlice(currentRegion, highlight),
+ shapeid = this.valueShapes[currentRegion];
+ delete this.shapes[shapeid];
+ this.target.replaceWithShape(shapeid, newslice);
+ this.valueShapes[currentRegion] = newslice.id;
+ this.shapes[newslice.id] = currentRegion;
+ },
+
+ renderSlice: function (valuenum, highlight) {
+ var target = this.target,
+ options = this.options,
+ radius = this.radius,
+ borderWidth = options.get('borderWidth'),
+ offset = options.get('offset'),
+ circle = 2 * Math.PI,
+ values = this.values,
+ total = this.total,
+ next = offset ? (2*Math.PI)*(offset/360) : 0,
+ start, end, i, vlen, color;
+
+ vlen = values.length;
+ for (i = 0; i < vlen; i++) {
+ start = next;
+ end = next;
+ if (total > 0) { // avoid divide by zero
+ end = next + (circle * (values[i] / total));
+ }
+ if (valuenum === i) {
+ color = options.get('sliceColors')[i % options.get('sliceColors').length];
+ if (highlight) {
+ color = this.calcHighlightColor(color, options);
+ }
+
+ return target.drawPieSlice(radius, radius, radius - borderWidth, start, end, undefined, color);
+ }
+ next = end;
+ }
+ },
+
+ render: function () {
+ var target = this.target,
+ values = this.values,
+ options = this.options,
+ radius = this.radius,
+ borderWidth = options.get('borderWidth'),
+ shape, i;
+
+ if (!pie._super.render.call(this)) {
+ return;
+ }
+ if (borderWidth) {
+ target.drawCircle(radius, radius, Math.floor(radius - (borderWidth / 2)),
+ options.get('borderColor'), undefined, borderWidth).append();
+ }
+ for (i = values.length; i--;) {
+ if (values[i]) { // don't render zero values
+ shape = this.renderSlice(i).append();
+ this.valueShapes[i] = shape.id; // store just the shapeid
+ this.shapes[shape.id] = i;
+ }
+ }
+ target.render();
+ }
+ });
+
+ /**
+ * Box plots
+ */
+ $.fn.sparkline.box = box = createClass($.fn.sparkline._base, {
+ type: 'box',
+
+ init: function (el, values, options, width, height) {
+ box._super.init.call(this, el, values, options, width, height);
+ this.values = $.map(values, Number);
+ this.width = options.get('width') === 'auto' ? '4.0em' : width;
+ this.initTarget();
+ if (!this.values.length) {
+ this.disabled = 1;
+ }
+ },
+
+ /**
+ * Simulate a single region
+ */
+ getRegion: function () {
+ return 1;
+ },
+
+ getCurrentRegionFields: function () {
+ var result = [
+ { field: 'lq', value: this.quartiles[0] },
+ { field: 'med', value: this.quartiles[1] },
+ { field: 'uq', value: this.quartiles[2] }
+ ];
+ if (this.loutlier !== undefined) {
+ result.push({ field: 'lo', value: this.loutlier});
+ }
+ if (this.routlier !== undefined) {
+ result.push({ field: 'ro', value: this.routlier});
+ }
+ if (this.lwhisker !== undefined) {
+ result.push({ field: 'lw', value: this.lwhisker});
+ }
+ if (this.rwhisker !== undefined) {
+ result.push({ field: 'rw', value: this.rwhisker});
+ }
+ return result;
+ },
+
+ render: function () {
+ var target = this.target,
+ values = this.values,
+ vlen = values.length,
+ options = this.options,
+ canvasWidth = this.canvasWidth,
+ canvasHeight = this.canvasHeight,
+ minValue = options.get('chartRangeMin') === undefined ? Math.min.apply(Math, values) : options.get('chartRangeMin'),
+ maxValue = options.get('chartRangeMax') === undefined ? Math.max.apply(Math, values) : options.get('chartRangeMax'),
+ canvasLeft = 0,
+ lwhisker, loutlier, iqr, q1, q2, q3, rwhisker, routlier, i,
+ size, unitSize;
+
+ if (!box._super.render.call(this)) {
+ return;
+ }
+
+ if (options.get('raw')) {
+ if (options.get('showOutliers') && values.length > 5) {
+ loutlier = values[0];
+ lwhisker = values[1];
+ q1 = values[2];
+ q2 = values[3];
+ q3 = values[4];
+ rwhisker = values[5];
+ routlier = values[6];
+ } else {
+ lwhisker = values[0];
+ q1 = values[1];
+ q2 = values[2];
+ q3 = values[3];
+ rwhisker = values[4];
+ }
+ } else {
+ values.sort(function (a, b) { return a - b; });
+ q1 = quartile(values, 1);
+ q2 = quartile(values, 2);
+ q3 = quartile(values, 3);
+ iqr = q3 - q1;
+ if (options.get('showOutliers')) {
+ lwhisker = rwhisker = undefined;
+ for (i = 0; i < vlen; i++) {
+ if (lwhisker === undefined && values[i] > q1 - (iqr * options.get('outlierIQR'))) {
+ lwhisker = values[i];
+ }
+ if (values[i] < q3 + (iqr * options.get('outlierIQR'))) {
+ rwhisker = values[i];
+ }
+ }
+ loutlier = values[0];
+ routlier = values[vlen - 1];
+ } else {
+ lwhisker = values[0];
+ rwhisker = values[vlen - 1];
+ }
+ }
+ this.quartiles = [q1, q2, q3];
+ this.lwhisker = lwhisker;
+ this.rwhisker = rwhisker;
+ this.loutlier = loutlier;
+ this.routlier = routlier;
+
+ unitSize = canvasWidth / (maxValue - minValue + 1);
+ if (options.get('showOutliers')) {
+ canvasLeft = Math.ceil(options.get('spotRadius'));
+ canvasWidth -= 2 * Math.ceil(options.get('spotRadius'));
+ unitSize = canvasWidth / (maxValue - minValue + 1);
+ if (loutlier < lwhisker) {
+ target.drawCircle((loutlier - minValue) * unitSize + canvasLeft,
+ canvasHeight / 2,
+ options.get('spotRadius'),
+ options.get('outlierLineColor'),
+ options.get('outlierFillColor')).append();
+ }
+ if (routlier > rwhisker) {
+ target.drawCircle((routlier - minValue) * unitSize + canvasLeft,
+ canvasHeight / 2,
+ options.get('spotRadius'),
+ options.get('outlierLineColor'),
+ options.get('outlierFillColor')).append();
+ }
+ }
+
+ // box
+ target.drawRect(
+ Math.round((q1 - minValue) * unitSize + canvasLeft),
+ Math.round(canvasHeight * 0.1),
+ Math.round((q3 - q1) * unitSize),
+ Math.round(canvasHeight * 0.8),
+ options.get('boxLineColor'),
+ options.get('boxFillColor')).append();
+ // left whisker
+ target.drawLine(
+ Math.round((lwhisker - minValue) * unitSize + canvasLeft),
+ Math.round(canvasHeight / 2),
+ Math.round((q1 - minValue) * unitSize + canvasLeft),
+ Math.round(canvasHeight / 2),
+ options.get('lineColor')).append();
+ target.drawLine(
+ Math.round((lwhisker - minValue) * unitSize + canvasLeft),
+ Math.round(canvasHeight / 4),
+ Math.round((lwhisker - minValue) * unitSize + canvasLeft),
+ Math.round(canvasHeight - canvasHeight / 4),
+ options.get('whiskerColor')).append();
+ // right whisker
+ target.drawLine(Math.round((rwhisker - minValue) * unitSize + canvasLeft),
+ Math.round(canvasHeight / 2),
+ Math.round((q3 - minValue) * unitSize + canvasLeft),
+ Math.round(canvasHeight / 2),
+ options.get('lineColor')).append();
+ target.drawLine(
+ Math.round((rwhisker - minValue) * unitSize + canvasLeft),
+ Math.round(canvasHeight / 4),
+ Math.round((rwhisker - minValue) * unitSize + canvasLeft),
+ Math.round(canvasHeight - canvasHeight / 4),
+ options.get('whiskerColor')).append();
+ // median line
+ target.drawLine(
+ Math.round((q2 - minValue) * unitSize + canvasLeft),
+ Math.round(canvasHeight * 0.1),
+ Math.round((q2 - minValue) * unitSize + canvasLeft),
+ Math.round(canvasHeight * 0.9),
+ options.get('medianColor')).append();
+ if (options.get('target')) {
+ size = Math.ceil(options.get('spotRadius'));
+ target.drawLine(
+ Math.round((options.get('target') - minValue) * unitSize + canvasLeft),
+ Math.round((canvasHeight / 2) - size),
+ Math.round((options.get('target') - minValue) * unitSize + canvasLeft),
+ Math.round((canvasHeight / 2) + size),
+ options.get('targetColor')).append();
+ target.drawLine(
+ Math.round((options.get('target') - minValue) * unitSize + canvasLeft - size),
+ Math.round(canvasHeight / 2),
+ Math.round((options.get('target') - minValue) * unitSize + canvasLeft + size),
+ Math.round(canvasHeight / 2),
+ options.get('targetColor')).append();
+ }
+ target.render();
+ }
+ });
+
+ // Setup a very simple "virtual canvas" to make drawing the few shapes we need easier
+ // This is accessible as $(foo).simpledraw()
+
+ // Detect browser renderer support
+ (function() {
+ if (document.namespaces && !document.namespaces.v) {
+ $.fn.sparkline.hasVML = true;
+ document.namespaces.add('v', 'urn:schemas-microsoft-com:vml', '#default#VML');
+ } else {
+ $.fn.sparkline.hasVML = false;
+ }
+
+ var el = document.createElement('canvas');
+ $.fn.sparkline.hasCanvas = !!(el.getContext && el.getContext('2d'));
+
+ })()
+
+ VShape = createClass({
+ init: function (target, id, type, args) {
+ this.target = target;
+ this.id = id;
+ this.type = type;
+ this.args = args;
+ },
+ append: function () {
+ this.target.appendShape(this);
+ return this;
+ }
+ });
+
+ VCanvas_base = createClass({
+ _pxregex: /(\d+)(px)?\s*$/i,
+
+ init: function (width, height, target) {
+ if (!width) {
+ return;
+ }
+ this.width = width;
+ this.height = height;
+ this.target = target;
+ this.lastShapeId = null;
+ if (target[0]) {
+ target = target[0];
+ }
+ $.data(target, '_jqs_vcanvas', this);
+ },
+
+ drawLine: function (x1, y1, x2, y2, lineColor, lineWidth) {
+ return this.drawShape([[x1, y1], [x2, y2]], lineColor, lineWidth);
+ },
+
+ drawShape: function (path, lineColor, fillColor, lineWidth) {
+ return this._genShape('Shape', [path, lineColor, fillColor, lineWidth]);
+ },
+
+ drawCircle: function (x, y, radius, lineColor, fillColor, lineWidth) {
+ return this._genShape('Circle', [x, y, radius, lineColor, fillColor, lineWidth]);
+ },
+
+ drawPieSlice: function (x, y, radius, startAngle, endAngle, lineColor, fillColor) {
+ return this._genShape('PieSlice', [x, y, radius, startAngle, endAngle, lineColor, fillColor]);
+ },
+
+ drawRect: function (x, y, width, height, lineColor, fillColor) {
+ return this._genShape('Rect', [x, y, width, height, lineColor, fillColor]);
+ },
+
+ getElement: function () {
+ return this.canvas;
+ },
+
+ /**
+ * Return the most recently inserted shape id
+ */
+ getLastShapeId: function () {
+ return this.lastShapeId;
+ },
+
+ /**
+ * Clear and reset the canvas
+ */
+ reset: function () {
+ alert('reset not implemented');
+ },
+
+ _insert: function (el, target) {
+ $(target).html(el);
+ },
+
+ /**
+ * Calculate the pixel dimensions of the canvas
+ */
+ _calculatePixelDims: function (width, height, canvas) {
+ // XXX This should probably be a configurable option
+ var match;
+ match = this._pxregex.exec(height);
+ if (match) {
+ this.pixelHeight = match[1];
+ } else {
+ this.pixelHeight = $(canvas).height();
+ }
+ match = this._pxregex.exec(width);
+ if (match) {
+ this.pixelWidth = match[1];
+ } else {
+ this.pixelWidth = $(canvas).width();
+ }
+ },
+
+ /**
+ * Generate a shape object and id for later rendering
+ */
+ _genShape: function (shapetype, shapeargs) {
+ var id = shapeCount++;
+ shapeargs.unshift(id);
+ return new VShape(this, id, shapetype, shapeargs);
+ },
+
+ /**
+ * Add a shape to the end of the render queue
+ */
+ appendShape: function (shape) {
+ alert('appendShape not implemented');
+ },
+
+ /**
+ * Replace one shape with another
+ */
+ replaceWithShape: function (shapeid, shape) {
+ alert('replaceWithShape not implemented');
+ },
+
+ /**
+ * Insert one shape after another in the render queue
+ */
+ insertAfterShape: function (shapeid, shape) {
+ alert('insertAfterShape not implemented');
+ },
+
+ /**
+ * Remove a shape from the queue
+ */
+ removeShapeId: function (shapeid) {
+ alert('removeShapeId not implemented');
+ },
+
+ /**
+ * Find a shape at the specified x/y co-ordinates
+ */
+ getShapeAt: function (el, x, y) {
+ alert('getShapeAt not implemented');
+ },
+
+ /**
+ * Render all queued shapes onto the canvas
+ */
+ render: function () {
+ alert('render not implemented');
+ }
+ });
+
+ VCanvas_canvas = createClass(VCanvas_base, {
+ init: function (width, height, target, interact) {
+ VCanvas_canvas._super.init.call(this, width, height, target);
+ this.canvas = document.createElement('canvas');
+ if (target[0]) {
+ target = target[0];
+ }
+ $.data(target, '_jqs_vcanvas', this);
+ $(this.canvas).css({ display: 'inline-block', width: width, height: height, verticalAlign: 'top' });
+ this._insert(this.canvas, target);
+ this._calculatePixelDims(width, height, this.canvas);
+ this.canvas.width = this.pixelWidth;
+ this.canvas.height = this.pixelHeight;
+ this.interact = interact;
+ this.shapes = {};
+ this.shapeseq = [];
+ this.currentTargetShapeId = undefined;
+ $(this.canvas).css({width: this.pixelWidth, height: this.pixelHeight});
+ },
+
+ _getContext: function (lineColor, fillColor, lineWidth) {
+ var context = this.canvas.getContext('2d');
+ if (lineColor !== undefined) {
+ context.strokeStyle = lineColor;
+ }
+ context.lineWidth = lineWidth === undefined ? 1 : lineWidth;
+ if (fillColor !== undefined) {
+ context.fillStyle = fillColor;
+ }
+ return context;
+ },
+
+ reset: function () {
+ var context = this._getContext();
+ context.clearRect(0, 0, this.pixelWidth, this.pixelHeight);
+ this.shapes = {};
+ this.shapeseq = [];
+ this.currentTargetShapeId = undefined;
+ },
+
+ _drawShape: function (shapeid, path, lineColor, fillColor, lineWidth) {
+ var context = this._getContext(lineColor, fillColor, lineWidth),
+ i, plen;
+ context.beginPath();
+ context.moveTo(path[0][0] + 0.5, path[0][1] + 0.5);
+ for (i = 1, plen = path.length; i < plen; i++) {
+ context.lineTo(path[i][0] + 0.5, path[i][1] + 0.5); // the 0.5 offset gives us crisp pixel-width lines
+ }
+ if (lineColor !== undefined) {
+ context.stroke();
+ }
+ if (fillColor !== undefined) {
+ context.fill();
+ }
+ if (this.targetX !== undefined && this.targetY !== undefined &&
+ context.isPointInPath(this.targetX, this.targetY)) {
+ this.currentTargetShapeId = shapeid;
+ }
+ },
+
+ _drawCircle: function (shapeid, x, y, radius, lineColor, fillColor, lineWidth) {
+ var context = this._getContext(lineColor, fillColor, lineWidth);
+ context.beginPath();
+ context.arc(x, y, radius, 0, 2 * Math.PI, false);
+ if (this.targetX !== undefined && this.targetY !== undefined &&
+ context.isPointInPath(this.targetX, this.targetY)) {
+ this.currentTargetShapeId = shapeid;
+ }
+ if (lineColor !== undefined) {
+ context.stroke();
+ }
+ if (fillColor !== undefined) {
+ context.fill();
+ }
+ },
+
+ _drawPieSlice: function (shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) {
+ var context = this._getContext(lineColor, fillColor);
+ context.beginPath();
+ context.moveTo(x, y);
+ context.arc(x, y, radius, startAngle, endAngle, false);
+ context.lineTo(x, y);
+ context.closePath();
+ if (lineColor !== undefined) {
+ context.stroke();
+ }
+ if (fillColor) {
+ context.fill();
+ }
+ if (this.targetX !== undefined && this.targetY !== undefined &&
+ context.isPointInPath(this.targetX, this.targetY)) {
+ this.currentTargetShapeId = shapeid;
+ }
+ },
+
+ _drawRect: function (shapeid, x, y, width, height, lineColor, fillColor) {
+ return this._drawShape(shapeid, [[x, y], [x + width, y], [x + width, y + height], [x, y + height], [x, y]], lineColor, fillColor);
+ },
+
+ appendShape: function (shape) {
+ this.shapes[shape.id] = shape;
+ this.shapeseq.push(shape.id);
+ this.lastShapeId = shape.id;
+ return shape.id;
+ },
+
+ replaceWithShape: function (shapeid, shape) {
+ var shapeseq = this.shapeseq,
+ i;
+ this.shapes[shape.id] = shape;
+ for (i = shapeseq.length; i--;) {
+ if (shapeseq[i] == shapeid) {
+ shapeseq[i] = shape.id;
+ }
+ }
+ delete this.shapes[shapeid];
+ },
+
+ replaceWithShapes: function (shapeids, shapes) {
+ var shapeseq = this.shapeseq,
+ shapemap = {},
+ sid, i, first;
+
+ for (i = shapeids.length; i--;) {
+ shapemap[shapeids[i]] = true;
+ }
+ for (i = shapeseq.length; i--;) {
+ sid = shapeseq[i];
+ if (shapemap[sid]) {
+ shapeseq.splice(i, 1);
+ delete this.shapes[sid];
+ first = i;
+ }
+ }
+ for (i = shapes.length; i--;) {
+ shapeseq.splice(first, 0, shapes[i].id);
+ this.shapes[shapes[i].id] = shapes[i];
+ }
+
+ },
+
+ insertAfterShape: function (shapeid, shape) {
+ var shapeseq = this.shapeseq,
+ i;
+ for (i = shapeseq.length; i--;) {
+ if (shapeseq[i] === shapeid) {
+ shapeseq.splice(i + 1, 0, shape.id);
+ this.shapes[shape.id] = shape;
+ return;
+ }
+ }
+ },
+
+ removeShapeId: function (shapeid) {
+ var shapeseq = this.shapeseq,
+ i;
+ for (i = shapeseq.length; i--;) {
+ if (shapeseq[i] === shapeid) {
+ shapeseq.splice(i, 1);
+ break;
+ }
+ }
+ delete this.shapes[shapeid];
+ },
+
+ getShapeAt: function (el, x, y) {
+ this.targetX = x;
+ this.targetY = y;
+ this.render();
+ return this.currentTargetShapeId;
+ },
+
+ render: function () {
+ var shapeseq = this.shapeseq,
+ shapes = this.shapes,
+ shapeCount = shapeseq.length,
+ context = this._getContext(),
+ shapeid, shape, i;
+ context.clearRect(0, 0, this.pixelWidth, this.pixelHeight);
+ for (i = 0; i < shapeCount; i++) {
+ shapeid = shapeseq[i];
+ shape = shapes[shapeid];
+ this['_draw' + shape.type].apply(this, shape.args);
+ }
+ if (!this.interact) {
+ // not interactive so no need to keep the shapes array
+ this.shapes = {};
+ this.shapeseq = [];
+ }
+ }
+
+ });
+
+ VCanvas_vml = createClass(VCanvas_base, {
+ init: function (width, height, target) {
+ var groupel;
+ VCanvas_vml._super.init.call(this, width, height, target);
+ if (target[0]) {
+ target = target[0];
+ }
+ $.data(target, '_jqs_vcanvas', this);
+ this.canvas = document.createElement('span');
+ $(this.canvas).css({ display: 'inline-block', position: 'relative', overflow: 'hidden', width: width, height: height, margin: '0px', padding: '0px', verticalAlign: 'top'});
+ this._insert(this.canvas, target);
+ this._calculatePixelDims(width, height, this.canvas);
+ this.canvas.width = this.pixelWidth;
+ this.canvas.height = this.pixelHeight;
+ groupel = '';
+ this.canvas.insertAdjacentHTML('beforeEnd', groupel);
+ this.group = $(this.canvas).children()[0];
+ this.rendered = false;
+ this.prerender = '';
+ },
+
+ _drawShape: function (shapeid, path, lineColor, fillColor, lineWidth) {
+ var vpath = [],
+ initial, stroke, fill, closed, vel, plen, i;
+ for (i = 0, plen = path.length; i < plen; i++) {
+ vpath[i] = '' + (path[i][0]) + ',' + (path[i][1]);
+ }
+ initial = vpath.splice(0, 1);
+ lineWidth = lineWidth === undefined ? 1 : lineWidth;
+ stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" ';
+ fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
+ closed = vpath[0] === vpath[vpath.length - 1] ? 'x ' : '';
+ vel = '' +
+ ' ';
+ return vel;
+ },
+
+ _drawCircle: function (shapeid, x, y, radius, lineColor, fillColor, lineWidth) {
+ var stroke, fill, vel;
+ x -= radius;
+ y -= radius;
+ stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" ';
+ fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
+ vel = '';
+ return vel;
+
+ },
+
+ _drawPieSlice: function (shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) {
+ var vpath, startx, starty, endx, endy, stroke, fill, vel;
+ if (startAngle === endAngle) {
+ return ''; // VML seems to have problem when start angle equals end angle.
+ }
+ if ((endAngle - startAngle) === (2 * Math.PI)) {
+ startAngle = 0.0; // VML seems to have a problem when drawing a full circle that doesn't start 0
+ endAngle = (2 * Math.PI);
+ }
+
+ startx = x + Math.round(Math.cos(startAngle) * radius);
+ starty = y + Math.round(Math.sin(startAngle) * radius);
+ endx = x + Math.round(Math.cos(endAngle) * radius);
+ endy = y + Math.round(Math.sin(endAngle) * radius);
+
+ if (startx === endx && starty === endy) {
+ if ((endAngle - startAngle) < Math.PI) {
+ // Prevent very small slices from being mistaken as a whole pie
+ return '';
+ }
+ // essentially going to be the entire circle, so ignore startAngle
+ startx = endx = x + radius;
+ starty = endy = y;
+ }
+
+ if (startx === endx && starty === endy && (endAngle - startAngle) < Math.PI) {
+ return '';
+ }
+
+ vpath = [x - radius, y - radius, x + radius, y + radius, startx, starty, endx, endy];
+ stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="1px" strokeColor="' + lineColor + '" ';
+ fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
+ vel = '' +
+ ' ';
+ return vel;
+ },
+
+ _drawRect: function (shapeid, x, y, width, height, lineColor, fillColor) {
+ return this._drawShape(shapeid, [[x, y], [x, y + height], [x + width, y + height], [x + width, y], [x, y]], lineColor, fillColor);
+ },
+
+ reset: function () {
+ this.group.innerHTML = '';
+ },
+
+ appendShape: function (shape) {
+ var vel = this['_draw' + shape.type].apply(this, shape.args);
+ if (this.rendered) {
+ this.group.insertAdjacentHTML('beforeEnd', vel);
+ } else {
+ this.prerender += vel;
+ }
+ this.lastShapeId = shape.id;
+ return shape.id;
+ },
+
+ replaceWithShape: function (shapeid, shape) {
+ var existing = $('#jqsshape' + shapeid),
+ vel = this['_draw' + shape.type].apply(this, shape.args);
+ existing[0].outerHTML = vel;
+ },
+
+ replaceWithShapes: function (shapeids, shapes) {
+ // replace the first shapeid with all the new shapes then toast the remaining old shapes
+ var existing = $('#jqsshape' + shapeids[0]),
+ replace = '',
+ slen = shapes.length,
+ i;
+ for (i = 0; i < slen; i++) {
+ replace += this['_draw' + shapes[i].type].apply(this, shapes[i].args);
+ }
+ existing[0].outerHTML = replace;
+ for (i = 1; i < shapeids.length; i++) {
+ $('#jqsshape' + shapeids[i]).remove();
+ }
+ },
+
+ insertAfterShape: function (shapeid, shape) {
+ var existing = $('#jqsshape' + shapeid),
+ vel = this['_draw' + shape.type].apply(this, shape.args);
+ existing[0].insertAdjacentHTML('afterEnd', vel);
+ },
+
+ removeShapeId: function (shapeid) {
+ var existing = $('#jqsshape' + shapeid);
+ this.group.removeChild(existing[0]);
+ },
+
+ getShapeAt: function (el, x, y) {
+ var shapeid = el.id.substr(8);
+ return shapeid;
+ },
+
+ render: function () {
+ if (!this.rendered) {
+ // batch the intial render into a single repaint
+ this.group.innerHTML = this.prerender;
+ this.rendered = true;
+ }
+ }
+ });
+
+}));
diff --git a/addons/web/static/lib/justgage/justgage.1.0.1.js b/addons/web/static/lib/justgage/justgage.1.0.1.js
new file mode 100644
index 00000000000..24540beb293
--- /dev/null
+++ b/addons/web/static/lib/justgage/justgage.1.0.1.js
@@ -0,0 +1,946 @@
+/**
+ * JustGage - this is work-in-progress, unreleased, unofficial code, so it might not work top-notch :)
+ * Check http://www.justgage.com for official releases
+ * Licensed under MIT.
+ * @author Bojan Djuricic (@Toorshia)
+ *
+ * LATEST UPDATES
+
+ * -----------------------------
+ * April 18, 2013.
+ * -----------------------------
+ * parentNode - use this instead of id, to attach gauge to node which is outside of DOM tree - https://github.com/toorshia/justgage/issues/48
+ * width - force gauge width
+ * height - force gauge height
+
+ * -----------------------------
+ * April 17, 2013.
+ * -----------------------------
+ * fix - https://github.com/toorshia/justgage/issues/49
+
+ * -----------------------------
+ * April 01, 2013.
+ * -----------------------------
+ * fix - https://github.com/toorshia/justgage/issues/46
+
+ * -----------------------------
+ * March 26, 2013.
+ * -----------------------------
+ * customSectors - define specific color for value range (0-10 : red, 10-30 : blue etc.)
+
+ * -----------------------------
+ * March 23, 2013.
+ * -----------------------------
+ * counter - option to animate value in counting fashion
+ * fix - https://github.com/toorshia/justgage/issues/45
+
+ * -----------------------------
+ * March 13, 2013.
+ * -----------------------------
+ * refresh method - added optional 'max' parameter to use when you need to update max value
+
+ * -----------------------------
+ * February 26, 2013.
+ * -----------------------------
+ * decimals - option to define/limit number of decimals when not using humanFriendly or customRenderer to display value
+ * fixed a missing parameters bug when calling generateShadow() for IE < 9
+
+ * -----------------------------
+ * December 31, 2012.
+ * -----------------------------
+ * fixed text y-position for hidden divs - workaround for Raphael 'dy' bug - https://github.com/DmitryBaranovskiy/raphael/issues/491
+ * 'show' parameters, like showMinMax are now 'hide' because I am lame developer - please update these in your setups
+ * Min and Max labels are now auto-off when in donut mode
+ * Start angle in donut mode is now 90
+ * donutStartAngle - option to define start angle for donut
+
+ * -----------------------------
+ * November 25, 2012.
+ * -----------------------------
+ * Option to define custom rendering function for displayed value
+
+ * -----------------------------
+ * November 19, 2012.
+ * -----------------------------
+ * Config.value is now updated after gauge refresh
+
+ * -----------------------------
+ * November 13, 2012.
+ * -----------------------------
+ * Donut display mode added
+ * Option to hide value label
+ * Option to enable responsive gauge size
+ * Removed default title attribute
+ * Option to accept min and max defined as string values
+ * Option to configure value symbol
+ * Fixed bad aspect ratio calculations
+ * Option to configure minimum font size for all texts
+ * Option to show shorthand big numbers (human friendly)
+ */
+
+ JustGage = function(config) {
+
+ // if (!config.id) {alert("Missing id parameter for gauge!"); return false;}
+ // if (!document.getElementById(config.id)) {alert("No element with id: \""+config.id+"\" found!"); return false;}
+
+ var obj = this;
+
+ // configurable parameters
+ obj.config =
+ {
+ // id : string
+ // this is container element id
+ id : config.id,
+
+ // parentNode : node object
+ // this is container element
+ parentNode : (config.parentNode) ? config.parentNode : null,
+
+ // width : int
+ // gauge width
+ width : (config.width) ? config.width : null,
+
+ // height : int
+ // gauge height
+ height : (config.height) ? config.height : null,
+
+ // title : string
+ // gauge title
+ title : (config.title) ? config.title : "",
+
+ // titleFontColor : string
+ // color of gauge title
+ titleFontColor : (config.titleFontColor) ? config.titleFontColor : "#999999",
+
+ // value : int
+ // value gauge is showing
+ value : (config.value) ? config.value : 0,
+
+ // valueFontColor : string
+ // color of label showing current value
+ valueFontColor : (config.valueFontColor) ? config.valueFontColor : "#010101",
+
+ // symbol : string
+ // special symbol to show next to value
+ symbol : (config.symbol) ? config.symbol : "",
+
+ // min : int
+ // min value
+ min : (config.min !== undefined) ? parseFloat(config.min) : 0,
+
+ // max : int
+ // max value
+ max : (config.max !== undefined) ? parseFloat(config.max) : 100,
+
+ // humanFriendlyDecimal : int
+ // number of decimal places for our human friendly number to contain
+ humanFriendlyDecimal : (config.humanFriendlyDecimal) ? config.humanFriendlyDecimal : 0,
+
+ // textRenderer: func
+ // function applied before rendering text
+ textRenderer : (config.textRenderer) ? config.textRenderer : null,
+
+ // gaugeWidthScale : float
+ // width of the gauge element
+ gaugeWidthScale : (config.gaugeWidthScale) ? config.gaugeWidthScale : 1.0,
+
+ // gaugeColor : string
+ // background color of gauge element
+ gaugeColor : (config.gaugeColor) ? config.gaugeColor : "#edebeb",
+
+ // label : string
+ // text to show below value
+ label : (config.label) ? config.label : "",
+
+ // labelFontColor : string
+ // color of label showing label under value
+ labelFontColor : (config.labelFontColor) ? config.labelFontColor : "#b3b3b3",
+
+ // shadowOpacity : int
+ // 0 ~ 1
+ shadowOpacity : (config.shadowOpacity) ? config.shadowOpacity : 0.2,
+
+ // shadowSize: int
+ // inner shadow size
+ shadowSize : (config.shadowSize) ? config.shadowSize : 5,
+
+ // shadowVerticalOffset : int
+ // how much shadow is offset from top
+ shadowVerticalOffset : (config.shadowVerticalOffset) ? config.shadowVerticalOffset : 3,
+
+ // levelColors : string[]
+ // colors of indicator, from lower to upper, in RGB format
+ levelColors : (config.levelColors) ? config.levelColors : [
+ "#a9d70b",
+ "#f9c802",
+ "#ff0000"
+ ],
+
+ // startAnimationTime : int
+ // length of initial animation
+ startAnimationTime : (config.startAnimationTime) ? config.startAnimationTime : 700,
+
+ // startAnimationType : string
+ // type of initial animation (linear, >, <, <>, bounce)
+ startAnimationType : (config.startAnimationType) ? config.startAnimationType : ">",
+
+ // refreshAnimationTime : int
+ // length of refresh animation
+ refreshAnimationTime : (config.refreshAnimationTime) ? config.refreshAnimationTime : 700,
+
+ // refreshAnimationType : string
+ // type of refresh animation (linear, >, <, <>, bounce)
+ refreshAnimationType : (config.refreshAnimationType) ? config.refreshAnimationType : ">",
+
+ // donutStartAngle : int
+ // angle to start from when in donut mode
+ donutStartAngle : (config.donutStartAngle) ? config.donutStartAngle : 90,
+
+ // valueMinFontSize : int
+ // absolute minimum font size for the value
+ valueMinFontSize : config.valueMinFontSize || 16,
+
+ // titleMinFontSize
+ // absolute minimum font size for the title
+ titleMinFontSize : config.titleMinFontSize || 10,
+
+ // labelMinFontSize
+ // absolute minimum font size for the label
+ labelMinFontSize : config.labelMinFontSize || 10,
+
+ // minLabelMinFontSize
+ // absolute minimum font size for the minimum label
+ minLabelMinFontSize : config.minLabelMinFontSize || 10,
+
+ // maxLabelMinFontSize
+ // absolute minimum font size for the maximum label
+ maxLabelMinFontSize : config.maxLabelMinFontSize || 10,
+
+ // hideValue : bool
+ // hide value text
+ hideValue : (config.hideValue) ? config.hideValue : false,
+
+ // hideMinMax : bool
+ // hide min and max values
+ hideMinMax : (config.hideMinMax) ? config.hideMinMax : false,
+
+ // hideInnerShadow : bool
+ // hide inner shadow
+ hideInnerShadow : (config.hideInnerShadow) ? config.hideInnerShadow : false,
+
+ // humanFriendly : bool
+ // convert large numbers for min, max, value to human friendly (e.g. 1234567 -> 1.23M)
+ humanFriendly : (config.humanFriendly) ? config.humanFriendly : false,
+
+ // noGradient : bool
+ // whether to use gradual color change for value, or sector-based
+ noGradient : (config.noGradient) ? config.noGradient : false,
+
+ // donut : bool
+ // show full donut gauge
+ donut : (config.donut) ? config.donut : false,
+
+ // relativeGaugeSize : bool
+ // whether gauge size should follow changes in container element size
+ relativeGaugeSize : (config.relativeGaugeSize) ? config.relativeGaugeSize : false,
+
+ // counter : bool
+ // animate level number change
+ counter : (config.counter) ? config.counter : false,
+
+ // decimals : int
+ // number of digits after floating point
+ decimals : (config.decimals) ? config.decimals : 0,
+
+ // customSectors : [] of objects
+ // number of digits after floating point
+ customSectors : (config.customSectors) ? config.customSectors : []
+ };
+
+ // variables
+ var
+ canvasW,
+ canvasH,
+ widgetW,
+ widgetH,
+ aspect,
+ dx,
+ dy,
+ titleFontSize,
+ titleX,
+ titleY,
+ valueFontSize,
+ valueX,
+ valueY,
+ labelFontSize,
+ labelX,
+ labelY,
+ minFontSize,
+ minX,
+ minY,
+ maxFontSize,
+ maxX,
+ maxY;
+
+ // overflow values
+ if (obj.config.value > obj.config.max) obj.config.value = obj.config.max;
+ if (obj.config.value < obj.config.min) obj.config.value = obj.config.min;
+ obj.originalValue = config.value;
+
+ // create canvas
+ if (obj.config.id !== null && (document.getElementById(obj.config.id)) !== null) {
+ obj.canvas = Raphael(obj.config.id, "100%", "100%");
+ } else if (obj.config.parentNode !== null) {
+ obj.canvas = Raphael(obj.config.parentNode, "100%", "100%");
+ }
+
+ if (obj.config.relativeGaugeSize === true) {
+ obj.canvas.setViewBox(0, 0, 200, 150, true);
+ }
+
+ // canvas dimensions
+ if (obj.config.relativeGaugeSize === true) {
+ canvasW = 200;
+ canvasH = 150;
+ } else if (obj.config.width !== null && obj.config.height !== null) {
+ canvasW = obj.config.width;
+ canvasH = obj.config.height;
+ } else if (obj.config.parentNode !== null) {
+ obj.canvas.setViewBox(0, 0, 200, 150, true);
+ canvasW = 200;
+ canvasH = 150;
+ } else {
+ canvasW = getStyle(document.getElementById(obj.config.id), "width").slice(0, -2) * 1;
+ canvasH = getStyle(document.getElementById(obj.config.id), "height").slice(0, -2) * 1;
+ }
+
+ // widget dimensions
+ if (obj.config.donut === true) {
+
+ // DONUT *******************************
+
+ // width more than height
+ if(canvasW > canvasH) {
+ widgetH = canvasH;
+ widgetW = widgetH;
+ // width less than height
+ } else if (canvasW < canvasH) {
+ widgetW = canvasW;
+ widgetH = widgetW;
+ // if height don't fit, rescale both
+ if(widgetH > canvasH) {
+ aspect = widgetH / canvasH;
+ widgetH = widgetH / aspect;
+ widgetW = widgetH / aspect;
+ }
+ // equal
+ } else {
+ widgetW = canvasW;
+ widgetH = widgetW;
+ }
+
+ // delta
+ dx = (canvasW - widgetW)/2;
+ dy = (canvasH - widgetH)/2;
+
+ // title
+ titleFontSize = ((widgetH / 8) > 10) ? (widgetH / 10) : 10;
+ titleX = dx + widgetW / 2;
+ titleY = dy + widgetH / 11;
+
+ // value
+ valueFontSize = ((widgetH / 6.4) > 16) ? (widgetH / 5.4) : 18;
+ valueX = dx + widgetW / 2;
+ if(obj.config.label !== '') {
+ valueY = dy + widgetH / 1.85;
+ } else {
+ valueY = dy + widgetH / 1.7;
+ }
+
+ // label
+ labelFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10;
+ labelX = dx + widgetW / 2;
+ labelY = valueY + labelFontSize;
+
+ // min
+ minFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10;
+ minX = dx + (widgetW / 10) + (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
+ minY = labelY;
+
+ // max
+ maxFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10;
+ maxX = dx + widgetW - (widgetW / 10) - (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
+ maxY = labelY;
+
+ } else {
+ // HALF *******************************
+
+ // width more than height
+ if(canvasW > canvasH) {
+ widgetH = canvasH;
+ widgetW = widgetH * 1.25;
+ //if width doesn't fit, rescale both
+ if(widgetW > canvasW) {
+ aspect = widgetW / canvasW;
+ widgetW = widgetW / aspect;
+ widgetH = widgetH / aspect;
+ }
+ // width less than height
+ } else if (canvasW < canvasH) {
+ widgetW = canvasW;
+ widgetH = widgetW / 1.25;
+ // if height don't fit, rescale both
+ if(widgetH > canvasH) {
+ aspect = widgetH / canvasH;
+ widgetH = widgetH / aspect;
+ widgetW = widgetH / aspect;
+ }
+ // equal
+ } else {
+ widgetW = canvasW;
+ widgetH = widgetW * 0.75;
+ }
+
+ // delta
+ dx = (canvasW - widgetW)/2;
+ dy = (canvasH - widgetH)/2;
+
+ // title
+ titleFontSize = ((widgetH / 8) > obj.config.titleMinFontSize) ? (widgetH / 10) : obj.config.titleMinFontSize;
+ titleX = dx + widgetW / 2;
+ titleY = dy + widgetH / 6.4;
+
+ // value
+ valueFontSize = ((widgetH / 6.5) > obj.config.valueMinFontSize) ? (widgetH / 6.5) : obj.config.valueMinFontSize;
+ valueX = dx + widgetW / 2;
+ valueY = dy + widgetH / 1.275;
+
+ // label
+ labelFontSize = ((widgetH / 16) > obj.config.labelMinFontSize) ? (widgetH / 16) : obj.config.labelMinFontSize;
+ labelX = dx + widgetW / 2;
+ labelY = valueY + valueFontSize / 2 + 5;
+
+ // min
+ minFontSize = ((widgetH / 16) > obj.config.minLabelMinFontSize) ? (widgetH / 16) : obj.config.minLabelMinFontSize;
+ minX = dx + (widgetW / 10) + (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
+ minY = labelY;
+
+ // max
+ maxFontSize = ((widgetH / 16) > obj.config.maxLabelMinFontSize) ? (widgetH / 16) : obj.config.maxLabelMinFontSize;
+ maxX = dx + widgetW - (widgetW / 10) - (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
+ maxY = labelY;
+ }
+
+ // parameters
+ obj.params = {
+ canvasW : canvasW,
+ canvasH : canvasH,
+ widgetW : widgetW,
+ widgetH : widgetH,
+ dx : dx,
+ dy : dy,
+ titleFontSize : titleFontSize,
+ titleX : titleX,
+ titleY : titleY,
+ valueFontSize : valueFontSize,
+ valueX : valueX,
+ valueY : valueY,
+ labelFontSize : labelFontSize,
+ labelX : labelX,
+ labelY : labelY,
+ minFontSize : minFontSize,
+ minX : minX,
+ minY : minY,
+ maxFontSize : maxFontSize,
+ maxX : maxX,
+ maxY : maxY
+ };
+
+ // var clear
+ canvasW, canvasH, widgetW, widgetH, aspect, dx, dy, titleFontSize, titleX, titleY, valueFontSize, valueX, valueY, labelFontSize, labelX, labelY, minFontSize, minX, minY, maxFontSize, maxX, maxY = null
+
+ // pki - custom attribute for generating gauge paths
+ obj.canvas.customAttributes.pki = function (value, min, max, w, h, dx, dy, gws, donut) {
+
+ var alpha, Ro, Ri, Cx, Cy, Xo, Yo, Xi, Yi, path;
+
+ if (donut) {
+ alpha = (1 - 2 * (value - min) / (max - min)) * Math.PI;
+ Ro = w / 2 - w / 7;
+ Ri = Ro - w / 6.666666666666667 * gws;
+
+ Cx = w / 2 + dx;
+ Cy = h / 1.95 + dy;
+
+ Xo = w / 2 + dx + Ro * Math.cos(alpha);
+ Yo = h - (h - Cy) + 0 - Ro * Math.sin(alpha);
+ Xi = w / 2 + dx + Ri * Math.cos(alpha);
+ Yi = h - (h - Cy) + 0 - Ri * Math.sin(alpha);
+
+ path += "M" + (Cx - Ri) + "," + Cy + " ";
+ path += "L" + (Cx - Ro) + "," + Cy + " ";
+ if (value > ((max - min) / 2)) {
+ path += "A" + Ro + "," + Ro + " 0 0 1 " + (Cx + Ro) + "," + Cy + " ";
+ }
+ path += "A" + Ro + "," + Ro + " 0 0 1 " + Xo + "," + Yo + " ";
+ path += "L" + Xi + "," + Yi + " ";
+ if (value > ((max - min) / 2)) {
+ path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx + Ri) + "," + Cy + " ";
+ }
+ path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx - Ri) + "," + Cy + " ";
+ path += "Z ";
+
+ return { path: path };
+
+ } else {
+ alpha = (1 - (value - min) / (max - min)) * Math.PI;
+ Ro = w / 2 - w / 10;
+ Ri = Ro - w / 6.666666666666667 * gws;
+
+ Cx = w / 2 + dx;
+ Cy = h / 1.25 + dy;
+
+ Xo = w / 2 + dx + Ro * Math.cos(alpha);
+ Yo = h - (h - Cy) + 0 - Ro * Math.sin(alpha);
+ Xi = w / 2 + dx + Ri * Math.cos(alpha);
+ Yi = h - (h - Cy) + 0 - Ri * Math.sin(alpha);
+
+ path += "M" + (Cx - Ri) + "," + Cy + " ";
+ path += "L" + (Cx - Ro) + "," + Cy + " ";
+ path += "A" + Ro + "," + Ro + " 0 0 1 " + Xo + "," + Yo + " ";
+ path += "L" + Xi + "," + Yi + " ";
+ path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx - Ri) + "," + Cy + " ";
+ path += "Z ";
+
+ return { path: path };
+ }
+
+ // var clear
+ alpha, Ro, Ri, Cx, Cy, Xo, Yo, Xi, Yi, path = null;
+ };
+
+ // gauge
+ obj.gauge = obj.canvas.path().attr({
+ "stroke": "none",
+ "fill": obj.config.gaugeColor,
+ pki: [
+ obj.config.max,
+ obj.config.min,
+ obj.config.max,
+ obj.params.widgetW,
+ obj.params.widgetH,
+ obj.params.dx,
+ obj.params.dy,
+ obj.config.gaugeWidthScale,
+ obj.config.donut
+ ]
+ });
+
+ // level
+ obj.level = obj.canvas.path().attr({
+ "stroke": "none",
+ "fill": getColor(obj.config.value, (obj.config.value - obj.config.min) / (obj.config.max - obj.config.min), obj.config.levelColors, obj.config.noGradient, obj.config.customSectors),
+ pki: [
+ obj.config.min,
+ obj.config.min,
+ obj.config.max,
+ obj.params.widgetW,
+ obj.params.widgetH,
+ obj.params.dx,
+ obj.params.dy,
+ obj.config.gaugeWidthScale,
+ obj.config.donut
+ ]
+ });
+ if(obj.config.donut) {
+ obj.level.transform("r" + obj.config.donutStartAngle + ", " + (obj.params.widgetW/2 + obj.params.dx) + ", " + (obj.params.widgetH/1.95 + obj.params.dy));
+ }
+
+ // title
+ obj.txtTitle = obj.canvas.text(obj.params.titleX, obj.params.titleY, obj.config.title);
+ obj.txtTitle.attr({
+ "font-size":obj.params.titleFontSize,
+ "font-weight":"bold",
+ "font-family":"Arial",
+ "fill":obj.config.titleFontColor,
+ "fill-opacity":"1"
+ });
+ setDy(obj.txtTitle, obj.params.titleFontSize, obj.params.titleY);
+
+ // value
+ obj.txtValue = obj.canvas.text(obj.params.valueX, obj.params.valueY, 0);
+ obj.txtValue.attr({
+ "font-size":obj.params.valueFontSize,
+ "font-weight":"bold",
+ "font-family":"Arial",
+ "fill":obj.config.valueFontColor,
+ "fill-opacity":"0"
+ });
+ setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
+
+ // label
+ obj.txtLabel = obj.canvas.text(obj.params.labelX, obj.params.labelY, obj.config.label);
+ obj.txtLabel.attr({
+ "font-size":obj.params.labelFontSize,
+ "font-weight":"normal",
+ "font-family":"Arial",
+ "fill":obj.config.labelFontColor,
+ "fill-opacity":"0"
+ });
+ setDy(obj.txtLabel, obj.params.labelFontSize, obj.params.labelY);
+
+ // min
+ obj.txtMinimum = obj.config.min;
+ if( obj.config.humanFriendly ) obj.txtMinimum = humanFriendlyNumber( obj.config.min, obj.config.humanFriendlyDecimal );
+ obj.txtMin = obj.canvas.text(obj.params.minX, obj.params.minY, obj.txtMinimum);
+ obj.txtMin.attr({
+ "font-size":obj.params.minFontSize,
+ "font-weight":"normal",
+ "font-family":"Arial",
+ "fill":obj.config.labelFontColor,
+ "fill-opacity": (obj.config.hideMinMax || obj.config.donut)? "0" : "1"
+ });
+ setDy(obj.txtMin, obj.params.minFontSize, obj.params.minY);
+
+ // max
+ obj.txtMaximum = obj.config.max;
+ if( obj.config.humanFriendly ) obj.txtMaximum = humanFriendlyNumber( obj.config.max, obj.config.humanFriendlyDecimal );
+ obj.txtMax = obj.canvas.text(obj.params.maxX, obj.params.maxY, obj.txtMaximum);
+ obj.txtMax.attr({
+ "font-size":obj.params.maxFontSize,
+ "font-weight":"normal",
+ "font-family":"Arial",
+ "fill":obj.config.labelFontColor,
+ "fill-opacity": (obj.config.hideMinMax || obj.config.donut)? "0" : "1"
+ });
+ setDy(obj.txtMax, obj.params.maxFontSize, obj.params.maxY);
+
+ var defs = obj.canvas.canvas.childNodes[1];
+ var svg = "http://www.w3.org/2000/svg";
+
+ if (ie < 9) {
+ onCreateElementNsReady(function() {
+ obj.generateShadow(svg, defs);
+ });
+ } else {
+ obj.generateShadow(svg, defs);
+ }
+
+ // var clear
+ defs, svg = null;
+
+ // set value to display
+ if(obj.config.textRenderer) {
+ obj.originalValue = obj.config.textRenderer(obj.originalValue);
+ } else if(obj.config.humanFriendly) {
+ obj.originalValue = humanFriendlyNumber( obj.originalValue, obj.config.humanFriendlyDecimal ) + obj.config.symbol;
+ } else {
+ obj.originalValue = (obj.originalValue * 1).toFixed(obj.config.decimals) + obj.config.symbol;
+ }
+
+ if(obj.config.counter === true) {
+ //on each animation frame
+ eve.on("raphael.anim.frame." + (obj.level.id), function() {
+ var currentValue = obj.level.attr("pki");
+ if(obj.config.textRenderer) {
+ obj.txtValue.attr("text", obj.config.textRenderer(Math.floor(currentValue[0])));
+ } else if(obj.config.humanFriendly) {
+ obj.txtValue.attr("text", humanFriendlyNumber( Math.floor(currentValue[0]), obj.config.humanFriendlyDecimal ) + obj.config.symbol);
+ } else {
+ obj.txtValue.attr("text", (currentValue[0] * 1).toFixed(obj.config.decimals) + obj.config.symbol);
+ }
+ setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
+ currentValue = null;
+ });
+ //on animation end
+ eve.on("raphael.anim.finish." + (obj.level.id), function() {
+ obj.txtValue.attr({"text" : obj.originalValue});
+ setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
+ });
+ } else {
+ //on animation start
+ eve.on("raphael.anim.start." + (obj.level.id), function() {
+ obj.txtValue.attr({"text" : obj.originalValue});
+ setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
+ });
+ }
+
+ // animate gauge level, value & label
+ obj.level.animate({
+ pki: [
+ obj.config.value,
+ obj.config.min,
+ obj.config.max,
+ obj.params.widgetW,
+ obj.params.widgetH,
+ obj.params.dx,
+ obj.params.dy,
+ obj.config.gaugeWidthScale,
+ obj.config.donut
+ ]
+ }, obj.config.startAnimationTime, obj.config.startAnimationType);
+ obj.txtValue.animate({"fill-opacity":(obj.config.hideValue)?"0":"1"}, obj.config.startAnimationTime, obj.config.startAnimationType);
+ obj.txtLabel.animate({"fill-opacity":"1"}, obj.config.startAnimationTime, obj.config.startAnimationType);
+};
+
+/** Refresh gauge level */
+JustGage.prototype.refresh = function(val, max) {
+
+ var obj = this;
+ var displayVal, color, max = max || null;
+
+ // set new max
+ if(max !== null) {
+ obj.config.max = max;
+
+ obj.txtMaximum = obj.config.max;
+ if( obj.config.humanFriendly ) obj.txtMaximum = humanFriendlyNumber( obj.config.max, obj.config.humanFriendlyDecimal );
+ obj.txtMax.attr({"text" : obj.txtMaximum});
+ setDy(obj.txtMax, obj.params.maxFontSize, obj.params.maxY);
+ }
+
+ // overflow values
+ displayVal = val;
+ if ((val * 1) > (obj.config.max * 1)) {val = (obj.config.max * 1);}
+ if ((val * 1) < (obj.config.min * 1)) {val = (obj.config.min * 1);}
+
+ color = getColor(val, (val - obj.config.min) / (obj.config.max - obj.config.min), obj.config.levelColors, obj.config.noGradient, obj.config.customSectors);
+
+ if(obj.config.textRenderer) {
+ displayVal = obj.config.textRenderer(displayVal);
+ } else if( obj.config.humanFriendly ) {
+ displayVal = humanFriendlyNumber( displayVal, obj.config.humanFriendlyDecimal ) + obj.config.symbol;
+ } else {
+ displayVal = (displayVal * 1).toFixed(obj.config.decimals) + obj.config.symbol;
+ }
+ obj.originalValue = displayVal;
+ obj.config.value = val * 1;
+
+ if(!obj.config.counter) {
+ obj.txtValue.attr({"text":displayVal});
+ setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
+ }
+
+ obj.level.animate({
+ pki: [
+ obj.config.value,
+ obj.config.min,
+ obj.config.max,
+ obj.params.widgetW,
+ obj.params.widgetH,
+ obj.params.dx,
+ obj.params.dy,
+ obj.config.gaugeWidthScale,
+ obj.config.donut
+ ],
+ "fill":color
+ }, obj.config.refreshAnimationTime, obj.config.refreshAnimationType);
+
+ // var clear
+ obj, displayVal, color, max = null;
+};
+
+/** Generate shadow */
+JustGage.prototype.generateShadow = function(svg, defs) {
+
+ var obj = this;
+ var gaussFilter, feOffset, feGaussianBlur, feComposite1, feFlood, feComposite2, feComposite3;
+
+ // FILTER
+ gaussFilter = document.createElementNS(svg,"filter");
+ gaussFilter.setAttribute("id","inner-shadow");
+ defs.appendChild(gaussFilter);
+
+ // offset
+ feOffset = document.createElementNS(svg,"feOffset");
+ feOffset.setAttribute("dx", 0);
+ feOffset.setAttribute("dy", obj.config.shadowVerticalOffset);
+ gaussFilter.appendChild(feOffset);
+
+ // blur
+ feGaussianBlur = document.createElementNS(svg,"feGaussianBlur");
+ feGaussianBlur.setAttribute("result","offset-blur");
+ feGaussianBlur.setAttribute("stdDeviation", obj.config.shadowSize);
+ gaussFilter.appendChild(feGaussianBlur);
+
+ // composite 1
+ feComposite1 = document.createElementNS(svg,"feComposite");
+ feComposite1.setAttribute("operator","out");
+ feComposite1.setAttribute("in", "SourceGraphic");
+ feComposite1.setAttribute("in2","offset-blur");
+ feComposite1.setAttribute("result","inverse");
+ gaussFilter.appendChild(feComposite1);
+
+ // flood
+ feFlood = document.createElementNS(svg,"feFlood");
+ feFlood.setAttribute("flood-color","black");
+ feFlood.setAttribute("flood-opacity", obj.config.shadowOpacity);
+ feFlood.setAttribute("result","color");
+ gaussFilter.appendChild(feFlood);
+
+ // composite 2
+ feComposite2 = document.createElementNS(svg,"feComposite");
+ feComposite2.setAttribute("operator","in");
+ feComposite2.setAttribute("in", "color");
+ feComposite2.setAttribute("in2","inverse");
+ feComposite2.setAttribute("result","shadow");
+ gaussFilter.appendChild(feComposite2);
+
+ // composite 3
+ feComposite3 = document.createElementNS(svg,"feComposite");
+ feComposite3.setAttribute("operator","over");
+ feComposite3.setAttribute("in", "shadow");
+ feComposite3.setAttribute("in2","SourceGraphic");
+ gaussFilter.appendChild(feComposite3);
+
+ // set shadow
+ if (!obj.config.hideInnerShadow) {
+ obj.canvas.canvas.childNodes[2].setAttribute("filter", "url(#inner-shadow)");
+ obj.canvas.canvas.childNodes[3].setAttribute("filter", "url(#inner-shadow)");
+ }
+
+ // var clear
+ gaussFilter, feOffset, feGaussianBlur, feComposite1, feFlood, feComposite2, feComposite3 = null;
+
+};
+
+/** Get color for value */
+function getColor(val, pct, col, noGradient, custSec) {
+
+ var no, inc, colors, percentage, rval, gval, bval, lower, upper, range, rangePct, pctLower, pctUpper, color;
+ var noGradient = noGradient || custSec.length > 0;
+
+ if(custSec.length > 0) {
+ for(var i = 0; i < custSec.length; i++) {
+ if(val > custSec[i].lo && val <= custSec[i].hi) {
+ return custSec[i].color;
+ }
+ }
+ }
+
+ no = col.length;
+ if (no === 1) return col[0];
+ inc = (noGradient) ? (1 / no) : (1 / (no - 1));
+ colors = [];
+ for (var i = 0; i < col.length; i++) {
+ percentage = (noGradient) ? (inc * (i + 1)) : (inc * i);
+ rval = parseInt((cutHex(col[i])).substring(0,2),16);
+ gval = parseInt((cutHex(col[i])).substring(2,4),16);
+ bval = parseInt((cutHex(col[i])).substring(4,6),16);
+ colors[i] = { pct: percentage, color: { r: rval, g: gval, b: bval } };
+ }
+
+ if(pct === 0) {
+ return 'rgb(' + [colors[0].color.r, colors[0].color.g, colors[0].color.b].join(',') + ')';
+ }
+
+ for (var j = 0; j < colors.length; j++) {
+ if (pct <= colors[j].pct) {
+ if (noGradient) {
+ return 'rgb(' + [colors[j].color.r, colors[j].color.g, colors[j].color.b].join(',') + ')';
+ } else {
+ lower = colors[j - 1];
+ upper = colors[j];
+ range = upper.pct - lower.pct;
+ rangePct = (pct - lower.pct) / range;
+ pctLower = 1 - rangePct;
+ pctUpper = rangePct;
+ color = {
+ r: Math.floor(lower.color.r * pctLower + upper.color.r * pctUpper),
+ g: Math.floor(lower.color.g * pctLower + upper.color.g * pctUpper),
+ b: Math.floor(lower.color.b * pctLower + upper.color.b * pctUpper)
+ };
+ return 'rgb(' + [color.r, color.g, color.b].join(',') + ')';
+ }
+ }
+ }
+
+}
+
+/** Fix Raphael display:none tspan dy attribute bug */
+function setDy(elem, fontSize, txtYpos) {
+ if (!ie || ie > 9) {
+ elem.node.firstChild.attributes.dy.value = 0;
+ }
+}
+
+/** Random integer */
+function getRandomInt (min, max) {
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+}
+
+/** Cut hex */
+function cutHex(str) {
+ return (str.charAt(0)=="#") ? str.substring(1,7):str;
+}
+
+/** Human friendly number suffix - From: http://stackoverflow.com/questions/2692323/code-golf-friendly-number-abbreviator */
+function humanFriendlyNumber( n, d ) {
+ var p, d2, i, s;
+
+ p = Math.pow;
+ d2 = p(10, d);
+ i = 7;
+ while( i ) {
+ s = p(10,i--*3);
+ if( s <= n ) {
+ n = Math.round(n*d2/s)/d2+"KMGTPE"[i];
+ }
+ }
+ return n;
+}
+
+/** Get style */
+function getStyle(oElm, strCssRule){
+ var strValue = "";
+ if(document.defaultView && document.defaultView.getComputedStyle){
+ strValue = document.defaultView.getComputedStyle(oElm, "").getPropertyValue(strCssRule);
+ }
+ else if(oElm.currentStyle){
+ strCssRule = strCssRule.replace(/\-(\w)/g, function (strMatch, p1){
+ return p1.toUpperCase();
+ });
+ strValue = oElm.currentStyle[strCssRule];
+ }
+ return strValue;
+}
+
+/** Create Element NS Ready */
+function onCreateElementNsReady(func) {
+ if (document.createElementNS !== undefined) {
+ func();
+ } else {
+ setTimeout(function() { onCreateElementNsReady(func); }, 100);
+ }
+}
+
+/** Get IE version */
+// ----------------------------------------------------------
+// A short snippet for detecting versions of IE in JavaScript
+// without resorting to user-agent sniffing
+// ----------------------------------------------------------
+// If you're not in IE (or IE version is less than 5) then:
+// ie === undefined
+// If you're in IE (>=5) then you can determine which version:
+// ie === 7; // IE7
+// Thus, to detect IE:
+// if (ie) {}
+// And to detect the version:
+// ie === 6 // IE6
+// ie > 7 // IE8, IE9 ...
+// ie < 9 // Anything less than IE9
+// ----------------------------------------------------------
+// UPDATE: Now using Live NodeList idea from @jdalton
+var ie = (function(){
+
+ var undef,
+ v = 3,
+ div = document.createElement('div'),
+ all = div.getElementsByTagName('i');
+
+ while (
+ div.innerHTML = '',
+ all[0]
+ );
+ return v > 4 ? v : undef;
+}());
\ No newline at end of file
diff --git a/addons/web/static/lib/qweb/qweb2.js b/addons/web/static/lib/qweb/qweb2.js
index 52e975322e5..44db469cd89 100644
--- a/addons/web/static/lib/qweb/qweb2.js
+++ b/addons/web/static/lib/qweb/qweb2.js
@@ -384,6 +384,7 @@ QWeb2.Engine = (function() {
this.compiled_templates[template] = tcompiled;
return this.render(template, dict);
} else {
+ console.log(this.compiled_templates, template);
return this.tools.exception("Template '" + template + "' not found");
}
},
diff --git a/addons/web_kanban/static/src/js/kanban.js b/addons/web_kanban/static/src/js/kanban.js
index 6f47f9c128d..aff3673f687 100644
--- a/addons/web_kanban/static/src/js/kanban.js
+++ b/addons/web_kanban/static/src/js/kanban.js
@@ -1231,6 +1231,148 @@ instance.web_kanban.AbstractField = instance.web.Widget.extend(instance.web_kanb
});
instance.web_kanban.fields_registry = new instance.web.Registry({});
+
+
+/**
+ * Kanban widgets: Sparkline
+ *
+ */
+
+instance.web_kanban.SparklineBarWidget = instance.web_kanban.AbstractField.extend({
+ className: "oe_sparkline_bar",
+ start: function() {
+ var self = this;
+ var title = this.$node.html() || this.field.string;
+ setTimeout(function () {
+ var value = _.pluck(self.field.value, 'value');
+ var tooltips = _.pluck(self.field.value, 'tooltip');
+ var sparkline_options = _.extend({
+ type: 'bar',
+ barWidth: 5,
+ height: '50px',
+ barWidth: '10px',
+ barSpacing: '5px',
+ barColor: '#96d854',
+ tooltipFormat: '{{offset:offset}} {{value}}',
+ tooltipValueLookups: {
+ 'offset': tooltips
+ }
+ }, this.options);
+ self.$el.sparkline(value, sparkline_options);
+ self.$el.tipsy({'delayIn': 0, 'html': true, 'title': function(){return title}, 'gravity': 'n'});
+ }, 0);
+ },
+});
+
+instance.web_kanban.fields_registry.add("sparkline_bar", "instance.web_kanban.SparklineBarWidget");
+
+
+/**
+ * Kanban widgets: GaugeWidget
+ *
+ */
+
+instance.web_kanban.GaugeWidget = instance.web_kanban.AbstractField.extend({
+ className: "oe_gauge",
+ start: function() {
+ var self = this;
+ var max = 100;
+ if (this.options.max_field) {
+ max = this.getParent().record[this.options.max_field].raw_value;
+ }
+ var label = this.options.label || "";
+ if (this.options.label_field) {
+ label = this.getParent().record[this.options.label_field].raw_value;
+ }
+ var val = this.field.value;
+ var value = _.isArray(val) && val.length ? val[val.length-1]['value'] : val;
+ var title = this.$node.html() || this.field.string;
+ console.log(value, title, max);
+ this.gage = new JustGage({
+ parentNode: this.$el[0],
+ value: value,
+ title: title,
+ min: 0,
+ max: max,
+ relativeGaugeSize: true,
+ humanFriendly: true,
+ label: label,
+ levelColors: [
+ "#ff0000",
+ "#f9c802",
+ "#a9d70b"
+ ]
+ });
+
+ var flag_open = false;
+ if (self.options.action_change) {
+ var $svg = self.$el.find('svg');
+ var css = {
+ 'text-align': 'center',
+ 'position': 'absolute',
+ 'width': self.$el.outerWidth() + 'px',
+ 'top': (self.$el.outerHeight()/2-5) + 'px'
+ };
+
+ self.$el.click(function (event) {
+ event.stopPropagation();
+ flag_open = false;
+ if (!parent.view.is_action_enabled('edit')) {
+ return;
+ }
+ if (!self.$el.find(".oe_justgage_edit").size()) {
+ $div = $('');
+ $div.css(css);
+ $input = $('').val(value);
+ $input.css({
+ 'text-align': 'center',
+ 'margin': 'auto',
+ 'width': ($svg.outerWidth()-40) + 'px'
+ });
+ $div.append($input);
+ self.$el.prepend($div)
+ $input.focus()
+ .keydown(function (event) {
+ event.stopPropagation();
+ if (event.keyCode == 13 || event.keyCode == 9) {
+ if ($input.val() != value) {
+ parent.view.dataset.call(self.options.action_change, [parent.id, $input.val()]).then(function () {
+ parent.do_reload();
+ });
+ } else {
+ $div.remove();
+ }
+ }
+ })
+ .click(function (event) {
+ event.stopPropagation();
+ flag_open = false;
+ })
+ .blur(function (event) {
+ if(!flag_open) {
+ self.$el.find(".oe_justgage_edit").remove();
+ } else {
+ flag_open = false;
+ setTimeout(function () {$input.focus();}, 0);
+ }
+ });
+ }
+ }).mousedown(function () {
+ flag_open = true;
+ });
+
+ if (!+value) {
+ $svg.fadeTo(0, 0.3);
+ $div = $('').text(_t("Click to change value"));
+ $div.css(css);
+ self.$el.append($div);
+ }
+ }
+ },
+});
+
+instance.web_kanban.fields_registry.add("gauge", "instance.web_kanban.GaugeWidget");
+
};
// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
--
cgit v1.2.3
From 903ca548c7ec709d905b7b1ff20c93678dcab2e5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Tue, 3 Sep 2013 16:59:36 +0200
Subject: [IMP] mass_mailing: added kanban views for mass_mailing, implemented
segment for campaigns. Moved sparkline and gauge widgets to web.
Segment: template, date, mails.
Campaign: grouped statistics.
bzr revid: tde@openerp.com-20130903145936-nwy250w1suxmtbl3
---
addons/crm/__openerp__.py | 1 -
.../crm/static/lib/sparkline/jquery.sparkline.js | 3047 --------------------
addons/crm/static/src/js/crm_case_section.js | 23 -
addons/mail/mail_mail.py | 31 +-
addons/mail/mail_thread.py | 3 +-
addons/mail/tests/__init__.py | 3 +-
addons/mass_mailing/__openerp__.py | 6 +
addons/mass_mailing/mail_mail.py | 10 +-
addons/mass_mailing/mail_mail_view.xml | 1 +
addons/mass_mailing/mass_mailing.py | 170 +-
addons/mass_mailing/mass_mailing_demo.xml | 81 +
addons/mass_mailing/mass_mailing_view.xml | 175 +-
.../mass_mailing/static/src/css/mass_mailing.css | 54 +
addons/sale_crm/__openerp__.py | 1 -
addons/sale_crm/sale_crm_view.xml | 4 +-
addons/sale_crm/static/lib/justgage.js | 883 ------
addons/sale_crm/static/src/js/sale_crm.js | 108 -
17 files changed, 493 insertions(+), 4108 deletions(-)
delete mode 100644 addons/crm/static/lib/sparkline/jquery.sparkline.js
create mode 100644 addons/mass_mailing/mass_mailing_demo.xml
create mode 100644 addons/mass_mailing/static/src/css/mass_mailing.css
delete mode 100644 addons/sale_crm/static/lib/justgage.js
delete mode 100644 addons/sale_crm/static/src/js/sale_crm.js
diff --git a/addons/crm/__openerp__.py b/addons/crm/__openerp__.py
index 1b4ba191b82..ae9da7ade08 100644
--- a/addons/crm/__openerp__.py
+++ b/addons/crm/__openerp__.py
@@ -121,7 +121,6 @@ Dashboard for CRM will include:
'static/src/css/crm.css'
],
'js': [
- 'static/lib/sparkline/jquery.sparkline.js',
'static/src/js/crm_case_section.js',
],
'installable': True,
diff --git a/addons/crm/static/lib/sparkline/jquery.sparkline.js b/addons/crm/static/lib/sparkline/jquery.sparkline.js
deleted file mode 100644
index c003923e03b..00000000000
--- a/addons/crm/static/lib/sparkline/jquery.sparkline.js
+++ /dev/null
@@ -1,3047 +0,0 @@
-/**
-*
-* jquery.sparkline.js
-*
-* v2.1.1
-* (c) Splunk, Inc
-* Contact: Gareth Watts (gareth@splunk.com)
-* http://omnipotent.net/jquery.sparkline/
-*
-* Generates inline sparkline charts from data supplied either to the method
-* or inline in HTML
-*
-* Compatible with Internet Explorer 6.0+ and modern browsers equipped with the canvas tag
-* (Firefox 2.0+, Safari, Opera, etc)
-*
-* License: New BSD License
-*
-* Copyright (c) 2012, Splunk Inc.
-* All rights reserved.
-*
-* Redistribution and use in source and binary forms, with or without modification,
-* are permitted provided that the following conditions are met:
-*
-* * Redistributions of source code must retain the above copyright notice,
-* this list of conditions and the following disclaimer.
-* * Redistributions in binary form must reproduce the above copyright notice,
-* this list of conditions and the following disclaimer in the documentation
-* and/or other materials provided with the distribution.
-* * Neither the name of Splunk Inc nor the names of its contributors may
-* be used to endorse or promote products derived from this software without
-* specific prior written permission.
-*
-* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
-* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
-* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
-* SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
-* OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-*
-*
-* Usage:
-* $(selector).sparkline(values, options)
-*
-* If values is undefined or set to 'html' then the data values are read from the specified tag:
-*
Sparkline: 1,4,6,6,8,5,3,5
-* $('.sparkline').sparkline();
-* There must be no spaces in the enclosed data set
-*
-* Otherwise values must be an array of numbers or null values
-*
Sparkline: This text replaced if the browser is compatible
-* $('#sparkline1').sparkline([1,4,6,6,8,5,3,5])
-* $('#sparkline2').sparkline([1,4,6,null,null,5,3,5])
-*
-* Values can also be specified in an HTML comment, or as a values attribute:
-*
Sparkline:
-*
Sparkline:
-* $('.sparkline').sparkline();
-*
-* For line charts, x values can also be specified:
-*
Sparkline: 1:1,2.7:4,3.4:6,5:6,6:8,8.7:5,9:3,10:5
-* $('#sparkline1').sparkline([ [1,1], [2.7,4], [3.4,6], [5,6], [6,8], [8.7,5], [9,3], [10,5] ])
-*
-* By default, options should be passed in as teh second argument to the sparkline function:
-* $('.sparkline').sparkline([1,2,3,4], {type: 'bar'})
-*
-* Options can also be set by passing them on the tag itself. This feature is disabled by default though
-* as there's a slight performance overhead:
-* $('.sparkline').sparkline([1,2,3,4], {enableTagOptions: true})
-*
Sparkline: loading
-* Prefix all options supplied as tag attribute with "spark" (configurable by setting tagOptionPrefix)
-*
-* Supported options:
-* lineColor - Color of the line used for the chart
-* fillColor - Color used to fill in the chart - Set to '' or false for a transparent chart
-* width - Width of the chart - Defaults to 3 times the number of values in pixels
-* height - Height of the chart - Defaults to the height of the containing element
-* chartRangeMin - Specify the minimum value to use for the Y range of the chart - Defaults to the minimum value supplied
-* chartRangeMax - Specify the maximum value to use for the Y range of the chart - Defaults to the maximum value supplied
-* chartRangeClip - Clip out of range values to the max/min specified by chartRangeMin and chartRangeMax
-* chartRangeMinX - Specify the minimum value to use for the X range of the chart - Defaults to the minimum value supplied
-* chartRangeMaxX - Specify the maximum value to use for the X range of the chart - Defaults to the maximum value supplied
-* composite - If true then don't erase any existing chart attached to the tag, but draw
-* another chart over the top - Note that width and height are ignored if an
-* existing chart is detected.
-* tagValuesAttribute - Name of tag attribute to check for data values - Defaults to 'values'
-* enableTagOptions - Whether to check tags for sparkline options
-* tagOptionPrefix - Prefix used for options supplied as tag attributes - Defaults to 'spark'
-* disableHiddenCheck - If set to true, then the plugin will assume that charts will never be drawn into a
-* hidden dom element, avoding a browser reflow
-* disableInteraction - If set to true then all mouseover/click interaction behaviour will be disabled,
-* making the plugin perform much like it did in 1.x
-* disableTooltips - If set to true then tooltips will be disabled - Defaults to false (tooltips enabled)
-* disableHighlight - If set to true then highlighting of selected chart elements on mouseover will be disabled
-* defaults to false (highlights enabled)
-* highlightLighten - Factor to lighten/darken highlighted chart values by - Defaults to 1.4 for a 40% increase
-* tooltipContainer - Specify which DOM element the tooltip should be rendered into - defaults to document.body
-* tooltipClassname - Optional CSS classname to apply to tooltips - If not specified then a default style will be applied
-* tooltipOffsetX - How many pixels away from the mouse pointer to render the tooltip on the X axis
-* tooltipOffsetY - How many pixels away from the mouse pointer to render the tooltip on the r axis
-* tooltipFormatter - Optional callback that allows you to override the HTML displayed in the tooltip
-* callback is given arguments of (sparkline, options, fields)
-* tooltipChartTitle - If specified then the tooltip uses the string specified by this setting as a title
-* tooltipFormat - A format string or SPFormat object (or an array thereof for multiple entries)
-* to control the format of the tooltip
-* tooltipPrefix - A string to prepend to each field displayed in a tooltip
-* tooltipSuffix - A string to append to each field displayed in a tooltip
-* tooltipSkipNull - If true then null values will not have a tooltip displayed (defaults to true)
-* tooltipValueLookups - An object or range map to map field values to tooltip strings
-* (eg. to map -1 to "Lost", 0 to "Draw", and 1 to "Win")
-* numberFormatter - Optional callback for formatting numbers in tooltips
-* numberDigitGroupSep - Character to use for group separator in numbers "1,234" - Defaults to ","
-* numberDecimalMark - Character to use for the decimal point when formatting numbers - Defaults to "."
-* numberDigitGroupCount - Number of digits between group separator - Defaults to 3
-*
-* There are 7 types of sparkline, selected by supplying a "type" option of 'line' (default),
-* 'bar', 'tristate', 'bullet', 'discrete', 'pie' or 'box'
-* line - Line chart. Options:
-* spotColor - Set to '' to not end each line in a circular spot
-* minSpotColor - If set, color of spot at minimum value
-* maxSpotColor - If set, color of spot at maximum value
-* spotRadius - Radius in pixels
-* lineWidth - Width of line in pixels
-* normalRangeMin
-* normalRangeMax - If set draws a filled horizontal bar between these two values marking the "normal"
-* or expected range of values
-* normalRangeColor - Color to use for the above bar
-* drawNormalOnTop - Draw the normal range above the chart fill color if true
-* defaultPixelsPerValue - Defaults to 3 pixels of width for each value in the chart
-* highlightSpotColor - The color to use for drawing a highlight spot on mouseover - Set to null to disable
-* highlightLineColor - The color to use for drawing a highlight line on mouseover - Set to null to disable
-* valueSpots - Specify which points to draw spots on, and in which color. Accepts a range map
-*
-* bar - Bar chart. Options:
-* barColor - Color of bars for postive values
-* negBarColor - Color of bars for negative values
-* zeroColor - Color of bars with zero values
-* nullColor - Color of bars with null values - Defaults to omitting the bar entirely
-* barWidth - Width of bars in pixels
-* colorMap - Optional mappnig of values to colors to override the *BarColor values above
-* can be an Array of values to control the color of individual bars or a range map
-* to specify colors for individual ranges of values
-* barSpacing - Gap between bars in pixels
-* zeroAxis - Centers the y-axis around zero if true
-*
-* tristate - Charts values of win (>0), lose (<0) or draw (=0)
-* posBarColor - Color of win values
-* negBarColor - Color of lose values
-* zeroBarColor - Color of draw values
-* barWidth - Width of bars in pixels
-* barSpacing - Gap between bars in pixels
-* colorMap - Optional mappnig of values to colors to override the *BarColor values above
-* can be an Array of values to control the color of individual bars or a range map
-* to specify colors for individual ranges of values
-*
-* discrete - Options:
-* lineHeight - Height of each line in pixels - Defaults to 30% of the graph height
-* thesholdValue - Values less than this value will be drawn using thresholdColor instead of lineColor
-* thresholdColor
-*
-* bullet - Values for bullet graphs msut be in the order: target, performance, range1, range2, range3, ...
-* options:
-* targetColor - The color of the vertical target marker
-* targetWidth - The width of the target marker in pixels
-* performanceColor - The color of the performance measure horizontal bar
-* rangeColors - Colors to use for each qualitative range background color
-*
-* pie - Pie chart. Options:
-* sliceColors - An array of colors to use for pie slices
-* offset - Angle in degrees to offset the first slice - Try -90 or +90
-* borderWidth - Width of border to draw around the pie chart, in pixels - Defaults to 0 (no border)
-* borderColor - Color to use for the pie chart border - Defaults to #000
-*
-* box - Box plot. Options:
-* raw - Set to true to supply pre-computed plot points as values
-* values should be: low_outlier, low_whisker, q1, median, q3, high_whisker, high_outlier
-* When set to false you can supply any number of values and the box plot will
-* be computed for you. Default is false.
-* showOutliers - Set to true (default) to display outliers as circles
-* outlierIQR - Interquartile range used to determine outliers. Default 1.5
-* boxLineColor - Outline color of the box
-* boxFillColor - Fill color for the box
-* whiskerColor - Line color used for whiskers
-* outlierLineColor - Outline color of outlier circles
-* outlierFillColor - Fill color of the outlier circles
-* spotRadius - Radius of outlier circles
-* medianColor - Line color of the median line
-* target - Draw a target cross hair at the supplied value (default undefined)
-*
-*
-*
-* Examples:
-* $('#sparkline1').sparkline(myvalues, { lineColor: '#f00', fillColor: false });
-* $('.barsparks').sparkline('html', { type:'bar', height:'40px', barWidth:5 });
-* $('#tristate').sparkline([1,1,-1,1,0,0,-1], { type:'tristate' }):
-* $('#discrete').sparkline([1,3,4,5,5,3,4,5], { type:'discrete' });
-* $('#bullet').sparkline([10,12,12,9,7], { type:'bullet' });
-* $('#pie').sparkline([1,1,2], { type:'pie' });
-*/
-
-/*jslint regexp: true, browser: true, jquery: true, white: true, nomen: false, plusplus: false, maxerr: 500, indent: 4 */
-
-(function(factory) {
- if(typeof define === 'function' && define.amd) {
- define(['jquery'], factory);
- }
- else {
- factory(jQuery);
- }
-}
-(function($) {
- 'use strict';
-
- var UNSET_OPTION = {},
- getDefaults, createClass, SPFormat, clipval, quartile, normalizeValue, normalizeValues,
- remove, isNumber, all, sum, addCSS, ensureArray, formatNumber, RangeMap,
- MouseHandler, Tooltip, barHighlightMixin,
- line, bar, tristate, discrete, bullet, pie, box, defaultStyles, initStyles,
- VShape, VCanvas_base, VCanvas_canvas, VCanvas_vml, pending, shapeCount = 0;
-
- /**
- * Default configuration settings
- */
- getDefaults = function () {
- return {
- // Settings common to most/all chart types
- common: {
- type: 'line',
- lineColor: '#00f',
- fillColor: '#cdf',
- defaultPixelsPerValue: 3,
- width: 'auto',
- height: 'auto',
- composite: false,
- tagValuesAttribute: 'values',
- tagOptionsPrefix: 'spark',
- enableTagOptions: false,
- enableHighlight: true,
- highlightLighten: 1.4,
- tooltipSkipNull: true,
- tooltipPrefix: '',
- tooltipSuffix: '',
- disableHiddenCheck: false,
- numberFormatter: false,
- numberDigitGroupCount: 3,
- numberDigitGroupSep: ',',
- numberDecimalMark: '.',
- disableTooltips: false,
- disableInteraction: false
- },
- // Defaults for line charts
- line: {
- spotColor: '#f80',
- highlightSpotColor: '#5f5',
- highlightLineColor: '#f22',
- spotRadius: 1.5,
- minSpotColor: '#f80',
- maxSpotColor: '#f80',
- lineWidth: 1,
- normalRangeMin: undefined,
- normalRangeMax: undefined,
- normalRangeColor: '#ccc',
- drawNormalOnTop: false,
- chartRangeMin: undefined,
- chartRangeMax: undefined,
- chartRangeMinX: undefined,
- chartRangeMaxX: undefined,
- tooltipFormat: new SPFormat('● {{prefix}}{{y}}{{suffix}}')
- },
- // Defaults for bar charts
- bar: {
- barColor: '#3366cc',
- negBarColor: '#f44',
- stackedBarColor: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#66aa00',
- '#dd4477', '#0099c6', '#990099'],
- zeroColor: undefined,
- nullColor: undefined,
- zeroAxis: true,
- barWidth: 4,
- barSpacing: 1,
- chartRangeMax: undefined,
- chartRangeMin: undefined,
- chartRangeClip: false,
- colorMap: undefined,
- tooltipFormat: new SPFormat('● {{prefix}}{{value}}{{suffix}}')
- },
- // Defaults for tristate charts
- tristate: {
- barWidth: 4,
- barSpacing: 1,
- posBarColor: '#6f6',
- negBarColor: '#f44',
- zeroBarColor: '#999',
- colorMap: {},
- tooltipFormat: new SPFormat('● {{value:map}}'),
- tooltipValueLookups: { map: { '-1': 'Loss', '0': 'Draw', '1': 'Win' } }
- },
- // Defaults for discrete charts
- discrete: {
- lineHeight: 'auto',
- thresholdColor: undefined,
- thresholdValue: 0,
- chartRangeMax: undefined,
- chartRangeMin: undefined,
- chartRangeClip: false,
- tooltipFormat: new SPFormat('{{prefix}}{{value}}{{suffix}}')
- },
- // Defaults for bullet charts
- bullet: {
- targetColor: '#f33',
- targetWidth: 3, // width of the target bar in pixels
- performanceColor: '#33f',
- rangeColors: ['#d3dafe', '#a8b6ff', '#7f94ff'],
- base: undefined, // set this to a number to change the base start number
- tooltipFormat: new SPFormat('{{fieldkey:fields}} - {{value}}'),
- tooltipValueLookups: { fields: {r: 'Range', p: 'Performance', t: 'Target'} }
- },
- // Defaults for pie charts
- pie: {
- offset: 0,
- sliceColors: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#66aa00',
- '#dd4477', '#0099c6', '#990099'],
- borderWidth: 0,
- borderColor: '#000',
- tooltipFormat: new SPFormat('● {{value}} ({{percent.1}}%)')
- },
- // Defaults for box plots
- box: {
- raw: false,
- boxLineColor: '#000',
- boxFillColor: '#cdf',
- whiskerColor: '#000',
- outlierLineColor: '#333',
- outlierFillColor: '#fff',
- medianColor: '#f00',
- showOutliers: true,
- outlierIQR: 1.5,
- spotRadius: 1.5,
- target: undefined,
- targetColor: '#4a2',
- chartRangeMax: undefined,
- chartRangeMin: undefined,
- tooltipFormat: new SPFormat('{{field:fields}}: {{value}}'),
- tooltipFormatFieldlistKey: 'field',
- tooltipValueLookups: { fields: { lq: 'Lower Quartile', med: 'Median',
- uq: 'Upper Quartile', lo: 'Left Outlier', ro: 'Right Outlier',
- lw: 'Left Whisker', rw: 'Right Whisker'} }
- }
- };
- };
-
- // You can have tooltips use a css class other than jqstooltip by specifying tooltipClassname
- defaultStyles = '.jqstooltip { ' +
- 'position: absolute;' +
- 'left: 0px;' +
- 'top: 0px;' +
- 'visibility: hidden;' +
- 'background: rgb(0, 0, 0) transparent;' +
- 'background-color: rgba(0,0,0,0.6);' +
- 'filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000);' +
- '-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000)";' +
- 'color: white;' +
- 'font: 10px arial, san serif;' +
- 'text-align: left;' +
- 'white-space: nowrap;' +
- 'padding: 5px;' +
- 'border: 1px solid white;' +
- 'z-index: 10000;' +
- '}' +
- '.jqsfield { ' +
- 'color: white;' +
- 'font: 10px arial, san serif;' +
- 'text-align: left;' +
- '}';
-
- /**
- * Utilities
- */
-
- createClass = function (/* [baseclass, [mixin, ...]], definition */) {
- var Class, args;
- Class = function () {
- this.init.apply(this, arguments);
- };
- if (arguments.length > 1) {
- if (arguments[0]) {
- Class.prototype = $.extend(new arguments[0](), arguments[arguments.length - 1]);
- Class._super = arguments[0].prototype;
- } else {
- Class.prototype = arguments[arguments.length - 1];
- }
- if (arguments.length > 2) {
- args = Array.prototype.slice.call(arguments, 1, -1);
- args.unshift(Class.prototype);
- $.extend.apply($, args);
- }
- } else {
- Class.prototype = arguments[0];
- }
- Class.prototype.cls = Class;
- return Class;
- };
-
- /**
- * Wraps a format string for tooltips
- * {{x}}
- * {{x.2}
- * {{x:months}}
- */
- $.SPFormatClass = SPFormat = createClass({
- fre: /\{\{([\w.]+?)(:(.+?))?\}\}/g,
- precre: /(\w+)\.(\d+)/,
-
- init: function (format, fclass) {
- this.format = format;
- this.fclass = fclass;
- },
-
- render: function (fieldset, lookups, options) {
- var self = this,
- fields = fieldset,
- match, token, lookupkey, fieldvalue, prec;
- return this.format.replace(this.fre, function () {
- var lookup;
- token = arguments[1];
- lookupkey = arguments[3];
- match = self.precre.exec(token);
- if (match) {
- prec = match[2];
- token = match[1];
- } else {
- prec = false;
- }
- fieldvalue = fields[token];
- if (fieldvalue === undefined) {
- return '';
- }
- if (lookupkey && lookups && lookups[lookupkey]) {
- lookup = lookups[lookupkey];
- if (lookup.get) { // RangeMap
- return lookups[lookupkey].get(fieldvalue) || fieldvalue;
- } else {
- return lookups[lookupkey][fieldvalue] || fieldvalue;
- }
- }
- if (isNumber(fieldvalue)) {
- if (options.get('numberFormatter')) {
- fieldvalue = options.get('numberFormatter')(fieldvalue);
- } else {
- fieldvalue = formatNumber(fieldvalue, prec,
- options.get('numberDigitGroupCount'),
- options.get('numberDigitGroupSep'),
- options.get('numberDecimalMark'));
- }
- }
- return fieldvalue;
- });
- }
- });
-
- // convience method to avoid needing the new operator
- $.spformat = function(format, fclass) {
- return new SPFormat(format, fclass);
- };
-
- clipval = function (val, min, max) {
- if (val < min) {
- return min;
- }
- if (val > max) {
- return max;
- }
- return val;
- };
-
- quartile = function (values, q) {
- var vl;
- if (q === 2) {
- vl = Math.floor(values.length / 2);
- return values.length % 2 ? values[vl] : (values[vl-1] + values[vl]) / 2;
- } else {
- if (values.length % 2 ) { // odd
- vl = (values.length * q + q) / 4;
- return vl % 1 ? (values[Math.floor(vl)] + values[Math.floor(vl) - 1]) / 2 : values[vl-1];
- } else { //even
- vl = (values.length * q + 2) / 4;
- return vl % 1 ? (values[Math.floor(vl)] + values[Math.floor(vl) - 1]) / 2 : values[vl-1];
-
- }
- }
- };
-
- normalizeValue = function (val) {
- var nf;
- switch (val) {
- case 'undefined':
- val = undefined;
- break;
- case 'null':
- val = null;
- break;
- case 'true':
- val = true;
- break;
- case 'false':
- val = false;
- break;
- default:
- nf = parseFloat(val);
- if (val == nf) {
- val = nf;
- }
- }
- return val;
- };
-
- normalizeValues = function (vals) {
- var i, result = [];
- for (i = vals.length; i--;) {
- result[i] = normalizeValue(vals[i]);
- }
- return result;
- };
-
- remove = function (vals, filter) {
- var i, vl, result = [];
- for (i = 0, vl = vals.length; i < vl; i++) {
- if (vals[i] !== filter) {
- result.push(vals[i]);
- }
- }
- return result;
- };
-
- isNumber = function (num) {
- return !isNaN(parseFloat(num)) && isFinite(num);
- };
-
- formatNumber = function (num, prec, groupsize, groupsep, decsep) {
- var p, i;
- num = (prec === false ? parseFloat(num).toString() : num.toFixed(prec)).split('');
- p = (p = $.inArray('.', num)) < 0 ? num.length : p;
- if (p < num.length) {
- num[p] = decsep;
- }
- for (i = p - groupsize; i > 0; i -= groupsize) {
- num.splice(i, 0, groupsep);
- }
- return num.join('');
- };
-
- // determine if all values of an array match a value
- // returns true if the array is empty
- all = function (val, arr, ignoreNull) {
- var i;
- for (i = arr.length; i--; ) {
- if (ignoreNull && arr[i] === null) continue;
- if (arr[i] !== val) {
- return false;
- }
- }
- return true;
- };
-
- // sums the numeric values in an array, ignoring other values
- sum = function (vals) {
- var total = 0, i;
- for (i = vals.length; i--;) {
- total += typeof vals[i] === 'number' ? vals[i] : 0;
- }
- return total;
- };
-
- ensureArray = function (val) {
- return $.isArray(val) ? val : [val];
- };
-
- // http://paulirish.com/2008/bookmarklet-inject-new-css-rules/
- addCSS = function(css) {
- var tag;
- //if ('\v' == 'v') /* ie only */ {
- if (document.createStyleSheet) {
- document.createStyleSheet().cssText = css;
- } else {
- tag = document.createElement('style');
- tag.type = 'text/css';
- document.getElementsByTagName('head')[0].appendChild(tag);
- tag[(typeof document.body.style.WebkitAppearance == 'string') /* webkit only */ ? 'innerText' : 'innerHTML'] = css;
- }
- };
-
- // Provide a cross-browser interface to a few simple drawing primitives
- $.fn.simpledraw = function (width, height, useExisting, interact) {
- var target, mhandler;
- if (useExisting && (target = this.data('_jqs_vcanvas'))) {
- return target;
- }
- if (width === undefined) {
- width = $(this).innerWidth();
- }
- if (height === undefined) {
- height = $(this).innerHeight();
- }
- if ($.fn.sparkline.hasCanvas) {
- target = new VCanvas_canvas(width, height, this, interact);
- } else if ($.fn.sparkline.hasVML) {
- target = new VCanvas_vml(width, height, this);
- } else {
- return false;
- }
- mhandler = $(this).data('_jqs_mhandler');
- if (mhandler) {
- mhandler.registerCanvas(target);
- }
- return target;
- };
-
- $.fn.cleardraw = function () {
- var target = this.data('_jqs_vcanvas');
- if (target) {
- target.reset();
- }
- };
-
- $.RangeMapClass = RangeMap = createClass({
- init: function (map) {
- var key, range, rangelist = [];
- for (key in map) {
- if (map.hasOwnProperty(key) && typeof key === 'string' && key.indexOf(':') > -1) {
- range = key.split(':');
- range[0] = range[0].length === 0 ? -Infinity : parseFloat(range[0]);
- range[1] = range[1].length === 0 ? Infinity : parseFloat(range[1]);
- range[2] = map[key];
- rangelist.push(range);
- }
- }
- this.map = map;
- this.rangelist = rangelist || false;
- },
-
- get: function (value) {
- var rangelist = this.rangelist,
- i, range, result;
- if ((result = this.map[value]) !== undefined) {
- return result;
- }
- if (rangelist) {
- for (i = rangelist.length; i--;) {
- range = rangelist[i];
- if (range[0] <= value && range[1] >= value) {
- return range[2];
- }
- }
- }
- return undefined;
- }
- });
-
- // Convenience function
- $.range_map = function(map) {
- return new RangeMap(map);
- };
-
- MouseHandler = createClass({
- init: function (el, options) {
- var $el = $(el);
- this.$el = $el;
- this.options = options;
- this.currentPageX = 0;
- this.currentPageY = 0;
- this.el = el;
- this.splist = [];
- this.tooltip = null;
- this.over = false;
- this.displayTooltips = !options.get('disableTooltips');
- this.highlightEnabled = !options.get('disableHighlight');
- },
-
- registerSparkline: function (sp) {
- this.splist.push(sp);
- if (this.over) {
- this.updateDisplay();
- }
- },
-
- registerCanvas: function (canvas) {
- var $canvas = $(canvas.canvas);
- this.canvas = canvas;
- this.$canvas = $canvas;
- $canvas.mouseenter($.proxy(this.mouseenter, this));
- $canvas.mouseleave($.proxy(this.mouseleave, this));
- $canvas.click($.proxy(this.mouseclick, this));
- },
-
- reset: function (removeTooltip) {
- this.splist = [];
- if (this.tooltip && removeTooltip) {
- this.tooltip.remove();
- this.tooltip = undefined;
- }
- },
-
- mouseclick: function (e) {
- var clickEvent = $.Event('sparklineClick');
- clickEvent.originalEvent = e;
- clickEvent.sparklines = this.splist;
- this.$el.trigger(clickEvent);
- },
-
- mouseenter: function (e) {
- $(document.body).unbind('mousemove.jqs');
- $(document.body).bind('mousemove.jqs', $.proxy(this.mousemove, this));
- this.over = true;
- this.currentPageX = e.pageX;
- this.currentPageY = e.pageY;
- this.currentEl = e.target;
- if (!this.tooltip && this.displayTooltips) {
- this.tooltip = new Tooltip(this.options);
- this.tooltip.updatePosition(e.pageX, e.pageY);
- }
- this.updateDisplay();
- },
-
- mouseleave: function () {
- $(document.body).unbind('mousemove.jqs');
- var splist = this.splist,
- spcount = splist.length,
- needsRefresh = false,
- sp, i;
- this.over = false;
- this.currentEl = null;
-
- if (this.tooltip) {
- this.tooltip.remove();
- this.tooltip = null;
- }
-
- for (i = 0; i < spcount; i++) {
- sp = splist[i];
- if (sp.clearRegionHighlight()) {
- needsRefresh = true;
- }
- }
-
- if (needsRefresh) {
- this.canvas.render();
- }
- },
-
- mousemove: function (e) {
- this.currentPageX = e.pageX;
- this.currentPageY = e.pageY;
- this.currentEl = e.target;
- if (this.tooltip) {
- this.tooltip.updatePosition(e.pageX, e.pageY);
- }
- this.updateDisplay();
- },
-
- updateDisplay: function () {
- var splist = this.splist,
- spcount = splist.length,
- needsRefresh = false,
- offset = this.$canvas.offset(),
- localX = this.currentPageX - offset.left,
- localY = this.currentPageY - offset.top,
- tooltiphtml, sp, i, result, changeEvent;
- if (!this.over) {
- return;
- }
- for (i = 0; i < spcount; i++) {
- sp = splist[i];
- result = sp.setRegionHighlight(this.currentEl, localX, localY);
- if (result) {
- needsRefresh = true;
- }
- }
- if (needsRefresh) {
- changeEvent = $.Event('sparklineRegionChange');
- changeEvent.sparklines = this.splist;
- this.$el.trigger(changeEvent);
- if (this.tooltip) {
- tooltiphtml = '';
- for (i = 0; i < spcount; i++) {
- sp = splist[i];
- tooltiphtml += sp.getCurrentRegionTooltip();
- }
- this.tooltip.setContent(tooltiphtml);
- }
- if (!this.disableHighlight) {
- this.canvas.render();
- }
- }
- if (result === null) {
- this.mouseleave();
- }
- }
- });
-
-
- Tooltip = createClass({
- sizeStyle: 'position: static !important;' +
- 'display: block !important;' +
- 'visibility: hidden !important;' +
- 'float: left !important;',
-
- init: function (options) {
- var tooltipClassname = options.get('tooltipClassname', 'jqstooltip'),
- sizetipStyle = this.sizeStyle,
- offset;
- this.container = options.get('tooltipContainer') || document.body;
- this.tooltipOffsetX = options.get('tooltipOffsetX', 10);
- this.tooltipOffsetY = options.get('tooltipOffsetY', 12);
- // remove any previous lingering tooltip
- $('#jqssizetip').remove();
- $('#jqstooltip').remove();
- this.sizetip = $('', {
- id: 'jqssizetip',
- style: sizetipStyle,
- 'class': tooltipClassname
- });
- this.tooltip = $('', {
- id: 'jqstooltip',
- 'class': tooltipClassname
- }).appendTo(this.container);
- // account for the container's location
- offset = this.tooltip.offset();
- this.offsetLeft = offset.left;
- this.offsetTop = offset.top;
- this.hidden = true;
- $(window).unbind('resize.jqs scroll.jqs');
- $(window).bind('resize.jqs scroll.jqs', $.proxy(this.updateWindowDims, this));
- this.updateWindowDims();
- },
-
- updateWindowDims: function () {
- this.scrollTop = $(window).scrollTop();
- this.scrollLeft = $(window).scrollLeft();
- this.scrollRight = this.scrollLeft + $(window).width();
- this.updatePosition();
- },
-
- getSize: function (content) {
- this.sizetip.html(content).appendTo(this.container);
- this.width = this.sizetip.width() + 1;
- this.height = this.sizetip.height();
- this.sizetip.remove();
- },
-
- setContent: function (content) {
- if (!content) {
- this.tooltip.css('visibility', 'hidden');
- this.hidden = true;
- return;
- }
- this.getSize(content);
- this.tooltip.html(content)
- .css({
- 'width': this.width,
- 'height': this.height,
- 'visibility': 'visible'
- });
- if (this.hidden) {
- this.hidden = false;
- this.updatePosition();
- }
- },
-
- updatePosition: function (x, y) {
- if (x === undefined) {
- if (this.mousex === undefined) {
- return;
- }
- x = this.mousex - this.offsetLeft;
- y = this.mousey - this.offsetTop;
-
- } else {
- this.mousex = x = x - this.offsetLeft;
- this.mousey = y = y - this.offsetTop;
- }
- if (!this.height || !this.width || this.hidden) {
- return;
- }
-
- y -= this.height + this.tooltipOffsetY;
- x += this.tooltipOffsetX;
-
- if (y < this.scrollTop) {
- y = this.scrollTop;
- }
- if (x < this.scrollLeft) {
- x = this.scrollLeft;
- } else if (x + this.width > this.scrollRight) {
- x = this.scrollRight - this.width;
- }
-
- this.tooltip.css({
- 'left': x,
- 'top': y
- });
- },
-
- remove: function () {
- this.tooltip.remove();
- this.sizetip.remove();
- this.sizetip = this.tooltip = undefined;
- $(window).unbind('resize.jqs scroll.jqs');
- }
- });
-
- initStyles = function() {
- addCSS(defaultStyles);
- };
-
- $(initStyles);
-
- pending = [];
- $.fn.sparkline = function (userValues, userOptions) {
- return this.each(function () {
- var options = new $.fn.sparkline.options(this, userOptions),
- $this = $(this),
- render, i;
- render = function () {
- var values, width, height, tmp, mhandler, sp, vals;
- if (userValues === 'html' || userValues === undefined) {
- vals = this.getAttribute(options.get('tagValuesAttribute'));
- if (vals === undefined || vals === null) {
- vals = $this.html();
- }
- values = vals.replace(/(^\s*\s*$)|\s+/g, '').split(',');
- } else {
- values = userValues;
- }
-
- width = options.get('width') === 'auto' ? values.length * options.get('defaultPixelsPerValue') : options.get('width');
- if (options.get('height') === 'auto') {
- if (!options.get('composite') || !$.data(this, '_jqs_vcanvas')) {
- // must be a better way to get the line height
- tmp = document.createElement('span');
- tmp.innerHTML = 'a';
- $this.html(tmp);
- height = $(tmp).innerHeight() || $(tmp).height();
- $(tmp).remove();
- tmp = null;
- }
- } else {
- height = options.get('height');
- }
-
- if (!options.get('disableInteraction')) {
- mhandler = $.data(this, '_jqs_mhandler');
- if (!mhandler) {
- mhandler = new MouseHandler(this, options);
- $.data(this, '_jqs_mhandler', mhandler);
- } else if (!options.get('composite')) {
- mhandler.reset();
- }
- } else {
- mhandler = false;
- }
-
- if (options.get('composite') && !$.data(this, '_jqs_vcanvas')) {
- if (!$.data(this, '_jqs_errnotify')) {
- alert('Attempted to attach a composite sparkline to an element with no existing sparkline');
- $.data(this, '_jqs_errnotify', true);
- }
- return;
- }
-
- sp = new $.fn.sparkline[options.get('type')](this, values, options, width, height);
-
- sp.render();
-
- if (mhandler) {
- mhandler.registerSparkline(sp);
- }
- };
- // jQuery 1.3.0 completely changed the meaning of :hidden :-/
- if (($(this).html() && !options.get('disableHiddenCheck') && $(this).is(':hidden')) || ($.fn.jquery < '1.3.0' && $(this).parents().is(':hidden')) || !$(this).parents('body').length) {
- if (!options.get('composite') && $.data(this, '_jqs_pending')) {
- // remove any existing references to the element
- for (i = pending.length; i; i--) {
- if (pending[i - 1][0] == this) {
- pending.splice(i - 1, 1);
- }
- }
- }
- pending.push([this, render]);
- $.data(this, '_jqs_pending', true);
- } else {
- render.call(this);
- }
- });
- };
-
- $.fn.sparkline.defaults = getDefaults();
-
-
- $.sparkline_display_visible = function () {
- var el, i, pl;
- var done = [];
- for (i = 0, pl = pending.length; i < pl; i++) {
- el = pending[i][0];
- if ($(el).is(':visible') && !$(el).parents().is(':hidden')) {
- pending[i][1].call(el);
- $.data(pending[i][0], '_jqs_pending', false);
- done.push(i);
- } else if (!$(el).closest('html').length && !$.data(el, '_jqs_pending')) {
- // element has been inserted and removed from the DOM
- // If it was not yet inserted into the dom then the .data request
- // will return true.
- // removing from the dom causes the data to be removed.
- $.data(pending[i][0], '_jqs_pending', false);
- done.push(i);
- }
- }
- for (i = done.length; i; i--) {
- pending.splice(done[i - 1], 1);
- }
- };
-
-
- /**
- * User option handler
- */
- $.fn.sparkline.options = createClass({
- init: function (tag, userOptions) {
- var extendedOptions, defaults, base, tagOptionType;
- this.userOptions = userOptions = userOptions || {};
- this.tag = tag;
- this.tagValCache = {};
- defaults = $.fn.sparkline.defaults;
- base = defaults.common;
- this.tagOptionsPrefix = userOptions.enableTagOptions && (userOptions.tagOptionsPrefix || base.tagOptionsPrefix);
-
- tagOptionType = this.getTagSetting('type');
- if (tagOptionType === UNSET_OPTION) {
- extendedOptions = defaults[userOptions.type || base.type];
- } else {
- extendedOptions = defaults[tagOptionType];
- }
- this.mergedOptions = $.extend({}, base, extendedOptions, userOptions);
- },
-
-
- getTagSetting: function (key) {
- var prefix = this.tagOptionsPrefix,
- val, i, pairs, keyval;
- if (prefix === false || prefix === undefined) {
- return UNSET_OPTION;
- }
- if (this.tagValCache.hasOwnProperty(key)) {
- val = this.tagValCache.key;
- } else {
- val = this.tag.getAttribute(prefix + key);
- if (val === undefined || val === null) {
- val = UNSET_OPTION;
- } else if (val.substr(0, 1) === '[') {
- val = val.substr(1, val.length - 2).split(',');
- for (i = val.length; i--;) {
- val[i] = normalizeValue(val[i].replace(/(^\s*)|(\s*$)/g, ''));
- }
- } else if (val.substr(0, 1) === '{') {
- pairs = val.substr(1, val.length - 2).split(',');
- val = {};
- for (i = pairs.length; i--;) {
- keyval = pairs[i].split(':', 2);
- val[keyval[0].replace(/(^\s*)|(\s*$)/g, '')] = normalizeValue(keyval[1].replace(/(^\s*)|(\s*$)/g, ''));
- }
- } else {
- val = normalizeValue(val);
- }
- this.tagValCache.key = val;
- }
- return val;
- },
-
- get: function (key, defaultval) {
- var tagOption = this.getTagSetting(key),
- result;
- if (tagOption !== UNSET_OPTION) {
- return tagOption;
- }
- return (result = this.mergedOptions[key]) === undefined ? defaultval : result;
- }
- });
-
-
- $.fn.sparkline._base = createClass({
- disabled: false,
-
- init: function (el, values, options, width, height) {
- this.el = el;
- this.$el = $(el);
- this.values = values;
- this.options = options;
- this.width = width;
- this.height = height;
- this.currentRegion = undefined;
- },
-
- /**
- * Setup the canvas
- */
- initTarget: function () {
- var interactive = !this.options.get('disableInteraction');
- if (!(this.target = this.$el.simpledraw(this.width, this.height, this.options.get('composite'), interactive))) {
- this.disabled = true;
- } else {
- this.canvasWidth = this.target.pixelWidth;
- this.canvasHeight = this.target.pixelHeight;
- }
- },
-
- /**
- * Actually render the chart to the canvas
- */
- render: function () {
- if (this.disabled) {
- this.el.innerHTML = '';
- return false;
- }
- return true;
- },
-
- /**
- * Return a region id for a given x/y co-ordinate
- */
- getRegion: function (x, y) {
- },
-
- /**
- * Highlight an item based on the moused-over x,y co-ordinate
- */
- setRegionHighlight: function (el, x, y) {
- var currentRegion = this.currentRegion,
- highlightEnabled = !this.options.get('disableHighlight'),
- newRegion;
- if (x > this.canvasWidth || y > this.canvasHeight || x < 0 || y < 0) {
- return null;
- }
- newRegion = this.getRegion(el, x, y);
- if (currentRegion !== newRegion) {
- if (currentRegion !== undefined && highlightEnabled) {
- this.removeHighlight();
- }
- this.currentRegion = newRegion;
- if (newRegion !== undefined && highlightEnabled) {
- this.renderHighlight();
- }
- return true;
- }
- return false;
- },
-
- /**
- * Reset any currently highlighted item
- */
- clearRegionHighlight: function () {
- if (this.currentRegion !== undefined) {
- this.removeHighlight();
- this.currentRegion = undefined;
- return true;
- }
- return false;
- },
-
- renderHighlight: function () {
- this.changeHighlight(true);
- },
-
- removeHighlight: function () {
- this.changeHighlight(false);
- },
-
- changeHighlight: function (highlight) {},
-
- /**
- * Fetch the HTML to display as a tooltip
- */
- getCurrentRegionTooltip: function () {
- var options = this.options,
- header = '',
- entries = [],
- fields, formats, formatlen, fclass, text, i,
- showFields, showFieldsKey, newFields, fv,
- formatter, format, fieldlen, j;
- if (this.currentRegion === undefined) {
- return '';
- }
- fields = this.getCurrentRegionFields();
- formatter = options.get('tooltipFormatter');
- if (formatter) {
- return formatter(this, options, fields);
- }
- if (options.get('tooltipChartTitle')) {
- header += '
' + options.get('tooltipChartTitle') + '
\n';
- }
- formats = this.options.get('tooltipFormat');
- if (!formats) {
- return '';
- }
- if (!$.isArray(formats)) {
- formats = [formats];
- }
- if (!$.isArray(fields)) {
- fields = [fields];
- }
- showFields = this.options.get('tooltipFormatFieldlist');
- showFieldsKey = this.options.get('tooltipFormatFieldlistKey');
- if (showFields && showFieldsKey) {
- // user-selected ordering of fields
- newFields = [];
- for (i = fields.length; i--;) {
- fv = fields[i][showFieldsKey];
- if ((j = $.inArray(fv, showFields)) != -1) {
- newFields[j] = fields[i];
- }
- }
- fields = newFields;
- }
- formatlen = formats.length;
- fieldlen = fields.length;
- for (i = 0; i < formatlen; i++) {
- format = formats[i];
- if (typeof format === 'string') {
- format = new SPFormat(format);
- }
- fclass = format.fclass || 'jqsfield';
- for (j = 0; j < fieldlen; j++) {
- if (!fields[j].isNull || !options.get('tooltipSkipNull')) {
- $.extend(fields[j], {
- prefix: options.get('tooltipPrefix'),
- suffix: options.get('tooltipSuffix')
- });
- text = format.render(fields[j], options.get('tooltipValueLookups'), options);
- entries.push('
' + text + '
');
- }
- }
- }
- if (entries.length) {
- return header + entries.join('\n');
- }
- return '';
- },
-
- getCurrentRegionFields: function () {},
-
- calcHighlightColor: function (color, options) {
- var highlightColor = options.get('highlightColor'),
- lighten = options.get('highlightLighten'),
- parse, mult, rgbnew, i;
- if (highlightColor) {
- return highlightColor;
- }
- if (lighten) {
- // extract RGB values
- parse = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(color) || /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(color);
- if (parse) {
- rgbnew = [];
- mult = color.length === 4 ? 16 : 1;
- for (i = 0; i < 3; i++) {
- rgbnew[i] = clipval(Math.round(parseInt(parse[i + 1], 16) * mult * lighten), 0, 255);
- }
- return 'rgb(' + rgbnew.join(',') + ')';
- }
-
- }
- return color;
- }
-
- });
-
- barHighlightMixin = {
- changeHighlight: function (highlight) {
- var currentRegion = this.currentRegion,
- target = this.target,
- shapeids = this.regionShapes[currentRegion],
- newShapes;
- // will be null if the region value was null
- if (shapeids) {
- newShapes = this.renderRegion(currentRegion, highlight);
- if ($.isArray(newShapes) || $.isArray(shapeids)) {
- target.replaceWithShapes(shapeids, newShapes);
- this.regionShapes[currentRegion] = $.map(newShapes, function (newShape) {
- return newShape.id;
- });
- } else {
- target.replaceWithShape(shapeids, newShapes);
- this.regionShapes[currentRegion] = newShapes.id;
- }
- }
- },
-
- render: function () {
- var values = this.values,
- target = this.target,
- regionShapes = this.regionShapes,
- shapes, ids, i, j;
-
- if (!this.cls._super.render.call(this)) {
- return;
- }
- for (i = values.length; i--;) {
- shapes = this.renderRegion(i);
- if (shapes) {
- if ($.isArray(shapes)) {
- ids = [];
- for (j = shapes.length; j--;) {
- shapes[j].append();
- ids.push(shapes[j].id);
- }
- regionShapes[i] = ids;
- } else {
- shapes.append();
- regionShapes[i] = shapes.id; // store just the shapeid
- }
- } else {
- // null value
- regionShapes[i] = null;
- }
- }
- target.render();
- }
- };
-
- /**
- * Line charts
- */
- $.fn.sparkline.line = line = createClass($.fn.sparkline._base, {
- type: 'line',
-
- init: function (el, values, options, width, height) {
- line._super.init.call(this, el, values, options, width, height);
- this.vertices = [];
- this.regionMap = [];
- this.xvalues = [];
- this.yvalues = [];
- this.yminmax = [];
- this.hightlightSpotId = null;
- this.lastShapeId = null;
- this.initTarget();
- },
-
- getRegion: function (el, x, y) {
- var i,
- regionMap = this.regionMap; // maps regions to value positions
- for (i = regionMap.length; i--;) {
- if (regionMap[i] !== null && x >= regionMap[i][0] && x <= regionMap[i][1]) {
- return regionMap[i][2];
- }
- }
- return undefined;
- },
-
- getCurrentRegionFields: function () {
- var currentRegion = this.currentRegion;
- return {
- isNull: this.yvalues[currentRegion] === null,
- x: this.xvalues[currentRegion],
- y: this.yvalues[currentRegion],
- color: this.options.get('lineColor'),
- fillColor: this.options.get('fillColor'),
- offset: currentRegion
- };
- },
-
- renderHighlight: function () {
- var currentRegion = this.currentRegion,
- target = this.target,
- vertex = this.vertices[currentRegion],
- options = this.options,
- spotRadius = options.get('spotRadius'),
- highlightSpotColor = options.get('highlightSpotColor'),
- highlightLineColor = options.get('highlightLineColor'),
- highlightSpot, highlightLine;
-
- if (!vertex) {
- return;
- }
- if (spotRadius && highlightSpotColor) {
- highlightSpot = target.drawCircle(vertex[0], vertex[1],
- spotRadius, undefined, highlightSpotColor);
- this.highlightSpotId = highlightSpot.id;
- target.insertAfterShape(this.lastShapeId, highlightSpot);
- }
- if (highlightLineColor) {
- highlightLine = target.drawLine(vertex[0], this.canvasTop, vertex[0],
- this.canvasTop + this.canvasHeight, highlightLineColor);
- this.highlightLineId = highlightLine.id;
- target.insertAfterShape(this.lastShapeId, highlightLine);
- }
- },
-
- removeHighlight: function () {
- var target = this.target;
- if (this.highlightSpotId) {
- target.removeShapeId(this.highlightSpotId);
- this.highlightSpotId = null;
- }
- if (this.highlightLineId) {
- target.removeShapeId(this.highlightLineId);
- this.highlightLineId = null;
- }
- },
-
- scanValues: function () {
- var values = this.values,
- valcount = values.length,
- xvalues = this.xvalues,
- yvalues = this.yvalues,
- yminmax = this.yminmax,
- i, val, isStr, isArray, sp;
- for (i = 0; i < valcount; i++) {
- val = values[i];
- isStr = typeof(values[i]) === 'string';
- isArray = typeof(values[i]) === 'object' && values[i] instanceof Array;
- sp = isStr && values[i].split(':');
- if (isStr && sp.length === 2) { // x:y
- xvalues.push(Number(sp[0]));
- yvalues.push(Number(sp[1]));
- yminmax.push(Number(sp[1]));
- } else if (isArray) {
- xvalues.push(val[0]);
- yvalues.push(val[1]);
- yminmax.push(val[1]);
- } else {
- xvalues.push(i);
- if (values[i] === null || values[i] === 'null') {
- yvalues.push(null);
- } else {
- yvalues.push(Number(val));
- yminmax.push(Number(val));
- }
- }
- }
- if (this.options.get('xvalues')) {
- xvalues = this.options.get('xvalues');
- }
-
- this.maxy = this.maxyorg = Math.max.apply(Math, yminmax);
- this.miny = this.minyorg = Math.min.apply(Math, yminmax);
-
- this.maxx = Math.max.apply(Math, xvalues);
- this.minx = Math.min.apply(Math, xvalues);
-
- this.xvalues = xvalues;
- this.yvalues = yvalues;
- this.yminmax = yminmax;
-
- },
-
- processRangeOptions: function () {
- var options = this.options,
- normalRangeMin = options.get('normalRangeMin'),
- normalRangeMax = options.get('normalRangeMax');
-
- if (normalRangeMin !== undefined) {
- if (normalRangeMin < this.miny) {
- this.miny = normalRangeMin;
- }
- if (normalRangeMax > this.maxy) {
- this.maxy = normalRangeMax;
- }
- }
- if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < this.miny)) {
- this.miny = options.get('chartRangeMin');
- }
- if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > this.maxy)) {
- this.maxy = options.get('chartRangeMax');
- }
- if (options.get('chartRangeMinX') !== undefined && (options.get('chartRangeClipX') || options.get('chartRangeMinX') < this.minx)) {
- this.minx = options.get('chartRangeMinX');
- }
- if (options.get('chartRangeMaxX') !== undefined && (options.get('chartRangeClipX') || options.get('chartRangeMaxX') > this.maxx)) {
- this.maxx = options.get('chartRangeMaxX');
- }
-
- },
-
- drawNormalRange: function (canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey) {
- var normalRangeMin = this.options.get('normalRangeMin'),
- normalRangeMax = this.options.get('normalRangeMax'),
- ytop = canvasTop + Math.round(canvasHeight - (canvasHeight * ((normalRangeMax - this.miny) / rangey))),
- height = Math.round((canvasHeight * (normalRangeMax - normalRangeMin)) / rangey);
- this.target.drawRect(canvasLeft, ytop, canvasWidth, height, undefined, this.options.get('normalRangeColor')).append();
- },
-
- render: function () {
- var options = this.options,
- target = this.target,
- canvasWidth = this.canvasWidth,
- canvasHeight = this.canvasHeight,
- vertices = this.vertices,
- spotRadius = options.get('spotRadius'),
- regionMap = this.regionMap,
- rangex, rangey, yvallast,
- canvasTop, canvasLeft,
- vertex, path, paths, x, y, xnext, xpos, xposnext,
- last, next, yvalcount, lineShapes, fillShapes, plen,
- valueSpots, hlSpotsEnabled, color, xvalues, yvalues, i;
-
- if (!line._super.render.call(this)) {
- return;
- }
-
- this.scanValues();
- this.processRangeOptions();
-
- xvalues = this.xvalues;
- yvalues = this.yvalues;
-
- if (!this.yminmax.length || this.yvalues.length < 2) {
- // empty or all null valuess
- return;
- }
-
- canvasTop = canvasLeft = 0;
-
- rangex = this.maxx - this.minx === 0 ? 1 : this.maxx - this.minx;
- rangey = this.maxy - this.miny === 0 ? 1 : this.maxy - this.miny;
- yvallast = this.yvalues.length - 1;
-
- if (spotRadius && (canvasWidth < (spotRadius * 4) || canvasHeight < (spotRadius * 4))) {
- spotRadius = 0;
- }
- if (spotRadius) {
- // adjust the canvas size as required so that spots will fit
- hlSpotsEnabled = options.get('highlightSpotColor') && !options.get('disableInteraction');
- if (hlSpotsEnabled || options.get('minSpotColor') || (options.get('spotColor') && yvalues[yvallast] === this.miny)) {
- canvasHeight -= Math.ceil(spotRadius);
- }
- if (hlSpotsEnabled || options.get('maxSpotColor') || (options.get('spotColor') && yvalues[yvallast] === this.maxy)) {
- canvasHeight -= Math.ceil(spotRadius);
- canvasTop += Math.ceil(spotRadius);
- }
- if (hlSpotsEnabled ||
- ((options.get('minSpotColor') || options.get('maxSpotColor')) && (yvalues[0] === this.miny || yvalues[0] === this.maxy))) {
- canvasLeft += Math.ceil(spotRadius);
- canvasWidth -= Math.ceil(spotRadius);
- }
- if (hlSpotsEnabled || options.get('spotColor') ||
- (options.get('minSpotColor') || options.get('maxSpotColor') &&
- (yvalues[yvallast] === this.miny || yvalues[yvallast] === this.maxy))) {
- canvasWidth -= Math.ceil(spotRadius);
- }
- }
-
-
- canvasHeight--;
-
- if (options.get('normalRangeMin') !== undefined && !options.get('drawNormalOnTop')) {
- this.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey);
- }
-
- path = [];
- paths = [path];
- last = next = null;
- yvalcount = yvalues.length;
- for (i = 0; i < yvalcount; i++) {
- x = xvalues[i];
- xnext = xvalues[i + 1];
- y = yvalues[i];
- xpos = canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex));
- xposnext = i < yvalcount - 1 ? canvasLeft + Math.round((xnext - this.minx) * (canvasWidth / rangex)) : canvasWidth;
- next = xpos + ((xposnext - xpos) / 2);
- regionMap[i] = [last || 0, next, i];
- last = next;
- if (y === null) {
- if (i) {
- if (yvalues[i - 1] !== null) {
- path = [];
- paths.push(path);
- }
- vertices.push(null);
- }
- } else {
- if (y < this.miny) {
- y = this.miny;
- }
- if (y > this.maxy) {
- y = this.maxy;
- }
- if (!path.length) {
- // previous value was null
- path.push([xpos, canvasTop + canvasHeight]);
- }
- vertex = [xpos, canvasTop + Math.round(canvasHeight - (canvasHeight * ((y - this.miny) / rangey)))];
- path.push(vertex);
- vertices.push(vertex);
- }
- }
-
- lineShapes = [];
- fillShapes = [];
- plen = paths.length;
- for (i = 0; i < plen; i++) {
- path = paths[i];
- if (path.length) {
- if (options.get('fillColor')) {
- path.push([path[path.length - 1][0], (canvasTop + canvasHeight)]);
- fillShapes.push(path.slice(0));
- path.pop();
- }
- // if there's only a single point in this path, then we want to display it
- // as a vertical line which means we keep path[0] as is
- if (path.length > 2) {
- // else we want the first value
- path[0] = [path[0][0], path[1][1]];
- }
- lineShapes.push(path);
- }
- }
-
- // draw the fill first, then optionally the normal range, then the line on top of that
- plen = fillShapes.length;
- for (i = 0; i < plen; i++) {
- target.drawShape(fillShapes[i],
- options.get('fillColor'), options.get('fillColor')).append();
- }
-
- if (options.get('normalRangeMin') !== undefined && options.get('drawNormalOnTop')) {
- this.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey);
- }
-
- plen = lineShapes.length;
- for (i = 0; i < plen; i++) {
- target.drawShape(lineShapes[i], options.get('lineColor'), undefined,
- options.get('lineWidth')).append();
- }
-
- if (spotRadius && options.get('valueSpots')) {
- valueSpots = options.get('valueSpots');
- if (valueSpots.get === undefined) {
- valueSpots = new RangeMap(valueSpots);
- }
- for (i = 0; i < yvalcount; i++) {
- color = valueSpots.get(yvalues[i]);
- if (color) {
- target.drawCircle(canvasLeft + Math.round((xvalues[i] - this.minx) * (canvasWidth / rangex)),
- canvasTop + Math.round(canvasHeight - (canvasHeight * ((yvalues[i] - this.miny) / rangey))),
- spotRadius, undefined,
- color).append();
- }
- }
-
- }
- if (spotRadius && options.get('spotColor') && yvalues[yvallast] !== null) {
- target.drawCircle(canvasLeft + Math.round((xvalues[xvalues.length - 1] - this.minx) * (canvasWidth / rangex)),
- canvasTop + Math.round(canvasHeight - (canvasHeight * ((yvalues[yvallast] - this.miny) / rangey))),
- spotRadius, undefined,
- options.get('spotColor')).append();
- }
- if (this.maxy !== this.minyorg) {
- if (spotRadius && options.get('minSpotColor')) {
- x = xvalues[$.inArray(this.minyorg, yvalues)];
- target.drawCircle(canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)),
- canvasTop + Math.round(canvasHeight - (canvasHeight * ((this.minyorg - this.miny) / rangey))),
- spotRadius, undefined,
- options.get('minSpotColor')).append();
- }
- if (spotRadius && options.get('maxSpotColor')) {
- x = xvalues[$.inArray(this.maxyorg, yvalues)];
- target.drawCircle(canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)),
- canvasTop + Math.round(canvasHeight - (canvasHeight * ((this.maxyorg - this.miny) / rangey))),
- spotRadius, undefined,
- options.get('maxSpotColor')).append();
- }
- }
-
- this.lastShapeId = target.getLastShapeId();
- this.canvasTop = canvasTop;
- target.render();
- }
- });
-
- /**
- * Bar charts
- */
- $.fn.sparkline.bar = bar = createClass($.fn.sparkline._base, barHighlightMixin, {
- type: 'bar',
-
- init: function (el, values, options, width, height) {
- var barWidth = parseInt(options.get('barWidth'), 10),
- barSpacing = parseInt(options.get('barSpacing'), 10),
- chartRangeMin = options.get('chartRangeMin'),
- chartRangeMax = options.get('chartRangeMax'),
- chartRangeClip = options.get('chartRangeClip'),
- stackMin = Infinity,
- stackMax = -Infinity,
- isStackString, groupMin, groupMax, stackRanges,
- numValues, i, vlen, range, zeroAxis, xaxisOffset, min, max, clipMin, clipMax,
- stacked, vlist, j, slen, svals, val, yoffset, yMaxCalc, canvasHeightEf;
- bar._super.init.call(this, el, values, options, width, height);
-
- // scan values to determine whether to stack bars
- for (i = 0, vlen = values.length; i < vlen; i++) {
- val = values[i];
- isStackString = typeof(val) === 'string' && val.indexOf(':') > -1;
- if (isStackString || $.isArray(val)) {
- stacked = true;
- if (isStackString) {
- val = values[i] = normalizeValues(val.split(':'));
- }
- val = remove(val, null); // min/max will treat null as zero
- groupMin = Math.min.apply(Math, val);
- groupMax = Math.max.apply(Math, val);
- if (groupMin < stackMin) {
- stackMin = groupMin;
- }
- if (groupMax > stackMax) {
- stackMax = groupMax;
- }
- }
- }
-
- this.stacked = stacked;
- this.regionShapes = {};
- this.barWidth = barWidth;
- this.barSpacing = barSpacing;
- this.totalBarWidth = barWidth + barSpacing;
- this.width = width = (values.length * barWidth) + ((values.length - 1) * barSpacing);
-
- this.initTarget();
-
- if (chartRangeClip) {
- clipMin = chartRangeMin === undefined ? -Infinity : chartRangeMin;
- clipMax = chartRangeMax === undefined ? Infinity : chartRangeMax;
- }
-
- numValues = [];
- stackRanges = stacked ? [] : numValues;
- var stackTotals = [];
- var stackRangesNeg = [];
- for (i = 0, vlen = values.length; i < vlen; i++) {
- if (stacked) {
- vlist = values[i];
- values[i] = svals = [];
- stackTotals[i] = 0;
- stackRanges[i] = stackRangesNeg[i] = 0;
- for (j = 0, slen = vlist.length; j < slen; j++) {
- val = svals[j] = chartRangeClip ? clipval(vlist[j], clipMin, clipMax) : vlist[j];
- if (val !== null) {
- if (val > 0) {
- stackTotals[i] += val;
- }
- if (stackMin < 0 && stackMax > 0) {
- if (val < 0) {
- stackRangesNeg[i] += Math.abs(val);
- } else {
- stackRanges[i] += val;
- }
- } else {
- stackRanges[i] += Math.abs(val - (val < 0 ? stackMax : stackMin));
- }
- numValues.push(val);
- }
- }
- } else {
- val = chartRangeClip ? clipval(values[i], clipMin, clipMax) : values[i];
- val = values[i] = normalizeValue(val);
- if (val !== null) {
- numValues.push(val);
- }
- }
- }
- this.max = max = Math.max.apply(Math, numValues);
- this.min = min = Math.min.apply(Math, numValues);
- this.stackMax = stackMax = stacked ? Math.max.apply(Math, stackTotals) : max;
- this.stackMin = stackMin = stacked ? Math.min.apply(Math, numValues) : min;
-
- if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < min)) {
- min = options.get('chartRangeMin');
- }
- if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > max)) {
- max = options.get('chartRangeMax');
- }
-
- this.zeroAxis = zeroAxis = options.get('zeroAxis', true);
- if (min <= 0 && max >= 0 && zeroAxis) {
- xaxisOffset = 0;
- } else if (zeroAxis == false) {
- xaxisOffset = min;
- } else if (min > 0) {
- xaxisOffset = min;
- } else {
- xaxisOffset = max;
- }
- this.xaxisOffset = xaxisOffset;
-
- range = stacked ? (Math.max.apply(Math, stackRanges) + Math.max.apply(Math, stackRangesNeg)) : max - min;
-
- // as we plot zero/min values a single pixel line, we add a pixel to all other
- // values - Reduce the effective canvas size to suit
- this.canvasHeightEf = (zeroAxis && min < 0) ? this.canvasHeight - 2 : this.canvasHeight - 1;
-
- if (min < xaxisOffset) {
- yMaxCalc = (stacked && max >= 0) ? stackMax : max;
- yoffset = (yMaxCalc - xaxisOffset) / range * this.canvasHeight;
- if (yoffset !== Math.ceil(yoffset)) {
- this.canvasHeightEf -= 2;
- yoffset = Math.ceil(yoffset);
- }
- } else {
- yoffset = this.canvasHeight;
- }
- this.yoffset = yoffset;
-
- if ($.isArray(options.get('colorMap'))) {
- this.colorMapByIndex = options.get('colorMap');
- this.colorMapByValue = null;
- } else {
- this.colorMapByIndex = null;
- this.colorMapByValue = options.get('colorMap');
- if (this.colorMapByValue && this.colorMapByValue.get === undefined) {
- this.colorMapByValue = new RangeMap(this.colorMapByValue);
- }
- }
-
- this.range = range;
- },
-
- getRegion: function (el, x, y) {
- var result = Math.floor(x / this.totalBarWidth);
- return (result < 0 || result >= this.values.length) ? undefined : result;
- },
-
- getCurrentRegionFields: function () {
- var currentRegion = this.currentRegion,
- values = ensureArray(this.values[currentRegion]),
- result = [],
- value, i;
- for (i = values.length; i--;) {
- value = values[i];
- result.push({
- isNull: value === null,
- value: value,
- color: this.calcColor(i, value, currentRegion),
- offset: currentRegion
- });
- }
- return result;
- },
-
- calcColor: function (stacknum, value, valuenum) {
- var colorMapByIndex = this.colorMapByIndex,
- colorMapByValue = this.colorMapByValue,
- options = this.options,
- color, newColor;
- if (this.stacked) {
- color = options.get('stackedBarColor');
- } else {
- color = (value < 0) ? options.get('negBarColor') : options.get('barColor');
- }
- if (value === 0 && options.get('zeroColor') !== undefined) {
- color = options.get('zeroColor');
- }
- if (colorMapByValue && (newColor = colorMapByValue.get(value))) {
- color = newColor;
- } else if (colorMapByIndex && colorMapByIndex.length > valuenum) {
- color = colorMapByIndex[valuenum];
- }
- return $.isArray(color) ? color[stacknum % color.length] : color;
- },
-
- /**
- * Render bar(s) for a region
- */
- renderRegion: function (valuenum, highlight) {
- var vals = this.values[valuenum],
- options = this.options,
- xaxisOffset = this.xaxisOffset,
- result = [],
- range = this.range,
- stacked = this.stacked,
- target = this.target,
- x = valuenum * this.totalBarWidth,
- canvasHeightEf = this.canvasHeightEf,
- yoffset = this.yoffset,
- y, height, color, isNull, yoffsetNeg, i, valcount, val, minPlotted, allMin;
-
- vals = $.isArray(vals) ? vals : [vals];
- valcount = vals.length;
- val = vals[0];
- isNull = all(null, vals);
- allMin = all(xaxisOffset, vals, true);
-
- if (isNull) {
- if (options.get('nullColor')) {
- color = highlight ? options.get('nullColor') : this.calcHighlightColor(options.get('nullColor'), options);
- y = (yoffset > 0) ? yoffset - 1 : yoffset;
- return target.drawRect(x, y, this.barWidth - 1, 0, color, color);
- } else {
- return undefined;
- }
- }
- yoffsetNeg = yoffset;
- for (i = 0; i < valcount; i++) {
- val = vals[i];
-
- if (stacked && val === xaxisOffset) {
- if (!allMin || minPlotted) {
- continue;
- }
- minPlotted = true;
- }
-
- if (range > 0) {
- height = Math.floor(canvasHeightEf * ((Math.abs(val - xaxisOffset) / range))) + 1;
- } else {
- height = 1;
- }
- if (val < xaxisOffset || (val === xaxisOffset && yoffset === 0)) {
- y = yoffsetNeg;
- yoffsetNeg += height;
- } else {
- y = yoffset - height;
- yoffset -= height;
- }
- color = this.calcColor(i, val, valuenum);
- if (highlight) {
- color = this.calcHighlightColor(color, options);
- }
- result.push(target.drawRect(x, y, this.barWidth - 1, height - 1, color, color));
- }
- if (result.length === 1) {
- return result[0];
- }
- return result;
- }
- });
-
- /**
- * Tristate charts
- */
- $.fn.sparkline.tristate = tristate = createClass($.fn.sparkline._base, barHighlightMixin, {
- type: 'tristate',
-
- init: function (el, values, options, width, height) {
- var barWidth = parseInt(options.get('barWidth'), 10),
- barSpacing = parseInt(options.get('barSpacing'), 10);
- tristate._super.init.call(this, el, values, options, width, height);
-
- this.regionShapes = {};
- this.barWidth = barWidth;
- this.barSpacing = barSpacing;
- this.totalBarWidth = barWidth + barSpacing;
- this.values = $.map(values, Number);
- this.width = width = (values.length * barWidth) + ((values.length - 1) * barSpacing);
-
- if ($.isArray(options.get('colorMap'))) {
- this.colorMapByIndex = options.get('colorMap');
- this.colorMapByValue = null;
- } else {
- this.colorMapByIndex = null;
- this.colorMapByValue = options.get('colorMap');
- if (this.colorMapByValue && this.colorMapByValue.get === undefined) {
- this.colorMapByValue = new RangeMap(this.colorMapByValue);
- }
- }
- this.initTarget();
- },
-
- getRegion: function (el, x, y) {
- return Math.floor(x / this.totalBarWidth);
- },
-
- getCurrentRegionFields: function () {
- var currentRegion = this.currentRegion;
- return {
- isNull: this.values[currentRegion] === undefined,
- value: this.values[currentRegion],
- color: this.calcColor(this.values[currentRegion], currentRegion),
- offset: currentRegion
- };
- },
-
- calcColor: function (value, valuenum) {
- var values = this.values,
- options = this.options,
- colorMapByIndex = this.colorMapByIndex,
- colorMapByValue = this.colorMapByValue,
- color, newColor;
-
- if (colorMapByValue && (newColor = colorMapByValue.get(value))) {
- color = newColor;
- } else if (colorMapByIndex && colorMapByIndex.length > valuenum) {
- color = colorMapByIndex[valuenum];
- } else if (values[valuenum] < 0) {
- color = options.get('negBarColor');
- } else if (values[valuenum] > 0) {
- color = options.get('posBarColor');
- } else {
- color = options.get('zeroBarColor');
- }
- return color;
- },
-
- renderRegion: function (valuenum, highlight) {
- var values = this.values,
- options = this.options,
- target = this.target,
- canvasHeight, height, halfHeight,
- x, y, color;
-
- canvasHeight = target.pixelHeight;
- halfHeight = Math.round(canvasHeight / 2);
-
- x = valuenum * this.totalBarWidth;
- if (values[valuenum] < 0) {
- y = halfHeight;
- height = halfHeight - 1;
- } else if (values[valuenum] > 0) {
- y = 0;
- height = halfHeight - 1;
- } else {
- y = halfHeight - 1;
- height = 2;
- }
- color = this.calcColor(values[valuenum], valuenum);
- if (color === null) {
- return;
- }
- if (highlight) {
- color = this.calcHighlightColor(color, options);
- }
- return target.drawRect(x, y, this.barWidth - 1, height - 1, color, color);
- }
- });
-
- /**
- * Discrete charts
- */
- $.fn.sparkline.discrete = discrete = createClass($.fn.sparkline._base, barHighlightMixin, {
- type: 'discrete',
-
- init: function (el, values, options, width, height) {
- discrete._super.init.call(this, el, values, options, width, height);
-
- this.regionShapes = {};
- this.values = values = $.map(values, Number);
- this.min = Math.min.apply(Math, values);
- this.max = Math.max.apply(Math, values);
- this.range = this.max - this.min;
- this.width = width = options.get('width') === 'auto' ? values.length * 2 : this.width;
- this.interval = Math.floor(width / values.length);
- this.itemWidth = width / values.length;
- if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < this.min)) {
- this.min = options.get('chartRangeMin');
- }
- if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > this.max)) {
- this.max = options.get('chartRangeMax');
- }
- this.initTarget();
- if (this.target) {
- this.lineHeight = options.get('lineHeight') === 'auto' ? Math.round(this.canvasHeight * 0.3) : options.get('lineHeight');
- }
- },
-
- getRegion: function (el, x, y) {
- return Math.floor(x / this.itemWidth);
- },
-
- getCurrentRegionFields: function () {
- var currentRegion = this.currentRegion;
- return {
- isNull: this.values[currentRegion] === undefined,
- value: this.values[currentRegion],
- offset: currentRegion
- };
- },
-
- renderRegion: function (valuenum, highlight) {
- var values = this.values,
- options = this.options,
- min = this.min,
- max = this.max,
- range = this.range,
- interval = this.interval,
- target = this.target,
- canvasHeight = this.canvasHeight,
- lineHeight = this.lineHeight,
- pheight = canvasHeight - lineHeight,
- ytop, val, color, x;
-
- val = clipval(values[valuenum], min, max);
- x = valuenum * interval;
- ytop = Math.round(pheight - pheight * ((val - min) / range));
- color = (options.get('thresholdColor') && val < options.get('thresholdValue')) ? options.get('thresholdColor') : options.get('lineColor');
- if (highlight) {
- color = this.calcHighlightColor(color, options);
- }
- return target.drawLine(x, ytop, x, ytop + lineHeight, color);
- }
- });
-
- /**
- * Bullet charts
- */
- $.fn.sparkline.bullet = bullet = createClass($.fn.sparkline._base, {
- type: 'bullet',
-
- init: function (el, values, options, width, height) {
- var min, max, vals;
- bullet._super.init.call(this, el, values, options, width, height);
-
- // values: target, performance, range1, range2, range3
- this.values = values = normalizeValues(values);
- // target or performance could be null
- vals = values.slice();
- vals[0] = vals[0] === null ? vals[2] : vals[0];
- vals[1] = values[1] === null ? vals[2] : vals[1];
- min = Math.min.apply(Math, values);
- max = Math.max.apply(Math, values);
- if (options.get('base') === undefined) {
- min = min < 0 ? min : 0;
- } else {
- min = options.get('base');
- }
- this.min = min;
- this.max = max;
- this.range = max - min;
- this.shapes = {};
- this.valueShapes = {};
- this.regiondata = {};
- this.width = width = options.get('width') === 'auto' ? '4.0em' : width;
- this.target = this.$el.simpledraw(width, height, options.get('composite'));
- if (!values.length) {
- this.disabled = true;
- }
- this.initTarget();
- },
-
- getRegion: function (el, x, y) {
- var shapeid = this.target.getShapeAt(el, x, y);
- return (shapeid !== undefined && this.shapes[shapeid] !== undefined) ? this.shapes[shapeid] : undefined;
- },
-
- getCurrentRegionFields: function () {
- var currentRegion = this.currentRegion;
- return {
- fieldkey: currentRegion.substr(0, 1),
- value: this.values[currentRegion.substr(1)],
- region: currentRegion
- };
- },
-
- changeHighlight: function (highlight) {
- var currentRegion = this.currentRegion,
- shapeid = this.valueShapes[currentRegion],
- shape;
- delete this.shapes[shapeid];
- switch (currentRegion.substr(0, 1)) {
- case 'r':
- shape = this.renderRange(currentRegion.substr(1), highlight);
- break;
- case 'p':
- shape = this.renderPerformance(highlight);
- break;
- case 't':
- shape = this.renderTarget(highlight);
- break;
- }
- this.valueShapes[currentRegion] = shape.id;
- this.shapes[shape.id] = currentRegion;
- this.target.replaceWithShape(shapeid, shape);
- },
-
- renderRange: function (rn, highlight) {
- var rangeval = this.values[rn],
- rangewidth = Math.round(this.canvasWidth * ((rangeval - this.min) / this.range)),
- color = this.options.get('rangeColors')[rn - 2];
- if (highlight) {
- color = this.calcHighlightColor(color, this.options);
- }
- return this.target.drawRect(0, 0, rangewidth - 1, this.canvasHeight - 1, color, color);
- },
-
- renderPerformance: function (highlight) {
- var perfval = this.values[1],
- perfwidth = Math.round(this.canvasWidth * ((perfval - this.min) / this.range)),
- color = this.options.get('performanceColor');
- if (highlight) {
- color = this.calcHighlightColor(color, this.options);
- }
- return this.target.drawRect(0, Math.round(this.canvasHeight * 0.3), perfwidth - 1,
- Math.round(this.canvasHeight * 0.4) - 1, color, color);
- },
-
- renderTarget: function (highlight) {
- var targetval = this.values[0],
- x = Math.round(this.canvasWidth * ((targetval - this.min) / this.range) - (this.options.get('targetWidth') / 2)),
- targettop = Math.round(this.canvasHeight * 0.10),
- targetheight = this.canvasHeight - (targettop * 2),
- color = this.options.get('targetColor');
- if (highlight) {
- color = this.calcHighlightColor(color, this.options);
- }
- return this.target.drawRect(x, targettop, this.options.get('targetWidth') - 1, targetheight - 1, color, color);
- },
-
- render: function () {
- var vlen = this.values.length,
- target = this.target,
- i, shape;
- if (!bullet._super.render.call(this)) {
- return;
- }
- for (i = 2; i < vlen; i++) {
- shape = this.renderRange(i).append();
- this.shapes[shape.id] = 'r' + i;
- this.valueShapes['r' + i] = shape.id;
- }
- if (this.values[1] !== null) {
- shape = this.renderPerformance().append();
- this.shapes[shape.id] = 'p1';
- this.valueShapes.p1 = shape.id;
- }
- if (this.values[0] !== null) {
- shape = this.renderTarget().append();
- this.shapes[shape.id] = 't0';
- this.valueShapes.t0 = shape.id;
- }
- target.render();
- }
- });
-
- /**
- * Pie charts
- */
- $.fn.sparkline.pie = pie = createClass($.fn.sparkline._base, {
- type: 'pie',
-
- init: function (el, values, options, width, height) {
- var total = 0, i;
-
- pie._super.init.call(this, el, values, options, width, height);
-
- this.shapes = {}; // map shape ids to value offsets
- this.valueShapes = {}; // maps value offsets to shape ids
- this.values = values = $.map(values, Number);
-
- if (options.get('width') === 'auto') {
- this.width = this.height;
- }
-
- if (values.length > 0) {
- for (i = values.length; i--;) {
- total += values[i];
- }
- }
- this.total = total;
- this.initTarget();
- this.radius = Math.floor(Math.min(this.canvasWidth, this.canvasHeight) / 2);
- },
-
- getRegion: function (el, x, y) {
- var shapeid = this.target.getShapeAt(el, x, y);
- return (shapeid !== undefined && this.shapes[shapeid] !== undefined) ? this.shapes[shapeid] : undefined;
- },
-
- getCurrentRegionFields: function () {
- var currentRegion = this.currentRegion;
- return {
- isNull: this.values[currentRegion] === undefined,
- value: this.values[currentRegion],
- percent: this.values[currentRegion] / this.total * 100,
- color: this.options.get('sliceColors')[currentRegion % this.options.get('sliceColors').length],
- offset: currentRegion
- };
- },
-
- changeHighlight: function (highlight) {
- var currentRegion = this.currentRegion,
- newslice = this.renderSlice(currentRegion, highlight),
- shapeid = this.valueShapes[currentRegion];
- delete this.shapes[shapeid];
- this.target.replaceWithShape(shapeid, newslice);
- this.valueShapes[currentRegion] = newslice.id;
- this.shapes[newslice.id] = currentRegion;
- },
-
- renderSlice: function (valuenum, highlight) {
- var target = this.target,
- options = this.options,
- radius = this.radius,
- borderWidth = options.get('borderWidth'),
- offset = options.get('offset'),
- circle = 2 * Math.PI,
- values = this.values,
- total = this.total,
- next = offset ? (2*Math.PI)*(offset/360) : 0,
- start, end, i, vlen, color;
-
- vlen = values.length;
- for (i = 0; i < vlen; i++) {
- start = next;
- end = next;
- if (total > 0) { // avoid divide by zero
- end = next + (circle * (values[i] / total));
- }
- if (valuenum === i) {
- color = options.get('sliceColors')[i % options.get('sliceColors').length];
- if (highlight) {
- color = this.calcHighlightColor(color, options);
- }
-
- return target.drawPieSlice(radius, radius, radius - borderWidth, start, end, undefined, color);
- }
- next = end;
- }
- },
-
- render: function () {
- var target = this.target,
- values = this.values,
- options = this.options,
- radius = this.radius,
- borderWidth = options.get('borderWidth'),
- shape, i;
-
- if (!pie._super.render.call(this)) {
- return;
- }
- if (borderWidth) {
- target.drawCircle(radius, radius, Math.floor(radius - (borderWidth / 2)),
- options.get('borderColor'), undefined, borderWidth).append();
- }
- for (i = values.length; i--;) {
- if (values[i]) { // don't render zero values
- shape = this.renderSlice(i).append();
- this.valueShapes[i] = shape.id; // store just the shapeid
- this.shapes[shape.id] = i;
- }
- }
- target.render();
- }
- });
-
- /**
- * Box plots
- */
- $.fn.sparkline.box = box = createClass($.fn.sparkline._base, {
- type: 'box',
-
- init: function (el, values, options, width, height) {
- box._super.init.call(this, el, values, options, width, height);
- this.values = $.map(values, Number);
- this.width = options.get('width') === 'auto' ? '4.0em' : width;
- this.initTarget();
- if (!this.values.length) {
- this.disabled = 1;
- }
- },
-
- /**
- * Simulate a single region
- */
- getRegion: function () {
- return 1;
- },
-
- getCurrentRegionFields: function () {
- var result = [
- { field: 'lq', value: this.quartiles[0] },
- { field: 'med', value: this.quartiles[1] },
- { field: 'uq', value: this.quartiles[2] }
- ];
- if (this.loutlier !== undefined) {
- result.push({ field: 'lo', value: this.loutlier});
- }
- if (this.routlier !== undefined) {
- result.push({ field: 'ro', value: this.routlier});
- }
- if (this.lwhisker !== undefined) {
- result.push({ field: 'lw', value: this.lwhisker});
- }
- if (this.rwhisker !== undefined) {
- result.push({ field: 'rw', value: this.rwhisker});
- }
- return result;
- },
-
- render: function () {
- var target = this.target,
- values = this.values,
- vlen = values.length,
- options = this.options,
- canvasWidth = this.canvasWidth,
- canvasHeight = this.canvasHeight,
- minValue = options.get('chartRangeMin') === undefined ? Math.min.apply(Math, values) : options.get('chartRangeMin'),
- maxValue = options.get('chartRangeMax') === undefined ? Math.max.apply(Math, values) : options.get('chartRangeMax'),
- canvasLeft = 0,
- lwhisker, loutlier, iqr, q1, q2, q3, rwhisker, routlier, i,
- size, unitSize;
-
- if (!box._super.render.call(this)) {
- return;
- }
-
- if (options.get('raw')) {
- if (options.get('showOutliers') && values.length > 5) {
- loutlier = values[0];
- lwhisker = values[1];
- q1 = values[2];
- q2 = values[3];
- q3 = values[4];
- rwhisker = values[5];
- routlier = values[6];
- } else {
- lwhisker = values[0];
- q1 = values[1];
- q2 = values[2];
- q3 = values[3];
- rwhisker = values[4];
- }
- } else {
- values.sort(function (a, b) { return a - b; });
- q1 = quartile(values, 1);
- q2 = quartile(values, 2);
- q3 = quartile(values, 3);
- iqr = q3 - q1;
- if (options.get('showOutliers')) {
- lwhisker = rwhisker = undefined;
- for (i = 0; i < vlen; i++) {
- if (lwhisker === undefined && values[i] > q1 - (iqr * options.get('outlierIQR'))) {
- lwhisker = values[i];
- }
- if (values[i] < q3 + (iqr * options.get('outlierIQR'))) {
- rwhisker = values[i];
- }
- }
- loutlier = values[0];
- routlier = values[vlen - 1];
- } else {
- lwhisker = values[0];
- rwhisker = values[vlen - 1];
- }
- }
- this.quartiles = [q1, q2, q3];
- this.lwhisker = lwhisker;
- this.rwhisker = rwhisker;
- this.loutlier = loutlier;
- this.routlier = routlier;
-
- unitSize = canvasWidth / (maxValue - minValue + 1);
- if (options.get('showOutliers')) {
- canvasLeft = Math.ceil(options.get('spotRadius'));
- canvasWidth -= 2 * Math.ceil(options.get('spotRadius'));
- unitSize = canvasWidth / (maxValue - minValue + 1);
- if (loutlier < lwhisker) {
- target.drawCircle((loutlier - minValue) * unitSize + canvasLeft,
- canvasHeight / 2,
- options.get('spotRadius'),
- options.get('outlierLineColor'),
- options.get('outlierFillColor')).append();
- }
- if (routlier > rwhisker) {
- target.drawCircle((routlier - minValue) * unitSize + canvasLeft,
- canvasHeight / 2,
- options.get('spotRadius'),
- options.get('outlierLineColor'),
- options.get('outlierFillColor')).append();
- }
- }
-
- // box
- target.drawRect(
- Math.round((q1 - minValue) * unitSize + canvasLeft),
- Math.round(canvasHeight * 0.1),
- Math.round((q3 - q1) * unitSize),
- Math.round(canvasHeight * 0.8),
- options.get('boxLineColor'),
- options.get('boxFillColor')).append();
- // left whisker
- target.drawLine(
- Math.round((lwhisker - minValue) * unitSize + canvasLeft),
- Math.round(canvasHeight / 2),
- Math.round((q1 - minValue) * unitSize + canvasLeft),
- Math.round(canvasHeight / 2),
- options.get('lineColor')).append();
- target.drawLine(
- Math.round((lwhisker - minValue) * unitSize + canvasLeft),
- Math.round(canvasHeight / 4),
- Math.round((lwhisker - minValue) * unitSize + canvasLeft),
- Math.round(canvasHeight - canvasHeight / 4),
- options.get('whiskerColor')).append();
- // right whisker
- target.drawLine(Math.round((rwhisker - minValue) * unitSize + canvasLeft),
- Math.round(canvasHeight / 2),
- Math.round((q3 - minValue) * unitSize + canvasLeft),
- Math.round(canvasHeight / 2),
- options.get('lineColor')).append();
- target.drawLine(
- Math.round((rwhisker - minValue) * unitSize + canvasLeft),
- Math.round(canvasHeight / 4),
- Math.round((rwhisker - minValue) * unitSize + canvasLeft),
- Math.round(canvasHeight - canvasHeight / 4),
- options.get('whiskerColor')).append();
- // median line
- target.drawLine(
- Math.round((q2 - minValue) * unitSize + canvasLeft),
- Math.round(canvasHeight * 0.1),
- Math.round((q2 - minValue) * unitSize + canvasLeft),
- Math.round(canvasHeight * 0.9),
- options.get('medianColor')).append();
- if (options.get('target')) {
- size = Math.ceil(options.get('spotRadius'));
- target.drawLine(
- Math.round((options.get('target') - minValue) * unitSize + canvasLeft),
- Math.round((canvasHeight / 2) - size),
- Math.round((options.get('target') - minValue) * unitSize + canvasLeft),
- Math.round((canvasHeight / 2) + size),
- options.get('targetColor')).append();
- target.drawLine(
- Math.round((options.get('target') - minValue) * unitSize + canvasLeft - size),
- Math.round(canvasHeight / 2),
- Math.round((options.get('target') - minValue) * unitSize + canvasLeft + size),
- Math.round(canvasHeight / 2),
- options.get('targetColor')).append();
- }
- target.render();
- }
- });
-
- // Setup a very simple "virtual canvas" to make drawing the few shapes we need easier
- // This is accessible as $(foo).simpledraw()
-
- // Detect browser renderer support
- (function() {
- if (document.namespaces && !document.namespaces.v) {
- $.fn.sparkline.hasVML = true;
- document.namespaces.add('v', 'urn:schemas-microsoft-com:vml', '#default#VML');
- } else {
- $.fn.sparkline.hasVML = false;
- }
-
- var el = document.createElement('canvas');
- $.fn.sparkline.hasCanvas = !!(el.getContext && el.getContext('2d'));
-
- })()
-
- VShape = createClass({
- init: function (target, id, type, args) {
- this.target = target;
- this.id = id;
- this.type = type;
- this.args = args;
- },
- append: function () {
- this.target.appendShape(this);
- return this;
- }
- });
-
- VCanvas_base = createClass({
- _pxregex: /(\d+)(px)?\s*$/i,
-
- init: function (width, height, target) {
- if (!width) {
- return;
- }
- this.width = width;
- this.height = height;
- this.target = target;
- this.lastShapeId = null;
- if (target[0]) {
- target = target[0];
- }
- $.data(target, '_jqs_vcanvas', this);
- },
-
- drawLine: function (x1, y1, x2, y2, lineColor, lineWidth) {
- return this.drawShape([[x1, y1], [x2, y2]], lineColor, lineWidth);
- },
-
- drawShape: function (path, lineColor, fillColor, lineWidth) {
- return this._genShape('Shape', [path, lineColor, fillColor, lineWidth]);
- },
-
- drawCircle: function (x, y, radius, lineColor, fillColor, lineWidth) {
- return this._genShape('Circle', [x, y, radius, lineColor, fillColor, lineWidth]);
- },
-
- drawPieSlice: function (x, y, radius, startAngle, endAngle, lineColor, fillColor) {
- return this._genShape('PieSlice', [x, y, radius, startAngle, endAngle, lineColor, fillColor]);
- },
-
- drawRect: function (x, y, width, height, lineColor, fillColor) {
- return this._genShape('Rect', [x, y, width, height, lineColor, fillColor]);
- },
-
- getElement: function () {
- return this.canvas;
- },
-
- /**
- * Return the most recently inserted shape id
- */
- getLastShapeId: function () {
- return this.lastShapeId;
- },
-
- /**
- * Clear and reset the canvas
- */
- reset: function () {
- alert('reset not implemented');
- },
-
- _insert: function (el, target) {
- $(target).html(el);
- },
-
- /**
- * Calculate the pixel dimensions of the canvas
- */
- _calculatePixelDims: function (width, height, canvas) {
- // XXX This should probably be a configurable option
- var match;
- match = this._pxregex.exec(height);
- if (match) {
- this.pixelHeight = match[1];
- } else {
- this.pixelHeight = $(canvas).height();
- }
- match = this._pxregex.exec(width);
- if (match) {
- this.pixelWidth = match[1];
- } else {
- this.pixelWidth = $(canvas).width();
- }
- },
-
- /**
- * Generate a shape object and id for later rendering
- */
- _genShape: function (shapetype, shapeargs) {
- var id = shapeCount++;
- shapeargs.unshift(id);
- return new VShape(this, id, shapetype, shapeargs);
- },
-
- /**
- * Add a shape to the end of the render queue
- */
- appendShape: function (shape) {
- alert('appendShape not implemented');
- },
-
- /**
- * Replace one shape with another
- */
- replaceWithShape: function (shapeid, shape) {
- alert('replaceWithShape not implemented');
- },
-
- /**
- * Insert one shape after another in the render queue
- */
- insertAfterShape: function (shapeid, shape) {
- alert('insertAfterShape not implemented');
- },
-
- /**
- * Remove a shape from the queue
- */
- removeShapeId: function (shapeid) {
- alert('removeShapeId not implemented');
- },
-
- /**
- * Find a shape at the specified x/y co-ordinates
- */
- getShapeAt: function (el, x, y) {
- alert('getShapeAt not implemented');
- },
-
- /**
- * Render all queued shapes onto the canvas
- */
- render: function () {
- alert('render not implemented');
- }
- });
-
- VCanvas_canvas = createClass(VCanvas_base, {
- init: function (width, height, target, interact) {
- VCanvas_canvas._super.init.call(this, width, height, target);
- this.canvas = document.createElement('canvas');
- if (target[0]) {
- target = target[0];
- }
- $.data(target, '_jqs_vcanvas', this);
- $(this.canvas).css({ display: 'inline-block', width: width, height: height, verticalAlign: 'top' });
- this._insert(this.canvas, target);
- this._calculatePixelDims(width, height, this.canvas);
- this.canvas.width = this.pixelWidth;
- this.canvas.height = this.pixelHeight;
- this.interact = interact;
- this.shapes = {};
- this.shapeseq = [];
- this.currentTargetShapeId = undefined;
- $(this.canvas).css({width: this.pixelWidth, height: this.pixelHeight});
- },
-
- _getContext: function (lineColor, fillColor, lineWidth) {
- var context = this.canvas.getContext('2d');
- if (lineColor !== undefined) {
- context.strokeStyle = lineColor;
- }
- context.lineWidth = lineWidth === undefined ? 1 : lineWidth;
- if (fillColor !== undefined) {
- context.fillStyle = fillColor;
- }
- return context;
- },
-
- reset: function () {
- var context = this._getContext();
- context.clearRect(0, 0, this.pixelWidth, this.pixelHeight);
- this.shapes = {};
- this.shapeseq = [];
- this.currentTargetShapeId = undefined;
- },
-
- _drawShape: function (shapeid, path, lineColor, fillColor, lineWidth) {
- var context = this._getContext(lineColor, fillColor, lineWidth),
- i, plen;
- context.beginPath();
- context.moveTo(path[0][0] + 0.5, path[0][1] + 0.5);
- for (i = 1, plen = path.length; i < plen; i++) {
- context.lineTo(path[i][0] + 0.5, path[i][1] + 0.5); // the 0.5 offset gives us crisp pixel-width lines
- }
- if (lineColor !== undefined) {
- context.stroke();
- }
- if (fillColor !== undefined) {
- context.fill();
- }
- if (this.targetX !== undefined && this.targetY !== undefined &&
- context.isPointInPath(this.targetX, this.targetY)) {
- this.currentTargetShapeId = shapeid;
- }
- },
-
- _drawCircle: function (shapeid, x, y, radius, lineColor, fillColor, lineWidth) {
- var context = this._getContext(lineColor, fillColor, lineWidth);
- context.beginPath();
- context.arc(x, y, radius, 0, 2 * Math.PI, false);
- if (this.targetX !== undefined && this.targetY !== undefined &&
- context.isPointInPath(this.targetX, this.targetY)) {
- this.currentTargetShapeId = shapeid;
- }
- if (lineColor !== undefined) {
- context.stroke();
- }
- if (fillColor !== undefined) {
- context.fill();
- }
- },
-
- _drawPieSlice: function (shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) {
- var context = this._getContext(lineColor, fillColor);
- context.beginPath();
- context.moveTo(x, y);
- context.arc(x, y, radius, startAngle, endAngle, false);
- context.lineTo(x, y);
- context.closePath();
- if (lineColor !== undefined) {
- context.stroke();
- }
- if (fillColor) {
- context.fill();
- }
- if (this.targetX !== undefined && this.targetY !== undefined &&
- context.isPointInPath(this.targetX, this.targetY)) {
- this.currentTargetShapeId = shapeid;
- }
- },
-
- _drawRect: function (shapeid, x, y, width, height, lineColor, fillColor) {
- return this._drawShape(shapeid, [[x, y], [x + width, y], [x + width, y + height], [x, y + height], [x, y]], lineColor, fillColor);
- },
-
- appendShape: function (shape) {
- this.shapes[shape.id] = shape;
- this.shapeseq.push(shape.id);
- this.lastShapeId = shape.id;
- return shape.id;
- },
-
- replaceWithShape: function (shapeid, shape) {
- var shapeseq = this.shapeseq,
- i;
- this.shapes[shape.id] = shape;
- for (i = shapeseq.length; i--;) {
- if (shapeseq[i] == shapeid) {
- shapeseq[i] = shape.id;
- }
- }
- delete this.shapes[shapeid];
- },
-
- replaceWithShapes: function (shapeids, shapes) {
- var shapeseq = this.shapeseq,
- shapemap = {},
- sid, i, first;
-
- for (i = shapeids.length; i--;) {
- shapemap[shapeids[i]] = true;
- }
- for (i = shapeseq.length; i--;) {
- sid = shapeseq[i];
- if (shapemap[sid]) {
- shapeseq.splice(i, 1);
- delete this.shapes[sid];
- first = i;
- }
- }
- for (i = shapes.length; i--;) {
- shapeseq.splice(first, 0, shapes[i].id);
- this.shapes[shapes[i].id] = shapes[i];
- }
-
- },
-
- insertAfterShape: function (shapeid, shape) {
- var shapeseq = this.shapeseq,
- i;
- for (i = shapeseq.length; i--;) {
- if (shapeseq[i] === shapeid) {
- shapeseq.splice(i + 1, 0, shape.id);
- this.shapes[shape.id] = shape;
- return;
- }
- }
- },
-
- removeShapeId: function (shapeid) {
- var shapeseq = this.shapeseq,
- i;
- for (i = shapeseq.length; i--;) {
- if (shapeseq[i] === shapeid) {
- shapeseq.splice(i, 1);
- break;
- }
- }
- delete this.shapes[shapeid];
- },
-
- getShapeAt: function (el, x, y) {
- this.targetX = x;
- this.targetY = y;
- this.render();
- return this.currentTargetShapeId;
- },
-
- render: function () {
- var shapeseq = this.shapeseq,
- shapes = this.shapes,
- shapeCount = shapeseq.length,
- context = this._getContext(),
- shapeid, shape, i;
- context.clearRect(0, 0, this.pixelWidth, this.pixelHeight);
- for (i = 0; i < shapeCount; i++) {
- shapeid = shapeseq[i];
- shape = shapes[shapeid];
- this['_draw' + shape.type].apply(this, shape.args);
- }
- if (!this.interact) {
- // not interactive so no need to keep the shapes array
- this.shapes = {};
- this.shapeseq = [];
- }
- }
-
- });
-
- VCanvas_vml = createClass(VCanvas_base, {
- init: function (width, height, target) {
- var groupel;
- VCanvas_vml._super.init.call(this, width, height, target);
- if (target[0]) {
- target = target[0];
- }
- $.data(target, '_jqs_vcanvas', this);
- this.canvas = document.createElement('span');
- $(this.canvas).css({ display: 'inline-block', position: 'relative', overflow: 'hidden', width: width, height: height, margin: '0px', padding: '0px', verticalAlign: 'top'});
- this._insert(this.canvas, target);
- this._calculatePixelDims(width, height, this.canvas);
- this.canvas.width = this.pixelWidth;
- this.canvas.height = this.pixelHeight;
- groupel = '';
- this.canvas.insertAdjacentHTML('beforeEnd', groupel);
- this.group = $(this.canvas).children()[0];
- this.rendered = false;
- this.prerender = '';
- },
-
- _drawShape: function (shapeid, path, lineColor, fillColor, lineWidth) {
- var vpath = [],
- initial, stroke, fill, closed, vel, plen, i;
- for (i = 0, plen = path.length; i < plen; i++) {
- vpath[i] = '' + (path[i][0]) + ',' + (path[i][1]);
- }
- initial = vpath.splice(0, 1);
- lineWidth = lineWidth === undefined ? 1 : lineWidth;
- stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" ';
- fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
- closed = vpath[0] === vpath[vpath.length - 1] ? 'x ' : '';
- vel = '' +
- ' ';
- return vel;
- },
-
- _drawCircle: function (shapeid, x, y, radius, lineColor, fillColor, lineWidth) {
- var stroke, fill, vel;
- x -= radius;
- y -= radius;
- stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" ';
- fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
- vel = '';
- return vel;
-
- },
-
- _drawPieSlice: function (shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) {
- var vpath, startx, starty, endx, endy, stroke, fill, vel;
- if (startAngle === endAngle) {
- return ''; // VML seems to have problem when start angle equals end angle.
- }
- if ((endAngle - startAngle) === (2 * Math.PI)) {
- startAngle = 0.0; // VML seems to have a problem when drawing a full circle that doesn't start 0
- endAngle = (2 * Math.PI);
- }
-
- startx = x + Math.round(Math.cos(startAngle) * radius);
- starty = y + Math.round(Math.sin(startAngle) * radius);
- endx = x + Math.round(Math.cos(endAngle) * radius);
- endy = y + Math.round(Math.sin(endAngle) * radius);
-
- if (startx === endx && starty === endy) {
- if ((endAngle - startAngle) < Math.PI) {
- // Prevent very small slices from being mistaken as a whole pie
- return '';
- }
- // essentially going to be the entire circle, so ignore startAngle
- startx = endx = x + radius;
- starty = endy = y;
- }
-
- if (startx === endx && starty === endy && (endAngle - startAngle) < Math.PI) {
- return '';
- }
-
- vpath = [x - radius, y - radius, x + radius, y + radius, startx, starty, endx, endy];
- stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="1px" strokeColor="' + lineColor + '" ';
- fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
- vel = '' +
- ' ';
- return vel;
- },
-
- _drawRect: function (shapeid, x, y, width, height, lineColor, fillColor) {
- return this._drawShape(shapeid, [[x, y], [x, y + height], [x + width, y + height], [x + width, y], [x, y]], lineColor, fillColor);
- },
-
- reset: function () {
- this.group.innerHTML = '';
- },
-
- appendShape: function (shape) {
- var vel = this['_draw' + shape.type].apply(this, shape.args);
- if (this.rendered) {
- this.group.insertAdjacentHTML('beforeEnd', vel);
- } else {
- this.prerender += vel;
- }
- this.lastShapeId = shape.id;
- return shape.id;
- },
-
- replaceWithShape: function (shapeid, shape) {
- var existing = $('#jqsshape' + shapeid),
- vel = this['_draw' + shape.type].apply(this, shape.args);
- existing[0].outerHTML = vel;
- },
-
- replaceWithShapes: function (shapeids, shapes) {
- // replace the first shapeid with all the new shapes then toast the remaining old shapes
- var existing = $('#jqsshape' + shapeids[0]),
- replace = '',
- slen = shapes.length,
- i;
- for (i = 0; i < slen; i++) {
- replace += this['_draw' + shapes[i].type].apply(this, shapes[i].args);
- }
- existing[0].outerHTML = replace;
- for (i = 1; i < shapeids.length; i++) {
- $('#jqsshape' + shapeids[i]).remove();
- }
- },
-
- insertAfterShape: function (shapeid, shape) {
- var existing = $('#jqsshape' + shapeid),
- vel = this['_draw' + shape.type].apply(this, shape.args);
- existing[0].insertAdjacentHTML('afterEnd', vel);
- },
-
- removeShapeId: function (shapeid) {
- var existing = $('#jqsshape' + shapeid);
- this.group.removeChild(existing[0]);
- },
-
- getShapeAt: function (el, x, y) {
- var shapeid = el.id.substr(8);
- return shapeid;
- },
-
- render: function () {
- if (!this.rendered) {
- // batch the intial render into a single repaint
- this.group.innerHTML = this.prerender;
- this.rendered = true;
- }
- }
- });
-
-}));
diff --git a/addons/crm/static/src/js/crm_case_section.js b/addons/crm/static/src/js/crm_case_section.js
index 2b4ca741d75..e027313d6dc 100644
--- a/addons/crm/static/src/js/crm_case_section.js
+++ b/addons/crm/static/src/js/crm_case_section.js
@@ -8,27 +8,4 @@ openerp.crm = function(openerp) {
}
},
});
-
- openerp.crm.SparklineBarWidget = openerp.web_kanban.AbstractField.extend({
- className: "oe_sparkline_bar",
- start: function() {
- var self = this;
- var title = this.$node.html();
- setTimeout(function () {
- var value = _.pluck(self.field.value, 'value');
- var tooltips = _.pluck(self.field.value, 'tooltip');
- self.$el.sparkline(value, {
- type: 'bar',
- barWidth: 5,
- tooltipFormat: '{{offset:offset}} {{value}}',
- tooltipValueLookups: {
- 'offset': tooltips
- },
- });
- self.$el.tipsy({'delayIn': 0, 'html': true, 'title': function(){return title}, 'gravity': 'n'});
- }, 0);
- },
- });
- openerp.web_kanban.fields_registry.add("sparkline_bar", "openerp.crm.SparklineBarWidget");
-
};
diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py
index e5cb2b56eb3..73fb148c0ed 100644
--- a/addons/mail/mail_mail.py
+++ b/addons/mail/mail_mail.py
@@ -63,12 +63,16 @@ class mail_mail(osv.Model):
'notification': fields.boolean('Is Notification',
help='Mail has been created to notify people of an existing mail.message'),
# Bounce and tracking
- 'opened': fields.integer(
+ 'opened': fields.datetime(
'Opened',
- help='Number of times this email has been seen, using the OpenERP tracking.'),
- 'replied': fields.integer(
- 'Reply Received',
- help='If checked, a reply to this email has been received.'),
+ help='Date when this email has been opened for the first time.'),
+ 'replied': fields.datetime(
+ 'Replied',
+ help='Date when this email has been replied for the first time.'),
+ 'bounced': fields.datetime(
+ 'Bounced',
+ help='Date when this email has bounced.'
+ ),
}
_defaults = {
@@ -103,15 +107,24 @@ class mail_mail(osv.Model):
return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
def set_opened(self, cr, uid, ids, context=None):
- """ Increment opened counter """
+ """ Set as opened """
for mail in self.browse(cr, uid, ids, context=context):
- self.write(cr, uid, [mail.id], {'opened': (mail.opened + 1)}, context=context)
+ if not mail.opened:
+ self.write(cr, uid, [mail.id], {'opened': fields.datetime.now()}, context=context)
return True
def set_replied(self, cr, uid, ids, context=None):
- """ Increment replied counter """
+ """ Set as replied """
for mail in self.browse(cr, uid, ids, context=context):
- self.write(cr, uid, [mail.id], {'replied': (mail.replied + 1)}, context=context)
+ if not mail.replied:
+ self.write(cr, uid, [mail.id], {'replied': fields.datetime.now()}, context=context)
+ return True
+
+ def set_bounced(self, cr, uid, ids, context=None):
+ """ Set as bounced """
+ for mail in self.browse(cr, uid, ids, context=context):
+ if not mail.bounced:
+ self.write(cr, uid, [mail.id], {'bounced': fields.datetime.now()}, context=context)
return True
def process_email_queue(self, cr, uid, ids=None, context=None):
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
index 9943114a07a..9a5d3e3580d 100644
--- a/addons/mail/mail_thread.py
+++ b/addons/mail/mail_thread.py
@@ -1426,8 +1426,7 @@ class mail_thread(osv.AbstractModel):
# update original mail_mail if exists
if type == 'email':
mail_mail_ids = self.pool['mail.mail'].search(cr, SUPERUSER_ID, [('mail_message_id', '=', parent_id)], context=context)
- for mail in self.pool['mail.mail'].browse(cr, SUPERUSER_ID, mail_mail_ids, context=context):
- self.pool['mail.mail'].write(cr, SUPERUSER_ID, [mail.id], {'replied': mail.replied + 1}, context=context)
+ self.pool['mail.mail'].set_replied(cr, SUPERUSER_ID, mail_mail_ids, context=context)
message_ids = mail_message.search(cr, SUPERUSER_ID, [('id', '=', parent_id), ('parent_id', '!=', False)], context=context)
# avoid loops when finding ancestors
diff --git a/addons/mail/tests/__init__.py b/addons/mail/tests/__init__.py
index 242beb60bf1..ff075d73d73 100644
--- a/addons/mail/tests/__init__.py
+++ b/addons/mail/tests/__init__.py
@@ -19,9 +19,10 @@
#
##############################################################################
-from . import test_mail_group, test_mail_message, test_mail_features, test_mail_gateway, test_message_read, test_invite
+from . import test_mail_mail, test_mail_group, test_mail_message, test_mail_features, test_mail_gateway, test_message_read, test_invite
checks = [
+ # test_mail_mail,
test_mail_group,
test_mail_message,
test_mail_features,
diff --git a/addons/mass_mailing/__openerp__.py b/addons/mass_mailing/__openerp__.py
index e6fbb42e886..9bb5b746f63 100644
--- a/addons/mass_mailing/__openerp__.py
+++ b/addons/mass_mailing/__openerp__.py
@@ -29,10 +29,16 @@
'description': """TODO""",
'data': [
'mass_mailing_view.xml',
+ 'mass_mailing_demo.xml',
'mail_mail_view.xml',
'wizard/mail_compose_message_view.xml',
'security/ir.model.access.csv',
],
+ 'js': [],
+ 'qweb': [],
+ 'css': [
+ 'static/src/css/mass_mailing.css'
+ ],
'demo': [],
'installable': True,
'auto_install': False,
diff --git a/addons/mass_mailing/mail_mail.py b/addons/mass_mailing/mail_mail.py
index d7f46446b56..529c6c73e7b 100644
--- a/addons/mass_mailing/mail_mail.py
+++ b/addons/mass_mailing/mail_mail.py
@@ -28,8 +28,14 @@ class MailMail(osv.Model):
_inherit = ['mail.mail']
_columns = {
- 'mass_mailing_campaign_id': fields.many2one(
- 'mail.mass_mailing.campaign', 'Mass Mailing Campaign',
+ 'mass_mailing_segment_id': fields.many2one(
+ 'mail.mass_mailing.segment', 'Mass Mailing Segment',
ondelete='set null',
),
+ 'mass_mailing_campaign_id': fields.related(
+ 'mass_mailing_segment_id', 'mass_mailing_campaign_id',
+ type='many2one', ondelete='set null',
+ relation='mail.mass_mailing.campaign',
+ store=True, readonly=True,
+ ),
}
diff --git a/addons/mass_mailing/mail_mail_view.xml b/addons/mass_mailing/mail_mail_view.xml
index dd66aede66f..957ee0f6f93 100644
--- a/addons/mass_mailing/mail_mail_view.xml
+++ b/addons/mass_mailing/mail_mail_view.xml
@@ -10,6 +10,7 @@
+
diff --git a/addons/mass_mailing/mass_mailing.py b/addons/mass_mailing/mass_mailing.py
index 91392759b0e..f37f86e385a 100644
--- a/addons/mass_mailing/mass_mailing.py
+++ b/addons/mass_mailing/mass_mailing.py
@@ -19,6 +19,10 @@
#
##############################################################################
+from datetime import date, datetime
+from dateutil import relativedelta
+
+from openerp import tools
from openerp.osv import osv, fields
@@ -32,53 +36,175 @@ class MassMailingCampaign(osv.Model):
""" Compute statistics of the mass mailing campaign """
results = dict.fromkeys(ids, False)
for campaign in self.browse(cr, uid, ids, context=context):
- if not campaign.mail_ids:
- results[campaign.id] = {
- 'sent': 0,
- 'opened_ratio': 0.0,
- 'replied_ratio': 0.0,
- 'bounce_ratio': 0.0,
- }
- continue
results[campaign.id] = {
'sent': len(campaign.mail_ids),
- 'opened_ratio': len([mail for mail in campaign.mail_ids if mail.opened]) * 1.0 / len(campaign.mail_ids),
- 'replied_ratio': len([mail for mail in campaign.mail_ids if mail.replied]) * 1.0 / len(campaign.mail_ids),
- 'bounce_ratio': 0.0,
+ 'opened': len([mail for mail in campaign.mail_ids if mail.opened]),
+ 'replied': len([mail for mail in campaign.mail_ids if mail.replied]),
+ 'bounced': len([mail for mail in campaign.mail_ids if mail.bounced]),
}
return results
+ def _get_segment_kanban_ids(self, cr, uid, ids, name, arg, context=None):
+ results = dict.fromkeys(ids, '')
+ for campaign in self.browse(cr, uid, ids, context=context):
+ segment_results = []
+ for segment in campaign.segment_ids:
+ segment_object = {}
+ for attr in ['name', 'sent', 'opened', 'replied', 'bounced']:
+ segment_object[attr] = getattr(segment, attr)
+ segment_results.append(segment_object)
+ results[campaign.id] = segment_results
+ return results
+
_columns = {
'name': fields.char(
'Campaign Name', required=True,
),
+ 'segment_ids': fields.one2many(
+ 'mail.mass_mailing.segment', 'mass_mailing_campaign_id',
+ 'Segments',
+ ),
+ 'segment_kanban_ids': fields.function(
+ _get_segment_kanban_ids,
+ type='text', string='Segments (kanban data)',
+ help='This field has for purpose to gather data about segment to display them in kanban view as nested kanban views is not possible currently',
+ ),
+ 'mail_ids': fields.one2many(
+ 'mail.mail', 'mass_mailing_campaign_id',
+ 'Sent Emails',
+ ),
+ # stat fields
+ 'sent': fields.function(
+ _get_statistics,
+ string='Sent Emails',
+ type='integer', multi='_get_statistics'
+ ),
+ 'opened': fields.function(
+ _get_statistics,
+ string='Opened',
+ type='integer', multi='_get_statistics',
+ ),
+ 'replied': fields.function(
+ _get_statistics,
+ string='Replied',
+ type='integer', multi='_get_statistics'
+ ),
+ 'bounced': fields.function(
+ _get_statistics,
+ string='Bounced',
+ type='integer', multi='_get_statistics'
+ ),
+ }
+
+
+class MassMailingSegment(osv.Model):
+ """ TODO """
+ _name = 'mail.mass_mailing.segment'
+ _description = 'Segment of a mass mailing campaign'
+ # number of periods for tracking mail_mail statistics
+ _period_number = 6
+
+ def __get_bar_values(self, cr, uid, obj, domain, read_fields, value_field, groupby_field, context=None):
+ """ Generic method to generate data for bar chart values using SparklineBarWidget.
+ This method performs obj.read_group(cr, uid, domain, read_fields, groupby_field).
+
+ :param obj: the target model (i.e. crm_lead)
+ :param domain: the domain applied to the read_group
+ :param list read_fields: the list of fields to read in the read_group
+ :param str value_field: the field used to compute the value of the bar slice
+ :param str groupby_field: the fields used to group
+
+ :return list section_result: a list of dicts: [
+ { 'value': (int) bar_column_value,
+ 'tootip': (str) bar_column_tooltip,
+ }
+ ]
+ """
+ month_begin = date.today().replace(day=1)
+ section_result = [{'value': 0,
+ 'tooltip': (month_begin + relativedelta.relativedelta(months=-i)).strftime('%B'),
+ } for i in range(self._period_number - 1, -1, -1)]
+ group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context)
+ print group_obj
+ for group in group_obj:
+ group_begin_date = datetime.strptime(group['__domain'][0][2], tools.DEFAULT_SERVER_DATE_FORMAT)
+ month_delta = relativedelta.relativedelta(month_begin, group_begin_date)
+ section_result[self._period_number - (month_delta.months + 1)] = {'value': group.get(value_field, 0), 'tooltip': group_begin_date.strftime('%B')}
+ return section_result
+
+ def _get_monthly_statistics(self, cr, uid, ids, field_name, arg, context=None):
+ """ TODO
+ """
+ obj = self.pool.get('mail.mail')
+ res = dict.fromkeys(ids, False)
+ month_begin = date.today().replace(day=1)
+ groupby_begin = (month_begin + relativedelta.relativedelta(months=-4)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
+ for id in ids:
+ res[id] = dict()
+ domain = [('mass_mailing_segment_id', '=', id), ('opened', '>=', groupby_begin)]
+ res[id]['opened_monthly'] = self.__get_bar_values(cr, uid, obj, domain, ['opened'], 'opened_count', 'opened', context=context)
+ domain = [('mass_mailing_segment_id', '=', id), ('replied', '>=', groupby_begin)]
+ res[id]['replied_monthly'] = self.__get_bar_values(cr, uid, obj, domain, ['replied'], 'replied_count', 'replied', context=context)
+ return res
+
+ def _get_statistics(self, cr, uid, ids, name, arg, context=None):
+ """ Compute statistics of the mass mailing campaign """
+ results = dict.fromkeys(ids, False)
+ for segment in self.browse(cr, uid, ids, context=context):
+ results[segment.id] = {
+ 'sent': len(segment.mail_ids),
+ 'opened': len([mail for mail in segment.mail_ids if mail.opened]),
+ 'replied': len([mail for mail in segment.mail_ids if mail.replied]),
+ 'bounced': len([mail for mail in segment.mail_ids if mail.bounced]),
+ }
+ return results
+
+ _columns = {
+ 'name': fields.char('Name', required=True),
+ 'mass_mailing_campaign_id': fields.many2one(
+ 'mail.mass_mailing.campaign', 'Mass Mailing Campaign',
+ ondelete='cascade',
+ ),
'template_id': fields.many2one(
'email.template', 'Email Template',
ondelete='set null',
),
+ 'domain': fields.char('Domain'),
+ 'date': fields.datetime('Date'),
+ # mail_mail data
'mail_ids': fields.one2many(
- 'mail.mail', 'mass_mailing_campaign_id',
+ 'mail.mail', 'mass_mailing_segment_id',
'Send Emails',
),
- # stat fields
'sent': fields.function(
_get_statistics,
string='Sent Emails',
type='integer', multi='_get_statistics'
),
- 'opened_ratio': fields.function(
+ 'opened': fields.function(
_get_statistics,
- string='Opened Ratio',
- type='float', multi='_get_statistics',
+ string='Opened',
+ type='integer', multi='_get_statistics',
),
- 'replied_ratio': fields.function(
+ 'replied': fields.function(
_get_statistics,
- string='Replied Ratio',
- type='float', multi='_get_statistics'
+ string='Replied',
+ type='integer', multi='_get_statistics'
),
- 'bounce_ratio': fields.function(
+ 'bounced': fields.function(
_get_statistics,
- string='Bounce Ratio',
- type='float', multi='_get_statistics'
+ string='Bounce',
+ type='integer', multi='_get_statistics'
+ ),
+ # monthly ratio
+ 'opened_monthly': fields.function(
+ _get_monthly_statistics,
+ string='Sent Emails',
+ type='char', multi='_get_monthly_statistics',
+ ),
+ 'replied_monthly': fields.function(
+ _get_monthly_statistics,
+ string='Replied',
+ type='char', multi='_get_monthly_statistics',
),
}
diff --git a/addons/mass_mailing/mass_mailing_demo.xml b/addons/mass_mailing/mass_mailing_demo.xml
new file mode 100644
index 00000000000..3e04baf7cef
--- /dev/null
+++ b/addons/mass_mailing/mass_mailing_demo.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+ Partner Newsletter 1
+
+
+ ${object.id}
+ Hello]]>
+
+
+ Partner Newsletter 2
+
+
+ ${object.id}
+ Hello]]>
+
+
+
+ Partners Newsletter
+
+
+
+ First Newsletter
+
+
+
+
+
+ Second Newsletter
+
+
+
+
+
+
+
+
+
+ sent
+
+
+
+
+
+ sent
+
+
+
+
+ sent
+
+
+
+ sent
+
+
+
+
+ sent
+
+
+
+
+
+ sent
+
+
+
+
+ sent
+
+
+
+ sent
+
+
+
+
diff --git a/addons/mass_mailing/mass_mailing_view.xml b/addons/mass_mailing/mass_mailing_view.xml
index bc28827343d..b847a89db53 100644
--- a/addons/mass_mailing/mass_mailing_view.xml
+++ b/addons/mass_mailing/mass_mailing_view.xml
@@ -3,7 +3,7 @@
-
+ mail.mass_mailing.campaign.treemail.mass_mailing.campaign10
@@ -14,7 +14,7 @@
-
+ mail.mass_mailing.campaign.formmail.mass_mailing.campaign
@@ -22,27 +22,188 @@
-
-
-
-
+
+
+
+
+
+
+ mail.mass_mailing.campaign.kanban
+ mail.mass_mailing.campaign
+
+
+
+
+
+
+
--
cgit v1.2.3
From 68f9b63c73823514602429df9c6a1d7b5851d725 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Tue, 3 Sep 2013 17:25:53 +0200
Subject: [IMP] mass_mailing: added fields on segment form view + rules for
segment model
bzr revid: tde@openerp.com-20130903152553-y0s4jhqagu0ff92m
---
addons/mass_mailing/mass_mailing_view.xml | 13 +++++++++----
addons/mass_mailing/security/ir.model.access.csv | 4 +++-
2 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/addons/mass_mailing/mass_mailing_view.xml b/addons/mass_mailing/mass_mailing_view.xml
index c85bb136bd3..ab421dda881 100644
--- a/addons/mass_mailing/mass_mailing_view.xml
+++ b/addons/mass_mailing/mass_mailing_view.xml
@@ -116,10 +116,15 @@
diff --git a/addons/mass_mailing/security/ir.model.access.csv b/addons/mass_mailing/security/ir.model.access.csv
index af6eaa7bb06..54f3413e9f1 100644
--- a/addons/mass_mailing/security/ir.model.access.csv
+++ b/addons/mass_mailing/security/ir.model.access.csv
@@ -1,3 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_mass_mailing_campaign,mail.mass_mailing.campaign.template,model_mail_mass_mailing_campaign,,1,1,1,0
+access_mass_mailing_campaign,mail.mass_mailing.campaign,model_mail_mass_mailing_campaign,,1,1,1,0
access_mass_mailing_campaign_system,mail.mass_mailing.campaign.system,model_mail_mass_mailing_campaign,base.group_system,1,1,1,1
+access_mass_mailing_segment,mail.mass_mailing.segment,model_mail_mass_mailing_segment,,1,1,1,0
+access_mass_mailing_segment_system,mail.mass_mailing.segment.system,model_mail_mass_mailing_segment,base.group_system,1,1,1,1
\ No newline at end of file
--
cgit v1.2.3
From a937b5679ba6b829f769cc4e119de7a22009a8e2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Fri, 6 Sep 2013 11:32:21 +0200
Subject: [IMP] [FIX] kanban: updated justgage and sparklin libraries; fixed
gauge widget showing 0 as value by forcing a refresh
bzr revid: tde@openerp.com-20130906093221-n94whqrrj2meknwf
---
addons/web/__openerp__.py | 2 +-
.../lib/jquery.sparkline/jquery.sparkline.js | 61 +-
addons/web/static/lib/justgage/justgage.1.0.1.js | 946 ---------------------
addons/web/static/lib/justgage/justgage.js | 946 +++++++++++++++++++++
addons/web_kanban/static/src/js/kanban.js | 25 +-
5 files changed, 999 insertions(+), 981 deletions(-)
delete mode 100644 addons/web/static/lib/justgage/justgage.1.0.1.js
create mode 100644 addons/web/static/lib/justgage/justgage.js
diff --git a/addons/web/__openerp__.py b/addons/web/__openerp__.py
index 32a108c131d..9b13fd48b0a 100644
--- a/addons/web/__openerp__.py
+++ b/addons/web/__openerp__.py
@@ -36,7 +36,7 @@ This module provides the core of the OpenERP Web Client.
"static/lib/jquery.tipsy/jquery.tipsy.js",
"static/lib/jquery.textext/jquery.textext.js",
"static/lib/jquery.timeago/jquery.timeago.js",
- "static/lib/justgage/justgage.1.0.1.js",
+ "static/lib/justgage/justgage.js",
"static/lib/qweb/qweb2.js",
"static/lib/underscore/underscore.js",
"static/lib/underscore.string/lib/underscore.string.js",
diff --git a/addons/web/static/lib/jquery.sparkline/jquery.sparkline.js b/addons/web/static/lib/jquery.sparkline/jquery.sparkline.js
index 8069db45fe9..43b24c080a6 100644
--- a/addons/web/static/lib/jquery.sparkline/jquery.sparkline.js
+++ b/addons/web/static/lib/jquery.sparkline/jquery.sparkline.js
@@ -2,7 +2,7 @@
*
* jquery.sparkline.js
*
-* v2.1.1
+* v2.1.2
* (c) Splunk, Inc
* Contact: Gareth Watts (gareth@splunk.com)
* http://omnipotent.net/jquery.sparkline/
@@ -202,11 +202,11 @@
/*jslint regexp: true, browser: true, jquery: true, white: true, nomen: false, plusplus: false, maxerr: 500, indent: 4 */
+(function(document, Math, undefined) { // performance/minified-size optimization
(function(factory) {
if(typeof define === 'function' && define.amd) {
define(['jquery'], factory);
- }
- else {
+ } else if (jQuery && !jQuery.fn.sparkline) {
factory(jQuery);
}
}
@@ -597,19 +597,41 @@
if (useExisting && (target = this.data('_jqs_vcanvas'))) {
return target;
}
+
+ if ($.fn.sparkline.canvas === false) {
+ // We've already determined that neither Canvas nor VML are available
+ return false;
+
+ } else if ($.fn.sparkline.canvas === undefined) {
+ // No function defined yet -- need to see if we support Canvas or VML
+ var el = document.createElement('canvas');
+ if (!!(el.getContext && el.getContext('2d'))) {
+ // Canvas is available
+ $.fn.sparkline.canvas = function(width, height, target, interact) {
+ return new VCanvas_canvas(width, height, target, interact);
+ };
+ } else if (document.namespaces && !document.namespaces.v) {
+ // VML is available
+ document.namespaces.add('v', 'urn:schemas-microsoft-com:vml', '#default#VML');
+ $.fn.sparkline.canvas = function(width, height, target, interact) {
+ return new VCanvas_vml(width, height, target);
+ };
+ } else {
+ // Neither Canvas nor VML are available
+ $.fn.sparkline.canvas = false;
+ return false;
+ }
+ }
+
if (width === undefined) {
width = $(this).innerWidth();
}
if (height === undefined) {
height = $(this).innerHeight();
}
- if ($.fn.sparkline.hasCanvas) {
- target = new VCanvas_canvas(width, height, this, interact);
- } else if ($.fn.sparkline.hasVML) {
- target = new VCanvas_vml(width, height, this);
- } else {
- return false;
- }
+
+ target = $.fn.sparkline.canvas(width, height, this, interact);
+
mhandler = $(this).data('_jqs_mhandler');
if (mhandler) {
mhandler.registerCanvas(target);
@@ -977,8 +999,7 @@
mhandler.registerSparkline(sp);
}
};
- // jQuery 1.3.0 completely changed the meaning of :hidden :-/
- if (($(this).html() && !options.get('disableHiddenCheck') && $(this).is(':hidden')) || ($.fn.jquery < '1.3.0' && $(this).parents().is(':hidden')) || !$(this).parents('body').length) {
+ if (($(this).html() && !options.get('disableHiddenCheck') && $(this).is(':hidden')) || !$(this).parents('body').length) {
if (!options.get('composite') && $.data(this, '_jqs_pending')) {
// remove any existing references to the element
for (i = pending.length; i; i--) {
@@ -2528,20 +2549,6 @@
// Setup a very simple "virtual canvas" to make drawing the few shapes we need easier
// This is accessible as $(foo).simpledraw()
- // Detect browser renderer support
- (function() {
- if (document.namespaces && !document.namespaces.v) {
- $.fn.sparkline.hasVML = true;
- document.namespaces.add('v', 'urn:schemas-microsoft-com:vml', '#default#VML');
- } else {
- $.fn.sparkline.hasVML = false;
- }
-
- var el = document.createElement('canvas');
- $.fn.sparkline.hasCanvas = !!(el.getContext && el.getContext('2d'));
-
- })()
-
VShape = createClass({
init: function (target, id, type, args) {
this.target = target;
@@ -3044,4 +3051,4 @@
}
});
-}));
+}))}(document, Math));
\ No newline at end of file
diff --git a/addons/web/static/lib/justgage/justgage.1.0.1.js b/addons/web/static/lib/justgage/justgage.1.0.1.js
deleted file mode 100644
index 24540beb293..00000000000
--- a/addons/web/static/lib/justgage/justgage.1.0.1.js
+++ /dev/null
@@ -1,946 +0,0 @@
-/**
- * JustGage - this is work-in-progress, unreleased, unofficial code, so it might not work top-notch :)
- * Check http://www.justgage.com for official releases
- * Licensed under MIT.
- * @author Bojan Djuricic (@Toorshia)
- *
- * LATEST UPDATES
-
- * -----------------------------
- * April 18, 2013.
- * -----------------------------
- * parentNode - use this instead of id, to attach gauge to node which is outside of DOM tree - https://github.com/toorshia/justgage/issues/48
- * width - force gauge width
- * height - force gauge height
-
- * -----------------------------
- * April 17, 2013.
- * -----------------------------
- * fix - https://github.com/toorshia/justgage/issues/49
-
- * -----------------------------
- * April 01, 2013.
- * -----------------------------
- * fix - https://github.com/toorshia/justgage/issues/46
-
- * -----------------------------
- * March 26, 2013.
- * -----------------------------
- * customSectors - define specific color for value range (0-10 : red, 10-30 : blue etc.)
-
- * -----------------------------
- * March 23, 2013.
- * -----------------------------
- * counter - option to animate value in counting fashion
- * fix - https://github.com/toorshia/justgage/issues/45
-
- * -----------------------------
- * March 13, 2013.
- * -----------------------------
- * refresh method - added optional 'max' parameter to use when you need to update max value
-
- * -----------------------------
- * February 26, 2013.
- * -----------------------------
- * decimals - option to define/limit number of decimals when not using humanFriendly or customRenderer to display value
- * fixed a missing parameters bug when calling generateShadow() for IE < 9
-
- * -----------------------------
- * December 31, 2012.
- * -----------------------------
- * fixed text y-position for hidden divs - workaround for Raphael 'dy' bug - https://github.com/DmitryBaranovskiy/raphael/issues/491
- * 'show' parameters, like showMinMax are now 'hide' because I am lame developer - please update these in your setups
- * Min and Max labels are now auto-off when in donut mode
- * Start angle in donut mode is now 90
- * donutStartAngle - option to define start angle for donut
-
- * -----------------------------
- * November 25, 2012.
- * -----------------------------
- * Option to define custom rendering function for displayed value
-
- * -----------------------------
- * November 19, 2012.
- * -----------------------------
- * Config.value is now updated after gauge refresh
-
- * -----------------------------
- * November 13, 2012.
- * -----------------------------
- * Donut display mode added
- * Option to hide value label
- * Option to enable responsive gauge size
- * Removed default title attribute
- * Option to accept min and max defined as string values
- * Option to configure value symbol
- * Fixed bad aspect ratio calculations
- * Option to configure minimum font size for all texts
- * Option to show shorthand big numbers (human friendly)
- */
-
- JustGage = function(config) {
-
- // if (!config.id) {alert("Missing id parameter for gauge!"); return false;}
- // if (!document.getElementById(config.id)) {alert("No element with id: \""+config.id+"\" found!"); return false;}
-
- var obj = this;
-
- // configurable parameters
- obj.config =
- {
- // id : string
- // this is container element id
- id : config.id,
-
- // parentNode : node object
- // this is container element
- parentNode : (config.parentNode) ? config.parentNode : null,
-
- // width : int
- // gauge width
- width : (config.width) ? config.width : null,
-
- // height : int
- // gauge height
- height : (config.height) ? config.height : null,
-
- // title : string
- // gauge title
- title : (config.title) ? config.title : "",
-
- // titleFontColor : string
- // color of gauge title
- titleFontColor : (config.titleFontColor) ? config.titleFontColor : "#999999",
-
- // value : int
- // value gauge is showing
- value : (config.value) ? config.value : 0,
-
- // valueFontColor : string
- // color of label showing current value
- valueFontColor : (config.valueFontColor) ? config.valueFontColor : "#010101",
-
- // symbol : string
- // special symbol to show next to value
- symbol : (config.symbol) ? config.symbol : "",
-
- // min : int
- // min value
- min : (config.min !== undefined) ? parseFloat(config.min) : 0,
-
- // max : int
- // max value
- max : (config.max !== undefined) ? parseFloat(config.max) : 100,
-
- // humanFriendlyDecimal : int
- // number of decimal places for our human friendly number to contain
- humanFriendlyDecimal : (config.humanFriendlyDecimal) ? config.humanFriendlyDecimal : 0,
-
- // textRenderer: func
- // function applied before rendering text
- textRenderer : (config.textRenderer) ? config.textRenderer : null,
-
- // gaugeWidthScale : float
- // width of the gauge element
- gaugeWidthScale : (config.gaugeWidthScale) ? config.gaugeWidthScale : 1.0,
-
- // gaugeColor : string
- // background color of gauge element
- gaugeColor : (config.gaugeColor) ? config.gaugeColor : "#edebeb",
-
- // label : string
- // text to show below value
- label : (config.label) ? config.label : "",
-
- // labelFontColor : string
- // color of label showing label under value
- labelFontColor : (config.labelFontColor) ? config.labelFontColor : "#b3b3b3",
-
- // shadowOpacity : int
- // 0 ~ 1
- shadowOpacity : (config.shadowOpacity) ? config.shadowOpacity : 0.2,
-
- // shadowSize: int
- // inner shadow size
- shadowSize : (config.shadowSize) ? config.shadowSize : 5,
-
- // shadowVerticalOffset : int
- // how much shadow is offset from top
- shadowVerticalOffset : (config.shadowVerticalOffset) ? config.shadowVerticalOffset : 3,
-
- // levelColors : string[]
- // colors of indicator, from lower to upper, in RGB format
- levelColors : (config.levelColors) ? config.levelColors : [
- "#a9d70b",
- "#f9c802",
- "#ff0000"
- ],
-
- // startAnimationTime : int
- // length of initial animation
- startAnimationTime : (config.startAnimationTime) ? config.startAnimationTime : 700,
-
- // startAnimationType : string
- // type of initial animation (linear, >, <, <>, bounce)
- startAnimationType : (config.startAnimationType) ? config.startAnimationType : ">",
-
- // refreshAnimationTime : int
- // length of refresh animation
- refreshAnimationTime : (config.refreshAnimationTime) ? config.refreshAnimationTime : 700,
-
- // refreshAnimationType : string
- // type of refresh animation (linear, >, <, <>, bounce)
- refreshAnimationType : (config.refreshAnimationType) ? config.refreshAnimationType : ">",
-
- // donutStartAngle : int
- // angle to start from when in donut mode
- donutStartAngle : (config.donutStartAngle) ? config.donutStartAngle : 90,
-
- // valueMinFontSize : int
- // absolute minimum font size for the value
- valueMinFontSize : config.valueMinFontSize || 16,
-
- // titleMinFontSize
- // absolute minimum font size for the title
- titleMinFontSize : config.titleMinFontSize || 10,
-
- // labelMinFontSize
- // absolute minimum font size for the label
- labelMinFontSize : config.labelMinFontSize || 10,
-
- // minLabelMinFontSize
- // absolute minimum font size for the minimum label
- minLabelMinFontSize : config.minLabelMinFontSize || 10,
-
- // maxLabelMinFontSize
- // absolute minimum font size for the maximum label
- maxLabelMinFontSize : config.maxLabelMinFontSize || 10,
-
- // hideValue : bool
- // hide value text
- hideValue : (config.hideValue) ? config.hideValue : false,
-
- // hideMinMax : bool
- // hide min and max values
- hideMinMax : (config.hideMinMax) ? config.hideMinMax : false,
-
- // hideInnerShadow : bool
- // hide inner shadow
- hideInnerShadow : (config.hideInnerShadow) ? config.hideInnerShadow : false,
-
- // humanFriendly : bool
- // convert large numbers for min, max, value to human friendly (e.g. 1234567 -> 1.23M)
- humanFriendly : (config.humanFriendly) ? config.humanFriendly : false,
-
- // noGradient : bool
- // whether to use gradual color change for value, or sector-based
- noGradient : (config.noGradient) ? config.noGradient : false,
-
- // donut : bool
- // show full donut gauge
- donut : (config.donut) ? config.donut : false,
-
- // relativeGaugeSize : bool
- // whether gauge size should follow changes in container element size
- relativeGaugeSize : (config.relativeGaugeSize) ? config.relativeGaugeSize : false,
-
- // counter : bool
- // animate level number change
- counter : (config.counter) ? config.counter : false,
-
- // decimals : int
- // number of digits after floating point
- decimals : (config.decimals) ? config.decimals : 0,
-
- // customSectors : [] of objects
- // number of digits after floating point
- customSectors : (config.customSectors) ? config.customSectors : []
- };
-
- // variables
- var
- canvasW,
- canvasH,
- widgetW,
- widgetH,
- aspect,
- dx,
- dy,
- titleFontSize,
- titleX,
- titleY,
- valueFontSize,
- valueX,
- valueY,
- labelFontSize,
- labelX,
- labelY,
- minFontSize,
- minX,
- minY,
- maxFontSize,
- maxX,
- maxY;
-
- // overflow values
- if (obj.config.value > obj.config.max) obj.config.value = obj.config.max;
- if (obj.config.value < obj.config.min) obj.config.value = obj.config.min;
- obj.originalValue = config.value;
-
- // create canvas
- if (obj.config.id !== null && (document.getElementById(obj.config.id)) !== null) {
- obj.canvas = Raphael(obj.config.id, "100%", "100%");
- } else if (obj.config.parentNode !== null) {
- obj.canvas = Raphael(obj.config.parentNode, "100%", "100%");
- }
-
- if (obj.config.relativeGaugeSize === true) {
- obj.canvas.setViewBox(0, 0, 200, 150, true);
- }
-
- // canvas dimensions
- if (obj.config.relativeGaugeSize === true) {
- canvasW = 200;
- canvasH = 150;
- } else if (obj.config.width !== null && obj.config.height !== null) {
- canvasW = obj.config.width;
- canvasH = obj.config.height;
- } else if (obj.config.parentNode !== null) {
- obj.canvas.setViewBox(0, 0, 200, 150, true);
- canvasW = 200;
- canvasH = 150;
- } else {
- canvasW = getStyle(document.getElementById(obj.config.id), "width").slice(0, -2) * 1;
- canvasH = getStyle(document.getElementById(obj.config.id), "height").slice(0, -2) * 1;
- }
-
- // widget dimensions
- if (obj.config.donut === true) {
-
- // DONUT *******************************
-
- // width more than height
- if(canvasW > canvasH) {
- widgetH = canvasH;
- widgetW = widgetH;
- // width less than height
- } else if (canvasW < canvasH) {
- widgetW = canvasW;
- widgetH = widgetW;
- // if height don't fit, rescale both
- if(widgetH > canvasH) {
- aspect = widgetH / canvasH;
- widgetH = widgetH / aspect;
- widgetW = widgetH / aspect;
- }
- // equal
- } else {
- widgetW = canvasW;
- widgetH = widgetW;
- }
-
- // delta
- dx = (canvasW - widgetW)/2;
- dy = (canvasH - widgetH)/2;
-
- // title
- titleFontSize = ((widgetH / 8) > 10) ? (widgetH / 10) : 10;
- titleX = dx + widgetW / 2;
- titleY = dy + widgetH / 11;
-
- // value
- valueFontSize = ((widgetH / 6.4) > 16) ? (widgetH / 5.4) : 18;
- valueX = dx + widgetW / 2;
- if(obj.config.label !== '') {
- valueY = dy + widgetH / 1.85;
- } else {
- valueY = dy + widgetH / 1.7;
- }
-
- // label
- labelFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10;
- labelX = dx + widgetW / 2;
- labelY = valueY + labelFontSize;
-
- // min
- minFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10;
- minX = dx + (widgetW / 10) + (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
- minY = labelY;
-
- // max
- maxFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10;
- maxX = dx + widgetW - (widgetW / 10) - (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
- maxY = labelY;
-
- } else {
- // HALF *******************************
-
- // width more than height
- if(canvasW > canvasH) {
- widgetH = canvasH;
- widgetW = widgetH * 1.25;
- //if width doesn't fit, rescale both
- if(widgetW > canvasW) {
- aspect = widgetW / canvasW;
- widgetW = widgetW / aspect;
- widgetH = widgetH / aspect;
- }
- // width less than height
- } else if (canvasW < canvasH) {
- widgetW = canvasW;
- widgetH = widgetW / 1.25;
- // if height don't fit, rescale both
- if(widgetH > canvasH) {
- aspect = widgetH / canvasH;
- widgetH = widgetH / aspect;
- widgetW = widgetH / aspect;
- }
- // equal
- } else {
- widgetW = canvasW;
- widgetH = widgetW * 0.75;
- }
-
- // delta
- dx = (canvasW - widgetW)/2;
- dy = (canvasH - widgetH)/2;
-
- // title
- titleFontSize = ((widgetH / 8) > obj.config.titleMinFontSize) ? (widgetH / 10) : obj.config.titleMinFontSize;
- titleX = dx + widgetW / 2;
- titleY = dy + widgetH / 6.4;
-
- // value
- valueFontSize = ((widgetH / 6.5) > obj.config.valueMinFontSize) ? (widgetH / 6.5) : obj.config.valueMinFontSize;
- valueX = dx + widgetW / 2;
- valueY = dy + widgetH / 1.275;
-
- // label
- labelFontSize = ((widgetH / 16) > obj.config.labelMinFontSize) ? (widgetH / 16) : obj.config.labelMinFontSize;
- labelX = dx + widgetW / 2;
- labelY = valueY + valueFontSize / 2 + 5;
-
- // min
- minFontSize = ((widgetH / 16) > obj.config.minLabelMinFontSize) ? (widgetH / 16) : obj.config.minLabelMinFontSize;
- minX = dx + (widgetW / 10) + (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
- minY = labelY;
-
- // max
- maxFontSize = ((widgetH / 16) > obj.config.maxLabelMinFontSize) ? (widgetH / 16) : obj.config.maxLabelMinFontSize;
- maxX = dx + widgetW - (widgetW / 10) - (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
- maxY = labelY;
- }
-
- // parameters
- obj.params = {
- canvasW : canvasW,
- canvasH : canvasH,
- widgetW : widgetW,
- widgetH : widgetH,
- dx : dx,
- dy : dy,
- titleFontSize : titleFontSize,
- titleX : titleX,
- titleY : titleY,
- valueFontSize : valueFontSize,
- valueX : valueX,
- valueY : valueY,
- labelFontSize : labelFontSize,
- labelX : labelX,
- labelY : labelY,
- minFontSize : minFontSize,
- minX : minX,
- minY : minY,
- maxFontSize : maxFontSize,
- maxX : maxX,
- maxY : maxY
- };
-
- // var clear
- canvasW, canvasH, widgetW, widgetH, aspect, dx, dy, titleFontSize, titleX, titleY, valueFontSize, valueX, valueY, labelFontSize, labelX, labelY, minFontSize, minX, minY, maxFontSize, maxX, maxY = null
-
- // pki - custom attribute for generating gauge paths
- obj.canvas.customAttributes.pki = function (value, min, max, w, h, dx, dy, gws, donut) {
-
- var alpha, Ro, Ri, Cx, Cy, Xo, Yo, Xi, Yi, path;
-
- if (donut) {
- alpha = (1 - 2 * (value - min) / (max - min)) * Math.PI;
- Ro = w / 2 - w / 7;
- Ri = Ro - w / 6.666666666666667 * gws;
-
- Cx = w / 2 + dx;
- Cy = h / 1.95 + dy;
-
- Xo = w / 2 + dx + Ro * Math.cos(alpha);
- Yo = h - (h - Cy) + 0 - Ro * Math.sin(alpha);
- Xi = w / 2 + dx + Ri * Math.cos(alpha);
- Yi = h - (h - Cy) + 0 - Ri * Math.sin(alpha);
-
- path += "M" + (Cx - Ri) + "," + Cy + " ";
- path += "L" + (Cx - Ro) + "," + Cy + " ";
- if (value > ((max - min) / 2)) {
- path += "A" + Ro + "," + Ro + " 0 0 1 " + (Cx + Ro) + "," + Cy + " ";
- }
- path += "A" + Ro + "," + Ro + " 0 0 1 " + Xo + "," + Yo + " ";
- path += "L" + Xi + "," + Yi + " ";
- if (value > ((max - min) / 2)) {
- path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx + Ri) + "," + Cy + " ";
- }
- path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx - Ri) + "," + Cy + " ";
- path += "Z ";
-
- return { path: path };
-
- } else {
- alpha = (1 - (value - min) / (max - min)) * Math.PI;
- Ro = w / 2 - w / 10;
- Ri = Ro - w / 6.666666666666667 * gws;
-
- Cx = w / 2 + dx;
- Cy = h / 1.25 + dy;
-
- Xo = w / 2 + dx + Ro * Math.cos(alpha);
- Yo = h - (h - Cy) + 0 - Ro * Math.sin(alpha);
- Xi = w / 2 + dx + Ri * Math.cos(alpha);
- Yi = h - (h - Cy) + 0 - Ri * Math.sin(alpha);
-
- path += "M" + (Cx - Ri) + "," + Cy + " ";
- path += "L" + (Cx - Ro) + "," + Cy + " ";
- path += "A" + Ro + "," + Ro + " 0 0 1 " + Xo + "," + Yo + " ";
- path += "L" + Xi + "," + Yi + " ";
- path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx - Ri) + "," + Cy + " ";
- path += "Z ";
-
- return { path: path };
- }
-
- // var clear
- alpha, Ro, Ri, Cx, Cy, Xo, Yo, Xi, Yi, path = null;
- };
-
- // gauge
- obj.gauge = obj.canvas.path().attr({
- "stroke": "none",
- "fill": obj.config.gaugeColor,
- pki: [
- obj.config.max,
- obj.config.min,
- obj.config.max,
- obj.params.widgetW,
- obj.params.widgetH,
- obj.params.dx,
- obj.params.dy,
- obj.config.gaugeWidthScale,
- obj.config.donut
- ]
- });
-
- // level
- obj.level = obj.canvas.path().attr({
- "stroke": "none",
- "fill": getColor(obj.config.value, (obj.config.value - obj.config.min) / (obj.config.max - obj.config.min), obj.config.levelColors, obj.config.noGradient, obj.config.customSectors),
- pki: [
- obj.config.min,
- obj.config.min,
- obj.config.max,
- obj.params.widgetW,
- obj.params.widgetH,
- obj.params.dx,
- obj.params.dy,
- obj.config.gaugeWidthScale,
- obj.config.donut
- ]
- });
- if(obj.config.donut) {
- obj.level.transform("r" + obj.config.donutStartAngle + ", " + (obj.params.widgetW/2 + obj.params.dx) + ", " + (obj.params.widgetH/1.95 + obj.params.dy));
- }
-
- // title
- obj.txtTitle = obj.canvas.text(obj.params.titleX, obj.params.titleY, obj.config.title);
- obj.txtTitle.attr({
- "font-size":obj.params.titleFontSize,
- "font-weight":"bold",
- "font-family":"Arial",
- "fill":obj.config.titleFontColor,
- "fill-opacity":"1"
- });
- setDy(obj.txtTitle, obj.params.titleFontSize, obj.params.titleY);
-
- // value
- obj.txtValue = obj.canvas.text(obj.params.valueX, obj.params.valueY, 0);
- obj.txtValue.attr({
- "font-size":obj.params.valueFontSize,
- "font-weight":"bold",
- "font-family":"Arial",
- "fill":obj.config.valueFontColor,
- "fill-opacity":"0"
- });
- setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
-
- // label
- obj.txtLabel = obj.canvas.text(obj.params.labelX, obj.params.labelY, obj.config.label);
- obj.txtLabel.attr({
- "font-size":obj.params.labelFontSize,
- "font-weight":"normal",
- "font-family":"Arial",
- "fill":obj.config.labelFontColor,
- "fill-opacity":"0"
- });
- setDy(obj.txtLabel, obj.params.labelFontSize, obj.params.labelY);
-
- // min
- obj.txtMinimum = obj.config.min;
- if( obj.config.humanFriendly ) obj.txtMinimum = humanFriendlyNumber( obj.config.min, obj.config.humanFriendlyDecimal );
- obj.txtMin = obj.canvas.text(obj.params.minX, obj.params.minY, obj.txtMinimum);
- obj.txtMin.attr({
- "font-size":obj.params.minFontSize,
- "font-weight":"normal",
- "font-family":"Arial",
- "fill":obj.config.labelFontColor,
- "fill-opacity": (obj.config.hideMinMax || obj.config.donut)? "0" : "1"
- });
- setDy(obj.txtMin, obj.params.minFontSize, obj.params.minY);
-
- // max
- obj.txtMaximum = obj.config.max;
- if( obj.config.humanFriendly ) obj.txtMaximum = humanFriendlyNumber( obj.config.max, obj.config.humanFriendlyDecimal );
- obj.txtMax = obj.canvas.text(obj.params.maxX, obj.params.maxY, obj.txtMaximum);
- obj.txtMax.attr({
- "font-size":obj.params.maxFontSize,
- "font-weight":"normal",
- "font-family":"Arial",
- "fill":obj.config.labelFontColor,
- "fill-opacity": (obj.config.hideMinMax || obj.config.donut)? "0" : "1"
- });
- setDy(obj.txtMax, obj.params.maxFontSize, obj.params.maxY);
-
- var defs = obj.canvas.canvas.childNodes[1];
- var svg = "http://www.w3.org/2000/svg";
-
- if (ie < 9) {
- onCreateElementNsReady(function() {
- obj.generateShadow(svg, defs);
- });
- } else {
- obj.generateShadow(svg, defs);
- }
-
- // var clear
- defs, svg = null;
-
- // set value to display
- if(obj.config.textRenderer) {
- obj.originalValue = obj.config.textRenderer(obj.originalValue);
- } else if(obj.config.humanFriendly) {
- obj.originalValue = humanFriendlyNumber( obj.originalValue, obj.config.humanFriendlyDecimal ) + obj.config.symbol;
- } else {
- obj.originalValue = (obj.originalValue * 1).toFixed(obj.config.decimals) + obj.config.symbol;
- }
-
- if(obj.config.counter === true) {
- //on each animation frame
- eve.on("raphael.anim.frame." + (obj.level.id), function() {
- var currentValue = obj.level.attr("pki");
- if(obj.config.textRenderer) {
- obj.txtValue.attr("text", obj.config.textRenderer(Math.floor(currentValue[0])));
- } else if(obj.config.humanFriendly) {
- obj.txtValue.attr("text", humanFriendlyNumber( Math.floor(currentValue[0]), obj.config.humanFriendlyDecimal ) + obj.config.symbol);
- } else {
- obj.txtValue.attr("text", (currentValue[0] * 1).toFixed(obj.config.decimals) + obj.config.symbol);
- }
- setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
- currentValue = null;
- });
- //on animation end
- eve.on("raphael.anim.finish." + (obj.level.id), function() {
- obj.txtValue.attr({"text" : obj.originalValue});
- setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
- });
- } else {
- //on animation start
- eve.on("raphael.anim.start." + (obj.level.id), function() {
- obj.txtValue.attr({"text" : obj.originalValue});
- setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
- });
- }
-
- // animate gauge level, value & label
- obj.level.animate({
- pki: [
- obj.config.value,
- obj.config.min,
- obj.config.max,
- obj.params.widgetW,
- obj.params.widgetH,
- obj.params.dx,
- obj.params.dy,
- obj.config.gaugeWidthScale,
- obj.config.donut
- ]
- }, obj.config.startAnimationTime, obj.config.startAnimationType);
- obj.txtValue.animate({"fill-opacity":(obj.config.hideValue)?"0":"1"}, obj.config.startAnimationTime, obj.config.startAnimationType);
- obj.txtLabel.animate({"fill-opacity":"1"}, obj.config.startAnimationTime, obj.config.startAnimationType);
-};
-
-/** Refresh gauge level */
-JustGage.prototype.refresh = function(val, max) {
-
- var obj = this;
- var displayVal, color, max = max || null;
-
- // set new max
- if(max !== null) {
- obj.config.max = max;
-
- obj.txtMaximum = obj.config.max;
- if( obj.config.humanFriendly ) obj.txtMaximum = humanFriendlyNumber( obj.config.max, obj.config.humanFriendlyDecimal );
- obj.txtMax.attr({"text" : obj.txtMaximum});
- setDy(obj.txtMax, obj.params.maxFontSize, obj.params.maxY);
- }
-
- // overflow values
- displayVal = val;
- if ((val * 1) > (obj.config.max * 1)) {val = (obj.config.max * 1);}
- if ((val * 1) < (obj.config.min * 1)) {val = (obj.config.min * 1);}
-
- color = getColor(val, (val - obj.config.min) / (obj.config.max - obj.config.min), obj.config.levelColors, obj.config.noGradient, obj.config.customSectors);
-
- if(obj.config.textRenderer) {
- displayVal = obj.config.textRenderer(displayVal);
- } else if( obj.config.humanFriendly ) {
- displayVal = humanFriendlyNumber( displayVal, obj.config.humanFriendlyDecimal ) + obj.config.symbol;
- } else {
- displayVal = (displayVal * 1).toFixed(obj.config.decimals) + obj.config.symbol;
- }
- obj.originalValue = displayVal;
- obj.config.value = val * 1;
-
- if(!obj.config.counter) {
- obj.txtValue.attr({"text":displayVal});
- setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
- }
-
- obj.level.animate({
- pki: [
- obj.config.value,
- obj.config.min,
- obj.config.max,
- obj.params.widgetW,
- obj.params.widgetH,
- obj.params.dx,
- obj.params.dy,
- obj.config.gaugeWidthScale,
- obj.config.donut
- ],
- "fill":color
- }, obj.config.refreshAnimationTime, obj.config.refreshAnimationType);
-
- // var clear
- obj, displayVal, color, max = null;
-};
-
-/** Generate shadow */
-JustGage.prototype.generateShadow = function(svg, defs) {
-
- var obj = this;
- var gaussFilter, feOffset, feGaussianBlur, feComposite1, feFlood, feComposite2, feComposite3;
-
- // FILTER
- gaussFilter = document.createElementNS(svg,"filter");
- gaussFilter.setAttribute("id","inner-shadow");
- defs.appendChild(gaussFilter);
-
- // offset
- feOffset = document.createElementNS(svg,"feOffset");
- feOffset.setAttribute("dx", 0);
- feOffset.setAttribute("dy", obj.config.shadowVerticalOffset);
- gaussFilter.appendChild(feOffset);
-
- // blur
- feGaussianBlur = document.createElementNS(svg,"feGaussianBlur");
- feGaussianBlur.setAttribute("result","offset-blur");
- feGaussianBlur.setAttribute("stdDeviation", obj.config.shadowSize);
- gaussFilter.appendChild(feGaussianBlur);
-
- // composite 1
- feComposite1 = document.createElementNS(svg,"feComposite");
- feComposite1.setAttribute("operator","out");
- feComposite1.setAttribute("in", "SourceGraphic");
- feComposite1.setAttribute("in2","offset-blur");
- feComposite1.setAttribute("result","inverse");
- gaussFilter.appendChild(feComposite1);
-
- // flood
- feFlood = document.createElementNS(svg,"feFlood");
- feFlood.setAttribute("flood-color","black");
- feFlood.setAttribute("flood-opacity", obj.config.shadowOpacity);
- feFlood.setAttribute("result","color");
- gaussFilter.appendChild(feFlood);
-
- // composite 2
- feComposite2 = document.createElementNS(svg,"feComposite");
- feComposite2.setAttribute("operator","in");
- feComposite2.setAttribute("in", "color");
- feComposite2.setAttribute("in2","inverse");
- feComposite2.setAttribute("result","shadow");
- gaussFilter.appendChild(feComposite2);
-
- // composite 3
- feComposite3 = document.createElementNS(svg,"feComposite");
- feComposite3.setAttribute("operator","over");
- feComposite3.setAttribute("in", "shadow");
- feComposite3.setAttribute("in2","SourceGraphic");
- gaussFilter.appendChild(feComposite3);
-
- // set shadow
- if (!obj.config.hideInnerShadow) {
- obj.canvas.canvas.childNodes[2].setAttribute("filter", "url(#inner-shadow)");
- obj.canvas.canvas.childNodes[3].setAttribute("filter", "url(#inner-shadow)");
- }
-
- // var clear
- gaussFilter, feOffset, feGaussianBlur, feComposite1, feFlood, feComposite2, feComposite3 = null;
-
-};
-
-/** Get color for value */
-function getColor(val, pct, col, noGradient, custSec) {
-
- var no, inc, colors, percentage, rval, gval, bval, lower, upper, range, rangePct, pctLower, pctUpper, color;
- var noGradient = noGradient || custSec.length > 0;
-
- if(custSec.length > 0) {
- for(var i = 0; i < custSec.length; i++) {
- if(val > custSec[i].lo && val <= custSec[i].hi) {
- return custSec[i].color;
- }
- }
- }
-
- no = col.length;
- if (no === 1) return col[0];
- inc = (noGradient) ? (1 / no) : (1 / (no - 1));
- colors = [];
- for (var i = 0; i < col.length; i++) {
- percentage = (noGradient) ? (inc * (i + 1)) : (inc * i);
- rval = parseInt((cutHex(col[i])).substring(0,2),16);
- gval = parseInt((cutHex(col[i])).substring(2,4),16);
- bval = parseInt((cutHex(col[i])).substring(4,6),16);
- colors[i] = { pct: percentage, color: { r: rval, g: gval, b: bval } };
- }
-
- if(pct === 0) {
- return 'rgb(' + [colors[0].color.r, colors[0].color.g, colors[0].color.b].join(',') + ')';
- }
-
- for (var j = 0; j < colors.length; j++) {
- if (pct <= colors[j].pct) {
- if (noGradient) {
- return 'rgb(' + [colors[j].color.r, colors[j].color.g, colors[j].color.b].join(',') + ')';
- } else {
- lower = colors[j - 1];
- upper = colors[j];
- range = upper.pct - lower.pct;
- rangePct = (pct - lower.pct) / range;
- pctLower = 1 - rangePct;
- pctUpper = rangePct;
- color = {
- r: Math.floor(lower.color.r * pctLower + upper.color.r * pctUpper),
- g: Math.floor(lower.color.g * pctLower + upper.color.g * pctUpper),
- b: Math.floor(lower.color.b * pctLower + upper.color.b * pctUpper)
- };
- return 'rgb(' + [color.r, color.g, color.b].join(',') + ')';
- }
- }
- }
-
-}
-
-/** Fix Raphael display:none tspan dy attribute bug */
-function setDy(elem, fontSize, txtYpos) {
- if (!ie || ie > 9) {
- elem.node.firstChild.attributes.dy.value = 0;
- }
-}
-
-/** Random integer */
-function getRandomInt (min, max) {
- return Math.floor(Math.random() * (max - min + 1)) + min;
-}
-
-/** Cut hex */
-function cutHex(str) {
- return (str.charAt(0)=="#") ? str.substring(1,7):str;
-}
-
-/** Human friendly number suffix - From: http://stackoverflow.com/questions/2692323/code-golf-friendly-number-abbreviator */
-function humanFriendlyNumber( n, d ) {
- var p, d2, i, s;
-
- p = Math.pow;
- d2 = p(10, d);
- i = 7;
- while( i ) {
- s = p(10,i--*3);
- if( s <= n ) {
- n = Math.round(n*d2/s)/d2+"KMGTPE"[i];
- }
- }
- return n;
-}
-
-/** Get style */
-function getStyle(oElm, strCssRule){
- var strValue = "";
- if(document.defaultView && document.defaultView.getComputedStyle){
- strValue = document.defaultView.getComputedStyle(oElm, "").getPropertyValue(strCssRule);
- }
- else if(oElm.currentStyle){
- strCssRule = strCssRule.replace(/\-(\w)/g, function (strMatch, p1){
- return p1.toUpperCase();
- });
- strValue = oElm.currentStyle[strCssRule];
- }
- return strValue;
-}
-
-/** Create Element NS Ready */
-function onCreateElementNsReady(func) {
- if (document.createElementNS !== undefined) {
- func();
- } else {
- setTimeout(function() { onCreateElementNsReady(func); }, 100);
- }
-}
-
-/** Get IE version */
-// ----------------------------------------------------------
-// A short snippet for detecting versions of IE in JavaScript
-// without resorting to user-agent sniffing
-// ----------------------------------------------------------
-// If you're not in IE (or IE version is less than 5) then:
-// ie === undefined
-// If you're in IE (>=5) then you can determine which version:
-// ie === 7; // IE7
-// Thus, to detect IE:
-// if (ie) {}
-// And to detect the version:
-// ie === 6 // IE6
-// ie > 7 // IE8, IE9 ...
-// ie < 9 // Anything less than IE9
-// ----------------------------------------------------------
-// UPDATE: Now using Live NodeList idea from @jdalton
-var ie = (function(){
-
- var undef,
- v = 3,
- div = document.createElement('div'),
- all = div.getElementsByTagName('i');
-
- while (
- div.innerHTML = '',
- all[0]
- );
- return v > 4 ? v : undef;
-}());
\ No newline at end of file
diff --git a/addons/web/static/lib/justgage/justgage.js b/addons/web/static/lib/justgage/justgage.js
new file mode 100644
index 00000000000..24540beb293
--- /dev/null
+++ b/addons/web/static/lib/justgage/justgage.js
@@ -0,0 +1,946 @@
+/**
+ * JustGage - this is work-in-progress, unreleased, unofficial code, so it might not work top-notch :)
+ * Check http://www.justgage.com for official releases
+ * Licensed under MIT.
+ * @author Bojan Djuricic (@Toorshia)
+ *
+ * LATEST UPDATES
+
+ * -----------------------------
+ * April 18, 2013.
+ * -----------------------------
+ * parentNode - use this instead of id, to attach gauge to node which is outside of DOM tree - https://github.com/toorshia/justgage/issues/48
+ * width - force gauge width
+ * height - force gauge height
+
+ * -----------------------------
+ * April 17, 2013.
+ * -----------------------------
+ * fix - https://github.com/toorshia/justgage/issues/49
+
+ * -----------------------------
+ * April 01, 2013.
+ * -----------------------------
+ * fix - https://github.com/toorshia/justgage/issues/46
+
+ * -----------------------------
+ * March 26, 2013.
+ * -----------------------------
+ * customSectors - define specific color for value range (0-10 : red, 10-30 : blue etc.)
+
+ * -----------------------------
+ * March 23, 2013.
+ * -----------------------------
+ * counter - option to animate value in counting fashion
+ * fix - https://github.com/toorshia/justgage/issues/45
+
+ * -----------------------------
+ * March 13, 2013.
+ * -----------------------------
+ * refresh method - added optional 'max' parameter to use when you need to update max value
+
+ * -----------------------------
+ * February 26, 2013.
+ * -----------------------------
+ * decimals - option to define/limit number of decimals when not using humanFriendly or customRenderer to display value
+ * fixed a missing parameters bug when calling generateShadow() for IE < 9
+
+ * -----------------------------
+ * December 31, 2012.
+ * -----------------------------
+ * fixed text y-position for hidden divs - workaround for Raphael 'dy' bug - https://github.com/DmitryBaranovskiy/raphael/issues/491
+ * 'show' parameters, like showMinMax are now 'hide' because I am lame developer - please update these in your setups
+ * Min and Max labels are now auto-off when in donut mode
+ * Start angle in donut mode is now 90
+ * donutStartAngle - option to define start angle for donut
+
+ * -----------------------------
+ * November 25, 2012.
+ * -----------------------------
+ * Option to define custom rendering function for displayed value
+
+ * -----------------------------
+ * November 19, 2012.
+ * -----------------------------
+ * Config.value is now updated after gauge refresh
+
+ * -----------------------------
+ * November 13, 2012.
+ * -----------------------------
+ * Donut display mode added
+ * Option to hide value label
+ * Option to enable responsive gauge size
+ * Removed default title attribute
+ * Option to accept min and max defined as string values
+ * Option to configure value symbol
+ * Fixed bad aspect ratio calculations
+ * Option to configure minimum font size for all texts
+ * Option to show shorthand big numbers (human friendly)
+ */
+
+ JustGage = function(config) {
+
+ // if (!config.id) {alert("Missing id parameter for gauge!"); return false;}
+ // if (!document.getElementById(config.id)) {alert("No element with id: \""+config.id+"\" found!"); return false;}
+
+ var obj = this;
+
+ // configurable parameters
+ obj.config =
+ {
+ // id : string
+ // this is container element id
+ id : config.id,
+
+ // parentNode : node object
+ // this is container element
+ parentNode : (config.parentNode) ? config.parentNode : null,
+
+ // width : int
+ // gauge width
+ width : (config.width) ? config.width : null,
+
+ // height : int
+ // gauge height
+ height : (config.height) ? config.height : null,
+
+ // title : string
+ // gauge title
+ title : (config.title) ? config.title : "",
+
+ // titleFontColor : string
+ // color of gauge title
+ titleFontColor : (config.titleFontColor) ? config.titleFontColor : "#999999",
+
+ // value : int
+ // value gauge is showing
+ value : (config.value) ? config.value : 0,
+
+ // valueFontColor : string
+ // color of label showing current value
+ valueFontColor : (config.valueFontColor) ? config.valueFontColor : "#010101",
+
+ // symbol : string
+ // special symbol to show next to value
+ symbol : (config.symbol) ? config.symbol : "",
+
+ // min : int
+ // min value
+ min : (config.min !== undefined) ? parseFloat(config.min) : 0,
+
+ // max : int
+ // max value
+ max : (config.max !== undefined) ? parseFloat(config.max) : 100,
+
+ // humanFriendlyDecimal : int
+ // number of decimal places for our human friendly number to contain
+ humanFriendlyDecimal : (config.humanFriendlyDecimal) ? config.humanFriendlyDecimal : 0,
+
+ // textRenderer: func
+ // function applied before rendering text
+ textRenderer : (config.textRenderer) ? config.textRenderer : null,
+
+ // gaugeWidthScale : float
+ // width of the gauge element
+ gaugeWidthScale : (config.gaugeWidthScale) ? config.gaugeWidthScale : 1.0,
+
+ // gaugeColor : string
+ // background color of gauge element
+ gaugeColor : (config.gaugeColor) ? config.gaugeColor : "#edebeb",
+
+ // label : string
+ // text to show below value
+ label : (config.label) ? config.label : "",
+
+ // labelFontColor : string
+ // color of label showing label under value
+ labelFontColor : (config.labelFontColor) ? config.labelFontColor : "#b3b3b3",
+
+ // shadowOpacity : int
+ // 0 ~ 1
+ shadowOpacity : (config.shadowOpacity) ? config.shadowOpacity : 0.2,
+
+ // shadowSize: int
+ // inner shadow size
+ shadowSize : (config.shadowSize) ? config.shadowSize : 5,
+
+ // shadowVerticalOffset : int
+ // how much shadow is offset from top
+ shadowVerticalOffset : (config.shadowVerticalOffset) ? config.shadowVerticalOffset : 3,
+
+ // levelColors : string[]
+ // colors of indicator, from lower to upper, in RGB format
+ levelColors : (config.levelColors) ? config.levelColors : [
+ "#a9d70b",
+ "#f9c802",
+ "#ff0000"
+ ],
+
+ // startAnimationTime : int
+ // length of initial animation
+ startAnimationTime : (config.startAnimationTime) ? config.startAnimationTime : 700,
+
+ // startAnimationType : string
+ // type of initial animation (linear, >, <, <>, bounce)
+ startAnimationType : (config.startAnimationType) ? config.startAnimationType : ">",
+
+ // refreshAnimationTime : int
+ // length of refresh animation
+ refreshAnimationTime : (config.refreshAnimationTime) ? config.refreshAnimationTime : 700,
+
+ // refreshAnimationType : string
+ // type of refresh animation (linear, >, <, <>, bounce)
+ refreshAnimationType : (config.refreshAnimationType) ? config.refreshAnimationType : ">",
+
+ // donutStartAngle : int
+ // angle to start from when in donut mode
+ donutStartAngle : (config.donutStartAngle) ? config.donutStartAngle : 90,
+
+ // valueMinFontSize : int
+ // absolute minimum font size for the value
+ valueMinFontSize : config.valueMinFontSize || 16,
+
+ // titleMinFontSize
+ // absolute minimum font size for the title
+ titleMinFontSize : config.titleMinFontSize || 10,
+
+ // labelMinFontSize
+ // absolute minimum font size for the label
+ labelMinFontSize : config.labelMinFontSize || 10,
+
+ // minLabelMinFontSize
+ // absolute minimum font size for the minimum label
+ minLabelMinFontSize : config.minLabelMinFontSize || 10,
+
+ // maxLabelMinFontSize
+ // absolute minimum font size for the maximum label
+ maxLabelMinFontSize : config.maxLabelMinFontSize || 10,
+
+ // hideValue : bool
+ // hide value text
+ hideValue : (config.hideValue) ? config.hideValue : false,
+
+ // hideMinMax : bool
+ // hide min and max values
+ hideMinMax : (config.hideMinMax) ? config.hideMinMax : false,
+
+ // hideInnerShadow : bool
+ // hide inner shadow
+ hideInnerShadow : (config.hideInnerShadow) ? config.hideInnerShadow : false,
+
+ // humanFriendly : bool
+ // convert large numbers for min, max, value to human friendly (e.g. 1234567 -> 1.23M)
+ humanFriendly : (config.humanFriendly) ? config.humanFriendly : false,
+
+ // noGradient : bool
+ // whether to use gradual color change for value, or sector-based
+ noGradient : (config.noGradient) ? config.noGradient : false,
+
+ // donut : bool
+ // show full donut gauge
+ donut : (config.donut) ? config.donut : false,
+
+ // relativeGaugeSize : bool
+ // whether gauge size should follow changes in container element size
+ relativeGaugeSize : (config.relativeGaugeSize) ? config.relativeGaugeSize : false,
+
+ // counter : bool
+ // animate level number change
+ counter : (config.counter) ? config.counter : false,
+
+ // decimals : int
+ // number of digits after floating point
+ decimals : (config.decimals) ? config.decimals : 0,
+
+ // customSectors : [] of objects
+ // number of digits after floating point
+ customSectors : (config.customSectors) ? config.customSectors : []
+ };
+
+ // variables
+ var
+ canvasW,
+ canvasH,
+ widgetW,
+ widgetH,
+ aspect,
+ dx,
+ dy,
+ titleFontSize,
+ titleX,
+ titleY,
+ valueFontSize,
+ valueX,
+ valueY,
+ labelFontSize,
+ labelX,
+ labelY,
+ minFontSize,
+ minX,
+ minY,
+ maxFontSize,
+ maxX,
+ maxY;
+
+ // overflow values
+ if (obj.config.value > obj.config.max) obj.config.value = obj.config.max;
+ if (obj.config.value < obj.config.min) obj.config.value = obj.config.min;
+ obj.originalValue = config.value;
+
+ // create canvas
+ if (obj.config.id !== null && (document.getElementById(obj.config.id)) !== null) {
+ obj.canvas = Raphael(obj.config.id, "100%", "100%");
+ } else if (obj.config.parentNode !== null) {
+ obj.canvas = Raphael(obj.config.parentNode, "100%", "100%");
+ }
+
+ if (obj.config.relativeGaugeSize === true) {
+ obj.canvas.setViewBox(0, 0, 200, 150, true);
+ }
+
+ // canvas dimensions
+ if (obj.config.relativeGaugeSize === true) {
+ canvasW = 200;
+ canvasH = 150;
+ } else if (obj.config.width !== null && obj.config.height !== null) {
+ canvasW = obj.config.width;
+ canvasH = obj.config.height;
+ } else if (obj.config.parentNode !== null) {
+ obj.canvas.setViewBox(0, 0, 200, 150, true);
+ canvasW = 200;
+ canvasH = 150;
+ } else {
+ canvasW = getStyle(document.getElementById(obj.config.id), "width").slice(0, -2) * 1;
+ canvasH = getStyle(document.getElementById(obj.config.id), "height").slice(0, -2) * 1;
+ }
+
+ // widget dimensions
+ if (obj.config.donut === true) {
+
+ // DONUT *******************************
+
+ // width more than height
+ if(canvasW > canvasH) {
+ widgetH = canvasH;
+ widgetW = widgetH;
+ // width less than height
+ } else if (canvasW < canvasH) {
+ widgetW = canvasW;
+ widgetH = widgetW;
+ // if height don't fit, rescale both
+ if(widgetH > canvasH) {
+ aspect = widgetH / canvasH;
+ widgetH = widgetH / aspect;
+ widgetW = widgetH / aspect;
+ }
+ // equal
+ } else {
+ widgetW = canvasW;
+ widgetH = widgetW;
+ }
+
+ // delta
+ dx = (canvasW - widgetW)/2;
+ dy = (canvasH - widgetH)/2;
+
+ // title
+ titleFontSize = ((widgetH / 8) > 10) ? (widgetH / 10) : 10;
+ titleX = dx + widgetW / 2;
+ titleY = dy + widgetH / 11;
+
+ // value
+ valueFontSize = ((widgetH / 6.4) > 16) ? (widgetH / 5.4) : 18;
+ valueX = dx + widgetW / 2;
+ if(obj.config.label !== '') {
+ valueY = dy + widgetH / 1.85;
+ } else {
+ valueY = dy + widgetH / 1.7;
+ }
+
+ // label
+ labelFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10;
+ labelX = dx + widgetW / 2;
+ labelY = valueY + labelFontSize;
+
+ // min
+ minFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10;
+ minX = dx + (widgetW / 10) + (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
+ minY = labelY;
+
+ // max
+ maxFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10;
+ maxX = dx + widgetW - (widgetW / 10) - (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
+ maxY = labelY;
+
+ } else {
+ // HALF *******************************
+
+ // width more than height
+ if(canvasW > canvasH) {
+ widgetH = canvasH;
+ widgetW = widgetH * 1.25;
+ //if width doesn't fit, rescale both
+ if(widgetW > canvasW) {
+ aspect = widgetW / canvasW;
+ widgetW = widgetW / aspect;
+ widgetH = widgetH / aspect;
+ }
+ // width less than height
+ } else if (canvasW < canvasH) {
+ widgetW = canvasW;
+ widgetH = widgetW / 1.25;
+ // if height don't fit, rescale both
+ if(widgetH > canvasH) {
+ aspect = widgetH / canvasH;
+ widgetH = widgetH / aspect;
+ widgetW = widgetH / aspect;
+ }
+ // equal
+ } else {
+ widgetW = canvasW;
+ widgetH = widgetW * 0.75;
+ }
+
+ // delta
+ dx = (canvasW - widgetW)/2;
+ dy = (canvasH - widgetH)/2;
+
+ // title
+ titleFontSize = ((widgetH / 8) > obj.config.titleMinFontSize) ? (widgetH / 10) : obj.config.titleMinFontSize;
+ titleX = dx + widgetW / 2;
+ titleY = dy + widgetH / 6.4;
+
+ // value
+ valueFontSize = ((widgetH / 6.5) > obj.config.valueMinFontSize) ? (widgetH / 6.5) : obj.config.valueMinFontSize;
+ valueX = dx + widgetW / 2;
+ valueY = dy + widgetH / 1.275;
+
+ // label
+ labelFontSize = ((widgetH / 16) > obj.config.labelMinFontSize) ? (widgetH / 16) : obj.config.labelMinFontSize;
+ labelX = dx + widgetW / 2;
+ labelY = valueY + valueFontSize / 2 + 5;
+
+ // min
+ minFontSize = ((widgetH / 16) > obj.config.minLabelMinFontSize) ? (widgetH / 16) : obj.config.minLabelMinFontSize;
+ minX = dx + (widgetW / 10) + (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
+ minY = labelY;
+
+ // max
+ maxFontSize = ((widgetH / 16) > obj.config.maxLabelMinFontSize) ? (widgetH / 16) : obj.config.maxLabelMinFontSize;
+ maxX = dx + widgetW - (widgetW / 10) - (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
+ maxY = labelY;
+ }
+
+ // parameters
+ obj.params = {
+ canvasW : canvasW,
+ canvasH : canvasH,
+ widgetW : widgetW,
+ widgetH : widgetH,
+ dx : dx,
+ dy : dy,
+ titleFontSize : titleFontSize,
+ titleX : titleX,
+ titleY : titleY,
+ valueFontSize : valueFontSize,
+ valueX : valueX,
+ valueY : valueY,
+ labelFontSize : labelFontSize,
+ labelX : labelX,
+ labelY : labelY,
+ minFontSize : minFontSize,
+ minX : minX,
+ minY : minY,
+ maxFontSize : maxFontSize,
+ maxX : maxX,
+ maxY : maxY
+ };
+
+ // var clear
+ canvasW, canvasH, widgetW, widgetH, aspect, dx, dy, titleFontSize, titleX, titleY, valueFontSize, valueX, valueY, labelFontSize, labelX, labelY, minFontSize, minX, minY, maxFontSize, maxX, maxY = null
+
+ // pki - custom attribute for generating gauge paths
+ obj.canvas.customAttributes.pki = function (value, min, max, w, h, dx, dy, gws, donut) {
+
+ var alpha, Ro, Ri, Cx, Cy, Xo, Yo, Xi, Yi, path;
+
+ if (donut) {
+ alpha = (1 - 2 * (value - min) / (max - min)) * Math.PI;
+ Ro = w / 2 - w / 7;
+ Ri = Ro - w / 6.666666666666667 * gws;
+
+ Cx = w / 2 + dx;
+ Cy = h / 1.95 + dy;
+
+ Xo = w / 2 + dx + Ro * Math.cos(alpha);
+ Yo = h - (h - Cy) + 0 - Ro * Math.sin(alpha);
+ Xi = w / 2 + dx + Ri * Math.cos(alpha);
+ Yi = h - (h - Cy) + 0 - Ri * Math.sin(alpha);
+
+ path += "M" + (Cx - Ri) + "," + Cy + " ";
+ path += "L" + (Cx - Ro) + "," + Cy + " ";
+ if (value > ((max - min) / 2)) {
+ path += "A" + Ro + "," + Ro + " 0 0 1 " + (Cx + Ro) + "," + Cy + " ";
+ }
+ path += "A" + Ro + "," + Ro + " 0 0 1 " + Xo + "," + Yo + " ";
+ path += "L" + Xi + "," + Yi + " ";
+ if (value > ((max - min) / 2)) {
+ path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx + Ri) + "," + Cy + " ";
+ }
+ path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx - Ri) + "," + Cy + " ";
+ path += "Z ";
+
+ return { path: path };
+
+ } else {
+ alpha = (1 - (value - min) / (max - min)) * Math.PI;
+ Ro = w / 2 - w / 10;
+ Ri = Ro - w / 6.666666666666667 * gws;
+
+ Cx = w / 2 + dx;
+ Cy = h / 1.25 + dy;
+
+ Xo = w / 2 + dx + Ro * Math.cos(alpha);
+ Yo = h - (h - Cy) + 0 - Ro * Math.sin(alpha);
+ Xi = w / 2 + dx + Ri * Math.cos(alpha);
+ Yi = h - (h - Cy) + 0 - Ri * Math.sin(alpha);
+
+ path += "M" + (Cx - Ri) + "," + Cy + " ";
+ path += "L" + (Cx - Ro) + "," + Cy + " ";
+ path += "A" + Ro + "," + Ro + " 0 0 1 " + Xo + "," + Yo + " ";
+ path += "L" + Xi + "," + Yi + " ";
+ path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx - Ri) + "," + Cy + " ";
+ path += "Z ";
+
+ return { path: path };
+ }
+
+ // var clear
+ alpha, Ro, Ri, Cx, Cy, Xo, Yo, Xi, Yi, path = null;
+ };
+
+ // gauge
+ obj.gauge = obj.canvas.path().attr({
+ "stroke": "none",
+ "fill": obj.config.gaugeColor,
+ pki: [
+ obj.config.max,
+ obj.config.min,
+ obj.config.max,
+ obj.params.widgetW,
+ obj.params.widgetH,
+ obj.params.dx,
+ obj.params.dy,
+ obj.config.gaugeWidthScale,
+ obj.config.donut
+ ]
+ });
+
+ // level
+ obj.level = obj.canvas.path().attr({
+ "stroke": "none",
+ "fill": getColor(obj.config.value, (obj.config.value - obj.config.min) / (obj.config.max - obj.config.min), obj.config.levelColors, obj.config.noGradient, obj.config.customSectors),
+ pki: [
+ obj.config.min,
+ obj.config.min,
+ obj.config.max,
+ obj.params.widgetW,
+ obj.params.widgetH,
+ obj.params.dx,
+ obj.params.dy,
+ obj.config.gaugeWidthScale,
+ obj.config.donut
+ ]
+ });
+ if(obj.config.donut) {
+ obj.level.transform("r" + obj.config.donutStartAngle + ", " + (obj.params.widgetW/2 + obj.params.dx) + ", " + (obj.params.widgetH/1.95 + obj.params.dy));
+ }
+
+ // title
+ obj.txtTitle = obj.canvas.text(obj.params.titleX, obj.params.titleY, obj.config.title);
+ obj.txtTitle.attr({
+ "font-size":obj.params.titleFontSize,
+ "font-weight":"bold",
+ "font-family":"Arial",
+ "fill":obj.config.titleFontColor,
+ "fill-opacity":"1"
+ });
+ setDy(obj.txtTitle, obj.params.titleFontSize, obj.params.titleY);
+
+ // value
+ obj.txtValue = obj.canvas.text(obj.params.valueX, obj.params.valueY, 0);
+ obj.txtValue.attr({
+ "font-size":obj.params.valueFontSize,
+ "font-weight":"bold",
+ "font-family":"Arial",
+ "fill":obj.config.valueFontColor,
+ "fill-opacity":"0"
+ });
+ setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
+
+ // label
+ obj.txtLabel = obj.canvas.text(obj.params.labelX, obj.params.labelY, obj.config.label);
+ obj.txtLabel.attr({
+ "font-size":obj.params.labelFontSize,
+ "font-weight":"normal",
+ "font-family":"Arial",
+ "fill":obj.config.labelFontColor,
+ "fill-opacity":"0"
+ });
+ setDy(obj.txtLabel, obj.params.labelFontSize, obj.params.labelY);
+
+ // min
+ obj.txtMinimum = obj.config.min;
+ if( obj.config.humanFriendly ) obj.txtMinimum = humanFriendlyNumber( obj.config.min, obj.config.humanFriendlyDecimal );
+ obj.txtMin = obj.canvas.text(obj.params.minX, obj.params.minY, obj.txtMinimum);
+ obj.txtMin.attr({
+ "font-size":obj.params.minFontSize,
+ "font-weight":"normal",
+ "font-family":"Arial",
+ "fill":obj.config.labelFontColor,
+ "fill-opacity": (obj.config.hideMinMax || obj.config.donut)? "0" : "1"
+ });
+ setDy(obj.txtMin, obj.params.minFontSize, obj.params.minY);
+
+ // max
+ obj.txtMaximum = obj.config.max;
+ if( obj.config.humanFriendly ) obj.txtMaximum = humanFriendlyNumber( obj.config.max, obj.config.humanFriendlyDecimal );
+ obj.txtMax = obj.canvas.text(obj.params.maxX, obj.params.maxY, obj.txtMaximum);
+ obj.txtMax.attr({
+ "font-size":obj.params.maxFontSize,
+ "font-weight":"normal",
+ "font-family":"Arial",
+ "fill":obj.config.labelFontColor,
+ "fill-opacity": (obj.config.hideMinMax || obj.config.donut)? "0" : "1"
+ });
+ setDy(obj.txtMax, obj.params.maxFontSize, obj.params.maxY);
+
+ var defs = obj.canvas.canvas.childNodes[1];
+ var svg = "http://www.w3.org/2000/svg";
+
+ if (ie < 9) {
+ onCreateElementNsReady(function() {
+ obj.generateShadow(svg, defs);
+ });
+ } else {
+ obj.generateShadow(svg, defs);
+ }
+
+ // var clear
+ defs, svg = null;
+
+ // set value to display
+ if(obj.config.textRenderer) {
+ obj.originalValue = obj.config.textRenderer(obj.originalValue);
+ } else if(obj.config.humanFriendly) {
+ obj.originalValue = humanFriendlyNumber( obj.originalValue, obj.config.humanFriendlyDecimal ) + obj.config.symbol;
+ } else {
+ obj.originalValue = (obj.originalValue * 1).toFixed(obj.config.decimals) + obj.config.symbol;
+ }
+
+ if(obj.config.counter === true) {
+ //on each animation frame
+ eve.on("raphael.anim.frame." + (obj.level.id), function() {
+ var currentValue = obj.level.attr("pki");
+ if(obj.config.textRenderer) {
+ obj.txtValue.attr("text", obj.config.textRenderer(Math.floor(currentValue[0])));
+ } else if(obj.config.humanFriendly) {
+ obj.txtValue.attr("text", humanFriendlyNumber( Math.floor(currentValue[0]), obj.config.humanFriendlyDecimal ) + obj.config.symbol);
+ } else {
+ obj.txtValue.attr("text", (currentValue[0] * 1).toFixed(obj.config.decimals) + obj.config.symbol);
+ }
+ setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
+ currentValue = null;
+ });
+ //on animation end
+ eve.on("raphael.anim.finish." + (obj.level.id), function() {
+ obj.txtValue.attr({"text" : obj.originalValue});
+ setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
+ });
+ } else {
+ //on animation start
+ eve.on("raphael.anim.start." + (obj.level.id), function() {
+ obj.txtValue.attr({"text" : obj.originalValue});
+ setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
+ });
+ }
+
+ // animate gauge level, value & label
+ obj.level.animate({
+ pki: [
+ obj.config.value,
+ obj.config.min,
+ obj.config.max,
+ obj.params.widgetW,
+ obj.params.widgetH,
+ obj.params.dx,
+ obj.params.dy,
+ obj.config.gaugeWidthScale,
+ obj.config.donut
+ ]
+ }, obj.config.startAnimationTime, obj.config.startAnimationType);
+ obj.txtValue.animate({"fill-opacity":(obj.config.hideValue)?"0":"1"}, obj.config.startAnimationTime, obj.config.startAnimationType);
+ obj.txtLabel.animate({"fill-opacity":"1"}, obj.config.startAnimationTime, obj.config.startAnimationType);
+};
+
+/** Refresh gauge level */
+JustGage.prototype.refresh = function(val, max) {
+
+ var obj = this;
+ var displayVal, color, max = max || null;
+
+ // set new max
+ if(max !== null) {
+ obj.config.max = max;
+
+ obj.txtMaximum = obj.config.max;
+ if( obj.config.humanFriendly ) obj.txtMaximum = humanFriendlyNumber( obj.config.max, obj.config.humanFriendlyDecimal );
+ obj.txtMax.attr({"text" : obj.txtMaximum});
+ setDy(obj.txtMax, obj.params.maxFontSize, obj.params.maxY);
+ }
+
+ // overflow values
+ displayVal = val;
+ if ((val * 1) > (obj.config.max * 1)) {val = (obj.config.max * 1);}
+ if ((val * 1) < (obj.config.min * 1)) {val = (obj.config.min * 1);}
+
+ color = getColor(val, (val - obj.config.min) / (obj.config.max - obj.config.min), obj.config.levelColors, obj.config.noGradient, obj.config.customSectors);
+
+ if(obj.config.textRenderer) {
+ displayVal = obj.config.textRenderer(displayVal);
+ } else if( obj.config.humanFriendly ) {
+ displayVal = humanFriendlyNumber( displayVal, obj.config.humanFriendlyDecimal ) + obj.config.symbol;
+ } else {
+ displayVal = (displayVal * 1).toFixed(obj.config.decimals) + obj.config.symbol;
+ }
+ obj.originalValue = displayVal;
+ obj.config.value = val * 1;
+
+ if(!obj.config.counter) {
+ obj.txtValue.attr({"text":displayVal});
+ setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
+ }
+
+ obj.level.animate({
+ pki: [
+ obj.config.value,
+ obj.config.min,
+ obj.config.max,
+ obj.params.widgetW,
+ obj.params.widgetH,
+ obj.params.dx,
+ obj.params.dy,
+ obj.config.gaugeWidthScale,
+ obj.config.donut
+ ],
+ "fill":color
+ }, obj.config.refreshAnimationTime, obj.config.refreshAnimationType);
+
+ // var clear
+ obj, displayVal, color, max = null;
+};
+
+/** Generate shadow */
+JustGage.prototype.generateShadow = function(svg, defs) {
+
+ var obj = this;
+ var gaussFilter, feOffset, feGaussianBlur, feComposite1, feFlood, feComposite2, feComposite3;
+
+ // FILTER
+ gaussFilter = document.createElementNS(svg,"filter");
+ gaussFilter.setAttribute("id","inner-shadow");
+ defs.appendChild(gaussFilter);
+
+ // offset
+ feOffset = document.createElementNS(svg,"feOffset");
+ feOffset.setAttribute("dx", 0);
+ feOffset.setAttribute("dy", obj.config.shadowVerticalOffset);
+ gaussFilter.appendChild(feOffset);
+
+ // blur
+ feGaussianBlur = document.createElementNS(svg,"feGaussianBlur");
+ feGaussianBlur.setAttribute("result","offset-blur");
+ feGaussianBlur.setAttribute("stdDeviation", obj.config.shadowSize);
+ gaussFilter.appendChild(feGaussianBlur);
+
+ // composite 1
+ feComposite1 = document.createElementNS(svg,"feComposite");
+ feComposite1.setAttribute("operator","out");
+ feComposite1.setAttribute("in", "SourceGraphic");
+ feComposite1.setAttribute("in2","offset-blur");
+ feComposite1.setAttribute("result","inverse");
+ gaussFilter.appendChild(feComposite1);
+
+ // flood
+ feFlood = document.createElementNS(svg,"feFlood");
+ feFlood.setAttribute("flood-color","black");
+ feFlood.setAttribute("flood-opacity", obj.config.shadowOpacity);
+ feFlood.setAttribute("result","color");
+ gaussFilter.appendChild(feFlood);
+
+ // composite 2
+ feComposite2 = document.createElementNS(svg,"feComposite");
+ feComposite2.setAttribute("operator","in");
+ feComposite2.setAttribute("in", "color");
+ feComposite2.setAttribute("in2","inverse");
+ feComposite2.setAttribute("result","shadow");
+ gaussFilter.appendChild(feComposite2);
+
+ // composite 3
+ feComposite3 = document.createElementNS(svg,"feComposite");
+ feComposite3.setAttribute("operator","over");
+ feComposite3.setAttribute("in", "shadow");
+ feComposite3.setAttribute("in2","SourceGraphic");
+ gaussFilter.appendChild(feComposite3);
+
+ // set shadow
+ if (!obj.config.hideInnerShadow) {
+ obj.canvas.canvas.childNodes[2].setAttribute("filter", "url(#inner-shadow)");
+ obj.canvas.canvas.childNodes[3].setAttribute("filter", "url(#inner-shadow)");
+ }
+
+ // var clear
+ gaussFilter, feOffset, feGaussianBlur, feComposite1, feFlood, feComposite2, feComposite3 = null;
+
+};
+
+/** Get color for value */
+function getColor(val, pct, col, noGradient, custSec) {
+
+ var no, inc, colors, percentage, rval, gval, bval, lower, upper, range, rangePct, pctLower, pctUpper, color;
+ var noGradient = noGradient || custSec.length > 0;
+
+ if(custSec.length > 0) {
+ for(var i = 0; i < custSec.length; i++) {
+ if(val > custSec[i].lo && val <= custSec[i].hi) {
+ return custSec[i].color;
+ }
+ }
+ }
+
+ no = col.length;
+ if (no === 1) return col[0];
+ inc = (noGradient) ? (1 / no) : (1 / (no - 1));
+ colors = [];
+ for (var i = 0; i < col.length; i++) {
+ percentage = (noGradient) ? (inc * (i + 1)) : (inc * i);
+ rval = parseInt((cutHex(col[i])).substring(0,2),16);
+ gval = parseInt((cutHex(col[i])).substring(2,4),16);
+ bval = parseInt((cutHex(col[i])).substring(4,6),16);
+ colors[i] = { pct: percentage, color: { r: rval, g: gval, b: bval } };
+ }
+
+ if(pct === 0) {
+ return 'rgb(' + [colors[0].color.r, colors[0].color.g, colors[0].color.b].join(',') + ')';
+ }
+
+ for (var j = 0; j < colors.length; j++) {
+ if (pct <= colors[j].pct) {
+ if (noGradient) {
+ return 'rgb(' + [colors[j].color.r, colors[j].color.g, colors[j].color.b].join(',') + ')';
+ } else {
+ lower = colors[j - 1];
+ upper = colors[j];
+ range = upper.pct - lower.pct;
+ rangePct = (pct - lower.pct) / range;
+ pctLower = 1 - rangePct;
+ pctUpper = rangePct;
+ color = {
+ r: Math.floor(lower.color.r * pctLower + upper.color.r * pctUpper),
+ g: Math.floor(lower.color.g * pctLower + upper.color.g * pctUpper),
+ b: Math.floor(lower.color.b * pctLower + upper.color.b * pctUpper)
+ };
+ return 'rgb(' + [color.r, color.g, color.b].join(',') + ')';
+ }
+ }
+ }
+
+}
+
+/** Fix Raphael display:none tspan dy attribute bug */
+function setDy(elem, fontSize, txtYpos) {
+ if (!ie || ie > 9) {
+ elem.node.firstChild.attributes.dy.value = 0;
+ }
+}
+
+/** Random integer */
+function getRandomInt (min, max) {
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+}
+
+/** Cut hex */
+function cutHex(str) {
+ return (str.charAt(0)=="#") ? str.substring(1,7):str;
+}
+
+/** Human friendly number suffix - From: http://stackoverflow.com/questions/2692323/code-golf-friendly-number-abbreviator */
+function humanFriendlyNumber( n, d ) {
+ var p, d2, i, s;
+
+ p = Math.pow;
+ d2 = p(10, d);
+ i = 7;
+ while( i ) {
+ s = p(10,i--*3);
+ if( s <= n ) {
+ n = Math.round(n*d2/s)/d2+"KMGTPE"[i];
+ }
+ }
+ return n;
+}
+
+/** Get style */
+function getStyle(oElm, strCssRule){
+ var strValue = "";
+ if(document.defaultView && document.defaultView.getComputedStyle){
+ strValue = document.defaultView.getComputedStyle(oElm, "").getPropertyValue(strCssRule);
+ }
+ else if(oElm.currentStyle){
+ strCssRule = strCssRule.replace(/\-(\w)/g, function (strMatch, p1){
+ return p1.toUpperCase();
+ });
+ strValue = oElm.currentStyle[strCssRule];
+ }
+ return strValue;
+}
+
+/** Create Element NS Ready */
+function onCreateElementNsReady(func) {
+ if (document.createElementNS !== undefined) {
+ func();
+ } else {
+ setTimeout(function() { onCreateElementNsReady(func); }, 100);
+ }
+}
+
+/** Get IE version */
+// ----------------------------------------------------------
+// A short snippet for detecting versions of IE in JavaScript
+// without resorting to user-agent sniffing
+// ----------------------------------------------------------
+// If you're not in IE (or IE version is less than 5) then:
+// ie === undefined
+// If you're in IE (>=5) then you can determine which version:
+// ie === 7; // IE7
+// Thus, to detect IE:
+// if (ie) {}
+// And to detect the version:
+// ie === 6 // IE6
+// ie > 7 // IE8, IE9 ...
+// ie < 9 // Anything less than IE9
+// ----------------------------------------------------------
+// UPDATE: Now using Live NodeList idea from @jdalton
+var ie = (function(){
+
+ var undef,
+ v = 3,
+ div = document.createElement('div'),
+ all = div.getElementsByTagName('i');
+
+ while (
+ div.innerHTML = '',
+ all[0]
+ );
+ return v > 4 ? v : undef;
+}());
\ No newline at end of file
diff --git a/addons/web_kanban/static/src/js/kanban.js b/addons/web_kanban/static/src/js/kanban.js
index bd1c116aded..9bc8e3bd785 100644
--- a/addons/web_kanban/static/src/js/kanban.js
+++ b/addons/web_kanban/static/src/js/kanban.js
@@ -1276,6 +1276,7 @@ instance.web_kanban.GaugeWidget = instance.web_kanban.AbstractField.extend({
className: "oe_gauge",
start: function() {
var self = this;
+ var parent = this.getParent();
var max = 100;
if (this.options.max_field) {
max = this.getParent().record[this.options.max_field].raw_value;
@@ -1287,33 +1288,43 @@ instance.web_kanban.GaugeWidget = instance.web_kanban.AbstractField.extend({
var val = this.field.value;
var value = _.isArray(val) && val.length ? val[val.length-1]['value'] : val;
var title = this.$node.html() || this.field.string;
+ // var unique_id = _.uniqueId("JustGage");
+
+ this.$el.empty()
+ .attr('style', this.$node.attr('style') + ';position:relative; display:inline-block;');
+
this.gage = new JustGage({
parentNode: this.$el[0],
+ // id: unique_id,
value: value,
title: title,
min: 0,
max: max,
relativeGaugeSize: true,
humanFriendly: true,
+ titleFontColor: '#333333',
+ valueFontColor: '#333333',
+ labelFontColor: '#000',
label: label,
levelColors: [
"#ff0000",
"#f9c802",
"#a9d70b"
- ]
+ ],
});
+ this.gage.refresh(value, max);
var flag_open = false;
- if (self.options.action_change) {
- var $svg = self.$el.find('svg');
+ if (this.options.action_change) {
+ var $svg = this.$el.find('svg');
var css = {
'text-align': 'center',
'position': 'absolute',
- 'width': self.$el.outerWidth() + 'px',
- 'top': (self.$el.outerHeight()/2-5) + 'px'
+ 'width': this.$el.outerWidth() + 'px',
+ 'top': (this.$el.outerHeight()/2-5) + 'px'
};
- self.$el.click(function (event) {
+ this.$el.click(function (event) {
event.stopPropagation();
flag_open = false;
if (!parent.view.is_action_enabled('edit')) {
@@ -1364,7 +1375,7 @@ instance.web_kanban.GaugeWidget = instance.web_kanban.AbstractField.extend({
$svg.fadeTo(0, 0.3);
$div = $('').text(_t("Click to change value"));
$div.css(css);
- self.$el.append($div);
+ this.$el.append($div);
}
}
},
--
cgit v1.2.3
From 5245e732bc31dfe65d2480565bf0b9f3be307dd0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Fri, 6 Sep 2013 12:05:28 +0200
Subject: [MOV] [ADD] Moved gauge and sparkline widgets into their own modules.
Introducing two new web modules: web_kanban_gauge and web_kanban_sparkline.
bzr revid: tde@openerp.com-20130906100528-rfk9u9op5u8kadl9
---
addons/web/__openerp__.py | 2 -
.../lib/jquery.sparkline/jquery.sparkline.js | 3054 --------------------
addons/web/static/lib/justgage/justgage.js | 946 ------
addons/web_kanban/static/src/js/kanban.js | 151 -
addons/web_kanban_gauge/__init__.py | 0
addons/web_kanban_gauge/__openerp__.py | 18 +
.../static/lib/justgage/justgage.js | 946 ++++++
.../web_kanban_gauge/static/src/js/kanban_gauge.js | 119 +
addons/web_kanban_sparkline/__init__.py | 0
addons/web_kanban_sparkline/__openerp__.py | 18 +
.../lib/jquery.sparkline/jquery.sparkline.js | 3054 ++++++++++++++++++++
.../static/src/js/kanban_sparkline.js | 37 +
12 files changed, 4192 insertions(+), 4153 deletions(-)
delete mode 100644 addons/web/static/lib/jquery.sparkline/jquery.sparkline.js
delete mode 100644 addons/web/static/lib/justgage/justgage.js
create mode 100644 addons/web_kanban_gauge/__init__.py
create mode 100644 addons/web_kanban_gauge/__openerp__.py
create mode 100644 addons/web_kanban_gauge/static/lib/justgage/justgage.js
create mode 100644 addons/web_kanban_gauge/static/src/js/kanban_gauge.js
create mode 100644 addons/web_kanban_sparkline/__init__.py
create mode 100644 addons/web_kanban_sparkline/__openerp__.py
create mode 100644 addons/web_kanban_sparkline/static/lib/jquery.sparkline/jquery.sparkline.js
create mode 100644 addons/web_kanban_sparkline/static/src/js/kanban_sparkline.js
diff --git a/addons/web/__openerp__.py b/addons/web/__openerp__.py
index 9b13fd48b0a..5c8d27e11cb 100644
--- a/addons/web/__openerp__.py
+++ b/addons/web/__openerp__.py
@@ -32,11 +32,9 @@ This module provides the core of the OpenERP Web Client.
"static/lib/jquery.ui.notify/js/jquery.notify.js",
"static/lib/jquery.deferred-queue/jquery.deferred-queue.js",
"static/lib/jquery.scrollTo/jquery.scrollTo-min.js",
- "static/lib/jquery.sparkline/jquery.sparkline.js",
"static/lib/jquery.tipsy/jquery.tipsy.js",
"static/lib/jquery.textext/jquery.textext.js",
"static/lib/jquery.timeago/jquery.timeago.js",
- "static/lib/justgage/justgage.js",
"static/lib/qweb/qweb2.js",
"static/lib/underscore/underscore.js",
"static/lib/underscore.string/lib/underscore.string.js",
diff --git a/addons/web/static/lib/jquery.sparkline/jquery.sparkline.js b/addons/web/static/lib/jquery.sparkline/jquery.sparkline.js
deleted file mode 100644
index 43b24c080a6..00000000000
--- a/addons/web/static/lib/jquery.sparkline/jquery.sparkline.js
+++ /dev/null
@@ -1,3054 +0,0 @@
-/**
-*
-* jquery.sparkline.js
-*
-* v2.1.2
-* (c) Splunk, Inc
-* Contact: Gareth Watts (gareth@splunk.com)
-* http://omnipotent.net/jquery.sparkline/
-*
-* Generates inline sparkline charts from data supplied either to the method
-* or inline in HTML
-*
-* Compatible with Internet Explorer 6.0+ and modern browsers equipped with the canvas tag
-* (Firefox 2.0+, Safari, Opera, etc)
-*
-* License: New BSD License
-*
-* Copyright (c) 2012, Splunk Inc.
-* All rights reserved.
-*
-* Redistribution and use in source and binary forms, with or without modification,
-* are permitted provided that the following conditions are met:
-*
-* * Redistributions of source code must retain the above copyright notice,
-* this list of conditions and the following disclaimer.
-* * Redistributions in binary form must reproduce the above copyright notice,
-* this list of conditions and the following disclaimer in the documentation
-* and/or other materials provided with the distribution.
-* * Neither the name of Splunk Inc nor the names of its contributors may
-* be used to endorse or promote products derived from this software without
-* specific prior written permission.
-*
-* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
-* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
-* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
-* SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
-* OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-*
-*
-* Usage:
-* $(selector).sparkline(values, options)
-*
-* If values is undefined or set to 'html' then the data values are read from the specified tag:
-*
Sparkline: 1,4,6,6,8,5,3,5
-* $('.sparkline').sparkline();
-* There must be no spaces in the enclosed data set
-*
-* Otherwise values must be an array of numbers or null values
-*
Sparkline: This text replaced if the browser is compatible
-* $('#sparkline1').sparkline([1,4,6,6,8,5,3,5])
-* $('#sparkline2').sparkline([1,4,6,null,null,5,3,5])
-*
-* Values can also be specified in an HTML comment, or as a values attribute:
-*
Sparkline:
-*
Sparkline:
-* $('.sparkline').sparkline();
-*
-* For line charts, x values can also be specified:
-*
Sparkline: 1:1,2.7:4,3.4:6,5:6,6:8,8.7:5,9:3,10:5
-* $('#sparkline1').sparkline([ [1,1], [2.7,4], [3.4,6], [5,6], [6,8], [8.7,5], [9,3], [10,5] ])
-*
-* By default, options should be passed in as teh second argument to the sparkline function:
-* $('.sparkline').sparkline([1,2,3,4], {type: 'bar'})
-*
-* Options can also be set by passing them on the tag itself. This feature is disabled by default though
-* as there's a slight performance overhead:
-* $('.sparkline').sparkline([1,2,3,4], {enableTagOptions: true})
-*
Sparkline: loading
-* Prefix all options supplied as tag attribute with "spark" (configurable by setting tagOptionPrefix)
-*
-* Supported options:
-* lineColor - Color of the line used for the chart
-* fillColor - Color used to fill in the chart - Set to '' or false for a transparent chart
-* width - Width of the chart - Defaults to 3 times the number of values in pixels
-* height - Height of the chart - Defaults to the height of the containing element
-* chartRangeMin - Specify the minimum value to use for the Y range of the chart - Defaults to the minimum value supplied
-* chartRangeMax - Specify the maximum value to use for the Y range of the chart - Defaults to the maximum value supplied
-* chartRangeClip - Clip out of range values to the max/min specified by chartRangeMin and chartRangeMax
-* chartRangeMinX - Specify the minimum value to use for the X range of the chart - Defaults to the minimum value supplied
-* chartRangeMaxX - Specify the maximum value to use for the X range of the chart - Defaults to the maximum value supplied
-* composite - If true then don't erase any existing chart attached to the tag, but draw
-* another chart over the top - Note that width and height are ignored if an
-* existing chart is detected.
-* tagValuesAttribute - Name of tag attribute to check for data values - Defaults to 'values'
-* enableTagOptions - Whether to check tags for sparkline options
-* tagOptionPrefix - Prefix used for options supplied as tag attributes - Defaults to 'spark'
-* disableHiddenCheck - If set to true, then the plugin will assume that charts will never be drawn into a
-* hidden dom element, avoding a browser reflow
-* disableInteraction - If set to true then all mouseover/click interaction behaviour will be disabled,
-* making the plugin perform much like it did in 1.x
-* disableTooltips - If set to true then tooltips will be disabled - Defaults to false (tooltips enabled)
-* disableHighlight - If set to true then highlighting of selected chart elements on mouseover will be disabled
-* defaults to false (highlights enabled)
-* highlightLighten - Factor to lighten/darken highlighted chart values by - Defaults to 1.4 for a 40% increase
-* tooltipContainer - Specify which DOM element the tooltip should be rendered into - defaults to document.body
-* tooltipClassname - Optional CSS classname to apply to tooltips - If not specified then a default style will be applied
-* tooltipOffsetX - How many pixels away from the mouse pointer to render the tooltip on the X axis
-* tooltipOffsetY - How many pixels away from the mouse pointer to render the tooltip on the r axis
-* tooltipFormatter - Optional callback that allows you to override the HTML displayed in the tooltip
-* callback is given arguments of (sparkline, options, fields)
-* tooltipChartTitle - If specified then the tooltip uses the string specified by this setting as a title
-* tooltipFormat - A format string or SPFormat object (or an array thereof for multiple entries)
-* to control the format of the tooltip
-* tooltipPrefix - A string to prepend to each field displayed in a tooltip
-* tooltipSuffix - A string to append to each field displayed in a tooltip
-* tooltipSkipNull - If true then null values will not have a tooltip displayed (defaults to true)
-* tooltipValueLookups - An object or range map to map field values to tooltip strings
-* (eg. to map -1 to "Lost", 0 to "Draw", and 1 to "Win")
-* numberFormatter - Optional callback for formatting numbers in tooltips
-* numberDigitGroupSep - Character to use for group separator in numbers "1,234" - Defaults to ","
-* numberDecimalMark - Character to use for the decimal point when formatting numbers - Defaults to "."
-* numberDigitGroupCount - Number of digits between group separator - Defaults to 3
-*
-* There are 7 types of sparkline, selected by supplying a "type" option of 'line' (default),
-* 'bar', 'tristate', 'bullet', 'discrete', 'pie' or 'box'
-* line - Line chart. Options:
-* spotColor - Set to '' to not end each line in a circular spot
-* minSpotColor - If set, color of spot at minimum value
-* maxSpotColor - If set, color of spot at maximum value
-* spotRadius - Radius in pixels
-* lineWidth - Width of line in pixels
-* normalRangeMin
-* normalRangeMax - If set draws a filled horizontal bar between these two values marking the "normal"
-* or expected range of values
-* normalRangeColor - Color to use for the above bar
-* drawNormalOnTop - Draw the normal range above the chart fill color if true
-* defaultPixelsPerValue - Defaults to 3 pixels of width for each value in the chart
-* highlightSpotColor - The color to use for drawing a highlight spot on mouseover - Set to null to disable
-* highlightLineColor - The color to use for drawing a highlight line on mouseover - Set to null to disable
-* valueSpots - Specify which points to draw spots on, and in which color. Accepts a range map
-*
-* bar - Bar chart. Options:
-* barColor - Color of bars for postive values
-* negBarColor - Color of bars for negative values
-* zeroColor - Color of bars with zero values
-* nullColor - Color of bars with null values - Defaults to omitting the bar entirely
-* barWidth - Width of bars in pixels
-* colorMap - Optional mappnig of values to colors to override the *BarColor values above
-* can be an Array of values to control the color of individual bars or a range map
-* to specify colors for individual ranges of values
-* barSpacing - Gap between bars in pixels
-* zeroAxis - Centers the y-axis around zero if true
-*
-* tristate - Charts values of win (>0), lose (<0) or draw (=0)
-* posBarColor - Color of win values
-* negBarColor - Color of lose values
-* zeroBarColor - Color of draw values
-* barWidth - Width of bars in pixels
-* barSpacing - Gap between bars in pixels
-* colorMap - Optional mappnig of values to colors to override the *BarColor values above
-* can be an Array of values to control the color of individual bars or a range map
-* to specify colors for individual ranges of values
-*
-* discrete - Options:
-* lineHeight - Height of each line in pixels - Defaults to 30% of the graph height
-* thesholdValue - Values less than this value will be drawn using thresholdColor instead of lineColor
-* thresholdColor
-*
-* bullet - Values for bullet graphs msut be in the order: target, performance, range1, range2, range3, ...
-* options:
-* targetColor - The color of the vertical target marker
-* targetWidth - The width of the target marker in pixels
-* performanceColor - The color of the performance measure horizontal bar
-* rangeColors - Colors to use for each qualitative range background color
-*
-* pie - Pie chart. Options:
-* sliceColors - An array of colors to use for pie slices
-* offset - Angle in degrees to offset the first slice - Try -90 or +90
-* borderWidth - Width of border to draw around the pie chart, in pixels - Defaults to 0 (no border)
-* borderColor - Color to use for the pie chart border - Defaults to #000
-*
-* box - Box plot. Options:
-* raw - Set to true to supply pre-computed plot points as values
-* values should be: low_outlier, low_whisker, q1, median, q3, high_whisker, high_outlier
-* When set to false you can supply any number of values and the box plot will
-* be computed for you. Default is false.
-* showOutliers - Set to true (default) to display outliers as circles
-* outlierIQR - Interquartile range used to determine outliers. Default 1.5
-* boxLineColor - Outline color of the box
-* boxFillColor - Fill color for the box
-* whiskerColor - Line color used for whiskers
-* outlierLineColor - Outline color of outlier circles
-* outlierFillColor - Fill color of the outlier circles
-* spotRadius - Radius of outlier circles
-* medianColor - Line color of the median line
-* target - Draw a target cross hair at the supplied value (default undefined)
-*
-*
-*
-* Examples:
-* $('#sparkline1').sparkline(myvalues, { lineColor: '#f00', fillColor: false });
-* $('.barsparks').sparkline('html', { type:'bar', height:'40px', barWidth:5 });
-* $('#tristate').sparkline([1,1,-1,1,0,0,-1], { type:'tristate' }):
-* $('#discrete').sparkline([1,3,4,5,5,3,4,5], { type:'discrete' });
-* $('#bullet').sparkline([10,12,12,9,7], { type:'bullet' });
-* $('#pie').sparkline([1,1,2], { type:'pie' });
-*/
-
-/*jslint regexp: true, browser: true, jquery: true, white: true, nomen: false, plusplus: false, maxerr: 500, indent: 4 */
-
-(function(document, Math, undefined) { // performance/minified-size optimization
-(function(factory) {
- if(typeof define === 'function' && define.amd) {
- define(['jquery'], factory);
- } else if (jQuery && !jQuery.fn.sparkline) {
- factory(jQuery);
- }
-}
-(function($) {
- 'use strict';
-
- var UNSET_OPTION = {},
- getDefaults, createClass, SPFormat, clipval, quartile, normalizeValue, normalizeValues,
- remove, isNumber, all, sum, addCSS, ensureArray, formatNumber, RangeMap,
- MouseHandler, Tooltip, barHighlightMixin,
- line, bar, tristate, discrete, bullet, pie, box, defaultStyles, initStyles,
- VShape, VCanvas_base, VCanvas_canvas, VCanvas_vml, pending, shapeCount = 0;
-
- /**
- * Default configuration settings
- */
- getDefaults = function () {
- return {
- // Settings common to most/all chart types
- common: {
- type: 'line',
- lineColor: '#00f',
- fillColor: '#cdf',
- defaultPixelsPerValue: 3,
- width: 'auto',
- height: 'auto',
- composite: false,
- tagValuesAttribute: 'values',
- tagOptionsPrefix: 'spark',
- enableTagOptions: false,
- enableHighlight: true,
- highlightLighten: 1.4,
- tooltipSkipNull: true,
- tooltipPrefix: '',
- tooltipSuffix: '',
- disableHiddenCheck: false,
- numberFormatter: false,
- numberDigitGroupCount: 3,
- numberDigitGroupSep: ',',
- numberDecimalMark: '.',
- disableTooltips: false,
- disableInteraction: false
- },
- // Defaults for line charts
- line: {
- spotColor: '#f80',
- highlightSpotColor: '#5f5',
- highlightLineColor: '#f22',
- spotRadius: 1.5,
- minSpotColor: '#f80',
- maxSpotColor: '#f80',
- lineWidth: 1,
- normalRangeMin: undefined,
- normalRangeMax: undefined,
- normalRangeColor: '#ccc',
- drawNormalOnTop: false,
- chartRangeMin: undefined,
- chartRangeMax: undefined,
- chartRangeMinX: undefined,
- chartRangeMaxX: undefined,
- tooltipFormat: new SPFormat('● {{prefix}}{{y}}{{suffix}}')
- },
- // Defaults for bar charts
- bar: {
- barColor: '#3366cc',
- negBarColor: '#f44',
- stackedBarColor: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#66aa00',
- '#dd4477', '#0099c6', '#990099'],
- zeroColor: undefined,
- nullColor: undefined,
- zeroAxis: true,
- barWidth: 4,
- barSpacing: 1,
- chartRangeMax: undefined,
- chartRangeMin: undefined,
- chartRangeClip: false,
- colorMap: undefined,
- tooltipFormat: new SPFormat('● {{prefix}}{{value}}{{suffix}}')
- },
- // Defaults for tristate charts
- tristate: {
- barWidth: 4,
- barSpacing: 1,
- posBarColor: '#6f6',
- negBarColor: '#f44',
- zeroBarColor: '#999',
- colorMap: {},
- tooltipFormat: new SPFormat('● {{value:map}}'),
- tooltipValueLookups: { map: { '-1': 'Loss', '0': 'Draw', '1': 'Win' } }
- },
- // Defaults for discrete charts
- discrete: {
- lineHeight: 'auto',
- thresholdColor: undefined,
- thresholdValue: 0,
- chartRangeMax: undefined,
- chartRangeMin: undefined,
- chartRangeClip: false,
- tooltipFormat: new SPFormat('{{prefix}}{{value}}{{suffix}}')
- },
- // Defaults for bullet charts
- bullet: {
- targetColor: '#f33',
- targetWidth: 3, // width of the target bar in pixels
- performanceColor: '#33f',
- rangeColors: ['#d3dafe', '#a8b6ff', '#7f94ff'],
- base: undefined, // set this to a number to change the base start number
- tooltipFormat: new SPFormat('{{fieldkey:fields}} - {{value}}'),
- tooltipValueLookups: { fields: {r: 'Range', p: 'Performance', t: 'Target'} }
- },
- // Defaults for pie charts
- pie: {
- offset: 0,
- sliceColors: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#66aa00',
- '#dd4477', '#0099c6', '#990099'],
- borderWidth: 0,
- borderColor: '#000',
- tooltipFormat: new SPFormat('● {{value}} ({{percent.1}}%)')
- },
- // Defaults for box plots
- box: {
- raw: false,
- boxLineColor: '#000',
- boxFillColor: '#cdf',
- whiskerColor: '#000',
- outlierLineColor: '#333',
- outlierFillColor: '#fff',
- medianColor: '#f00',
- showOutliers: true,
- outlierIQR: 1.5,
- spotRadius: 1.5,
- target: undefined,
- targetColor: '#4a2',
- chartRangeMax: undefined,
- chartRangeMin: undefined,
- tooltipFormat: new SPFormat('{{field:fields}}: {{value}}'),
- tooltipFormatFieldlistKey: 'field',
- tooltipValueLookups: { fields: { lq: 'Lower Quartile', med: 'Median',
- uq: 'Upper Quartile', lo: 'Left Outlier', ro: 'Right Outlier',
- lw: 'Left Whisker', rw: 'Right Whisker'} }
- }
- };
- };
-
- // You can have tooltips use a css class other than jqstooltip by specifying tooltipClassname
- defaultStyles = '.jqstooltip { ' +
- 'position: absolute;' +
- 'left: 0px;' +
- 'top: 0px;' +
- 'visibility: hidden;' +
- 'background: rgb(0, 0, 0) transparent;' +
- 'background-color: rgba(0,0,0,0.6);' +
- 'filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000);' +
- '-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000)";' +
- 'color: white;' +
- 'font: 10px arial, san serif;' +
- 'text-align: left;' +
- 'white-space: nowrap;' +
- 'padding: 5px;' +
- 'border: 1px solid white;' +
- 'z-index: 10000;' +
- '}' +
- '.jqsfield { ' +
- 'color: white;' +
- 'font: 10px arial, san serif;' +
- 'text-align: left;' +
- '}';
-
- /**
- * Utilities
- */
-
- createClass = function (/* [baseclass, [mixin, ...]], definition */) {
- var Class, args;
- Class = function () {
- this.init.apply(this, arguments);
- };
- if (arguments.length > 1) {
- if (arguments[0]) {
- Class.prototype = $.extend(new arguments[0](), arguments[arguments.length - 1]);
- Class._super = arguments[0].prototype;
- } else {
- Class.prototype = arguments[arguments.length - 1];
- }
- if (arguments.length > 2) {
- args = Array.prototype.slice.call(arguments, 1, -1);
- args.unshift(Class.prototype);
- $.extend.apply($, args);
- }
- } else {
- Class.prototype = arguments[0];
- }
- Class.prototype.cls = Class;
- return Class;
- };
-
- /**
- * Wraps a format string for tooltips
- * {{x}}
- * {{x.2}
- * {{x:months}}
- */
- $.SPFormatClass = SPFormat = createClass({
- fre: /\{\{([\w.]+?)(:(.+?))?\}\}/g,
- precre: /(\w+)\.(\d+)/,
-
- init: function (format, fclass) {
- this.format = format;
- this.fclass = fclass;
- },
-
- render: function (fieldset, lookups, options) {
- var self = this,
- fields = fieldset,
- match, token, lookupkey, fieldvalue, prec;
- return this.format.replace(this.fre, function () {
- var lookup;
- token = arguments[1];
- lookupkey = arguments[3];
- match = self.precre.exec(token);
- if (match) {
- prec = match[2];
- token = match[1];
- } else {
- prec = false;
- }
- fieldvalue = fields[token];
- if (fieldvalue === undefined) {
- return '';
- }
- if (lookupkey && lookups && lookups[lookupkey]) {
- lookup = lookups[lookupkey];
- if (lookup.get) { // RangeMap
- return lookups[lookupkey].get(fieldvalue) || fieldvalue;
- } else {
- return lookups[lookupkey][fieldvalue] || fieldvalue;
- }
- }
- if (isNumber(fieldvalue)) {
- if (options.get('numberFormatter')) {
- fieldvalue = options.get('numberFormatter')(fieldvalue);
- } else {
- fieldvalue = formatNumber(fieldvalue, prec,
- options.get('numberDigitGroupCount'),
- options.get('numberDigitGroupSep'),
- options.get('numberDecimalMark'));
- }
- }
- return fieldvalue;
- });
- }
- });
-
- // convience method to avoid needing the new operator
- $.spformat = function(format, fclass) {
- return new SPFormat(format, fclass);
- };
-
- clipval = function (val, min, max) {
- if (val < min) {
- return min;
- }
- if (val > max) {
- return max;
- }
- return val;
- };
-
- quartile = function (values, q) {
- var vl;
- if (q === 2) {
- vl = Math.floor(values.length / 2);
- return values.length % 2 ? values[vl] : (values[vl-1] + values[vl]) / 2;
- } else {
- if (values.length % 2 ) { // odd
- vl = (values.length * q + q) / 4;
- return vl % 1 ? (values[Math.floor(vl)] + values[Math.floor(vl) - 1]) / 2 : values[vl-1];
- } else { //even
- vl = (values.length * q + 2) / 4;
- return vl % 1 ? (values[Math.floor(vl)] + values[Math.floor(vl) - 1]) / 2 : values[vl-1];
-
- }
- }
- };
-
- normalizeValue = function (val) {
- var nf;
- switch (val) {
- case 'undefined':
- val = undefined;
- break;
- case 'null':
- val = null;
- break;
- case 'true':
- val = true;
- break;
- case 'false':
- val = false;
- break;
- default:
- nf = parseFloat(val);
- if (val == nf) {
- val = nf;
- }
- }
- return val;
- };
-
- normalizeValues = function (vals) {
- var i, result = [];
- for (i = vals.length; i--;) {
- result[i] = normalizeValue(vals[i]);
- }
- return result;
- };
-
- remove = function (vals, filter) {
- var i, vl, result = [];
- for (i = 0, vl = vals.length; i < vl; i++) {
- if (vals[i] !== filter) {
- result.push(vals[i]);
- }
- }
- return result;
- };
-
- isNumber = function (num) {
- return !isNaN(parseFloat(num)) && isFinite(num);
- };
-
- formatNumber = function (num, prec, groupsize, groupsep, decsep) {
- var p, i;
- num = (prec === false ? parseFloat(num).toString() : num.toFixed(prec)).split('');
- p = (p = $.inArray('.', num)) < 0 ? num.length : p;
- if (p < num.length) {
- num[p] = decsep;
- }
- for (i = p - groupsize; i > 0; i -= groupsize) {
- num.splice(i, 0, groupsep);
- }
- return num.join('');
- };
-
- // determine if all values of an array match a value
- // returns true if the array is empty
- all = function (val, arr, ignoreNull) {
- var i;
- for (i = arr.length; i--; ) {
- if (ignoreNull && arr[i] === null) continue;
- if (arr[i] !== val) {
- return false;
- }
- }
- return true;
- };
-
- // sums the numeric values in an array, ignoring other values
- sum = function (vals) {
- var total = 0, i;
- for (i = vals.length; i--;) {
- total += typeof vals[i] === 'number' ? vals[i] : 0;
- }
- return total;
- };
-
- ensureArray = function (val) {
- return $.isArray(val) ? val : [val];
- };
-
- // http://paulirish.com/2008/bookmarklet-inject-new-css-rules/
- addCSS = function(css) {
- var tag;
- //if ('\v' == 'v') /* ie only */ {
- if (document.createStyleSheet) {
- document.createStyleSheet().cssText = css;
- } else {
- tag = document.createElement('style');
- tag.type = 'text/css';
- document.getElementsByTagName('head')[0].appendChild(tag);
- tag[(typeof document.body.style.WebkitAppearance == 'string') /* webkit only */ ? 'innerText' : 'innerHTML'] = css;
- }
- };
-
- // Provide a cross-browser interface to a few simple drawing primitives
- $.fn.simpledraw = function (width, height, useExisting, interact) {
- var target, mhandler;
- if (useExisting && (target = this.data('_jqs_vcanvas'))) {
- return target;
- }
-
- if ($.fn.sparkline.canvas === false) {
- // We've already determined that neither Canvas nor VML are available
- return false;
-
- } else if ($.fn.sparkline.canvas === undefined) {
- // No function defined yet -- need to see if we support Canvas or VML
- var el = document.createElement('canvas');
- if (!!(el.getContext && el.getContext('2d'))) {
- // Canvas is available
- $.fn.sparkline.canvas = function(width, height, target, interact) {
- return new VCanvas_canvas(width, height, target, interact);
- };
- } else if (document.namespaces && !document.namespaces.v) {
- // VML is available
- document.namespaces.add('v', 'urn:schemas-microsoft-com:vml', '#default#VML');
- $.fn.sparkline.canvas = function(width, height, target, interact) {
- return new VCanvas_vml(width, height, target);
- };
- } else {
- // Neither Canvas nor VML are available
- $.fn.sparkline.canvas = false;
- return false;
- }
- }
-
- if (width === undefined) {
- width = $(this).innerWidth();
- }
- if (height === undefined) {
- height = $(this).innerHeight();
- }
-
- target = $.fn.sparkline.canvas(width, height, this, interact);
-
- mhandler = $(this).data('_jqs_mhandler');
- if (mhandler) {
- mhandler.registerCanvas(target);
- }
- return target;
- };
-
- $.fn.cleardraw = function () {
- var target = this.data('_jqs_vcanvas');
- if (target) {
- target.reset();
- }
- };
-
- $.RangeMapClass = RangeMap = createClass({
- init: function (map) {
- var key, range, rangelist = [];
- for (key in map) {
- if (map.hasOwnProperty(key) && typeof key === 'string' && key.indexOf(':') > -1) {
- range = key.split(':');
- range[0] = range[0].length === 0 ? -Infinity : parseFloat(range[0]);
- range[1] = range[1].length === 0 ? Infinity : parseFloat(range[1]);
- range[2] = map[key];
- rangelist.push(range);
- }
- }
- this.map = map;
- this.rangelist = rangelist || false;
- },
-
- get: function (value) {
- var rangelist = this.rangelist,
- i, range, result;
- if ((result = this.map[value]) !== undefined) {
- return result;
- }
- if (rangelist) {
- for (i = rangelist.length; i--;) {
- range = rangelist[i];
- if (range[0] <= value && range[1] >= value) {
- return range[2];
- }
- }
- }
- return undefined;
- }
- });
-
- // Convenience function
- $.range_map = function(map) {
- return new RangeMap(map);
- };
-
- MouseHandler = createClass({
- init: function (el, options) {
- var $el = $(el);
- this.$el = $el;
- this.options = options;
- this.currentPageX = 0;
- this.currentPageY = 0;
- this.el = el;
- this.splist = [];
- this.tooltip = null;
- this.over = false;
- this.displayTooltips = !options.get('disableTooltips');
- this.highlightEnabled = !options.get('disableHighlight');
- },
-
- registerSparkline: function (sp) {
- this.splist.push(sp);
- if (this.over) {
- this.updateDisplay();
- }
- },
-
- registerCanvas: function (canvas) {
- var $canvas = $(canvas.canvas);
- this.canvas = canvas;
- this.$canvas = $canvas;
- $canvas.mouseenter($.proxy(this.mouseenter, this));
- $canvas.mouseleave($.proxy(this.mouseleave, this));
- $canvas.click($.proxy(this.mouseclick, this));
- },
-
- reset: function (removeTooltip) {
- this.splist = [];
- if (this.tooltip && removeTooltip) {
- this.tooltip.remove();
- this.tooltip = undefined;
- }
- },
-
- mouseclick: function (e) {
- var clickEvent = $.Event('sparklineClick');
- clickEvent.originalEvent = e;
- clickEvent.sparklines = this.splist;
- this.$el.trigger(clickEvent);
- },
-
- mouseenter: function (e) {
- $(document.body).unbind('mousemove.jqs');
- $(document.body).bind('mousemove.jqs', $.proxy(this.mousemove, this));
- this.over = true;
- this.currentPageX = e.pageX;
- this.currentPageY = e.pageY;
- this.currentEl = e.target;
- if (!this.tooltip && this.displayTooltips) {
- this.tooltip = new Tooltip(this.options);
- this.tooltip.updatePosition(e.pageX, e.pageY);
- }
- this.updateDisplay();
- },
-
- mouseleave: function () {
- $(document.body).unbind('mousemove.jqs');
- var splist = this.splist,
- spcount = splist.length,
- needsRefresh = false,
- sp, i;
- this.over = false;
- this.currentEl = null;
-
- if (this.tooltip) {
- this.tooltip.remove();
- this.tooltip = null;
- }
-
- for (i = 0; i < spcount; i++) {
- sp = splist[i];
- if (sp.clearRegionHighlight()) {
- needsRefresh = true;
- }
- }
-
- if (needsRefresh) {
- this.canvas.render();
- }
- },
-
- mousemove: function (e) {
- this.currentPageX = e.pageX;
- this.currentPageY = e.pageY;
- this.currentEl = e.target;
- if (this.tooltip) {
- this.tooltip.updatePosition(e.pageX, e.pageY);
- }
- this.updateDisplay();
- },
-
- updateDisplay: function () {
- var splist = this.splist,
- spcount = splist.length,
- needsRefresh = false,
- offset = this.$canvas.offset(),
- localX = this.currentPageX - offset.left,
- localY = this.currentPageY - offset.top,
- tooltiphtml, sp, i, result, changeEvent;
- if (!this.over) {
- return;
- }
- for (i = 0; i < spcount; i++) {
- sp = splist[i];
- result = sp.setRegionHighlight(this.currentEl, localX, localY);
- if (result) {
- needsRefresh = true;
- }
- }
- if (needsRefresh) {
- changeEvent = $.Event('sparklineRegionChange');
- changeEvent.sparklines = this.splist;
- this.$el.trigger(changeEvent);
- if (this.tooltip) {
- tooltiphtml = '';
- for (i = 0; i < spcount; i++) {
- sp = splist[i];
- tooltiphtml += sp.getCurrentRegionTooltip();
- }
- this.tooltip.setContent(tooltiphtml);
- }
- if (!this.disableHighlight) {
- this.canvas.render();
- }
- }
- if (result === null) {
- this.mouseleave();
- }
- }
- });
-
-
- Tooltip = createClass({
- sizeStyle: 'position: static !important;' +
- 'display: block !important;' +
- 'visibility: hidden !important;' +
- 'float: left !important;',
-
- init: function (options) {
- var tooltipClassname = options.get('tooltipClassname', 'jqstooltip'),
- sizetipStyle = this.sizeStyle,
- offset;
- this.container = options.get('tooltipContainer') || document.body;
- this.tooltipOffsetX = options.get('tooltipOffsetX', 10);
- this.tooltipOffsetY = options.get('tooltipOffsetY', 12);
- // remove any previous lingering tooltip
- $('#jqssizetip').remove();
- $('#jqstooltip').remove();
- this.sizetip = $('', {
- id: 'jqssizetip',
- style: sizetipStyle,
- 'class': tooltipClassname
- });
- this.tooltip = $('', {
- id: 'jqstooltip',
- 'class': tooltipClassname
- }).appendTo(this.container);
- // account for the container's location
- offset = this.tooltip.offset();
- this.offsetLeft = offset.left;
- this.offsetTop = offset.top;
- this.hidden = true;
- $(window).unbind('resize.jqs scroll.jqs');
- $(window).bind('resize.jqs scroll.jqs', $.proxy(this.updateWindowDims, this));
- this.updateWindowDims();
- },
-
- updateWindowDims: function () {
- this.scrollTop = $(window).scrollTop();
- this.scrollLeft = $(window).scrollLeft();
- this.scrollRight = this.scrollLeft + $(window).width();
- this.updatePosition();
- },
-
- getSize: function (content) {
- this.sizetip.html(content).appendTo(this.container);
- this.width = this.sizetip.width() + 1;
- this.height = this.sizetip.height();
- this.sizetip.remove();
- },
-
- setContent: function (content) {
- if (!content) {
- this.tooltip.css('visibility', 'hidden');
- this.hidden = true;
- return;
- }
- this.getSize(content);
- this.tooltip.html(content)
- .css({
- 'width': this.width,
- 'height': this.height,
- 'visibility': 'visible'
- });
- if (this.hidden) {
- this.hidden = false;
- this.updatePosition();
- }
- },
-
- updatePosition: function (x, y) {
- if (x === undefined) {
- if (this.mousex === undefined) {
- return;
- }
- x = this.mousex - this.offsetLeft;
- y = this.mousey - this.offsetTop;
-
- } else {
- this.mousex = x = x - this.offsetLeft;
- this.mousey = y = y - this.offsetTop;
- }
- if (!this.height || !this.width || this.hidden) {
- return;
- }
-
- y -= this.height + this.tooltipOffsetY;
- x += this.tooltipOffsetX;
-
- if (y < this.scrollTop) {
- y = this.scrollTop;
- }
- if (x < this.scrollLeft) {
- x = this.scrollLeft;
- } else if (x + this.width > this.scrollRight) {
- x = this.scrollRight - this.width;
- }
-
- this.tooltip.css({
- 'left': x,
- 'top': y
- });
- },
-
- remove: function () {
- this.tooltip.remove();
- this.sizetip.remove();
- this.sizetip = this.tooltip = undefined;
- $(window).unbind('resize.jqs scroll.jqs');
- }
- });
-
- initStyles = function() {
- addCSS(defaultStyles);
- };
-
- $(initStyles);
-
- pending = [];
- $.fn.sparkline = function (userValues, userOptions) {
- return this.each(function () {
- var options = new $.fn.sparkline.options(this, userOptions),
- $this = $(this),
- render, i;
- render = function () {
- var values, width, height, tmp, mhandler, sp, vals;
- if (userValues === 'html' || userValues === undefined) {
- vals = this.getAttribute(options.get('tagValuesAttribute'));
- if (vals === undefined || vals === null) {
- vals = $this.html();
- }
- values = vals.replace(/(^\s*\s*$)|\s+/g, '').split(',');
- } else {
- values = userValues;
- }
-
- width = options.get('width') === 'auto' ? values.length * options.get('defaultPixelsPerValue') : options.get('width');
- if (options.get('height') === 'auto') {
- if (!options.get('composite') || !$.data(this, '_jqs_vcanvas')) {
- // must be a better way to get the line height
- tmp = document.createElement('span');
- tmp.innerHTML = 'a';
- $this.html(tmp);
- height = $(tmp).innerHeight() || $(tmp).height();
- $(tmp).remove();
- tmp = null;
- }
- } else {
- height = options.get('height');
- }
-
- if (!options.get('disableInteraction')) {
- mhandler = $.data(this, '_jqs_mhandler');
- if (!mhandler) {
- mhandler = new MouseHandler(this, options);
- $.data(this, '_jqs_mhandler', mhandler);
- } else if (!options.get('composite')) {
- mhandler.reset();
- }
- } else {
- mhandler = false;
- }
-
- if (options.get('composite') && !$.data(this, '_jqs_vcanvas')) {
- if (!$.data(this, '_jqs_errnotify')) {
- alert('Attempted to attach a composite sparkline to an element with no existing sparkline');
- $.data(this, '_jqs_errnotify', true);
- }
- return;
- }
-
- sp = new $.fn.sparkline[options.get('type')](this, values, options, width, height);
-
- sp.render();
-
- if (mhandler) {
- mhandler.registerSparkline(sp);
- }
- };
- if (($(this).html() && !options.get('disableHiddenCheck') && $(this).is(':hidden')) || !$(this).parents('body').length) {
- if (!options.get('composite') && $.data(this, '_jqs_pending')) {
- // remove any existing references to the element
- for (i = pending.length; i; i--) {
- if (pending[i - 1][0] == this) {
- pending.splice(i - 1, 1);
- }
- }
- }
- pending.push([this, render]);
- $.data(this, '_jqs_pending', true);
- } else {
- render.call(this);
- }
- });
- };
-
- $.fn.sparkline.defaults = getDefaults();
-
-
- $.sparkline_display_visible = function () {
- var el, i, pl;
- var done = [];
- for (i = 0, pl = pending.length; i < pl; i++) {
- el = pending[i][0];
- if ($(el).is(':visible') && !$(el).parents().is(':hidden')) {
- pending[i][1].call(el);
- $.data(pending[i][0], '_jqs_pending', false);
- done.push(i);
- } else if (!$(el).closest('html').length && !$.data(el, '_jqs_pending')) {
- // element has been inserted and removed from the DOM
- // If it was not yet inserted into the dom then the .data request
- // will return true.
- // removing from the dom causes the data to be removed.
- $.data(pending[i][0], '_jqs_pending', false);
- done.push(i);
- }
- }
- for (i = done.length; i; i--) {
- pending.splice(done[i - 1], 1);
- }
- };
-
-
- /**
- * User option handler
- */
- $.fn.sparkline.options = createClass({
- init: function (tag, userOptions) {
- var extendedOptions, defaults, base, tagOptionType;
- this.userOptions = userOptions = userOptions || {};
- this.tag = tag;
- this.tagValCache = {};
- defaults = $.fn.sparkline.defaults;
- base = defaults.common;
- this.tagOptionsPrefix = userOptions.enableTagOptions && (userOptions.tagOptionsPrefix || base.tagOptionsPrefix);
-
- tagOptionType = this.getTagSetting('type');
- if (tagOptionType === UNSET_OPTION) {
- extendedOptions = defaults[userOptions.type || base.type];
- } else {
- extendedOptions = defaults[tagOptionType];
- }
- this.mergedOptions = $.extend({}, base, extendedOptions, userOptions);
- },
-
-
- getTagSetting: function (key) {
- var prefix = this.tagOptionsPrefix,
- val, i, pairs, keyval;
- if (prefix === false || prefix === undefined) {
- return UNSET_OPTION;
- }
- if (this.tagValCache.hasOwnProperty(key)) {
- val = this.tagValCache.key;
- } else {
- val = this.tag.getAttribute(prefix + key);
- if (val === undefined || val === null) {
- val = UNSET_OPTION;
- } else if (val.substr(0, 1) === '[') {
- val = val.substr(1, val.length - 2).split(',');
- for (i = val.length; i--;) {
- val[i] = normalizeValue(val[i].replace(/(^\s*)|(\s*$)/g, ''));
- }
- } else if (val.substr(0, 1) === '{') {
- pairs = val.substr(1, val.length - 2).split(',');
- val = {};
- for (i = pairs.length; i--;) {
- keyval = pairs[i].split(':', 2);
- val[keyval[0].replace(/(^\s*)|(\s*$)/g, '')] = normalizeValue(keyval[1].replace(/(^\s*)|(\s*$)/g, ''));
- }
- } else {
- val = normalizeValue(val);
- }
- this.tagValCache.key = val;
- }
- return val;
- },
-
- get: function (key, defaultval) {
- var tagOption = this.getTagSetting(key),
- result;
- if (tagOption !== UNSET_OPTION) {
- return tagOption;
- }
- return (result = this.mergedOptions[key]) === undefined ? defaultval : result;
- }
- });
-
-
- $.fn.sparkline._base = createClass({
- disabled: false,
-
- init: function (el, values, options, width, height) {
- this.el = el;
- this.$el = $(el);
- this.values = values;
- this.options = options;
- this.width = width;
- this.height = height;
- this.currentRegion = undefined;
- },
-
- /**
- * Setup the canvas
- */
- initTarget: function () {
- var interactive = !this.options.get('disableInteraction');
- if (!(this.target = this.$el.simpledraw(this.width, this.height, this.options.get('composite'), interactive))) {
- this.disabled = true;
- } else {
- this.canvasWidth = this.target.pixelWidth;
- this.canvasHeight = this.target.pixelHeight;
- }
- },
-
- /**
- * Actually render the chart to the canvas
- */
- render: function () {
- if (this.disabled) {
- this.el.innerHTML = '';
- return false;
- }
- return true;
- },
-
- /**
- * Return a region id for a given x/y co-ordinate
- */
- getRegion: function (x, y) {
- },
-
- /**
- * Highlight an item based on the moused-over x,y co-ordinate
- */
- setRegionHighlight: function (el, x, y) {
- var currentRegion = this.currentRegion,
- highlightEnabled = !this.options.get('disableHighlight'),
- newRegion;
- if (x > this.canvasWidth || y > this.canvasHeight || x < 0 || y < 0) {
- return null;
- }
- newRegion = this.getRegion(el, x, y);
- if (currentRegion !== newRegion) {
- if (currentRegion !== undefined && highlightEnabled) {
- this.removeHighlight();
- }
- this.currentRegion = newRegion;
- if (newRegion !== undefined && highlightEnabled) {
- this.renderHighlight();
- }
- return true;
- }
- return false;
- },
-
- /**
- * Reset any currently highlighted item
- */
- clearRegionHighlight: function () {
- if (this.currentRegion !== undefined) {
- this.removeHighlight();
- this.currentRegion = undefined;
- return true;
- }
- return false;
- },
-
- renderHighlight: function () {
- this.changeHighlight(true);
- },
-
- removeHighlight: function () {
- this.changeHighlight(false);
- },
-
- changeHighlight: function (highlight) {},
-
- /**
- * Fetch the HTML to display as a tooltip
- */
- getCurrentRegionTooltip: function () {
- var options = this.options,
- header = '',
- entries = [],
- fields, formats, formatlen, fclass, text, i,
- showFields, showFieldsKey, newFields, fv,
- formatter, format, fieldlen, j;
- if (this.currentRegion === undefined) {
- return '';
- }
- fields = this.getCurrentRegionFields();
- formatter = options.get('tooltipFormatter');
- if (formatter) {
- return formatter(this, options, fields);
- }
- if (options.get('tooltipChartTitle')) {
- header += '
' + options.get('tooltipChartTitle') + '
\n';
- }
- formats = this.options.get('tooltipFormat');
- if (!formats) {
- return '';
- }
- if (!$.isArray(formats)) {
- formats = [formats];
- }
- if (!$.isArray(fields)) {
- fields = [fields];
- }
- showFields = this.options.get('tooltipFormatFieldlist');
- showFieldsKey = this.options.get('tooltipFormatFieldlistKey');
- if (showFields && showFieldsKey) {
- // user-selected ordering of fields
- newFields = [];
- for (i = fields.length; i--;) {
- fv = fields[i][showFieldsKey];
- if ((j = $.inArray(fv, showFields)) != -1) {
- newFields[j] = fields[i];
- }
- }
- fields = newFields;
- }
- formatlen = formats.length;
- fieldlen = fields.length;
- for (i = 0; i < formatlen; i++) {
- format = formats[i];
- if (typeof format === 'string') {
- format = new SPFormat(format);
- }
- fclass = format.fclass || 'jqsfield';
- for (j = 0; j < fieldlen; j++) {
- if (!fields[j].isNull || !options.get('tooltipSkipNull')) {
- $.extend(fields[j], {
- prefix: options.get('tooltipPrefix'),
- suffix: options.get('tooltipSuffix')
- });
- text = format.render(fields[j], options.get('tooltipValueLookups'), options);
- entries.push('
' + text + '
');
- }
- }
- }
- if (entries.length) {
- return header + entries.join('\n');
- }
- return '';
- },
-
- getCurrentRegionFields: function () {},
-
- calcHighlightColor: function (color, options) {
- var highlightColor = options.get('highlightColor'),
- lighten = options.get('highlightLighten'),
- parse, mult, rgbnew, i;
- if (highlightColor) {
- return highlightColor;
- }
- if (lighten) {
- // extract RGB values
- parse = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(color) || /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(color);
- if (parse) {
- rgbnew = [];
- mult = color.length === 4 ? 16 : 1;
- for (i = 0; i < 3; i++) {
- rgbnew[i] = clipval(Math.round(parseInt(parse[i + 1], 16) * mult * lighten), 0, 255);
- }
- return 'rgb(' + rgbnew.join(',') + ')';
- }
-
- }
- return color;
- }
-
- });
-
- barHighlightMixin = {
- changeHighlight: function (highlight) {
- var currentRegion = this.currentRegion,
- target = this.target,
- shapeids = this.regionShapes[currentRegion],
- newShapes;
- // will be null if the region value was null
- if (shapeids) {
- newShapes = this.renderRegion(currentRegion, highlight);
- if ($.isArray(newShapes) || $.isArray(shapeids)) {
- target.replaceWithShapes(shapeids, newShapes);
- this.regionShapes[currentRegion] = $.map(newShapes, function (newShape) {
- return newShape.id;
- });
- } else {
- target.replaceWithShape(shapeids, newShapes);
- this.regionShapes[currentRegion] = newShapes.id;
- }
- }
- },
-
- render: function () {
- var values = this.values,
- target = this.target,
- regionShapes = this.regionShapes,
- shapes, ids, i, j;
-
- if (!this.cls._super.render.call(this)) {
- return;
- }
- for (i = values.length; i--;) {
- shapes = this.renderRegion(i);
- if (shapes) {
- if ($.isArray(shapes)) {
- ids = [];
- for (j = shapes.length; j--;) {
- shapes[j].append();
- ids.push(shapes[j].id);
- }
- regionShapes[i] = ids;
- } else {
- shapes.append();
- regionShapes[i] = shapes.id; // store just the shapeid
- }
- } else {
- // null value
- regionShapes[i] = null;
- }
- }
- target.render();
- }
- };
-
- /**
- * Line charts
- */
- $.fn.sparkline.line = line = createClass($.fn.sparkline._base, {
- type: 'line',
-
- init: function (el, values, options, width, height) {
- line._super.init.call(this, el, values, options, width, height);
- this.vertices = [];
- this.regionMap = [];
- this.xvalues = [];
- this.yvalues = [];
- this.yminmax = [];
- this.hightlightSpotId = null;
- this.lastShapeId = null;
- this.initTarget();
- },
-
- getRegion: function (el, x, y) {
- var i,
- regionMap = this.regionMap; // maps regions to value positions
- for (i = regionMap.length; i--;) {
- if (regionMap[i] !== null && x >= regionMap[i][0] && x <= regionMap[i][1]) {
- return regionMap[i][2];
- }
- }
- return undefined;
- },
-
- getCurrentRegionFields: function () {
- var currentRegion = this.currentRegion;
- return {
- isNull: this.yvalues[currentRegion] === null,
- x: this.xvalues[currentRegion],
- y: this.yvalues[currentRegion],
- color: this.options.get('lineColor'),
- fillColor: this.options.get('fillColor'),
- offset: currentRegion
- };
- },
-
- renderHighlight: function () {
- var currentRegion = this.currentRegion,
- target = this.target,
- vertex = this.vertices[currentRegion],
- options = this.options,
- spotRadius = options.get('spotRadius'),
- highlightSpotColor = options.get('highlightSpotColor'),
- highlightLineColor = options.get('highlightLineColor'),
- highlightSpot, highlightLine;
-
- if (!vertex) {
- return;
- }
- if (spotRadius && highlightSpotColor) {
- highlightSpot = target.drawCircle(vertex[0], vertex[1],
- spotRadius, undefined, highlightSpotColor);
- this.highlightSpotId = highlightSpot.id;
- target.insertAfterShape(this.lastShapeId, highlightSpot);
- }
- if (highlightLineColor) {
- highlightLine = target.drawLine(vertex[0], this.canvasTop, vertex[0],
- this.canvasTop + this.canvasHeight, highlightLineColor);
- this.highlightLineId = highlightLine.id;
- target.insertAfterShape(this.lastShapeId, highlightLine);
- }
- },
-
- removeHighlight: function () {
- var target = this.target;
- if (this.highlightSpotId) {
- target.removeShapeId(this.highlightSpotId);
- this.highlightSpotId = null;
- }
- if (this.highlightLineId) {
- target.removeShapeId(this.highlightLineId);
- this.highlightLineId = null;
- }
- },
-
- scanValues: function () {
- var values = this.values,
- valcount = values.length,
- xvalues = this.xvalues,
- yvalues = this.yvalues,
- yminmax = this.yminmax,
- i, val, isStr, isArray, sp;
- for (i = 0; i < valcount; i++) {
- val = values[i];
- isStr = typeof(values[i]) === 'string';
- isArray = typeof(values[i]) === 'object' && values[i] instanceof Array;
- sp = isStr && values[i].split(':');
- if (isStr && sp.length === 2) { // x:y
- xvalues.push(Number(sp[0]));
- yvalues.push(Number(sp[1]));
- yminmax.push(Number(sp[1]));
- } else if (isArray) {
- xvalues.push(val[0]);
- yvalues.push(val[1]);
- yminmax.push(val[1]);
- } else {
- xvalues.push(i);
- if (values[i] === null || values[i] === 'null') {
- yvalues.push(null);
- } else {
- yvalues.push(Number(val));
- yminmax.push(Number(val));
- }
- }
- }
- if (this.options.get('xvalues')) {
- xvalues = this.options.get('xvalues');
- }
-
- this.maxy = this.maxyorg = Math.max.apply(Math, yminmax);
- this.miny = this.minyorg = Math.min.apply(Math, yminmax);
-
- this.maxx = Math.max.apply(Math, xvalues);
- this.minx = Math.min.apply(Math, xvalues);
-
- this.xvalues = xvalues;
- this.yvalues = yvalues;
- this.yminmax = yminmax;
-
- },
-
- processRangeOptions: function () {
- var options = this.options,
- normalRangeMin = options.get('normalRangeMin'),
- normalRangeMax = options.get('normalRangeMax');
-
- if (normalRangeMin !== undefined) {
- if (normalRangeMin < this.miny) {
- this.miny = normalRangeMin;
- }
- if (normalRangeMax > this.maxy) {
- this.maxy = normalRangeMax;
- }
- }
- if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < this.miny)) {
- this.miny = options.get('chartRangeMin');
- }
- if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > this.maxy)) {
- this.maxy = options.get('chartRangeMax');
- }
- if (options.get('chartRangeMinX') !== undefined && (options.get('chartRangeClipX') || options.get('chartRangeMinX') < this.minx)) {
- this.minx = options.get('chartRangeMinX');
- }
- if (options.get('chartRangeMaxX') !== undefined && (options.get('chartRangeClipX') || options.get('chartRangeMaxX') > this.maxx)) {
- this.maxx = options.get('chartRangeMaxX');
- }
-
- },
-
- drawNormalRange: function (canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey) {
- var normalRangeMin = this.options.get('normalRangeMin'),
- normalRangeMax = this.options.get('normalRangeMax'),
- ytop = canvasTop + Math.round(canvasHeight - (canvasHeight * ((normalRangeMax - this.miny) / rangey))),
- height = Math.round((canvasHeight * (normalRangeMax - normalRangeMin)) / rangey);
- this.target.drawRect(canvasLeft, ytop, canvasWidth, height, undefined, this.options.get('normalRangeColor')).append();
- },
-
- render: function () {
- var options = this.options,
- target = this.target,
- canvasWidth = this.canvasWidth,
- canvasHeight = this.canvasHeight,
- vertices = this.vertices,
- spotRadius = options.get('spotRadius'),
- regionMap = this.regionMap,
- rangex, rangey, yvallast,
- canvasTop, canvasLeft,
- vertex, path, paths, x, y, xnext, xpos, xposnext,
- last, next, yvalcount, lineShapes, fillShapes, plen,
- valueSpots, hlSpotsEnabled, color, xvalues, yvalues, i;
-
- if (!line._super.render.call(this)) {
- return;
- }
-
- this.scanValues();
- this.processRangeOptions();
-
- xvalues = this.xvalues;
- yvalues = this.yvalues;
-
- if (!this.yminmax.length || this.yvalues.length < 2) {
- // empty or all null valuess
- return;
- }
-
- canvasTop = canvasLeft = 0;
-
- rangex = this.maxx - this.minx === 0 ? 1 : this.maxx - this.minx;
- rangey = this.maxy - this.miny === 0 ? 1 : this.maxy - this.miny;
- yvallast = this.yvalues.length - 1;
-
- if (spotRadius && (canvasWidth < (spotRadius * 4) || canvasHeight < (spotRadius * 4))) {
- spotRadius = 0;
- }
- if (spotRadius) {
- // adjust the canvas size as required so that spots will fit
- hlSpotsEnabled = options.get('highlightSpotColor') && !options.get('disableInteraction');
- if (hlSpotsEnabled || options.get('minSpotColor') || (options.get('spotColor') && yvalues[yvallast] === this.miny)) {
- canvasHeight -= Math.ceil(spotRadius);
- }
- if (hlSpotsEnabled || options.get('maxSpotColor') || (options.get('spotColor') && yvalues[yvallast] === this.maxy)) {
- canvasHeight -= Math.ceil(spotRadius);
- canvasTop += Math.ceil(spotRadius);
- }
- if (hlSpotsEnabled ||
- ((options.get('minSpotColor') || options.get('maxSpotColor')) && (yvalues[0] === this.miny || yvalues[0] === this.maxy))) {
- canvasLeft += Math.ceil(spotRadius);
- canvasWidth -= Math.ceil(spotRadius);
- }
- if (hlSpotsEnabled || options.get('spotColor') ||
- (options.get('minSpotColor') || options.get('maxSpotColor') &&
- (yvalues[yvallast] === this.miny || yvalues[yvallast] === this.maxy))) {
- canvasWidth -= Math.ceil(spotRadius);
- }
- }
-
-
- canvasHeight--;
-
- if (options.get('normalRangeMin') !== undefined && !options.get('drawNormalOnTop')) {
- this.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey);
- }
-
- path = [];
- paths = [path];
- last = next = null;
- yvalcount = yvalues.length;
- for (i = 0; i < yvalcount; i++) {
- x = xvalues[i];
- xnext = xvalues[i + 1];
- y = yvalues[i];
- xpos = canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex));
- xposnext = i < yvalcount - 1 ? canvasLeft + Math.round((xnext - this.minx) * (canvasWidth / rangex)) : canvasWidth;
- next = xpos + ((xposnext - xpos) / 2);
- regionMap[i] = [last || 0, next, i];
- last = next;
- if (y === null) {
- if (i) {
- if (yvalues[i - 1] !== null) {
- path = [];
- paths.push(path);
- }
- vertices.push(null);
- }
- } else {
- if (y < this.miny) {
- y = this.miny;
- }
- if (y > this.maxy) {
- y = this.maxy;
- }
- if (!path.length) {
- // previous value was null
- path.push([xpos, canvasTop + canvasHeight]);
- }
- vertex = [xpos, canvasTop + Math.round(canvasHeight - (canvasHeight * ((y - this.miny) / rangey)))];
- path.push(vertex);
- vertices.push(vertex);
- }
- }
-
- lineShapes = [];
- fillShapes = [];
- plen = paths.length;
- for (i = 0; i < plen; i++) {
- path = paths[i];
- if (path.length) {
- if (options.get('fillColor')) {
- path.push([path[path.length - 1][0], (canvasTop + canvasHeight)]);
- fillShapes.push(path.slice(0));
- path.pop();
- }
- // if there's only a single point in this path, then we want to display it
- // as a vertical line which means we keep path[0] as is
- if (path.length > 2) {
- // else we want the first value
- path[0] = [path[0][0], path[1][1]];
- }
- lineShapes.push(path);
- }
- }
-
- // draw the fill first, then optionally the normal range, then the line on top of that
- plen = fillShapes.length;
- for (i = 0; i < plen; i++) {
- target.drawShape(fillShapes[i],
- options.get('fillColor'), options.get('fillColor')).append();
- }
-
- if (options.get('normalRangeMin') !== undefined && options.get('drawNormalOnTop')) {
- this.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey);
- }
-
- plen = lineShapes.length;
- for (i = 0; i < plen; i++) {
- target.drawShape(lineShapes[i], options.get('lineColor'), undefined,
- options.get('lineWidth')).append();
- }
-
- if (spotRadius && options.get('valueSpots')) {
- valueSpots = options.get('valueSpots');
- if (valueSpots.get === undefined) {
- valueSpots = new RangeMap(valueSpots);
- }
- for (i = 0; i < yvalcount; i++) {
- color = valueSpots.get(yvalues[i]);
- if (color) {
- target.drawCircle(canvasLeft + Math.round((xvalues[i] - this.minx) * (canvasWidth / rangex)),
- canvasTop + Math.round(canvasHeight - (canvasHeight * ((yvalues[i] - this.miny) / rangey))),
- spotRadius, undefined,
- color).append();
- }
- }
-
- }
- if (spotRadius && options.get('spotColor') && yvalues[yvallast] !== null) {
- target.drawCircle(canvasLeft + Math.round((xvalues[xvalues.length - 1] - this.minx) * (canvasWidth / rangex)),
- canvasTop + Math.round(canvasHeight - (canvasHeight * ((yvalues[yvallast] - this.miny) / rangey))),
- spotRadius, undefined,
- options.get('spotColor')).append();
- }
- if (this.maxy !== this.minyorg) {
- if (spotRadius && options.get('minSpotColor')) {
- x = xvalues[$.inArray(this.minyorg, yvalues)];
- target.drawCircle(canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)),
- canvasTop + Math.round(canvasHeight - (canvasHeight * ((this.minyorg - this.miny) / rangey))),
- spotRadius, undefined,
- options.get('minSpotColor')).append();
- }
- if (spotRadius && options.get('maxSpotColor')) {
- x = xvalues[$.inArray(this.maxyorg, yvalues)];
- target.drawCircle(canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)),
- canvasTop + Math.round(canvasHeight - (canvasHeight * ((this.maxyorg - this.miny) / rangey))),
- spotRadius, undefined,
- options.get('maxSpotColor')).append();
- }
- }
-
- this.lastShapeId = target.getLastShapeId();
- this.canvasTop = canvasTop;
- target.render();
- }
- });
-
- /**
- * Bar charts
- */
- $.fn.sparkline.bar = bar = createClass($.fn.sparkline._base, barHighlightMixin, {
- type: 'bar',
-
- init: function (el, values, options, width, height) {
- var barWidth = parseInt(options.get('barWidth'), 10),
- barSpacing = parseInt(options.get('barSpacing'), 10),
- chartRangeMin = options.get('chartRangeMin'),
- chartRangeMax = options.get('chartRangeMax'),
- chartRangeClip = options.get('chartRangeClip'),
- stackMin = Infinity,
- stackMax = -Infinity,
- isStackString, groupMin, groupMax, stackRanges,
- numValues, i, vlen, range, zeroAxis, xaxisOffset, min, max, clipMin, clipMax,
- stacked, vlist, j, slen, svals, val, yoffset, yMaxCalc, canvasHeightEf;
- bar._super.init.call(this, el, values, options, width, height);
-
- // scan values to determine whether to stack bars
- for (i = 0, vlen = values.length; i < vlen; i++) {
- val = values[i];
- isStackString = typeof(val) === 'string' && val.indexOf(':') > -1;
- if (isStackString || $.isArray(val)) {
- stacked = true;
- if (isStackString) {
- val = values[i] = normalizeValues(val.split(':'));
- }
- val = remove(val, null); // min/max will treat null as zero
- groupMin = Math.min.apply(Math, val);
- groupMax = Math.max.apply(Math, val);
- if (groupMin < stackMin) {
- stackMin = groupMin;
- }
- if (groupMax > stackMax) {
- stackMax = groupMax;
- }
- }
- }
-
- this.stacked = stacked;
- this.regionShapes = {};
- this.barWidth = barWidth;
- this.barSpacing = barSpacing;
- this.totalBarWidth = barWidth + barSpacing;
- this.width = width = (values.length * barWidth) + ((values.length - 1) * barSpacing);
-
- this.initTarget();
-
- if (chartRangeClip) {
- clipMin = chartRangeMin === undefined ? -Infinity : chartRangeMin;
- clipMax = chartRangeMax === undefined ? Infinity : chartRangeMax;
- }
-
- numValues = [];
- stackRanges = stacked ? [] : numValues;
- var stackTotals = [];
- var stackRangesNeg = [];
- for (i = 0, vlen = values.length; i < vlen; i++) {
- if (stacked) {
- vlist = values[i];
- values[i] = svals = [];
- stackTotals[i] = 0;
- stackRanges[i] = stackRangesNeg[i] = 0;
- for (j = 0, slen = vlist.length; j < slen; j++) {
- val = svals[j] = chartRangeClip ? clipval(vlist[j], clipMin, clipMax) : vlist[j];
- if (val !== null) {
- if (val > 0) {
- stackTotals[i] += val;
- }
- if (stackMin < 0 && stackMax > 0) {
- if (val < 0) {
- stackRangesNeg[i] += Math.abs(val);
- } else {
- stackRanges[i] += val;
- }
- } else {
- stackRanges[i] += Math.abs(val - (val < 0 ? stackMax : stackMin));
- }
- numValues.push(val);
- }
- }
- } else {
- val = chartRangeClip ? clipval(values[i], clipMin, clipMax) : values[i];
- val = values[i] = normalizeValue(val);
- if (val !== null) {
- numValues.push(val);
- }
- }
- }
- this.max = max = Math.max.apply(Math, numValues);
- this.min = min = Math.min.apply(Math, numValues);
- this.stackMax = stackMax = stacked ? Math.max.apply(Math, stackTotals) : max;
- this.stackMin = stackMin = stacked ? Math.min.apply(Math, numValues) : min;
-
- if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < min)) {
- min = options.get('chartRangeMin');
- }
- if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > max)) {
- max = options.get('chartRangeMax');
- }
-
- this.zeroAxis = zeroAxis = options.get('zeroAxis', true);
- if (min <= 0 && max >= 0 && zeroAxis) {
- xaxisOffset = 0;
- } else if (zeroAxis == false) {
- xaxisOffset = min;
- } else if (min > 0) {
- xaxisOffset = min;
- } else {
- xaxisOffset = max;
- }
- this.xaxisOffset = xaxisOffset;
-
- range = stacked ? (Math.max.apply(Math, stackRanges) + Math.max.apply(Math, stackRangesNeg)) : max - min;
-
- // as we plot zero/min values a single pixel line, we add a pixel to all other
- // values - Reduce the effective canvas size to suit
- this.canvasHeightEf = (zeroAxis && min < 0) ? this.canvasHeight - 2 : this.canvasHeight - 1;
-
- if (min < xaxisOffset) {
- yMaxCalc = (stacked && max >= 0) ? stackMax : max;
- yoffset = (yMaxCalc - xaxisOffset) / range * this.canvasHeight;
- if (yoffset !== Math.ceil(yoffset)) {
- this.canvasHeightEf -= 2;
- yoffset = Math.ceil(yoffset);
- }
- } else {
- yoffset = this.canvasHeight;
- }
- this.yoffset = yoffset;
-
- if ($.isArray(options.get('colorMap'))) {
- this.colorMapByIndex = options.get('colorMap');
- this.colorMapByValue = null;
- } else {
- this.colorMapByIndex = null;
- this.colorMapByValue = options.get('colorMap');
- if (this.colorMapByValue && this.colorMapByValue.get === undefined) {
- this.colorMapByValue = new RangeMap(this.colorMapByValue);
- }
- }
-
- this.range = range;
- },
-
- getRegion: function (el, x, y) {
- var result = Math.floor(x / this.totalBarWidth);
- return (result < 0 || result >= this.values.length) ? undefined : result;
- },
-
- getCurrentRegionFields: function () {
- var currentRegion = this.currentRegion,
- values = ensureArray(this.values[currentRegion]),
- result = [],
- value, i;
- for (i = values.length; i--;) {
- value = values[i];
- result.push({
- isNull: value === null,
- value: value,
- color: this.calcColor(i, value, currentRegion),
- offset: currentRegion
- });
- }
- return result;
- },
-
- calcColor: function (stacknum, value, valuenum) {
- var colorMapByIndex = this.colorMapByIndex,
- colorMapByValue = this.colorMapByValue,
- options = this.options,
- color, newColor;
- if (this.stacked) {
- color = options.get('stackedBarColor');
- } else {
- color = (value < 0) ? options.get('negBarColor') : options.get('barColor');
- }
- if (value === 0 && options.get('zeroColor') !== undefined) {
- color = options.get('zeroColor');
- }
- if (colorMapByValue && (newColor = colorMapByValue.get(value))) {
- color = newColor;
- } else if (colorMapByIndex && colorMapByIndex.length > valuenum) {
- color = colorMapByIndex[valuenum];
- }
- return $.isArray(color) ? color[stacknum % color.length] : color;
- },
-
- /**
- * Render bar(s) for a region
- */
- renderRegion: function (valuenum, highlight) {
- var vals = this.values[valuenum],
- options = this.options,
- xaxisOffset = this.xaxisOffset,
- result = [],
- range = this.range,
- stacked = this.stacked,
- target = this.target,
- x = valuenum * this.totalBarWidth,
- canvasHeightEf = this.canvasHeightEf,
- yoffset = this.yoffset,
- y, height, color, isNull, yoffsetNeg, i, valcount, val, minPlotted, allMin;
-
- vals = $.isArray(vals) ? vals : [vals];
- valcount = vals.length;
- val = vals[0];
- isNull = all(null, vals);
- allMin = all(xaxisOffset, vals, true);
-
- if (isNull) {
- if (options.get('nullColor')) {
- color = highlight ? options.get('nullColor') : this.calcHighlightColor(options.get('nullColor'), options);
- y = (yoffset > 0) ? yoffset - 1 : yoffset;
- return target.drawRect(x, y, this.barWidth - 1, 0, color, color);
- } else {
- return undefined;
- }
- }
- yoffsetNeg = yoffset;
- for (i = 0; i < valcount; i++) {
- val = vals[i];
-
- if (stacked && val === xaxisOffset) {
- if (!allMin || minPlotted) {
- continue;
- }
- minPlotted = true;
- }
-
- if (range > 0) {
- height = Math.floor(canvasHeightEf * ((Math.abs(val - xaxisOffset) / range))) + 1;
- } else {
- height = 1;
- }
- if (val < xaxisOffset || (val === xaxisOffset && yoffset === 0)) {
- y = yoffsetNeg;
- yoffsetNeg += height;
- } else {
- y = yoffset - height;
- yoffset -= height;
- }
- color = this.calcColor(i, val, valuenum);
- if (highlight) {
- color = this.calcHighlightColor(color, options);
- }
- result.push(target.drawRect(x, y, this.barWidth - 1, height - 1, color, color));
- }
- if (result.length === 1) {
- return result[0];
- }
- return result;
- }
- });
-
- /**
- * Tristate charts
- */
- $.fn.sparkline.tristate = tristate = createClass($.fn.sparkline._base, barHighlightMixin, {
- type: 'tristate',
-
- init: function (el, values, options, width, height) {
- var barWidth = parseInt(options.get('barWidth'), 10),
- barSpacing = parseInt(options.get('barSpacing'), 10);
- tristate._super.init.call(this, el, values, options, width, height);
-
- this.regionShapes = {};
- this.barWidth = barWidth;
- this.barSpacing = barSpacing;
- this.totalBarWidth = barWidth + barSpacing;
- this.values = $.map(values, Number);
- this.width = width = (values.length * barWidth) + ((values.length - 1) * barSpacing);
-
- if ($.isArray(options.get('colorMap'))) {
- this.colorMapByIndex = options.get('colorMap');
- this.colorMapByValue = null;
- } else {
- this.colorMapByIndex = null;
- this.colorMapByValue = options.get('colorMap');
- if (this.colorMapByValue && this.colorMapByValue.get === undefined) {
- this.colorMapByValue = new RangeMap(this.colorMapByValue);
- }
- }
- this.initTarget();
- },
-
- getRegion: function (el, x, y) {
- return Math.floor(x / this.totalBarWidth);
- },
-
- getCurrentRegionFields: function () {
- var currentRegion = this.currentRegion;
- return {
- isNull: this.values[currentRegion] === undefined,
- value: this.values[currentRegion],
- color: this.calcColor(this.values[currentRegion], currentRegion),
- offset: currentRegion
- };
- },
-
- calcColor: function (value, valuenum) {
- var values = this.values,
- options = this.options,
- colorMapByIndex = this.colorMapByIndex,
- colorMapByValue = this.colorMapByValue,
- color, newColor;
-
- if (colorMapByValue && (newColor = colorMapByValue.get(value))) {
- color = newColor;
- } else if (colorMapByIndex && colorMapByIndex.length > valuenum) {
- color = colorMapByIndex[valuenum];
- } else if (values[valuenum] < 0) {
- color = options.get('negBarColor');
- } else if (values[valuenum] > 0) {
- color = options.get('posBarColor');
- } else {
- color = options.get('zeroBarColor');
- }
- return color;
- },
-
- renderRegion: function (valuenum, highlight) {
- var values = this.values,
- options = this.options,
- target = this.target,
- canvasHeight, height, halfHeight,
- x, y, color;
-
- canvasHeight = target.pixelHeight;
- halfHeight = Math.round(canvasHeight / 2);
-
- x = valuenum * this.totalBarWidth;
- if (values[valuenum] < 0) {
- y = halfHeight;
- height = halfHeight - 1;
- } else if (values[valuenum] > 0) {
- y = 0;
- height = halfHeight - 1;
- } else {
- y = halfHeight - 1;
- height = 2;
- }
- color = this.calcColor(values[valuenum], valuenum);
- if (color === null) {
- return;
- }
- if (highlight) {
- color = this.calcHighlightColor(color, options);
- }
- return target.drawRect(x, y, this.barWidth - 1, height - 1, color, color);
- }
- });
-
- /**
- * Discrete charts
- */
- $.fn.sparkline.discrete = discrete = createClass($.fn.sparkline._base, barHighlightMixin, {
- type: 'discrete',
-
- init: function (el, values, options, width, height) {
- discrete._super.init.call(this, el, values, options, width, height);
-
- this.regionShapes = {};
- this.values = values = $.map(values, Number);
- this.min = Math.min.apply(Math, values);
- this.max = Math.max.apply(Math, values);
- this.range = this.max - this.min;
- this.width = width = options.get('width') === 'auto' ? values.length * 2 : this.width;
- this.interval = Math.floor(width / values.length);
- this.itemWidth = width / values.length;
- if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < this.min)) {
- this.min = options.get('chartRangeMin');
- }
- if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > this.max)) {
- this.max = options.get('chartRangeMax');
- }
- this.initTarget();
- if (this.target) {
- this.lineHeight = options.get('lineHeight') === 'auto' ? Math.round(this.canvasHeight * 0.3) : options.get('lineHeight');
- }
- },
-
- getRegion: function (el, x, y) {
- return Math.floor(x / this.itemWidth);
- },
-
- getCurrentRegionFields: function () {
- var currentRegion = this.currentRegion;
- return {
- isNull: this.values[currentRegion] === undefined,
- value: this.values[currentRegion],
- offset: currentRegion
- };
- },
-
- renderRegion: function (valuenum, highlight) {
- var values = this.values,
- options = this.options,
- min = this.min,
- max = this.max,
- range = this.range,
- interval = this.interval,
- target = this.target,
- canvasHeight = this.canvasHeight,
- lineHeight = this.lineHeight,
- pheight = canvasHeight - lineHeight,
- ytop, val, color, x;
-
- val = clipval(values[valuenum], min, max);
- x = valuenum * interval;
- ytop = Math.round(pheight - pheight * ((val - min) / range));
- color = (options.get('thresholdColor') && val < options.get('thresholdValue')) ? options.get('thresholdColor') : options.get('lineColor');
- if (highlight) {
- color = this.calcHighlightColor(color, options);
- }
- return target.drawLine(x, ytop, x, ytop + lineHeight, color);
- }
- });
-
- /**
- * Bullet charts
- */
- $.fn.sparkline.bullet = bullet = createClass($.fn.sparkline._base, {
- type: 'bullet',
-
- init: function (el, values, options, width, height) {
- var min, max, vals;
- bullet._super.init.call(this, el, values, options, width, height);
-
- // values: target, performance, range1, range2, range3
- this.values = values = normalizeValues(values);
- // target or performance could be null
- vals = values.slice();
- vals[0] = vals[0] === null ? vals[2] : vals[0];
- vals[1] = values[1] === null ? vals[2] : vals[1];
- min = Math.min.apply(Math, values);
- max = Math.max.apply(Math, values);
- if (options.get('base') === undefined) {
- min = min < 0 ? min : 0;
- } else {
- min = options.get('base');
- }
- this.min = min;
- this.max = max;
- this.range = max - min;
- this.shapes = {};
- this.valueShapes = {};
- this.regiondata = {};
- this.width = width = options.get('width') === 'auto' ? '4.0em' : width;
- this.target = this.$el.simpledraw(width, height, options.get('composite'));
- if (!values.length) {
- this.disabled = true;
- }
- this.initTarget();
- },
-
- getRegion: function (el, x, y) {
- var shapeid = this.target.getShapeAt(el, x, y);
- return (shapeid !== undefined && this.shapes[shapeid] !== undefined) ? this.shapes[shapeid] : undefined;
- },
-
- getCurrentRegionFields: function () {
- var currentRegion = this.currentRegion;
- return {
- fieldkey: currentRegion.substr(0, 1),
- value: this.values[currentRegion.substr(1)],
- region: currentRegion
- };
- },
-
- changeHighlight: function (highlight) {
- var currentRegion = this.currentRegion,
- shapeid = this.valueShapes[currentRegion],
- shape;
- delete this.shapes[shapeid];
- switch (currentRegion.substr(0, 1)) {
- case 'r':
- shape = this.renderRange(currentRegion.substr(1), highlight);
- break;
- case 'p':
- shape = this.renderPerformance(highlight);
- break;
- case 't':
- shape = this.renderTarget(highlight);
- break;
- }
- this.valueShapes[currentRegion] = shape.id;
- this.shapes[shape.id] = currentRegion;
- this.target.replaceWithShape(shapeid, shape);
- },
-
- renderRange: function (rn, highlight) {
- var rangeval = this.values[rn],
- rangewidth = Math.round(this.canvasWidth * ((rangeval - this.min) / this.range)),
- color = this.options.get('rangeColors')[rn - 2];
- if (highlight) {
- color = this.calcHighlightColor(color, this.options);
- }
- return this.target.drawRect(0, 0, rangewidth - 1, this.canvasHeight - 1, color, color);
- },
-
- renderPerformance: function (highlight) {
- var perfval = this.values[1],
- perfwidth = Math.round(this.canvasWidth * ((perfval - this.min) / this.range)),
- color = this.options.get('performanceColor');
- if (highlight) {
- color = this.calcHighlightColor(color, this.options);
- }
- return this.target.drawRect(0, Math.round(this.canvasHeight * 0.3), perfwidth - 1,
- Math.round(this.canvasHeight * 0.4) - 1, color, color);
- },
-
- renderTarget: function (highlight) {
- var targetval = this.values[0],
- x = Math.round(this.canvasWidth * ((targetval - this.min) / this.range) - (this.options.get('targetWidth') / 2)),
- targettop = Math.round(this.canvasHeight * 0.10),
- targetheight = this.canvasHeight - (targettop * 2),
- color = this.options.get('targetColor');
- if (highlight) {
- color = this.calcHighlightColor(color, this.options);
- }
- return this.target.drawRect(x, targettop, this.options.get('targetWidth') - 1, targetheight - 1, color, color);
- },
-
- render: function () {
- var vlen = this.values.length,
- target = this.target,
- i, shape;
- if (!bullet._super.render.call(this)) {
- return;
- }
- for (i = 2; i < vlen; i++) {
- shape = this.renderRange(i).append();
- this.shapes[shape.id] = 'r' + i;
- this.valueShapes['r' + i] = shape.id;
- }
- if (this.values[1] !== null) {
- shape = this.renderPerformance().append();
- this.shapes[shape.id] = 'p1';
- this.valueShapes.p1 = shape.id;
- }
- if (this.values[0] !== null) {
- shape = this.renderTarget().append();
- this.shapes[shape.id] = 't0';
- this.valueShapes.t0 = shape.id;
- }
- target.render();
- }
- });
-
- /**
- * Pie charts
- */
- $.fn.sparkline.pie = pie = createClass($.fn.sparkline._base, {
- type: 'pie',
-
- init: function (el, values, options, width, height) {
- var total = 0, i;
-
- pie._super.init.call(this, el, values, options, width, height);
-
- this.shapes = {}; // map shape ids to value offsets
- this.valueShapes = {}; // maps value offsets to shape ids
- this.values = values = $.map(values, Number);
-
- if (options.get('width') === 'auto') {
- this.width = this.height;
- }
-
- if (values.length > 0) {
- for (i = values.length; i--;) {
- total += values[i];
- }
- }
- this.total = total;
- this.initTarget();
- this.radius = Math.floor(Math.min(this.canvasWidth, this.canvasHeight) / 2);
- },
-
- getRegion: function (el, x, y) {
- var shapeid = this.target.getShapeAt(el, x, y);
- return (shapeid !== undefined && this.shapes[shapeid] !== undefined) ? this.shapes[shapeid] : undefined;
- },
-
- getCurrentRegionFields: function () {
- var currentRegion = this.currentRegion;
- return {
- isNull: this.values[currentRegion] === undefined,
- value: this.values[currentRegion],
- percent: this.values[currentRegion] / this.total * 100,
- color: this.options.get('sliceColors')[currentRegion % this.options.get('sliceColors').length],
- offset: currentRegion
- };
- },
-
- changeHighlight: function (highlight) {
- var currentRegion = this.currentRegion,
- newslice = this.renderSlice(currentRegion, highlight),
- shapeid = this.valueShapes[currentRegion];
- delete this.shapes[shapeid];
- this.target.replaceWithShape(shapeid, newslice);
- this.valueShapes[currentRegion] = newslice.id;
- this.shapes[newslice.id] = currentRegion;
- },
-
- renderSlice: function (valuenum, highlight) {
- var target = this.target,
- options = this.options,
- radius = this.radius,
- borderWidth = options.get('borderWidth'),
- offset = options.get('offset'),
- circle = 2 * Math.PI,
- values = this.values,
- total = this.total,
- next = offset ? (2*Math.PI)*(offset/360) : 0,
- start, end, i, vlen, color;
-
- vlen = values.length;
- for (i = 0; i < vlen; i++) {
- start = next;
- end = next;
- if (total > 0) { // avoid divide by zero
- end = next + (circle * (values[i] / total));
- }
- if (valuenum === i) {
- color = options.get('sliceColors')[i % options.get('sliceColors').length];
- if (highlight) {
- color = this.calcHighlightColor(color, options);
- }
-
- return target.drawPieSlice(radius, radius, radius - borderWidth, start, end, undefined, color);
- }
- next = end;
- }
- },
-
- render: function () {
- var target = this.target,
- values = this.values,
- options = this.options,
- radius = this.radius,
- borderWidth = options.get('borderWidth'),
- shape, i;
-
- if (!pie._super.render.call(this)) {
- return;
- }
- if (borderWidth) {
- target.drawCircle(radius, radius, Math.floor(radius - (borderWidth / 2)),
- options.get('borderColor'), undefined, borderWidth).append();
- }
- for (i = values.length; i--;) {
- if (values[i]) { // don't render zero values
- shape = this.renderSlice(i).append();
- this.valueShapes[i] = shape.id; // store just the shapeid
- this.shapes[shape.id] = i;
- }
- }
- target.render();
- }
- });
-
- /**
- * Box plots
- */
- $.fn.sparkline.box = box = createClass($.fn.sparkline._base, {
- type: 'box',
-
- init: function (el, values, options, width, height) {
- box._super.init.call(this, el, values, options, width, height);
- this.values = $.map(values, Number);
- this.width = options.get('width') === 'auto' ? '4.0em' : width;
- this.initTarget();
- if (!this.values.length) {
- this.disabled = 1;
- }
- },
-
- /**
- * Simulate a single region
- */
- getRegion: function () {
- return 1;
- },
-
- getCurrentRegionFields: function () {
- var result = [
- { field: 'lq', value: this.quartiles[0] },
- { field: 'med', value: this.quartiles[1] },
- { field: 'uq', value: this.quartiles[2] }
- ];
- if (this.loutlier !== undefined) {
- result.push({ field: 'lo', value: this.loutlier});
- }
- if (this.routlier !== undefined) {
- result.push({ field: 'ro', value: this.routlier});
- }
- if (this.lwhisker !== undefined) {
- result.push({ field: 'lw', value: this.lwhisker});
- }
- if (this.rwhisker !== undefined) {
- result.push({ field: 'rw', value: this.rwhisker});
- }
- return result;
- },
-
- render: function () {
- var target = this.target,
- values = this.values,
- vlen = values.length,
- options = this.options,
- canvasWidth = this.canvasWidth,
- canvasHeight = this.canvasHeight,
- minValue = options.get('chartRangeMin') === undefined ? Math.min.apply(Math, values) : options.get('chartRangeMin'),
- maxValue = options.get('chartRangeMax') === undefined ? Math.max.apply(Math, values) : options.get('chartRangeMax'),
- canvasLeft = 0,
- lwhisker, loutlier, iqr, q1, q2, q3, rwhisker, routlier, i,
- size, unitSize;
-
- if (!box._super.render.call(this)) {
- return;
- }
-
- if (options.get('raw')) {
- if (options.get('showOutliers') && values.length > 5) {
- loutlier = values[0];
- lwhisker = values[1];
- q1 = values[2];
- q2 = values[3];
- q3 = values[4];
- rwhisker = values[5];
- routlier = values[6];
- } else {
- lwhisker = values[0];
- q1 = values[1];
- q2 = values[2];
- q3 = values[3];
- rwhisker = values[4];
- }
- } else {
- values.sort(function (a, b) { return a - b; });
- q1 = quartile(values, 1);
- q2 = quartile(values, 2);
- q3 = quartile(values, 3);
- iqr = q3 - q1;
- if (options.get('showOutliers')) {
- lwhisker = rwhisker = undefined;
- for (i = 0; i < vlen; i++) {
- if (lwhisker === undefined && values[i] > q1 - (iqr * options.get('outlierIQR'))) {
- lwhisker = values[i];
- }
- if (values[i] < q3 + (iqr * options.get('outlierIQR'))) {
- rwhisker = values[i];
- }
- }
- loutlier = values[0];
- routlier = values[vlen - 1];
- } else {
- lwhisker = values[0];
- rwhisker = values[vlen - 1];
- }
- }
- this.quartiles = [q1, q2, q3];
- this.lwhisker = lwhisker;
- this.rwhisker = rwhisker;
- this.loutlier = loutlier;
- this.routlier = routlier;
-
- unitSize = canvasWidth / (maxValue - minValue + 1);
- if (options.get('showOutliers')) {
- canvasLeft = Math.ceil(options.get('spotRadius'));
- canvasWidth -= 2 * Math.ceil(options.get('spotRadius'));
- unitSize = canvasWidth / (maxValue - minValue + 1);
- if (loutlier < lwhisker) {
- target.drawCircle((loutlier - minValue) * unitSize + canvasLeft,
- canvasHeight / 2,
- options.get('spotRadius'),
- options.get('outlierLineColor'),
- options.get('outlierFillColor')).append();
- }
- if (routlier > rwhisker) {
- target.drawCircle((routlier - minValue) * unitSize + canvasLeft,
- canvasHeight / 2,
- options.get('spotRadius'),
- options.get('outlierLineColor'),
- options.get('outlierFillColor')).append();
- }
- }
-
- // box
- target.drawRect(
- Math.round((q1 - minValue) * unitSize + canvasLeft),
- Math.round(canvasHeight * 0.1),
- Math.round((q3 - q1) * unitSize),
- Math.round(canvasHeight * 0.8),
- options.get('boxLineColor'),
- options.get('boxFillColor')).append();
- // left whisker
- target.drawLine(
- Math.round((lwhisker - minValue) * unitSize + canvasLeft),
- Math.round(canvasHeight / 2),
- Math.round((q1 - minValue) * unitSize + canvasLeft),
- Math.round(canvasHeight / 2),
- options.get('lineColor')).append();
- target.drawLine(
- Math.round((lwhisker - minValue) * unitSize + canvasLeft),
- Math.round(canvasHeight / 4),
- Math.round((lwhisker - minValue) * unitSize + canvasLeft),
- Math.round(canvasHeight - canvasHeight / 4),
- options.get('whiskerColor')).append();
- // right whisker
- target.drawLine(Math.round((rwhisker - minValue) * unitSize + canvasLeft),
- Math.round(canvasHeight / 2),
- Math.round((q3 - minValue) * unitSize + canvasLeft),
- Math.round(canvasHeight / 2),
- options.get('lineColor')).append();
- target.drawLine(
- Math.round((rwhisker - minValue) * unitSize + canvasLeft),
- Math.round(canvasHeight / 4),
- Math.round((rwhisker - minValue) * unitSize + canvasLeft),
- Math.round(canvasHeight - canvasHeight / 4),
- options.get('whiskerColor')).append();
- // median line
- target.drawLine(
- Math.round((q2 - minValue) * unitSize + canvasLeft),
- Math.round(canvasHeight * 0.1),
- Math.round((q2 - minValue) * unitSize + canvasLeft),
- Math.round(canvasHeight * 0.9),
- options.get('medianColor')).append();
- if (options.get('target')) {
- size = Math.ceil(options.get('spotRadius'));
- target.drawLine(
- Math.round((options.get('target') - minValue) * unitSize + canvasLeft),
- Math.round((canvasHeight / 2) - size),
- Math.round((options.get('target') - minValue) * unitSize + canvasLeft),
- Math.round((canvasHeight / 2) + size),
- options.get('targetColor')).append();
- target.drawLine(
- Math.round((options.get('target') - minValue) * unitSize + canvasLeft - size),
- Math.round(canvasHeight / 2),
- Math.round((options.get('target') - minValue) * unitSize + canvasLeft + size),
- Math.round(canvasHeight / 2),
- options.get('targetColor')).append();
- }
- target.render();
- }
- });
-
- // Setup a very simple "virtual canvas" to make drawing the few shapes we need easier
- // This is accessible as $(foo).simpledraw()
-
- VShape = createClass({
- init: function (target, id, type, args) {
- this.target = target;
- this.id = id;
- this.type = type;
- this.args = args;
- },
- append: function () {
- this.target.appendShape(this);
- return this;
- }
- });
-
- VCanvas_base = createClass({
- _pxregex: /(\d+)(px)?\s*$/i,
-
- init: function (width, height, target) {
- if (!width) {
- return;
- }
- this.width = width;
- this.height = height;
- this.target = target;
- this.lastShapeId = null;
- if (target[0]) {
- target = target[0];
- }
- $.data(target, '_jqs_vcanvas', this);
- },
-
- drawLine: function (x1, y1, x2, y2, lineColor, lineWidth) {
- return this.drawShape([[x1, y1], [x2, y2]], lineColor, lineWidth);
- },
-
- drawShape: function (path, lineColor, fillColor, lineWidth) {
- return this._genShape('Shape', [path, lineColor, fillColor, lineWidth]);
- },
-
- drawCircle: function (x, y, radius, lineColor, fillColor, lineWidth) {
- return this._genShape('Circle', [x, y, radius, lineColor, fillColor, lineWidth]);
- },
-
- drawPieSlice: function (x, y, radius, startAngle, endAngle, lineColor, fillColor) {
- return this._genShape('PieSlice', [x, y, radius, startAngle, endAngle, lineColor, fillColor]);
- },
-
- drawRect: function (x, y, width, height, lineColor, fillColor) {
- return this._genShape('Rect', [x, y, width, height, lineColor, fillColor]);
- },
-
- getElement: function () {
- return this.canvas;
- },
-
- /**
- * Return the most recently inserted shape id
- */
- getLastShapeId: function () {
- return this.lastShapeId;
- },
-
- /**
- * Clear and reset the canvas
- */
- reset: function () {
- alert('reset not implemented');
- },
-
- _insert: function (el, target) {
- $(target).html(el);
- },
-
- /**
- * Calculate the pixel dimensions of the canvas
- */
- _calculatePixelDims: function (width, height, canvas) {
- // XXX This should probably be a configurable option
- var match;
- match = this._pxregex.exec(height);
- if (match) {
- this.pixelHeight = match[1];
- } else {
- this.pixelHeight = $(canvas).height();
- }
- match = this._pxregex.exec(width);
- if (match) {
- this.pixelWidth = match[1];
- } else {
- this.pixelWidth = $(canvas).width();
- }
- },
-
- /**
- * Generate a shape object and id for later rendering
- */
- _genShape: function (shapetype, shapeargs) {
- var id = shapeCount++;
- shapeargs.unshift(id);
- return new VShape(this, id, shapetype, shapeargs);
- },
-
- /**
- * Add a shape to the end of the render queue
- */
- appendShape: function (shape) {
- alert('appendShape not implemented');
- },
-
- /**
- * Replace one shape with another
- */
- replaceWithShape: function (shapeid, shape) {
- alert('replaceWithShape not implemented');
- },
-
- /**
- * Insert one shape after another in the render queue
- */
- insertAfterShape: function (shapeid, shape) {
- alert('insertAfterShape not implemented');
- },
-
- /**
- * Remove a shape from the queue
- */
- removeShapeId: function (shapeid) {
- alert('removeShapeId not implemented');
- },
-
- /**
- * Find a shape at the specified x/y co-ordinates
- */
- getShapeAt: function (el, x, y) {
- alert('getShapeAt not implemented');
- },
-
- /**
- * Render all queued shapes onto the canvas
- */
- render: function () {
- alert('render not implemented');
- }
- });
-
- VCanvas_canvas = createClass(VCanvas_base, {
- init: function (width, height, target, interact) {
- VCanvas_canvas._super.init.call(this, width, height, target);
- this.canvas = document.createElement('canvas');
- if (target[0]) {
- target = target[0];
- }
- $.data(target, '_jqs_vcanvas', this);
- $(this.canvas).css({ display: 'inline-block', width: width, height: height, verticalAlign: 'top' });
- this._insert(this.canvas, target);
- this._calculatePixelDims(width, height, this.canvas);
- this.canvas.width = this.pixelWidth;
- this.canvas.height = this.pixelHeight;
- this.interact = interact;
- this.shapes = {};
- this.shapeseq = [];
- this.currentTargetShapeId = undefined;
- $(this.canvas).css({width: this.pixelWidth, height: this.pixelHeight});
- },
-
- _getContext: function (lineColor, fillColor, lineWidth) {
- var context = this.canvas.getContext('2d');
- if (lineColor !== undefined) {
- context.strokeStyle = lineColor;
- }
- context.lineWidth = lineWidth === undefined ? 1 : lineWidth;
- if (fillColor !== undefined) {
- context.fillStyle = fillColor;
- }
- return context;
- },
-
- reset: function () {
- var context = this._getContext();
- context.clearRect(0, 0, this.pixelWidth, this.pixelHeight);
- this.shapes = {};
- this.shapeseq = [];
- this.currentTargetShapeId = undefined;
- },
-
- _drawShape: function (shapeid, path, lineColor, fillColor, lineWidth) {
- var context = this._getContext(lineColor, fillColor, lineWidth),
- i, plen;
- context.beginPath();
- context.moveTo(path[0][0] + 0.5, path[0][1] + 0.5);
- for (i = 1, plen = path.length; i < plen; i++) {
- context.lineTo(path[i][0] + 0.5, path[i][1] + 0.5); // the 0.5 offset gives us crisp pixel-width lines
- }
- if (lineColor !== undefined) {
- context.stroke();
- }
- if (fillColor !== undefined) {
- context.fill();
- }
- if (this.targetX !== undefined && this.targetY !== undefined &&
- context.isPointInPath(this.targetX, this.targetY)) {
- this.currentTargetShapeId = shapeid;
- }
- },
-
- _drawCircle: function (shapeid, x, y, radius, lineColor, fillColor, lineWidth) {
- var context = this._getContext(lineColor, fillColor, lineWidth);
- context.beginPath();
- context.arc(x, y, radius, 0, 2 * Math.PI, false);
- if (this.targetX !== undefined && this.targetY !== undefined &&
- context.isPointInPath(this.targetX, this.targetY)) {
- this.currentTargetShapeId = shapeid;
- }
- if (lineColor !== undefined) {
- context.stroke();
- }
- if (fillColor !== undefined) {
- context.fill();
- }
- },
-
- _drawPieSlice: function (shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) {
- var context = this._getContext(lineColor, fillColor);
- context.beginPath();
- context.moveTo(x, y);
- context.arc(x, y, radius, startAngle, endAngle, false);
- context.lineTo(x, y);
- context.closePath();
- if (lineColor !== undefined) {
- context.stroke();
- }
- if (fillColor) {
- context.fill();
- }
- if (this.targetX !== undefined && this.targetY !== undefined &&
- context.isPointInPath(this.targetX, this.targetY)) {
- this.currentTargetShapeId = shapeid;
- }
- },
-
- _drawRect: function (shapeid, x, y, width, height, lineColor, fillColor) {
- return this._drawShape(shapeid, [[x, y], [x + width, y], [x + width, y + height], [x, y + height], [x, y]], lineColor, fillColor);
- },
-
- appendShape: function (shape) {
- this.shapes[shape.id] = shape;
- this.shapeseq.push(shape.id);
- this.lastShapeId = shape.id;
- return shape.id;
- },
-
- replaceWithShape: function (shapeid, shape) {
- var shapeseq = this.shapeseq,
- i;
- this.shapes[shape.id] = shape;
- for (i = shapeseq.length; i--;) {
- if (shapeseq[i] == shapeid) {
- shapeseq[i] = shape.id;
- }
- }
- delete this.shapes[shapeid];
- },
-
- replaceWithShapes: function (shapeids, shapes) {
- var shapeseq = this.shapeseq,
- shapemap = {},
- sid, i, first;
-
- for (i = shapeids.length; i--;) {
- shapemap[shapeids[i]] = true;
- }
- for (i = shapeseq.length; i--;) {
- sid = shapeseq[i];
- if (shapemap[sid]) {
- shapeseq.splice(i, 1);
- delete this.shapes[sid];
- first = i;
- }
- }
- for (i = shapes.length; i--;) {
- shapeseq.splice(first, 0, shapes[i].id);
- this.shapes[shapes[i].id] = shapes[i];
- }
-
- },
-
- insertAfterShape: function (shapeid, shape) {
- var shapeseq = this.shapeseq,
- i;
- for (i = shapeseq.length; i--;) {
- if (shapeseq[i] === shapeid) {
- shapeseq.splice(i + 1, 0, shape.id);
- this.shapes[shape.id] = shape;
- return;
- }
- }
- },
-
- removeShapeId: function (shapeid) {
- var shapeseq = this.shapeseq,
- i;
- for (i = shapeseq.length; i--;) {
- if (shapeseq[i] === shapeid) {
- shapeseq.splice(i, 1);
- break;
- }
- }
- delete this.shapes[shapeid];
- },
-
- getShapeAt: function (el, x, y) {
- this.targetX = x;
- this.targetY = y;
- this.render();
- return this.currentTargetShapeId;
- },
-
- render: function () {
- var shapeseq = this.shapeseq,
- shapes = this.shapes,
- shapeCount = shapeseq.length,
- context = this._getContext(),
- shapeid, shape, i;
- context.clearRect(0, 0, this.pixelWidth, this.pixelHeight);
- for (i = 0; i < shapeCount; i++) {
- shapeid = shapeseq[i];
- shape = shapes[shapeid];
- this['_draw' + shape.type].apply(this, shape.args);
- }
- if (!this.interact) {
- // not interactive so no need to keep the shapes array
- this.shapes = {};
- this.shapeseq = [];
- }
- }
-
- });
-
- VCanvas_vml = createClass(VCanvas_base, {
- init: function (width, height, target) {
- var groupel;
- VCanvas_vml._super.init.call(this, width, height, target);
- if (target[0]) {
- target = target[0];
- }
- $.data(target, '_jqs_vcanvas', this);
- this.canvas = document.createElement('span');
- $(this.canvas).css({ display: 'inline-block', position: 'relative', overflow: 'hidden', width: width, height: height, margin: '0px', padding: '0px', verticalAlign: 'top'});
- this._insert(this.canvas, target);
- this._calculatePixelDims(width, height, this.canvas);
- this.canvas.width = this.pixelWidth;
- this.canvas.height = this.pixelHeight;
- groupel = '';
- this.canvas.insertAdjacentHTML('beforeEnd', groupel);
- this.group = $(this.canvas).children()[0];
- this.rendered = false;
- this.prerender = '';
- },
-
- _drawShape: function (shapeid, path, lineColor, fillColor, lineWidth) {
- var vpath = [],
- initial, stroke, fill, closed, vel, plen, i;
- for (i = 0, plen = path.length; i < plen; i++) {
- vpath[i] = '' + (path[i][0]) + ',' + (path[i][1]);
- }
- initial = vpath.splice(0, 1);
- lineWidth = lineWidth === undefined ? 1 : lineWidth;
- stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" ';
- fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
- closed = vpath[0] === vpath[vpath.length - 1] ? 'x ' : '';
- vel = '' +
- ' ';
- return vel;
- },
-
- _drawCircle: function (shapeid, x, y, radius, lineColor, fillColor, lineWidth) {
- var stroke, fill, vel;
- x -= radius;
- y -= radius;
- stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" ';
- fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
- vel = '';
- return vel;
-
- },
-
- _drawPieSlice: function (shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) {
- var vpath, startx, starty, endx, endy, stroke, fill, vel;
- if (startAngle === endAngle) {
- return ''; // VML seems to have problem when start angle equals end angle.
- }
- if ((endAngle - startAngle) === (2 * Math.PI)) {
- startAngle = 0.0; // VML seems to have a problem when drawing a full circle that doesn't start 0
- endAngle = (2 * Math.PI);
- }
-
- startx = x + Math.round(Math.cos(startAngle) * radius);
- starty = y + Math.round(Math.sin(startAngle) * radius);
- endx = x + Math.round(Math.cos(endAngle) * radius);
- endy = y + Math.round(Math.sin(endAngle) * radius);
-
- if (startx === endx && starty === endy) {
- if ((endAngle - startAngle) < Math.PI) {
- // Prevent very small slices from being mistaken as a whole pie
- return '';
- }
- // essentially going to be the entire circle, so ignore startAngle
- startx = endx = x + radius;
- starty = endy = y;
- }
-
- if (startx === endx && starty === endy && (endAngle - startAngle) < Math.PI) {
- return '';
- }
-
- vpath = [x - radius, y - radius, x + radius, y + radius, startx, starty, endx, endy];
- stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="1px" strokeColor="' + lineColor + '" ';
- fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
- vel = '' +
- ' ';
- return vel;
- },
-
- _drawRect: function (shapeid, x, y, width, height, lineColor, fillColor) {
- return this._drawShape(shapeid, [[x, y], [x, y + height], [x + width, y + height], [x + width, y], [x, y]], lineColor, fillColor);
- },
-
- reset: function () {
- this.group.innerHTML = '';
- },
-
- appendShape: function (shape) {
- var vel = this['_draw' + shape.type].apply(this, shape.args);
- if (this.rendered) {
- this.group.insertAdjacentHTML('beforeEnd', vel);
- } else {
- this.prerender += vel;
- }
- this.lastShapeId = shape.id;
- return shape.id;
- },
-
- replaceWithShape: function (shapeid, shape) {
- var existing = $('#jqsshape' + shapeid),
- vel = this['_draw' + shape.type].apply(this, shape.args);
- existing[0].outerHTML = vel;
- },
-
- replaceWithShapes: function (shapeids, shapes) {
- // replace the first shapeid with all the new shapes then toast the remaining old shapes
- var existing = $('#jqsshape' + shapeids[0]),
- replace = '',
- slen = shapes.length,
- i;
- for (i = 0; i < slen; i++) {
- replace += this['_draw' + shapes[i].type].apply(this, shapes[i].args);
- }
- existing[0].outerHTML = replace;
- for (i = 1; i < shapeids.length; i++) {
- $('#jqsshape' + shapeids[i]).remove();
- }
- },
-
- insertAfterShape: function (shapeid, shape) {
- var existing = $('#jqsshape' + shapeid),
- vel = this['_draw' + shape.type].apply(this, shape.args);
- existing[0].insertAdjacentHTML('afterEnd', vel);
- },
-
- removeShapeId: function (shapeid) {
- var existing = $('#jqsshape' + shapeid);
- this.group.removeChild(existing[0]);
- },
-
- getShapeAt: function (el, x, y) {
- var shapeid = el.id.substr(8);
- return shapeid;
- },
-
- render: function () {
- if (!this.rendered) {
- // batch the intial render into a single repaint
- this.group.innerHTML = this.prerender;
- this.rendered = true;
- }
- }
- });
-
-}))}(document, Math));
\ No newline at end of file
diff --git a/addons/web/static/lib/justgage/justgage.js b/addons/web/static/lib/justgage/justgage.js
deleted file mode 100644
index 24540beb293..00000000000
--- a/addons/web/static/lib/justgage/justgage.js
+++ /dev/null
@@ -1,946 +0,0 @@
-/**
- * JustGage - this is work-in-progress, unreleased, unofficial code, so it might not work top-notch :)
- * Check http://www.justgage.com for official releases
- * Licensed under MIT.
- * @author Bojan Djuricic (@Toorshia)
- *
- * LATEST UPDATES
-
- * -----------------------------
- * April 18, 2013.
- * -----------------------------
- * parentNode - use this instead of id, to attach gauge to node which is outside of DOM tree - https://github.com/toorshia/justgage/issues/48
- * width - force gauge width
- * height - force gauge height
-
- * -----------------------------
- * April 17, 2013.
- * -----------------------------
- * fix - https://github.com/toorshia/justgage/issues/49
-
- * -----------------------------
- * April 01, 2013.
- * -----------------------------
- * fix - https://github.com/toorshia/justgage/issues/46
-
- * -----------------------------
- * March 26, 2013.
- * -----------------------------
- * customSectors - define specific color for value range (0-10 : red, 10-30 : blue etc.)
-
- * -----------------------------
- * March 23, 2013.
- * -----------------------------
- * counter - option to animate value in counting fashion
- * fix - https://github.com/toorshia/justgage/issues/45
-
- * -----------------------------
- * March 13, 2013.
- * -----------------------------
- * refresh method - added optional 'max' parameter to use when you need to update max value
-
- * -----------------------------
- * February 26, 2013.
- * -----------------------------
- * decimals - option to define/limit number of decimals when not using humanFriendly or customRenderer to display value
- * fixed a missing parameters bug when calling generateShadow() for IE < 9
-
- * -----------------------------
- * December 31, 2012.
- * -----------------------------
- * fixed text y-position for hidden divs - workaround for Raphael 'dy' bug - https://github.com/DmitryBaranovskiy/raphael/issues/491
- * 'show' parameters, like showMinMax are now 'hide' because I am lame developer - please update these in your setups
- * Min and Max labels are now auto-off when in donut mode
- * Start angle in donut mode is now 90
- * donutStartAngle - option to define start angle for donut
-
- * -----------------------------
- * November 25, 2012.
- * -----------------------------
- * Option to define custom rendering function for displayed value
-
- * -----------------------------
- * November 19, 2012.
- * -----------------------------
- * Config.value is now updated after gauge refresh
-
- * -----------------------------
- * November 13, 2012.
- * -----------------------------
- * Donut display mode added
- * Option to hide value label
- * Option to enable responsive gauge size
- * Removed default title attribute
- * Option to accept min and max defined as string values
- * Option to configure value symbol
- * Fixed bad aspect ratio calculations
- * Option to configure minimum font size for all texts
- * Option to show shorthand big numbers (human friendly)
- */
-
- JustGage = function(config) {
-
- // if (!config.id) {alert("Missing id parameter for gauge!"); return false;}
- // if (!document.getElementById(config.id)) {alert("No element with id: \""+config.id+"\" found!"); return false;}
-
- var obj = this;
-
- // configurable parameters
- obj.config =
- {
- // id : string
- // this is container element id
- id : config.id,
-
- // parentNode : node object
- // this is container element
- parentNode : (config.parentNode) ? config.parentNode : null,
-
- // width : int
- // gauge width
- width : (config.width) ? config.width : null,
-
- // height : int
- // gauge height
- height : (config.height) ? config.height : null,
-
- // title : string
- // gauge title
- title : (config.title) ? config.title : "",
-
- // titleFontColor : string
- // color of gauge title
- titleFontColor : (config.titleFontColor) ? config.titleFontColor : "#999999",
-
- // value : int
- // value gauge is showing
- value : (config.value) ? config.value : 0,
-
- // valueFontColor : string
- // color of label showing current value
- valueFontColor : (config.valueFontColor) ? config.valueFontColor : "#010101",
-
- // symbol : string
- // special symbol to show next to value
- symbol : (config.symbol) ? config.symbol : "",
-
- // min : int
- // min value
- min : (config.min !== undefined) ? parseFloat(config.min) : 0,
-
- // max : int
- // max value
- max : (config.max !== undefined) ? parseFloat(config.max) : 100,
-
- // humanFriendlyDecimal : int
- // number of decimal places for our human friendly number to contain
- humanFriendlyDecimal : (config.humanFriendlyDecimal) ? config.humanFriendlyDecimal : 0,
-
- // textRenderer: func
- // function applied before rendering text
- textRenderer : (config.textRenderer) ? config.textRenderer : null,
-
- // gaugeWidthScale : float
- // width of the gauge element
- gaugeWidthScale : (config.gaugeWidthScale) ? config.gaugeWidthScale : 1.0,
-
- // gaugeColor : string
- // background color of gauge element
- gaugeColor : (config.gaugeColor) ? config.gaugeColor : "#edebeb",
-
- // label : string
- // text to show below value
- label : (config.label) ? config.label : "",
-
- // labelFontColor : string
- // color of label showing label under value
- labelFontColor : (config.labelFontColor) ? config.labelFontColor : "#b3b3b3",
-
- // shadowOpacity : int
- // 0 ~ 1
- shadowOpacity : (config.shadowOpacity) ? config.shadowOpacity : 0.2,
-
- // shadowSize: int
- // inner shadow size
- shadowSize : (config.shadowSize) ? config.shadowSize : 5,
-
- // shadowVerticalOffset : int
- // how much shadow is offset from top
- shadowVerticalOffset : (config.shadowVerticalOffset) ? config.shadowVerticalOffset : 3,
-
- // levelColors : string[]
- // colors of indicator, from lower to upper, in RGB format
- levelColors : (config.levelColors) ? config.levelColors : [
- "#a9d70b",
- "#f9c802",
- "#ff0000"
- ],
-
- // startAnimationTime : int
- // length of initial animation
- startAnimationTime : (config.startAnimationTime) ? config.startAnimationTime : 700,
-
- // startAnimationType : string
- // type of initial animation (linear, >, <, <>, bounce)
- startAnimationType : (config.startAnimationType) ? config.startAnimationType : ">",
-
- // refreshAnimationTime : int
- // length of refresh animation
- refreshAnimationTime : (config.refreshAnimationTime) ? config.refreshAnimationTime : 700,
-
- // refreshAnimationType : string
- // type of refresh animation (linear, >, <, <>, bounce)
- refreshAnimationType : (config.refreshAnimationType) ? config.refreshAnimationType : ">",
-
- // donutStartAngle : int
- // angle to start from when in donut mode
- donutStartAngle : (config.donutStartAngle) ? config.donutStartAngle : 90,
-
- // valueMinFontSize : int
- // absolute minimum font size for the value
- valueMinFontSize : config.valueMinFontSize || 16,
-
- // titleMinFontSize
- // absolute minimum font size for the title
- titleMinFontSize : config.titleMinFontSize || 10,
-
- // labelMinFontSize
- // absolute minimum font size for the label
- labelMinFontSize : config.labelMinFontSize || 10,
-
- // minLabelMinFontSize
- // absolute minimum font size for the minimum label
- minLabelMinFontSize : config.minLabelMinFontSize || 10,
-
- // maxLabelMinFontSize
- // absolute minimum font size for the maximum label
- maxLabelMinFontSize : config.maxLabelMinFontSize || 10,
-
- // hideValue : bool
- // hide value text
- hideValue : (config.hideValue) ? config.hideValue : false,
-
- // hideMinMax : bool
- // hide min and max values
- hideMinMax : (config.hideMinMax) ? config.hideMinMax : false,
-
- // hideInnerShadow : bool
- // hide inner shadow
- hideInnerShadow : (config.hideInnerShadow) ? config.hideInnerShadow : false,
-
- // humanFriendly : bool
- // convert large numbers for min, max, value to human friendly (e.g. 1234567 -> 1.23M)
- humanFriendly : (config.humanFriendly) ? config.humanFriendly : false,
-
- // noGradient : bool
- // whether to use gradual color change for value, or sector-based
- noGradient : (config.noGradient) ? config.noGradient : false,
-
- // donut : bool
- // show full donut gauge
- donut : (config.donut) ? config.donut : false,
-
- // relativeGaugeSize : bool
- // whether gauge size should follow changes in container element size
- relativeGaugeSize : (config.relativeGaugeSize) ? config.relativeGaugeSize : false,
-
- // counter : bool
- // animate level number change
- counter : (config.counter) ? config.counter : false,
-
- // decimals : int
- // number of digits after floating point
- decimals : (config.decimals) ? config.decimals : 0,
-
- // customSectors : [] of objects
- // number of digits after floating point
- customSectors : (config.customSectors) ? config.customSectors : []
- };
-
- // variables
- var
- canvasW,
- canvasH,
- widgetW,
- widgetH,
- aspect,
- dx,
- dy,
- titleFontSize,
- titleX,
- titleY,
- valueFontSize,
- valueX,
- valueY,
- labelFontSize,
- labelX,
- labelY,
- minFontSize,
- minX,
- minY,
- maxFontSize,
- maxX,
- maxY;
-
- // overflow values
- if (obj.config.value > obj.config.max) obj.config.value = obj.config.max;
- if (obj.config.value < obj.config.min) obj.config.value = obj.config.min;
- obj.originalValue = config.value;
-
- // create canvas
- if (obj.config.id !== null && (document.getElementById(obj.config.id)) !== null) {
- obj.canvas = Raphael(obj.config.id, "100%", "100%");
- } else if (obj.config.parentNode !== null) {
- obj.canvas = Raphael(obj.config.parentNode, "100%", "100%");
- }
-
- if (obj.config.relativeGaugeSize === true) {
- obj.canvas.setViewBox(0, 0, 200, 150, true);
- }
-
- // canvas dimensions
- if (obj.config.relativeGaugeSize === true) {
- canvasW = 200;
- canvasH = 150;
- } else if (obj.config.width !== null && obj.config.height !== null) {
- canvasW = obj.config.width;
- canvasH = obj.config.height;
- } else if (obj.config.parentNode !== null) {
- obj.canvas.setViewBox(0, 0, 200, 150, true);
- canvasW = 200;
- canvasH = 150;
- } else {
- canvasW = getStyle(document.getElementById(obj.config.id), "width").slice(0, -2) * 1;
- canvasH = getStyle(document.getElementById(obj.config.id), "height").slice(0, -2) * 1;
- }
-
- // widget dimensions
- if (obj.config.donut === true) {
-
- // DONUT *******************************
-
- // width more than height
- if(canvasW > canvasH) {
- widgetH = canvasH;
- widgetW = widgetH;
- // width less than height
- } else if (canvasW < canvasH) {
- widgetW = canvasW;
- widgetH = widgetW;
- // if height don't fit, rescale both
- if(widgetH > canvasH) {
- aspect = widgetH / canvasH;
- widgetH = widgetH / aspect;
- widgetW = widgetH / aspect;
- }
- // equal
- } else {
- widgetW = canvasW;
- widgetH = widgetW;
- }
-
- // delta
- dx = (canvasW - widgetW)/2;
- dy = (canvasH - widgetH)/2;
-
- // title
- titleFontSize = ((widgetH / 8) > 10) ? (widgetH / 10) : 10;
- titleX = dx + widgetW / 2;
- titleY = dy + widgetH / 11;
-
- // value
- valueFontSize = ((widgetH / 6.4) > 16) ? (widgetH / 5.4) : 18;
- valueX = dx + widgetW / 2;
- if(obj.config.label !== '') {
- valueY = dy + widgetH / 1.85;
- } else {
- valueY = dy + widgetH / 1.7;
- }
-
- // label
- labelFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10;
- labelX = dx + widgetW / 2;
- labelY = valueY + labelFontSize;
-
- // min
- minFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10;
- minX = dx + (widgetW / 10) + (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
- minY = labelY;
-
- // max
- maxFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10;
- maxX = dx + widgetW - (widgetW / 10) - (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
- maxY = labelY;
-
- } else {
- // HALF *******************************
-
- // width more than height
- if(canvasW > canvasH) {
- widgetH = canvasH;
- widgetW = widgetH * 1.25;
- //if width doesn't fit, rescale both
- if(widgetW > canvasW) {
- aspect = widgetW / canvasW;
- widgetW = widgetW / aspect;
- widgetH = widgetH / aspect;
- }
- // width less than height
- } else if (canvasW < canvasH) {
- widgetW = canvasW;
- widgetH = widgetW / 1.25;
- // if height don't fit, rescale both
- if(widgetH > canvasH) {
- aspect = widgetH / canvasH;
- widgetH = widgetH / aspect;
- widgetW = widgetH / aspect;
- }
- // equal
- } else {
- widgetW = canvasW;
- widgetH = widgetW * 0.75;
- }
-
- // delta
- dx = (canvasW - widgetW)/2;
- dy = (canvasH - widgetH)/2;
-
- // title
- titleFontSize = ((widgetH / 8) > obj.config.titleMinFontSize) ? (widgetH / 10) : obj.config.titleMinFontSize;
- titleX = dx + widgetW / 2;
- titleY = dy + widgetH / 6.4;
-
- // value
- valueFontSize = ((widgetH / 6.5) > obj.config.valueMinFontSize) ? (widgetH / 6.5) : obj.config.valueMinFontSize;
- valueX = dx + widgetW / 2;
- valueY = dy + widgetH / 1.275;
-
- // label
- labelFontSize = ((widgetH / 16) > obj.config.labelMinFontSize) ? (widgetH / 16) : obj.config.labelMinFontSize;
- labelX = dx + widgetW / 2;
- labelY = valueY + valueFontSize / 2 + 5;
-
- // min
- minFontSize = ((widgetH / 16) > obj.config.minLabelMinFontSize) ? (widgetH / 16) : obj.config.minLabelMinFontSize;
- minX = dx + (widgetW / 10) + (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
- minY = labelY;
-
- // max
- maxFontSize = ((widgetH / 16) > obj.config.maxLabelMinFontSize) ? (widgetH / 16) : obj.config.maxLabelMinFontSize;
- maxX = dx + widgetW - (widgetW / 10) - (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
- maxY = labelY;
- }
-
- // parameters
- obj.params = {
- canvasW : canvasW,
- canvasH : canvasH,
- widgetW : widgetW,
- widgetH : widgetH,
- dx : dx,
- dy : dy,
- titleFontSize : titleFontSize,
- titleX : titleX,
- titleY : titleY,
- valueFontSize : valueFontSize,
- valueX : valueX,
- valueY : valueY,
- labelFontSize : labelFontSize,
- labelX : labelX,
- labelY : labelY,
- minFontSize : minFontSize,
- minX : minX,
- minY : minY,
- maxFontSize : maxFontSize,
- maxX : maxX,
- maxY : maxY
- };
-
- // var clear
- canvasW, canvasH, widgetW, widgetH, aspect, dx, dy, titleFontSize, titleX, titleY, valueFontSize, valueX, valueY, labelFontSize, labelX, labelY, minFontSize, minX, minY, maxFontSize, maxX, maxY = null
-
- // pki - custom attribute for generating gauge paths
- obj.canvas.customAttributes.pki = function (value, min, max, w, h, dx, dy, gws, donut) {
-
- var alpha, Ro, Ri, Cx, Cy, Xo, Yo, Xi, Yi, path;
-
- if (donut) {
- alpha = (1 - 2 * (value - min) / (max - min)) * Math.PI;
- Ro = w / 2 - w / 7;
- Ri = Ro - w / 6.666666666666667 * gws;
-
- Cx = w / 2 + dx;
- Cy = h / 1.95 + dy;
-
- Xo = w / 2 + dx + Ro * Math.cos(alpha);
- Yo = h - (h - Cy) + 0 - Ro * Math.sin(alpha);
- Xi = w / 2 + dx + Ri * Math.cos(alpha);
- Yi = h - (h - Cy) + 0 - Ri * Math.sin(alpha);
-
- path += "M" + (Cx - Ri) + "," + Cy + " ";
- path += "L" + (Cx - Ro) + "," + Cy + " ";
- if (value > ((max - min) / 2)) {
- path += "A" + Ro + "," + Ro + " 0 0 1 " + (Cx + Ro) + "," + Cy + " ";
- }
- path += "A" + Ro + "," + Ro + " 0 0 1 " + Xo + "," + Yo + " ";
- path += "L" + Xi + "," + Yi + " ";
- if (value > ((max - min) / 2)) {
- path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx + Ri) + "," + Cy + " ";
- }
- path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx - Ri) + "," + Cy + " ";
- path += "Z ";
-
- return { path: path };
-
- } else {
- alpha = (1 - (value - min) / (max - min)) * Math.PI;
- Ro = w / 2 - w / 10;
- Ri = Ro - w / 6.666666666666667 * gws;
-
- Cx = w / 2 + dx;
- Cy = h / 1.25 + dy;
-
- Xo = w / 2 + dx + Ro * Math.cos(alpha);
- Yo = h - (h - Cy) + 0 - Ro * Math.sin(alpha);
- Xi = w / 2 + dx + Ri * Math.cos(alpha);
- Yi = h - (h - Cy) + 0 - Ri * Math.sin(alpha);
-
- path += "M" + (Cx - Ri) + "," + Cy + " ";
- path += "L" + (Cx - Ro) + "," + Cy + " ";
- path += "A" + Ro + "," + Ro + " 0 0 1 " + Xo + "," + Yo + " ";
- path += "L" + Xi + "," + Yi + " ";
- path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx - Ri) + "," + Cy + " ";
- path += "Z ";
-
- return { path: path };
- }
-
- // var clear
- alpha, Ro, Ri, Cx, Cy, Xo, Yo, Xi, Yi, path = null;
- };
-
- // gauge
- obj.gauge = obj.canvas.path().attr({
- "stroke": "none",
- "fill": obj.config.gaugeColor,
- pki: [
- obj.config.max,
- obj.config.min,
- obj.config.max,
- obj.params.widgetW,
- obj.params.widgetH,
- obj.params.dx,
- obj.params.dy,
- obj.config.gaugeWidthScale,
- obj.config.donut
- ]
- });
-
- // level
- obj.level = obj.canvas.path().attr({
- "stroke": "none",
- "fill": getColor(obj.config.value, (obj.config.value - obj.config.min) / (obj.config.max - obj.config.min), obj.config.levelColors, obj.config.noGradient, obj.config.customSectors),
- pki: [
- obj.config.min,
- obj.config.min,
- obj.config.max,
- obj.params.widgetW,
- obj.params.widgetH,
- obj.params.dx,
- obj.params.dy,
- obj.config.gaugeWidthScale,
- obj.config.donut
- ]
- });
- if(obj.config.donut) {
- obj.level.transform("r" + obj.config.donutStartAngle + ", " + (obj.params.widgetW/2 + obj.params.dx) + ", " + (obj.params.widgetH/1.95 + obj.params.dy));
- }
-
- // title
- obj.txtTitle = obj.canvas.text(obj.params.titleX, obj.params.titleY, obj.config.title);
- obj.txtTitle.attr({
- "font-size":obj.params.titleFontSize,
- "font-weight":"bold",
- "font-family":"Arial",
- "fill":obj.config.titleFontColor,
- "fill-opacity":"1"
- });
- setDy(obj.txtTitle, obj.params.titleFontSize, obj.params.titleY);
-
- // value
- obj.txtValue = obj.canvas.text(obj.params.valueX, obj.params.valueY, 0);
- obj.txtValue.attr({
- "font-size":obj.params.valueFontSize,
- "font-weight":"bold",
- "font-family":"Arial",
- "fill":obj.config.valueFontColor,
- "fill-opacity":"0"
- });
- setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
-
- // label
- obj.txtLabel = obj.canvas.text(obj.params.labelX, obj.params.labelY, obj.config.label);
- obj.txtLabel.attr({
- "font-size":obj.params.labelFontSize,
- "font-weight":"normal",
- "font-family":"Arial",
- "fill":obj.config.labelFontColor,
- "fill-opacity":"0"
- });
- setDy(obj.txtLabel, obj.params.labelFontSize, obj.params.labelY);
-
- // min
- obj.txtMinimum = obj.config.min;
- if( obj.config.humanFriendly ) obj.txtMinimum = humanFriendlyNumber( obj.config.min, obj.config.humanFriendlyDecimal );
- obj.txtMin = obj.canvas.text(obj.params.minX, obj.params.minY, obj.txtMinimum);
- obj.txtMin.attr({
- "font-size":obj.params.minFontSize,
- "font-weight":"normal",
- "font-family":"Arial",
- "fill":obj.config.labelFontColor,
- "fill-opacity": (obj.config.hideMinMax || obj.config.donut)? "0" : "1"
- });
- setDy(obj.txtMin, obj.params.minFontSize, obj.params.minY);
-
- // max
- obj.txtMaximum = obj.config.max;
- if( obj.config.humanFriendly ) obj.txtMaximum = humanFriendlyNumber( obj.config.max, obj.config.humanFriendlyDecimal );
- obj.txtMax = obj.canvas.text(obj.params.maxX, obj.params.maxY, obj.txtMaximum);
- obj.txtMax.attr({
- "font-size":obj.params.maxFontSize,
- "font-weight":"normal",
- "font-family":"Arial",
- "fill":obj.config.labelFontColor,
- "fill-opacity": (obj.config.hideMinMax || obj.config.donut)? "0" : "1"
- });
- setDy(obj.txtMax, obj.params.maxFontSize, obj.params.maxY);
-
- var defs = obj.canvas.canvas.childNodes[1];
- var svg = "http://www.w3.org/2000/svg";
-
- if (ie < 9) {
- onCreateElementNsReady(function() {
- obj.generateShadow(svg, defs);
- });
- } else {
- obj.generateShadow(svg, defs);
- }
-
- // var clear
- defs, svg = null;
-
- // set value to display
- if(obj.config.textRenderer) {
- obj.originalValue = obj.config.textRenderer(obj.originalValue);
- } else if(obj.config.humanFriendly) {
- obj.originalValue = humanFriendlyNumber( obj.originalValue, obj.config.humanFriendlyDecimal ) + obj.config.symbol;
- } else {
- obj.originalValue = (obj.originalValue * 1).toFixed(obj.config.decimals) + obj.config.symbol;
- }
-
- if(obj.config.counter === true) {
- //on each animation frame
- eve.on("raphael.anim.frame." + (obj.level.id), function() {
- var currentValue = obj.level.attr("pki");
- if(obj.config.textRenderer) {
- obj.txtValue.attr("text", obj.config.textRenderer(Math.floor(currentValue[0])));
- } else if(obj.config.humanFriendly) {
- obj.txtValue.attr("text", humanFriendlyNumber( Math.floor(currentValue[0]), obj.config.humanFriendlyDecimal ) + obj.config.symbol);
- } else {
- obj.txtValue.attr("text", (currentValue[0] * 1).toFixed(obj.config.decimals) + obj.config.symbol);
- }
- setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
- currentValue = null;
- });
- //on animation end
- eve.on("raphael.anim.finish." + (obj.level.id), function() {
- obj.txtValue.attr({"text" : obj.originalValue});
- setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
- });
- } else {
- //on animation start
- eve.on("raphael.anim.start." + (obj.level.id), function() {
- obj.txtValue.attr({"text" : obj.originalValue});
- setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
- });
- }
-
- // animate gauge level, value & label
- obj.level.animate({
- pki: [
- obj.config.value,
- obj.config.min,
- obj.config.max,
- obj.params.widgetW,
- obj.params.widgetH,
- obj.params.dx,
- obj.params.dy,
- obj.config.gaugeWidthScale,
- obj.config.donut
- ]
- }, obj.config.startAnimationTime, obj.config.startAnimationType);
- obj.txtValue.animate({"fill-opacity":(obj.config.hideValue)?"0":"1"}, obj.config.startAnimationTime, obj.config.startAnimationType);
- obj.txtLabel.animate({"fill-opacity":"1"}, obj.config.startAnimationTime, obj.config.startAnimationType);
-};
-
-/** Refresh gauge level */
-JustGage.prototype.refresh = function(val, max) {
-
- var obj = this;
- var displayVal, color, max = max || null;
-
- // set new max
- if(max !== null) {
- obj.config.max = max;
-
- obj.txtMaximum = obj.config.max;
- if( obj.config.humanFriendly ) obj.txtMaximum = humanFriendlyNumber( obj.config.max, obj.config.humanFriendlyDecimal );
- obj.txtMax.attr({"text" : obj.txtMaximum});
- setDy(obj.txtMax, obj.params.maxFontSize, obj.params.maxY);
- }
-
- // overflow values
- displayVal = val;
- if ((val * 1) > (obj.config.max * 1)) {val = (obj.config.max * 1);}
- if ((val * 1) < (obj.config.min * 1)) {val = (obj.config.min * 1);}
-
- color = getColor(val, (val - obj.config.min) / (obj.config.max - obj.config.min), obj.config.levelColors, obj.config.noGradient, obj.config.customSectors);
-
- if(obj.config.textRenderer) {
- displayVal = obj.config.textRenderer(displayVal);
- } else if( obj.config.humanFriendly ) {
- displayVal = humanFriendlyNumber( displayVal, obj.config.humanFriendlyDecimal ) + obj.config.symbol;
- } else {
- displayVal = (displayVal * 1).toFixed(obj.config.decimals) + obj.config.symbol;
- }
- obj.originalValue = displayVal;
- obj.config.value = val * 1;
-
- if(!obj.config.counter) {
- obj.txtValue.attr({"text":displayVal});
- setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
- }
-
- obj.level.animate({
- pki: [
- obj.config.value,
- obj.config.min,
- obj.config.max,
- obj.params.widgetW,
- obj.params.widgetH,
- obj.params.dx,
- obj.params.dy,
- obj.config.gaugeWidthScale,
- obj.config.donut
- ],
- "fill":color
- }, obj.config.refreshAnimationTime, obj.config.refreshAnimationType);
-
- // var clear
- obj, displayVal, color, max = null;
-};
-
-/** Generate shadow */
-JustGage.prototype.generateShadow = function(svg, defs) {
-
- var obj = this;
- var gaussFilter, feOffset, feGaussianBlur, feComposite1, feFlood, feComposite2, feComposite3;
-
- // FILTER
- gaussFilter = document.createElementNS(svg,"filter");
- gaussFilter.setAttribute("id","inner-shadow");
- defs.appendChild(gaussFilter);
-
- // offset
- feOffset = document.createElementNS(svg,"feOffset");
- feOffset.setAttribute("dx", 0);
- feOffset.setAttribute("dy", obj.config.shadowVerticalOffset);
- gaussFilter.appendChild(feOffset);
-
- // blur
- feGaussianBlur = document.createElementNS(svg,"feGaussianBlur");
- feGaussianBlur.setAttribute("result","offset-blur");
- feGaussianBlur.setAttribute("stdDeviation", obj.config.shadowSize);
- gaussFilter.appendChild(feGaussianBlur);
-
- // composite 1
- feComposite1 = document.createElementNS(svg,"feComposite");
- feComposite1.setAttribute("operator","out");
- feComposite1.setAttribute("in", "SourceGraphic");
- feComposite1.setAttribute("in2","offset-blur");
- feComposite1.setAttribute("result","inverse");
- gaussFilter.appendChild(feComposite1);
-
- // flood
- feFlood = document.createElementNS(svg,"feFlood");
- feFlood.setAttribute("flood-color","black");
- feFlood.setAttribute("flood-opacity", obj.config.shadowOpacity);
- feFlood.setAttribute("result","color");
- gaussFilter.appendChild(feFlood);
-
- // composite 2
- feComposite2 = document.createElementNS(svg,"feComposite");
- feComposite2.setAttribute("operator","in");
- feComposite2.setAttribute("in", "color");
- feComposite2.setAttribute("in2","inverse");
- feComposite2.setAttribute("result","shadow");
- gaussFilter.appendChild(feComposite2);
-
- // composite 3
- feComposite3 = document.createElementNS(svg,"feComposite");
- feComposite3.setAttribute("operator","over");
- feComposite3.setAttribute("in", "shadow");
- feComposite3.setAttribute("in2","SourceGraphic");
- gaussFilter.appendChild(feComposite3);
-
- // set shadow
- if (!obj.config.hideInnerShadow) {
- obj.canvas.canvas.childNodes[2].setAttribute("filter", "url(#inner-shadow)");
- obj.canvas.canvas.childNodes[3].setAttribute("filter", "url(#inner-shadow)");
- }
-
- // var clear
- gaussFilter, feOffset, feGaussianBlur, feComposite1, feFlood, feComposite2, feComposite3 = null;
-
-};
-
-/** Get color for value */
-function getColor(val, pct, col, noGradient, custSec) {
-
- var no, inc, colors, percentage, rval, gval, bval, lower, upper, range, rangePct, pctLower, pctUpper, color;
- var noGradient = noGradient || custSec.length > 0;
-
- if(custSec.length > 0) {
- for(var i = 0; i < custSec.length; i++) {
- if(val > custSec[i].lo && val <= custSec[i].hi) {
- return custSec[i].color;
- }
- }
- }
-
- no = col.length;
- if (no === 1) return col[0];
- inc = (noGradient) ? (1 / no) : (1 / (no - 1));
- colors = [];
- for (var i = 0; i < col.length; i++) {
- percentage = (noGradient) ? (inc * (i + 1)) : (inc * i);
- rval = parseInt((cutHex(col[i])).substring(0,2),16);
- gval = parseInt((cutHex(col[i])).substring(2,4),16);
- bval = parseInt((cutHex(col[i])).substring(4,6),16);
- colors[i] = { pct: percentage, color: { r: rval, g: gval, b: bval } };
- }
-
- if(pct === 0) {
- return 'rgb(' + [colors[0].color.r, colors[0].color.g, colors[0].color.b].join(',') + ')';
- }
-
- for (var j = 0; j < colors.length; j++) {
- if (pct <= colors[j].pct) {
- if (noGradient) {
- return 'rgb(' + [colors[j].color.r, colors[j].color.g, colors[j].color.b].join(',') + ')';
- } else {
- lower = colors[j - 1];
- upper = colors[j];
- range = upper.pct - lower.pct;
- rangePct = (pct - lower.pct) / range;
- pctLower = 1 - rangePct;
- pctUpper = rangePct;
- color = {
- r: Math.floor(lower.color.r * pctLower + upper.color.r * pctUpper),
- g: Math.floor(lower.color.g * pctLower + upper.color.g * pctUpper),
- b: Math.floor(lower.color.b * pctLower + upper.color.b * pctUpper)
- };
- return 'rgb(' + [color.r, color.g, color.b].join(',') + ')';
- }
- }
- }
-
-}
-
-/** Fix Raphael display:none tspan dy attribute bug */
-function setDy(elem, fontSize, txtYpos) {
- if (!ie || ie > 9) {
- elem.node.firstChild.attributes.dy.value = 0;
- }
-}
-
-/** Random integer */
-function getRandomInt (min, max) {
- return Math.floor(Math.random() * (max - min + 1)) + min;
-}
-
-/** Cut hex */
-function cutHex(str) {
- return (str.charAt(0)=="#") ? str.substring(1,7):str;
-}
-
-/** Human friendly number suffix - From: http://stackoverflow.com/questions/2692323/code-golf-friendly-number-abbreviator */
-function humanFriendlyNumber( n, d ) {
- var p, d2, i, s;
-
- p = Math.pow;
- d2 = p(10, d);
- i = 7;
- while( i ) {
- s = p(10,i--*3);
- if( s <= n ) {
- n = Math.round(n*d2/s)/d2+"KMGTPE"[i];
- }
- }
- return n;
-}
-
-/** Get style */
-function getStyle(oElm, strCssRule){
- var strValue = "";
- if(document.defaultView && document.defaultView.getComputedStyle){
- strValue = document.defaultView.getComputedStyle(oElm, "").getPropertyValue(strCssRule);
- }
- else if(oElm.currentStyle){
- strCssRule = strCssRule.replace(/\-(\w)/g, function (strMatch, p1){
- return p1.toUpperCase();
- });
- strValue = oElm.currentStyle[strCssRule];
- }
- return strValue;
-}
-
-/** Create Element NS Ready */
-function onCreateElementNsReady(func) {
- if (document.createElementNS !== undefined) {
- func();
- } else {
- setTimeout(function() { onCreateElementNsReady(func); }, 100);
- }
-}
-
-/** Get IE version */
-// ----------------------------------------------------------
-// A short snippet for detecting versions of IE in JavaScript
-// without resorting to user-agent sniffing
-// ----------------------------------------------------------
-// If you're not in IE (or IE version is less than 5) then:
-// ie === undefined
-// If you're in IE (>=5) then you can determine which version:
-// ie === 7; // IE7
-// Thus, to detect IE:
-// if (ie) {}
-// And to detect the version:
-// ie === 6 // IE6
-// ie > 7 // IE8, IE9 ...
-// ie < 9 // Anything less than IE9
-// ----------------------------------------------------------
-// UPDATE: Now using Live NodeList idea from @jdalton
-var ie = (function(){
-
- var undef,
- v = 3,
- div = document.createElement('div'),
- all = div.getElementsByTagName('i');
-
- while (
- div.innerHTML = '',
- all[0]
- );
- return v > 4 ? v : undef;
-}());
\ No newline at end of file
diff --git a/addons/web_kanban/static/src/js/kanban.js b/addons/web_kanban/static/src/js/kanban.js
index 9bc8e3bd785..8ec11e37d25 100644
--- a/addons/web_kanban/static/src/js/kanban.js
+++ b/addons/web_kanban/static/src/js/kanban.js
@@ -1232,157 +1232,6 @@ instance.web_kanban.AbstractField = instance.web.Widget.extend(instance.web_kanb
instance.web_kanban.fields_registry = new instance.web.Registry({});
-
-/**
- * Kanban widgets: Sparkline
- *
- */
-
-instance.web_kanban.SparklineBarWidget = instance.web_kanban.AbstractField.extend({
- className: "oe_sparkline_bar",
- start: function() {
- var self = this;
- var title = this.$node.html() || this.field.string;
- setTimeout(function () {
- var value = _.pluck(self.field.value, 'value');
- var tooltips = _.pluck(self.field.value, 'tooltip');
- var sparkline_options = _.extend({
- type: 'bar',
- barWidth: 5,
- height: '20px',
- barWidth: 4,
- barSpacing: 1,
- barColor: '#96d854',
- tooltipFormat: '{{offset:offset}} {{value}}',
- tooltipValueLookups: {
- 'offset': tooltips
- }
- }, self.options);
- self.$el.sparkline(value, sparkline_options);
- self.$el.tipsy({'delayIn': 0, 'html': true, 'title': function(){return title}, 'gravity': 'n'});
- }, 0);
- },
-});
-
-instance.web_kanban.fields_registry.add("sparkline_bar", "instance.web_kanban.SparklineBarWidget");
-
-
-/**
- * Kanban widgets: GaugeWidget
- *
- */
-
-instance.web_kanban.GaugeWidget = instance.web_kanban.AbstractField.extend({
- className: "oe_gauge",
- start: function() {
- var self = this;
- var parent = this.getParent();
- var max = 100;
- if (this.options.max_field) {
- max = this.getParent().record[this.options.max_field].raw_value;
- }
- var label = this.options.label || "";
- if (this.options.label_field) {
- label = this.getParent().record[this.options.label_field].raw_value;
- }
- var val = this.field.value;
- var value = _.isArray(val) && val.length ? val[val.length-1]['value'] : val;
- var title = this.$node.html() || this.field.string;
- // var unique_id = _.uniqueId("JustGage");
-
- this.$el.empty()
- .attr('style', this.$node.attr('style') + ';position:relative; display:inline-block;');
-
- this.gage = new JustGage({
- parentNode: this.$el[0],
- // id: unique_id,
- value: value,
- title: title,
- min: 0,
- max: max,
- relativeGaugeSize: true,
- humanFriendly: true,
- titleFontColor: '#333333',
- valueFontColor: '#333333',
- labelFontColor: '#000',
- label: label,
- levelColors: [
- "#ff0000",
- "#f9c802",
- "#a9d70b"
- ],
- });
- this.gage.refresh(value, max);
-
- var flag_open = false;
- if (this.options.action_change) {
- var $svg = this.$el.find('svg');
- var css = {
- 'text-align': 'center',
- 'position': 'absolute',
- 'width': this.$el.outerWidth() + 'px',
- 'top': (this.$el.outerHeight()/2-5) + 'px'
- };
-
- this.$el.click(function (event) {
- event.stopPropagation();
- flag_open = false;
- if (!parent.view.is_action_enabled('edit')) {
- return;
- }
- if (!self.$el.find(".oe_justgage_edit").size()) {
- $div = $('');
- $div.css(css);
- $input = $('').val(value);
- $input.css({
- 'text-align': 'center',
- 'margin': 'auto',
- 'width': ($svg.outerWidth()-40) + 'px'
- });
- $div.append($input);
- self.$el.prepend($div)
- $input.focus()
- .keydown(function (event) {
- event.stopPropagation();
- if (event.keyCode == 13 || event.keyCode == 9) {
- if ($input.val() != value) {
- parent.view.dataset.call(self.options.action_change, [parent.id, $input.val()]).then(function () {
- parent.do_reload();
- });
- } else {
- $div.remove();
- }
- }
- })
- .click(function (event) {
- event.stopPropagation();
- flag_open = false;
- })
- .blur(function (event) {
- if(!flag_open) {
- self.$el.find(".oe_justgage_edit").remove();
- } else {
- flag_open = false;
- setTimeout(function () {$input.focus();}, 0);
- }
- });
- }
- }).mousedown(function () {
- flag_open = true;
- });
-
- if (!+value) {
- $svg.fadeTo(0, 0.3);
- $div = $('').text(_t("Click to change value"));
- $div.css(css);
- this.$el.append($div);
- }
- }
- },
-});
-
-instance.web_kanban.fields_registry.add("gauge", "instance.web_kanban.GaugeWidget");
-
};
// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
diff --git a/addons/web_kanban_gauge/__init__.py b/addons/web_kanban_gauge/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/addons/web_kanban_gauge/__openerp__.py b/addons/web_kanban_gauge/__openerp__.py
new file mode 100644
index 00000000000..19b2d6f0da3
--- /dev/null
+++ b/addons/web_kanban_gauge/__openerp__.py
@@ -0,0 +1,18 @@
+{
+ 'name': 'Gauge Widget for Kanban',
+ 'category': 'Hidden',
+ 'description': """
+This widget allows to display gauges using jquery.sparkline library.
+""",
+ 'version': '1.0',
+ 'depends': ['web_kanban'],
+ 'js': [
+ 'static/lib/justgage/justgage.js',
+ 'static/src/js/kanban_gauge.js'
+ ],
+ 'css': [
+ ],
+ 'qweb': [
+ ],
+ 'auto_install': False,
+}
diff --git a/addons/web_kanban_gauge/static/lib/justgage/justgage.js b/addons/web_kanban_gauge/static/lib/justgage/justgage.js
new file mode 100644
index 00000000000..24540beb293
--- /dev/null
+++ b/addons/web_kanban_gauge/static/lib/justgage/justgage.js
@@ -0,0 +1,946 @@
+/**
+ * JustGage - this is work-in-progress, unreleased, unofficial code, so it might not work top-notch :)
+ * Check http://www.justgage.com for official releases
+ * Licensed under MIT.
+ * @author Bojan Djuricic (@Toorshia)
+ *
+ * LATEST UPDATES
+
+ * -----------------------------
+ * April 18, 2013.
+ * -----------------------------
+ * parentNode - use this instead of id, to attach gauge to node which is outside of DOM tree - https://github.com/toorshia/justgage/issues/48
+ * width - force gauge width
+ * height - force gauge height
+
+ * -----------------------------
+ * April 17, 2013.
+ * -----------------------------
+ * fix - https://github.com/toorshia/justgage/issues/49
+
+ * -----------------------------
+ * April 01, 2013.
+ * -----------------------------
+ * fix - https://github.com/toorshia/justgage/issues/46
+
+ * -----------------------------
+ * March 26, 2013.
+ * -----------------------------
+ * customSectors - define specific color for value range (0-10 : red, 10-30 : blue etc.)
+
+ * -----------------------------
+ * March 23, 2013.
+ * -----------------------------
+ * counter - option to animate value in counting fashion
+ * fix - https://github.com/toorshia/justgage/issues/45
+
+ * -----------------------------
+ * March 13, 2013.
+ * -----------------------------
+ * refresh method - added optional 'max' parameter to use when you need to update max value
+
+ * -----------------------------
+ * February 26, 2013.
+ * -----------------------------
+ * decimals - option to define/limit number of decimals when not using humanFriendly or customRenderer to display value
+ * fixed a missing parameters bug when calling generateShadow() for IE < 9
+
+ * -----------------------------
+ * December 31, 2012.
+ * -----------------------------
+ * fixed text y-position for hidden divs - workaround for Raphael 'dy' bug - https://github.com/DmitryBaranovskiy/raphael/issues/491
+ * 'show' parameters, like showMinMax are now 'hide' because I am lame developer - please update these in your setups
+ * Min and Max labels are now auto-off when in donut mode
+ * Start angle in donut mode is now 90
+ * donutStartAngle - option to define start angle for donut
+
+ * -----------------------------
+ * November 25, 2012.
+ * -----------------------------
+ * Option to define custom rendering function for displayed value
+
+ * -----------------------------
+ * November 19, 2012.
+ * -----------------------------
+ * Config.value is now updated after gauge refresh
+
+ * -----------------------------
+ * November 13, 2012.
+ * -----------------------------
+ * Donut display mode added
+ * Option to hide value label
+ * Option to enable responsive gauge size
+ * Removed default title attribute
+ * Option to accept min and max defined as string values
+ * Option to configure value symbol
+ * Fixed bad aspect ratio calculations
+ * Option to configure minimum font size for all texts
+ * Option to show shorthand big numbers (human friendly)
+ */
+
+ JustGage = function(config) {
+
+ // if (!config.id) {alert("Missing id parameter for gauge!"); return false;}
+ // if (!document.getElementById(config.id)) {alert("No element with id: \""+config.id+"\" found!"); return false;}
+
+ var obj = this;
+
+ // configurable parameters
+ obj.config =
+ {
+ // id : string
+ // this is container element id
+ id : config.id,
+
+ // parentNode : node object
+ // this is container element
+ parentNode : (config.parentNode) ? config.parentNode : null,
+
+ // width : int
+ // gauge width
+ width : (config.width) ? config.width : null,
+
+ // height : int
+ // gauge height
+ height : (config.height) ? config.height : null,
+
+ // title : string
+ // gauge title
+ title : (config.title) ? config.title : "",
+
+ // titleFontColor : string
+ // color of gauge title
+ titleFontColor : (config.titleFontColor) ? config.titleFontColor : "#999999",
+
+ // value : int
+ // value gauge is showing
+ value : (config.value) ? config.value : 0,
+
+ // valueFontColor : string
+ // color of label showing current value
+ valueFontColor : (config.valueFontColor) ? config.valueFontColor : "#010101",
+
+ // symbol : string
+ // special symbol to show next to value
+ symbol : (config.symbol) ? config.symbol : "",
+
+ // min : int
+ // min value
+ min : (config.min !== undefined) ? parseFloat(config.min) : 0,
+
+ // max : int
+ // max value
+ max : (config.max !== undefined) ? parseFloat(config.max) : 100,
+
+ // humanFriendlyDecimal : int
+ // number of decimal places for our human friendly number to contain
+ humanFriendlyDecimal : (config.humanFriendlyDecimal) ? config.humanFriendlyDecimal : 0,
+
+ // textRenderer: func
+ // function applied before rendering text
+ textRenderer : (config.textRenderer) ? config.textRenderer : null,
+
+ // gaugeWidthScale : float
+ // width of the gauge element
+ gaugeWidthScale : (config.gaugeWidthScale) ? config.gaugeWidthScale : 1.0,
+
+ // gaugeColor : string
+ // background color of gauge element
+ gaugeColor : (config.gaugeColor) ? config.gaugeColor : "#edebeb",
+
+ // label : string
+ // text to show below value
+ label : (config.label) ? config.label : "",
+
+ // labelFontColor : string
+ // color of label showing label under value
+ labelFontColor : (config.labelFontColor) ? config.labelFontColor : "#b3b3b3",
+
+ // shadowOpacity : int
+ // 0 ~ 1
+ shadowOpacity : (config.shadowOpacity) ? config.shadowOpacity : 0.2,
+
+ // shadowSize: int
+ // inner shadow size
+ shadowSize : (config.shadowSize) ? config.shadowSize : 5,
+
+ // shadowVerticalOffset : int
+ // how much shadow is offset from top
+ shadowVerticalOffset : (config.shadowVerticalOffset) ? config.shadowVerticalOffset : 3,
+
+ // levelColors : string[]
+ // colors of indicator, from lower to upper, in RGB format
+ levelColors : (config.levelColors) ? config.levelColors : [
+ "#a9d70b",
+ "#f9c802",
+ "#ff0000"
+ ],
+
+ // startAnimationTime : int
+ // length of initial animation
+ startAnimationTime : (config.startAnimationTime) ? config.startAnimationTime : 700,
+
+ // startAnimationType : string
+ // type of initial animation (linear, >, <, <>, bounce)
+ startAnimationType : (config.startAnimationType) ? config.startAnimationType : ">",
+
+ // refreshAnimationTime : int
+ // length of refresh animation
+ refreshAnimationTime : (config.refreshAnimationTime) ? config.refreshAnimationTime : 700,
+
+ // refreshAnimationType : string
+ // type of refresh animation (linear, >, <, <>, bounce)
+ refreshAnimationType : (config.refreshAnimationType) ? config.refreshAnimationType : ">",
+
+ // donutStartAngle : int
+ // angle to start from when in donut mode
+ donutStartAngle : (config.donutStartAngle) ? config.donutStartAngle : 90,
+
+ // valueMinFontSize : int
+ // absolute minimum font size for the value
+ valueMinFontSize : config.valueMinFontSize || 16,
+
+ // titleMinFontSize
+ // absolute minimum font size for the title
+ titleMinFontSize : config.titleMinFontSize || 10,
+
+ // labelMinFontSize
+ // absolute minimum font size for the label
+ labelMinFontSize : config.labelMinFontSize || 10,
+
+ // minLabelMinFontSize
+ // absolute minimum font size for the minimum label
+ minLabelMinFontSize : config.minLabelMinFontSize || 10,
+
+ // maxLabelMinFontSize
+ // absolute minimum font size for the maximum label
+ maxLabelMinFontSize : config.maxLabelMinFontSize || 10,
+
+ // hideValue : bool
+ // hide value text
+ hideValue : (config.hideValue) ? config.hideValue : false,
+
+ // hideMinMax : bool
+ // hide min and max values
+ hideMinMax : (config.hideMinMax) ? config.hideMinMax : false,
+
+ // hideInnerShadow : bool
+ // hide inner shadow
+ hideInnerShadow : (config.hideInnerShadow) ? config.hideInnerShadow : false,
+
+ // humanFriendly : bool
+ // convert large numbers for min, max, value to human friendly (e.g. 1234567 -> 1.23M)
+ humanFriendly : (config.humanFriendly) ? config.humanFriendly : false,
+
+ // noGradient : bool
+ // whether to use gradual color change for value, or sector-based
+ noGradient : (config.noGradient) ? config.noGradient : false,
+
+ // donut : bool
+ // show full donut gauge
+ donut : (config.donut) ? config.donut : false,
+
+ // relativeGaugeSize : bool
+ // whether gauge size should follow changes in container element size
+ relativeGaugeSize : (config.relativeGaugeSize) ? config.relativeGaugeSize : false,
+
+ // counter : bool
+ // animate level number change
+ counter : (config.counter) ? config.counter : false,
+
+ // decimals : int
+ // number of digits after floating point
+ decimals : (config.decimals) ? config.decimals : 0,
+
+ // customSectors : [] of objects
+ // number of digits after floating point
+ customSectors : (config.customSectors) ? config.customSectors : []
+ };
+
+ // variables
+ var
+ canvasW,
+ canvasH,
+ widgetW,
+ widgetH,
+ aspect,
+ dx,
+ dy,
+ titleFontSize,
+ titleX,
+ titleY,
+ valueFontSize,
+ valueX,
+ valueY,
+ labelFontSize,
+ labelX,
+ labelY,
+ minFontSize,
+ minX,
+ minY,
+ maxFontSize,
+ maxX,
+ maxY;
+
+ // overflow values
+ if (obj.config.value > obj.config.max) obj.config.value = obj.config.max;
+ if (obj.config.value < obj.config.min) obj.config.value = obj.config.min;
+ obj.originalValue = config.value;
+
+ // create canvas
+ if (obj.config.id !== null && (document.getElementById(obj.config.id)) !== null) {
+ obj.canvas = Raphael(obj.config.id, "100%", "100%");
+ } else if (obj.config.parentNode !== null) {
+ obj.canvas = Raphael(obj.config.parentNode, "100%", "100%");
+ }
+
+ if (obj.config.relativeGaugeSize === true) {
+ obj.canvas.setViewBox(0, 0, 200, 150, true);
+ }
+
+ // canvas dimensions
+ if (obj.config.relativeGaugeSize === true) {
+ canvasW = 200;
+ canvasH = 150;
+ } else if (obj.config.width !== null && obj.config.height !== null) {
+ canvasW = obj.config.width;
+ canvasH = obj.config.height;
+ } else if (obj.config.parentNode !== null) {
+ obj.canvas.setViewBox(0, 0, 200, 150, true);
+ canvasW = 200;
+ canvasH = 150;
+ } else {
+ canvasW = getStyle(document.getElementById(obj.config.id), "width").slice(0, -2) * 1;
+ canvasH = getStyle(document.getElementById(obj.config.id), "height").slice(0, -2) * 1;
+ }
+
+ // widget dimensions
+ if (obj.config.donut === true) {
+
+ // DONUT *******************************
+
+ // width more than height
+ if(canvasW > canvasH) {
+ widgetH = canvasH;
+ widgetW = widgetH;
+ // width less than height
+ } else if (canvasW < canvasH) {
+ widgetW = canvasW;
+ widgetH = widgetW;
+ // if height don't fit, rescale both
+ if(widgetH > canvasH) {
+ aspect = widgetH / canvasH;
+ widgetH = widgetH / aspect;
+ widgetW = widgetH / aspect;
+ }
+ // equal
+ } else {
+ widgetW = canvasW;
+ widgetH = widgetW;
+ }
+
+ // delta
+ dx = (canvasW - widgetW)/2;
+ dy = (canvasH - widgetH)/2;
+
+ // title
+ titleFontSize = ((widgetH / 8) > 10) ? (widgetH / 10) : 10;
+ titleX = dx + widgetW / 2;
+ titleY = dy + widgetH / 11;
+
+ // value
+ valueFontSize = ((widgetH / 6.4) > 16) ? (widgetH / 5.4) : 18;
+ valueX = dx + widgetW / 2;
+ if(obj.config.label !== '') {
+ valueY = dy + widgetH / 1.85;
+ } else {
+ valueY = dy + widgetH / 1.7;
+ }
+
+ // label
+ labelFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10;
+ labelX = dx + widgetW / 2;
+ labelY = valueY + labelFontSize;
+
+ // min
+ minFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10;
+ minX = dx + (widgetW / 10) + (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
+ minY = labelY;
+
+ // max
+ maxFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10;
+ maxX = dx + widgetW - (widgetW / 10) - (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
+ maxY = labelY;
+
+ } else {
+ // HALF *******************************
+
+ // width more than height
+ if(canvasW > canvasH) {
+ widgetH = canvasH;
+ widgetW = widgetH * 1.25;
+ //if width doesn't fit, rescale both
+ if(widgetW > canvasW) {
+ aspect = widgetW / canvasW;
+ widgetW = widgetW / aspect;
+ widgetH = widgetH / aspect;
+ }
+ // width less than height
+ } else if (canvasW < canvasH) {
+ widgetW = canvasW;
+ widgetH = widgetW / 1.25;
+ // if height don't fit, rescale both
+ if(widgetH > canvasH) {
+ aspect = widgetH / canvasH;
+ widgetH = widgetH / aspect;
+ widgetW = widgetH / aspect;
+ }
+ // equal
+ } else {
+ widgetW = canvasW;
+ widgetH = widgetW * 0.75;
+ }
+
+ // delta
+ dx = (canvasW - widgetW)/2;
+ dy = (canvasH - widgetH)/2;
+
+ // title
+ titleFontSize = ((widgetH / 8) > obj.config.titleMinFontSize) ? (widgetH / 10) : obj.config.titleMinFontSize;
+ titleX = dx + widgetW / 2;
+ titleY = dy + widgetH / 6.4;
+
+ // value
+ valueFontSize = ((widgetH / 6.5) > obj.config.valueMinFontSize) ? (widgetH / 6.5) : obj.config.valueMinFontSize;
+ valueX = dx + widgetW / 2;
+ valueY = dy + widgetH / 1.275;
+
+ // label
+ labelFontSize = ((widgetH / 16) > obj.config.labelMinFontSize) ? (widgetH / 16) : obj.config.labelMinFontSize;
+ labelX = dx + widgetW / 2;
+ labelY = valueY + valueFontSize / 2 + 5;
+
+ // min
+ minFontSize = ((widgetH / 16) > obj.config.minLabelMinFontSize) ? (widgetH / 16) : obj.config.minLabelMinFontSize;
+ minX = dx + (widgetW / 10) + (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
+ minY = labelY;
+
+ // max
+ maxFontSize = ((widgetH / 16) > obj.config.maxLabelMinFontSize) ? (widgetH / 16) : obj.config.maxLabelMinFontSize;
+ maxX = dx + widgetW - (widgetW / 10) - (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
+ maxY = labelY;
+ }
+
+ // parameters
+ obj.params = {
+ canvasW : canvasW,
+ canvasH : canvasH,
+ widgetW : widgetW,
+ widgetH : widgetH,
+ dx : dx,
+ dy : dy,
+ titleFontSize : titleFontSize,
+ titleX : titleX,
+ titleY : titleY,
+ valueFontSize : valueFontSize,
+ valueX : valueX,
+ valueY : valueY,
+ labelFontSize : labelFontSize,
+ labelX : labelX,
+ labelY : labelY,
+ minFontSize : minFontSize,
+ minX : minX,
+ minY : minY,
+ maxFontSize : maxFontSize,
+ maxX : maxX,
+ maxY : maxY
+ };
+
+ // var clear
+ canvasW, canvasH, widgetW, widgetH, aspect, dx, dy, titleFontSize, titleX, titleY, valueFontSize, valueX, valueY, labelFontSize, labelX, labelY, minFontSize, minX, minY, maxFontSize, maxX, maxY = null
+
+ // pki - custom attribute for generating gauge paths
+ obj.canvas.customAttributes.pki = function (value, min, max, w, h, dx, dy, gws, donut) {
+
+ var alpha, Ro, Ri, Cx, Cy, Xo, Yo, Xi, Yi, path;
+
+ if (donut) {
+ alpha = (1 - 2 * (value - min) / (max - min)) * Math.PI;
+ Ro = w / 2 - w / 7;
+ Ri = Ro - w / 6.666666666666667 * gws;
+
+ Cx = w / 2 + dx;
+ Cy = h / 1.95 + dy;
+
+ Xo = w / 2 + dx + Ro * Math.cos(alpha);
+ Yo = h - (h - Cy) + 0 - Ro * Math.sin(alpha);
+ Xi = w / 2 + dx + Ri * Math.cos(alpha);
+ Yi = h - (h - Cy) + 0 - Ri * Math.sin(alpha);
+
+ path += "M" + (Cx - Ri) + "," + Cy + " ";
+ path += "L" + (Cx - Ro) + "," + Cy + " ";
+ if (value > ((max - min) / 2)) {
+ path += "A" + Ro + "," + Ro + " 0 0 1 " + (Cx + Ro) + "," + Cy + " ";
+ }
+ path += "A" + Ro + "," + Ro + " 0 0 1 " + Xo + "," + Yo + " ";
+ path += "L" + Xi + "," + Yi + " ";
+ if (value > ((max - min) / 2)) {
+ path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx + Ri) + "," + Cy + " ";
+ }
+ path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx - Ri) + "," + Cy + " ";
+ path += "Z ";
+
+ return { path: path };
+
+ } else {
+ alpha = (1 - (value - min) / (max - min)) * Math.PI;
+ Ro = w / 2 - w / 10;
+ Ri = Ro - w / 6.666666666666667 * gws;
+
+ Cx = w / 2 + dx;
+ Cy = h / 1.25 + dy;
+
+ Xo = w / 2 + dx + Ro * Math.cos(alpha);
+ Yo = h - (h - Cy) + 0 - Ro * Math.sin(alpha);
+ Xi = w / 2 + dx + Ri * Math.cos(alpha);
+ Yi = h - (h - Cy) + 0 - Ri * Math.sin(alpha);
+
+ path += "M" + (Cx - Ri) + "," + Cy + " ";
+ path += "L" + (Cx - Ro) + "," + Cy + " ";
+ path += "A" + Ro + "," + Ro + " 0 0 1 " + Xo + "," + Yo + " ";
+ path += "L" + Xi + "," + Yi + " ";
+ path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx - Ri) + "," + Cy + " ";
+ path += "Z ";
+
+ return { path: path };
+ }
+
+ // var clear
+ alpha, Ro, Ri, Cx, Cy, Xo, Yo, Xi, Yi, path = null;
+ };
+
+ // gauge
+ obj.gauge = obj.canvas.path().attr({
+ "stroke": "none",
+ "fill": obj.config.gaugeColor,
+ pki: [
+ obj.config.max,
+ obj.config.min,
+ obj.config.max,
+ obj.params.widgetW,
+ obj.params.widgetH,
+ obj.params.dx,
+ obj.params.dy,
+ obj.config.gaugeWidthScale,
+ obj.config.donut
+ ]
+ });
+
+ // level
+ obj.level = obj.canvas.path().attr({
+ "stroke": "none",
+ "fill": getColor(obj.config.value, (obj.config.value - obj.config.min) / (obj.config.max - obj.config.min), obj.config.levelColors, obj.config.noGradient, obj.config.customSectors),
+ pki: [
+ obj.config.min,
+ obj.config.min,
+ obj.config.max,
+ obj.params.widgetW,
+ obj.params.widgetH,
+ obj.params.dx,
+ obj.params.dy,
+ obj.config.gaugeWidthScale,
+ obj.config.donut
+ ]
+ });
+ if(obj.config.donut) {
+ obj.level.transform("r" + obj.config.donutStartAngle + ", " + (obj.params.widgetW/2 + obj.params.dx) + ", " + (obj.params.widgetH/1.95 + obj.params.dy));
+ }
+
+ // title
+ obj.txtTitle = obj.canvas.text(obj.params.titleX, obj.params.titleY, obj.config.title);
+ obj.txtTitle.attr({
+ "font-size":obj.params.titleFontSize,
+ "font-weight":"bold",
+ "font-family":"Arial",
+ "fill":obj.config.titleFontColor,
+ "fill-opacity":"1"
+ });
+ setDy(obj.txtTitle, obj.params.titleFontSize, obj.params.titleY);
+
+ // value
+ obj.txtValue = obj.canvas.text(obj.params.valueX, obj.params.valueY, 0);
+ obj.txtValue.attr({
+ "font-size":obj.params.valueFontSize,
+ "font-weight":"bold",
+ "font-family":"Arial",
+ "fill":obj.config.valueFontColor,
+ "fill-opacity":"0"
+ });
+ setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
+
+ // label
+ obj.txtLabel = obj.canvas.text(obj.params.labelX, obj.params.labelY, obj.config.label);
+ obj.txtLabel.attr({
+ "font-size":obj.params.labelFontSize,
+ "font-weight":"normal",
+ "font-family":"Arial",
+ "fill":obj.config.labelFontColor,
+ "fill-opacity":"0"
+ });
+ setDy(obj.txtLabel, obj.params.labelFontSize, obj.params.labelY);
+
+ // min
+ obj.txtMinimum = obj.config.min;
+ if( obj.config.humanFriendly ) obj.txtMinimum = humanFriendlyNumber( obj.config.min, obj.config.humanFriendlyDecimal );
+ obj.txtMin = obj.canvas.text(obj.params.minX, obj.params.minY, obj.txtMinimum);
+ obj.txtMin.attr({
+ "font-size":obj.params.minFontSize,
+ "font-weight":"normal",
+ "font-family":"Arial",
+ "fill":obj.config.labelFontColor,
+ "fill-opacity": (obj.config.hideMinMax || obj.config.donut)? "0" : "1"
+ });
+ setDy(obj.txtMin, obj.params.minFontSize, obj.params.minY);
+
+ // max
+ obj.txtMaximum = obj.config.max;
+ if( obj.config.humanFriendly ) obj.txtMaximum = humanFriendlyNumber( obj.config.max, obj.config.humanFriendlyDecimal );
+ obj.txtMax = obj.canvas.text(obj.params.maxX, obj.params.maxY, obj.txtMaximum);
+ obj.txtMax.attr({
+ "font-size":obj.params.maxFontSize,
+ "font-weight":"normal",
+ "font-family":"Arial",
+ "fill":obj.config.labelFontColor,
+ "fill-opacity": (obj.config.hideMinMax || obj.config.donut)? "0" : "1"
+ });
+ setDy(obj.txtMax, obj.params.maxFontSize, obj.params.maxY);
+
+ var defs = obj.canvas.canvas.childNodes[1];
+ var svg = "http://www.w3.org/2000/svg";
+
+ if (ie < 9) {
+ onCreateElementNsReady(function() {
+ obj.generateShadow(svg, defs);
+ });
+ } else {
+ obj.generateShadow(svg, defs);
+ }
+
+ // var clear
+ defs, svg = null;
+
+ // set value to display
+ if(obj.config.textRenderer) {
+ obj.originalValue = obj.config.textRenderer(obj.originalValue);
+ } else if(obj.config.humanFriendly) {
+ obj.originalValue = humanFriendlyNumber( obj.originalValue, obj.config.humanFriendlyDecimal ) + obj.config.symbol;
+ } else {
+ obj.originalValue = (obj.originalValue * 1).toFixed(obj.config.decimals) + obj.config.symbol;
+ }
+
+ if(obj.config.counter === true) {
+ //on each animation frame
+ eve.on("raphael.anim.frame." + (obj.level.id), function() {
+ var currentValue = obj.level.attr("pki");
+ if(obj.config.textRenderer) {
+ obj.txtValue.attr("text", obj.config.textRenderer(Math.floor(currentValue[0])));
+ } else if(obj.config.humanFriendly) {
+ obj.txtValue.attr("text", humanFriendlyNumber( Math.floor(currentValue[0]), obj.config.humanFriendlyDecimal ) + obj.config.symbol);
+ } else {
+ obj.txtValue.attr("text", (currentValue[0] * 1).toFixed(obj.config.decimals) + obj.config.symbol);
+ }
+ setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
+ currentValue = null;
+ });
+ //on animation end
+ eve.on("raphael.anim.finish." + (obj.level.id), function() {
+ obj.txtValue.attr({"text" : obj.originalValue});
+ setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
+ });
+ } else {
+ //on animation start
+ eve.on("raphael.anim.start." + (obj.level.id), function() {
+ obj.txtValue.attr({"text" : obj.originalValue});
+ setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
+ });
+ }
+
+ // animate gauge level, value & label
+ obj.level.animate({
+ pki: [
+ obj.config.value,
+ obj.config.min,
+ obj.config.max,
+ obj.params.widgetW,
+ obj.params.widgetH,
+ obj.params.dx,
+ obj.params.dy,
+ obj.config.gaugeWidthScale,
+ obj.config.donut
+ ]
+ }, obj.config.startAnimationTime, obj.config.startAnimationType);
+ obj.txtValue.animate({"fill-opacity":(obj.config.hideValue)?"0":"1"}, obj.config.startAnimationTime, obj.config.startAnimationType);
+ obj.txtLabel.animate({"fill-opacity":"1"}, obj.config.startAnimationTime, obj.config.startAnimationType);
+};
+
+/** Refresh gauge level */
+JustGage.prototype.refresh = function(val, max) {
+
+ var obj = this;
+ var displayVal, color, max = max || null;
+
+ // set new max
+ if(max !== null) {
+ obj.config.max = max;
+
+ obj.txtMaximum = obj.config.max;
+ if( obj.config.humanFriendly ) obj.txtMaximum = humanFriendlyNumber( obj.config.max, obj.config.humanFriendlyDecimal );
+ obj.txtMax.attr({"text" : obj.txtMaximum});
+ setDy(obj.txtMax, obj.params.maxFontSize, obj.params.maxY);
+ }
+
+ // overflow values
+ displayVal = val;
+ if ((val * 1) > (obj.config.max * 1)) {val = (obj.config.max * 1);}
+ if ((val * 1) < (obj.config.min * 1)) {val = (obj.config.min * 1);}
+
+ color = getColor(val, (val - obj.config.min) / (obj.config.max - obj.config.min), obj.config.levelColors, obj.config.noGradient, obj.config.customSectors);
+
+ if(obj.config.textRenderer) {
+ displayVal = obj.config.textRenderer(displayVal);
+ } else if( obj.config.humanFriendly ) {
+ displayVal = humanFriendlyNumber( displayVal, obj.config.humanFriendlyDecimal ) + obj.config.symbol;
+ } else {
+ displayVal = (displayVal * 1).toFixed(obj.config.decimals) + obj.config.symbol;
+ }
+ obj.originalValue = displayVal;
+ obj.config.value = val * 1;
+
+ if(!obj.config.counter) {
+ obj.txtValue.attr({"text":displayVal});
+ setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
+ }
+
+ obj.level.animate({
+ pki: [
+ obj.config.value,
+ obj.config.min,
+ obj.config.max,
+ obj.params.widgetW,
+ obj.params.widgetH,
+ obj.params.dx,
+ obj.params.dy,
+ obj.config.gaugeWidthScale,
+ obj.config.donut
+ ],
+ "fill":color
+ }, obj.config.refreshAnimationTime, obj.config.refreshAnimationType);
+
+ // var clear
+ obj, displayVal, color, max = null;
+};
+
+/** Generate shadow */
+JustGage.prototype.generateShadow = function(svg, defs) {
+
+ var obj = this;
+ var gaussFilter, feOffset, feGaussianBlur, feComposite1, feFlood, feComposite2, feComposite3;
+
+ // FILTER
+ gaussFilter = document.createElementNS(svg,"filter");
+ gaussFilter.setAttribute("id","inner-shadow");
+ defs.appendChild(gaussFilter);
+
+ // offset
+ feOffset = document.createElementNS(svg,"feOffset");
+ feOffset.setAttribute("dx", 0);
+ feOffset.setAttribute("dy", obj.config.shadowVerticalOffset);
+ gaussFilter.appendChild(feOffset);
+
+ // blur
+ feGaussianBlur = document.createElementNS(svg,"feGaussianBlur");
+ feGaussianBlur.setAttribute("result","offset-blur");
+ feGaussianBlur.setAttribute("stdDeviation", obj.config.shadowSize);
+ gaussFilter.appendChild(feGaussianBlur);
+
+ // composite 1
+ feComposite1 = document.createElementNS(svg,"feComposite");
+ feComposite1.setAttribute("operator","out");
+ feComposite1.setAttribute("in", "SourceGraphic");
+ feComposite1.setAttribute("in2","offset-blur");
+ feComposite1.setAttribute("result","inverse");
+ gaussFilter.appendChild(feComposite1);
+
+ // flood
+ feFlood = document.createElementNS(svg,"feFlood");
+ feFlood.setAttribute("flood-color","black");
+ feFlood.setAttribute("flood-opacity", obj.config.shadowOpacity);
+ feFlood.setAttribute("result","color");
+ gaussFilter.appendChild(feFlood);
+
+ // composite 2
+ feComposite2 = document.createElementNS(svg,"feComposite");
+ feComposite2.setAttribute("operator","in");
+ feComposite2.setAttribute("in", "color");
+ feComposite2.setAttribute("in2","inverse");
+ feComposite2.setAttribute("result","shadow");
+ gaussFilter.appendChild(feComposite2);
+
+ // composite 3
+ feComposite3 = document.createElementNS(svg,"feComposite");
+ feComposite3.setAttribute("operator","over");
+ feComposite3.setAttribute("in", "shadow");
+ feComposite3.setAttribute("in2","SourceGraphic");
+ gaussFilter.appendChild(feComposite3);
+
+ // set shadow
+ if (!obj.config.hideInnerShadow) {
+ obj.canvas.canvas.childNodes[2].setAttribute("filter", "url(#inner-shadow)");
+ obj.canvas.canvas.childNodes[3].setAttribute("filter", "url(#inner-shadow)");
+ }
+
+ // var clear
+ gaussFilter, feOffset, feGaussianBlur, feComposite1, feFlood, feComposite2, feComposite3 = null;
+
+};
+
+/** Get color for value */
+function getColor(val, pct, col, noGradient, custSec) {
+
+ var no, inc, colors, percentage, rval, gval, bval, lower, upper, range, rangePct, pctLower, pctUpper, color;
+ var noGradient = noGradient || custSec.length > 0;
+
+ if(custSec.length > 0) {
+ for(var i = 0; i < custSec.length; i++) {
+ if(val > custSec[i].lo && val <= custSec[i].hi) {
+ return custSec[i].color;
+ }
+ }
+ }
+
+ no = col.length;
+ if (no === 1) return col[0];
+ inc = (noGradient) ? (1 / no) : (1 / (no - 1));
+ colors = [];
+ for (var i = 0; i < col.length; i++) {
+ percentage = (noGradient) ? (inc * (i + 1)) : (inc * i);
+ rval = parseInt((cutHex(col[i])).substring(0,2),16);
+ gval = parseInt((cutHex(col[i])).substring(2,4),16);
+ bval = parseInt((cutHex(col[i])).substring(4,6),16);
+ colors[i] = { pct: percentage, color: { r: rval, g: gval, b: bval } };
+ }
+
+ if(pct === 0) {
+ return 'rgb(' + [colors[0].color.r, colors[0].color.g, colors[0].color.b].join(',') + ')';
+ }
+
+ for (var j = 0; j < colors.length; j++) {
+ if (pct <= colors[j].pct) {
+ if (noGradient) {
+ return 'rgb(' + [colors[j].color.r, colors[j].color.g, colors[j].color.b].join(',') + ')';
+ } else {
+ lower = colors[j - 1];
+ upper = colors[j];
+ range = upper.pct - lower.pct;
+ rangePct = (pct - lower.pct) / range;
+ pctLower = 1 - rangePct;
+ pctUpper = rangePct;
+ color = {
+ r: Math.floor(lower.color.r * pctLower + upper.color.r * pctUpper),
+ g: Math.floor(lower.color.g * pctLower + upper.color.g * pctUpper),
+ b: Math.floor(lower.color.b * pctLower + upper.color.b * pctUpper)
+ };
+ return 'rgb(' + [color.r, color.g, color.b].join(',') + ')';
+ }
+ }
+ }
+
+}
+
+/** Fix Raphael display:none tspan dy attribute bug */
+function setDy(elem, fontSize, txtYpos) {
+ if (!ie || ie > 9) {
+ elem.node.firstChild.attributes.dy.value = 0;
+ }
+}
+
+/** Random integer */
+function getRandomInt (min, max) {
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+}
+
+/** Cut hex */
+function cutHex(str) {
+ return (str.charAt(0)=="#") ? str.substring(1,7):str;
+}
+
+/** Human friendly number suffix - From: http://stackoverflow.com/questions/2692323/code-golf-friendly-number-abbreviator */
+function humanFriendlyNumber( n, d ) {
+ var p, d2, i, s;
+
+ p = Math.pow;
+ d2 = p(10, d);
+ i = 7;
+ while( i ) {
+ s = p(10,i--*3);
+ if( s <= n ) {
+ n = Math.round(n*d2/s)/d2+"KMGTPE"[i];
+ }
+ }
+ return n;
+}
+
+/** Get style */
+function getStyle(oElm, strCssRule){
+ var strValue = "";
+ if(document.defaultView && document.defaultView.getComputedStyle){
+ strValue = document.defaultView.getComputedStyle(oElm, "").getPropertyValue(strCssRule);
+ }
+ else if(oElm.currentStyle){
+ strCssRule = strCssRule.replace(/\-(\w)/g, function (strMatch, p1){
+ return p1.toUpperCase();
+ });
+ strValue = oElm.currentStyle[strCssRule];
+ }
+ return strValue;
+}
+
+/** Create Element NS Ready */
+function onCreateElementNsReady(func) {
+ if (document.createElementNS !== undefined) {
+ func();
+ } else {
+ setTimeout(function() { onCreateElementNsReady(func); }, 100);
+ }
+}
+
+/** Get IE version */
+// ----------------------------------------------------------
+// A short snippet for detecting versions of IE in JavaScript
+// without resorting to user-agent sniffing
+// ----------------------------------------------------------
+// If you're not in IE (or IE version is less than 5) then:
+// ie === undefined
+// If you're in IE (>=5) then you can determine which version:
+// ie === 7; // IE7
+// Thus, to detect IE:
+// if (ie) {}
+// And to detect the version:
+// ie === 6 // IE6
+// ie > 7 // IE8, IE9 ...
+// ie < 9 // Anything less than IE9
+// ----------------------------------------------------------
+// UPDATE: Now using Live NodeList idea from @jdalton
+var ie = (function(){
+
+ var undef,
+ v = 3,
+ div = document.createElement('div'),
+ all = div.getElementsByTagName('i');
+
+ while (
+ div.innerHTML = '',
+ all[0]
+ );
+ return v > 4 ? v : undef;
+}());
\ No newline at end of file
diff --git a/addons/web_kanban_gauge/static/src/js/kanban_gauge.js b/addons/web_kanban_gauge/static/src/js/kanban_gauge.js
new file mode 100644
index 00000000000..cb2faf6874b
--- /dev/null
+++ b/addons/web_kanban_gauge/static/src/js/kanban_gauge.js
@@ -0,0 +1,119 @@
+openerp.web_kanban_gauge = function (instance) {
+
+/**
+ * Kanban widgets: GaugeWidget
+ *
+ */
+
+instance.web_kanban.GaugeWidget = instance.web_kanban.AbstractField.extend({
+ className: "oe_gauge",
+ start: function() {
+ var self = this;
+ var parent = this.getParent();
+ var max = 100;
+ if (this.options.max_field) {
+ max = this.getParent().record[this.options.max_field].raw_value;
+ }
+ var label = this.options.label || "";
+ if (this.options.label_field) {
+ label = this.getParent().record[this.options.label_field].raw_value;
+ }
+ var val = this.field.value;
+ var value = _.isArray(val) && val.length ? val[val.length-1]['value'] : val;
+ var title = this.$node.html() || this.field.string;
+ // var unique_id = _.uniqueId("JustGage");
+
+ this.$el.empty()
+ .attr('style', this.$node.attr('style') + ';position:relative; display:inline-block;');
+
+ this.gage = new JustGage({
+ parentNode: this.$el[0],
+ // id: unique_id,
+ value: value,
+ title: title,
+ min: 0,
+ max: max,
+ relativeGaugeSize: true,
+ humanFriendly: true,
+ titleFontColor: '#333333',
+ valueFontColor: '#333333',
+ labelFontColor: '#000',
+ label: label,
+ levelColors: [
+ "#ff0000",
+ "#f9c802",
+ "#a9d70b"
+ ],
+ });
+ this.gage.refresh(value, max);
+
+ var flag_open = false;
+ if (this.options.action_change) {
+ var $svg = this.$el.find('svg');
+ var css = {
+ 'text-align': 'center',
+ 'position': 'absolute',
+ 'width': this.$el.outerWidth() + 'px',
+ 'top': (this.$el.outerHeight()/2-5) + 'px'
+ };
+
+ this.$el.click(function (event) {
+ event.stopPropagation();
+ flag_open = false;
+ if (!parent.view.is_action_enabled('edit')) {
+ return;
+ }
+ if (!self.$el.find(".oe_justgage_edit").size()) {
+ $div = $('');
+ $div.css(css);
+ $input = $('').val(value);
+ $input.css({
+ 'text-align': 'center',
+ 'margin': 'auto',
+ 'width': ($svg.outerWidth()-40) + 'px'
+ });
+ $div.append($input);
+ self.$el.prepend($div)
+ $input.focus()
+ .keydown(function (event) {
+ event.stopPropagation();
+ if (event.keyCode == 13 || event.keyCode == 9) {
+ if ($input.val() != value) {
+ parent.view.dataset.call(self.options.action_change, [parent.id, $input.val()]).then(function () {
+ parent.do_reload();
+ });
+ } else {
+ $div.remove();
+ }
+ }
+ })
+ .click(function (event) {
+ event.stopPropagation();
+ flag_open = false;
+ })
+ .blur(function (event) {
+ if(!flag_open) {
+ self.$el.find(".oe_justgage_edit").remove();
+ } else {
+ flag_open = false;
+ setTimeout(function () {$input.focus();}, 0);
+ }
+ });
+ }
+ }).mousedown(function () {
+ flag_open = true;
+ });
+
+ if (!+value) {
+ $svg.fadeTo(0, 0.3);
+ $div = $('').text(_t("Click to change value"));
+ $div.css(css);
+ this.$el.append($div);
+ }
+ }
+ },
+});
+
+instance.web_kanban.fields_registry.add("gauge", "instance.web_kanban.GaugeWidget");
+
+}
diff --git a/addons/web_kanban_sparkline/__init__.py b/addons/web_kanban_sparkline/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/addons/web_kanban_sparkline/__openerp__.py b/addons/web_kanban_sparkline/__openerp__.py
new file mode 100644
index 00000000000..3fb4e13b4f7
--- /dev/null
+++ b/addons/web_kanban_sparkline/__openerp__.py
@@ -0,0 +1,18 @@
+{
+ 'name': 'Sparkline Widget for Kanban',
+ 'category': 'Hidden',
+ 'description': """
+This widget allows to display sparklines using jquery.sparkline library.
+""",
+ 'version': '1.0',
+ 'depends': ['web_kanban'],
+ 'js': [
+ "static/lib/jquery.sparkline/jquery.sparkline.js",
+ 'static/src/js/kanban_sparkline.js'
+ ],
+ 'css': [
+ ],
+ 'qweb': [
+ ],
+ 'auto_install': False,
+}
diff --git a/addons/web_kanban_sparkline/static/lib/jquery.sparkline/jquery.sparkline.js b/addons/web_kanban_sparkline/static/lib/jquery.sparkline/jquery.sparkline.js
new file mode 100644
index 00000000000..43b24c080a6
--- /dev/null
+++ b/addons/web_kanban_sparkline/static/lib/jquery.sparkline/jquery.sparkline.js
@@ -0,0 +1,3054 @@
+/**
+*
+* jquery.sparkline.js
+*
+* v2.1.2
+* (c) Splunk, Inc
+* Contact: Gareth Watts (gareth@splunk.com)
+* http://omnipotent.net/jquery.sparkline/
+*
+* Generates inline sparkline charts from data supplied either to the method
+* or inline in HTML
+*
+* Compatible with Internet Explorer 6.0+ and modern browsers equipped with the canvas tag
+* (Firefox 2.0+, Safari, Opera, etc)
+*
+* License: New BSD License
+*
+* Copyright (c) 2012, Splunk Inc.
+* All rights reserved.
+*
+* Redistribution and use in source and binary forms, with or without modification,
+* are permitted provided that the following conditions are met:
+*
+* * Redistributions of source code must retain the above copyright notice,
+* this list of conditions and the following disclaimer.
+* * Redistributions in binary form must reproduce the above copyright notice,
+* this list of conditions and the following disclaimer in the documentation
+* and/or other materials provided with the distribution.
+* * Neither the name of Splunk Inc nor the names of its contributors may
+* be used to endorse or promote products derived from this software without
+* specific prior written permission.
+*
+* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
+* SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+* OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*
+*
+* Usage:
+* $(selector).sparkline(values, options)
+*
+* If values is undefined or set to 'html' then the data values are read from the specified tag:
+*
Sparkline: 1,4,6,6,8,5,3,5
+* $('.sparkline').sparkline();
+* There must be no spaces in the enclosed data set
+*
+* Otherwise values must be an array of numbers or null values
+*
Sparkline: This text replaced if the browser is compatible
+* $('#sparkline1').sparkline([1,4,6,6,8,5,3,5])
+* $('#sparkline2').sparkline([1,4,6,null,null,5,3,5])
+*
+* Values can also be specified in an HTML comment, or as a values attribute:
+*
Sparkline:
+*
Sparkline:
+* $('.sparkline').sparkline();
+*
+* For line charts, x values can also be specified:
+*
Sparkline: 1:1,2.7:4,3.4:6,5:6,6:8,8.7:5,9:3,10:5
+* $('#sparkline1').sparkline([ [1,1], [2.7,4], [3.4,6], [5,6], [6,8], [8.7,5], [9,3], [10,5] ])
+*
+* By default, options should be passed in as teh second argument to the sparkline function:
+* $('.sparkline').sparkline([1,2,3,4], {type: 'bar'})
+*
+* Options can also be set by passing them on the tag itself. This feature is disabled by default though
+* as there's a slight performance overhead:
+* $('.sparkline').sparkline([1,2,3,4], {enableTagOptions: true})
+*
Sparkline: loading
+* Prefix all options supplied as tag attribute with "spark" (configurable by setting tagOptionPrefix)
+*
+* Supported options:
+* lineColor - Color of the line used for the chart
+* fillColor - Color used to fill in the chart - Set to '' or false for a transparent chart
+* width - Width of the chart - Defaults to 3 times the number of values in pixels
+* height - Height of the chart - Defaults to the height of the containing element
+* chartRangeMin - Specify the minimum value to use for the Y range of the chart - Defaults to the minimum value supplied
+* chartRangeMax - Specify the maximum value to use for the Y range of the chart - Defaults to the maximum value supplied
+* chartRangeClip - Clip out of range values to the max/min specified by chartRangeMin and chartRangeMax
+* chartRangeMinX - Specify the minimum value to use for the X range of the chart - Defaults to the minimum value supplied
+* chartRangeMaxX - Specify the maximum value to use for the X range of the chart - Defaults to the maximum value supplied
+* composite - If true then don't erase any existing chart attached to the tag, but draw
+* another chart over the top - Note that width and height are ignored if an
+* existing chart is detected.
+* tagValuesAttribute - Name of tag attribute to check for data values - Defaults to 'values'
+* enableTagOptions - Whether to check tags for sparkline options
+* tagOptionPrefix - Prefix used for options supplied as tag attributes - Defaults to 'spark'
+* disableHiddenCheck - If set to true, then the plugin will assume that charts will never be drawn into a
+* hidden dom element, avoding a browser reflow
+* disableInteraction - If set to true then all mouseover/click interaction behaviour will be disabled,
+* making the plugin perform much like it did in 1.x
+* disableTooltips - If set to true then tooltips will be disabled - Defaults to false (tooltips enabled)
+* disableHighlight - If set to true then highlighting of selected chart elements on mouseover will be disabled
+* defaults to false (highlights enabled)
+* highlightLighten - Factor to lighten/darken highlighted chart values by - Defaults to 1.4 for a 40% increase
+* tooltipContainer - Specify which DOM element the tooltip should be rendered into - defaults to document.body
+* tooltipClassname - Optional CSS classname to apply to tooltips - If not specified then a default style will be applied
+* tooltipOffsetX - How many pixels away from the mouse pointer to render the tooltip on the X axis
+* tooltipOffsetY - How many pixels away from the mouse pointer to render the tooltip on the r axis
+* tooltipFormatter - Optional callback that allows you to override the HTML displayed in the tooltip
+* callback is given arguments of (sparkline, options, fields)
+* tooltipChartTitle - If specified then the tooltip uses the string specified by this setting as a title
+* tooltipFormat - A format string or SPFormat object (or an array thereof for multiple entries)
+* to control the format of the tooltip
+* tooltipPrefix - A string to prepend to each field displayed in a tooltip
+* tooltipSuffix - A string to append to each field displayed in a tooltip
+* tooltipSkipNull - If true then null values will not have a tooltip displayed (defaults to true)
+* tooltipValueLookups - An object or range map to map field values to tooltip strings
+* (eg. to map -1 to "Lost", 0 to "Draw", and 1 to "Win")
+* numberFormatter - Optional callback for formatting numbers in tooltips
+* numberDigitGroupSep - Character to use for group separator in numbers "1,234" - Defaults to ","
+* numberDecimalMark - Character to use for the decimal point when formatting numbers - Defaults to "."
+* numberDigitGroupCount - Number of digits between group separator - Defaults to 3
+*
+* There are 7 types of sparkline, selected by supplying a "type" option of 'line' (default),
+* 'bar', 'tristate', 'bullet', 'discrete', 'pie' or 'box'
+* line - Line chart. Options:
+* spotColor - Set to '' to not end each line in a circular spot
+* minSpotColor - If set, color of spot at minimum value
+* maxSpotColor - If set, color of spot at maximum value
+* spotRadius - Radius in pixels
+* lineWidth - Width of line in pixels
+* normalRangeMin
+* normalRangeMax - If set draws a filled horizontal bar between these two values marking the "normal"
+* or expected range of values
+* normalRangeColor - Color to use for the above bar
+* drawNormalOnTop - Draw the normal range above the chart fill color if true
+* defaultPixelsPerValue - Defaults to 3 pixels of width for each value in the chart
+* highlightSpotColor - The color to use for drawing a highlight spot on mouseover - Set to null to disable
+* highlightLineColor - The color to use for drawing a highlight line on mouseover - Set to null to disable
+* valueSpots - Specify which points to draw spots on, and in which color. Accepts a range map
+*
+* bar - Bar chart. Options:
+* barColor - Color of bars for postive values
+* negBarColor - Color of bars for negative values
+* zeroColor - Color of bars with zero values
+* nullColor - Color of bars with null values - Defaults to omitting the bar entirely
+* barWidth - Width of bars in pixels
+* colorMap - Optional mappnig of values to colors to override the *BarColor values above
+* can be an Array of values to control the color of individual bars or a range map
+* to specify colors for individual ranges of values
+* barSpacing - Gap between bars in pixels
+* zeroAxis - Centers the y-axis around zero if true
+*
+* tristate - Charts values of win (>0), lose (<0) or draw (=0)
+* posBarColor - Color of win values
+* negBarColor - Color of lose values
+* zeroBarColor - Color of draw values
+* barWidth - Width of bars in pixels
+* barSpacing - Gap between bars in pixels
+* colorMap - Optional mappnig of values to colors to override the *BarColor values above
+* can be an Array of values to control the color of individual bars or a range map
+* to specify colors for individual ranges of values
+*
+* discrete - Options:
+* lineHeight - Height of each line in pixels - Defaults to 30% of the graph height
+* thesholdValue - Values less than this value will be drawn using thresholdColor instead of lineColor
+* thresholdColor
+*
+* bullet - Values for bullet graphs msut be in the order: target, performance, range1, range2, range3, ...
+* options:
+* targetColor - The color of the vertical target marker
+* targetWidth - The width of the target marker in pixels
+* performanceColor - The color of the performance measure horizontal bar
+* rangeColors - Colors to use for each qualitative range background color
+*
+* pie - Pie chart. Options:
+* sliceColors - An array of colors to use for pie slices
+* offset - Angle in degrees to offset the first slice - Try -90 or +90
+* borderWidth - Width of border to draw around the pie chart, in pixels - Defaults to 0 (no border)
+* borderColor - Color to use for the pie chart border - Defaults to #000
+*
+* box - Box plot. Options:
+* raw - Set to true to supply pre-computed plot points as values
+* values should be: low_outlier, low_whisker, q1, median, q3, high_whisker, high_outlier
+* When set to false you can supply any number of values and the box plot will
+* be computed for you. Default is false.
+* showOutliers - Set to true (default) to display outliers as circles
+* outlierIQR - Interquartile range used to determine outliers. Default 1.5
+* boxLineColor - Outline color of the box
+* boxFillColor - Fill color for the box
+* whiskerColor - Line color used for whiskers
+* outlierLineColor - Outline color of outlier circles
+* outlierFillColor - Fill color of the outlier circles
+* spotRadius - Radius of outlier circles
+* medianColor - Line color of the median line
+* target - Draw a target cross hair at the supplied value (default undefined)
+*
+*
+*
+* Examples:
+* $('#sparkline1').sparkline(myvalues, { lineColor: '#f00', fillColor: false });
+* $('.barsparks').sparkline('html', { type:'bar', height:'40px', barWidth:5 });
+* $('#tristate').sparkline([1,1,-1,1,0,0,-1], { type:'tristate' }):
+* $('#discrete').sparkline([1,3,4,5,5,3,4,5], { type:'discrete' });
+* $('#bullet').sparkline([10,12,12,9,7], { type:'bullet' });
+* $('#pie').sparkline([1,1,2], { type:'pie' });
+*/
+
+/*jslint regexp: true, browser: true, jquery: true, white: true, nomen: false, plusplus: false, maxerr: 500, indent: 4 */
+
+(function(document, Math, undefined) { // performance/minified-size optimization
+(function(factory) {
+ if(typeof define === 'function' && define.amd) {
+ define(['jquery'], factory);
+ } else if (jQuery && !jQuery.fn.sparkline) {
+ factory(jQuery);
+ }
+}
+(function($) {
+ 'use strict';
+
+ var UNSET_OPTION = {},
+ getDefaults, createClass, SPFormat, clipval, quartile, normalizeValue, normalizeValues,
+ remove, isNumber, all, sum, addCSS, ensureArray, formatNumber, RangeMap,
+ MouseHandler, Tooltip, barHighlightMixin,
+ line, bar, tristate, discrete, bullet, pie, box, defaultStyles, initStyles,
+ VShape, VCanvas_base, VCanvas_canvas, VCanvas_vml, pending, shapeCount = 0;
+
+ /**
+ * Default configuration settings
+ */
+ getDefaults = function () {
+ return {
+ // Settings common to most/all chart types
+ common: {
+ type: 'line',
+ lineColor: '#00f',
+ fillColor: '#cdf',
+ defaultPixelsPerValue: 3,
+ width: 'auto',
+ height: 'auto',
+ composite: false,
+ tagValuesAttribute: 'values',
+ tagOptionsPrefix: 'spark',
+ enableTagOptions: false,
+ enableHighlight: true,
+ highlightLighten: 1.4,
+ tooltipSkipNull: true,
+ tooltipPrefix: '',
+ tooltipSuffix: '',
+ disableHiddenCheck: false,
+ numberFormatter: false,
+ numberDigitGroupCount: 3,
+ numberDigitGroupSep: ',',
+ numberDecimalMark: '.',
+ disableTooltips: false,
+ disableInteraction: false
+ },
+ // Defaults for line charts
+ line: {
+ spotColor: '#f80',
+ highlightSpotColor: '#5f5',
+ highlightLineColor: '#f22',
+ spotRadius: 1.5,
+ minSpotColor: '#f80',
+ maxSpotColor: '#f80',
+ lineWidth: 1,
+ normalRangeMin: undefined,
+ normalRangeMax: undefined,
+ normalRangeColor: '#ccc',
+ drawNormalOnTop: false,
+ chartRangeMin: undefined,
+ chartRangeMax: undefined,
+ chartRangeMinX: undefined,
+ chartRangeMaxX: undefined,
+ tooltipFormat: new SPFormat('● {{prefix}}{{y}}{{suffix}}')
+ },
+ // Defaults for bar charts
+ bar: {
+ barColor: '#3366cc',
+ negBarColor: '#f44',
+ stackedBarColor: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#66aa00',
+ '#dd4477', '#0099c6', '#990099'],
+ zeroColor: undefined,
+ nullColor: undefined,
+ zeroAxis: true,
+ barWidth: 4,
+ barSpacing: 1,
+ chartRangeMax: undefined,
+ chartRangeMin: undefined,
+ chartRangeClip: false,
+ colorMap: undefined,
+ tooltipFormat: new SPFormat('● {{prefix}}{{value}}{{suffix}}')
+ },
+ // Defaults for tristate charts
+ tristate: {
+ barWidth: 4,
+ barSpacing: 1,
+ posBarColor: '#6f6',
+ negBarColor: '#f44',
+ zeroBarColor: '#999',
+ colorMap: {},
+ tooltipFormat: new SPFormat('● {{value:map}}'),
+ tooltipValueLookups: { map: { '-1': 'Loss', '0': 'Draw', '1': 'Win' } }
+ },
+ // Defaults for discrete charts
+ discrete: {
+ lineHeight: 'auto',
+ thresholdColor: undefined,
+ thresholdValue: 0,
+ chartRangeMax: undefined,
+ chartRangeMin: undefined,
+ chartRangeClip: false,
+ tooltipFormat: new SPFormat('{{prefix}}{{value}}{{suffix}}')
+ },
+ // Defaults for bullet charts
+ bullet: {
+ targetColor: '#f33',
+ targetWidth: 3, // width of the target bar in pixels
+ performanceColor: '#33f',
+ rangeColors: ['#d3dafe', '#a8b6ff', '#7f94ff'],
+ base: undefined, // set this to a number to change the base start number
+ tooltipFormat: new SPFormat('{{fieldkey:fields}} - {{value}}'),
+ tooltipValueLookups: { fields: {r: 'Range', p: 'Performance', t: 'Target'} }
+ },
+ // Defaults for pie charts
+ pie: {
+ offset: 0,
+ sliceColors: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#66aa00',
+ '#dd4477', '#0099c6', '#990099'],
+ borderWidth: 0,
+ borderColor: '#000',
+ tooltipFormat: new SPFormat('● {{value}} ({{percent.1}}%)')
+ },
+ // Defaults for box plots
+ box: {
+ raw: false,
+ boxLineColor: '#000',
+ boxFillColor: '#cdf',
+ whiskerColor: '#000',
+ outlierLineColor: '#333',
+ outlierFillColor: '#fff',
+ medianColor: '#f00',
+ showOutliers: true,
+ outlierIQR: 1.5,
+ spotRadius: 1.5,
+ target: undefined,
+ targetColor: '#4a2',
+ chartRangeMax: undefined,
+ chartRangeMin: undefined,
+ tooltipFormat: new SPFormat('{{field:fields}}: {{value}}'),
+ tooltipFormatFieldlistKey: 'field',
+ tooltipValueLookups: { fields: { lq: 'Lower Quartile', med: 'Median',
+ uq: 'Upper Quartile', lo: 'Left Outlier', ro: 'Right Outlier',
+ lw: 'Left Whisker', rw: 'Right Whisker'} }
+ }
+ };
+ };
+
+ // You can have tooltips use a css class other than jqstooltip by specifying tooltipClassname
+ defaultStyles = '.jqstooltip { ' +
+ 'position: absolute;' +
+ 'left: 0px;' +
+ 'top: 0px;' +
+ 'visibility: hidden;' +
+ 'background: rgb(0, 0, 0) transparent;' +
+ 'background-color: rgba(0,0,0,0.6);' +
+ 'filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000);' +
+ '-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000)";' +
+ 'color: white;' +
+ 'font: 10px arial, san serif;' +
+ 'text-align: left;' +
+ 'white-space: nowrap;' +
+ 'padding: 5px;' +
+ 'border: 1px solid white;' +
+ 'z-index: 10000;' +
+ '}' +
+ '.jqsfield { ' +
+ 'color: white;' +
+ 'font: 10px arial, san serif;' +
+ 'text-align: left;' +
+ '}';
+
+ /**
+ * Utilities
+ */
+
+ createClass = function (/* [baseclass, [mixin, ...]], definition */) {
+ var Class, args;
+ Class = function () {
+ this.init.apply(this, arguments);
+ };
+ if (arguments.length > 1) {
+ if (arguments[0]) {
+ Class.prototype = $.extend(new arguments[0](), arguments[arguments.length - 1]);
+ Class._super = arguments[0].prototype;
+ } else {
+ Class.prototype = arguments[arguments.length - 1];
+ }
+ if (arguments.length > 2) {
+ args = Array.prototype.slice.call(arguments, 1, -1);
+ args.unshift(Class.prototype);
+ $.extend.apply($, args);
+ }
+ } else {
+ Class.prototype = arguments[0];
+ }
+ Class.prototype.cls = Class;
+ return Class;
+ };
+
+ /**
+ * Wraps a format string for tooltips
+ * {{x}}
+ * {{x.2}
+ * {{x:months}}
+ */
+ $.SPFormatClass = SPFormat = createClass({
+ fre: /\{\{([\w.]+?)(:(.+?))?\}\}/g,
+ precre: /(\w+)\.(\d+)/,
+
+ init: function (format, fclass) {
+ this.format = format;
+ this.fclass = fclass;
+ },
+
+ render: function (fieldset, lookups, options) {
+ var self = this,
+ fields = fieldset,
+ match, token, lookupkey, fieldvalue, prec;
+ return this.format.replace(this.fre, function () {
+ var lookup;
+ token = arguments[1];
+ lookupkey = arguments[3];
+ match = self.precre.exec(token);
+ if (match) {
+ prec = match[2];
+ token = match[1];
+ } else {
+ prec = false;
+ }
+ fieldvalue = fields[token];
+ if (fieldvalue === undefined) {
+ return '';
+ }
+ if (lookupkey && lookups && lookups[lookupkey]) {
+ lookup = lookups[lookupkey];
+ if (lookup.get) { // RangeMap
+ return lookups[lookupkey].get(fieldvalue) || fieldvalue;
+ } else {
+ return lookups[lookupkey][fieldvalue] || fieldvalue;
+ }
+ }
+ if (isNumber(fieldvalue)) {
+ if (options.get('numberFormatter')) {
+ fieldvalue = options.get('numberFormatter')(fieldvalue);
+ } else {
+ fieldvalue = formatNumber(fieldvalue, prec,
+ options.get('numberDigitGroupCount'),
+ options.get('numberDigitGroupSep'),
+ options.get('numberDecimalMark'));
+ }
+ }
+ return fieldvalue;
+ });
+ }
+ });
+
+ // convience method to avoid needing the new operator
+ $.spformat = function(format, fclass) {
+ return new SPFormat(format, fclass);
+ };
+
+ clipval = function (val, min, max) {
+ if (val < min) {
+ return min;
+ }
+ if (val > max) {
+ return max;
+ }
+ return val;
+ };
+
+ quartile = function (values, q) {
+ var vl;
+ if (q === 2) {
+ vl = Math.floor(values.length / 2);
+ return values.length % 2 ? values[vl] : (values[vl-1] + values[vl]) / 2;
+ } else {
+ if (values.length % 2 ) { // odd
+ vl = (values.length * q + q) / 4;
+ return vl % 1 ? (values[Math.floor(vl)] + values[Math.floor(vl) - 1]) / 2 : values[vl-1];
+ } else { //even
+ vl = (values.length * q + 2) / 4;
+ return vl % 1 ? (values[Math.floor(vl)] + values[Math.floor(vl) - 1]) / 2 : values[vl-1];
+
+ }
+ }
+ };
+
+ normalizeValue = function (val) {
+ var nf;
+ switch (val) {
+ case 'undefined':
+ val = undefined;
+ break;
+ case 'null':
+ val = null;
+ break;
+ case 'true':
+ val = true;
+ break;
+ case 'false':
+ val = false;
+ break;
+ default:
+ nf = parseFloat(val);
+ if (val == nf) {
+ val = nf;
+ }
+ }
+ return val;
+ };
+
+ normalizeValues = function (vals) {
+ var i, result = [];
+ for (i = vals.length; i--;) {
+ result[i] = normalizeValue(vals[i]);
+ }
+ return result;
+ };
+
+ remove = function (vals, filter) {
+ var i, vl, result = [];
+ for (i = 0, vl = vals.length; i < vl; i++) {
+ if (vals[i] !== filter) {
+ result.push(vals[i]);
+ }
+ }
+ return result;
+ };
+
+ isNumber = function (num) {
+ return !isNaN(parseFloat(num)) && isFinite(num);
+ };
+
+ formatNumber = function (num, prec, groupsize, groupsep, decsep) {
+ var p, i;
+ num = (prec === false ? parseFloat(num).toString() : num.toFixed(prec)).split('');
+ p = (p = $.inArray('.', num)) < 0 ? num.length : p;
+ if (p < num.length) {
+ num[p] = decsep;
+ }
+ for (i = p - groupsize; i > 0; i -= groupsize) {
+ num.splice(i, 0, groupsep);
+ }
+ return num.join('');
+ };
+
+ // determine if all values of an array match a value
+ // returns true if the array is empty
+ all = function (val, arr, ignoreNull) {
+ var i;
+ for (i = arr.length; i--; ) {
+ if (ignoreNull && arr[i] === null) continue;
+ if (arr[i] !== val) {
+ return false;
+ }
+ }
+ return true;
+ };
+
+ // sums the numeric values in an array, ignoring other values
+ sum = function (vals) {
+ var total = 0, i;
+ for (i = vals.length; i--;) {
+ total += typeof vals[i] === 'number' ? vals[i] : 0;
+ }
+ return total;
+ };
+
+ ensureArray = function (val) {
+ return $.isArray(val) ? val : [val];
+ };
+
+ // http://paulirish.com/2008/bookmarklet-inject-new-css-rules/
+ addCSS = function(css) {
+ var tag;
+ //if ('\v' == 'v') /* ie only */ {
+ if (document.createStyleSheet) {
+ document.createStyleSheet().cssText = css;
+ } else {
+ tag = document.createElement('style');
+ tag.type = 'text/css';
+ document.getElementsByTagName('head')[0].appendChild(tag);
+ tag[(typeof document.body.style.WebkitAppearance == 'string') /* webkit only */ ? 'innerText' : 'innerHTML'] = css;
+ }
+ };
+
+ // Provide a cross-browser interface to a few simple drawing primitives
+ $.fn.simpledraw = function (width, height, useExisting, interact) {
+ var target, mhandler;
+ if (useExisting && (target = this.data('_jqs_vcanvas'))) {
+ return target;
+ }
+
+ if ($.fn.sparkline.canvas === false) {
+ // We've already determined that neither Canvas nor VML are available
+ return false;
+
+ } else if ($.fn.sparkline.canvas === undefined) {
+ // No function defined yet -- need to see if we support Canvas or VML
+ var el = document.createElement('canvas');
+ if (!!(el.getContext && el.getContext('2d'))) {
+ // Canvas is available
+ $.fn.sparkline.canvas = function(width, height, target, interact) {
+ return new VCanvas_canvas(width, height, target, interact);
+ };
+ } else if (document.namespaces && !document.namespaces.v) {
+ // VML is available
+ document.namespaces.add('v', 'urn:schemas-microsoft-com:vml', '#default#VML');
+ $.fn.sparkline.canvas = function(width, height, target, interact) {
+ return new VCanvas_vml(width, height, target);
+ };
+ } else {
+ // Neither Canvas nor VML are available
+ $.fn.sparkline.canvas = false;
+ return false;
+ }
+ }
+
+ if (width === undefined) {
+ width = $(this).innerWidth();
+ }
+ if (height === undefined) {
+ height = $(this).innerHeight();
+ }
+
+ target = $.fn.sparkline.canvas(width, height, this, interact);
+
+ mhandler = $(this).data('_jqs_mhandler');
+ if (mhandler) {
+ mhandler.registerCanvas(target);
+ }
+ return target;
+ };
+
+ $.fn.cleardraw = function () {
+ var target = this.data('_jqs_vcanvas');
+ if (target) {
+ target.reset();
+ }
+ };
+
+ $.RangeMapClass = RangeMap = createClass({
+ init: function (map) {
+ var key, range, rangelist = [];
+ for (key in map) {
+ if (map.hasOwnProperty(key) && typeof key === 'string' && key.indexOf(':') > -1) {
+ range = key.split(':');
+ range[0] = range[0].length === 0 ? -Infinity : parseFloat(range[0]);
+ range[1] = range[1].length === 0 ? Infinity : parseFloat(range[1]);
+ range[2] = map[key];
+ rangelist.push(range);
+ }
+ }
+ this.map = map;
+ this.rangelist = rangelist || false;
+ },
+
+ get: function (value) {
+ var rangelist = this.rangelist,
+ i, range, result;
+ if ((result = this.map[value]) !== undefined) {
+ return result;
+ }
+ if (rangelist) {
+ for (i = rangelist.length; i--;) {
+ range = rangelist[i];
+ if (range[0] <= value && range[1] >= value) {
+ return range[2];
+ }
+ }
+ }
+ return undefined;
+ }
+ });
+
+ // Convenience function
+ $.range_map = function(map) {
+ return new RangeMap(map);
+ };
+
+ MouseHandler = createClass({
+ init: function (el, options) {
+ var $el = $(el);
+ this.$el = $el;
+ this.options = options;
+ this.currentPageX = 0;
+ this.currentPageY = 0;
+ this.el = el;
+ this.splist = [];
+ this.tooltip = null;
+ this.over = false;
+ this.displayTooltips = !options.get('disableTooltips');
+ this.highlightEnabled = !options.get('disableHighlight');
+ },
+
+ registerSparkline: function (sp) {
+ this.splist.push(sp);
+ if (this.over) {
+ this.updateDisplay();
+ }
+ },
+
+ registerCanvas: function (canvas) {
+ var $canvas = $(canvas.canvas);
+ this.canvas = canvas;
+ this.$canvas = $canvas;
+ $canvas.mouseenter($.proxy(this.mouseenter, this));
+ $canvas.mouseleave($.proxy(this.mouseleave, this));
+ $canvas.click($.proxy(this.mouseclick, this));
+ },
+
+ reset: function (removeTooltip) {
+ this.splist = [];
+ if (this.tooltip && removeTooltip) {
+ this.tooltip.remove();
+ this.tooltip = undefined;
+ }
+ },
+
+ mouseclick: function (e) {
+ var clickEvent = $.Event('sparklineClick');
+ clickEvent.originalEvent = e;
+ clickEvent.sparklines = this.splist;
+ this.$el.trigger(clickEvent);
+ },
+
+ mouseenter: function (e) {
+ $(document.body).unbind('mousemove.jqs');
+ $(document.body).bind('mousemove.jqs', $.proxy(this.mousemove, this));
+ this.over = true;
+ this.currentPageX = e.pageX;
+ this.currentPageY = e.pageY;
+ this.currentEl = e.target;
+ if (!this.tooltip && this.displayTooltips) {
+ this.tooltip = new Tooltip(this.options);
+ this.tooltip.updatePosition(e.pageX, e.pageY);
+ }
+ this.updateDisplay();
+ },
+
+ mouseleave: function () {
+ $(document.body).unbind('mousemove.jqs');
+ var splist = this.splist,
+ spcount = splist.length,
+ needsRefresh = false,
+ sp, i;
+ this.over = false;
+ this.currentEl = null;
+
+ if (this.tooltip) {
+ this.tooltip.remove();
+ this.tooltip = null;
+ }
+
+ for (i = 0; i < spcount; i++) {
+ sp = splist[i];
+ if (sp.clearRegionHighlight()) {
+ needsRefresh = true;
+ }
+ }
+
+ if (needsRefresh) {
+ this.canvas.render();
+ }
+ },
+
+ mousemove: function (e) {
+ this.currentPageX = e.pageX;
+ this.currentPageY = e.pageY;
+ this.currentEl = e.target;
+ if (this.tooltip) {
+ this.tooltip.updatePosition(e.pageX, e.pageY);
+ }
+ this.updateDisplay();
+ },
+
+ updateDisplay: function () {
+ var splist = this.splist,
+ spcount = splist.length,
+ needsRefresh = false,
+ offset = this.$canvas.offset(),
+ localX = this.currentPageX - offset.left,
+ localY = this.currentPageY - offset.top,
+ tooltiphtml, sp, i, result, changeEvent;
+ if (!this.over) {
+ return;
+ }
+ for (i = 0; i < spcount; i++) {
+ sp = splist[i];
+ result = sp.setRegionHighlight(this.currentEl, localX, localY);
+ if (result) {
+ needsRefresh = true;
+ }
+ }
+ if (needsRefresh) {
+ changeEvent = $.Event('sparklineRegionChange');
+ changeEvent.sparklines = this.splist;
+ this.$el.trigger(changeEvent);
+ if (this.tooltip) {
+ tooltiphtml = '';
+ for (i = 0; i < spcount; i++) {
+ sp = splist[i];
+ tooltiphtml += sp.getCurrentRegionTooltip();
+ }
+ this.tooltip.setContent(tooltiphtml);
+ }
+ if (!this.disableHighlight) {
+ this.canvas.render();
+ }
+ }
+ if (result === null) {
+ this.mouseleave();
+ }
+ }
+ });
+
+
+ Tooltip = createClass({
+ sizeStyle: 'position: static !important;' +
+ 'display: block !important;' +
+ 'visibility: hidden !important;' +
+ 'float: left !important;',
+
+ init: function (options) {
+ var tooltipClassname = options.get('tooltipClassname', 'jqstooltip'),
+ sizetipStyle = this.sizeStyle,
+ offset;
+ this.container = options.get('tooltipContainer') || document.body;
+ this.tooltipOffsetX = options.get('tooltipOffsetX', 10);
+ this.tooltipOffsetY = options.get('tooltipOffsetY', 12);
+ // remove any previous lingering tooltip
+ $('#jqssizetip').remove();
+ $('#jqstooltip').remove();
+ this.sizetip = $('', {
+ id: 'jqssizetip',
+ style: sizetipStyle,
+ 'class': tooltipClassname
+ });
+ this.tooltip = $('', {
+ id: 'jqstooltip',
+ 'class': tooltipClassname
+ }).appendTo(this.container);
+ // account for the container's location
+ offset = this.tooltip.offset();
+ this.offsetLeft = offset.left;
+ this.offsetTop = offset.top;
+ this.hidden = true;
+ $(window).unbind('resize.jqs scroll.jqs');
+ $(window).bind('resize.jqs scroll.jqs', $.proxy(this.updateWindowDims, this));
+ this.updateWindowDims();
+ },
+
+ updateWindowDims: function () {
+ this.scrollTop = $(window).scrollTop();
+ this.scrollLeft = $(window).scrollLeft();
+ this.scrollRight = this.scrollLeft + $(window).width();
+ this.updatePosition();
+ },
+
+ getSize: function (content) {
+ this.sizetip.html(content).appendTo(this.container);
+ this.width = this.sizetip.width() + 1;
+ this.height = this.sizetip.height();
+ this.sizetip.remove();
+ },
+
+ setContent: function (content) {
+ if (!content) {
+ this.tooltip.css('visibility', 'hidden');
+ this.hidden = true;
+ return;
+ }
+ this.getSize(content);
+ this.tooltip.html(content)
+ .css({
+ 'width': this.width,
+ 'height': this.height,
+ 'visibility': 'visible'
+ });
+ if (this.hidden) {
+ this.hidden = false;
+ this.updatePosition();
+ }
+ },
+
+ updatePosition: function (x, y) {
+ if (x === undefined) {
+ if (this.mousex === undefined) {
+ return;
+ }
+ x = this.mousex - this.offsetLeft;
+ y = this.mousey - this.offsetTop;
+
+ } else {
+ this.mousex = x = x - this.offsetLeft;
+ this.mousey = y = y - this.offsetTop;
+ }
+ if (!this.height || !this.width || this.hidden) {
+ return;
+ }
+
+ y -= this.height + this.tooltipOffsetY;
+ x += this.tooltipOffsetX;
+
+ if (y < this.scrollTop) {
+ y = this.scrollTop;
+ }
+ if (x < this.scrollLeft) {
+ x = this.scrollLeft;
+ } else if (x + this.width > this.scrollRight) {
+ x = this.scrollRight - this.width;
+ }
+
+ this.tooltip.css({
+ 'left': x,
+ 'top': y
+ });
+ },
+
+ remove: function () {
+ this.tooltip.remove();
+ this.sizetip.remove();
+ this.sizetip = this.tooltip = undefined;
+ $(window).unbind('resize.jqs scroll.jqs');
+ }
+ });
+
+ initStyles = function() {
+ addCSS(defaultStyles);
+ };
+
+ $(initStyles);
+
+ pending = [];
+ $.fn.sparkline = function (userValues, userOptions) {
+ return this.each(function () {
+ var options = new $.fn.sparkline.options(this, userOptions),
+ $this = $(this),
+ render, i;
+ render = function () {
+ var values, width, height, tmp, mhandler, sp, vals;
+ if (userValues === 'html' || userValues === undefined) {
+ vals = this.getAttribute(options.get('tagValuesAttribute'));
+ if (vals === undefined || vals === null) {
+ vals = $this.html();
+ }
+ values = vals.replace(/(^\s*\s*$)|\s+/g, '').split(',');
+ } else {
+ values = userValues;
+ }
+
+ width = options.get('width') === 'auto' ? values.length * options.get('defaultPixelsPerValue') : options.get('width');
+ if (options.get('height') === 'auto') {
+ if (!options.get('composite') || !$.data(this, '_jqs_vcanvas')) {
+ // must be a better way to get the line height
+ tmp = document.createElement('span');
+ tmp.innerHTML = 'a';
+ $this.html(tmp);
+ height = $(tmp).innerHeight() || $(tmp).height();
+ $(tmp).remove();
+ tmp = null;
+ }
+ } else {
+ height = options.get('height');
+ }
+
+ if (!options.get('disableInteraction')) {
+ mhandler = $.data(this, '_jqs_mhandler');
+ if (!mhandler) {
+ mhandler = new MouseHandler(this, options);
+ $.data(this, '_jqs_mhandler', mhandler);
+ } else if (!options.get('composite')) {
+ mhandler.reset();
+ }
+ } else {
+ mhandler = false;
+ }
+
+ if (options.get('composite') && !$.data(this, '_jqs_vcanvas')) {
+ if (!$.data(this, '_jqs_errnotify')) {
+ alert('Attempted to attach a composite sparkline to an element with no existing sparkline');
+ $.data(this, '_jqs_errnotify', true);
+ }
+ return;
+ }
+
+ sp = new $.fn.sparkline[options.get('type')](this, values, options, width, height);
+
+ sp.render();
+
+ if (mhandler) {
+ mhandler.registerSparkline(sp);
+ }
+ };
+ if (($(this).html() && !options.get('disableHiddenCheck') && $(this).is(':hidden')) || !$(this).parents('body').length) {
+ if (!options.get('composite') && $.data(this, '_jqs_pending')) {
+ // remove any existing references to the element
+ for (i = pending.length; i; i--) {
+ if (pending[i - 1][0] == this) {
+ pending.splice(i - 1, 1);
+ }
+ }
+ }
+ pending.push([this, render]);
+ $.data(this, '_jqs_pending', true);
+ } else {
+ render.call(this);
+ }
+ });
+ };
+
+ $.fn.sparkline.defaults = getDefaults();
+
+
+ $.sparkline_display_visible = function () {
+ var el, i, pl;
+ var done = [];
+ for (i = 0, pl = pending.length; i < pl; i++) {
+ el = pending[i][0];
+ if ($(el).is(':visible') && !$(el).parents().is(':hidden')) {
+ pending[i][1].call(el);
+ $.data(pending[i][0], '_jqs_pending', false);
+ done.push(i);
+ } else if (!$(el).closest('html').length && !$.data(el, '_jqs_pending')) {
+ // element has been inserted and removed from the DOM
+ // If it was not yet inserted into the dom then the .data request
+ // will return true.
+ // removing from the dom causes the data to be removed.
+ $.data(pending[i][0], '_jqs_pending', false);
+ done.push(i);
+ }
+ }
+ for (i = done.length; i; i--) {
+ pending.splice(done[i - 1], 1);
+ }
+ };
+
+
+ /**
+ * User option handler
+ */
+ $.fn.sparkline.options = createClass({
+ init: function (tag, userOptions) {
+ var extendedOptions, defaults, base, tagOptionType;
+ this.userOptions = userOptions = userOptions || {};
+ this.tag = tag;
+ this.tagValCache = {};
+ defaults = $.fn.sparkline.defaults;
+ base = defaults.common;
+ this.tagOptionsPrefix = userOptions.enableTagOptions && (userOptions.tagOptionsPrefix || base.tagOptionsPrefix);
+
+ tagOptionType = this.getTagSetting('type');
+ if (tagOptionType === UNSET_OPTION) {
+ extendedOptions = defaults[userOptions.type || base.type];
+ } else {
+ extendedOptions = defaults[tagOptionType];
+ }
+ this.mergedOptions = $.extend({}, base, extendedOptions, userOptions);
+ },
+
+
+ getTagSetting: function (key) {
+ var prefix = this.tagOptionsPrefix,
+ val, i, pairs, keyval;
+ if (prefix === false || prefix === undefined) {
+ return UNSET_OPTION;
+ }
+ if (this.tagValCache.hasOwnProperty(key)) {
+ val = this.tagValCache.key;
+ } else {
+ val = this.tag.getAttribute(prefix + key);
+ if (val === undefined || val === null) {
+ val = UNSET_OPTION;
+ } else if (val.substr(0, 1) === '[') {
+ val = val.substr(1, val.length - 2).split(',');
+ for (i = val.length; i--;) {
+ val[i] = normalizeValue(val[i].replace(/(^\s*)|(\s*$)/g, ''));
+ }
+ } else if (val.substr(0, 1) === '{') {
+ pairs = val.substr(1, val.length - 2).split(',');
+ val = {};
+ for (i = pairs.length; i--;) {
+ keyval = pairs[i].split(':', 2);
+ val[keyval[0].replace(/(^\s*)|(\s*$)/g, '')] = normalizeValue(keyval[1].replace(/(^\s*)|(\s*$)/g, ''));
+ }
+ } else {
+ val = normalizeValue(val);
+ }
+ this.tagValCache.key = val;
+ }
+ return val;
+ },
+
+ get: function (key, defaultval) {
+ var tagOption = this.getTagSetting(key),
+ result;
+ if (tagOption !== UNSET_OPTION) {
+ return tagOption;
+ }
+ return (result = this.mergedOptions[key]) === undefined ? defaultval : result;
+ }
+ });
+
+
+ $.fn.sparkline._base = createClass({
+ disabled: false,
+
+ init: function (el, values, options, width, height) {
+ this.el = el;
+ this.$el = $(el);
+ this.values = values;
+ this.options = options;
+ this.width = width;
+ this.height = height;
+ this.currentRegion = undefined;
+ },
+
+ /**
+ * Setup the canvas
+ */
+ initTarget: function () {
+ var interactive = !this.options.get('disableInteraction');
+ if (!(this.target = this.$el.simpledraw(this.width, this.height, this.options.get('composite'), interactive))) {
+ this.disabled = true;
+ } else {
+ this.canvasWidth = this.target.pixelWidth;
+ this.canvasHeight = this.target.pixelHeight;
+ }
+ },
+
+ /**
+ * Actually render the chart to the canvas
+ */
+ render: function () {
+ if (this.disabled) {
+ this.el.innerHTML = '';
+ return false;
+ }
+ return true;
+ },
+
+ /**
+ * Return a region id for a given x/y co-ordinate
+ */
+ getRegion: function (x, y) {
+ },
+
+ /**
+ * Highlight an item based on the moused-over x,y co-ordinate
+ */
+ setRegionHighlight: function (el, x, y) {
+ var currentRegion = this.currentRegion,
+ highlightEnabled = !this.options.get('disableHighlight'),
+ newRegion;
+ if (x > this.canvasWidth || y > this.canvasHeight || x < 0 || y < 0) {
+ return null;
+ }
+ newRegion = this.getRegion(el, x, y);
+ if (currentRegion !== newRegion) {
+ if (currentRegion !== undefined && highlightEnabled) {
+ this.removeHighlight();
+ }
+ this.currentRegion = newRegion;
+ if (newRegion !== undefined && highlightEnabled) {
+ this.renderHighlight();
+ }
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Reset any currently highlighted item
+ */
+ clearRegionHighlight: function () {
+ if (this.currentRegion !== undefined) {
+ this.removeHighlight();
+ this.currentRegion = undefined;
+ return true;
+ }
+ return false;
+ },
+
+ renderHighlight: function () {
+ this.changeHighlight(true);
+ },
+
+ removeHighlight: function () {
+ this.changeHighlight(false);
+ },
+
+ changeHighlight: function (highlight) {},
+
+ /**
+ * Fetch the HTML to display as a tooltip
+ */
+ getCurrentRegionTooltip: function () {
+ var options = this.options,
+ header = '',
+ entries = [],
+ fields, formats, formatlen, fclass, text, i,
+ showFields, showFieldsKey, newFields, fv,
+ formatter, format, fieldlen, j;
+ if (this.currentRegion === undefined) {
+ return '';
+ }
+ fields = this.getCurrentRegionFields();
+ formatter = options.get('tooltipFormatter');
+ if (formatter) {
+ return formatter(this, options, fields);
+ }
+ if (options.get('tooltipChartTitle')) {
+ header += '
' + options.get('tooltipChartTitle') + '
\n';
+ }
+ formats = this.options.get('tooltipFormat');
+ if (!formats) {
+ return '';
+ }
+ if (!$.isArray(formats)) {
+ formats = [formats];
+ }
+ if (!$.isArray(fields)) {
+ fields = [fields];
+ }
+ showFields = this.options.get('tooltipFormatFieldlist');
+ showFieldsKey = this.options.get('tooltipFormatFieldlistKey');
+ if (showFields && showFieldsKey) {
+ // user-selected ordering of fields
+ newFields = [];
+ for (i = fields.length; i--;) {
+ fv = fields[i][showFieldsKey];
+ if ((j = $.inArray(fv, showFields)) != -1) {
+ newFields[j] = fields[i];
+ }
+ }
+ fields = newFields;
+ }
+ formatlen = formats.length;
+ fieldlen = fields.length;
+ for (i = 0; i < formatlen; i++) {
+ format = formats[i];
+ if (typeof format === 'string') {
+ format = new SPFormat(format);
+ }
+ fclass = format.fclass || 'jqsfield';
+ for (j = 0; j < fieldlen; j++) {
+ if (!fields[j].isNull || !options.get('tooltipSkipNull')) {
+ $.extend(fields[j], {
+ prefix: options.get('tooltipPrefix'),
+ suffix: options.get('tooltipSuffix')
+ });
+ text = format.render(fields[j], options.get('tooltipValueLookups'), options);
+ entries.push('
' + text + '
');
+ }
+ }
+ }
+ if (entries.length) {
+ return header + entries.join('\n');
+ }
+ return '';
+ },
+
+ getCurrentRegionFields: function () {},
+
+ calcHighlightColor: function (color, options) {
+ var highlightColor = options.get('highlightColor'),
+ lighten = options.get('highlightLighten'),
+ parse, mult, rgbnew, i;
+ if (highlightColor) {
+ return highlightColor;
+ }
+ if (lighten) {
+ // extract RGB values
+ parse = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(color) || /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(color);
+ if (parse) {
+ rgbnew = [];
+ mult = color.length === 4 ? 16 : 1;
+ for (i = 0; i < 3; i++) {
+ rgbnew[i] = clipval(Math.round(parseInt(parse[i + 1], 16) * mult * lighten), 0, 255);
+ }
+ return 'rgb(' + rgbnew.join(',') + ')';
+ }
+
+ }
+ return color;
+ }
+
+ });
+
+ barHighlightMixin = {
+ changeHighlight: function (highlight) {
+ var currentRegion = this.currentRegion,
+ target = this.target,
+ shapeids = this.regionShapes[currentRegion],
+ newShapes;
+ // will be null if the region value was null
+ if (shapeids) {
+ newShapes = this.renderRegion(currentRegion, highlight);
+ if ($.isArray(newShapes) || $.isArray(shapeids)) {
+ target.replaceWithShapes(shapeids, newShapes);
+ this.regionShapes[currentRegion] = $.map(newShapes, function (newShape) {
+ return newShape.id;
+ });
+ } else {
+ target.replaceWithShape(shapeids, newShapes);
+ this.regionShapes[currentRegion] = newShapes.id;
+ }
+ }
+ },
+
+ render: function () {
+ var values = this.values,
+ target = this.target,
+ regionShapes = this.regionShapes,
+ shapes, ids, i, j;
+
+ if (!this.cls._super.render.call(this)) {
+ return;
+ }
+ for (i = values.length; i--;) {
+ shapes = this.renderRegion(i);
+ if (shapes) {
+ if ($.isArray(shapes)) {
+ ids = [];
+ for (j = shapes.length; j--;) {
+ shapes[j].append();
+ ids.push(shapes[j].id);
+ }
+ regionShapes[i] = ids;
+ } else {
+ shapes.append();
+ regionShapes[i] = shapes.id; // store just the shapeid
+ }
+ } else {
+ // null value
+ regionShapes[i] = null;
+ }
+ }
+ target.render();
+ }
+ };
+
+ /**
+ * Line charts
+ */
+ $.fn.sparkline.line = line = createClass($.fn.sparkline._base, {
+ type: 'line',
+
+ init: function (el, values, options, width, height) {
+ line._super.init.call(this, el, values, options, width, height);
+ this.vertices = [];
+ this.regionMap = [];
+ this.xvalues = [];
+ this.yvalues = [];
+ this.yminmax = [];
+ this.hightlightSpotId = null;
+ this.lastShapeId = null;
+ this.initTarget();
+ },
+
+ getRegion: function (el, x, y) {
+ var i,
+ regionMap = this.regionMap; // maps regions to value positions
+ for (i = regionMap.length; i--;) {
+ if (regionMap[i] !== null && x >= regionMap[i][0] && x <= regionMap[i][1]) {
+ return regionMap[i][2];
+ }
+ }
+ return undefined;
+ },
+
+ getCurrentRegionFields: function () {
+ var currentRegion = this.currentRegion;
+ return {
+ isNull: this.yvalues[currentRegion] === null,
+ x: this.xvalues[currentRegion],
+ y: this.yvalues[currentRegion],
+ color: this.options.get('lineColor'),
+ fillColor: this.options.get('fillColor'),
+ offset: currentRegion
+ };
+ },
+
+ renderHighlight: function () {
+ var currentRegion = this.currentRegion,
+ target = this.target,
+ vertex = this.vertices[currentRegion],
+ options = this.options,
+ spotRadius = options.get('spotRadius'),
+ highlightSpotColor = options.get('highlightSpotColor'),
+ highlightLineColor = options.get('highlightLineColor'),
+ highlightSpot, highlightLine;
+
+ if (!vertex) {
+ return;
+ }
+ if (spotRadius && highlightSpotColor) {
+ highlightSpot = target.drawCircle(vertex[0], vertex[1],
+ spotRadius, undefined, highlightSpotColor);
+ this.highlightSpotId = highlightSpot.id;
+ target.insertAfterShape(this.lastShapeId, highlightSpot);
+ }
+ if (highlightLineColor) {
+ highlightLine = target.drawLine(vertex[0], this.canvasTop, vertex[0],
+ this.canvasTop + this.canvasHeight, highlightLineColor);
+ this.highlightLineId = highlightLine.id;
+ target.insertAfterShape(this.lastShapeId, highlightLine);
+ }
+ },
+
+ removeHighlight: function () {
+ var target = this.target;
+ if (this.highlightSpotId) {
+ target.removeShapeId(this.highlightSpotId);
+ this.highlightSpotId = null;
+ }
+ if (this.highlightLineId) {
+ target.removeShapeId(this.highlightLineId);
+ this.highlightLineId = null;
+ }
+ },
+
+ scanValues: function () {
+ var values = this.values,
+ valcount = values.length,
+ xvalues = this.xvalues,
+ yvalues = this.yvalues,
+ yminmax = this.yminmax,
+ i, val, isStr, isArray, sp;
+ for (i = 0; i < valcount; i++) {
+ val = values[i];
+ isStr = typeof(values[i]) === 'string';
+ isArray = typeof(values[i]) === 'object' && values[i] instanceof Array;
+ sp = isStr && values[i].split(':');
+ if (isStr && sp.length === 2) { // x:y
+ xvalues.push(Number(sp[0]));
+ yvalues.push(Number(sp[1]));
+ yminmax.push(Number(sp[1]));
+ } else if (isArray) {
+ xvalues.push(val[0]);
+ yvalues.push(val[1]);
+ yminmax.push(val[1]);
+ } else {
+ xvalues.push(i);
+ if (values[i] === null || values[i] === 'null') {
+ yvalues.push(null);
+ } else {
+ yvalues.push(Number(val));
+ yminmax.push(Number(val));
+ }
+ }
+ }
+ if (this.options.get('xvalues')) {
+ xvalues = this.options.get('xvalues');
+ }
+
+ this.maxy = this.maxyorg = Math.max.apply(Math, yminmax);
+ this.miny = this.minyorg = Math.min.apply(Math, yminmax);
+
+ this.maxx = Math.max.apply(Math, xvalues);
+ this.minx = Math.min.apply(Math, xvalues);
+
+ this.xvalues = xvalues;
+ this.yvalues = yvalues;
+ this.yminmax = yminmax;
+
+ },
+
+ processRangeOptions: function () {
+ var options = this.options,
+ normalRangeMin = options.get('normalRangeMin'),
+ normalRangeMax = options.get('normalRangeMax');
+
+ if (normalRangeMin !== undefined) {
+ if (normalRangeMin < this.miny) {
+ this.miny = normalRangeMin;
+ }
+ if (normalRangeMax > this.maxy) {
+ this.maxy = normalRangeMax;
+ }
+ }
+ if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < this.miny)) {
+ this.miny = options.get('chartRangeMin');
+ }
+ if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > this.maxy)) {
+ this.maxy = options.get('chartRangeMax');
+ }
+ if (options.get('chartRangeMinX') !== undefined && (options.get('chartRangeClipX') || options.get('chartRangeMinX') < this.minx)) {
+ this.minx = options.get('chartRangeMinX');
+ }
+ if (options.get('chartRangeMaxX') !== undefined && (options.get('chartRangeClipX') || options.get('chartRangeMaxX') > this.maxx)) {
+ this.maxx = options.get('chartRangeMaxX');
+ }
+
+ },
+
+ drawNormalRange: function (canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey) {
+ var normalRangeMin = this.options.get('normalRangeMin'),
+ normalRangeMax = this.options.get('normalRangeMax'),
+ ytop = canvasTop + Math.round(canvasHeight - (canvasHeight * ((normalRangeMax - this.miny) / rangey))),
+ height = Math.round((canvasHeight * (normalRangeMax - normalRangeMin)) / rangey);
+ this.target.drawRect(canvasLeft, ytop, canvasWidth, height, undefined, this.options.get('normalRangeColor')).append();
+ },
+
+ render: function () {
+ var options = this.options,
+ target = this.target,
+ canvasWidth = this.canvasWidth,
+ canvasHeight = this.canvasHeight,
+ vertices = this.vertices,
+ spotRadius = options.get('spotRadius'),
+ regionMap = this.regionMap,
+ rangex, rangey, yvallast,
+ canvasTop, canvasLeft,
+ vertex, path, paths, x, y, xnext, xpos, xposnext,
+ last, next, yvalcount, lineShapes, fillShapes, plen,
+ valueSpots, hlSpotsEnabled, color, xvalues, yvalues, i;
+
+ if (!line._super.render.call(this)) {
+ return;
+ }
+
+ this.scanValues();
+ this.processRangeOptions();
+
+ xvalues = this.xvalues;
+ yvalues = this.yvalues;
+
+ if (!this.yminmax.length || this.yvalues.length < 2) {
+ // empty or all null valuess
+ return;
+ }
+
+ canvasTop = canvasLeft = 0;
+
+ rangex = this.maxx - this.minx === 0 ? 1 : this.maxx - this.minx;
+ rangey = this.maxy - this.miny === 0 ? 1 : this.maxy - this.miny;
+ yvallast = this.yvalues.length - 1;
+
+ if (spotRadius && (canvasWidth < (spotRadius * 4) || canvasHeight < (spotRadius * 4))) {
+ spotRadius = 0;
+ }
+ if (spotRadius) {
+ // adjust the canvas size as required so that spots will fit
+ hlSpotsEnabled = options.get('highlightSpotColor') && !options.get('disableInteraction');
+ if (hlSpotsEnabled || options.get('minSpotColor') || (options.get('spotColor') && yvalues[yvallast] === this.miny)) {
+ canvasHeight -= Math.ceil(spotRadius);
+ }
+ if (hlSpotsEnabled || options.get('maxSpotColor') || (options.get('spotColor') && yvalues[yvallast] === this.maxy)) {
+ canvasHeight -= Math.ceil(spotRadius);
+ canvasTop += Math.ceil(spotRadius);
+ }
+ if (hlSpotsEnabled ||
+ ((options.get('minSpotColor') || options.get('maxSpotColor')) && (yvalues[0] === this.miny || yvalues[0] === this.maxy))) {
+ canvasLeft += Math.ceil(spotRadius);
+ canvasWidth -= Math.ceil(spotRadius);
+ }
+ if (hlSpotsEnabled || options.get('spotColor') ||
+ (options.get('minSpotColor') || options.get('maxSpotColor') &&
+ (yvalues[yvallast] === this.miny || yvalues[yvallast] === this.maxy))) {
+ canvasWidth -= Math.ceil(spotRadius);
+ }
+ }
+
+
+ canvasHeight--;
+
+ if (options.get('normalRangeMin') !== undefined && !options.get('drawNormalOnTop')) {
+ this.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey);
+ }
+
+ path = [];
+ paths = [path];
+ last = next = null;
+ yvalcount = yvalues.length;
+ for (i = 0; i < yvalcount; i++) {
+ x = xvalues[i];
+ xnext = xvalues[i + 1];
+ y = yvalues[i];
+ xpos = canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex));
+ xposnext = i < yvalcount - 1 ? canvasLeft + Math.round((xnext - this.minx) * (canvasWidth / rangex)) : canvasWidth;
+ next = xpos + ((xposnext - xpos) / 2);
+ regionMap[i] = [last || 0, next, i];
+ last = next;
+ if (y === null) {
+ if (i) {
+ if (yvalues[i - 1] !== null) {
+ path = [];
+ paths.push(path);
+ }
+ vertices.push(null);
+ }
+ } else {
+ if (y < this.miny) {
+ y = this.miny;
+ }
+ if (y > this.maxy) {
+ y = this.maxy;
+ }
+ if (!path.length) {
+ // previous value was null
+ path.push([xpos, canvasTop + canvasHeight]);
+ }
+ vertex = [xpos, canvasTop + Math.round(canvasHeight - (canvasHeight * ((y - this.miny) / rangey)))];
+ path.push(vertex);
+ vertices.push(vertex);
+ }
+ }
+
+ lineShapes = [];
+ fillShapes = [];
+ plen = paths.length;
+ for (i = 0; i < plen; i++) {
+ path = paths[i];
+ if (path.length) {
+ if (options.get('fillColor')) {
+ path.push([path[path.length - 1][0], (canvasTop + canvasHeight)]);
+ fillShapes.push(path.slice(0));
+ path.pop();
+ }
+ // if there's only a single point in this path, then we want to display it
+ // as a vertical line which means we keep path[0] as is
+ if (path.length > 2) {
+ // else we want the first value
+ path[0] = [path[0][0], path[1][1]];
+ }
+ lineShapes.push(path);
+ }
+ }
+
+ // draw the fill first, then optionally the normal range, then the line on top of that
+ plen = fillShapes.length;
+ for (i = 0; i < plen; i++) {
+ target.drawShape(fillShapes[i],
+ options.get('fillColor'), options.get('fillColor')).append();
+ }
+
+ if (options.get('normalRangeMin') !== undefined && options.get('drawNormalOnTop')) {
+ this.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey);
+ }
+
+ plen = lineShapes.length;
+ for (i = 0; i < plen; i++) {
+ target.drawShape(lineShapes[i], options.get('lineColor'), undefined,
+ options.get('lineWidth')).append();
+ }
+
+ if (spotRadius && options.get('valueSpots')) {
+ valueSpots = options.get('valueSpots');
+ if (valueSpots.get === undefined) {
+ valueSpots = new RangeMap(valueSpots);
+ }
+ for (i = 0; i < yvalcount; i++) {
+ color = valueSpots.get(yvalues[i]);
+ if (color) {
+ target.drawCircle(canvasLeft + Math.round((xvalues[i] - this.minx) * (canvasWidth / rangex)),
+ canvasTop + Math.round(canvasHeight - (canvasHeight * ((yvalues[i] - this.miny) / rangey))),
+ spotRadius, undefined,
+ color).append();
+ }
+ }
+
+ }
+ if (spotRadius && options.get('spotColor') && yvalues[yvallast] !== null) {
+ target.drawCircle(canvasLeft + Math.round((xvalues[xvalues.length - 1] - this.minx) * (canvasWidth / rangex)),
+ canvasTop + Math.round(canvasHeight - (canvasHeight * ((yvalues[yvallast] - this.miny) / rangey))),
+ spotRadius, undefined,
+ options.get('spotColor')).append();
+ }
+ if (this.maxy !== this.minyorg) {
+ if (spotRadius && options.get('minSpotColor')) {
+ x = xvalues[$.inArray(this.minyorg, yvalues)];
+ target.drawCircle(canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)),
+ canvasTop + Math.round(canvasHeight - (canvasHeight * ((this.minyorg - this.miny) / rangey))),
+ spotRadius, undefined,
+ options.get('minSpotColor')).append();
+ }
+ if (spotRadius && options.get('maxSpotColor')) {
+ x = xvalues[$.inArray(this.maxyorg, yvalues)];
+ target.drawCircle(canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)),
+ canvasTop + Math.round(canvasHeight - (canvasHeight * ((this.maxyorg - this.miny) / rangey))),
+ spotRadius, undefined,
+ options.get('maxSpotColor')).append();
+ }
+ }
+
+ this.lastShapeId = target.getLastShapeId();
+ this.canvasTop = canvasTop;
+ target.render();
+ }
+ });
+
+ /**
+ * Bar charts
+ */
+ $.fn.sparkline.bar = bar = createClass($.fn.sparkline._base, barHighlightMixin, {
+ type: 'bar',
+
+ init: function (el, values, options, width, height) {
+ var barWidth = parseInt(options.get('barWidth'), 10),
+ barSpacing = parseInt(options.get('barSpacing'), 10),
+ chartRangeMin = options.get('chartRangeMin'),
+ chartRangeMax = options.get('chartRangeMax'),
+ chartRangeClip = options.get('chartRangeClip'),
+ stackMin = Infinity,
+ stackMax = -Infinity,
+ isStackString, groupMin, groupMax, stackRanges,
+ numValues, i, vlen, range, zeroAxis, xaxisOffset, min, max, clipMin, clipMax,
+ stacked, vlist, j, slen, svals, val, yoffset, yMaxCalc, canvasHeightEf;
+ bar._super.init.call(this, el, values, options, width, height);
+
+ // scan values to determine whether to stack bars
+ for (i = 0, vlen = values.length; i < vlen; i++) {
+ val = values[i];
+ isStackString = typeof(val) === 'string' && val.indexOf(':') > -1;
+ if (isStackString || $.isArray(val)) {
+ stacked = true;
+ if (isStackString) {
+ val = values[i] = normalizeValues(val.split(':'));
+ }
+ val = remove(val, null); // min/max will treat null as zero
+ groupMin = Math.min.apply(Math, val);
+ groupMax = Math.max.apply(Math, val);
+ if (groupMin < stackMin) {
+ stackMin = groupMin;
+ }
+ if (groupMax > stackMax) {
+ stackMax = groupMax;
+ }
+ }
+ }
+
+ this.stacked = stacked;
+ this.regionShapes = {};
+ this.barWidth = barWidth;
+ this.barSpacing = barSpacing;
+ this.totalBarWidth = barWidth + barSpacing;
+ this.width = width = (values.length * barWidth) + ((values.length - 1) * barSpacing);
+
+ this.initTarget();
+
+ if (chartRangeClip) {
+ clipMin = chartRangeMin === undefined ? -Infinity : chartRangeMin;
+ clipMax = chartRangeMax === undefined ? Infinity : chartRangeMax;
+ }
+
+ numValues = [];
+ stackRanges = stacked ? [] : numValues;
+ var stackTotals = [];
+ var stackRangesNeg = [];
+ for (i = 0, vlen = values.length; i < vlen; i++) {
+ if (stacked) {
+ vlist = values[i];
+ values[i] = svals = [];
+ stackTotals[i] = 0;
+ stackRanges[i] = stackRangesNeg[i] = 0;
+ for (j = 0, slen = vlist.length; j < slen; j++) {
+ val = svals[j] = chartRangeClip ? clipval(vlist[j], clipMin, clipMax) : vlist[j];
+ if (val !== null) {
+ if (val > 0) {
+ stackTotals[i] += val;
+ }
+ if (stackMin < 0 && stackMax > 0) {
+ if (val < 0) {
+ stackRangesNeg[i] += Math.abs(val);
+ } else {
+ stackRanges[i] += val;
+ }
+ } else {
+ stackRanges[i] += Math.abs(val - (val < 0 ? stackMax : stackMin));
+ }
+ numValues.push(val);
+ }
+ }
+ } else {
+ val = chartRangeClip ? clipval(values[i], clipMin, clipMax) : values[i];
+ val = values[i] = normalizeValue(val);
+ if (val !== null) {
+ numValues.push(val);
+ }
+ }
+ }
+ this.max = max = Math.max.apply(Math, numValues);
+ this.min = min = Math.min.apply(Math, numValues);
+ this.stackMax = stackMax = stacked ? Math.max.apply(Math, stackTotals) : max;
+ this.stackMin = stackMin = stacked ? Math.min.apply(Math, numValues) : min;
+
+ if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < min)) {
+ min = options.get('chartRangeMin');
+ }
+ if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > max)) {
+ max = options.get('chartRangeMax');
+ }
+
+ this.zeroAxis = zeroAxis = options.get('zeroAxis', true);
+ if (min <= 0 && max >= 0 && zeroAxis) {
+ xaxisOffset = 0;
+ } else if (zeroAxis == false) {
+ xaxisOffset = min;
+ } else if (min > 0) {
+ xaxisOffset = min;
+ } else {
+ xaxisOffset = max;
+ }
+ this.xaxisOffset = xaxisOffset;
+
+ range = stacked ? (Math.max.apply(Math, stackRanges) + Math.max.apply(Math, stackRangesNeg)) : max - min;
+
+ // as we plot zero/min values a single pixel line, we add a pixel to all other
+ // values - Reduce the effective canvas size to suit
+ this.canvasHeightEf = (zeroAxis && min < 0) ? this.canvasHeight - 2 : this.canvasHeight - 1;
+
+ if (min < xaxisOffset) {
+ yMaxCalc = (stacked && max >= 0) ? stackMax : max;
+ yoffset = (yMaxCalc - xaxisOffset) / range * this.canvasHeight;
+ if (yoffset !== Math.ceil(yoffset)) {
+ this.canvasHeightEf -= 2;
+ yoffset = Math.ceil(yoffset);
+ }
+ } else {
+ yoffset = this.canvasHeight;
+ }
+ this.yoffset = yoffset;
+
+ if ($.isArray(options.get('colorMap'))) {
+ this.colorMapByIndex = options.get('colorMap');
+ this.colorMapByValue = null;
+ } else {
+ this.colorMapByIndex = null;
+ this.colorMapByValue = options.get('colorMap');
+ if (this.colorMapByValue && this.colorMapByValue.get === undefined) {
+ this.colorMapByValue = new RangeMap(this.colorMapByValue);
+ }
+ }
+
+ this.range = range;
+ },
+
+ getRegion: function (el, x, y) {
+ var result = Math.floor(x / this.totalBarWidth);
+ return (result < 0 || result >= this.values.length) ? undefined : result;
+ },
+
+ getCurrentRegionFields: function () {
+ var currentRegion = this.currentRegion,
+ values = ensureArray(this.values[currentRegion]),
+ result = [],
+ value, i;
+ for (i = values.length; i--;) {
+ value = values[i];
+ result.push({
+ isNull: value === null,
+ value: value,
+ color: this.calcColor(i, value, currentRegion),
+ offset: currentRegion
+ });
+ }
+ return result;
+ },
+
+ calcColor: function (stacknum, value, valuenum) {
+ var colorMapByIndex = this.colorMapByIndex,
+ colorMapByValue = this.colorMapByValue,
+ options = this.options,
+ color, newColor;
+ if (this.stacked) {
+ color = options.get('stackedBarColor');
+ } else {
+ color = (value < 0) ? options.get('negBarColor') : options.get('barColor');
+ }
+ if (value === 0 && options.get('zeroColor') !== undefined) {
+ color = options.get('zeroColor');
+ }
+ if (colorMapByValue && (newColor = colorMapByValue.get(value))) {
+ color = newColor;
+ } else if (colorMapByIndex && colorMapByIndex.length > valuenum) {
+ color = colorMapByIndex[valuenum];
+ }
+ return $.isArray(color) ? color[stacknum % color.length] : color;
+ },
+
+ /**
+ * Render bar(s) for a region
+ */
+ renderRegion: function (valuenum, highlight) {
+ var vals = this.values[valuenum],
+ options = this.options,
+ xaxisOffset = this.xaxisOffset,
+ result = [],
+ range = this.range,
+ stacked = this.stacked,
+ target = this.target,
+ x = valuenum * this.totalBarWidth,
+ canvasHeightEf = this.canvasHeightEf,
+ yoffset = this.yoffset,
+ y, height, color, isNull, yoffsetNeg, i, valcount, val, minPlotted, allMin;
+
+ vals = $.isArray(vals) ? vals : [vals];
+ valcount = vals.length;
+ val = vals[0];
+ isNull = all(null, vals);
+ allMin = all(xaxisOffset, vals, true);
+
+ if (isNull) {
+ if (options.get('nullColor')) {
+ color = highlight ? options.get('nullColor') : this.calcHighlightColor(options.get('nullColor'), options);
+ y = (yoffset > 0) ? yoffset - 1 : yoffset;
+ return target.drawRect(x, y, this.barWidth - 1, 0, color, color);
+ } else {
+ return undefined;
+ }
+ }
+ yoffsetNeg = yoffset;
+ for (i = 0; i < valcount; i++) {
+ val = vals[i];
+
+ if (stacked && val === xaxisOffset) {
+ if (!allMin || minPlotted) {
+ continue;
+ }
+ minPlotted = true;
+ }
+
+ if (range > 0) {
+ height = Math.floor(canvasHeightEf * ((Math.abs(val - xaxisOffset) / range))) + 1;
+ } else {
+ height = 1;
+ }
+ if (val < xaxisOffset || (val === xaxisOffset && yoffset === 0)) {
+ y = yoffsetNeg;
+ yoffsetNeg += height;
+ } else {
+ y = yoffset - height;
+ yoffset -= height;
+ }
+ color = this.calcColor(i, val, valuenum);
+ if (highlight) {
+ color = this.calcHighlightColor(color, options);
+ }
+ result.push(target.drawRect(x, y, this.barWidth - 1, height - 1, color, color));
+ }
+ if (result.length === 1) {
+ return result[0];
+ }
+ return result;
+ }
+ });
+
+ /**
+ * Tristate charts
+ */
+ $.fn.sparkline.tristate = tristate = createClass($.fn.sparkline._base, barHighlightMixin, {
+ type: 'tristate',
+
+ init: function (el, values, options, width, height) {
+ var barWidth = parseInt(options.get('barWidth'), 10),
+ barSpacing = parseInt(options.get('barSpacing'), 10);
+ tristate._super.init.call(this, el, values, options, width, height);
+
+ this.regionShapes = {};
+ this.barWidth = barWidth;
+ this.barSpacing = barSpacing;
+ this.totalBarWidth = barWidth + barSpacing;
+ this.values = $.map(values, Number);
+ this.width = width = (values.length * barWidth) + ((values.length - 1) * barSpacing);
+
+ if ($.isArray(options.get('colorMap'))) {
+ this.colorMapByIndex = options.get('colorMap');
+ this.colorMapByValue = null;
+ } else {
+ this.colorMapByIndex = null;
+ this.colorMapByValue = options.get('colorMap');
+ if (this.colorMapByValue && this.colorMapByValue.get === undefined) {
+ this.colorMapByValue = new RangeMap(this.colorMapByValue);
+ }
+ }
+ this.initTarget();
+ },
+
+ getRegion: function (el, x, y) {
+ return Math.floor(x / this.totalBarWidth);
+ },
+
+ getCurrentRegionFields: function () {
+ var currentRegion = this.currentRegion;
+ return {
+ isNull: this.values[currentRegion] === undefined,
+ value: this.values[currentRegion],
+ color: this.calcColor(this.values[currentRegion], currentRegion),
+ offset: currentRegion
+ };
+ },
+
+ calcColor: function (value, valuenum) {
+ var values = this.values,
+ options = this.options,
+ colorMapByIndex = this.colorMapByIndex,
+ colorMapByValue = this.colorMapByValue,
+ color, newColor;
+
+ if (colorMapByValue && (newColor = colorMapByValue.get(value))) {
+ color = newColor;
+ } else if (colorMapByIndex && colorMapByIndex.length > valuenum) {
+ color = colorMapByIndex[valuenum];
+ } else if (values[valuenum] < 0) {
+ color = options.get('negBarColor');
+ } else if (values[valuenum] > 0) {
+ color = options.get('posBarColor');
+ } else {
+ color = options.get('zeroBarColor');
+ }
+ return color;
+ },
+
+ renderRegion: function (valuenum, highlight) {
+ var values = this.values,
+ options = this.options,
+ target = this.target,
+ canvasHeight, height, halfHeight,
+ x, y, color;
+
+ canvasHeight = target.pixelHeight;
+ halfHeight = Math.round(canvasHeight / 2);
+
+ x = valuenum * this.totalBarWidth;
+ if (values[valuenum] < 0) {
+ y = halfHeight;
+ height = halfHeight - 1;
+ } else if (values[valuenum] > 0) {
+ y = 0;
+ height = halfHeight - 1;
+ } else {
+ y = halfHeight - 1;
+ height = 2;
+ }
+ color = this.calcColor(values[valuenum], valuenum);
+ if (color === null) {
+ return;
+ }
+ if (highlight) {
+ color = this.calcHighlightColor(color, options);
+ }
+ return target.drawRect(x, y, this.barWidth - 1, height - 1, color, color);
+ }
+ });
+
+ /**
+ * Discrete charts
+ */
+ $.fn.sparkline.discrete = discrete = createClass($.fn.sparkline._base, barHighlightMixin, {
+ type: 'discrete',
+
+ init: function (el, values, options, width, height) {
+ discrete._super.init.call(this, el, values, options, width, height);
+
+ this.regionShapes = {};
+ this.values = values = $.map(values, Number);
+ this.min = Math.min.apply(Math, values);
+ this.max = Math.max.apply(Math, values);
+ this.range = this.max - this.min;
+ this.width = width = options.get('width') === 'auto' ? values.length * 2 : this.width;
+ this.interval = Math.floor(width / values.length);
+ this.itemWidth = width / values.length;
+ if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < this.min)) {
+ this.min = options.get('chartRangeMin');
+ }
+ if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > this.max)) {
+ this.max = options.get('chartRangeMax');
+ }
+ this.initTarget();
+ if (this.target) {
+ this.lineHeight = options.get('lineHeight') === 'auto' ? Math.round(this.canvasHeight * 0.3) : options.get('lineHeight');
+ }
+ },
+
+ getRegion: function (el, x, y) {
+ return Math.floor(x / this.itemWidth);
+ },
+
+ getCurrentRegionFields: function () {
+ var currentRegion = this.currentRegion;
+ return {
+ isNull: this.values[currentRegion] === undefined,
+ value: this.values[currentRegion],
+ offset: currentRegion
+ };
+ },
+
+ renderRegion: function (valuenum, highlight) {
+ var values = this.values,
+ options = this.options,
+ min = this.min,
+ max = this.max,
+ range = this.range,
+ interval = this.interval,
+ target = this.target,
+ canvasHeight = this.canvasHeight,
+ lineHeight = this.lineHeight,
+ pheight = canvasHeight - lineHeight,
+ ytop, val, color, x;
+
+ val = clipval(values[valuenum], min, max);
+ x = valuenum * interval;
+ ytop = Math.round(pheight - pheight * ((val - min) / range));
+ color = (options.get('thresholdColor') && val < options.get('thresholdValue')) ? options.get('thresholdColor') : options.get('lineColor');
+ if (highlight) {
+ color = this.calcHighlightColor(color, options);
+ }
+ return target.drawLine(x, ytop, x, ytop + lineHeight, color);
+ }
+ });
+
+ /**
+ * Bullet charts
+ */
+ $.fn.sparkline.bullet = bullet = createClass($.fn.sparkline._base, {
+ type: 'bullet',
+
+ init: function (el, values, options, width, height) {
+ var min, max, vals;
+ bullet._super.init.call(this, el, values, options, width, height);
+
+ // values: target, performance, range1, range2, range3
+ this.values = values = normalizeValues(values);
+ // target or performance could be null
+ vals = values.slice();
+ vals[0] = vals[0] === null ? vals[2] : vals[0];
+ vals[1] = values[1] === null ? vals[2] : vals[1];
+ min = Math.min.apply(Math, values);
+ max = Math.max.apply(Math, values);
+ if (options.get('base') === undefined) {
+ min = min < 0 ? min : 0;
+ } else {
+ min = options.get('base');
+ }
+ this.min = min;
+ this.max = max;
+ this.range = max - min;
+ this.shapes = {};
+ this.valueShapes = {};
+ this.regiondata = {};
+ this.width = width = options.get('width') === 'auto' ? '4.0em' : width;
+ this.target = this.$el.simpledraw(width, height, options.get('composite'));
+ if (!values.length) {
+ this.disabled = true;
+ }
+ this.initTarget();
+ },
+
+ getRegion: function (el, x, y) {
+ var shapeid = this.target.getShapeAt(el, x, y);
+ return (shapeid !== undefined && this.shapes[shapeid] !== undefined) ? this.shapes[shapeid] : undefined;
+ },
+
+ getCurrentRegionFields: function () {
+ var currentRegion = this.currentRegion;
+ return {
+ fieldkey: currentRegion.substr(0, 1),
+ value: this.values[currentRegion.substr(1)],
+ region: currentRegion
+ };
+ },
+
+ changeHighlight: function (highlight) {
+ var currentRegion = this.currentRegion,
+ shapeid = this.valueShapes[currentRegion],
+ shape;
+ delete this.shapes[shapeid];
+ switch (currentRegion.substr(0, 1)) {
+ case 'r':
+ shape = this.renderRange(currentRegion.substr(1), highlight);
+ break;
+ case 'p':
+ shape = this.renderPerformance(highlight);
+ break;
+ case 't':
+ shape = this.renderTarget(highlight);
+ break;
+ }
+ this.valueShapes[currentRegion] = shape.id;
+ this.shapes[shape.id] = currentRegion;
+ this.target.replaceWithShape(shapeid, shape);
+ },
+
+ renderRange: function (rn, highlight) {
+ var rangeval = this.values[rn],
+ rangewidth = Math.round(this.canvasWidth * ((rangeval - this.min) / this.range)),
+ color = this.options.get('rangeColors')[rn - 2];
+ if (highlight) {
+ color = this.calcHighlightColor(color, this.options);
+ }
+ return this.target.drawRect(0, 0, rangewidth - 1, this.canvasHeight - 1, color, color);
+ },
+
+ renderPerformance: function (highlight) {
+ var perfval = this.values[1],
+ perfwidth = Math.round(this.canvasWidth * ((perfval - this.min) / this.range)),
+ color = this.options.get('performanceColor');
+ if (highlight) {
+ color = this.calcHighlightColor(color, this.options);
+ }
+ return this.target.drawRect(0, Math.round(this.canvasHeight * 0.3), perfwidth - 1,
+ Math.round(this.canvasHeight * 0.4) - 1, color, color);
+ },
+
+ renderTarget: function (highlight) {
+ var targetval = this.values[0],
+ x = Math.round(this.canvasWidth * ((targetval - this.min) / this.range) - (this.options.get('targetWidth') / 2)),
+ targettop = Math.round(this.canvasHeight * 0.10),
+ targetheight = this.canvasHeight - (targettop * 2),
+ color = this.options.get('targetColor');
+ if (highlight) {
+ color = this.calcHighlightColor(color, this.options);
+ }
+ return this.target.drawRect(x, targettop, this.options.get('targetWidth') - 1, targetheight - 1, color, color);
+ },
+
+ render: function () {
+ var vlen = this.values.length,
+ target = this.target,
+ i, shape;
+ if (!bullet._super.render.call(this)) {
+ return;
+ }
+ for (i = 2; i < vlen; i++) {
+ shape = this.renderRange(i).append();
+ this.shapes[shape.id] = 'r' + i;
+ this.valueShapes['r' + i] = shape.id;
+ }
+ if (this.values[1] !== null) {
+ shape = this.renderPerformance().append();
+ this.shapes[shape.id] = 'p1';
+ this.valueShapes.p1 = shape.id;
+ }
+ if (this.values[0] !== null) {
+ shape = this.renderTarget().append();
+ this.shapes[shape.id] = 't0';
+ this.valueShapes.t0 = shape.id;
+ }
+ target.render();
+ }
+ });
+
+ /**
+ * Pie charts
+ */
+ $.fn.sparkline.pie = pie = createClass($.fn.sparkline._base, {
+ type: 'pie',
+
+ init: function (el, values, options, width, height) {
+ var total = 0, i;
+
+ pie._super.init.call(this, el, values, options, width, height);
+
+ this.shapes = {}; // map shape ids to value offsets
+ this.valueShapes = {}; // maps value offsets to shape ids
+ this.values = values = $.map(values, Number);
+
+ if (options.get('width') === 'auto') {
+ this.width = this.height;
+ }
+
+ if (values.length > 0) {
+ for (i = values.length; i--;) {
+ total += values[i];
+ }
+ }
+ this.total = total;
+ this.initTarget();
+ this.radius = Math.floor(Math.min(this.canvasWidth, this.canvasHeight) / 2);
+ },
+
+ getRegion: function (el, x, y) {
+ var shapeid = this.target.getShapeAt(el, x, y);
+ return (shapeid !== undefined && this.shapes[shapeid] !== undefined) ? this.shapes[shapeid] : undefined;
+ },
+
+ getCurrentRegionFields: function () {
+ var currentRegion = this.currentRegion;
+ return {
+ isNull: this.values[currentRegion] === undefined,
+ value: this.values[currentRegion],
+ percent: this.values[currentRegion] / this.total * 100,
+ color: this.options.get('sliceColors')[currentRegion % this.options.get('sliceColors').length],
+ offset: currentRegion
+ };
+ },
+
+ changeHighlight: function (highlight) {
+ var currentRegion = this.currentRegion,
+ newslice = this.renderSlice(currentRegion, highlight),
+ shapeid = this.valueShapes[currentRegion];
+ delete this.shapes[shapeid];
+ this.target.replaceWithShape(shapeid, newslice);
+ this.valueShapes[currentRegion] = newslice.id;
+ this.shapes[newslice.id] = currentRegion;
+ },
+
+ renderSlice: function (valuenum, highlight) {
+ var target = this.target,
+ options = this.options,
+ radius = this.radius,
+ borderWidth = options.get('borderWidth'),
+ offset = options.get('offset'),
+ circle = 2 * Math.PI,
+ values = this.values,
+ total = this.total,
+ next = offset ? (2*Math.PI)*(offset/360) : 0,
+ start, end, i, vlen, color;
+
+ vlen = values.length;
+ for (i = 0; i < vlen; i++) {
+ start = next;
+ end = next;
+ if (total > 0) { // avoid divide by zero
+ end = next + (circle * (values[i] / total));
+ }
+ if (valuenum === i) {
+ color = options.get('sliceColors')[i % options.get('sliceColors').length];
+ if (highlight) {
+ color = this.calcHighlightColor(color, options);
+ }
+
+ return target.drawPieSlice(radius, radius, radius - borderWidth, start, end, undefined, color);
+ }
+ next = end;
+ }
+ },
+
+ render: function () {
+ var target = this.target,
+ values = this.values,
+ options = this.options,
+ radius = this.radius,
+ borderWidth = options.get('borderWidth'),
+ shape, i;
+
+ if (!pie._super.render.call(this)) {
+ return;
+ }
+ if (borderWidth) {
+ target.drawCircle(radius, radius, Math.floor(radius - (borderWidth / 2)),
+ options.get('borderColor'), undefined, borderWidth).append();
+ }
+ for (i = values.length; i--;) {
+ if (values[i]) { // don't render zero values
+ shape = this.renderSlice(i).append();
+ this.valueShapes[i] = shape.id; // store just the shapeid
+ this.shapes[shape.id] = i;
+ }
+ }
+ target.render();
+ }
+ });
+
+ /**
+ * Box plots
+ */
+ $.fn.sparkline.box = box = createClass($.fn.sparkline._base, {
+ type: 'box',
+
+ init: function (el, values, options, width, height) {
+ box._super.init.call(this, el, values, options, width, height);
+ this.values = $.map(values, Number);
+ this.width = options.get('width') === 'auto' ? '4.0em' : width;
+ this.initTarget();
+ if (!this.values.length) {
+ this.disabled = 1;
+ }
+ },
+
+ /**
+ * Simulate a single region
+ */
+ getRegion: function () {
+ return 1;
+ },
+
+ getCurrentRegionFields: function () {
+ var result = [
+ { field: 'lq', value: this.quartiles[0] },
+ { field: 'med', value: this.quartiles[1] },
+ { field: 'uq', value: this.quartiles[2] }
+ ];
+ if (this.loutlier !== undefined) {
+ result.push({ field: 'lo', value: this.loutlier});
+ }
+ if (this.routlier !== undefined) {
+ result.push({ field: 'ro', value: this.routlier});
+ }
+ if (this.lwhisker !== undefined) {
+ result.push({ field: 'lw', value: this.lwhisker});
+ }
+ if (this.rwhisker !== undefined) {
+ result.push({ field: 'rw', value: this.rwhisker});
+ }
+ return result;
+ },
+
+ render: function () {
+ var target = this.target,
+ values = this.values,
+ vlen = values.length,
+ options = this.options,
+ canvasWidth = this.canvasWidth,
+ canvasHeight = this.canvasHeight,
+ minValue = options.get('chartRangeMin') === undefined ? Math.min.apply(Math, values) : options.get('chartRangeMin'),
+ maxValue = options.get('chartRangeMax') === undefined ? Math.max.apply(Math, values) : options.get('chartRangeMax'),
+ canvasLeft = 0,
+ lwhisker, loutlier, iqr, q1, q2, q3, rwhisker, routlier, i,
+ size, unitSize;
+
+ if (!box._super.render.call(this)) {
+ return;
+ }
+
+ if (options.get('raw')) {
+ if (options.get('showOutliers') && values.length > 5) {
+ loutlier = values[0];
+ lwhisker = values[1];
+ q1 = values[2];
+ q2 = values[3];
+ q3 = values[4];
+ rwhisker = values[5];
+ routlier = values[6];
+ } else {
+ lwhisker = values[0];
+ q1 = values[1];
+ q2 = values[2];
+ q3 = values[3];
+ rwhisker = values[4];
+ }
+ } else {
+ values.sort(function (a, b) { return a - b; });
+ q1 = quartile(values, 1);
+ q2 = quartile(values, 2);
+ q3 = quartile(values, 3);
+ iqr = q3 - q1;
+ if (options.get('showOutliers')) {
+ lwhisker = rwhisker = undefined;
+ for (i = 0; i < vlen; i++) {
+ if (lwhisker === undefined && values[i] > q1 - (iqr * options.get('outlierIQR'))) {
+ lwhisker = values[i];
+ }
+ if (values[i] < q3 + (iqr * options.get('outlierIQR'))) {
+ rwhisker = values[i];
+ }
+ }
+ loutlier = values[0];
+ routlier = values[vlen - 1];
+ } else {
+ lwhisker = values[0];
+ rwhisker = values[vlen - 1];
+ }
+ }
+ this.quartiles = [q1, q2, q3];
+ this.lwhisker = lwhisker;
+ this.rwhisker = rwhisker;
+ this.loutlier = loutlier;
+ this.routlier = routlier;
+
+ unitSize = canvasWidth / (maxValue - minValue + 1);
+ if (options.get('showOutliers')) {
+ canvasLeft = Math.ceil(options.get('spotRadius'));
+ canvasWidth -= 2 * Math.ceil(options.get('spotRadius'));
+ unitSize = canvasWidth / (maxValue - minValue + 1);
+ if (loutlier < lwhisker) {
+ target.drawCircle((loutlier - minValue) * unitSize + canvasLeft,
+ canvasHeight / 2,
+ options.get('spotRadius'),
+ options.get('outlierLineColor'),
+ options.get('outlierFillColor')).append();
+ }
+ if (routlier > rwhisker) {
+ target.drawCircle((routlier - minValue) * unitSize + canvasLeft,
+ canvasHeight / 2,
+ options.get('spotRadius'),
+ options.get('outlierLineColor'),
+ options.get('outlierFillColor')).append();
+ }
+ }
+
+ // box
+ target.drawRect(
+ Math.round((q1 - minValue) * unitSize + canvasLeft),
+ Math.round(canvasHeight * 0.1),
+ Math.round((q3 - q1) * unitSize),
+ Math.round(canvasHeight * 0.8),
+ options.get('boxLineColor'),
+ options.get('boxFillColor')).append();
+ // left whisker
+ target.drawLine(
+ Math.round((lwhisker - minValue) * unitSize + canvasLeft),
+ Math.round(canvasHeight / 2),
+ Math.round((q1 - minValue) * unitSize + canvasLeft),
+ Math.round(canvasHeight / 2),
+ options.get('lineColor')).append();
+ target.drawLine(
+ Math.round((lwhisker - minValue) * unitSize + canvasLeft),
+ Math.round(canvasHeight / 4),
+ Math.round((lwhisker - minValue) * unitSize + canvasLeft),
+ Math.round(canvasHeight - canvasHeight / 4),
+ options.get('whiskerColor')).append();
+ // right whisker
+ target.drawLine(Math.round((rwhisker - minValue) * unitSize + canvasLeft),
+ Math.round(canvasHeight / 2),
+ Math.round((q3 - minValue) * unitSize + canvasLeft),
+ Math.round(canvasHeight / 2),
+ options.get('lineColor')).append();
+ target.drawLine(
+ Math.round((rwhisker - minValue) * unitSize + canvasLeft),
+ Math.round(canvasHeight / 4),
+ Math.round((rwhisker - minValue) * unitSize + canvasLeft),
+ Math.round(canvasHeight - canvasHeight / 4),
+ options.get('whiskerColor')).append();
+ // median line
+ target.drawLine(
+ Math.round((q2 - minValue) * unitSize + canvasLeft),
+ Math.round(canvasHeight * 0.1),
+ Math.round((q2 - minValue) * unitSize + canvasLeft),
+ Math.round(canvasHeight * 0.9),
+ options.get('medianColor')).append();
+ if (options.get('target')) {
+ size = Math.ceil(options.get('spotRadius'));
+ target.drawLine(
+ Math.round((options.get('target') - minValue) * unitSize + canvasLeft),
+ Math.round((canvasHeight / 2) - size),
+ Math.round((options.get('target') - minValue) * unitSize + canvasLeft),
+ Math.round((canvasHeight / 2) + size),
+ options.get('targetColor')).append();
+ target.drawLine(
+ Math.round((options.get('target') - minValue) * unitSize + canvasLeft - size),
+ Math.round(canvasHeight / 2),
+ Math.round((options.get('target') - minValue) * unitSize + canvasLeft + size),
+ Math.round(canvasHeight / 2),
+ options.get('targetColor')).append();
+ }
+ target.render();
+ }
+ });
+
+ // Setup a very simple "virtual canvas" to make drawing the few shapes we need easier
+ // This is accessible as $(foo).simpledraw()
+
+ VShape = createClass({
+ init: function (target, id, type, args) {
+ this.target = target;
+ this.id = id;
+ this.type = type;
+ this.args = args;
+ },
+ append: function () {
+ this.target.appendShape(this);
+ return this;
+ }
+ });
+
+ VCanvas_base = createClass({
+ _pxregex: /(\d+)(px)?\s*$/i,
+
+ init: function (width, height, target) {
+ if (!width) {
+ return;
+ }
+ this.width = width;
+ this.height = height;
+ this.target = target;
+ this.lastShapeId = null;
+ if (target[0]) {
+ target = target[0];
+ }
+ $.data(target, '_jqs_vcanvas', this);
+ },
+
+ drawLine: function (x1, y1, x2, y2, lineColor, lineWidth) {
+ return this.drawShape([[x1, y1], [x2, y2]], lineColor, lineWidth);
+ },
+
+ drawShape: function (path, lineColor, fillColor, lineWidth) {
+ return this._genShape('Shape', [path, lineColor, fillColor, lineWidth]);
+ },
+
+ drawCircle: function (x, y, radius, lineColor, fillColor, lineWidth) {
+ return this._genShape('Circle', [x, y, radius, lineColor, fillColor, lineWidth]);
+ },
+
+ drawPieSlice: function (x, y, radius, startAngle, endAngle, lineColor, fillColor) {
+ return this._genShape('PieSlice', [x, y, radius, startAngle, endAngle, lineColor, fillColor]);
+ },
+
+ drawRect: function (x, y, width, height, lineColor, fillColor) {
+ return this._genShape('Rect', [x, y, width, height, lineColor, fillColor]);
+ },
+
+ getElement: function () {
+ return this.canvas;
+ },
+
+ /**
+ * Return the most recently inserted shape id
+ */
+ getLastShapeId: function () {
+ return this.lastShapeId;
+ },
+
+ /**
+ * Clear and reset the canvas
+ */
+ reset: function () {
+ alert('reset not implemented');
+ },
+
+ _insert: function (el, target) {
+ $(target).html(el);
+ },
+
+ /**
+ * Calculate the pixel dimensions of the canvas
+ */
+ _calculatePixelDims: function (width, height, canvas) {
+ // XXX This should probably be a configurable option
+ var match;
+ match = this._pxregex.exec(height);
+ if (match) {
+ this.pixelHeight = match[1];
+ } else {
+ this.pixelHeight = $(canvas).height();
+ }
+ match = this._pxregex.exec(width);
+ if (match) {
+ this.pixelWidth = match[1];
+ } else {
+ this.pixelWidth = $(canvas).width();
+ }
+ },
+
+ /**
+ * Generate a shape object and id for later rendering
+ */
+ _genShape: function (shapetype, shapeargs) {
+ var id = shapeCount++;
+ shapeargs.unshift(id);
+ return new VShape(this, id, shapetype, shapeargs);
+ },
+
+ /**
+ * Add a shape to the end of the render queue
+ */
+ appendShape: function (shape) {
+ alert('appendShape not implemented');
+ },
+
+ /**
+ * Replace one shape with another
+ */
+ replaceWithShape: function (shapeid, shape) {
+ alert('replaceWithShape not implemented');
+ },
+
+ /**
+ * Insert one shape after another in the render queue
+ */
+ insertAfterShape: function (shapeid, shape) {
+ alert('insertAfterShape not implemented');
+ },
+
+ /**
+ * Remove a shape from the queue
+ */
+ removeShapeId: function (shapeid) {
+ alert('removeShapeId not implemented');
+ },
+
+ /**
+ * Find a shape at the specified x/y co-ordinates
+ */
+ getShapeAt: function (el, x, y) {
+ alert('getShapeAt not implemented');
+ },
+
+ /**
+ * Render all queued shapes onto the canvas
+ */
+ render: function () {
+ alert('render not implemented');
+ }
+ });
+
+ VCanvas_canvas = createClass(VCanvas_base, {
+ init: function (width, height, target, interact) {
+ VCanvas_canvas._super.init.call(this, width, height, target);
+ this.canvas = document.createElement('canvas');
+ if (target[0]) {
+ target = target[0];
+ }
+ $.data(target, '_jqs_vcanvas', this);
+ $(this.canvas).css({ display: 'inline-block', width: width, height: height, verticalAlign: 'top' });
+ this._insert(this.canvas, target);
+ this._calculatePixelDims(width, height, this.canvas);
+ this.canvas.width = this.pixelWidth;
+ this.canvas.height = this.pixelHeight;
+ this.interact = interact;
+ this.shapes = {};
+ this.shapeseq = [];
+ this.currentTargetShapeId = undefined;
+ $(this.canvas).css({width: this.pixelWidth, height: this.pixelHeight});
+ },
+
+ _getContext: function (lineColor, fillColor, lineWidth) {
+ var context = this.canvas.getContext('2d');
+ if (lineColor !== undefined) {
+ context.strokeStyle = lineColor;
+ }
+ context.lineWidth = lineWidth === undefined ? 1 : lineWidth;
+ if (fillColor !== undefined) {
+ context.fillStyle = fillColor;
+ }
+ return context;
+ },
+
+ reset: function () {
+ var context = this._getContext();
+ context.clearRect(0, 0, this.pixelWidth, this.pixelHeight);
+ this.shapes = {};
+ this.shapeseq = [];
+ this.currentTargetShapeId = undefined;
+ },
+
+ _drawShape: function (shapeid, path, lineColor, fillColor, lineWidth) {
+ var context = this._getContext(lineColor, fillColor, lineWidth),
+ i, plen;
+ context.beginPath();
+ context.moveTo(path[0][0] + 0.5, path[0][1] + 0.5);
+ for (i = 1, plen = path.length; i < plen; i++) {
+ context.lineTo(path[i][0] + 0.5, path[i][1] + 0.5); // the 0.5 offset gives us crisp pixel-width lines
+ }
+ if (lineColor !== undefined) {
+ context.stroke();
+ }
+ if (fillColor !== undefined) {
+ context.fill();
+ }
+ if (this.targetX !== undefined && this.targetY !== undefined &&
+ context.isPointInPath(this.targetX, this.targetY)) {
+ this.currentTargetShapeId = shapeid;
+ }
+ },
+
+ _drawCircle: function (shapeid, x, y, radius, lineColor, fillColor, lineWidth) {
+ var context = this._getContext(lineColor, fillColor, lineWidth);
+ context.beginPath();
+ context.arc(x, y, radius, 0, 2 * Math.PI, false);
+ if (this.targetX !== undefined && this.targetY !== undefined &&
+ context.isPointInPath(this.targetX, this.targetY)) {
+ this.currentTargetShapeId = shapeid;
+ }
+ if (lineColor !== undefined) {
+ context.stroke();
+ }
+ if (fillColor !== undefined) {
+ context.fill();
+ }
+ },
+
+ _drawPieSlice: function (shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) {
+ var context = this._getContext(lineColor, fillColor);
+ context.beginPath();
+ context.moveTo(x, y);
+ context.arc(x, y, radius, startAngle, endAngle, false);
+ context.lineTo(x, y);
+ context.closePath();
+ if (lineColor !== undefined) {
+ context.stroke();
+ }
+ if (fillColor) {
+ context.fill();
+ }
+ if (this.targetX !== undefined && this.targetY !== undefined &&
+ context.isPointInPath(this.targetX, this.targetY)) {
+ this.currentTargetShapeId = shapeid;
+ }
+ },
+
+ _drawRect: function (shapeid, x, y, width, height, lineColor, fillColor) {
+ return this._drawShape(shapeid, [[x, y], [x + width, y], [x + width, y + height], [x, y + height], [x, y]], lineColor, fillColor);
+ },
+
+ appendShape: function (shape) {
+ this.shapes[shape.id] = shape;
+ this.shapeseq.push(shape.id);
+ this.lastShapeId = shape.id;
+ return shape.id;
+ },
+
+ replaceWithShape: function (shapeid, shape) {
+ var shapeseq = this.shapeseq,
+ i;
+ this.shapes[shape.id] = shape;
+ for (i = shapeseq.length; i--;) {
+ if (shapeseq[i] == shapeid) {
+ shapeseq[i] = shape.id;
+ }
+ }
+ delete this.shapes[shapeid];
+ },
+
+ replaceWithShapes: function (shapeids, shapes) {
+ var shapeseq = this.shapeseq,
+ shapemap = {},
+ sid, i, first;
+
+ for (i = shapeids.length; i--;) {
+ shapemap[shapeids[i]] = true;
+ }
+ for (i = shapeseq.length; i--;) {
+ sid = shapeseq[i];
+ if (shapemap[sid]) {
+ shapeseq.splice(i, 1);
+ delete this.shapes[sid];
+ first = i;
+ }
+ }
+ for (i = shapes.length; i--;) {
+ shapeseq.splice(first, 0, shapes[i].id);
+ this.shapes[shapes[i].id] = shapes[i];
+ }
+
+ },
+
+ insertAfterShape: function (shapeid, shape) {
+ var shapeseq = this.shapeseq,
+ i;
+ for (i = shapeseq.length; i--;) {
+ if (shapeseq[i] === shapeid) {
+ shapeseq.splice(i + 1, 0, shape.id);
+ this.shapes[shape.id] = shape;
+ return;
+ }
+ }
+ },
+
+ removeShapeId: function (shapeid) {
+ var shapeseq = this.shapeseq,
+ i;
+ for (i = shapeseq.length; i--;) {
+ if (shapeseq[i] === shapeid) {
+ shapeseq.splice(i, 1);
+ break;
+ }
+ }
+ delete this.shapes[shapeid];
+ },
+
+ getShapeAt: function (el, x, y) {
+ this.targetX = x;
+ this.targetY = y;
+ this.render();
+ return this.currentTargetShapeId;
+ },
+
+ render: function () {
+ var shapeseq = this.shapeseq,
+ shapes = this.shapes,
+ shapeCount = shapeseq.length,
+ context = this._getContext(),
+ shapeid, shape, i;
+ context.clearRect(0, 0, this.pixelWidth, this.pixelHeight);
+ for (i = 0; i < shapeCount; i++) {
+ shapeid = shapeseq[i];
+ shape = shapes[shapeid];
+ this['_draw' + shape.type].apply(this, shape.args);
+ }
+ if (!this.interact) {
+ // not interactive so no need to keep the shapes array
+ this.shapes = {};
+ this.shapeseq = [];
+ }
+ }
+
+ });
+
+ VCanvas_vml = createClass(VCanvas_base, {
+ init: function (width, height, target) {
+ var groupel;
+ VCanvas_vml._super.init.call(this, width, height, target);
+ if (target[0]) {
+ target = target[0];
+ }
+ $.data(target, '_jqs_vcanvas', this);
+ this.canvas = document.createElement('span');
+ $(this.canvas).css({ display: 'inline-block', position: 'relative', overflow: 'hidden', width: width, height: height, margin: '0px', padding: '0px', verticalAlign: 'top'});
+ this._insert(this.canvas, target);
+ this._calculatePixelDims(width, height, this.canvas);
+ this.canvas.width = this.pixelWidth;
+ this.canvas.height = this.pixelHeight;
+ groupel = '';
+ this.canvas.insertAdjacentHTML('beforeEnd', groupel);
+ this.group = $(this.canvas).children()[0];
+ this.rendered = false;
+ this.prerender = '';
+ },
+
+ _drawShape: function (shapeid, path, lineColor, fillColor, lineWidth) {
+ var vpath = [],
+ initial, stroke, fill, closed, vel, plen, i;
+ for (i = 0, plen = path.length; i < plen; i++) {
+ vpath[i] = '' + (path[i][0]) + ',' + (path[i][1]);
+ }
+ initial = vpath.splice(0, 1);
+ lineWidth = lineWidth === undefined ? 1 : lineWidth;
+ stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" ';
+ fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
+ closed = vpath[0] === vpath[vpath.length - 1] ? 'x ' : '';
+ vel = '' +
+ ' ';
+ return vel;
+ },
+
+ _drawCircle: function (shapeid, x, y, radius, lineColor, fillColor, lineWidth) {
+ var stroke, fill, vel;
+ x -= radius;
+ y -= radius;
+ stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" ';
+ fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
+ vel = '';
+ return vel;
+
+ },
+
+ _drawPieSlice: function (shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) {
+ var vpath, startx, starty, endx, endy, stroke, fill, vel;
+ if (startAngle === endAngle) {
+ return ''; // VML seems to have problem when start angle equals end angle.
+ }
+ if ((endAngle - startAngle) === (2 * Math.PI)) {
+ startAngle = 0.0; // VML seems to have a problem when drawing a full circle that doesn't start 0
+ endAngle = (2 * Math.PI);
+ }
+
+ startx = x + Math.round(Math.cos(startAngle) * radius);
+ starty = y + Math.round(Math.sin(startAngle) * radius);
+ endx = x + Math.round(Math.cos(endAngle) * radius);
+ endy = y + Math.round(Math.sin(endAngle) * radius);
+
+ if (startx === endx && starty === endy) {
+ if ((endAngle - startAngle) < Math.PI) {
+ // Prevent very small slices from being mistaken as a whole pie
+ return '';
+ }
+ // essentially going to be the entire circle, so ignore startAngle
+ startx = endx = x + radius;
+ starty = endy = y;
+ }
+
+ if (startx === endx && starty === endy && (endAngle - startAngle) < Math.PI) {
+ return '';
+ }
+
+ vpath = [x - radius, y - radius, x + radius, y + radius, startx, starty, endx, endy];
+ stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="1px" strokeColor="' + lineColor + '" ';
+ fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
+ vel = '' +
+ ' ';
+ return vel;
+ },
+
+ _drawRect: function (shapeid, x, y, width, height, lineColor, fillColor) {
+ return this._drawShape(shapeid, [[x, y], [x, y + height], [x + width, y + height], [x + width, y], [x, y]], lineColor, fillColor);
+ },
+
+ reset: function () {
+ this.group.innerHTML = '';
+ },
+
+ appendShape: function (shape) {
+ var vel = this['_draw' + shape.type].apply(this, shape.args);
+ if (this.rendered) {
+ this.group.insertAdjacentHTML('beforeEnd', vel);
+ } else {
+ this.prerender += vel;
+ }
+ this.lastShapeId = shape.id;
+ return shape.id;
+ },
+
+ replaceWithShape: function (shapeid, shape) {
+ var existing = $('#jqsshape' + shapeid),
+ vel = this['_draw' + shape.type].apply(this, shape.args);
+ existing[0].outerHTML = vel;
+ },
+
+ replaceWithShapes: function (shapeids, shapes) {
+ // replace the first shapeid with all the new shapes then toast the remaining old shapes
+ var existing = $('#jqsshape' + shapeids[0]),
+ replace = '',
+ slen = shapes.length,
+ i;
+ for (i = 0; i < slen; i++) {
+ replace += this['_draw' + shapes[i].type].apply(this, shapes[i].args);
+ }
+ existing[0].outerHTML = replace;
+ for (i = 1; i < shapeids.length; i++) {
+ $('#jqsshape' + shapeids[i]).remove();
+ }
+ },
+
+ insertAfterShape: function (shapeid, shape) {
+ var existing = $('#jqsshape' + shapeid),
+ vel = this['_draw' + shape.type].apply(this, shape.args);
+ existing[0].insertAdjacentHTML('afterEnd', vel);
+ },
+
+ removeShapeId: function (shapeid) {
+ var existing = $('#jqsshape' + shapeid);
+ this.group.removeChild(existing[0]);
+ },
+
+ getShapeAt: function (el, x, y) {
+ var shapeid = el.id.substr(8);
+ return shapeid;
+ },
+
+ render: function () {
+ if (!this.rendered) {
+ // batch the intial render into a single repaint
+ this.group.innerHTML = this.prerender;
+ this.rendered = true;
+ }
+ }
+ });
+
+}))}(document, Math));
\ No newline at end of file
diff --git a/addons/web_kanban_sparkline/static/src/js/kanban_sparkline.js b/addons/web_kanban_sparkline/static/src/js/kanban_sparkline.js
new file mode 100644
index 00000000000..de21cbba945
--- /dev/null
+++ b/addons/web_kanban_sparkline/static/src/js/kanban_sparkline.js
@@ -0,0 +1,37 @@
+openerp.web_kanban_sparkline = function (instance) {
+
+/**
+ * Kanban widgets: Sparkline
+ *
+ */
+
+instance.web_kanban.SparklineBarWidget = instance.web_kanban.AbstractField.extend({
+ className: "oe_sparkline_bar",
+ start: function() {
+ var self = this;
+ var title = this.$node.html() || this.field.string;
+ setTimeout(function () {
+ var value = _.pluck(self.field.value, 'value');
+ var tooltips = _.pluck(self.field.value, 'tooltip');
+ var sparkline_options = _.extend({
+ type: 'bar',
+ barWidth: 5,
+ height: '20px',
+ barWidth: 4,
+ barSpacing: 1,
+ barColor: '#96d854',
+ tooltipFormat: '{{offset:offset}} {{value}}',
+ tooltipValueLookups: {
+ 'offset': tooltips
+ }
+ }, self.options);
+ self.$el.sparkline(value, sparkline_options);
+ self.$el.tipsy({'delayIn': 0, 'html': true, 'title': function(){return title}, 'gravity': 'n'});
+ }, 0);
+ },
+});
+
+instance.web_kanban.fields_registry.add("sparkline_bar", "instance.web_kanban.SparklineBarWidget");
+
+
+}
--
cgit v1.2.3
From 20ba3bd349647750452e35703dc78c0869957ed5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Fri, 6 Sep 2013 12:05:54 +0200
Subject: [IMP] crm, mass_mailing, sale_crm: added dependencies to
web_kanban_gauge/sparkline newly introduced modules
bzr revid: tde@openerp.com-20130906100554-vcg4rux2f5omyq3t
---
addons/crm/__openerp__.py | 3 ++-
addons/mass_mailing/__openerp__.py | 7 ++++++-
addons/mass_mailing/mass_mailing_view.xml | 4 ----
addons/sale_crm/__openerp__.py | 2 +-
4 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/addons/crm/__openerp__.py b/addons/crm/__openerp__.py
index ae9da7ade08..176186f016c 100644
--- a/addons/crm/__openerp__.py
+++ b/addons/crm/__openerp__.py
@@ -57,7 +57,8 @@ Dashboard for CRM will include:
'base_calendar',
'resource',
'board',
- 'fetchmail'
+ 'fetchmail',
+ 'web_kanban_sparkline',
],
'data': [
'crm_data.xml',
diff --git a/addons/mass_mailing/__openerp__.py b/addons/mass_mailing/__openerp__.py
index 9bb5b746f63..147b9bea89c 100644
--- a/addons/mass_mailing/__openerp__.py
+++ b/addons/mass_mailing/__openerp__.py
@@ -25,7 +25,12 @@
'author': 'OpenERP',
'website': 'http://www.openerp.com',
'category': 'Marketing',
- 'depends': ['mail', 'email_template'],
+ 'depends': [
+ 'mail',
+ 'email_template',
+ 'web_kanban_gauge',
+ 'web_kanban_sparkline',
+ ],
'description': """TODO""",
'data': [
'mass_mailing_view.xml',
diff --git a/addons/mass_mailing/mass_mailing_view.xml b/addons/mass_mailing/mass_mailing_view.xml
index ab421dda881..4a3c2b5db07 100644
--- a/addons/mass_mailing/mass_mailing_view.xml
+++ b/addons/mass_mailing/mass_mailing_view.xml
@@ -193,10 +193,6 @@
kanban,tree,form
-
-
-
diff --git a/addons/sale_crm/__openerp__.py b/addons/sale_crm/__openerp__.py
index 0cc4ad3fad2..7a57484eab8 100644
--- a/addons/sale_crm/__openerp__.py
+++ b/addons/sale_crm/__openerp__.py
@@ -37,7 +37,7 @@ modules.
'author': 'OpenERP SA',
'website': 'http://www.openerp.com',
'images': ['images/crm_statistics_dashboard.jpeg', 'images/opportunity_to_quote.jpeg'],
- 'depends': ['sale', 'crm'],
+ 'depends': ['sale', 'crm', 'web_kanban_gauge'],
'data': [
'wizard/crm_make_sale_view.xml',
'sale_crm_view.xml',
--
cgit v1.2.3
From 205a5c37c0925d82507ea97884ef427f0794ef27 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Fri, 6 Sep 2013 12:11:55 +0200
Subject: [CLEAN] qweb, kanban: removed forgotten console.log + unnecessary
void line
bzr revid: tde@openerp.com-20130906101155-0o58oycwb992s093
---
addons/web/static/lib/qweb/qweb2.js | 1 -
addons/web_kanban/static/src/js/kanban.js | 1 -
2 files changed, 2 deletions(-)
diff --git a/addons/web/static/lib/qweb/qweb2.js b/addons/web/static/lib/qweb/qweb2.js
index 44db469cd89..52e975322e5 100644
--- a/addons/web/static/lib/qweb/qweb2.js
+++ b/addons/web/static/lib/qweb/qweb2.js
@@ -384,7 +384,6 @@ QWeb2.Engine = (function() {
this.compiled_templates[template] = tcompiled;
return this.render(template, dict);
} else {
- console.log(this.compiled_templates, template);
return this.tools.exception("Template '" + template + "' not found");
}
},
diff --git a/addons/web_kanban/static/src/js/kanban.js b/addons/web_kanban/static/src/js/kanban.js
index 8ec11e37d25..6f47f9c128d 100644
--- a/addons/web_kanban/static/src/js/kanban.js
+++ b/addons/web_kanban/static/src/js/kanban.js
@@ -1231,7 +1231,6 @@ instance.web_kanban.AbstractField = instance.web.Widget.extend(instance.web_kanb
});
instance.web_kanban.fields_registry = new instance.web.Registry({});
-
};
// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
--
cgit v1.2.3
From cd63d4d21a171c0c35e593cd04631ad25f8e1d1a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Fri, 6 Sep 2013 12:33:25 +0200
Subject: [ADD] mass_mailing: added directories + skeletons for doc and tests
bzr revid: tde@openerp.com-20130906103325-e15fmq49puldcwu3
---
addons/mass_mailing/doc/changelog.rst | 9 +++++++++
addons/mass_mailing/doc/index.rst | 13 +++++++++++++
addons/mass_mailing/tests/__init__.py | 26 ++++++++++++++++++++++++++
addons/mass_mailing/tests/test_mail.py | 29 +++++++++++++++++++++++++++++
4 files changed, 77 insertions(+)
create mode 100644 addons/mass_mailing/doc/changelog.rst
create mode 100644 addons/mass_mailing/doc/index.rst
create mode 100644 addons/mass_mailing/tests/__init__.py
create mode 100644 addons/mass_mailing/tests/test_mail.py
diff --git a/addons/mass_mailing/doc/changelog.rst b/addons/mass_mailing/doc/changelog.rst
new file mode 100644
index 00000000000..a000c4ae396
--- /dev/null
+++ b/addons/mass_mailing/doc/changelog.rst
@@ -0,0 +1,9 @@
+.. _changelog:
+
+Changelog
+=========
+
+`trunk (saas-2)`
+----------------
+
+ - added module
\ No newline at end of file
diff --git a/addons/mass_mailing/doc/index.rst b/addons/mass_mailing/doc/index.rst
new file mode 100644
index 00000000000..3d991c7d9dd
--- /dev/null
+++ b/addons/mass_mailing/doc/index.rst
@@ -0,0 +1,13 @@
+Mass Mailing module documentation
+=================================
+
+Mass Mailing documentation topics
+'''''''''''''''''''''''''''''''''
+
+Changelog
+'''''''''
+
+.. toctree::
+ :maxdepth: 1
+
+ changelog.rst
diff --git a/addons/mass_mailing/tests/__init__.py b/addons/mass_mailing/tests/__init__.py
new file mode 100644
index 00000000000..f8610905393
--- /dev/null
+++ b/addons/mass_mailing/tests/__init__.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Business Applications
+# Copyright (C) 2013-Today OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+from openerp.addons.mass_mailing.tests import test_mail
+
+checks = [
+ test_mail,
+]
diff --git a/addons/mass_mailing/tests/test_mail.py b/addons/mass_mailing/tests/test_mail.py
new file mode 100644
index 00000000000..0e3b4b8608e
--- /dev/null
+++ b/addons/mass_mailing/tests/test_mail.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Business Applications
+# Copyright (C) 2013-Today OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+from openerp.addons.mail.tests.common import TestMail
+
+
+class test_message_compose(TestMail):
+
+ def test_OO_mail_mail_tracking(self):
+ """ Tests designed for mail_mail tracking (opened, replied, bounced) """
+ pass
--
cgit v1.2.3
From 87ff039a2aa41435e4fc593b97aaf58ce275f140 Mon Sep 17 00:00:00 2001
From: "Sunil Sharma (OpenERP Trainee)"
Date: Fri, 6 Sep 2013 16:27:05 +0530
Subject: [IMP]:trunk account chart template data
bzr revid: sunilsharma.sharma07@gmail.com-20130906105705-j2u30ldytf3qev3r
---
addons/l10n_ar/l10n_ar_chart.xml | 1 +
addons/l10n_at/account_chart.xml | 1 +
addons/l10n_be/account_chart_template.xml | 1 +
addons/l10n_bo/l10n_bo_chart.xml | 1 +
addons/l10n_br/data/account_chart_template.xml | 1 +
addons/l10n_ca/account_chart_template_en.xml | 1 +
addons/l10n_ca/account_chart_template_fr.xml | 1 +
addons/l10n_ch/sterchi_chart/account.xml | 1 +
addons/l10n_cl/l10n_cl_chart.xml | 1 +
addons/l10n_cn/account_chart.xml | 1 +
addons/l10n_co/data/account_chart.xml | 1 +
addons/l10n_cr/data/account_chart_template.xml | 2 ++
addons/l10n_de/account_chart_template_skr03.xml | 1 +
addons/l10n_de/account_chart_template_skr04.xml | 1 +
addons/l10n_ec/account_chart.xml | 3 ++-
addons/l10n_es/account_chart.xml | 1 +
addons/l10n_es/account_chart_assoc.xml | 1 +
addons/l10n_es/account_chart_pymes.xml | 1 +
addons/l10n_fr/fr_pcg_taxes.xml | 1 +
addons/l10n_gr/account_tax.xml | 1 +
addons/l10n_gt/l10n_gt_base.xml | 1 +
addons/l10n_hn/l10n_hn_base.xml | 1 +
addons/l10n_hr/l10n_hr_chart_template.xml | 1 +
addons/l10n_in/l10n_in_private_chart.xml | 1 +
addons/l10n_in/l10n_in_public_chart.xml | 1 +
addons/l10n_it/account_chart.xml | 1 +
addons/l10n_lu/account_chart_template.xml | 1 +
addons/l10n_ma/l10n_ma_tax.xml | 1 +
addons/l10n_mx/data/account_chart.xml | 1 +
addons/l10n_nl/account_chart_netherlands.xml | 1 +
addons/l10n_pa/l10n_pa_chart.xml | 1 +
addons/l10n_pe/l10n_pe_chart.xml | 3 ++-
addons/l10n_pl/account_chart.xml | 1 +
addons/l10n_pt/account_chart_template.xml | 1 +
addons/l10n_ro/account_chart.xml | 2 +-
addons/l10n_syscohada/l10n_syscohada_data.xml | 1 +
addons/l10n_th/account_data.xml | 1 +
addons/l10n_tr/account_chart_template.xml | 1 +
addons/l10n_us/account_chart_template_after.xml | 8 ++++++++
addons/l10n_uy/account_chart_template.xml | 1 +
addons/l10n_ve/data/account_chart.xml | 1 +
addons/l10n_vn/account_chart.xml | 4 ++--
42 files changed, 53 insertions(+), 5 deletions(-)
diff --git a/addons/l10n_ar/l10n_ar_chart.xml b/addons/l10n_ar/l10n_ar_chart.xml
index 062697d5aa6..02b2f3e97bf 100644
--- a/addons/l10n_ar/l10n_ar_chart.xml
+++ b/addons/l10n_ar/l10n_ar_chart.xml
@@ -252,6 +252,7 @@
+
diff --git a/addons/l10n_at/account_chart.xml b/addons/l10n_at/account_chart.xml
index 51d0c9a85a8..eca220994e6 100644
--- a/addons/l10n_at/account_chart.xml
+++ b/addons/l10n_at/account_chart.xml
@@ -2518,6 +2518,7 @@
+
diff --git a/addons/l10n_be/account_chart_template.xml b/addons/l10n_be/account_chart_template.xml
index 55726fdc335..48e506ded79 100644
--- a/addons/l10n_be/account_chart_template.xml
+++ b/addons/l10n_be/account_chart_template.xml
@@ -13,6 +13,7 @@
+
diff --git a/addons/l10n_bo/l10n_bo_chart.xml b/addons/l10n_bo/l10n_bo_chart.xml
index dc9e9598909..3d7e5cc1935 100644
--- a/addons/l10n_bo/l10n_bo_chart.xml
+++ b/addons/l10n_bo/l10n_bo_chart.xml
@@ -249,6 +249,7 @@
+
diff --git a/addons/l10n_br/data/account_chart_template.xml b/addons/l10n_br/data/account_chart_template.xml
index c99bbd03d2f..598c3191cba 100644
--- a/addons/l10n_br/data/account_chart_template.xml
+++ b/addons/l10n_br/data/account_chart_template.xml
@@ -11,6 +11,7 @@
+
diff --git a/addons/l10n_ca/account_chart_template_en.xml b/addons/l10n_ca/account_chart_template_en.xml
index 635fa7b8bb1..64e0632382e 100644
--- a/addons/l10n_ca/account_chart_template_en.xml
+++ b/addons/l10n_ca/account_chart_template_en.xml
@@ -13,6 +13,7 @@
+
diff --git a/addons/l10n_ca/account_chart_template_fr.xml b/addons/l10n_ca/account_chart_template_fr.xml
index 7d1cc0a9ae2..510153200c6 100644
--- a/addons/l10n_ca/account_chart_template_fr.xml
+++ b/addons/l10n_ca/account_chart_template_fr.xml
@@ -12,6 +12,7 @@
+
diff --git a/addons/l10n_ch/sterchi_chart/account.xml b/addons/l10n_ch/sterchi_chart/account.xml
index ddad62946ae..acee99cad4a 100644
--- a/addons/l10n_ch/sterchi_chart/account.xml
+++ b/addons/l10n_ch/sterchi_chart/account.xml
@@ -11812,6 +11812,7 @@
+
diff --git a/addons/l10n_cl/l10n_cl_chart.xml b/addons/l10n_cl/l10n_cl_chart.xml
index 8bc4272c2eb..e6d7b1d75bd 100644
--- a/addons/l10n_cl/l10n_cl_chart.xml
+++ b/addons/l10n_cl/l10n_cl_chart.xml
@@ -248,6 +248,7 @@
+
diff --git a/addons/l10n_cn/account_chart.xml b/addons/l10n_cn/account_chart.xml
index afcb3e04b0c..fe4690e306c 100644
--- a/addons/l10n_cn/account_chart.xml
+++ b/addons/l10n_cn/account_chart.xml
@@ -957,6 +957,7 @@
+
diff --git a/addons/l10n_co/data/account_chart.xml b/addons/l10n_co/data/account_chart.xml
index 2fe4ac9a1b9..06d6d471e1e 100644
--- a/addons/l10n_co/data/account_chart.xml
+++ b/addons/l10n_co/data/account_chart.xml
@@ -103348,6 +103348,7 @@ participacion, de conformidad con las disposiciones legales vigentes.
+
diff --git a/addons/l10n_cr/data/account_chart_template.xml b/addons/l10n_cr/data/account_chart_template.xml
index e84ea73fbed..a8a08647c00 100644
--- a/addons/l10n_cr/data/account_chart_template.xml
+++ b/addons/l10n_cr/data/account_chart_template.xml
@@ -16,6 +16,7 @@
+ Costa Rica - Company 1
@@ -28,6 +29,7 @@
+
diff --git a/addons/l10n_de/account_chart_template_skr03.xml b/addons/l10n_de/account_chart_template_skr03.xml
index 058424188f6..ce71f132de3 100644
--- a/addons/l10n_de/account_chart_template_skr03.xml
+++ b/addons/l10n_de/account_chart_template_skr03.xml
@@ -12,6 +12,7 @@
+
diff --git a/addons/l10n_de/account_chart_template_skr04.xml b/addons/l10n_de/account_chart_template_skr04.xml
index c33c0d415eb..fee680af379 100644
--- a/addons/l10n_de/account_chart_template_skr04.xml
+++ b/addons/l10n_de/account_chart_template_skr04.xml
@@ -12,6 +12,7 @@
+
diff --git a/addons/l10n_ec/account_chart.xml b/addons/l10n_ec/account_chart.xml
index 17f1b71d047..1cf12529c6c 100644
--- a/addons/l10n_ec/account_chart.xml
+++ b/addons/l10n_ec/account_chart.xml
@@ -4202,7 +4202,8 @@
-
+
+
diff --git a/addons/l10n_es/account_chart.xml b/addons/l10n_es/account_chart.xml
index 1bc63f6419f..8260a76dca1 100644
--- a/addons/l10n_es/account_chart.xml
+++ b/addons/l10n_es/account_chart.xml
@@ -13333,6 +13333,7 @@
+
diff --git a/addons/l10n_es/account_chart_assoc.xml b/addons/l10n_es/account_chart_assoc.xml
index 670eed53c9b..105be447ca8 100644
--- a/addons/l10n_es/account_chart_assoc.xml
+++ b/addons/l10n_es/account_chart_assoc.xml
@@ -12420,6 +12420,7 @@
+
diff --git a/addons/l10n_es/account_chart_pymes.xml b/addons/l10n_es/account_chart_pymes.xml
index 223d80e3193..1986a532a66 100644
--- a/addons/l10n_es/account_chart_pymes.xml
+++ b/addons/l10n_es/account_chart_pymes.xml
@@ -11464,6 +11464,7 @@
+
diff --git a/addons/l10n_fr/fr_pcg_taxes.xml b/addons/l10n_fr/fr_pcg_taxes.xml
index 78b34f2d696..2279b077383 100644
--- a/addons/l10n_fr/fr_pcg_taxes.xml
+++ b/addons/l10n_fr/fr_pcg_taxes.xml
@@ -441,6 +441,7 @@
+
diff --git a/addons/l10n_gr/account_tax.xml b/addons/l10n_gr/account_tax.xml
index c3de033869b..36148f49a9b 100644
--- a/addons/l10n_gr/account_tax.xml
+++ b/addons/l10n_gr/account_tax.xml
@@ -21,6 +21,7 @@
+
diff --git a/addons/l10n_gt/l10n_gt_base.xml b/addons/l10n_gt/l10n_gt_base.xml
index 8caf6ebe48c..bfc91ddb83e 100644
--- a/addons/l10n_gt/l10n_gt_base.xml
+++ b/addons/l10n_gt/l10n_gt_base.xml
@@ -31,6 +31,7 @@
+
diff --git a/addons/l10n_hn/l10n_hn_base.xml b/addons/l10n_hn/l10n_hn_base.xml
index b8b85c3f2a7..8b9acf5c50a 100644
--- a/addons/l10n_hn/l10n_hn_base.xml
+++ b/addons/l10n_hn/l10n_hn_base.xml
@@ -17,6 +17,7 @@
+
diff --git a/addons/l10n_hr/l10n_hr_chart_template.xml b/addons/l10n_hr/l10n_hr_chart_template.xml
index d9a7db5710c..1cd998115c6 100644
--- a/addons/l10n_hr/l10n_hr_chart_template.xml
+++ b/addons/l10n_hr/l10n_hr_chart_template.xml
@@ -15,6 +15,7 @@
+
diff --git a/addons/l10n_in/l10n_in_private_chart.xml b/addons/l10n_in/l10n_in_private_chart.xml
index 4b8e4be55bf..efaa9844416 100644
--- a/addons/l10n_in/l10n_in_private_chart.xml
+++ b/addons/l10n_in/l10n_in_private_chart.xml
@@ -550,6 +550,7 @@
+
diff --git a/addons/l10n_in/l10n_in_public_chart.xml b/addons/l10n_in/l10n_in_public_chart.xml
index 91d285408d5..18633af054f 100644
--- a/addons/l10n_in/l10n_in_public_chart.xml
+++ b/addons/l10n_in/l10n_in_public_chart.xml
@@ -988,6 +988,7 @@
+
diff --git a/addons/l10n_it/account_chart.xml b/addons/l10n_it/account_chart.xml
index 6dd7b539b49..ca64348d482 100644
--- a/addons/l10n_it/account_chart.xml
+++ b/addons/l10n_it/account_chart.xml
@@ -11,6 +11,7 @@
+
diff --git a/addons/l10n_lu/account_chart_template.xml b/addons/l10n_lu/account_chart_template.xml
index f0986277343..254feaa932e 100644
--- a/addons/l10n_lu/account_chart_template.xml
+++ b/addons/l10n_lu/account_chart_template.xml
@@ -13,6 +13,7 @@
+
diff --git a/addons/l10n_ma/l10n_ma_tax.xml b/addons/l10n_ma/l10n_ma_tax.xml
index f296b22edca..5bc59035861 100644
--- a/addons/l10n_ma/l10n_ma_tax.xml
+++ b/addons/l10n_ma/l10n_ma_tax.xml
@@ -744,6 +744,7 @@
+
diff --git a/addons/l10n_mx/data/account_chart.xml b/addons/l10n_mx/data/account_chart.xml
index 491942e4b72..daa564fd10e 100644
--- a/addons/l10n_mx/data/account_chart.xml
+++ b/addons/l10n_mx/data/account_chart.xml
@@ -3695,6 +3695,7 @@ Cuentas del plan
+
diff --git a/addons/l10n_nl/account_chart_netherlands.xml b/addons/l10n_nl/account_chart_netherlands.xml
index c7eefb42cde..03d9879a100 100644
--- a/addons/l10n_nl/account_chart_netherlands.xml
+++ b/addons/l10n_nl/account_chart_netherlands.xml
@@ -4102,6 +4102,7 @@
+
-
+
diff --git a/addons/l10n_syscohada/l10n_syscohada_data.xml b/addons/l10n_syscohada/l10n_syscohada_data.xml
index d90988e00e3..d4be067bfa2 100644
--- a/addons/l10n_syscohada/l10n_syscohada_data.xml
+++ b/addons/l10n_syscohada/l10n_syscohada_data.xml
@@ -1850,6 +1850,7 @@
+
diff --git a/addons/l10n_th/account_data.xml b/addons/l10n_th/account_data.xml
index 9ae3d9e3364..a6de0ced477 100644
--- a/addons/l10n_th/account_data.xml
+++ b/addons/l10n_th/account_data.xml
@@ -477,6 +477,7 @@
+
diff --git a/addons/l10n_tr/account_chart_template.xml b/addons/l10n_tr/account_chart_template.xml
index 3e10353298e..60853ff7127 100644
--- a/addons/l10n_tr/account_chart_template.xml
+++ b/addons/l10n_tr/account_chart_template.xml
@@ -12,6 +12,7 @@
+
diff --git a/addons/l10n_us/account_chart_template_after.xml b/addons/l10n_us/account_chart_template_after.xml
index 4dea930f09f..4cd1472773d 100644
--- a/addons/l10n_us/account_chart_template_after.xml
+++ b/addons/l10n_us/account_chart_template_after.xml
@@ -8,33 +8,41 @@
+
+
+
+
+
+
+
+
diff --git a/addons/l10n_uy/account_chart_template.xml b/addons/l10n_uy/account_chart_template.xml
index 4ce35184967..8433ed18c25 100644
--- a/addons/l10n_uy/account_chart_template.xml
+++ b/addons/l10n_uy/account_chart_template.xml
@@ -1895,6 +1895,7 @@
+
diff --git a/addons/l10n_ve/data/account_chart.xml b/addons/l10n_ve/data/account_chart.xml
index 05645805a84..7c859083aed 100644
--- a/addons/l10n_ve/data/account_chart.xml
+++ b/addons/l10n_ve/data/account_chart.xml
@@ -3315,6 +3315,7 @@
+
diff --git a/addons/l10n_vn/account_chart.xml b/addons/l10n_vn/account_chart.xml
index c7d3f6a9ee3..a4d63681e01 100755
--- a/addons/l10n_vn/account_chart.xml
+++ b/addons/l10n_vn/account_chart.xml
@@ -1,4 +1,4 @@
-
+
@@ -2015,7 +2015,7 @@
-
+
--
cgit v1.2.3
From 7650083606d0eaac4572d71f4bf08601d22268ee Mon Sep 17 00:00:00 2001
From: "Turkesh Patel (Open ERP)"
Date: Fri, 6 Sep 2013 18:18:36 +0530
Subject: [IMP] l10n_de: improved currency.
bzr revid: tpa@tinyerp.com-20130906124836-40p7xtnu0qwl8g94
---
addons/l10n_de/account_chart_template_skr03.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/l10n_de/account_chart_template_skr03.xml b/addons/l10n_de/account_chart_template_skr03.xml
index ce71f132de3..0e2e239fd19 100644
--- a/addons/l10n_de/account_chart_template_skr03.xml
+++ b/addons/l10n_de/account_chart_template_skr03.xml
@@ -12,7 +12,7 @@
-
+
--
cgit v1.2.3
From 12580e690aea8b9ea3a4862e547cf30e323c683a Mon Sep 17 00:00:00 2001
From: Antony Lesuisse
Date: Sun, 8 Sep 2013 19:35:35 +0200
Subject: [IMP] openerp threaded, gevent, prefork service cleanup - unify
signal handling - unify start and stop no new feature yet, it paves the way
for - developement mode auto reload - graceful restart on HUP -
multiprocessing and gevent on windows
bzr revid: al@openerp.com-20130908173535-xomt5w7xmqtwkmyy
---
openerp-long-polling | 11 --
openerp/__init__.py | 11 +-
openerp/cli/server.py | 120 +---------------
openerp/service/__init__.py | 116 +---------------
openerp/service/cron.py | 76 ----------
openerp/service/workers.py | 309 +++++++++++++++++++++++++++++++++++++++--
openerp/service/wsgi_server.py | 66 ---------
7 files changed, 317 insertions(+), 392 deletions(-)
delete mode 100755 openerp-long-polling
delete mode 100644 openerp/service/cron.py
diff --git a/openerp-long-polling b/openerp-long-polling
deleted file mode 100755
index 304cd01c1ee..00000000000
--- a/openerp-long-polling
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/usr/bin/env python
-
-import gevent.monkey
-gevent.monkey.patch_all()
-import gevent_psycopg2
-gevent_psycopg2.monkey_patch()
-
-import openerp
-
-if __name__ == "__main__":
- openerp.cli.main()
diff --git a/openerp/__init__.py b/openerp/__init__.py
index 22c8c98256a..161b06b0990 100644
--- a/openerp/__init__.py
+++ b/openerp/__init__.py
@@ -27,9 +27,16 @@ import sys
# Is the server running with gevent.
evented = False
-if sys.modules.get("gevent") is not None:
- evented = True
+for i in sys.argv:
+ if i.startswith('--gevent'):
+ evented = True
+ break
+if evented:
+ import gevent.monkey
+ gevent.monkey.patch_all()
+ import gevent_psycopg2
+ gevent_psycopg2.monkey_patch()
# Make sure the OpenERP server runs in UTC. This is especially necessary
# under Windows as under Linux it seems the real import of time is
diff --git a/openerp/cli/server.py b/openerp/cli/server.py
index fc1bf61b617..0c081b6156b 100644
--- a/openerp/cli/server.py
+++ b/openerp/cli/server.py
@@ -147,112 +147,15 @@ def import_translation():
cr.commit()
cr.close()
-# Variable keeping track of the number of calls to the signal handler defined
-# below. This variable is monitored by ``quit_on_signals()``.
-quit_signals_received = 0
-
-def signal_handler(sig, frame):
- """ Signal handler: exit ungracefully on the second handled signal.
-
- :param sig: the signal number
- :param frame: the interrupted stack frame or None
- """
- global quit_signals_received
- quit_signals_received += 1
- if quit_signals_received > 1:
- # logging.shutdown was already called at this point.
- sys.stderr.write("Forced shutdown.\n")
- os._exit(0)
-
-def dumpstacks(sig, frame):
- """ Signal handler: dump a stack trace for each existing thread."""
- # code from http://stackoverflow.com/questions/132058/getting-stack-trace-from-a-running-python-application#answer-2569696
- # modified for python 2.5 compatibility
- threads_info = dict([(th.ident, {'name': th.name,
- 'uid': getattr(th,'uid','n/a')})
- for th in threading.enumerate()])
- code = []
- for threadId, stack in sys._current_frames().items():
- thread_info = threads_info.get(threadId)
- code.append("\n# Thread: %s (id:%s) (uid:%s)" % \
- (thread_info and thread_info['name'] or 'n/a',
- threadId,
- thread_info and thread_info['uid'] or 'n/a'))
- for filename, lineno, name, line in traceback.extract_stack(stack):
- code.append('File: "%s", line %d, in %s' % (filename, lineno, name))
- if line:
- code.append(" %s" % (line.strip()))
- _logger.info("\n".join(code))
-
-def setup_signal_handlers(signal_handler):
- """ Register the given signal handler. """
- SIGNALS = (signal.SIGINT, signal.SIGTERM)
- if os.name == 'posix':
- map(lambda sig: signal.signal(sig, signal_handler), SIGNALS)
- signal.signal(signal.SIGQUIT, dumpstacks)
- elif os.name == 'nt':
- import win32api
- win32api.SetConsoleCtrlHandler(lambda sig: signal_handler(sig, None), 1)
-
-def quit_on_signals():
- """ Wait for one or two signals then shutdown the server.
-
- The first SIGINT or SIGTERM signal will initiate a graceful shutdown while
- a second one if any will force an immediate exit.
-
- """
- # Wait for a first signal to be handled. (time.sleep will be interrupted
- # by the signal handler.) The try/except is for the win32 case.
- try:
- while quit_signals_received == 0:
- time.sleep(60)
- except KeyboardInterrupt:
- pass
-
- config = openerp.tools.config
- openerp.service.stop_services()
-
- if getattr(openerp, 'phoenix', False):
- # like the phoenix, reborn from ashes...
- openerp.service._reexec()
- return
-
- if config['pidfile']:
- os.unlink(config['pidfile'])
- sys.exit(0)
-
-def watch_parent(beat=4):
- import gevent
- ppid = os.getppid()
- while True:
- if ppid != os.getppid():
- pid = os.getpid()
- _logger.info("LongPolling (%s) Parent changed", pid)
- # suicide !!
- os.kill(pid, signal.SIGTERM)
- return
- gevent.sleep(beat)
-
def main(args):
check_root_user()
openerp.tools.config.parse_config(args)
-
- if openerp.tools.config.options["gevent"]:
- openerp.evented = True
- _logger.info('Using gevent mode')
- import gevent.monkey
- gevent.monkey.patch_all()
- import gevent_psycopg2
- gevent_psycopg2.monkey_patch()
-
check_postgres_user()
openerp.netsvc.init_logger()
report_configuration()
config = openerp.tools.config
- setup_signal_handlers(signal_handler)
-
if config["test_file"]:
run_test_file(config['db_name'], config['test_file'])
sys.exit(0)
@@ -265,28 +168,19 @@ def main(args):
import_translation()
sys.exit(0)
- if not config["stop_after_init"]:
- setup_pid_file()
- # Some module register themselves when they are loaded so we need the
- # services to be running before loading any registry.
- if not openerp.evented:
- if config['workers']:
- openerp.service.start_services_workers()
- else:
- openerp.service.start_services()
- else:
- config['xmlrpc_port'] = config['longpolling_port']
- import gevent
- gevent.spawn(watch_parent)
- openerp.service.start_services()
-
+ # preload registryies, needed for -u --stop_after_init
rc = 0
if config['db_name']:
for dbname in config['db_name'].split(','):
if not preload_registry(dbname):
rc += 1
- if config["stop_after_init"]:
+ if not config["stop_after_init"]:
+ setup_pid_file()
+ openerp.service.workers.start()
+ if config['pidfile']:
+ os.unlink(config['pidfile'])
+ else:
sys.exit(rc)
_logger.info('OpenERP server is running, waiting for connections...')
diff --git a/openerp/service/__init__.py b/openerp/service/__init__.py
index 2c6c80eb80c..02401d455e1 100644
--- a/openerp/service/__init__.py
+++ b/openerp/service/__init__.py
@@ -20,29 +20,12 @@
#
##############################################################################
-import logging
-import os
-import signal
-import subprocess
-import sys
-import threading
-import time
-
-import cron
-import wsgi_server
-
-import openerp
-import openerp.modules
-import openerp.netsvc
-import openerp.osv
-from openerp.release import nt_service_name
-import openerp.tools
-from openerp.tools.misc import stripped_sys_argv
-
import common
import db
import model
import report
+import wsgi_server
+import workers
#.apidoc title: RPC Services
@@ -55,100 +38,5 @@ import report
low-level behavior of the wire.
"""
-_logger = logging.getLogger(__name__)
-
-def load_server_wide_modules():
- for m in openerp.conf.server_wide_modules:
- try:
- openerp.modules.module.load_openerp_module(m)
- except Exception:
- msg = ''
- if m == 'web':
- msg = """
-The `web` module is provided by the addons found in the `openerp-web` project.
-Maybe you forgot to add those addons in your addons_path configuration."""
- _logger.exception('Failed to load server-wide module `%s`.%s', m, msg)
-
-start_internal_done = False
-main_thread_id = threading.currentThread().ident
-
-def start_internal():
- global start_internal_done
- if start_internal_done:
- return
- openerp.netsvc.init_logger()
-
- load_server_wide_modules()
- start_internal_done = True
-
-def start_services():
- """ Start all services including http, and cron """
- start_internal()
- # Start the WSGI server.
- wsgi_server.start_service()
- # Start the main cron thread.
- if not openerp.evented:
- cron.start_service()
-
-def stop_services():
- """ Stop all services. """
- # stop services
- if not openerp.evented:
- cron.stop_service()
- wsgi_server.stop_service()
-
- _logger.info("Initiating shutdown")
- _logger.info("Hit CTRL-C again or send a second signal to force the shutdown.")
-
- # Manually join() all threads before calling sys.exit() to allow a second signal
- # to trigger _force_quit() in case some non-daemon threads won't exit cleanly.
- # threading.Thread.join() should not mask signals (at least in python 2.5).
- me = threading.currentThread()
- _logger.debug('current thread: %r', me)
- for thread in threading.enumerate():
- _logger.debug('process %r (%r)', thread, thread.isDaemon())
- if thread != me and not thread.isDaemon() and thread.ident != main_thread_id:
- while thread.isAlive():
- _logger.debug('join and sleep')
- # Need a busyloop here as thread.join() masks signals
- # and would prevent the forced shutdown.
- thread.join(0.05)
- time.sleep(0.05)
-
- _logger.debug('--')
- openerp.modules.registry.RegistryManager.delete_all()
- logging.shutdown()
-
-def start_services_workers():
- import openerp.service.workers
- openerp.multi_process = True
- openerp.service.workers.Multicorn(openerp.service.wsgi_server.application).run()
-
-def _reexec():
- """reexecute openerp-server process with (nearly) the same arguments"""
- if openerp.tools.osutil.is_running_as_nt_service():
- subprocess.call('net stop {0} && net start {0}'.format(nt_service_name), shell=True)
- exe = os.path.basename(sys.executable)
- args = stripped_sys_argv()
- if not args or args[0] != exe:
- args.insert(0, exe)
- os.execv(sys.executable, args)
-
-def restart_server():
- if openerp.multi_process:
- raise NotImplementedError("Multicorn is not supported (but gunicorn was)")
- pid = openerp.wsgi.core.arbiter_pid
- os.kill(pid, signal.SIGHUP)
- else:
- if os.name == 'nt':
- def reborn():
- stop_services()
- _reexec()
-
- # run in a thread to let the current thread return response to the caller.
- threading.Thread(target=reborn).start()
- else:
- openerp.phoenix = True
- os.kill(os.getpid(), signal.SIGINT)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/service/cron.py b/openerp/service/cron.py
deleted file mode 100644
index f21b27f7fea..00000000000
--- a/openerp/service/cron.py
+++ /dev/null
@@ -1,76 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2011 OpenERP SA ()
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
-#
-##############################################################################
-
-""" Cron jobs scheduling
-
-Cron jobs are defined in the ir_cron table/model. This module deals with all
-cron jobs, for all databases of a single OpenERP server instance.
-
-"""
-
-import logging
-import threading
-import time
-from datetime import datetime
-
-import openerp
-
-_logger = logging.getLogger(__name__)
-
-SLEEP_INTERVAL = 60 # 1 min
-
-def cron_runner(number):
- while True:
- time.sleep(SLEEP_INTERVAL + number) # Steve Reich timing style
- registries = openerp.modules.registry.RegistryManager.registries
- _logger.debug('cron%d polling for jobs', number)
- for db_name, registry in registries.items():
- while True and registry.ready:
- acquired = openerp.addons.base.ir.ir_cron.ir_cron._acquire_job(db_name)
- if not acquired:
- break
-
-def start_service():
- """ Start the above runner function in a daemon thread.
-
- The thread is a typical daemon thread: it will never quit and must be
- terminated when the main process exits - with no consequence (the processing
- threads it spawns are not marked daemon).
-
- """
-
- # Force call to strptime just before starting the cron thread
- # to prevent time.strptime AttributeError within the thread.
- # See: http://bugs.python.org/issue7980
- datetime.strptime('2012-01-01', '%Y-%m-%d')
-
- for i in range(openerp.tools.config['max_cron_threads']):
- def target():
- cron_runner(i)
- t = threading.Thread(target=target, name="openerp.service.cron.cron%d" % i)
- t.setDaemon(True)
- t.start()
- _logger.debug("cron%d started!" % i)
-
-def stop_service():
- pass
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/service/workers.py b/openerp/service/workers.py
index c8b8b84bca2..382d581720f 100644
--- a/openerp/service/workers.py
+++ b/openerp/service/workers.py
@@ -1,7 +1,7 @@
#-----------------------------------------------------------
-# Multicorn, multiprocessing inspired by gunicorn
-# TODO rename class: Multicorn -> Arbiter ?
+# Threaded, Gevent and Prefork Servers
#-----------------------------------------------------------
+import datetime
import errno
import fcntl
import logging
@@ -13,10 +13,14 @@ import select
import signal
import socket
import sys
+import threading
import time
+import traceback
import subprocess
import os.path
+import wsgi_server
+
import werkzeug.serving
try:
from setproctitle import setproctitle
@@ -25,11 +29,238 @@ except ImportError:
import openerp
import openerp.tools.config as config
+from openerp.release import nt_service_name
from openerp.tools.misc import stripped_sys_argv
_logger = logging.getLogger(__name__)
-class Multicorn(object):
+SLEEP_INTERVAL = 60 # 1 min
+
+#----------------------------------------------------------
+# Common
+#----------------------------------------------------------
+
+class CommonServer(object):
+ def __init__(self, app):
+ # TODO Change the xmlrpc_* options to http_*
+ self.app = app
+ # config
+ self.interface = config['xmlrpc_interface'] or '0.0.0.0'
+ self.port = config['xmlrpc_port']
+ # runtime
+ self.pid = os.getpid()
+
+ def dumpstacks(self):
+ """ Signal handler: dump a stack trace for each existing thread."""
+ # code from http://stackoverflow.com/questions/132058/getting-stack-trace-from-a-running-python-application#answer-2569696
+ # modified for python 2.5 compatibility
+ threads_info = dict([(th.ident, {'name': th.name,
+ 'uid': getattr(th,'uid','n/a')})
+ for th in threading.enumerate()])
+ code = []
+ for threadId, stack in sys._current_frames().items():
+ thread_info = threads_info.get(threadId)
+ code.append("\n# Thread: %s (id:%s) (uid:%s)" % \
+ (thread_info and thread_info['name'] or 'n/a',
+ threadId,
+ thread_info and thread_info['uid'] or 'n/a'))
+ for filename, lineno, name, line in traceback.extract_stack(stack):
+ code.append('File: "%s", line %d, in %s' % (filename, lineno, name))
+ if line:
+ code.append(" %s" % (line.strip()))
+ _logger.info("\n".join(code))
+
+ def close_socket(self, sock):
+ """ Closes a socket instance cleanly
+ :param sock: the network socket to close
+ :type sock: socket.socket
+ """
+ try:
+ sock.shutdown(socket.SHUT_RDWR)
+ except socket.error, e:
+ # On OSX, socket shutdowns both sides if any side closes it
+ # causing an error 57 'Socket is not connected' on shutdown
+ # of the other side (or something), see
+ # http://bugs.python.org/issue4397
+ # note: stdlib fixed test, not behavior
+ if e.errno != errno.ENOTCONN or platform.system() not in ['Darwin', 'Windows']:
+ raise
+ sock.close()
+
+#----------------------------------------------------------
+# Threaded
+#----------------------------------------------------------
+
+class ThreadedServer(CommonServer):
+ def __init__(self, app):
+ super(ThreadedServer, self).__init__(app)
+ self.main_thread_id = threading.currentThread().ident
+ # Variable keeping track of the number of calls to the signal handler defined
+ # below. This variable is monitored by ``quit_on_signals()``.
+ self.quit_signals_received = 0
+
+ #self.socket = None
+ #self.queue = []
+ self.httpd = None
+
+ def signal_handler(self, sig, frame):
+ if sig in [signal.SIGINT,signal.SIGTERM]:
+ # shutdown on kill -INT or -TERM
+ self.quit_signals_received += 1
+ if self.quit_signals_received > 1:
+ # logging.shutdown was already called at this point.
+ sys.stderr.write("Forced shutdown.\n")
+ os._exit(0)
+ elif sig == signal.SIGHUP:
+ # restart on kill -HUP
+ openerp.phoenix = True
+ self.quit_signals_received += 1
+ elif sig == signal.SIGQUIT:
+ # dump stacks on kill -3
+ self.dumpstacks()
+
+ def cron_thread(self, number):
+ while True:
+ time.sleep(SLEEP_INTERVAL + number) # Steve Reich timing style
+ registries = openerp.modules.registry.RegistryManager.registries
+ _logger.debug('cron%d polling for jobs', number)
+ for db_name, registry in registries.items():
+ while True and registry.ready:
+ acquired = openerp.addons.base.ir.ir_cron.ir_cron._acquire_job(db_name)
+ if not acquired:
+ break
+
+ def cron_spawn(self):
+ """ Start the above runner function in a daemon thread.
+
+ The thread is a typical daemon thread: it will never quit and must be
+ terminated when the main process exits - with no consequence (the processing
+ threads it spawns are not marked daemon).
+
+ """
+ # Force call to strptime just before starting the cron thread
+ # to prevent time.strptime AttributeError within the thread.
+ # See: http://bugs.python.org/issue7980
+ datetime.datetime.strptime('2012-01-01', '%Y-%m-%d')
+ for i in range(openerp.tools.config['max_cron_threads']):
+ def target():
+ self.cron_thread(i)
+ t = threading.Thread(target=target, name="openerp.service.cron.cron%d" % i)
+ t.setDaemon(True)
+ t.start()
+ _logger.debug("cron%d started!" % i)
+
+ def http_thread(self):
+ self.httpd = werkzeug.serving.make_server(self.interface, self.port, self.app, threaded=True)
+ self.httpd.serve_forever()
+
+ def http_spawn(self):
+ threading.Thread(target=self.http_thread).start()
+ _logger.info('HTTP service (werkzeug) running on %s:%s', self.interface, self.port)
+
+ def start(self):
+ _logger.debug("Setting signal handlers")
+ if os.name == 'posix':
+ signal.signal(signal.SIGINT, self.signal_handler)
+ signal.signal(signal.SIGTERM, self.signal_handler)
+ signal.signal(signal.SIGCHLD, self.signal_handler)
+ signal.signal(signal.SIGHUP, self.signal_handler)
+ signal.signal(signal.SIGQUIT, self.signal_handler)
+ elif os.name == 'nt':
+ import win32api
+ win32api.SetConsoleCtrlHandler(lambda sig: signal_handler(sig, None), 1)
+ self.cron_spawn()
+ self.http_spawn()
+
+ def stop(self):
+ """ Shutdown the WSGI server. Wait for non deamon threads.
+ """
+ _logger.info("Initiating shutdown")
+ _logger.info("Hit CTRL-C again or send a second signal to force the shutdown.")
+
+ self.httpd.shutdown()
+ self.close_socket(self.httpd.socket)
+
+ # Manually join() all threads before calling sys.exit() to allow a second signal
+ # to trigger _force_quit() in case some non-daemon threads won't exit cleanly.
+ # threading.Thread.join() should not mask signals (at least in python 2.5).
+ me = threading.currentThread()
+ _logger.debug('current thread: %r', me)
+ for thread in threading.enumerate():
+ _logger.debug('process %r (%r)', thread, thread.isDaemon())
+ if thread != me and not thread.isDaemon() and thread.ident != main_thread_id:
+ while thread.isAlive():
+ _logger.debug('join and sleep')
+ # Need a busyloop here as thread.join() masks signals
+ # and would prevent the forced shutdown.
+ thread.join(0.05)
+ time.sleep(0.05)
+
+ _logger.debug('--')
+ openerp.modules.registry.RegistryManager.delete_all()
+ logging.shutdown()
+
+ def run(self):
+ """ Start the http server and the cron thread then wait for a signal.
+
+ The first SIGINT or SIGTERM signal will initiate a graceful shutdown while
+ a second one if any will force an immediate exit.
+ """
+ self.start()
+
+ # Wait for a first signal to be handled. (time.sleep will be interrupted
+ # by the signal handler.) The try/except is for the win32 case.
+ try:
+ while self.quit_signals_received == 0:
+ time.sleep(60)
+ except KeyboardInterrupt:
+ pass
+
+ self.stop()
+
+#----------------------------------------------------------
+# Gevent
+#----------------------------------------------------------
+
+class GeventServer(CommonServer):
+ def __init__(self, app):
+ super(GeventServer, self).__init__(app)
+ self.port = config['longpolling_port']
+ self.httpd = None
+
+ def watch_parent(self, beat=4):
+ import gevent
+ ppid = os.getppid()
+ while True:
+ if ppid != os.getppid():
+ pid = os.getpid()
+ _logger.info("LongPolling (%s) Parent changed", pid)
+ # suicide !!
+ os.kill(pid, signal.SIGTERM)
+ return
+ gevent.sleep(beat)
+
+ def start(self):
+ import gevent
+ from gevent.wsgi import WSGIServer
+ gevent.spawn(self.watch_parent)
+ self.httpd = WSGIServer((self.interface, self.port), self.app)
+ self.httpd.serve_forever()
+
+ def stop(self):
+ import gevent
+ self.httpd.stop()
+ gevent.shutdown()
+
+ def run(self):
+ self.start()
+ self.stop()
+
+#----------------------------------------------------------
+# Prefork
+#----------------------------------------------------------
+
+class Multicorn(CommonServer):
""" Multiprocessing inspired by (g)unicorn.
Multicorn currently uses accept(2) as dispatching method between workers
but we plan to replace it by a more intelligent dispatcher to will parse
@@ -92,10 +323,8 @@ class Multicorn(object):
sys.exit(0)
def long_polling_spawn(self):
- nargs = stripped_sys_argv('--pidfile')
- cmd = nargs[0]
- cmd = os.path.join(os.path.dirname(cmd), "openerp-long-polling")
- nargs[0] = cmd
+ nargs = stripped_sys_argv('--pidfile','--workers')
+ nargs += ['--gevent']
popen = subprocess.Popen(nargs)
self.long_polling_pid = popen.pid
@@ -122,6 +351,13 @@ class Multicorn(object):
sig = self.queue.pop(0)
if sig in [signal.SIGINT,signal.SIGTERM]:
raise KeyboardInterrupt
+ elif sig == signal.SIGHUP:
+ # restart on kill -HUP
+ openerp.phoenix = True
+ raise KeyboardInterrupt
+ elif sig == signal.SIGQUIT:
+ # dump stacks on kill -3
+ self.dumpstacks()
def process_zombie(self):
# reap dead workers
@@ -211,7 +447,6 @@ class Multicorn(object):
for pid in self.workers.keys():
self.worker_kill(pid, signal.SIGTERM)
self.socket.close()
- openerp.cli.server.quit_signals_received = 1
def run(self):
self.start()
@@ -389,7 +624,7 @@ class WorkerCron(Worker):
def sleep(self):
# Really sleep once all the databases have been processed.
if self.db_index == 0:
- interval = 60 + self.pid % 10 # chorus effect
+ interval = SLEEP_INTERVAL + self.pid % 10 # chorus effect
time.sleep(interval)
def _db_list(self):
@@ -437,12 +672,66 @@ class WorkerCron(Worker):
os.nice(10) # mommy always told me to be nice with others...
Worker.start(self)
self.multi.socket.close()
- openerp.service.start_internal()
# chorus effect: make cron workers do not all start at first database
mct = config['max_cron_threads']
p = float(self.pid % mct) / mct
self.db_index = int(len(self._db_list()) * p)
+#----------------------------------------------------------
+# start/stop public api
+#----------------------------------------------------------
+
+server = None
+
+def load_server_wide_modules():
+ for m in openerp.conf.server_wide_modules:
+ try:
+ openerp.modules.module.load_openerp_module(m)
+ except Exception:
+ msg = ''
+ if m == 'web':
+ msg = """
+The `web` module is provided by the addons found in the `openerp-web` project.
+Maybe you forgot to add those addons in your addons_path configuration."""
+ _logger.exception('Failed to load server-wide module `%s`.%s', m, msg)
+
+def _reexec():
+ """reexecute openerp-server process with (nearly) the same arguments"""
+ if openerp.tools.osutil.is_running_as_nt_service():
+ subprocess.call('net stop {0} && net start {0}'.format(nt_service_name), shell=True)
+ exe = os.path.basename(sys.executable)
+ args = stripped_sys_argv()
+ if not args or args[0] != exe:
+ args.insert(0, exe)
+ os.execv(sys.executable, args)
+
+def start():
+ """ Start the openerp http server and cron processor.
+ """
+ load_server_wide_modules()
+ if config['workers']:
+ openerp.multi_process = True
+ server = Multicorn(openerp.service.wsgi_server.application)
+ elif openerp.evented:
+ server = GeventServer(openerp.service.wsgi_server.application)
+ else:
+ server = ThreadedServer(openerp.service.wsgi_server.application)
+ server.run()
+
+ # like the legend of the phoenix, all ends with beginnings
+ if getattr(openerp, 'phoenix', False):
+ _reexec()
+ sys.exit(0)
+
+def restart_server():
+ """ Restart the server
+ """
+ if os.name == 'nt':
+ # run in a thread to let the current thread return response to the caller.
+ threading.Thread(target=_reexec).start()
+ else:
+ os.kill(server.pid, signal.SIGHUP)
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/service/wsgi_server.py b/openerp/service/wsgi_server.py
index acbfc67dd0b..002f8c248b9 100644
--- a/openerp/service/wsgi_server.py
+++ b/openerp/service/wsgi_server.py
@@ -400,8 +400,6 @@ def application_unproxied(environ, start_response):
if hasattr(threading.current_thread(), 'dbname'):
del threading.current_thread().dbname
- openerp.service.start_internal()
-
# Try all handlers until one returns some result (i.e. not None).
wsgi_handlers = [wsgi_xmlrpc_1, wsgi_xmlrpc, wsgi_xmlrpc_legacy, wsgi_webdav]
wsgi_handlers += module_handlers
@@ -422,69 +420,5 @@ def application(environ, start_response):
else:
return application_unproxied(environ, start_response)
-# The WSGI server, started by start_server(), stopped by stop_server().
-httpd = None
-
-def serve(interface, port, threaded):
- """ Serve HTTP requests via werkzeug development server.
-
- Calling this function is blocking, you might want to call it in its own
- thread.
- """
-
- global httpd
- if not openerp.evented:
- httpd = werkzeug.serving.make_server(interface, port, application, threaded=threaded)
- else:
- from gevent.wsgi import WSGIServer
- httpd = WSGIServer((interface, port), application)
- httpd.serve_forever()
-
-def start_service():
- """ Call serve() in its own thread.
-
- The WSGI server can be shutdown with stop_server() below.
- """
- # TODO Change the xmlrpc_* options to http_*
- interface = config['xmlrpc_interface'] or '0.0.0.0'
- port = config['xmlrpc_port']
- _logger.info('HTTP service (werkzeug) running on %s:%s', interface, port)
- if not openerp.evented:
- threading.Thread(target=serve, args=(interface, port, True)).start()
- else:
- serve(interface, port, True)
-
-def stop_service():
- """ Initiate the shutdown of the WSGI server.
-
- The server is supposed to have been started by start_server() above.
- """
- if httpd:
- if not openerp.evented:
- httpd.shutdown()
- close_socket(httpd.socket)
- else:
- import gevent
- httpd.stop()
- gevent.shutdown()
-
-def close_socket(sock):
- """ Closes a socket instance cleanly
-
- :param sock: the network socket to close
- :type sock: socket.socket
- """
- try:
- sock.shutdown(socket.SHUT_RDWR)
- except socket.error, e:
- # On OSX, socket shutdowns both sides if any side closes it
- # causing an error 57 'Socket is not connected' on shutdown
- # of the other side (or something), see
- # http://bugs.python.org/issue4397
- # note: stdlib fixed test, not behavior
- if e.errno != errno.ENOTCONN or platform.system() not in ['Darwin', 'Windows']:
- raise
- sock.close()
-
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
--
cgit v1.2.3
From 099fedc7a47efe82bf411a00531367a3f072138d Mon Sep 17 00:00:00 2001
From: "Sunil Sharma (OpenERP Trainee)"
Date: Mon, 9 Sep 2013 10:29:51 +0530
Subject: [IMP]:improved currency.
bzr revid: sunilsharma.sharma07@gmail.com-20130909045951-t2wjsr5xu5ajw432
---
addons/l10n_cl/l10n_cl_chart.xml | 2 +-
addons/l10n_de/account_chart_template_skr03.xml | 2 +-
addons/l10n_ec/account_chart.xml | 2 +-
addons/l10n_pl/account_chart.xml | 2 +-
addons/l10n_ro/account_chart.xml | 2 +-
addons/l10n_tr/account_chart_template.xml | 2 +-
addons/l10n_uy/account_chart_template.xml | 2 +-
addons/l10n_ve/data/account_chart.xml | 2 +-
8 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/addons/l10n_cl/l10n_cl_chart.xml b/addons/l10n_cl/l10n_cl_chart.xml
index e6d7b1d75bd..2e2339f212e 100644
--- a/addons/l10n_cl/l10n_cl_chart.xml
+++ b/addons/l10n_cl/l10n_cl_chart.xml
@@ -248,7 +248,7 @@
-
+
diff --git a/addons/l10n_de/account_chart_template_skr03.xml b/addons/l10n_de/account_chart_template_skr03.xml
index 0e2e239fd19..ea3c0cff3d7 100644
--- a/addons/l10n_de/account_chart_template_skr03.xml
+++ b/addons/l10n_de/account_chart_template_skr03.xml
@@ -12,7 +12,7 @@
-
+
diff --git a/addons/l10n_ec/account_chart.xml b/addons/l10n_ec/account_chart.xml
index 1cf12529c6c..afa847ea5a9 100644
--- a/addons/l10n_ec/account_chart.xml
+++ b/addons/l10n_ec/account_chart.xml
@@ -4203,7 +4203,7 @@
-
+
diff --git a/addons/l10n_pl/account_chart.xml b/addons/l10n_pl/account_chart.xml
index fcd1d216762..c2213c801dc 100644
--- a/addons/l10n_pl/account_chart.xml
+++ b/addons/l10n_pl/account_chart.xml
@@ -3202,7 +3202,7 @@
-
+
diff --git a/addons/l10n_ro/account_chart.xml b/addons/l10n_ro/account_chart.xml
index 8f531d1ffbf..169e4bfa206 100644
--- a/addons/l10n_ro/account_chart.xml
+++ b/addons/l10n_ro/account_chart.xml
@@ -4587,7 +4587,7 @@
-
+
diff --git a/addons/l10n_tr/account_chart_template.xml b/addons/l10n_tr/account_chart_template.xml
index 60853ff7127..a15af467932 100644
--- a/addons/l10n_tr/account_chart_template.xml
+++ b/addons/l10n_tr/account_chart_template.xml
@@ -12,7 +12,7 @@
-
+
diff --git a/addons/l10n_uy/account_chart_template.xml b/addons/l10n_uy/account_chart_template.xml
index 8433ed18c25..2e800e4c5cb 100644
--- a/addons/l10n_uy/account_chart_template.xml
+++ b/addons/l10n_uy/account_chart_template.xml
@@ -1895,7 +1895,7 @@
-
+
diff --git a/addons/l10n_ve/data/account_chart.xml b/addons/l10n_ve/data/account_chart.xml
index 7c859083aed..2dba15f1b18 100644
--- a/addons/l10n_ve/data/account_chart.xml
+++ b/addons/l10n_ve/data/account_chart.xml
@@ -3315,7 +3315,7 @@
-
+
--
cgit v1.2.3
From 093dec2ab01cd5cd8e3f1962cd707f4afdaea17f Mon Sep 17 00:00:00 2001
From: Stephane Wirtel
Date: Mon, 9 Sep 2013 15:10:24 +0200
Subject: [FIX] Add the platform lib [IMP] Log when the Long Polling Service is
running
bzr revid: stw@openerp.com-20130909131024-23an5cvt1qaa90su
---
openerp/service/workers.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/openerp/service/workers.py b/openerp/service/workers.py
index 382d581720f..73df8cc5aaf 100644
--- a/openerp/service/workers.py
+++ b/openerp/service/workers.py
@@ -18,6 +18,7 @@ import time
import traceback
import subprocess
import os.path
+import platform
import wsgi_server
@@ -245,6 +246,7 @@ class GeventServer(CommonServer):
from gevent.wsgi import WSGIServer
gevent.spawn(self.watch_parent)
self.httpd = WSGIServer((self.interface, self.port), self.app)
+ _logger.info('Evented Service (longpolling) running on %s:%s', self.interface, self.port)
self.httpd.serve_forever()
def stop(self):
--
cgit v1.2.3
From f8c0310d8ea8364b13a1a6b7c51862f1760a4ca9 Mon Sep 17 00:00:00 2001
From: Stephane Wirtel
Date: Mon, 9 Sep 2013 15:11:23 +0200
Subject: [FIX] Use psycogreen instead of gevent-psycopg2
green-psycopg2 has been deprecated by the developer, he asks to use an
alternative, in this case, the alternative is psycogreen
https://bitbucket.org/dvarrazzo/psycogreen
bzr revid: stw@openerp.com-20130909131123-u0gqhvxqwv13yqke
---
openerp/__init__.py | 4 ++--
openerpcommand/web.py | 4 ++--
setup.py | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/openerp/__init__.py b/openerp/__init__.py
index 161b06b0990..2cb3bce21d6 100644
--- a/openerp/__init__.py
+++ b/openerp/__init__.py
@@ -35,8 +35,8 @@ for i in sys.argv:
if evented:
import gevent.monkey
gevent.monkey.patch_all()
- import gevent_psycopg2
- gevent_psycopg2.monkey_patch()
+ import psycogreen.gevent
+ psycogreen.gevent.patch_psycopg()
# Make sure the OpenERP server runs in UTC. This is especially necessary
# under Windows as under Linux it seems the real import of time is
diff --git a/openerpcommand/web.py b/openerpcommand/web.py
index 5875978ad32..5d9d429244b 100644
--- a/openerpcommand/web.py
+++ b/openerpcommand/web.py
@@ -30,9 +30,9 @@ def run(args):
import gevent
import gevent.monkey
import gevent.wsgi
- import gevent_psycopg2
+ import psycogreen.gevent
gevent.monkey.patch_all()
- gevent_psycopg2.monkey_patch()
+ psycogreen.gevent.patch_psycopg()
import threading
import openerp
import openerp.cli.server
diff --git a/setup.py b/setup.py
index ff801a97933..4f4ffe6e505 100644
--- a/setup.py
+++ b/setup.py
@@ -120,7 +120,7 @@ setuptools.setup(
'feedparser',
'gdata',
'gevent',
- 'gevent-psycopg2',
+ 'psycogreen',
'Jinja2',
'lxml', # windows binary http://www.lfd.uci.edu/~gohlke/pythonlibs/
'mako',
--
cgit v1.2.3
From 2a6d0299d97251d93e1a10976096b2354f8f625b Mon Sep 17 00:00:00 2001
From: Antony Lesuisse
Date: Mon, 9 Sep 2013 22:08:25 +0200
Subject: revert to use a separate executable for gevent, add --dev option
bzr revid: al@openerp.com-20130909200825-sm5c3invmg7tt70j
---
openerp-gevent | 13 +++++++++++++
openerp/__init__.py | 10 ----------
openerp/service/workers.py | 4 +++-
openerp/tools/config.py | 4 ++--
4 files changed, 18 insertions(+), 13 deletions(-)
create mode 100755 openerp-gevent
diff --git a/openerp-gevent b/openerp-gevent
new file mode 100755
index 00000000000..e1c9fa797e1
--- /dev/null
+++ b/openerp-gevent
@@ -0,0 +1,13 @@
+#!/usr/bin/env python
+
+import gevent.monkey
+gevent.monkey.patch_all()
+import psycogreen.gevent
+psycogreen.gevent.patch_psycopg()
+
+import openerp
+
+openerp.evented = True
+
+if __name__ == "__main__":
+ openerp.cli.main()
diff --git a/openerp/__init__.py b/openerp/__init__.py
index 2cb3bce21d6..fe439838497 100644
--- a/openerp/__init__.py
+++ b/openerp/__init__.py
@@ -27,16 +27,6 @@ import sys
# Is the server running with gevent.
evented = False
-for i in sys.argv:
- if i.startswith('--gevent'):
- evented = True
- break
-
-if evented:
- import gevent.monkey
- gevent.monkey.patch_all()
- import psycogreen.gevent
- psycogreen.gevent.patch_psycopg()
# Make sure the OpenERP server runs in UTC. This is especially necessary
# under Windows as under Linux it seems the real import of time is
diff --git a/openerp/service/workers.py b/openerp/service/workers.py
index 73df8cc5aaf..50aa8f245d0 100644
--- a/openerp/service/workers.py
+++ b/openerp/service/workers.py
@@ -326,7 +326,9 @@ class Multicorn(CommonServer):
def long_polling_spawn(self):
nargs = stripped_sys_argv('--pidfile','--workers')
- nargs += ['--gevent']
+ cmd = nargs[0]
+ cmd = os.path.join(os.path.dirname(cmd), "openerp-gevent")
+ nargs[0] = cmd
popen = subprocess.Popen(nargs)
self.long_polling_pid = popen.pid
diff --git a/openerp/tools/config.py b/openerp/tools/config.py
index cc3c187c775..a399f6d133c 100644
--- a/openerp/tools/config.py
+++ b/openerp/tools/config.py
@@ -106,7 +106,7 @@ class configmanager(object):
help="specify additional addons paths (separated by commas).",
action="callback", callback=self._check_addons_path, nargs=1, type="string")
group.add_option("--load", dest="server_wide_modules", help="Comma-separated list of server-wide modules default=web")
- group.add_option("--gevent", dest="gevent", action="store_true", my_default=False, help="Activate the GEvent mode, this also desactivate the cron.")
+ group.add_option("--dev", dest="dev", action="store_true", my_default=False, help="Activate the developer mode. (code and views auto-reload).")
parser.add_option_group(group)
# XML-RPC / HTTP
@@ -399,7 +399,7 @@ class configmanager(object):
'list_db', 'xmlrpcs', 'proxy_mode',
'test_file', 'test_enable', 'test_commit', 'test_report_directory',
'osv_memory_count_limit', 'osv_memory_age_limit', 'max_cron_threads', 'unaccent',
- 'workers', 'limit_memory_hard', 'limit_memory_soft', 'limit_time_cpu', 'limit_time_real', 'limit_request', 'gevent'
+ 'workers', 'limit_memory_hard', 'limit_memory_soft', 'limit_time_cpu', 'limit_time_real', 'limit_request', 'dev'
]
for arg in keys:
--
cgit v1.2.3
From 3bc9a499d4e872176db809c80222edd5d6cd6f11 Mon Sep 17 00:00:00 2001
From: Antony Lesuisse
Date: Mon, 9 Sep 2013 22:17:06 +0200
Subject: add signals to increase/decrease number of workers
bzr revid: al@openerp.com-20130909201706-sllijtol7p6mi5nr
---
openerp/service/workers.py | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/openerp/service/workers.py b/openerp/service/workers.py
index 50aa8f245d0..0e7e15fcc39 100644
--- a/openerp/service/workers.py
+++ b/openerp/service/workers.py
@@ -362,6 +362,12 @@ class Multicorn(CommonServer):
elif sig == signal.SIGQUIT:
# dump stacks on kill -3
self.dumpstacks()
+ elif sig == signal.SIGTTIN:
+ # increase number of workers
+ self.population += 1
+ elif sig == signal.SIGTTOUT:
+ # decrease number of workers
+ self.population -= 1
def process_zombie(self):
# reap dead workers
@@ -423,10 +429,14 @@ class Multicorn(CommonServer):
# by a signal simulating a pseudo SA_RESTART. We write to a pipe in the
# signal handler to overcome this behaviour
self.pipe = self.pipe_new()
- # set signal
+ # set signal handlers
signal.signal(signal.SIGINT, self.signal_handler)
signal.signal(signal.SIGTERM, self.signal_handler)
+ signal.signal(signal.SIGHUP, self.signal_handler)
signal.signal(signal.SIGCHLD, self.signal_handler)
+ signal.signal(signal.SIGQUIT, self.signal_handler)
+ signal.signal(signal.SIGTTIN, self.signal_handler)
+ signal.signal(signal.SIGTTOUT, self.signal_handler)
# listen to socket
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
--
cgit v1.2.3
From 65c0538a8359c5836a3e42ac6c2c0e739d4d6bff Mon Sep 17 00:00:00 2001
From: Antony Lesuisse
Date: Tue, 10 Sep 2013 01:05:53 +0200
Subject: autoreload, first working version for both python and data, still wip
bzr revid: al@openerp.com-20130909230553-jn26ue5qenv0sd3p
---
openerp/service/workers.py | 117 +++++++++++++++++++++++++++++++++++++++++----
1 file changed, 107 insertions(+), 10 deletions(-)
diff --git a/openerp/service/workers.py b/openerp/service/workers.py
index 0e7e15fcc39..f2f4c60d5db 100644
--- a/openerp/service/workers.py
+++ b/openerp/service/workers.py
@@ -41,6 +41,97 @@ SLEEP_INTERVAL = 60 # 1 min
# Common
#----------------------------------------------------------
+class AutoReload(object):
+ def __init__(self):
+ self.files = {}
+ import pyinotify
+ class EventHandler(pyinotify.ProcessEvent):
+ def __init__(self, autoreload):
+ self.autoreload = autoreload
+
+ def process_IN_CREATE(self, event):
+ _logger.debug('File created: %s', event.pathname)
+ self.autoreload.files[event.pathname] = 1
+
+ def process_IN_MODIFY(self, event):
+ _logger.debug('File modified: %s', event.pathname)
+ self.autoreload.files[event.pathname] = 1
+
+ self.wm = pyinotify.WatchManager()
+ self.handler = EventHandler(self)
+ self.notifier = pyinotify.Notifier(self.wm, self.handler, timeout=0)
+ mask = pyinotify.IN_MODIFY | pyinotify.IN_CREATE # IN_MOVED_FROM, IN_MOVED_TO ?
+ for path in openerp.tools.config.options["addons_path"].split(','):
+ _logger.info('Watching addons folder %s', path)
+ self.wm.add_watch(path, mask, rec=True)
+
+ def process_data(self, touched_files):
+ # pyinotify notifier + fs modiciation tracker
+ from openerp.modules.module import load_information_from_description_file as load_manifest
+ addons_path = openerp.tools.config.options["addons_path"].split(',')
+ registries = openerp.modules.registry.RegistryManager.registries
+ keys = ['data', 'demo', 'test', 'init_xml', 'update_xml', 'demo_xml']
+ # This will only work for loaded registies, so we have to use -d
+ # al: proposed to move this code in the registry manager so it can be lazy
+ for db_name, registry in registries.items():
+ cr = registry.db.cursor()
+ try:
+ for tfile in touched_files:
+ for path in addons_path:
+ if tfile.startswith(path):
+ # find out wich addons path the file belongs to
+ # and extract it's module name
+ right = tfile[len(path) + 1:].split('/')
+ if len(right) < 2:
+ continue
+ module = right[0]
+ relname = "/".join(right[1:])
+ domain = [('name', '=', module), ('state', 'in', ['installed', 'to upgrade'])]
+ if registry.get('ir.module.module').search(cr, openerp.SUPERUSER_ID, domain):
+ manifest = load_manifest(module)
+ kind = [key for key in keys if relname in manifest[key]]
+ if kind:
+ _logger.info('Updating changed xml file: %s', tfile)
+ idref = {}
+ openerp.tools.convert_file(cr, module, relname, idref, mode='update', kind=kind[0])
+ cr.commit()
+ except Exception,e:
+ _logger.exception(e)
+ finally:
+ cr.close()
+
+ def process_python(self, files):
+ # process python changes
+ py_files = [i for i in files if i.endswith('.py')]
+ py_errors = []
+ # TODO keep python errors until they are ok
+ if py_files:
+ for i in py_files:
+ try:
+ source = open(i, 'rb').read() + '\n'
+ compile(source, i, 'exec')
+ except SyntaxError:
+ py_errors.append(i)
+ if py_errors:
+ _logger.info('autoreload: python code change detected, errors found')
+ for i in py_errors:
+ _logger.info('autoreload: SyntaxError %s',i)
+ else:
+ _logger.info('autoreload: python code updated, autoreload activated')
+ restart_server()
+
+ def check(self):
+ # Check if some files have been touched in the addons path.
+ # If true, check if the touched file belongs to an installed module
+ # in any of the database used in the registry manager.
+ while self.notifier.check_events(0):
+ self.notifier.read_events()
+ self.notifier.process_events()
+ l = self.files.keys()
+ self.files.clear()
+ self.process_data(l)
+ self.process_python(l)
+
class CommonServer(object):
def __init__(self, app):
# TODO Change the xmlrpc_* options to http_*
@@ -101,8 +192,8 @@ class ThreadedServer(CommonServer):
self.quit_signals_received = 0
#self.socket = None
- #self.queue = []
self.httpd = None
+ self.autoreload = None
def signal_handler(self, sig, frame):
if sig in [signal.SIGINT,signal.SIGTERM]:
@@ -152,7 +243,11 @@ class ThreadedServer(CommonServer):
_logger.debug("cron%d started!" % i)
def http_thread(self):
- self.httpd = werkzeug.serving.make_server(self.interface, self.port, self.app, threaded=True)
+ def app(e,s):
+ if self.autoreload:
+ self.autoreload.check()
+ return self.app(e,s)
+ self.httpd = werkzeug.serving.make_server(self.interface, self.port, app, threaded=True)
self.httpd.serve_forever()
def http_spawn(self):
@@ -172,6 +267,7 @@ class ThreadedServer(CommonServer):
win32api.SetConsoleCtrlHandler(lambda sig: signal_handler(sig, None), 1)
self.cron_spawn()
self.http_spawn()
+ self.autoreload = AutoReload()
def stop(self):
""" Shutdown the WSGI server. Wait for non deamon threads.
@@ -189,7 +285,7 @@ class ThreadedServer(CommonServer):
_logger.debug('current thread: %r', me)
for thread in threading.enumerate():
_logger.debug('process %r (%r)', thread, thread.isDaemon())
- if thread != me and not thread.isDaemon() and thread.ident != main_thread_id:
+ if thread != me and not thread.isDaemon() and thread.ident != self.main_thread_id:
while thread.isAlive():
_logger.debug('join and sleep')
# Need a busyloop here as thread.join() masks signals
@@ -262,11 +358,11 @@ class GeventServer(CommonServer):
# Prefork
#----------------------------------------------------------
-class Multicorn(CommonServer):
+class PreforkServer(CommonServer):
""" Multiprocessing inspired by (g)unicorn.
- Multicorn currently uses accept(2) as dispatching method between workers
- but we plan to replace it by a more intelligent dispatcher to will parse
- the first HTTP request line.
+ PreforkServer (aka Multicorn) currently uses accept(2) as dispatching
+ method between workers but we plan to replace it by a more intelligent
+ dispatcher to will parse the first HTTP request line.
"""
def __init__(self, app):
# config
@@ -617,12 +713,12 @@ class WorkerBaseWSGIServer(werkzeug.serving.BaseWSGIServer):
def __init__(self, app):
werkzeug.serving.BaseWSGIServer.__init__(self, "1", "1", app)
def server_bind(self):
- # we dont bind beause we use the listen socket of Multicorn#socket
+ # we dont bind beause we use the listen socket of PreforkServer#socket
# instead we close the socket
if self.socket:
self.socket.close()
def server_activate(self):
- # dont listen as we use Multicorn#socket
+ # dont listen as we use PreforkServer#socket
pass
class WorkerCron(Worker):
@@ -723,10 +819,11 @@ def _reexec():
def start():
""" Start the openerp http server and cron processor.
"""
+ global server
load_server_wide_modules()
if config['workers']:
openerp.multi_process = True
- server = Multicorn(openerp.service.wsgi_server.application)
+ server = PreforkServer(openerp.service.wsgi_server.application)
elif openerp.evented:
server = GeventServer(openerp.service.wsgi_server.application)
else:
--
cgit v1.2.3
From 082b1dc9fc91200d4eeb9a78c05930dbfac510c6 Mon Sep 17 00:00:00 2001
From: Antony Lesuisse
Date: Tue, 10 Sep 2013 01:19:46 +0200
Subject: restore the original gevent detection
bzr revid: al@openerp.com-20130909231946-aglho23enxhfwm0i
---
openerp-gevent | 2 --
openerp/__init__.py | 2 ++
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/openerp-gevent b/openerp-gevent
index e1c9fa797e1..55044976492 100755
--- a/openerp-gevent
+++ b/openerp-gevent
@@ -7,7 +7,5 @@ psycogreen.gevent.patch_psycopg()
import openerp
-openerp.evented = True
-
if __name__ == "__main__":
openerp.cli.main()
diff --git a/openerp/__init__.py b/openerp/__init__.py
index fe439838497..a06c85d0c21 100644
--- a/openerp/__init__.py
+++ b/openerp/__init__.py
@@ -27,6 +27,8 @@ import sys
# Is the server running with gevent.
evented = False
+if sys.modules.get("gevent") is not None:
+ evented = True
# Make sure the OpenERP server runs in UTC. This is especially necessary
# under Windows as under Linux it seems the real import of time is
--
cgit v1.2.3
From aca5e7606209cd3382c07f1a4aeedce3960650a4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Tue, 10 Sep 2013 12:45:00 +0200
Subject: [IMP] orm: first draft of improvign group_by on date field, allowing
to tune the grain, format and interval when grouping on a date field. Default
behavior is the old one, grouping on month.
bzr revid: tde@openerp.com-20130910104500-sfjosqkmpfxa2jxk
---
openerp/osv/orm.py | 36 ++++++++++++++++++++++++++++--------
1 file changed, 28 insertions(+), 8 deletions(-)
diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py
index 51b0c2b56b9..f081e11b9e2 100644
--- a/openerp/osv/orm.py
+++ b/openerp/osv/orm.py
@@ -2641,11 +2641,26 @@ class BaseModel(object):
fget = self.fields_get(cr, uid, fields)
flist = ''
group_count = group_by = groupby
+ group_by_params = {}
if groupby:
if fget.get(groupby):
groupby_type = fget[groupby]['type']
if groupby_type in ('date', 'datetime'):
- qualified_groupby_field = "to_char(%s,'yyyy-mm')" % qualified_groupby_field
+ if context.get('datetime_format') and isinstance(context['datetime_format'], dict) \
+ and context['datetime_format'].get(groupby) and isinstance(context['datetime_format'][groupby], dict):
+ groupby_format = context['datetime_format'][groupby].get('groupby_format', 'yyyy-mm')
+ display_format = context['datetime_format'][groupby].get('display_format', 'MMMM yyyy')
+ interval = context['datetime_format'][groupby].get('interval', 'month')
+ else:
+ groupby_format = 'yyyy-mm'
+ display_format = 'MMMM yyyy'
+ interval = 'month'
+ group_by_params = {
+ 'groupby_format': groupby_format,
+ 'display_format': display_format,
+ 'interval': interval,
+ }
+ qualified_groupby_field = "to_char(%s,%%s)" % qualified_groupby_field
flist = "%s as %s " % (qualified_groupby_field, groupby)
elif groupby_type == 'boolean':
qualified_groupby_field = "coalesce(%s,false)" % qualified_groupby_field
@@ -2672,6 +2687,8 @@ class BaseModel(object):
gb = groupby and (' GROUP BY ' + qualified_groupby_field) or ''
from_clause, where_clause, where_clause_params = query.get_sql()
+ if group_by_params and group_by_params.get('groupby_format'):
+ where_clause_params = [group_by_params['groupby_format']] + where_clause_params + [group_by_params['groupby_format']]
where_clause = where_clause and ' WHERE ' + where_clause
limit_str = limit and ' limit %d' % limit or ''
offset_str = offset and ' offset %d' % offset or ''
@@ -2708,14 +2725,17 @@ class BaseModel(object):
d['__context'] = {'group_by': groupby_list[1:]}
if groupby and groupby in fget:
if d[groupby] and fget[groupby]['type'] in ('date', 'datetime'):
- dt = datetime.datetime.strptime(alldata[d['id']][groupby][:7], '%Y-%m')
- days = calendar.monthrange(dt.year, dt.month)[1]
-
- date_value = datetime.datetime.strptime(d[groupby][:10], '%Y-%m-%d')
+ groupby_datetime = datetime.datetime.strptime(alldata[d['id']][groupby], '%Y-%m-%d')
d[groupby] = babel.dates.format_date(
- date_value, format='MMMM yyyy', locale=context.get('lang', 'en_US'))
- d['__domain'] = [(groupby, '>=', alldata[d['id']][groupby] and datetime.datetime.strptime(alldata[d['id']][groupby][:7] + '-01', '%Y-%m-%d').strftime('%Y-%m-%d') or False),\
- (groupby, '<=', alldata[d['id']][groupby] and datetime.datetime.strptime(alldata[d['id']][groupby][:7] + '-' + str(days), '%Y-%m-%d').strftime('%Y-%m-%d') or False)] + domain
+ groupby_datetime, format=group_by_params.get('display_format', 'MMMM yyyy'), locale=context.get('lang', 'en_US'))
+ if group_by_params.get('interval') == 'day':
+ domain_dt_begin = groupby_datetime.replace(hour=0, minute=0)
+ domain_dt_end = groupby_datetime.replace(hour=23, minute=59, second=59)
+ else:
+ days = calendar.monthrange(groupby_datetime.year, groupby_datetime.month)[1]
+ domain_dt_begin = groupby_datetime.replace(day=1)
+ domain_dt_end = groupby_datetime.replace(day=days)
+ d['__domain'] = [(group_by, '>=', domain_dt_begin.strftime('%Y-%m-%d')), (group_by, '<=', domain_dt_end.strftime('%Y-%m-%d'))] + domain
del alldata[d['id']][groupby]
d.update(alldata[d['id']])
del d['id']
--
cgit v1.2.3
From 316ce91243aaf6974f87ed3b0664f4522a391a5b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Tue, 10 Sep 2013 12:46:23 +0200
Subject: [IMP] mass_mailing Added delivered field, number of delivered emails
Improved kanban view of mass mailing campaign Improved statistics of
segments, now daily instead of monthly
bzr revid: tde@openerp.com-20130910104623-7ljg0q9pebbcqk7m
---
addons/mass_mailing/mass_mailing.py | 40 ++++++++++++++++++----
addons/mass_mailing/mass_mailing_demo.xml | 20 +++++------
addons/mass_mailing/mass_mailing_view.xml | 17 ++++++---
.../mass_mailing/static/src/css/mass_mailing.css | 2 +-
4 files changed, 57 insertions(+), 22 deletions(-)
diff --git a/addons/mass_mailing/mass_mailing.py b/addons/mass_mailing/mass_mailing.py
index f37f86e385a..ba56bc57d4f 100644
--- a/addons/mass_mailing/mass_mailing.py
+++ b/addons/mass_mailing/mass_mailing.py
@@ -41,6 +41,8 @@ class MassMailingCampaign(osv.Model):
'opened': len([mail for mail in campaign.mail_ids if mail.opened]),
'replied': len([mail for mail in campaign.mail_ids if mail.replied]),
'bounced': len([mail for mail in campaign.mail_ids if mail.bounced]),
+ # delivered: shouldn't be: all mails - (failed + bounced) ?
+ 'delivered': len([mail for mail in campaign.mail_ids if mail.state == 'sent' and not mail.bounced]),
}
return results
@@ -73,12 +75,18 @@ class MassMailingCampaign(osv.Model):
'mail.mail', 'mass_mailing_campaign_id',
'Sent Emails',
),
+ 'color': fields.integer('Color Index'),
# stat fields
'sent': fields.function(
_get_statistics,
string='Sent Emails',
type='integer', multi='_get_statistics'
),
+ 'delivered': fields.function(
+ _get_statistics,
+ string='Delivered',
+ type='integer', multi='_get_statistics',
+ ),
'opened': fields.function(
_get_statistics,
string='Opened',
@@ -120,16 +128,16 @@ class MassMailingSegment(osv.Model):
}
]
"""
- month_begin = date.today().replace(day=1)
+ # month_begin = date.today().replace(day=1)
+ date_begin = date.today()
section_result = [{'value': 0,
- 'tooltip': (month_begin + relativedelta.relativedelta(months=-i)).strftime('%B'),
+ 'tooltip': (date_begin + relativedelta.relativedelta(days=-i)).strftime('%d %B %Y'),
} for i in range(self._period_number - 1, -1, -1)]
group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context)
- print group_obj
for group in group_obj:
group_begin_date = datetime.strptime(group['__domain'][0][2], tools.DEFAULT_SERVER_DATE_FORMAT)
- month_delta = relativedelta.relativedelta(month_begin, group_begin_date)
- section_result[self._period_number - (month_delta.months + 1)] = {'value': group.get(value_field, 0), 'tooltip': group_begin_date.strftime('%B')}
+ month_delta = relativedelta.relativedelta(date_begin, group_begin_date)
+ section_result[self._period_number - (month_delta.days + 1)] = {'value': group.get(value_field, 0), 'tooltip': group.get(groupby_field)}
return section_result
def _get_monthly_statistics(self, cr, uid, ids, field_name, arg, context=None):
@@ -137,8 +145,20 @@ class MassMailingSegment(osv.Model):
"""
obj = self.pool.get('mail.mail')
res = dict.fromkeys(ids, False)
- month_begin = date.today().replace(day=1)
- groupby_begin = (month_begin + relativedelta.relativedelta(months=-4)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
+ date_begin = date.today()
+ context['datetime_format'] = {
+ 'opened': {
+ 'interval': 'day',
+ 'groupby_format': 'yyyy-mm-dd',
+ 'display_format': 'dd MMMM YYYY'
+ },
+ 'replied': {
+ 'interval': 'day',
+ 'groupby_format': 'yyyy-mm-dd',
+ 'display_format': 'dd MMMM YYYY'
+ },
+ }
+ groupby_begin = (date_begin + relativedelta.relativedelta(days=-4)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
for id in ids:
res[id] = dict()
domain = [('mass_mailing_segment_id', '=', id), ('opened', '>=', groupby_begin)]
@@ -153,6 +173,7 @@ class MassMailingSegment(osv.Model):
for segment in self.browse(cr, uid, ids, context=context):
results[segment.id] = {
'sent': len(segment.mail_ids),
+ 'delivered': len([mail for mail in segment.mail_ids if mail.state == 'sent' and not mail.bounced]),
'opened': len([mail for mail in segment.mail_ids if mail.opened]),
'replied': len([mail for mail in segment.mail_ids if mail.replied]),
'bounced': len([mail for mail in segment.mail_ids if mail.bounced]),
@@ -181,6 +202,11 @@ class MassMailingSegment(osv.Model):
string='Sent Emails',
type='integer', multi='_get_statistics'
),
+ 'delivered': fields.function(
+ _get_statistics,
+ string='Delivered',
+ type='integer', multi='_get_statistics',
+ ),
'opened': fields.function(
_get_statistics,
string='Opened',
diff --git a/addons/mass_mailing/mass_mailing_demo.xml b/addons/mass_mailing/mass_mailing_demo.xml
index 3e04baf7cef..9f525d61c3a 100644
--- a/addons/mass_mailing/mass_mailing_demo.xml
+++ b/addons/mass_mailing/mass_mailing_demo.xml
@@ -25,31 +25,31 @@
First Newsletter
-
+ Second Newsletter
-
+
-
-
+
+ sent
-
-
+
+ sent
-
+ sent
@@ -58,18 +58,18 @@
-
+ sent
-
+ sent
-
+ sent
diff --git a/addons/mass_mailing/mass_mailing_view.xml b/addons/mass_mailing/mass_mailing_view.xml
index 4a3c2b5db07..8d2d003d08b 100644
--- a/addons/mass_mailing/mass_mailing_view.xml
+++ b/addons/mass_mailing/mass_mailing_view.xml
@@ -23,6 +23,7 @@
+
@@ -41,6 +42,7 @@
+
@@ -66,16 +68,23 @@
-
-
+
+
+ i
+
+
+
+
-
+
-
diff --git a/addons/mass_mailing/static/src/css/mass_mailing.css b/addons/mass_mailing/static/src/css/mass_mailing.css
index 8b217d8d588..94f8e4ed070 100644
--- a/addons/mass_mailing/static/src/css/mass_mailing.css
+++ b/addons/mass_mailing/static/src/css/mass_mailing.css
@@ -1,5 +1,5 @@
.openerp .oe_kanban_view .oe_kanban_mass_mailing.oe_kanban_mass_mailing_campaign {
- width: 270px;
+ width: 540px;
}
.openerp .oe_kanban_view .oe_kanban_mass_mailing.oe_kanban_mass_mailing_segment {
--
cgit v1.2.3
From a89547aab615b7bde8e8804b8d66a1b48daa847e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Tue, 10 Sep 2013 13:01:04 +0200
Subject: [FIX] mass_mailing: forgotten brackets
bzr revid: tde@openerp.com-20130910110104-a0pr7q06k3i1lv6h
---
addons/mass_mailing/mass_mailing_demo.xml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/addons/mass_mailing/mass_mailing_demo.xml b/addons/mass_mailing/mass_mailing_demo.xml
index 9f525d61c3a..f09fb2d05da 100644
--- a/addons/mass_mailing/mass_mailing_demo.xml
+++ b/addons/mass_mailing/mass_mailing_demo.xml
@@ -25,13 +25,13 @@
First Newsletter
-
+ Second Newsletter
-
+
--
cgit v1.2.3
From 43bfef6918f7947c2733cb4f0e93cff480360040 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Tue, 10 Sep 2013 14:11:23 +0200
Subject: [FIX] mass_mailing: fixed sparkline computation for opened and
replied emails, using dates and group_by; fixed demo data
bzr revid: tde@openerp.com-20130910121123-uu4hplm91xb5aexi
---
addons/mass_mailing/mass_mailing.py | 38 +++++++++++++++----------------
addons/mass_mailing/mass_mailing_demo.xml | 4 ++--
2 files changed, 21 insertions(+), 21 deletions(-)
diff --git a/addons/mass_mailing/mass_mailing.py b/addons/mass_mailing/mass_mailing.py
index ba56bc57d4f..f116cb67c87 100644
--- a/addons/mass_mailing/mass_mailing.py
+++ b/addons/mass_mailing/mass_mailing.py
@@ -106,13 +106,15 @@ class MassMailingCampaign(osv.Model):
class MassMailingSegment(osv.Model):
- """ TODO """
+ """ MassMailingSegment models a segment for a mass mailign campaign. A segment
+ is an occurence of sending emails. """
+
_name = 'mail.mass_mailing.segment'
_description = 'Segment of a mass mailing campaign'
# number of periods for tracking mail_mail statistics
_period_number = 6
- def __get_bar_values(self, cr, uid, obj, domain, read_fields, value_field, groupby_field, context=None):
+ def __get_bar_values(self, cr, uid, id, obj, domain, read_fields, value_field, groupby_field, context=None):
""" Generic method to generate data for bar chart values using SparklineBarWidget.
This method performs obj.read_group(cr, uid, domain, read_fields, groupby_field).
@@ -128,24 +130,22 @@ class MassMailingSegment(osv.Model):
}
]
"""
- # month_begin = date.today().replace(day=1)
- date_begin = date.today()
+ date_begin = datetime.strptime(self.browse(cr, uid, id, context=context).date, tools.DEFAULT_SERVER_DATETIME_FORMAT).date()
section_result = [{'value': 0,
- 'tooltip': (date_begin + relativedelta.relativedelta(days=-i)).strftime('%d %B %Y'),
- } for i in range(self._period_number - 1, -1, -1)]
+ 'tooltip': (date_begin + relativedelta.relativedelta(days=i)).strftime('%d %B %Y'),
+ } for i in range(0, self._period_number)]
group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context)
for group in group_obj:
- group_begin_date = datetime.strptime(group['__domain'][0][2], tools.DEFAULT_SERVER_DATE_FORMAT)
- month_delta = relativedelta.relativedelta(date_begin, group_begin_date)
- section_result[self._period_number - (month_delta.days + 1)] = {'value': group.get(value_field, 0), 'tooltip': group.get(groupby_field)}
+ group_begin_date = datetime.strptime(group['__domain'][0][2], tools.DEFAULT_SERVER_DATE_FORMAT).date()
+ timedelta = relativedelta.relativedelta(group_begin_date, date_begin)
+ section_result[timedelta.days] = {'value': group.get(value_field, 0), 'tooltip': group.get(groupby_field)}
return section_result
def _get_monthly_statistics(self, cr, uid, ids, field_name, arg, context=None):
""" TODO
"""
- obj = self.pool.get('mail.mail')
- res = dict.fromkeys(ids, False)
- date_begin = date.today()
+ obj = self.pool['mail.mail']
+ res = {}
context['datetime_format'] = {
'opened': {
'interval': 'day',
@@ -158,13 +158,13 @@ class MassMailingSegment(osv.Model):
'display_format': 'dd MMMM YYYY'
},
}
- groupby_begin = (date_begin + relativedelta.relativedelta(days=-4)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
for id in ids:
- res[id] = dict()
- domain = [('mass_mailing_segment_id', '=', id), ('opened', '>=', groupby_begin)]
- res[id]['opened_monthly'] = self.__get_bar_values(cr, uid, obj, domain, ['opened'], 'opened_count', 'opened', context=context)
- domain = [('mass_mailing_segment_id', '=', id), ('replied', '>=', groupby_begin)]
- res[id]['replied_monthly'] = self.__get_bar_values(cr, uid, obj, domain, ['replied'], 'replied_count', 'replied', context=context)
+ res[id] = {}
+ date_begin = self.browse(cr, uid, id, context=context).date
+ domain = [('mass_mailing_segment_id', '=', id), ('opened', '>=', date_begin)]
+ res[id]['opened_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['opened'], 'opened_count', 'opened', context=context)
+ domain = [('mass_mailing_segment_id', '=', id), ('replied', '>=', date_begin)]
+ res[id]['replied_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['replied'], 'replied_count', 'replied', context=context)
return res
def _get_statistics(self, cr, uid, ids, name, arg, context=None):
@@ -225,7 +225,7 @@ class MassMailingSegment(osv.Model):
# monthly ratio
'opened_monthly': fields.function(
_get_monthly_statistics,
- string='Sent Emails',
+ string='Opened',
type='char', multi='_get_monthly_statistics',
),
'replied_monthly': fields.function(
diff --git a/addons/mass_mailing/mass_mailing_demo.xml b/addons/mass_mailing/mass_mailing_demo.xml
index f09fb2d05da..b2278064d36 100644
--- a/addons/mass_mailing/mass_mailing_demo.xml
+++ b/addons/mass_mailing/mass_mailing_demo.xml
@@ -25,13 +25,13 @@
First Newsletter
-
+ Second Newsletter
-
+
--
cgit v1.2.3
From c063f54691a5726a8ab90d273a70fe46bdaff844 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Tue, 10 Sep 2013 14:47:54 +0200
Subject: [IMP] mail: in mass mail mode, default value is now to send emails,
not to perform a mass post on documents.
bzr revid: tde@openerp.com-20130910124754-jssus6l43nil4qad
---
addons/email_template/tests/test_mail.py | 6 +++---
addons/mail/tests/test_mail_features.py | 2 ++
addons/mail/wizard/mail_compose_message.py | 5 +++--
addons/mail/wizard/mail_compose_message_view.xml | 24 ++++++++++++++----------
4 files changed, 22 insertions(+), 15 deletions(-)
diff --git a/addons/email_template/tests/test_mail.py b/addons/email_template/tests/test_mail.py
index adb550aa617..ccc8a49b875 100644
--- a/addons/email_template/tests/test_mail.py
+++ b/addons/email_template/tests/test_mail.py
@@ -73,7 +73,7 @@ class test_message_compose(TestMail):
# 1. Comment on pigs
compose_id = mail_compose.create(cr, uid,
- {'subject': 'Forget me subject', 'body': '
diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py
index 73fb148c0ed..127d1502028 100644
--- a/addons/mail/mail_mail.py
+++ b/addons/mail/mail_mail.py
@@ -108,21 +108,24 @@ class mail_mail(osv.Model):
def set_opened(self, cr, uid, ids, context=None):
""" Set as opened """
- for mail in self.browse(cr, uid, ids, context=context):
+ existing_ids = self.exists(cr, uid, ids, context=context)
+ for mail in self.browse(cr, uid, existing_ids, context=context):
if not mail.opened:
self.write(cr, uid, [mail.id], {'opened': fields.datetime.now()}, context=context)
return True
def set_replied(self, cr, uid, ids, context=None):
""" Set as replied """
- for mail in self.browse(cr, uid, ids, context=context):
+ existing_ids = self.exists(cr, uid, ids, context=context)
+ for mail in self.browse(cr, uid, existing_ids, context=context):
if not mail.replied:
self.write(cr, uid, [mail.id], {'replied': fields.datetime.now()}, context=context)
return True
def set_bounced(self, cr, uid, ids, context=None):
""" Set as bounced """
- for mail in self.browse(cr, uid, ids, context=context):
+ existing_ids = self.exists(cr, uid, ids, context=context)
+ for mail in self.browse(cr, uid, existing_ids, context=context):
if not mail.bounced:
self.write(cr, uid, [mail.id], {'bounced': fields.datetime.now()}, context=context)
return True
diff --git a/addons/mail/mail_mail_view.xml b/addons/mail/mail_mail_view.xml
index 3901a192225..9a2a178d61c 100644
--- a/addons/mail/mail_mail_view.xml
+++ b/addons/mail/mail_mail_view.xml
@@ -44,6 +44,7 @@
+
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
index 9a5d3e3580d..8c51e99a94c 100644
--- a/addons/mail/mail_thread.py
+++ b/addons/mail/mail_thread.py
@@ -792,6 +792,7 @@ class mail_thread(osv.AbstractModel):
bounce_match = tools.bounce_re.search(email_to)
if bounce_match:
bounced_mail_id = bounce_match.group(1)
+ self.pool['mail.mail'].set_bounced(cr, uid, [bounced_mail_id], context=context)
if self.pool['mail.mail'].exists(cr, uid, bounced_mail_id):
mail = self.pool['mail.mail'].browse(cr, uid, bounced_mail_id, context=context)
bounced_model = mail.model
@@ -1426,7 +1427,8 @@ class mail_thread(osv.AbstractModel):
# update original mail_mail if exists
if type == 'email':
mail_mail_ids = self.pool['mail.mail'].search(cr, SUPERUSER_ID, [('mail_message_id', '=', parent_id)], context=context)
- self.pool['mail.mail'].set_replied(cr, SUPERUSER_ID, mail_mail_ids, context=context)
+ if mail_mail_ids:
+ self.pool['mail.mail'].set_replied(cr, SUPERUSER_ID, mail_mail_ids, context=context)
message_ids = mail_message.search(cr, SUPERUSER_ID, [('id', '=', parent_id), ('parent_id', '!=', False)], context=context)
# avoid loops when finding ancestors
diff --git a/addons/mail/wizard/mail_compose_message_view.xml b/addons/mail/wizard/mail_compose_message_view.xml
index ff1dd96229e..0c86bae7e5f 100644
--- a/addons/mail/wizard/mail_compose_message_view.xml
+++ b/addons/mail/wizard/mail_compose_message_view.xml
@@ -14,6 +14,7 @@
+
@@ -32,9 +33,6 @@
context="{'force_email':True, 'show_email':True}"/>
\n"
+" Klik for at oprette en bogførings periode.\n"
+"
\n"
+" En bogførings periode er typisk en måned eller et kvartal. "
+"Den\n"
+" defineres normalt svarende til samme længde som en "
+"momsafregnings periode.\n"
+"