import 1st version of code
git-svn-id: http://graphy.googlecode.com/svn/trunk@16 30582518-8026-11dd-8d1c-71c7e1663bfb
This commit is contained in:
parent
a3ae78eb24
commit
a0b623fbf6
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
11
README
11
README
|
@ -1,4 +1,13 @@
|
|||
Graphy
|
||||
|
||||
Graphy is a chart library for python. It tries to get out of the way and just
|
||||
let you work with your data.
|
||||
|
||||
http://code.google.com/p/graphy
|
||||
The examples in the examples directory assume graphy is in the PYTHONPATH,
|
||||
so you might need to invoke them like this:
|
||||
$ cd examples
|
||||
$ PYTHONPATH=.. ./traffic.py
|
||||
|
||||
For more information, see http://code.google.com/p/graphy/
|
||||
|
||||
For license info, see the LICENSE file.
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
#!/usr/bin/python2.4
|
||||
#
|
||||
# Copyright 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from graphy.backends import google_chart_api
|
||||
|
||||
# Population data from http://www.abag.ca.gov
|
||||
# Population
|
||||
# Name 2000 1960
|
||||
cities = [
|
||||
('San Jose', 894943, 204196),
|
||||
('San Francisco', 776733, 740316),
|
||||
('Oakland', 399484, 367548),
|
||||
('Fremont', 203413, 43790),
|
||||
('Sunnyvale', 131760, 59898),
|
||||
('Palo Alto', 58598, 52287),
|
||||
]
|
||||
names, pop2000, pop1960 = zip(*cities)
|
||||
|
||||
print '<html><head><title>Population Bar Chart</title></head><body>'
|
||||
print '<h1>Population of Select Bay Area Cities</h1>'
|
||||
|
||||
chart = google_chart_api.BarChart()
|
||||
chart.left.labels = names
|
||||
chart.AddBars(pop2000, label='2000', color='0000aa')
|
||||
chart.AddBars(pop1960, label='1960', color='ddddff')
|
||||
|
||||
chart.vertical = False
|
||||
xlabels = range(0, 1000001, 200000)
|
||||
chart.bottom.grid_spacing = 200000
|
||||
chart.bottom.min = min(xlabels)
|
||||
chart.bottom.max = max(xlabels)
|
||||
chart.bottom.label_positions = xlabels
|
||||
chart.bottom.labels = ['%sK' % (x/1000) for x in xlabels]
|
||||
print chart.display.Img(400, 400)
|
||||
|
||||
|
||||
|
||||
|
||||
print '<h1>You could also do this as 2 charts</h1>'
|
||||
chart = google_chart_api.BarChart(pop2000)
|
||||
chart.left.labels = names
|
||||
chart.vertical = False
|
||||
xlabels = range(0, 1000001, 200000)
|
||||
chart.bottom.grid_spacing = 200000
|
||||
chart.bottom.min = min(xlabels)
|
||||
chart.bottom.max = max(xlabels)
|
||||
chart.bottom.label_positions = xlabels
|
||||
chart.bottom.labels = ['%sK' % (x/1000) for x in xlabels]
|
||||
print '<h3>2000</h3>'
|
||||
print chart.display.Img(400, 220)
|
||||
chart.data[0].data = pop1960 # Swap in older data
|
||||
print '<h3>1960</h3>'
|
||||
print chart.display.Img(400, 220)
|
||||
print '</body></html>'
|
|
@ -0,0 +1,56 @@
|
|||
#!/usr/bin/python2.4
|
||||
#
|
||||
# Copyright 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from graphy.backends import google_chart_api
|
||||
from graphy import formatters
|
||||
from graphy import line_chart
|
||||
|
||||
# Average monthly temperature
|
||||
sunnyvale = [49, 52, 55, 58, 62, 66, 68, 68, 66, 61, 54, 48, 49]
|
||||
chicago = [25, 31, 39, 50, 60, 70, 75, 74, 66, 55, 42, 30, 25]
|
||||
|
||||
print '<html>'
|
||||
print '<head><title>Yearly temperature in Chicago and Sunnyvale</title></head>'
|
||||
print '<body>'
|
||||
print '<h2>Yearly temperature in Chicago and Sunnyvale</h2>'
|
||||
chart = google_chart_api.LineChart()
|
||||
chart.AddLine(sunnyvale)
|
||||
chart.AddLine(chicago, pattern=line_chart.LineStyle.DASHED)
|
||||
print chart.display.Img(250, 100)
|
||||
|
||||
print "<p>But that's hard to understand. We need labels:</p>"
|
||||
chart.bottom.min = 0
|
||||
chart.bottom.max = 12
|
||||
chart.bottom.labels = ['Jan', 'Apr', 'Jul', 'Sep', 'Jan']
|
||||
chart.bottom.label_positions = [0, 3, 6, 9, 12]
|
||||
|
||||
chart.left.min = 0
|
||||
chart.left.max = 80
|
||||
chart.left.labels = [10, 32, 50, 70]
|
||||
chart.left.label_positions = [10, 32, 50, 70]
|
||||
chart.data[0].label = 'Sunnyvale'
|
||||
chart.data[1].label = 'Chicago'
|
||||
chart.AddFormatter(formatters.InlineLegend)
|
||||
print chart.display.Img(250, 100)
|
||||
|
||||
print '<p>A grid would be nice, too.</p>'
|
||||
|
||||
chart.left.label_gridlines = True
|
||||
chart.bottom.label_gridlines = True
|
||||
print chart.display.Img(250, 100)
|
||||
|
||||
print '</body>'
|
||||
print '</html>'
|
|
@ -0,0 +1,42 @@
|
|||
#!/usr/bin/python2.4
|
||||
#
|
||||
# Copyright 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from graphy.backends import google_chart_api
|
||||
|
||||
elevation = {'Death Valley': -210, # Showing off negative barchart values!
|
||||
'Mountain View': 32,
|
||||
'Livermore': 400,
|
||||
'Riverside': 819,
|
||||
'Auburn': 1536,
|
||||
'South Lake Tahoe': 6264,
|
||||
}
|
||||
cities = elevation.keys()
|
||||
elevations = [elevation[city] for city in cities]
|
||||
|
||||
print '<html>'
|
||||
print '<h1>Elevation</h1>'
|
||||
|
||||
chart = google_chart_api.BarChart(elevations)
|
||||
chart.left.labels = cities
|
||||
chart.vertical = False
|
||||
xlabels = range(-1000, 7001, 1000)
|
||||
chart.bottom.min = min(xlabels)
|
||||
chart.bottom.max = max(xlabels)
|
||||
chart.bottom.label_positions = xlabels
|
||||
chart.bottom.labels = ['%sK' % (x/1000) for x in xlabels]
|
||||
chart.bottom.grid_spacing = 1000
|
||||
print chart.display.Img(400, 220)
|
||||
print '</html>'
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/python2.4
|
||||
#
|
||||
# Copyright 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import math
|
||||
from graphy.backends import google_chart_api
|
||||
from graphy import bar_chart
|
||||
|
||||
left_channel = []
|
||||
right_channel = []
|
||||
for i in xrange(0, 360, 3):
|
||||
left_channel.append(100.0 * math.sin(math.radians(i)))
|
||||
right_channel.append(100.0 * math.sin(math.radians(i + 30)))
|
||||
|
||||
chart = google_chart_api.BarChart()
|
||||
chart.AddBars(left_channel, color='0000ff')
|
||||
chart.AddBars(right_channel, color='ff8040')
|
||||
chart.display.enhanced_encoding = True
|
||||
|
||||
print '<html><head><title>Audio Signal</title></head><body>'
|
||||
|
||||
print '<h1>Separate</h1>'
|
||||
chart.stacked = False
|
||||
chart.display.style = bar_chart.BarStyle(None, 0, 1)
|
||||
print chart.display.Img(640, 120)
|
||||
|
||||
print '<h1>Joined</h1>'
|
||||
chart.stacked = True
|
||||
chart.display.style = bar_chart.BarStyle(None, 1)
|
||||
print chart.display.Img(640, 120)
|
||||
|
||||
print '</body></html>'
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/python2.4
|
||||
#
|
||||
# Copyright 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from graphy.backends import google_chart_api
|
||||
|
||||
chart = google_chart_api.PieChart(
|
||||
[1, 2, 3, 4, 5, 6, 7],
|
||||
['Red', 'Orange', 'Yellow', 'Green', 'Blue', 'Indigo', 'Violet'],
|
||||
['ff0000', 'ff9933', 'ffff00', '00ff00', '0000ff', '000066', '6600cc'])
|
||||
img = chart.display.Img(300, 150)
|
||||
print '<html><body><h1>Colors</h1>%s' % img
|
||||
|
||||
chart.display.is3d = True
|
||||
img = chart.display.Img(300, 100)
|
||||
print '<h1>3D view</h1>%s</body></html>' % img
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/python2.4
|
||||
#
|
||||
# Copyright 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from graphy.backends import google_chart_api
|
||||
|
||||
prices = [78, 102, 175, 181, 160, 195, 138, 158, 179, 183, 222, 211, 215]
|
||||
url = google_chart_api.Sparkline(prices).display.Url(40, 12)
|
||||
img = '<img style="display:inline;" src="%s">' % url
|
||||
print '<html>Stock prices went up %s this quarter.</html>' % img
|
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/python2.4
|
||||
#
|
||||
# Copyright 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from graphy.backends import google_chart_api
|
||||
|
||||
monthly_rainfall = [3.2, 3.2, 2.7, 0.9, 0.4, 0.1, 0.0, 0.0, 0.2, 0.9, 1.8, 2.3]
|
||||
months = 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split()
|
||||
|
||||
chart = google_chart_api.LineChart(monthly_rainfall)
|
||||
chart.bottom.labels = months
|
||||
img = chart.display.Img(400, 100)
|
||||
print '<html><h1>Monthly Rainfall for Sunnyvale, CA</h1>%s</html>' % img
|
|
@ -0,0 +1,35 @@
|
|||
#!/usr/bin/python2.4
|
||||
#
|
||||
# Copyright 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from graphy.backends import google_chart_api
|
||||
|
||||
print '<html>'
|
||||
print '<h2>Oh no! Traffic is dropping off, something must be wrong!</h2>'
|
||||
traffic = [578, 579, 580, 550, 545, 552]
|
||||
chart = google_chart_api.LineChart(traffic)
|
||||
print chart.display.Img(100, 50)
|
||||
|
||||
print """<p>But wait, that was automatically scaled to fill the entire
|
||||
vertical range. We should scale from zero instead:</p>"""
|
||||
|
||||
chart.left.min = 0
|
||||
chart.left.max = 600
|
||||
print chart.display.Img(100, 50)
|
||||
|
||||
print """<p>Also, maybe some labels would help out here:</p>"""
|
||||
chart.left.labels = range(0, 601, 200)
|
||||
chart.left.label_positions = chart.left.labels
|
||||
print chart.display.Img(100, 50)
|
|
@ -0,0 +1,602 @@
|
|||
#!/usr/bin/python2.4
|
||||
#
|
||||
# Copyright 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Backend which can generate charts using the Google Chart API.
|
||||
|
||||
The only thing you should be using out of here are the helper methods:
|
||||
LineChart
|
||||
PieChart
|
||||
Sparkline
|
||||
etc.
|
||||
"""
|
||||
|
||||
import string
|
||||
import urllib
|
||||
import warnings
|
||||
|
||||
from graphy import common
|
||||
from graphy import line_chart
|
||||
from graphy import bar_chart
|
||||
from graphy import pie_chart
|
||||
|
||||
|
||||
# TODO: Find a better representation
|
||||
_LONG_NAMES = dict(
|
||||
client_id='chc',
|
||||
size='chs',
|
||||
chart_type='cht',
|
||||
axis_type='chxt',
|
||||
axis_label='chxl',
|
||||
axis_position='chxp',
|
||||
axis_range='chxr',
|
||||
axis_style='chxs',
|
||||
data='chd',
|
||||
label='chl',
|
||||
y_label='chly',
|
||||
data_label='chld',
|
||||
data_series_label='chdl',
|
||||
color='chco',
|
||||
extra='chp',
|
||||
right_label='chlr',
|
||||
label_position='chlp',
|
||||
y_label_position='chlyp',
|
||||
right_label_position='chlrp',
|
||||
grid='chg',
|
||||
axis='chx',
|
||||
# This undocumented parameter specifies the length of the tick marks for an
|
||||
# axis. Negative values will extend tick marks into the main graph area.
|
||||
axis_tick_marks='chxtc',
|
||||
line_style='chls',
|
||||
marker='chm',
|
||||
fill='chf',
|
||||
bar_height='chbh',
|
||||
label_color='chlc',
|
||||
signature='sig',
|
||||
output_format='chof',
|
||||
title='chtt',
|
||||
title_style='chts',
|
||||
callback='callback',
|
||||
)
|
||||
|
||||
""" Used for parameters which involve joining multiple values."""
|
||||
_JOIN_DELIMS = dict(
|
||||
data=',',
|
||||
color=',',
|
||||
line_style='|',
|
||||
marker='|',
|
||||
axis_type=',',
|
||||
axis_range='|',
|
||||
axis_label='|',
|
||||
axis_position='|',
|
||||
axis_tick_marks='|',
|
||||
data_series_label='|',
|
||||
label='|',
|
||||
bar_height=',',
|
||||
)
|
||||
|
||||
class BaseChartEncoder(object):
|
||||
|
||||
"""Base class for encoders which turn chart objects into Google Chart URLS.
|
||||
|
||||
Object attributes:
|
||||
extra_params: Dict to add/override specific chart params. Of the
|
||||
form param:string, passed directly to the Google Chart API.
|
||||
For example, 'cht':'lti' becomes ?cht=lti in the URL.
|
||||
url_base: The prefix to use for URLs. If you want to point to a different
|
||||
server for some reason, you would override this.
|
||||
formatters: TODO: Need to explain how these work, and how they are
|
||||
different from chart formatters.
|
||||
enhanced_encoding: If True, uses enhanced encoding. If
|
||||
False, simple encoding is used.
|
||||
escape_url: If True, URL will be properly escaped. If False, characters
|
||||
like | and , will be unescapped (which makes the URL easier to
|
||||
read).
|
||||
"""
|
||||
|
||||
def __init__(self, chart):
|
||||
self.extra_params = {} # You can add specific params here.
|
||||
self.url_base = 'http://chart.apis.google.com/chart'
|
||||
self.formatters = self._GetFormatters()
|
||||
self.chart = chart
|
||||
self.enhanced_encoding = False
|
||||
self.escape_url = True # You can turn off URL escaping for debugging.
|
||||
self._width = 0 # These are set when someone calls Url()
|
||||
self._height = 0
|
||||
|
||||
def Url(self, width, height):
|
||||
"""Get the URL for our graph."""
|
||||
self._width = width
|
||||
self._height = height
|
||||
params = self._Params(self.chart)
|
||||
return _EncodeUrl(self.url_base, params, self.escape_url)
|
||||
|
||||
def Img(self, width, height):
|
||||
"""Get an image tag for our graph."""
|
||||
url = self.Url(width, height)
|
||||
tag = "<img src='%s' width=%s height=%s alt='chart'/>"
|
||||
return tag % (url, width, height)
|
||||
|
||||
def _GetType(self, chart):
|
||||
"""Return the correct chart_type param for the chart."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _GetFormatters(self):
|
||||
"""Get a list of formatter functions to use for encoding."""
|
||||
formatters = [self._GetLegendParams,
|
||||
self._GetDataSeriesParams,
|
||||
self._GetAxisParams,
|
||||
self._GetGridParams,
|
||||
self._GetType,
|
||||
self._GetExtraParams,
|
||||
self._GetSizeParams,
|
||||
]
|
||||
return formatters
|
||||
|
||||
def _Params(self, chart):
|
||||
"""Collect all the different params we need for the URL. Collecting
|
||||
all params as a dict before converting to a URL makes testing easier.
|
||||
"""
|
||||
chart = chart.GetFormattedChart()
|
||||
params = {}
|
||||
def Add(new_params):
|
||||
params.update(_ShortenParameterNames(new_params))
|
||||
|
||||
for formatter in self.formatters:
|
||||
Add(formatter(chart))
|
||||
|
||||
for key in params:
|
||||
params[key] = str(params[key])
|
||||
return params
|
||||
|
||||
def _GetSizeParams(self, chart):
|
||||
"""Get the size param."""
|
||||
return {'size': '%sx%s' % (int(self._width), int(self._height))}
|
||||
|
||||
def _GetExtraParams(self, chart):
|
||||
"""Get any extra params (from extra_params)."""
|
||||
return self.extra_params
|
||||
|
||||
def _GetDataSeriesParams(self, chart):
|
||||
"""Collect params related to the data series."""
|
||||
y_min, y_max = chart.GetDependentAxis().min, chart.GetDependentAxis().max
|
||||
series_data = []
|
||||
colors = []
|
||||
styles = []
|
||||
markers = []
|
||||
for i, series in enumerate(chart.data):
|
||||
data = series.data
|
||||
if not data: # Drop empty series.
|
||||
continue
|
||||
series_data.append(data)
|
||||
colors.append(series.color)
|
||||
style = series.style
|
||||
if style:
|
||||
styles.append('%s,%s,%s' % (style.width, style.on, style.off))
|
||||
else:
|
||||
# If one style is missing, they must all be missing
|
||||
# TODO: Add a test for this; throw a more meaningful exception
|
||||
assert (not styles)
|
||||
|
||||
for x, marker in series.markers:
|
||||
args = [marker.shape, marker.color, i, x, marker.size]
|
||||
markers.append(','.join(str(arg) for arg in args))
|
||||
|
||||
encoder = self._GetDataEncoder(chart)
|
||||
result = _EncodeData(chart, series_data, y_min, y_max, encoder)
|
||||
result.update(_JoinLists(color = colors,
|
||||
line_style = styles,
|
||||
marker = markers))
|
||||
return result
|
||||
|
||||
def _GetDataEncoder(self, chart):
|
||||
"""Get a class which can encode the data the way the user requested."""
|
||||
if not self.enhanced_encoding:
|
||||
return _SimpleDataEncoder()
|
||||
return _EnhancedDataEncoder()
|
||||
|
||||
def _GetLegendParams(self, chart):
|
||||
"""Get params for showing a legend."""
|
||||
if chart._show_legend:
|
||||
return _JoinLists(data_series_label = chart._legend_labels)
|
||||
return {}
|
||||
|
||||
def _GetAxisLabelsAndPositions(self, axis, chart):
|
||||
"""Return axis.labels & axis.label_positions."""
|
||||
return axis.labels, axis.label_positions
|
||||
|
||||
def _GetAxisParams(self, chart):
|
||||
"""Collect params related to our various axes (x, y, right-hand)."""
|
||||
axis_types = []
|
||||
axis_ranges = []
|
||||
axis_labels = []
|
||||
axis_label_positions = []
|
||||
axis_label_gridlines = []
|
||||
mark_length = max(self._width, self._height)
|
||||
for i, axis_pair in enumerate(a for a in chart._GetAxes() if a[1].labels):
|
||||
axis_type_code, axis = axis_pair
|
||||
axis_types.append(axis_type_code)
|
||||
if axis.min is not None or axis.max is not None:
|
||||
assert axis.min is not None # Sanity check: both min & max must be set.
|
||||
assert axis.max is not None
|
||||
axis_ranges.append('%s,%s,%s' % (i, axis.min, axis.max))
|
||||
|
||||
labels, positions = self._GetAxisLabelsAndPositions(axis, chart)
|
||||
if labels:
|
||||
axis_labels.append('%s:' % i)
|
||||
axis_labels.extend(labels)
|
||||
if positions:
|
||||
positions = [i] + list(positions)
|
||||
axis_label_positions.append(','.join(str(x) for x in positions))
|
||||
if axis.label_gridlines:
|
||||
axis_label_gridlines.append("%d,%d" % (i, -mark_length))
|
||||
|
||||
return _JoinLists(axis_type = axis_types,
|
||||
axis_range = axis_ranges,
|
||||
axis_label = axis_labels,
|
||||
axis_position = axis_label_positions,
|
||||
axis_tick_marks = axis_label_gridlines,
|
||||
)
|
||||
|
||||
def _GetGridParams(self, chart):
|
||||
"""Collect params related to grid lines."""
|
||||
x = 0
|
||||
y = 0
|
||||
if chart.bottom.grid_spacing:
|
||||
# min/max must be set for this to make sense.
|
||||
assert(chart.bottom.min is not None)
|
||||
assert(chart.bottom.max is not None)
|
||||
total = float(chart.bottom.max - chart.bottom.min)
|
||||
x = 100 * chart.bottom.grid_spacing / total
|
||||
if chart.left.grid_spacing:
|
||||
# min/max must be set for this to make sense.
|
||||
assert(chart.left.min is not None)
|
||||
assert(chart.left.max is not None)
|
||||
total = float(chart.left.max - chart.left.min)
|
||||
y = 100 * chart.left.grid_spacing / total
|
||||
if x or y:
|
||||
return dict(grid = '%.3g,%.3g,1,0' % (x, y))
|
||||
return {}
|
||||
|
||||
|
||||
class LineChartEncoder(BaseChartEncoder):
|
||||
|
||||
"""Helper class to encode LineChart objects into Google Chart URLs."""
|
||||
|
||||
def _GetType(self, chart):
|
||||
return {'chart_type': 'lc'}
|
||||
|
||||
|
||||
class SparklineEncoder(BaseChartEncoder):
|
||||
|
||||
"""Helper class to encode Sparkline objects into Google Chart URLs."""
|
||||
|
||||
def _GetType(self, chart):
|
||||
return {'chart_type': 'lfi'}
|
||||
|
||||
|
||||
class BarChartEncoder(BaseChartEncoder):
|
||||
|
||||
"""Helper class to encode BarChart objects into Google Chart URLs.
|
||||
|
||||
Object attributes:
|
||||
style: The BarStyle for all bars on this chart.
|
||||
"""
|
||||
|
||||
def __init__(self, chart, style=None):
|
||||
"""Construct a new BarChartEncoder.
|
||||
|
||||
Args:
|
||||
style: The BarStyle for all bars on this chart, if any.
|
||||
"""
|
||||
super(BarChartEncoder, self).__init__(chart)
|
||||
self.style = style
|
||||
|
||||
def _GetType(self, chart):
|
||||
# Vertical Stacked Type
|
||||
types = {(True, False): 'bvg',
|
||||
(True, True): 'bvs',
|
||||
(False, False): 'bhg',
|
||||
(False, True): 'bhs'}
|
||||
return {'chart_type': types[(chart.vertical, chart.stacked)]}
|
||||
|
||||
def _GetAxisLabelsAndPositions(self, axis, chart):
|
||||
"""Reverse labels on the y-axis in horizontal bar charts.
|
||||
(Otherwise the labels come out backwards from what you would expect)
|
||||
"""
|
||||
if not chart.vertical and axis == chart.left:
|
||||
# The left axis of horizontal bar charts needs to have reversed labels
|
||||
return reversed(axis.labels), reversed(axis.label_positions)
|
||||
return axis.labels, axis.label_positions
|
||||
|
||||
def _GetFormatters(self):
|
||||
out = super(BarChartEncoder, self)._GetFormatters()
|
||||
out.append(self._ZeroPoint)
|
||||
out.append(self._ApplyBarStyle)
|
||||
return out
|
||||
|
||||
def _ZeroPoint(self, chart):
|
||||
"""Get the zero-point if any bars are negative."""
|
||||
# (Maybe) set the zero point.
|
||||
min, max = chart.GetDependentAxis().min, chart.GetDependentAxis().max
|
||||
out = {}
|
||||
if min < 0:
|
||||
if max < 0:
|
||||
out['chp'] = 1
|
||||
else:
|
||||
out['chp'] = -min/float(max - min)
|
||||
return out
|
||||
|
||||
def _ApplyBarStyle(self, chart):
|
||||
"""If bar style is specified, fill in the missing data and apply it."""
|
||||
# sanity checks
|
||||
if self.style is None or not chart.data:
|
||||
return {}
|
||||
if self.style.bar_thickness is None and \
|
||||
self.style.bar_gap is None and \
|
||||
self.style.group_gap is None:
|
||||
return {}
|
||||
# fill in missing values
|
||||
bar_gap = self.style.bar_gap
|
||||
group_gap = self.style.group_gap
|
||||
bar_thickness = self.style.bar_thickness
|
||||
if bar_gap is None and group_gap is not None:
|
||||
bar_gap = max(0, group_gap // 2)
|
||||
if group_gap is None and bar_gap is not None:
|
||||
group_gap = int(bar_gap * 2)
|
||||
if bar_thickness is None:
|
||||
if chart.vertical:
|
||||
space = self._width
|
||||
else:
|
||||
space = self._height
|
||||
assert(space is not None)
|
||||
if chart.stacked:
|
||||
num_bars = max(len(series.data) for series in chart.data)
|
||||
bar_thickness = (space - bar_gap * (num_bars - 1)) // num_bars
|
||||
else:
|
||||
num_bars = sum(len(series.data) for series in chart.data)
|
||||
num_groups = len(chart.data)
|
||||
space_left = (space - bar_gap * (num_bars - num_groups) -
|
||||
group_gap * (num_groups - 1))
|
||||
bar_thickness = space_left // num_bars
|
||||
bar_thickness = max(1, bar_thickness)
|
||||
# format the values
|
||||
spec = [bar_thickness]
|
||||
if bar_gap is not None:
|
||||
spec.append(bar_gap)
|
||||
if group_gap is not None and not chart.stacked:
|
||||
spec.append(group_gap)
|
||||
return _JoinLists(bar_height = spec)
|
||||
|
||||
|
||||
class PieChartEncoder(BaseChartEncoder):
|
||||
"""Helper class for encoding PieChart objects into Google Chart URLs.
|
||||
|
||||
Object Attributes:
|
||||
is3d: if True, draw a 3d pie chart. Default is False.
|
||||
"""
|
||||
|
||||
def __init__(self, chart, is3d=False):
|
||||
"""Construct a new PieChartEncoder.
|
||||
|
||||
Args:
|
||||
is3d: if True, draw a 3d pie chart. Default is False.
|
||||
"""
|
||||
super(PieChartEncoder, self).__init__(chart)
|
||||
self.is3d = is3d
|
||||
|
||||
def _GetType(self, chart):
|
||||
if self.is3d:
|
||||
return {'chart_type': 'p3'}
|
||||
else:
|
||||
return {'chart_type': 'p'}
|
||||
|
||||
def _GetDataSeriesParams(self, chart):
|
||||
"""Collect params related to the data series."""
|
||||
points = []
|
||||
labels = []
|
||||
colors = []
|
||||
for segment in chart.data:
|
||||
if segment:
|
||||
points.append(segment.size)
|
||||
labels.append(segment.label or '_')
|
||||
if segment.color:
|
||||
colors.append(segment.color)
|
||||
|
||||
if points:
|
||||
max_val = max(points)
|
||||
else:
|
||||
max_val = 1
|
||||
encoder = self._GetDataEncoder(chart)
|
||||
result = _EncodeData(chart, [points], 0, max_val, encoder)
|
||||
result.update(_JoinLists(color=colors, label=labels))
|
||||
return result
|
||||
|
||||
|
||||
class _SimpleDataEncoder:
|
||||
|
||||
"""Encode data using simple encoding. Out-of-range data will
|
||||
be dropped (encoded as '_').
|
||||
"""
|
||||
|
||||
# TODO: merge this with the cs_client implementation.
|
||||
def __init__(self):
|
||||
self.prefix = 's:'
|
||||
self.code = string.ascii_uppercase + string.ascii_lowercase + string.digits
|
||||
self.min = 0
|
||||
self.max = len(self.code) - 1
|
||||
|
||||
def Encode(self, data):
|
||||
return ''.join(self._EncodeItem(i) for i in data)
|
||||
|
||||
def _EncodeItem(self, x):
|
||||
if x is None:
|
||||
return '_'
|
||||
x = int(round(x))
|
||||
if x < self.min or x > self.max:
|
||||
return '_'
|
||||
return self.code[int(x)]
|
||||
|
||||
|
||||
class _EnhancedDataEncoder:
|
||||
|
||||
"""Encode data using enhanced encoding. Out-of-range data will
|
||||
be dropped (encoded as '_').
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.prefix = 'e:'
|
||||
chars = string.ascii_uppercase + string.ascii_lowercase + string.digits \
|
||||
+ '-.'
|
||||
self.code = [x + y for x in chars for y in chars]
|
||||
self.min = 0
|
||||
self.max = len(self.code) - 1
|
||||
|
||||
def Encode(self, data):
|
||||
return ''.join(self._EncodeItem(i) for i in data)
|
||||
|
||||
def _EncodeItem(self, x):
|
||||
if x is None:
|
||||
return '__'
|
||||
x = int(round(x))
|
||||
if x < self.min or x > self.max:
|
||||
return '__'
|
||||
return self.code[int(x)]
|
||||
|
||||
|
||||
def _EncodeUrl(base, params, escape_url):
|
||||
"""Escape params, combine and append them to base to generate a full URL."""
|
||||
real_params = []
|
||||
for key, value in params.iteritems():
|
||||
if escape_url:
|
||||
value = urllib.quote(value)
|
||||
if value:
|
||||
real_params.append('%s=%s' % (key, value))
|
||||
if real_params:
|
||||
return '%s?%s' % (base, '&'.join(real_params))
|
||||
else:
|
||||
return base
|
||||
|
||||
|
||||
def _ShortenParameterNames(params):
|
||||
"""Shorten long parameter names (like size) to short names (like chs)."""
|
||||
out = {}
|
||||
for name, value in params.iteritems():
|
||||
short_name = _LONG_NAMES.get(name, name)
|
||||
if short_name in out:
|
||||
# params can't have duplicate keys, so the caller must have specified
|
||||
# a parameter using both long & short names, like
|
||||
# {'size': '300x400', 'chs': '800x900'}. We don't know which to use.
|
||||
raise KeyError('Both long and short version of parameter %s (%s) '
|
||||
'found. It is unclear which one to use.' % (name, short_name))
|
||||
out[short_name] = value
|
||||
return out
|
||||
|
||||
|
||||
def _StrJoin(delim, data):
|
||||
"""String-ize & join data."""
|
||||
return delim.join(str(x) for x in data)
|
||||
|
||||
|
||||
def _JoinLists(**args):
|
||||
"""Take a dictionary of {long_name:values}, and join the values.
|
||||
|
||||
For each long_name, join the values into a string according to
|
||||
_JOIN_DELIMS. If values is empty or None, replace with an empty string.
|
||||
|
||||
Returns:
|
||||
A dictionary {long_name:joined_value} entries.
|
||||
"""
|
||||
out = {}
|
||||
for key, val in args.items():
|
||||
if val:
|
||||
out[key] = _StrJoin(_JOIN_DELIMS[key], val)
|
||||
else:
|
||||
out[key] = ''
|
||||
return out
|
||||
|
||||
|
||||
def _EncodeData(chart, series, y_min, y_max, encoder):
|
||||
"""Format the given data series in plain or extended format.
|
||||
|
||||
Use the chart's encoder to determine the format. The formatted data will
|
||||
be scaled to fit within the range of values supported by the chosen
|
||||
encoding.
|
||||
|
||||
Args:
|
||||
chart: The chart.
|
||||
series: A list of the the data series to format; each list element is
|
||||
a list of data points.
|
||||
y_min: Minimum data value. May be None if y_max is also None
|
||||
y_max: Maximum data value. May be None if y_min is also None
|
||||
Returns:
|
||||
A dictionary with one key, 'data', whose value is the fully encoded series.
|
||||
"""
|
||||
assert (y_min is None) == (y_max is None)
|
||||
if y_min is not None:
|
||||
def _ScaleAndEncode(series):
|
||||
series = _ScaleData(series, y_min, y_max, encoder.min, encoder.max)
|
||||
return encoder.Encode(series)
|
||||
encoded_series = [_ScaleAndEncode(s) for s in series]
|
||||
else:
|
||||
encoded_series = [encoder.Encode(s) for s in series]
|
||||
result = _JoinLists(**{'data': encoded_series})
|
||||
result['data'] = encoder.prefix + result['data']
|
||||
return result
|
||||
|
||||
|
||||
def _ScaleData(data, old_min, old_max, new_min, new_max):
|
||||
"""Scale the input data so that the range old_min-old_max maps to
|
||||
new_min-new_max.
|
||||
"""
|
||||
def ScalePoint(x):
|
||||
if x is None:
|
||||
return None
|
||||
return scale * x + translate
|
||||
|
||||
if old_min == old_max:
|
||||
scale = 1
|
||||
else:
|
||||
scale = (new_max - new_min) / float(old_max - old_min)
|
||||
translate = new_min - scale * old_min
|
||||
return map(ScalePoint, data)
|
||||
|
||||
|
||||
def _GetChartFactory(chart_class, display_class):
|
||||
"""Create a factory method for instantiating charts with displays.
|
||||
|
||||
Returns a method which, when called, will create & return a chart with
|
||||
chart.display already populated.
|
||||
"""
|
||||
def Inner(*args, **kwargs):
|
||||
chart = chart_class(*args, **kwargs)
|
||||
chart.display = display_class(chart)
|
||||
return chart
|
||||
return Inner
|
||||
|
||||
# These helper methods make it easy to get chart objects with display
|
||||
# objects already setup. For example, this:
|
||||
# chart = google_chart_api.LineChart()
|
||||
# is equivalent to:
|
||||
# chart = line_chart.LineChart()
|
||||
# chart.display = google_chart_api.LineChartEncoder()
|
||||
#
|
||||
# (If there's some chart type for which a helper method isn't available, you
|
||||
# can always just instantiate the correct encoder manually, like in the 2nd
|
||||
# example above).
|
||||
LineChart = _GetChartFactory(line_chart.LineChart, LineChartEncoder)
|
||||
Sparkline = _GetChartFactory(line_chart.Sparkline, SparklineEncoder)
|
||||
BarChart = _GetChartFactory(bar_chart.BarChart, BarChartEncoder)
|
||||
PieChart = _GetChartFactory(pie_chart.PieChart, PieChartEncoder)
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/python2.4
|
||||
#
|
||||
# Copyright 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Code related to bar charts."""
|
||||
|
||||
import copy
|
||||
|
||||
from graphy import common
|
||||
|
||||
|
||||
class BarStyle(object):
|
||||
"""Represents the style for bars on a BarChart.
|
||||
|
||||
Any of the object attributes may be set to None, in which case the
|
||||
value will be auto-calculated.
|
||||
|
||||
Object Attributes:
|
||||
bar_thickness: The thickness of a bar, in pixels.
|
||||
bar_gap: The gap between bars, in pixels.
|
||||
group_gap: The gap between groups of bars, in pixels.
|
||||
"""
|
||||
|
||||
_DEFAULT_GROUP_GAP = 8
|
||||
_DEFAULT_BAR_GAP = 4
|
||||
|
||||
def __init__(self, bar_thickness=None,
|
||||
bar_gap=_DEFAULT_BAR_GAP, group_gap=_DEFAULT_GROUP_GAP):
|
||||
"""Create a new BarStyle.
|
||||
|
||||
Args:
|
||||
bar_thickness: The thickness of a bar, in pixels. Set this to None if
|
||||
you want the bar thickness to be auto-calculated (this is the default
|
||||
behaviour).
|
||||
bar_gap: The gap between bars, in pixels. Default is 4.
|
||||
group_gap: The gap between groups of bars, in pixels. Default is 8.
|
||||
"""
|
||||
self.bar_thickness = bar_thickness
|
||||
self.bar_gap = bar_gap
|
||||
self.group_gap = group_gap
|
||||
|
||||
|
||||
class BarChart(common.BaseChart):
|
||||
"""Represents a bar chart.
|
||||
|
||||
Object attributes:
|
||||
vertical: if True, the bars will be vertical. Default is True.
|
||||
stacked: if True, the bars will be stacked. Default is False.
|
||||
display.style: The BarStyle for all bars on this chart.
|
||||
"""
|
||||
|
||||
def __init__(self, points=None):
|
||||
"""Constructor for BarChart objects."""
|
||||
super(BarChart, self).__init__()
|
||||
if points is not None:
|
||||
self.AddBars(points)
|
||||
self.vertical = True
|
||||
self.stacked = False
|
||||
|
||||
def AddBars(self, points, label=None, color=None):
|
||||
"""Add a series of bars to the chart.
|
||||
|
||||
points: List of y-values for the bars in this series
|
||||
label: Name of the series (used in the legend)
|
||||
color: Hex string, like '00ff00' for green
|
||||
|
||||
This is a convenience method which constructs & appends the DataSeries for
|
||||
you.
|
||||
"""
|
||||
series = common.DataSeries(points, color=color, label=label, style=None)
|
||||
self.data.append(series)
|
||||
return series
|
||||
|
||||
def GetDependentAxis(self):
|
||||
"""Get the dependendant axis, which depends on orientation."""
|
||||
if self.vertical:
|
||||
return self.left
|
||||
else:
|
||||
return self.bottom
|
||||
|
||||
def GetIndependentAxis(self):
|
||||
"""Get the independendant axis, which depends on orientation."""
|
||||
if self.vertical:
|
||||
return self.bottom
|
||||
else:
|
||||
return self.left
|
||||
|
||||
def GetMinMaxValues(self):
|
||||
"""Get the largest & smallest bar values as (min_value, max_value)."""
|
||||
if not self.stacked:
|
||||
return super(BarChart, self).GetMinMaxValues()
|
||||
|
||||
if not self.data:
|
||||
return None, None # No data, nothing to do.
|
||||
num_bars = max(len(series.data) for series in self.data)
|
||||
positives = [0 for i in xrange(0, num_bars)]
|
||||
negatives = list(positives)
|
||||
for series in self.data:
|
||||
for i, point in enumerate(series.data):
|
||||
if point:
|
||||
if point > 0:
|
||||
positives[i] += point
|
||||
else:
|
||||
negatives[i] += point
|
||||
min_value = min(min(positives), min(negatives))
|
||||
max_value = max(max(positives), max(negatives))
|
||||
return min_value, max_value
|
|
@ -0,0 +1,363 @@
|
|||
#!/usr/bin/python2.4
|
||||
#
|
||||
# Copyright 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Code common to all chart types."""
|
||||
|
||||
import copy
|
||||
import warnings
|
||||
|
||||
from graphy import formatters
|
||||
|
||||
|
||||
class Marker(object):
|
||||
|
||||
"""Represents an abstract marker, without position. You can attach these to
|
||||
a DataSeries.
|
||||
|
||||
Object attributes:
|
||||
shape: One of the shape codes (Marker.arrow, Marker.diamond, etc.)
|
||||
color: color (as hex string, f.ex. '0000ff' for blue)
|
||||
size: size of the marker
|
||||
"""
|
||||
# TODO: Write an example using markers.
|
||||
|
||||
# Shapes:
|
||||
arrow = 'a'
|
||||
cross = 'c'
|
||||
diamond = 'd'
|
||||
circle = 'o'
|
||||
square = 's'
|
||||
x = 'x'
|
||||
|
||||
# Note: The Google Chart API also knows some other markers ('v', 'V', 'r',
|
||||
# 'b') that I think would fit better into a grid API.
|
||||
# TODO: Make such a grid API
|
||||
|
||||
def __init__(self, shape, color, size):
|
||||
"""Construct a Marker. See class docstring for details on args."""
|
||||
# TODO: Shapes 'r' and 'b' would be much easier to use if they had a
|
||||
# special-purpose API (instead of trying to fake it with markers)
|
||||
self.shape = shape
|
||||
self.color = color
|
||||
self.size = size
|
||||
|
||||
|
||||
# TODO: might not be a good idea to be mixing content & style here. We
|
||||
# are combining the hard data with the colors & linestyles used. Seems odd.
|
||||
# An example of oddity: A DataSeries graphed on a barchart probably should have
|
||||
# a BarStyle, not a LineStyle.
|
||||
class DataSeries(object):
|
||||
|
||||
"""Represents one data series for a chart (both data & presentation
|
||||
information).
|
||||
|
||||
Object attributes:
|
||||
points: List of numbers representing y-values (x-values are not specified
|
||||
because the Google Chart API expects even x-value spacing).
|
||||
color: Hex string, like '0000ff' for blue
|
||||
style: A LineStyle object.
|
||||
markers: List of (x, m) tuples where m is a Marker object and x is the
|
||||
x-axis value to place it at.
|
||||
|
||||
The "fill" markers ('r' & 'b') are a little weird because they
|
||||
aren't a point on a line. For these, you can fake it by
|
||||
passing slightly weird data (I'd like a better API for them at
|
||||
some point):
|
||||
For 'b', you attach the marker to the starting series, and set x
|
||||
to the index of the ending line. Size is ignored, I think.
|
||||
|
||||
For 'r', you can attach to any line, specify the starting
|
||||
y-value for x and the ending y-value for size. Y, in this case,
|
||||
is becase 0.0 (bottom) and 1.0 (top).
|
||||
label: String with the series' label in the legend. The chart will only
|
||||
have a legend if at least one series has a label. If some series
|
||||
do not have a label then they will have an empty description in
|
||||
the legend. This is currently a limitation in the Google Chart
|
||||
API.
|
||||
"""
|
||||
|
||||
# TODO: Should color & style be optional?
|
||||
# TODO: Should we require the points list to be non-empty ?
|
||||
def __init__(self, points, color, style, markers=None, label=None):
|
||||
"""Construct a DataSeries. See class docstring for details on args."""
|
||||
self.data = points
|
||||
self.color = color
|
||||
self.style = style
|
||||
self.markers = markers or []
|
||||
self.label = label
|
||||
|
||||
|
||||
class AxisPosition(object):
|
||||
"""Represents all the available axis positions.
|
||||
|
||||
The available positions are as follows:
|
||||
AxisPosition.TOP
|
||||
AxisPosition.BOTTOM
|
||||
AxisPosition.LEFT
|
||||
AxisPosition.RIGHT
|
||||
"""
|
||||
LEFT = 'y'
|
||||
RIGHT = 'r'
|
||||
BOTTOM = 'x'
|
||||
TOP = 't'
|
||||
|
||||
|
||||
class Axis(object):
|
||||
|
||||
"""Represents one axis.
|
||||
|
||||
Object setings:
|
||||
min: Minimum value for the bottom or left end of the axis
|
||||
max: Max value.
|
||||
labels: List of labels to show along the axis.
|
||||
label_positions: List of positions to show the labels at. Uses the scale
|
||||
set by min & max, so if you set min = 0 and max = 10, then
|
||||
label positions [0, 5, 10] would be at the bottom,
|
||||
middle, and top of the axis, respectively.
|
||||
grid_spacing: Amount of space between gridlines (in min/max scale).
|
||||
A value of 0 disables gridlines.
|
||||
label_gridlines: If True, draw a line extending from each label
|
||||
on the axis all the way across the chart.
|
||||
"""
|
||||
|
||||
def __init__(self, axis_min=None, axis_max=None):
|
||||
"""Construct a new Axis.
|
||||
|
||||
Args:
|
||||
axis_min: smallest value on the axis
|
||||
axis_max: largest value on the axis
|
||||
"""
|
||||
self.min = axis_min
|
||||
self.max = axis_max
|
||||
self.labels = []
|
||||
self.label_positions = []
|
||||
self.grid_spacing = 0
|
||||
self.label_gridlines = False
|
||||
|
||||
# TODO: Add other chart types. Order of preference:
|
||||
# - scatter plots
|
||||
# - us/world maps
|
||||
|
||||
class BaseChart(object):
|
||||
"""Base chart object with standard behavior for all other charts.
|
||||
|
||||
Object attributes:
|
||||
data: List of DataSeries objects. Chart subtypes provide convenience
|
||||
functions (like AddLine, AddBars, AddSegment) to add more series
|
||||
later.
|
||||
left/right/bottom/top: Axis objects for the 4 different axes.
|
||||
formatters: A list of callables which will be used to format this chart for
|
||||
display. TODO: Need better documentation for how these
|
||||
work.
|
||||
auto_scale, auto_color, auto_legend:
|
||||
These aliases let users access the default formatters without poking
|
||||
around in self.formatters. If the user removes them from
|
||||
self.formatters then they will no longer be enabled, even though they'll
|
||||
still be accessible through the aliases. Similarly, re-assigning the
|
||||
aliases has no effect on the contents of self.formatters.
|
||||
display: This variable is reserved for backends to populate with a display
|
||||
object. The intention is that the display object would be used to
|
||||
render this chart. The details of what gets put here depends on
|
||||
the specific backend you are using.
|
||||
"""
|
||||
|
||||
# Canonical ordering of position keys
|
||||
_POSITION_CODES = 'yrxt'
|
||||
|
||||
# TODO: Add more inline args to __init__ (esp. labels).
|
||||
# TODO: Support multiple series in the constructor, if given.
|
||||
def __init__(self):
|
||||
"""Construct a BaseChart object."""
|
||||
self.data = []
|
||||
|
||||
self._axes = {}
|
||||
for code in self._POSITION_CODES:
|
||||
self._axes[code] = [Axis()]
|
||||
self._legend_labels = [] # AutoLegend fills this out
|
||||
self._show_legend = False # AutoLegend fills this out
|
||||
|
||||
# Aliases for default formatters
|
||||
self.auto_color = formatters.AutoColor()
|
||||
self.auto_scale = formatters.AutoScale()
|
||||
self.auto_legend = formatters.AutoLegend
|
||||
self.formatters = [self.auto_color, self.auto_scale, self.auto_legend]
|
||||
# display is used to convert the chart into something displayable (like a
|
||||
# url or img tag).
|
||||
self.display = None
|
||||
|
||||
def AddFormatter(self, formatter):
|
||||
"""Add a new formatter to the chart (convenience method)."""
|
||||
self.formatters.append(formatter)
|
||||
|
||||
def AddSeries(self, points, color=None, style=None, markers=None,
|
||||
label=None):
|
||||
"""DEPRECATED
|
||||
|
||||
Add a new series of data to the chart; return the DataSeries object."""
|
||||
warnings.warn('AddSeries is deprecated. Instead, call AddLine for '
|
||||
'LineCharts, AddBars for BarCharts, AddSegment for '
|
||||
'PieCharts ', DeprecationWarning, stacklevel=2)
|
||||
series = DataSeries(points, color, style, markers, label)
|
||||
self.data.append(series)
|
||||
return series
|
||||
|
||||
def GetDependentAxis(self):
|
||||
"""Return this chart's dependent axis (often 'left', but
|
||||
horizontal bar-charts use 'bottom').
|
||||
"""
|
||||
return self.left
|
||||
|
||||
def GetIndependentAxis(self):
|
||||
"""Return this chart's independent axis (often 'bottom', but
|
||||
horizontal bar-charts use 'left').
|
||||
"""
|
||||
return self.bottom
|
||||
|
||||
def _Clone(self):
|
||||
"""Make a deep copy this chart.
|
||||
|
||||
Formatters & display will be missing from the copy, due to limitations in
|
||||
deepcopy.
|
||||
"""
|
||||
orig_values = {}
|
||||
# Things which deepcopy will likely choke on if it tries to copy.
|
||||
uncopyables = ['formatters', 'display', 'auto_color', 'auto_scale',
|
||||
'auto_legend']
|
||||
for name in uncopyables:
|
||||
orig_values[name] = getattr(self, name)
|
||||
setattr(self, name, None)
|
||||
clone = copy.deepcopy(self)
|
||||
for name, orig_value in orig_values.iteritems():
|
||||
setattr(self, name, orig_value)
|
||||
return clone
|
||||
|
||||
def GetFormattedChart(self):
|
||||
"""Get a copy of the chart with formatting applied."""
|
||||
# Formatters need to mutate the chart, but we don't want to change it out
|
||||
# from under the user. So, we work on a copy of the chart.
|
||||
scratchpad = self._Clone()
|
||||
for formatter in self.formatters:
|
||||
formatter(scratchpad)
|
||||
return scratchpad
|
||||
|
||||
def GetMinMaxValues(self):
|
||||
"""Get the largest & smallest values in this chart, returned as
|
||||
(min_value, max_value). Takes into account complciations like stacked data
|
||||
series.
|
||||
|
||||
For example, with non-stacked series, a chart with [1, 2, 3] and [4, 5, 6]
|
||||
would return (1, 6). If the same chart was stacking the data series, it
|
||||
would return (5, 9).
|
||||
"""
|
||||
MinPoint = lambda data: min(x for x in data if x is not None)
|
||||
MaxPoint = lambda data: max(x for x in data if x is not None)
|
||||
mins = [MinPoint(series.data) for series in self.data if series.data]
|
||||
maxes = [MaxPoint(series.data) for series in self.data if series.data]
|
||||
if not mins or not maxes:
|
||||
return None, None # No data, just bail.
|
||||
return min(mins), max(maxes)
|
||||
|
||||
def AddAxis(self, position, axis):
|
||||
"""Add an axis to this chart in the given position.
|
||||
|
||||
Args:
|
||||
position: an AxisPosition object specifying the axis's position
|
||||
axis: The axis to add, an Axis object
|
||||
Returns:
|
||||
the value of the axis parameter
|
||||
"""
|
||||
self._axes.setdefault(position, []).append(axis)
|
||||
return axis
|
||||
|
||||
def GetAxis(self, position):
|
||||
"""Get or create the first available axis in the given position.
|
||||
|
||||
This is a helper method for the left, right, top, and bottom properties.
|
||||
If the specified axis does not exist, it will be created.
|
||||
|
||||
Args:
|
||||
position: the position to search for
|
||||
Returns:
|
||||
The first axis in the given position
|
||||
"""
|
||||
# Not using setdefault here just in case, to avoid calling the Axis()
|
||||
# constructor needlessly
|
||||
if position in self._axes:
|
||||
return self._axes[position][0]
|
||||
else:
|
||||
axis = Axis()
|
||||
self._axes[position] = [axis]
|
||||
return axis
|
||||
|
||||
def SetAxis(self, position, axis):
|
||||
"""Set the first axis in the given position to the given value.
|
||||
|
||||
This is a helper method for the left, right, top, and bottom properties.
|
||||
|
||||
Args:
|
||||
position: an AxisPosition object specifying the axis's position
|
||||
axis: The axis to set, an Axis object
|
||||
Returns:
|
||||
the value of the axis parameter
|
||||
"""
|
||||
self._axes.setdefault(position, [None])[0] = axis
|
||||
return axis
|
||||
|
||||
def _GetAxes(self):
|
||||
"""Return a generator of (position_code, Axis) tuples for this chart's axes.
|
||||
|
||||
The axes will be sorted by position using the canonical ordering sequence,
|
||||
_POSITION_CODES.
|
||||
"""
|
||||
for code in self._POSITION_CODES:
|
||||
for axis in self._axes.get(code, []):
|
||||
yield (code, axis)
|
||||
|
||||
def _GetBottom(self):
|
||||
return self.GetAxis(AxisPosition.BOTTOM)
|
||||
|
||||
def _SetBottom(self, value):
|
||||
self.SetAxis(AxisPosition.BOTTOM, value)
|
||||
|
||||
bottom = property(_GetBottom, _SetBottom,
|
||||
doc="""Get or set the bottom axis""")
|
||||
|
||||
def _GetLeft(self):
|
||||
return self.GetAxis(AxisPosition.LEFT)
|
||||
|
||||
def _SetLeft(self, value):
|
||||
self.SetAxis(AxisPosition.LEFT, value)
|
||||
|
||||
left = property(_GetLeft, _SetLeft,
|
||||
doc="""Get or set the left axis""")
|
||||
|
||||
def _GetRight(self):
|
||||
return self.GetAxis(AxisPosition.RIGHT)
|
||||
|
||||
def _SetRight(self, value):
|
||||
self.SetAxis(AxisPosition.RIGHT, value)
|
||||
|
||||
right = property(_GetRight, _SetRight,
|
||||
doc="""Get or set the right axis""")
|
||||
|
||||
def _GetTop(self):
|
||||
return self.GetAxis(AxisPosition.TOP)
|
||||
|
||||
def _SetTop(self, value):
|
||||
self.SetAxis(AxisPosition.TOP, value)
|
||||
|
||||
top = property(_GetTop, _SetTop,
|
||||
doc="""Get or set the top axis""")
|
|
@ -0,0 +1,185 @@
|
|||
#!/usr/bin/python2.4
|
||||
#
|
||||
# Copyright 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""This module contains various formatters which can help format a chart
|
||||
object. To use these, add them to your chart's list of formatters. For
|
||||
example:
|
||||
chart.formatters.append(InlineLegend)
|
||||
chart.formatters.append(LabelSeparator(right=8))
|
||||
|
||||
Feel free to write your own formatter. Formatters are just callables that
|
||||
modify the chart in some (hopefully useful) way. For example, the AutoColor
|
||||
formatter makes sure each DataSeries has a color applied to it. The formatter
|
||||
should take the chart to format as its only argument.
|
||||
|
||||
(The formatters work on a deepcopy of the user's chart, so modifications
|
||||
shouldn't leak back into the user's original chart)
|
||||
"""
|
||||
|
||||
def AutoLegend(chart):
|
||||
"""Automatically fill out the legend based on series labels. This will only
|
||||
fill out the legend if is at least one series with a label.
|
||||
"""
|
||||
chart._show_legend = False
|
||||
labels = []
|
||||
for series in chart.data:
|
||||
if series.label is None:
|
||||
labels.append('')
|
||||
else:
|
||||
labels.append(series.label)
|
||||
chart._show_legend = True
|
||||
if chart._show_legend:
|
||||
chart._legend_labels = labels
|
||||
|
||||
|
||||
class AutoColor(object):
|
||||
"""Automatically add colors to any series without colors.
|
||||
|
||||
Object attributes:
|
||||
colors: The list of colors (hex strings) to cycle through. You can modify
|
||||
this list if you don't like the default colors.
|
||||
"""
|
||||
def __init__(self):
|
||||
# TODO: Add a few more default colors.
|
||||
# TODO: Add a default styles too, so if you don't specify color or
|
||||
# style, you get a unique set of colors & styles for your data.
|
||||
self.colors = ['0000ff', 'ff0000', '00dd00', '000000']
|
||||
|
||||
def __call__(self, chart):
|
||||
index = -1
|
||||
for series in chart.data:
|
||||
if series.color is None:
|
||||
index += 1
|
||||
if index >= len(self.colors):
|
||||
index = 0
|
||||
series.color = self.colors[index]
|
||||
|
||||
|
||||
class AutoScale(object):
|
||||
"""If the user didn't set min/max on the dependent axis, calculate min/max
|
||||
dynamically from the data."""
|
||||
|
||||
def __init__(self, buffer=0.05):
|
||||
"""Create a new AutoScale formatter.
|
||||
|
||||
Args:
|
||||
buffer: percentage of extra space to allocate around the chart's axes.
|
||||
"""
|
||||
self.buffer = buffer
|
||||
|
||||
def __call__(self, chart):
|
||||
"""Format the chart by setting the min/max values on its dependent axis."""
|
||||
if not chart.data:
|
||||
return # Nothing to do.
|
||||
min_value, max_value = chart.GetMinMaxValues()
|
||||
if None in (min_value, max_value):
|
||||
return # No data. Nothing to do.
|
||||
|
||||
# TODO: I think that setting min/max on either the left OR right axis
|
||||
# should disable auto-scaling. Otherwise, if you set right labels but leave
|
||||
# left axis alone, your data will be scaled to a different range than your
|
||||
# labels. Weird.
|
||||
# TODO: It would actually be quite useful to have a function that
|
||||
# retrieved a list of dependent axes, as opposed to just the first one.
|
||||
axis = chart.GetDependentAxis()
|
||||
|
||||
# Honor user's choice, if they've picked min/max
|
||||
if axis.min is not None:
|
||||
min_value = axis.min
|
||||
if axis.max is not None:
|
||||
max_value = axis.max
|
||||
|
||||
buffer = (max_value - min_value) * self.buffer # Stay away from edge.
|
||||
if axis.min is None:
|
||||
axis.min = min_value - buffer
|
||||
if axis.max is None:
|
||||
axis.max = max_value + buffer
|
||||
|
||||
|
||||
class LabelSeparator(object):
|
||||
|
||||
"""Adjust the label positions to avoid having them overlap. This happens for
|
||||
any axis with minimum_label_spacing set.
|
||||
"""
|
||||
|
||||
def __init__(self, left=None, right=None, bottom=None):
|
||||
self.left = left
|
||||
self.right = right
|
||||
self.bottom = bottom
|
||||
|
||||
def __call__(self, chart):
|
||||
self.AdjustLabels(chart.left, self.left)
|
||||
self.AdjustLabels(chart.right, self.right)
|
||||
self.AdjustLabels(chart.bottom, self.bottom)
|
||||
|
||||
def AdjustLabels(self, axis, minimum_label_spacing):
|
||||
if minimum_label_spacing is None:
|
||||
return
|
||||
if len(axis.labels) <= 1: # Nothing to adjust
|
||||
return
|
||||
if axis.max is not None and axis.min is not None:
|
||||
# Find the spacing required to fit all labels evenly.
|
||||
# Don't try to push them farther apart than that.
|
||||
maximum_possible_spacing = (axis.max - axis.min) / (len(axis.labels) - 1)
|
||||
if minimum_label_spacing > maximum_possible_spacing:
|
||||
minimum_label_spacing = maximum_possible_spacing
|
||||
|
||||
labels = [list(x) for x in zip(axis.label_positions, axis.labels)]
|
||||
labels = sorted(labels, reverse=True)
|
||||
|
||||
# First pass from the top, moving colliding labels downward
|
||||
for i in range(1, len(labels)):
|
||||
if labels[i - 1][0] - labels[i][0] < minimum_label_spacing:
|
||||
new_position = labels[i - 1][0] - minimum_label_spacing
|
||||
if axis.min is not None and new_position < axis.min:
|
||||
new_position = axis.min
|
||||
labels[i][0] = new_position
|
||||
|
||||
# Second pass from the bottom, moving colliding labels upward
|
||||
for i in range(len(labels) - 2, -1, -1):
|
||||
if labels[i][0] - labels[i + 1][0] < minimum_label_spacing:
|
||||
new_position = labels[i + 1][0] + minimum_label_spacing
|
||||
if axis.max is not None and new_position > axis.max:
|
||||
new_position = axis.max
|
||||
labels[i][0] = new_position
|
||||
|
||||
# Separate positions and labels
|
||||
label_positions, labels = zip(*labels)
|
||||
axis.labels = labels
|
||||
axis.label_positions = label_positions
|
||||
|
||||
|
||||
def InlineLegend(chart):
|
||||
"""Provide a legend for line charts by attaching labels to the right
|
||||
end of each line. Supresses the regular legend.
|
||||
"""
|
||||
show = False
|
||||
labels = []
|
||||
label_positions = []
|
||||
for series in chart.data:
|
||||
if series.label is None:
|
||||
labels.append('')
|
||||
else:
|
||||
labels.append(series.label)
|
||||
show = True
|
||||
label_positions.append(series.data[-1])
|
||||
|
||||
if show:
|
||||
chart.right.min = chart.left.min
|
||||
chart.right.max = chart.left.max
|
||||
chart.right.labels = labels
|
||||
chart.right.label_positions = label_positions
|
||||
chart._show_legend = False # Supress the regular legend.
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,114 @@
|
|||
#!/usr/bin/python2.4
|
||||
#
|
||||
# Copyright 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Code related to line charts."""
|
||||
|
||||
import copy
|
||||
import warnings
|
||||
|
||||
from graphy import common
|
||||
|
||||
|
||||
class LineStyle(object):
|
||||
|
||||
"""Represents the style for a line on a line chart. Also provides some
|
||||
convenient presets.
|
||||
|
||||
Object attributes (Passed directly to the Google Chart API. Check there for
|
||||
details):
|
||||
width: Width of the line
|
||||
on: Length of a line segment (for dashed/dotted lines)
|
||||
off: Length of a break (for dashed/dotted lines)
|
||||
|
||||
Some common styles, such as LineStyle.dashed, are available:
|
||||
solid
|
||||
dashed
|
||||
dotted
|
||||
thick_solid
|
||||
thick_dashed
|
||||
thick_dotted
|
||||
"""
|
||||
|
||||
# Widths
|
||||
THIN = 1
|
||||
THICK = 2
|
||||
|
||||
# Patterns
|
||||
# ((on, off) tuples, as passed to LineChart.AddLine)
|
||||
SOLID = (1, 0)
|
||||
DASHED = (8, 4)
|
||||
DOTTED = (2, 4)
|
||||
|
||||
def __init__(self, width, on, off):
|
||||
"""Construct a LineStyle. See class docstring for details on args."""
|
||||
self.width = width
|
||||
self.on = on
|
||||
self.off = off
|
||||
|
||||
|
||||
LineStyle.solid = LineStyle(1, 1, 0)
|
||||
LineStyle.dashed = LineStyle(1, 8, 4)
|
||||
LineStyle.dotted = LineStyle(1, 2, 4)
|
||||
LineStyle.thick_solid = LineStyle(2, 1, 0)
|
||||
LineStyle.thick_dashed = LineStyle(2, 8, 4)
|
||||
LineStyle.thick_dotted = LineStyle(2, 2, 4)
|
||||
|
||||
|
||||
|
||||
class LineChart(common.BaseChart):
|
||||
|
||||
"""Represents a line chart."""
|
||||
|
||||
def __init__(self, points=None):
|
||||
super(LineChart, self).__init__()
|
||||
if points is not None:
|
||||
self.AddLine(points)
|
||||
|
||||
def AddLine(self, points, label=None, markers=None,
|
||||
color=None, pattern=LineStyle.SOLID, width=LineStyle.THIN):
|
||||
"""Add a new line to the chart.
|
||||
|
||||
This is a convenience method which constructs the DataSeries and appends it
|
||||
for you. It returns the new series.
|
||||
|
||||
points: List of equally-spaced y-values for the line
|
||||
label: Name of the line (used for the legend)
|
||||
markers: List of Marker objects to attach to this line (see DataSeries
|
||||
for more info)
|
||||
color: Hex string, like 'ff0000' for red
|
||||
pattern: Tuple for (length of segment, length of gap). i.e.
|
||||
LineStyle.DASHED
|
||||
width: Width of the line (i.e. LineStyle.THIN)
|
||||
|
||||
"""
|
||||
style = LineStyle(width, pattern[0], pattern[1])
|
||||
series = common.DataSeries(points, color, style, markers, label)
|
||||
self.data.append(series)
|
||||
return series
|
||||
|
||||
def AddSeries(self, points, color=None, style=LineStyle.solid, markers=None,
|
||||
label=None):
|
||||
"""DEPRECATED"""
|
||||
warnings.warn('LineChart.AddSeries is deprecated. Call AddLine instead. ',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
return super(LineChart, self).AddSeries(points, color, style,
|
||||
markers, label)
|
||||
|
||||
|
||||
class Sparkline(LineChart):
|
||||
"""Represent a sparkline. These behave like LineCharts,
|
||||
mostly, but come without axes.
|
||||
"""
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/python2.4
|
||||
#
|
||||
# Copyright 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Code for pie charts."""
|
||||
|
||||
import warnings
|
||||
|
||||
from graphy import common
|
||||
|
||||
|
||||
class Segment(common.DataSeries):
|
||||
"""A single segment of the pie chart.
|
||||
|
||||
Object attributes:
|
||||
size: relative size of the segment
|
||||
color: color of the segment (if any)
|
||||
label: label of the segment (if any)
|
||||
"""
|
||||
def __init__(self, size, color=None, label=None):
|
||||
super(Segment, self).__init__([size], color, None, None, label)
|
||||
assert size >= 0
|
||||
|
||||
def _GetSize(self):
|
||||
return self.data[0]
|
||||
|
||||
def _SetSize(self, value):
|
||||
assert value >= 0
|
||||
self.data[0] = value
|
||||
|
||||
size = property(_GetSize, _SetSize,
|
||||
doc = """The relative size of this pie segment.""")
|
||||
|
||||
|
||||
class PieChart(common.BaseChart):
|
||||
"""Represent a pie chart.
|
||||
|
||||
TODO: Move is3d to a pie-chart specific style object
|
||||
|
||||
Object attributes:
|
||||
display.is3d: If true, draw a 3d pie chart; if false, draw a flat one.
|
||||
This is a property of the default PieChartEncoder.
|
||||
"""
|
||||
|
||||
def __init__(self, points=None, labels=None, colors=None):
|
||||
"""Constructor for PieChart objects
|
||||
|
||||
Args:
|
||||
data_points: A list of data points for the pie chart;
|
||||
i.e., relative sizes of the pie segments
|
||||
labels: A list of labels for the pie segments.
|
||||
TODO: Allow the user to pass in None as one of
|
||||
the labels in order to skip that label.
|
||||
colors: A list of colors for the pie segments, as hex strings
|
||||
(f.ex. '0000ff' for blue). Missing colors will be
|
||||
automatically interpolated by the server.
|
||||
"""
|
||||
super(PieChart, self).__init__()
|
||||
self.formatters = []
|
||||
if points:
|
||||
self.AddSegments(points, labels, colors)
|
||||
|
||||
def AddSegments(self, points, labels, colors):
|
||||
"""Add more segments to this pie chart."""
|
||||
num_colors = len(colors or [])
|
||||
for i, pt in enumerate(points):
|
||||
assert pt >= 0
|
||||
seg = Segment(pt, None, labels[i])
|
||||
if i < num_colors:
|
||||
seg.color = colors[i]
|
||||
self.AddSegment(seg)
|
||||
|
||||
# TODO: switch this to taking size/color/label instead, to better
|
||||
# match AddLine and AddBars.
|
||||
def AddSegment(self, segment):
|
||||
"""Add a pie segment to this chart, and return the segment.
|
||||
|
||||
The segment must have a unique label.
|
||||
"""
|
||||
assert segment.size >= 0
|
||||
self.data.append(segment)
|
||||
return segment
|
||||
|
||||
def AddSeries(self, points, color=None, style=None, markers=None, label=None):
|
||||
"""DEPRECATED
|
||||
|
||||
Add a new segment to the chart and return it.
|
||||
|
||||
The segment must contain exactly one data point; all parameters
|
||||
other than color and label are ignored.
|
||||
"""
|
||||
warnings.warn('PieChart.AddSeries is deprecated. Call AddSegment or '
|
||||
'AddSegments instead.', DeprecationWarning)
|
||||
return self.AddSegment(Segment(points[0], color, label))
|
||||
|
||||
def SetColors(self, *colors):
|
||||
"""Change the colors of this chart to the specified list of colors.
|
||||
|
||||
Missing colors will be interpolated by the server.
|
||||
"""
|
||||
num_colors = len(colors)
|
||||
assert num_colors <= len(self.data)
|
||||
for i,segment in enumerate(self.data):
|
||||
if i >= num_colors:
|
||||
segment.color = None
|
||||
else:
|
||||
segment.color = colors[i]
|
Loading…
Reference in New Issue