diff --git a/README.md b/README.md
index c916d43..54c10a7 100644
--- a/README.md
+++ b/README.md
@@ -53,7 +53,48 @@ The URLs will render an image of a chart:
-## Customizing your chart
+# Using Javascript functions in your chart
+
+Chart.js sometimes relies on Javascript functions (e.g. for formatting tick labels). There are a couple approaches:
+
+ - Build chart configuration as a string instead of a Python object. See `examples/simple_example_with_function.py`.
+ - Build chart configuration as a Python object and include a placeholder string for the Javascript function. Then, find and replace it.
+ - Use the provided `QuickChartFunction` class. See `examples/using_quickchartfunction.py` for a full example.
+
+A short example using `QuickChartFunction`:
+```py
+qc = QuickChart()
+qc.config = {
+ "type": "bar",
+ "data": {
+ "labels": ["A", "B"],
+ "datasets": [{
+ "label": "Foo",
+ "data": [1, 2]
+ }]
+ },
+ "options": {
+ "scales": {
+ "yAxes": [{
+ "ticks": {
+ "callback": QuickChartFunction('(val) => val + "k"')
+ }
+ }],
+ "xAxes": [{
+ "ticks": {
+ "callback": QuickChartFunction('''function(val) {
+ return val + '???';
+ }''')
+ }
+ }]
+ }
+ }
+}
+
+print(qc.get_url())
+```
+
+# Customizing your chart
You can set the following properties:
@@ -75,6 +116,12 @@ The background color of the chart. Any valid HTML color works. Defaults to #ffff
### device_pixel_ratio: float
The device pixel ratio of the chart. This will multiply the number of pixels by the value. This is usually used for retina displays. Defaults to 1.0.
+### host
+Override the host of the chart render server. Defaults to quickchart.io.
+
+### key
+Set an API key that will be included with the request.
+
## Getting URLs
There are two ways to get a URL for your chart object.
diff --git a/examples/using_quickchartfunction.py b/examples/using_quickchartfunction.py
new file mode 100644
index 0000000..0e3364c
--- /dev/null
+++ b/examples/using_quickchartfunction.py
@@ -0,0 +1,40 @@
+from datetime import datetime
+
+from quickchart import QuickChart, QuickChartFunction
+
+qc = QuickChart()
+qc.width = 600
+qc.height = 300
+qc.device_pixel_ratio = 2.0
+qc.config = {
+ "type": "bar",
+ "data": {
+ "labels": [datetime(2020, 1, 15), datetime(2021, 1, 15)],
+ "datasets": [{
+ "label": "Foo",
+ "data": [1, 2]
+ }]
+ },
+ "options": {
+ "scales": {
+ "yAxes": [{
+ "ticks": {
+ "callback": QuickChartFunction('(val) => val + "k"')
+ }
+ }, {
+ "ticks": {
+ "callback": QuickChartFunction('''function(val) {
+ return val + '???';
+ }''')
+ }
+ }],
+ "xAxes": [{
+ "ticks": {
+ "callback": QuickChartFunction('(val) => "$" + val')
+ }
+ }]
+ }
+ }
+}
+
+print(qc.get_url())
diff --git a/poetry.lock b/poetry.lock
index a58ce95..0dd080a 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,3 +1,15 @@
+[[package]]
+category = "dev"
+description = "A tool that automatically formats Python code to conform to the PEP 8 style guide"
+name = "autopep8"
+optional = false
+python-versions = "*"
+version = "1.5.5"
+
+[package.dependencies]
+pycodestyle = ">=2.6.0"
+toml = "*"
+
[[package]]
category = "main"
description = "Python package for providing Mozilla's CA Bundle."
@@ -22,6 +34,14 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.9"
+[[package]]
+category = "dev"
+description = "Python style guide checker"
+name = "pycodestyle"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "2.6.0"
+
[[package]]
category = "main"
description = "Python HTTP for Humans."
@@ -40,6 +60,14 @@ urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26"
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]
+[[package]]
+category = "dev"
+description = "Python Library for Tom's Obvious, Minimal Language"
+name = "toml"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+version = "0.10.2"
+
[[package]]
category = "main"
description = "HTTP library with thread-safe connection pooling, file post, and more."
@@ -53,10 +81,15 @@ secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "cer
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
[metadata]
-content-hash = "8723aa442d99899ea654df0b67023a6c95a7dfee20d67cf214b88da8c5f399e9"
+content-hash = "810271f04d060914ce66d44fdc2acdbd757749559758e7afb0220abd1cedcfa0"
+lock-version = "1.0"
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[metadata.files]
+autopep8 = [
+ {file = "autopep8-1.5.5-py2.py3-none-any.whl", hash = "sha256:9e136c472c475f4ee4978b51a88a494bfcd4e3ed17950a44a988d9e434837bea"},
+ {file = "autopep8-1.5.5.tar.gz", hash = "sha256:cae4bc0fb616408191af41d062d7ec7ef8679c7f27b068875ca3a9e2878d5443"},
+]
certifi = [
{file = "certifi-2020.4.5.1-py2.py3-none-any.whl", hash = "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304"},
{file = "certifi-2020.4.5.1.tar.gz", hash = "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"},
@@ -69,10 +102,19 @@ idna = [
{file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"},
{file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"},
]
+pycodestyle = [
+ {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"},
+ {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"},
+]
requests = [
+ {file = "requests-2.23.0-py2.7.egg", hash = "sha256:5d2d0ffbb515f39417009a46c14256291061ac01ba8f875b90cad137de83beb4"},
{file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"},
{file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"},
]
+toml = [
+ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
urllib3 = [
{file = "urllib3-1.22-py2.py3-none-any.whl", hash = "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b"},
{file = "urllib3-1.22.tar.gz", hash = "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"},
diff --git a/pyproject.toml b/pyproject.toml
index 0750fe6..b48e6e6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "quickchart.io"
-version = "0.1.5"
+version = "0.2.0"
description = "A client for quickchart.io, a service that generates static chart images"
keywords = ["chart api", "chart image", "charts"]
authors = ["Ian Webster "]
@@ -18,6 +18,7 @@ python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
requests = "^2.23.0"
[tool.poetry.dev-dependencies]
+autopep8 = "^1.5.5"
[build-system]
requires = ["poetry>=0.12"]
diff --git a/quickchart/__init__.py b/quickchart/__init__.py
index 0145fb9..80a8cef 100644
--- a/quickchart/__init__.py
+++ b/quickchart/__init__.py
@@ -1,13 +1,41 @@
"""A python client for quickchart.io, a web service that generates static
charts."""
+import datetime
import json
+import re
try:
from urllib import urlencode
except:
# For Python 3
from urllib.parse import urlencode
+FUNCTION_DELIMITER_RE = re.compile('\"__BEGINFUNCTION__(.*?)__ENDFUNCTION__\"')
+
+
+class QuickChartFunction:
+ def __init__(self, script):
+ self.script = script
+
+ def __repr__(self):
+ return self.script
+
+
+def serialize(obj):
+ if isinstance(obj, QuickChartFunction):
+ return '__BEGINFUNCTION__' + obj.script + '__ENDFUNCTION__'
+ if isinstance(obj, (datetime.date, datetime.datetime)):
+ return obj.isoformat()
+ return obj.__dict__
+
+
+def dump_json(obj):
+ ret = json.dumps(obj, default=serialize, separators=(',', ':'))
+ ret = FUNCTION_DELIMITER_RE.sub(
+ lambda match: json.loads('"' + match.group(1) + '"'), ret)
+ return ret
+
+
class QuickChart:
def __init__(self):
self.config = None
@@ -17,15 +45,21 @@ def __init__(self):
self.device_pixel_ratio = 1.0
self.format = 'png'
self.key = None
+ self.scheme = 'https'
+ self.host = 'quickchart.io'
def is_valid(self):
return self.config is not None
+ def get_url_base(self):
+ return '%s://%s' % (self.scheme, self.host)
+
def get_url(self):
if not self.is_valid():
- raise RuntimeError('You must set the `config` attribute before generating a url')
+ raise RuntimeError(
+ 'You must set the `config` attribute before generating a url')
params = {
- 'c': json.dumps(self.config) if type(self.config) == dict else self.config,
+ 'c': dump_json(self.config) if type(self.config) == dict else self.config,
'w': self.width,
'h': self.height,
'bkg': self.background_color,
@@ -34,7 +68,7 @@ def get_url(self):
}
if self.key:
params['key'] = self.key
- return 'https://quickchart.io/chart?%s' % urlencode(params)
+ return '%s/chart?%s' % (self.get_url_base(), urlencode(params))
def _post(self, url):
try:
@@ -43,7 +77,7 @@ def _post(self, url):
raise RuntimeError('Could not find `requests` dependency')
postdata = {
- 'chart': json.dumps(self.config) if type(self.config) == dict else self.config,
+ 'chart': dump_json(self.config) if type(self.config) == dict else self.config,
'width': self.width,
'height': self.height,
'backgroundColor': self.background_color,
@@ -54,22 +88,23 @@ def _post(self, url):
postdata['key'] = self.key
resp = requests.post(url, json=postdata)
if resp.status_code != 200:
- raise RuntimeError('Invalid response code from chart creation endpoint')
+ raise RuntimeError(
+ 'Invalid response code from chart creation endpoint')
return resp
def get_short_url(self):
- resp = self._post('https://quickchart.io/chart/create')
+ resp = self._post('%s/chart/create' % self.get_url_base())
parsed = json.loads(resp.text)
if not parsed['success']:
- raise RuntimeError('Failure response status from chart creation endpoint')
+ raise RuntimeError(
+ 'Failure response status from chart creation endpoint')
return parsed['url']
def get_bytes(self):
- resp = self._post('https://quickchart.io/chart')
+ resp = self._post('%s/chart' % self.get_url_base())
return resp.content
def to_file(self, path):
content = self.get_bytes()
with open(path, 'wb') as f:
f.write(content)
-
diff --git a/scripts/format.sh b/scripts/format.sh
new file mode 100755
index 0000000..539db9d
--- /dev/null
+++ b/scripts/format.sh
@@ -0,0 +1,3 @@
+#!/bin/bash -e
+
+poetry run autopep8 --in-place examples/*.py quickchart/*.py
diff --git a/tests.py b/tests.py
index 9692dcd..a3d29cf 100644
--- a/tests.py
+++ b/tests.py
@@ -1,6 +1,7 @@
import unittest
+from datetime import datetime
-from quickchart import QuickChart
+from quickchart import QuickChart, QuickChartFunction
class TestQuickChart(unittest.TestCase):
def test_simple(self):
@@ -49,5 +50,36 @@ def test_get_bytes(self):
}
self.assertTrue(len(qc.get_bytes()) > 8000)
+ def test_with_function_and_dates(self):
+ qc = QuickChart()
+ qc.config = {
+ "type": "bar",
+ "data": {
+ "labels": [datetime(2020, 1, 15), datetime(2021, 1, 15)],
+ "datasets": [{
+ "label": "Foo",
+ "data": [1, 2]
+ }]
+ },
+ "options": {
+ "scales": {
+ "yAxes": [{
+ "ticks": {
+ "callback": QuickChartFunction('(val) => val + "k"')
+ }
+ }],
+ "xAxes": [{
+ "ticks": {
+ "callback": QuickChartFunction('(val) => "$" + val')
+ }
+ }]
+ }
+ }
+ }
+
+ url = qc.get_url()
+ self.assertIn('7B%22ticks%22%3A%7B%22callback%22%3A%28val%29+%3D%3E+%22%24%22+%2B+val%7D%7D%5D%7D%7D%7D', url)
+ self.assertIn('2020-01-15T00%3A00%3A00', url)
+
if __name__ == '__main__':
unittest.main()