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:
zovirl@zovirl.com 2008-09-30 21:35:47 +00:00
parent a3ae78eb24
commit a0b623fbf6
19 changed files with 3106 additions and 1 deletions

201
LICENSE Normal file
View File

@ -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
View File

@ -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.

67
examples/bay_area_population.py Executable file
View 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>'

View File

@ -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>'

42
examples/elevation.py Executable file
View File

@ -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>'

44
examples/signal.py Executable file
View File

@ -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>'

28
examples/spectrum.py Executable file
View File

@ -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

22
examples/stock.py Executable file
View File

@ -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

25
examples/sunnyvale_rainfall.py Executable file
View File

@ -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

35
examples/traffic.py Executable file
View File

@ -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
graphy/__init__.py Normal file
View File

View File

View File

@ -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)

119
graphy/bar_chart.py Normal file
View File

@ -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

363
graphy/common.py Normal file
View File

@ -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""")

185
graphy/formatters.py Normal file
View File

@ -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.

1074
graphy/graphy_test.py Executable file

File diff suppressed because it is too large Load Diff

114
graphy/line_chart.py Normal file
View File

@ -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.
"""

119
graphy/pie_chart.py Normal file
View File

@ -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]