├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── __init__.py
├── examples
├── discord_bot.py
├── gradient_fill.py
├── short_url_example.py
├── short_url_example_with_function.py
├── simple_example.py
├── simple_example_with_function.py
├── using_quickchartfunction.py
└── write_file.py
├── poetry.lock
├── pyproject.toml
├── quickchart
└── __init__.py
├── scripts
└── format.sh
└── tests.py
/.gitignore:
--------------------------------------------------------------------------------
1 | venv
2 |
3 | ### Vim ###
4 | [._]*.s[a-w][a-z]
5 | [._]s[a-w][a-z]
6 | *.un~
7 | Session.vim
8 | .netrwhist
9 | *~
10 |
11 |
12 | ### OSX ###
13 | .DS_Store
14 | .AppleDouble
15 | .LSOverride
16 |
17 | # Icon must end with two \r
18 | Icon
19 |
20 |
21 | # Thumbnails
22 | ._*
23 |
24 | # Files that might appear in the root of a volume
25 | .DocumentRevisions-V100
26 | .fseventsd
27 | .Spotlight-V100
28 | .TemporaryItems
29 | .Trashes
30 | .VolumeIcon.icns
31 |
32 | # Directories potentially created on remote AFP share
33 | .AppleDB
34 | .AppleDesktop
35 | Network Trash Folder
36 | Temporary Items
37 | .apdisk
38 |
39 |
40 | ### Python ###
41 | # Byte-compiled / optimized / DLL files
42 | __pycache__/
43 | *.py[cod]
44 | *$py.class
45 |
46 | # C extensions
47 | *.so
48 |
49 | # Distribution / packaging
50 | .Python
51 | env/
52 | build/
53 | develop-eggs/
54 | dist/
55 | downloads/
56 | eggs/
57 | .eggs/
58 | lib/
59 | lib64/
60 | parts/
61 | sdist/
62 | var/
63 | *.egg-info/
64 | .installed.cfg
65 | *.egg
66 |
67 | # PyInstaller
68 | # Usually these files are written by a python script from a template
69 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
70 | *.manifest
71 | *.spec
72 |
73 | # Installer logs
74 | pip-log.txt
75 | pip-delete-this-directory.txt
76 |
77 | # Unit test / coverage reports
78 | htmlcov/
79 | .tox/
80 | .coverage
81 | .coverage.*
82 | .cache
83 | nosetests.xml
84 | coverage.xml
85 | *,cover
86 |
87 | # Translations
88 | *.mo
89 | *.pot
90 |
91 | # Django stuff:
92 | *.log
93 |
94 | # Sphinx documentation
95 | docs/_build/
96 |
97 | # PyBuilder
98 | target/
99 |
100 |
101 | ### Node ###
102 | # Logs
103 | logs
104 | *.log
105 | npm-debug.log*
106 |
107 | # Runtime data
108 | pids
109 | *.pid
110 | *.seed
111 |
112 | # Directory for instrumented libs generated by jscoverage/JSCover
113 | lib-cov
114 |
115 | # Coverage directory used by tools like istanbul
116 | coverage
117 |
118 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
119 | .grunt
120 |
121 | # node-waf configuration
122 | .lock-wscript
123 |
124 | # Compiled binary addons (http://nodejs.org/api/addons.html)
125 | build/Release
126 |
127 | # Dependency directory
128 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
129 | node_modules
130 |
131 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - 3.9
4 | cache:
5 | pip: true
6 | before_install:
7 | - pip install poetry
8 | install:
9 | - poetry install
10 | script:
11 | - poetry run python tests.py
12 |
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Ian Webster
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # quickchart-python
2 | [](https://travis-ci.com/typpo/quickchart-python)
3 | [](https://pypi.org/project/quickchart-io/)
4 | [](https://pypi.org/project/quickchart-io/)
5 |
6 | A Python client for the [quickchart.io](https://quickchart.io/) image charts web service.
7 |
8 | # Installation
9 |
10 | Use the `quickchart` library in this project, or install through [pip](https://pypi.org/project/quickchart.io/):
11 |
12 | ```
13 | pip install quickchart.io
14 | ```
15 |
16 | As of release 2.0, this package requires >= Python 3.7. If you need support for earlier versions of Python, use [version 1.0.1](https://pypi.org/project/quickchart-io/1.0.1/).
17 |
18 | # Usage
19 |
20 | This library provides a `QuickChart` class. Import and instantiate it. Then set properties on it and specify a [Chart.js](https://chartjs.org) config:
21 |
22 | ```python
23 | from quickchart import QuickChart
24 |
25 | qc = QuickChart()
26 | qc.width = 500
27 | qc.height = 300
28 | qc.config = {
29 | "type": "bar",
30 | "data": {
31 | "labels": ["Hello world", "Test"],
32 | "datasets": [{
33 | "label": "Foo",
34 | "data": [1, 2]
35 | }]
36 | }
37 | }
38 | ```
39 |
40 | Use `get_url()` on your quickchart object to get the encoded URL that renders your chart:
41 |
42 | ```python
43 | print(qc.get_url())
44 | # https://quickchart.io/chart?c=%7B%22chart%22%3A+%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%7D&w=600&h=300&bkg=%23ffffff&devicePixelRatio=2.0&f=png
45 | ```
46 |
47 | If you have a long or complicated chart, use `get_short_url()` to get a fixed-length URL using the quickchart.io web service (note that these URLs only persist for a short time unless you have a subscription):
48 |
49 | ```python
50 | print(qc.get_short_url())
51 | # https://quickchart.io/chart/render/f-a1d3e804-dfea-442c-88b0-9801b9808401
52 | ```
53 |
54 | The URLs will render an image of a chart:
55 |
56 |
57 |
58 | # Using Javascript functions in your chart
59 |
60 | Chart.js sometimes relies on Javascript functions (e.g. for formatting tick labels). There are a couple approaches:
61 |
62 | - Build chart configuration as a string instead of a Python object. See `examples/simple_example_with_function.py`.
63 | - Build chart configuration as a Python object and include a placeholder string for the Javascript function. Then, find and replace it.
64 | - Use the provided `QuickChartFunction` class. See `examples/using_quickchartfunction.py` for a full example.
65 |
66 | A short example using `QuickChartFunction`:
67 | ```py
68 | qc = QuickChart()
69 | qc.config = {
70 | "type": "bar",
71 | "data": {
72 | "labels": ["A", "B"],
73 | "datasets": [{
74 | "label": "Foo",
75 | "data": [1, 2]
76 | }]
77 | },
78 | "options": {
79 | "scales": {
80 | "yAxes": [{
81 | "ticks": {
82 | "callback": QuickChartFunction('(val) => val + "k"')
83 | }
84 | }],
85 | "xAxes": [{
86 | "ticks": {
87 | "callback": QuickChartFunction('''function(val) {
88 | return val + '???';
89 | }''')
90 | }
91 | }]
92 | }
93 | }
94 | }
95 |
96 | print(qc.get_url())
97 | ```
98 |
99 | # Customizing your chart
100 |
101 | You can set the following properties:
102 |
103 | ### config: dict or str
104 | The actual Chart.js chart configuration.
105 |
106 | If your chart configuration is JSON-compatible, it's usually easiest to pass an object ([example](https://github.com/typpo/quickchart-python/blob/master/examples/simple_example.py)). If your chart configuration contains a Javascript function, you may pass it as a string ([example](https://github.com/typpo/quickchart-python/blob/master/examples/simple_example_with_function.py)) or use `QuickChartFunction` ([example](https://github.com/typpo/quickchart-python/blob/master/examples/using_quickchartfunction.py)).
107 |
108 | ### width: int
109 | Width of the chart image in pixels. Defaults to 500
110 |
111 | ### height: int
112 | Height of the chart image in pixels. Defaults to 300
113 |
114 | ### format: str
115 | Format of the chart. Defaults to png. svg is also valid.
116 |
117 | ### background_color: str
118 | The background color of the chart. Any valid HTML color works. Defaults to #ffffff (white). Also takes rgb, rgba, and hsl values.
119 |
120 | ### device_pixel_ratio: float
121 | 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.
122 |
123 | ### version: str
124 | The version of Chart.js to use. Acceptable values are documented [here](https://quickchart.io/documentation/#parameters). Usually used to select Chart.js 3+.
125 |
126 | ### scheme: str
127 | The protocol to use. Defaults to `https`.
128 |
129 | ### host: str
130 | Override the host of the chart render server. Defaults to quickchart.io.
131 |
132 | ### key: str
133 | Set an API key that will be included with the request.
134 |
135 | ## Getting URLs
136 |
137 | There are two ways to get a URL for your chart object.
138 |
139 | ### get_url(): str
140 |
141 | Returns a URL that will display the chart image when loaded.
142 |
143 | ### get_short_url(): str
144 |
145 | Uses the quickchart.io web service to create a fixed-length chart URL that displays the chart image. Returns a URL such as `https://quickchart.io/chart/render/f-a1d3e804-dfea-442c-88b0-9801b9808401`.
146 |
147 | Note that short URLs expire after a few days for users of the free service. You can [subscribe](https://quickchart.io/pricing/) to keep them around longer.
148 |
149 | ## Other functionality
150 |
151 | ### get_bytes()
152 |
153 | Returns the bytes representing the chart image.
154 |
155 | ### to_file(path: str)
156 |
157 | Writes the chart image to a file path.
158 |
159 | ## More examples
160 |
161 | Checkout the `examples` directory to see other usage.
162 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | from quickchart import *
2 |
--------------------------------------------------------------------------------
/examples/discord_bot.py:
--------------------------------------------------------------------------------
1 | from io import BytesIO
2 |
3 | import discord
4 | from PIL import Image
5 | from discord.ext import commands
6 | from quickchart import QuickChart
7 |
8 | description = '''An example bot to showcase the use of QuickChart with discord.py module.'''
9 |
10 | intents = discord.Intents.default()
11 |
12 | bot = commands.Bot(command_prefix='!', description=description, intents=intents)
13 |
14 |
15 | @bot.event
16 | async def on_ready():
17 | print(f'Logged in as {bot.user.name}')
18 |
19 |
20 | @bot.command()
21 | async def graph(ctx):
22 | qc = QuickChart()
23 | qc.width = 600
24 | qc.height = 300
25 | qc.device_pixel_ratio = 2.0
26 | qc.config = {
27 | "type": "bar",
28 | "data": {
29 | "labels": ["Hello world", "Test"],
30 | "datasets": [{
31 | "label": "Foo",
32 | "data": [1, 2]
33 | }]
34 | }
35 | }
36 | with Image.open(BytesIO(qc.get_bytes())) as chat_sample:
37 | output_buffer = BytesIO() # By using BytesIO we don't have to save the file in our system.
38 | chat_sample.save(output_buffer, "png")
39 | output_buffer.seek(0)
40 | await ctx.send(file=discord.File(fp=output_buffer, filename="chart_sample.png")) # Change the file name accordingly.
41 |
42 |
43 | @graph.before_invoke
44 | async def before_test_invoke(ctx):
45 | await ctx.trigger_typing() # Take time to render and send graph so triggering typing to reflect bot action.
46 |
47 | bot.run('token')
48 |
--------------------------------------------------------------------------------
/examples/gradient_fill.py:
--------------------------------------------------------------------------------
1 | from quickchart import QuickChart, QuickChartFunction
2 |
3 | qc = QuickChart()
4 | qc.width = 600
5 | qc.height = 300
6 | qc.device_pixel_ratio = 2.0
7 | qc.config = {
8 | "type": "bar",
9 | "data": {
10 | "labels": ["Hello world", "Test"],
11 | "datasets": [{
12 | "label": "Foo",
13 | "data": [1, 2],
14 | "backgroundColor": QuickChartFunction("getGradientFillHelper('vertical', ['rgba(63, 100, 249, 0.2)', 'rgba(255, 255, 255, 0.2)'])"),
15 | }]
16 | }
17 | }
18 |
19 | print(qc.get_url())
20 |
--------------------------------------------------------------------------------
/examples/short_url_example.py:
--------------------------------------------------------------------------------
1 | import random
2 |
3 | from quickchart import QuickChart
4 |
5 | qc = QuickChart()
6 | qc.config = {
7 | "type": "line",
8 | "data": {
9 | "labels": list(range(0, 100)),
10 | "datasets": [{
11 | "label": "Foo",
12 | "data": random.sample(range(0, 100), 100),
13 | }]
14 | }
15 | }
16 |
17 | print(qc.get_short_url())
18 | #
19 | # Example output (note that this shortened URL is now expired and will not display a chart):
20 | #
21 | # https://quickchart.io/chart/render/f-b4bf9221-0499-4bc6-b1ae-6f7c78be9d93
22 | #
23 |
--------------------------------------------------------------------------------
/examples/short_url_example_with_function.py:
--------------------------------------------------------------------------------
1 | from quickchart import QuickChart
2 |
3 | qc = QuickChart()
4 | qc.config = '''{
5 | type: 'bar',
6 | data: {
7 | labels: ['Q1', 'Q2', 'Q3', 'Q4'],
8 | datasets: [{
9 | label: 'Users',
10 | data: [50, 60, 70, 180]
11 | }, {
12 | label: 'Revenue',
13 | data: [100, 200, 300, 400]
14 | }]
15 | },
16 | options: {
17 | scales: {
18 | yAxes: [{
19 | ticks: {
20 | callback: (val) => {
21 | return val + 'k';
22 | }
23 | }
24 | }]
25 | }
26 | }
27 | }'''
28 |
29 | print(qc.get_short_url())
30 | #
31 | # Example output (note that this shortened URL is now expired and will not display a chart):
32 | #
33 | # https://quickchart.io/chart/render/f-b4bf9221-0499-4bc6-b1ae-6f7c78be9d93
34 | #
35 |
--------------------------------------------------------------------------------
/examples/simple_example.py:
--------------------------------------------------------------------------------
1 | from quickchart import QuickChart
2 |
3 | qc = QuickChart()
4 | qc.width = 600
5 | qc.height = 300
6 | qc.device_pixel_ratio = 2.0
7 | qc.config = {
8 | "type": "bar",
9 | "data": {
10 | "labels": ["Hello world", "Test"],
11 | "datasets": [{
12 | "label": "Foo",
13 | "data": [1, 2]
14 | }]
15 | }
16 | }
17 |
18 | print(qc.get_url())
19 | # 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
20 |
--------------------------------------------------------------------------------
/examples/simple_example_with_function.py:
--------------------------------------------------------------------------------
1 | from quickchart import QuickChart
2 |
3 | qc = QuickChart()
4 | qc.width = 600
5 | qc.height = 300
6 | qc.device_pixel_ratio = 2.0
7 | qc.config = '''{
8 | type: 'bar',
9 | data: {
10 | labels: ['Q1', 'Q2', 'Q3', 'Q4'],
11 | datasets: [{
12 | label: 'Users',
13 | data: [50, 60, 70, 180]
14 | }, {
15 | label: 'Revenue',
16 | data: [100, 200, 300, 400]
17 | }]
18 | },
19 | options: {
20 | scales: {
21 | yAxes: [{
22 | ticks: {
23 | callback: (val) => {
24 | return val + 'k';
25 | }
26 | }
27 | }]
28 | }
29 | }
30 | }'''
31 |
32 | print(qc.get_url())
33 |
--------------------------------------------------------------------------------
/examples/using_quickchartfunction.py:
--------------------------------------------------------------------------------
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())
41 |
--------------------------------------------------------------------------------
/examples/write_file.py:
--------------------------------------------------------------------------------
1 | from quickchart import QuickChart
2 |
3 | qc = QuickChart()
4 | qc.width = 600
5 | qc.height = 300
6 | qc.device_pixel_ratio = 2.0
7 | qc.config = {
8 | "type": "bar",
9 | "data": {
10 | "labels": ["Hello world", "Test"],
11 | "datasets": [{
12 | "label": "Foo",
13 | "data": [1, 2]
14 | }]
15 | }
16 | }
17 |
18 | qc.to_file('/tmp/mychart.png')
19 |
20 | print('Done.')
21 |
--------------------------------------------------------------------------------
/poetry.lock:
--------------------------------------------------------------------------------
1 | [[package]]
2 | name = "autopep8"
3 | version = "1.7.0"
4 | description = "A tool that automatically formats Python code to conform to the PEP 8 style guide"
5 | category = "dev"
6 | optional = false
7 | python-versions = "*"
8 |
9 | [package.dependencies]
10 | pycodestyle = ">=2.9.1"
11 | toml = "*"
12 |
13 | [[package]]
14 | name = "certifi"
15 | version = "2022.9.24"
16 | description = "Python package for providing Mozilla's CA Bundle."
17 | category = "main"
18 | optional = false
19 | python-versions = ">=3.6"
20 |
21 | [[package]]
22 | name = "charset-normalizer"
23 | version = "2.1.1"
24 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
25 | category = "main"
26 | optional = false
27 | python-versions = ">=3.6.0"
28 |
29 | [package.extras]
30 | unicode_backport = ["unicodedata2"]
31 |
32 | [[package]]
33 | name = "idna"
34 | version = "3.4"
35 | description = "Internationalized Domain Names in Applications (IDNA)"
36 | category = "main"
37 | optional = false
38 | python-versions = ">=3.5"
39 |
40 | [[package]]
41 | name = "pycodestyle"
42 | version = "2.9.1"
43 | description = "Python style guide checker"
44 | category = "dev"
45 | optional = false
46 | python-versions = ">=3.6"
47 |
48 | [[package]]
49 | name = "requests"
50 | version = "2.28.1"
51 | description = "Python HTTP for Humans."
52 | category = "main"
53 | optional = false
54 | python-versions = ">=3.7, <4"
55 |
56 | [package.dependencies]
57 | certifi = ">=2017.4.17"
58 | charset-normalizer = ">=2,<3"
59 | idna = ">=2.5,<4"
60 | urllib3 = ">=1.21.1,<1.27"
61 |
62 | [package.extras]
63 | socks = ["PySocks (>=1.5.6,!=1.5.7)"]
64 | use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]
65 |
66 | [[package]]
67 | name = "toml"
68 | version = "0.10.2"
69 | description = "Python Library for Tom's Obvious, Minimal Language"
70 | category = "dev"
71 | optional = false
72 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
73 |
74 | [[package]]
75 | name = "urllib3"
76 | version = "1.26.12"
77 | description = "HTTP library with thread-safe connection pooling, file post, and more."
78 | category = "main"
79 | optional = false
80 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4"
81 |
82 | [package.extras]
83 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
84 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
85 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
86 |
87 | [metadata]
88 | lock-version = "1.1"
89 | python-versions = ">=3.7, <4"
90 | content-hash = "1aa6afcebedfcbf9c7ea21f4045e6b59825b1beaadd670bd71db353aac61f2f7"
91 |
92 | [metadata.files]
93 | autopep8 = [
94 | {file = "autopep8-1.7.0-py2.py3-none-any.whl", hash = "sha256:6f09e90a2be784317e84dc1add17ebfc7abe3924239957a37e5040e27d812087"},
95 | {file = "autopep8-1.7.0.tar.gz", hash = "sha256:ca9b1a83e53a7fad65d731dc7a2a2d50aa48f43850407c59f6a1a306c4201142"},
96 | ]
97 | certifi = [
98 | {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
99 | {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
100 | ]
101 | charset-normalizer = [
102 | {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"},
103 | {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"},
104 | ]
105 | idna = [
106 | {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
107 | {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
108 | ]
109 | pycodestyle = [
110 | {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"},
111 | {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"},
112 | ]
113 | requests = [
114 | {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
115 | {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
116 | ]
117 | toml = [
118 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
119 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
120 | ]
121 | urllib3 = [
122 | {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"},
123 | {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"},
124 | ]
125 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "quickchart.io"
3 | version = "2.0.0"
4 | description = "A client for quickchart.io, a service that generates static chart images"
5 | keywords = ["chart api", "chart image", "charts"]
6 | authors = ["Ian Webster "]
7 | maintainers = ["Ian Webster "]
8 | homepage = "https://quickchart.io/"
9 | readme = "README.md"
10 | repository = "https://github.com/typpo/quickchart-python"
11 | license = "MIT"
12 | packages = [
13 | { include = "quickchart" },
14 | ]
15 |
16 | [tool.poetry.dependencies]
17 | python = ">=3.7, <4"
18 | requests = "^2.28.1"
19 |
20 | [tool.poetry.dev-dependencies]
21 | autopep8 = "^1.5.5"
22 |
23 | [build-system]
24 | requires = ["poetry>=0.12"]
25 | build-backend = "poetry.masonry.api"
26 |
--------------------------------------------------------------------------------
/quickchart/__init__.py:
--------------------------------------------------------------------------------
1 | """A python client for quickchart.io, a web service that generates static
2 | charts."""
3 |
4 | import datetime
5 | import json
6 | import re
7 | try:
8 | from urllib import urlencode
9 | except:
10 | # For Python 3
11 | from urllib.parse import urlencode
12 |
13 | USER_AGENT = 'quickchart-python (2.0.0)'
14 |
15 | FUNCTION_DELIMITER_RE = re.compile('\"__BEGINFUNCTION__(.*?)__ENDFUNCTION__\"')
16 |
17 |
18 | class QuickChartFunction:
19 | def __init__(self, script):
20 | self.script = script
21 |
22 | def __repr__(self):
23 | return self.script
24 |
25 |
26 | def serialize(obj):
27 | if isinstance(obj, QuickChartFunction):
28 | return '__BEGINFUNCTION__' + obj.script + '__ENDFUNCTION__'
29 | if isinstance(obj, (datetime.date, datetime.datetime)):
30 | return obj.isoformat()
31 | return obj.__dict__
32 |
33 |
34 | def dump_json(obj):
35 | ret = json.dumps(obj, default=serialize, separators=(',', ':'))
36 | ret = FUNCTION_DELIMITER_RE.sub(
37 | lambda match: json.loads('"' + match.group(1) + '"'), ret)
38 | return ret
39 |
40 |
41 | class QuickChart:
42 | def __init__(self):
43 | self.config = None
44 | self.width = 500
45 | self.height = 300
46 | self.background_color = '#ffffff'
47 | self.device_pixel_ratio = 1.0
48 | self.format = 'png'
49 | self.version = '2.9.4'
50 | self.key = None
51 | self.scheme = 'https'
52 | self.host = 'quickchart.io'
53 |
54 | def is_valid(self):
55 | return self.config is not None
56 |
57 | def get_url_base(self):
58 | return '%s://%s' % (self.scheme, self.host)
59 |
60 | def get_url(self):
61 | if not self.is_valid():
62 | raise RuntimeError(
63 | 'You must set the `config` attribute before generating a url')
64 | params = {
65 | 'c': dump_json(self.config) if type(self.config) == dict else self.config,
66 | 'w': self.width,
67 | 'h': self.height,
68 | 'bkg': self.background_color,
69 | 'devicePixelRatio': self.device_pixel_ratio,
70 | 'f': self.format,
71 | 'v': self.version,
72 | }
73 | if self.key:
74 | params['key'] = self.key
75 | return '%s/chart?%s' % (self.get_url_base(), urlencode(params))
76 |
77 | def _post(self, url):
78 | try:
79 | import requests
80 | except:
81 | raise RuntimeError('Could not find `requests` dependency')
82 |
83 | postdata = {
84 | 'chart': dump_json(self.config) if type(self.config) == dict else self.config,
85 | 'width': self.width,
86 | 'height': self.height,
87 | 'backgroundColor': self.background_color,
88 | 'devicePixelRatio': self.device_pixel_ratio,
89 | 'format': self.format,
90 | 'version': self.version,
91 | }
92 | if self.key:
93 | postdata['key'] = self.key
94 | headers = {
95 | 'user-agent': USER_AGENT,
96 | }
97 | resp = requests.post(url, json=postdata, headers=headers)
98 | if resp.status_code != 200:
99 | err_description = resp.headers.get('x-quickchart-error')
100 | raise RuntimeError(
101 | 'Invalid response code from chart creation endpoint: %d%s'
102 | % (resp.status_code, '\n%s' % err_description if err_description else '')
103 | )
104 | return resp
105 |
106 |
107 | def get_short_url(self):
108 | resp = self._post('%s/chart/create' % self.get_url_base())
109 | parsed = json.loads(resp.text)
110 | if not parsed['success']:
111 | raise RuntimeError(
112 | 'Chart creation endpoint failed to create chart')
113 | return parsed['url']
114 |
115 | def get_bytes(self):
116 | resp = self._post('%s/chart' % self.get_url_base())
117 | return resp.content
118 |
119 | def to_file(self, path):
120 | content = self.get_bytes()
121 | with open(path, 'wb') as f:
122 | f.write(content)
123 |
--------------------------------------------------------------------------------
/scripts/format.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 |
3 | poetry run autopep8 --in-place examples/*.py quickchart/*.py
4 |
--------------------------------------------------------------------------------
/tests.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from datetime import datetime
3 |
4 | from quickchart import QuickChart, QuickChartFunction
5 |
6 | class TestQuickChart(unittest.TestCase):
7 | def test_simple(self):
8 | qc = QuickChart()
9 | qc.width = 600
10 | qc.height = 300
11 | qc.device_pixel_ratio = 2.0
12 | qc.config = {
13 | "type": "bar",
14 | "data": {
15 | "labels": ["Hello world", "Test"],
16 | "datasets": [{
17 | "label": "Foo",
18 | "data": [1, 2]
19 | }]
20 | }
21 | }
22 |
23 | url = qc.get_url()
24 | self.assertIn('w=600', url)
25 | self.assertIn('h=300', url)
26 | self.assertIn('devicePixelRatio=2', url)
27 | self.assertIn('Hello+world', url)
28 |
29 | def test_version(self):
30 | qc = QuickChart()
31 | qc.version = '3.4.0'
32 | qc.config = {
33 | "type": "bar",
34 | "data": {
35 | "labels": ["Hello world", "Test"],
36 | "datasets": [{
37 | "label": "Foo",
38 | "data": [1, 2]
39 | }]
40 | }
41 | }
42 |
43 | url = qc.get_url()
44 | self.assertIn('v=3.4.0', url)
45 |
46 | def test_no_chart(self):
47 | qc = QuickChart()
48 | qc.width = 600
49 | qc.height = 300
50 | qc.device_pixel_ratio = 2.0
51 |
52 | self.assertRaises(RuntimeError, qc.get_url)
53 |
54 | def test_get_bytes(self):
55 | qc = QuickChart()
56 | qc.width = 600
57 | qc.height = 300
58 | qc.config = {
59 | "type": "bar",
60 | "data": {
61 | "labels": ["Hello world", "Test"],
62 | "datasets": [{
63 | "label": "Foo",
64 | "data": [1, 2]
65 | }]
66 | }
67 | }
68 | self.assertTrue(len(qc.get_bytes()) > 8000)
69 |
70 | def test_with_function_and_dates(self):
71 | qc = QuickChart()
72 | qc.config = {
73 | "type": "bar",
74 | "data": {
75 | "labels": [datetime(2020, 1, 15), datetime(2021, 1, 15)],
76 | "datasets": [{
77 | "label": "Foo",
78 | "data": [1, 2]
79 | }]
80 | },
81 | "options": {
82 | "scales": {
83 | "yAxes": [{
84 | "ticks": {
85 | "callback": QuickChartFunction('(val) => val + "k"')
86 | }
87 | }],
88 | "xAxes": [{
89 | "ticks": {
90 | "callback": QuickChartFunction('(val) => "$" + val')
91 | }
92 | }]
93 | }
94 | }
95 | }
96 |
97 | url = qc.get_url()
98 | 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)
99 | self.assertIn('2020-01-15T00%3A00%3A00', url)
100 |
101 | if __name__ == '__main__':
102 | unittest.main()
103 |
--------------------------------------------------------------------------------