Skip to content

Commit 31754f4

Browse files
authored
Improve support for writing Chart.js Javascript functions (#5)
1 parent be0f897 commit 31754f4

File tree

7 files changed

+213
-13
lines changed

7 files changed

+213
-13
lines changed

README.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,48 @@ The URLs will render an image of a chart:
5353

5454
<img src="https://quickchart.io/chart?c=%7B%22type%22%3A+%22bar%22%2C+%22data%22%3A+%7B%22labels%22%3A+%5B%22Hello+world%22%2C+%22Test%22%5D%2C+%22datasets%22%3A+%5B%7B%22label%22%3A+%22Foo%22%2C+%22data%22%3A+%5B1%2C+2%5D%7D%5D%7D%7D&w=600&h=300&bkg=%23ffffff&devicePixelRatio=2.0&f=png" width="500" />
5555

56-
## Customizing your chart
56+
# Using Javascript functions in your chart
57+
58+
Chart.js sometimes relies on Javascript functions (e.g. for formatting tick labels). There are a couple approaches:
59+
60+
- Build chart configuration as a string instead of a Python object. See `examples/simple_example_with_function.py`.
61+
- Build chart configuration as a Python object and include a placeholder string for the Javascript function. Then, find and replace it.
62+
- Use the provided `QuickChartFunction` class. See `examples/using_quickchartfunction.py` for a full example.
63+
64+
A short example using `QuickChartFunction`:
65+
```py
66+
qc = QuickChart()
67+
qc.config = {
68+
"type": "bar",
69+
"data": {
70+
"labels": ["A", "B"],
71+
"datasets": [{
72+
"label": "Foo",
73+
"data": [1, 2]
74+
}]
75+
},
76+
"options": {
77+
"scales": {
78+
"yAxes": [{
79+
"ticks": {
80+
"callback": QuickChartFunction('(val) => val + "k"')
81+
}
82+
}],
83+
"xAxes": [{
84+
"ticks": {
85+
"callback": QuickChartFunction('''function(val) {
86+
return val + '???';
87+
}''')
88+
}
89+
}]
90+
}
91+
}
92+
}
93+
94+
print(qc.get_url())
95+
```
96+
97+
# Customizing your chart
5798

5899
You can set the following properties:
59100

@@ -75,6 +116,12 @@ The background color of the chart. Any valid HTML color works. Defaults to #ffff
75116
### device_pixel_ratio: float
76117
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.
77118

119+
### host
120+
Override the host of the chart render server. Defaults to quickchart.io.
121+
122+
### key
123+
Set an API key that will be included with the request.
124+
78125
## Getting URLs
79126

80127
There are two ways to get a URL for your chart object.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from datetime import datetime
2+
3+
from quickchart import QuickChart, QuickChartFunction
4+
5+
qc = QuickChart()
6+
qc.width = 600
7+
qc.height = 300
8+
qc.device_pixel_ratio = 2.0
9+
qc.config = {
10+
"type": "bar",
11+
"data": {
12+
"labels": [datetime(2020, 1, 15), datetime(2021, 1, 15)],
13+
"datasets": [{
14+
"label": "Foo",
15+
"data": [1, 2]
16+
}]
17+
},
18+
"options": {
19+
"scales": {
20+
"yAxes": [{
21+
"ticks": {
22+
"callback": QuickChartFunction('(val) => val + "k"')
23+
}
24+
}, {
25+
"ticks": {
26+
"callback": QuickChartFunction('''function(val) {
27+
return val + '???';
28+
}''')
29+
}
30+
}],
31+
"xAxes": [{
32+
"ticks": {
33+
"callback": QuickChartFunction('(val) => "$" + val')
34+
}
35+
}]
36+
}
37+
}
38+
}
39+
40+
print(qc.get_url())

poetry.lock

Lines changed: 43 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "quickchart.io"
3-
version = "0.1.5"
3+
version = "0.2.0"
44
description = "A client for quickchart.io, a service that generates static chart images"
55
keywords = ["chart api", "chart image", "charts"]
66
authors = ["Ian Webster <ianw_pypi@ianww.com>"]
@@ -18,6 +18,7 @@ python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
1818
requests = "^2.23.0"
1919

2020
[tool.poetry.dev-dependencies]
21+
autopep8 = "^1.5.5"
2122

2223
[build-system]
2324
requires = ["poetry>=0.12"]

quickchart/__init__.py

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,41 @@
11
"""A python client for quickchart.io, a web service that generates static
22
charts."""
33

4+
import datetime
45
import json
6+
import re
57
try:
68
from urllib import urlencode
79
except:
810
# For Python 3
911
from urllib.parse import urlencode
1012

13+
FUNCTION_DELIMITER_RE = re.compile('\"__BEGINFUNCTION__(.*?)__ENDFUNCTION__\"')
14+
15+
16+
class QuickChartFunction:
17+
def __init__(self, script):
18+
self.script = script
19+
20+
def __repr__(self):
21+
return self.script
22+
23+
24+
def serialize(obj):
25+
if isinstance(obj, QuickChartFunction):
26+
return '__BEGINFUNCTION__' + obj.script + '__ENDFUNCTION__'
27+
if isinstance(obj, (datetime.date, datetime.datetime)):
28+
return obj.isoformat()
29+
return obj.__dict__
30+
31+
32+
def dump_json(obj):
33+
ret = json.dumps(obj, default=serialize, separators=(',', ':'))
34+
ret = FUNCTION_DELIMITER_RE.sub(
35+
lambda match: json.loads('"' + match.group(1) + '"'), ret)
36+
return ret
37+
38+
1139
class QuickChart:
1240
def __init__(self):
1341
self.config = None
@@ -17,15 +45,21 @@ def __init__(self):
1745
self.device_pixel_ratio = 1.0
1846
self.format = 'png'
1947
self.key = None
48+
self.scheme = 'https'
49+
self.host = 'quickchart.io'
2050

2151
def is_valid(self):
2252
return self.config is not None
2353

54+
def get_url_base(self):
55+
return '%s://%s' % (self.scheme, self.host)
56+
2457
def get_url(self):
2558
if not self.is_valid():
26-
raise RuntimeError('You must set the `config` attribute before generating a url')
59+
raise RuntimeError(
60+
'You must set the `config` attribute before generating a url')
2761
params = {
28-
'c': json.dumps(self.config) if type(self.config) == dict else self.config,
62+
'c': dump_json(self.config) if type(self.config) == dict else self.config,
2963
'w': self.width,
3064
'h': self.height,
3165
'bkg': self.background_color,
@@ -34,7 +68,7 @@ def get_url(self):
3468
}
3569
if self.key:
3670
params['key'] = self.key
37-
return 'https://quickchart.io/chart?%s' % urlencode(params)
71+
return '%s/chart?%s' % (self.get_url_base(), urlencode(params))
3872

3973
def _post(self, url):
4074
try:
@@ -43,7 +77,7 @@ def _post(self, url):
4377
raise RuntimeError('Could not find `requests` dependency')
4478

4579
postdata = {
46-
'chart': json.dumps(self.config) if type(self.config) == dict else self.config,
80+
'chart': dump_json(self.config) if type(self.config) == dict else self.config,
4781
'width': self.width,
4882
'height': self.height,
4983
'backgroundColor': self.background_color,
@@ -54,22 +88,23 @@ def _post(self, url):
5488
postdata['key'] = self.key
5589
resp = requests.post(url, json=postdata)
5690
if resp.status_code != 200:
57-
raise RuntimeError('Invalid response code from chart creation endpoint')
91+
raise RuntimeError(
92+
'Invalid response code from chart creation endpoint')
5893
return resp
5994

6095
def get_short_url(self):
61-
resp = self._post('https://quickchart.io/chart/create')
96+
resp = self._post('%s/chart/create' % self.get_url_base())
6297
parsed = json.loads(resp.text)
6398
if not parsed['success']:
64-
raise RuntimeError('Failure response status from chart creation endpoint')
99+
raise RuntimeError(
100+
'Failure response status from chart creation endpoint')
65101
return parsed['url']
66102

67103
def get_bytes(self):
68-
resp = self._post('https://quickchart.io/chart')
104+
resp = self._post('%s/chart' % self.get_url_base())
69105
return resp.content
70106

71107
def to_file(self, path):
72108
content = self.get_bytes()
73109
with open(path, 'wb') as f:
74110
f.write(content)
75-

scripts/format.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash -e
2+
3+
poetry run autopep8 --in-place examples/*.py quickchart/*.py

tests.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import unittest
2+
from datetime import datetime
23

3-
from quickchart import QuickChart
4+
from quickchart import QuickChart, QuickChartFunction
45

56
class TestQuickChart(unittest.TestCase):
67
def test_simple(self):
@@ -49,5 +50,36 @@ def test_get_bytes(self):
4950
}
5051
self.assertTrue(len(qc.get_bytes()) > 8000)
5152

53+
def test_with_function_and_dates(self):
54+
qc = QuickChart()
55+
qc.config = {
56+
"type": "bar",
57+
"data": {
58+
"labels": [datetime(2020, 1, 15), datetime(2021, 1, 15)],
59+
"datasets": [{
60+
"label": "Foo",
61+
"data": [1, 2]
62+
}]
63+
},
64+
"options": {
65+
"scales": {
66+
"yAxes": [{
67+
"ticks": {
68+
"callback": QuickChartFunction('(val) => val + "k"')
69+
}
70+
}],
71+
"xAxes": [{
72+
"ticks": {
73+
"callback": QuickChartFunction('(val) => "$" + val')
74+
}
75+
}]
76+
}
77+
}
78+
}
79+
80+
url = qc.get_url()
81+
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)
82+
self.assertIn('2020-01-15T00%3A00%3A00', url)
83+
5284
if __name__ == '__main__':
5385
unittest.main()

0 commit comments

Comments
 (0)