graphy-1.0+dfsg/ 0000755 0000000 0000000 00000000000 11320654610 010447 5 ustar graphy-1.0+dfsg/README 0000644 0000000 0000000 00000001533 11203124512 011322 0 ustar Graphy
Graphy is a chart library for python. It tries to get out of the way and just
let you work with your data.
For more information, see http://code.google.com/p/graphy/
For license info, see the LICENSE file.
INSTALLATION:
No installer yet, so just manually copy graphy/ into python-lib/graphy.
QUICK START:
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
print chart.display.Url(400, 100)
EXAMPLES:
The examples in the examples/ directory assume graphy is in the PYTHONPATH.
If just want to run them without installing graphy first, you will need to do
something like this:
$ cd examples
$ PYTHONPATH=.. ./traffic.py
graphy-1.0+dfsg/examples/ 0000755 0000000 0000000 00000000000 11320654610 012265 5 ustar graphy-1.0+dfsg/examples/bay_area_population.py 0000644 0000000 0000000 00000004224 11203124512 016647 0 ustar #!/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 '
Population Bar Chart'
print 'Population of Select Bay Area Cities
'
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 'You could also do this as 2 charts
'
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 '2000
'
print chart.display.Img(400, 220)
chart.data[0].data = pop1960 # Swap in older data
print '1960
'
print chart.display.Img(400, 220)
print ''
graphy-1.0+dfsg/examples/traffic.py 0000644 0000000 0000000 00000002344 11203124512 014251 0 ustar #!/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 ''
print 'Oh no! Traffic is dropping off, something must be wrong!
'
traffic = [578, 579, 580, 550, 545, 552]
chart = google_chart_api.LineChart(traffic)
print chart.display.Img(100, 50)
print """But wait, that was automatically scaled to fill the entire
vertical range. We should scale from zero instead:
"""
chart.left.min = 0
chart.left.max = 600
print chart.display.Img(100, 50)
print """Also, maybe some labels would help out here:
"""
chart.left.labels = range(0, 601, 200)
chart.left.label_positions = chart.left.labels
print chart.display.Img(100, 50)
graphy-1.0+dfsg/examples/stock.py 0000644 0000000 0000000 00000001603 11203124512 013753 0 ustar #!/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 = '
' % url
print 'Stock prices went up %s this quarter.' % img
graphy-1.0+dfsg/examples/sunnyvale_rainfall.py 0000644 0000000 0000000 00000001727 11203124512 016533 0 ustar #!/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 'Monthly Rainfall for Sunnyvale, CA
%s' % img
graphy-1.0+dfsg/examples/signal.py 0000644 0000000 0000000 00000002547 11203124512 014115 0 ustar #!/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 'Audio Signal'
print 'Separate
'
chart.stacked = False
chart.style = bar_chart.BarChartStyle(None, 0, 1)
print chart.display.Img(640, 120)
print 'Joined
'
chart.stacked = True
chart.style = bar_chart.BarChartStyle(None, 1)
print chart.display.Img(640, 120)
print ''
graphy-1.0+dfsg/examples/spectrum.py 0000644 0000000 0000000 00000002020 11203124512 014464 0 ustar #!/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 'Colors
%s' % img
chart.display.is3d = True
img = chart.display.Img(300, 100)
print '3D view
%s' % img
graphy-1.0+dfsg/examples/chicago_vs_sunnyvale.py 0000644 0000000 0000000 00000003477 11203124512 017054 0 ustar #!/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 ''
print 'Yearly temperature in Chicago and Sunnyvale'
print ''
print 'Yearly temperature in Chicago and Sunnyvale
'
chart = google_chart_api.LineChart()
chart.AddLine(sunnyvale)
chart.AddLine(chicago, pattern=line_chart.LineStyle.DASHED)
print chart.display.Img(250, 100)
print "But that's hard to understand. We need labels:
"
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 'A grid would be nice, too.
'
chart.left.label_gridlines = True
chart.bottom.label_gridlines = True
print chart.display.Img(250, 100)
print ''
print ''
graphy-1.0+dfsg/examples/states.py 0000644 0000000 0000000 00000003550 11203124512 014136 0 ustar #!/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 common
# Area of some states, in square miles
# Data from http://www.enchantedlearning.com/usa/states/area.shtml
area = {'WA': 71303,
'OR': 98386,
'ID': 83574,
'MT': 147046,
'WY': 97818,
'ME': 35387,
'VT': 9615,
'NH': 9351,
'MA': 10555,
'NY': 54475,
'CT': 5544,
'RI': 1545,
'NJ': 8722,
'PA': 46058}
northwest = ('WA', 'OR', 'ID', 'MT', 'WY')
northeast = ('ME', 'VT', 'NH', 'MA', 'NY', 'CT', 'RI', 'NJ', 'PA')
states = northwest + northeast
areas = [area[s] for s in states]
print ''
print 'Areas of States in the Northwest and Northeast US
'
print '(in square miles)
'
chart = google_chart_api.BarChart(areas)
chart.bottom.labels = states
region_axis = common.Axis()
region_axis.min = 0
region_axis.max = 100
region_axis.labels = ['Northwest', 'Northeast']
region_axis.label_positions = [20, 60]
chart.AddAxis(common.AxisPosition.BOTTOM, region_axis)
ylabels = range(0, 150001, 50000)
chart.left.min = min(ylabels)
chart.left.max = max(ylabels)
chart.left.labels = ['%sK' % (x / 1000) for x in ylabels]
chart.left.label_positions = ylabels
print chart.display.Img(500, 220)
print ''
graphy-1.0+dfsg/examples/elevation.py 0000644 0000000 0000000 00000002633 11203124512 014622 0 ustar #!/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 ''
print 'Elevation
'
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 ''
graphy-1.0+dfsg/graphy/ 0000755 0000000 0000000 00000000000 11320654667 011755 5 ustar graphy-1.0+dfsg/graphy/pie_chart_test.py 0000644 0000000 0000000 00000007061 11203124512 015305 0 ustar #!/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.
"""Tests for pie_chart.py."""
import warnings
from graphy import pie_chart
from graphy import graphy_test
class SegmentTest(graphy_test.GraphyTest):
def setUp(self):
warnings.resetwarnings()
# TODO: remove once the deprecation warning is removed
def testSegmentOrder(self):
# Deprecated approach
warnings.filterwarnings('error')
self.assertRaises(DeprecationWarning, pie_chart.Segment, 1,
'0000FF', 'label')
# New order
s = pie_chart.Segment(1, 'label', '0000FF')
self.assertEqual('label', s.label)
self.assertEqual('0000FF', s.color)
class PieChartTest(graphy_test.GraphyTest):
def tearDown(self):
warnings.resetwarnings()
def testNegativeSegmentSizes(self):
self.assertRaises(AssertionError, pie_chart.PieChart,
[-5, 10], ['Negative', 'Positive'])
chart = pie_chart.PieChart()
self.assertRaises(AssertionError, pie_chart.Segment, -5, 'Dummy', '0000ff')
segment = chart.AddSegment(10, label='Dummy', color='0000ff')
self.assertRaises(AssertionError, segment._SetSize, -5)
# TODO: remove once the deprecation warning is removed
def testAddSegmentOrder(self):
chart = pie_chart.PieChart()
# Deprecated approach
warnings.filterwarnings('error')
self.assertRaises(DeprecationWarning, chart.AddSegment, 1,
'0000FF', 'label')
# New order
chart.AddSegment(1, 'label', '0000FF')
self.assertEqual('label', chart.data[0][0].label)
self.assertEqual('0000FF', chart.data[0][0].color)
# TODO: remove once the deprecation warning is removed
def testAddSegmentsOrder(self):
chart = pie_chart.PieChart()
# Deprecated approach
warnings.filterwarnings('error')
self.assertRaises(DeprecationWarning, chart.AddSegments, [1],
['0000FF'], ['label'])
# New order
warnings.filterwarnings('ignore')
chart.AddSegments([1], ['label'], ['0000FF'])
self.assertEqual('label', chart.data[0][0].label)
self.assertEqual('0000FF', chart.data[0][0].color)
def testAddPie(self):
chart = pie_chart.PieChart()
i = chart.AddPie([1], ['A'], ['ff0000'])
self.assertEqual(i, 0)
self.assertEqual(len(chart.data), 1)
self.assertEqual(len(chart.data[0]), 1)
self.assertEqual(chart.data[0][0].size, 1)
i = chart.AddPie([2], ['B'], ['0000ff'])
self.assertEqual(i, 1)
self.assertEqual(len(chart.data), 2)
self.assertEqual(len(chart.data[0]), 1)
self.assertEqual(chart.data[0][0].size, 1)
self.assertEqual(len(chart.data[1]), 1)
self.assertEqual(chart.data[1][0].size, 2)
def testAddSegmentToPie(self):
chart = pie_chart.PieChart()
chart.AddPie([1], ['A'], ['ff0000'])
chart.AddPie([2], ['B'], ['0000ff'])
chart.AddSegment([10], ['AA'])
self.assertEqual(len(chart.data[0]), 2)
self.assertEqual(len(chart.data[1]), 1)
chart.AddSegment([20], ['BB'], pie_index=1)
self.assertEqual(len(chart.data[0]), 2)
self.assertEqual(len(chart.data[1]), 2)
if __name__ == '__main__':
graphy_test.main()
graphy-1.0+dfsg/graphy/all_tests.py 0000644 0000000 0000000 00000003077 11203124512 014305 0 ustar #!/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.
"""Run all tests from *_test.py files."""
import os
import unittest
def ModuleName(filename, base_dir):
"""Given a filename, convert to the python module name."""
filename = filename.replace(base_dir, '')
filename = filename.lstrip(os.path.sep)
filename = filename.replace(os.path.sep, '.')
if filename.endswith('.py'):
filename = filename[:-3]
return filename
def FindTestModules():
"""Return names of any test modules (*_test.py)."""
tests = []
start_dir = os.path.dirname(os.path.abspath(__file__))
for dir, subdirs, files in os.walk(start_dir):
if dir.endswith('/.svn') or '/.svn/' in dir:
continue
tests.extend(ModuleName(os.path.join(dir, f), start_dir) for f
in files if f.endswith('_test.py'))
return tests
def AllTests():
suites = unittest.defaultTestLoader.loadTestsFromNames(FindTestModules())
return unittest.TestSuite(suites)
if __name__ == '__main__':
unittest.main(module=None, defaultTest='__main__.AllTests')
graphy-1.0+dfsg/graphy/graphy_test.py 0000644 0000000 0000000 00000002745 11203124512 014645 0 ustar #!/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.
"""Base test code for Graphy."""
import unittest
class GraphyTest(unittest.TestCase):
"""Base class for other Graphy tests."""
def assertIn(self, a, b, msg=None):
"""Just like self.assert_(a in b), but with a nicer default message."""
if msg is None:
msg = '"%s" not found in "%s"' % (a, b)
self.assert_(a in b, msg)
def assertNotIn(self, a, b, msg=None):
"""Just like self.assert_(a not in b), but with a nicer default message."""
if msg is None:
msg = '"%s" unexpectedly found in "%s"' % (a, b)
self.assert_(a not in b, msg)
def Param(self, param_name, chart=None):
"""Helper to look up a Google Chart API parameter for the given chart."""
if chart is None:
chart = self.chart
params = chart.display._Params(chart)
return params[param_name]
def main():
"""Wrap unittest.main (for convenience of caller)."""
return unittest.main()
graphy-1.0+dfsg/graphy/common_test.py 0000644 0000000 0000000 00000007157 11203124512 014645 0 ustar #!/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.
"""Tests for common.py."""
import warnings
from graphy import common
from graphy import graphy_test
from graphy.backends import google_chart_api
class CommonTest(graphy_test.GraphyTest):
def setUp(self):
self.chart = google_chart_api.LineChart()
def tearDown(self):
warnings.resetwarnings()
def testDependentAxis(self):
self.assertTrue(self.chart.left is self.chart.GetDependentAxis())
self.assertTrue(self.chart.bottom is self.chart.GetIndependentAxis())
def testAxisAssignment(self):
"""Make sure axis assignment works properly"""
new_axis = common.Axis()
self.chart.top = new_axis
self.assertTrue(self.chart.top is new_axis)
new_axis = common.Axis()
self.chart.bottom = new_axis
self.assertTrue(self.chart.bottom is new_axis)
new_axis = common.Axis()
self.chart.left = new_axis
self.assertTrue(self.chart.left is new_axis)
new_axis = common.Axis()
self.chart.right = new_axis
self.assertTrue(self.chart.right is new_axis)
def testAxisConstruction(self):
axis = common.Axis()
self.assertTrue(axis.min is None)
self.assertTrue(axis.max is None)
axis = common.Axis(-2, 16)
self.assertEqual(axis.min, -2)
self.assertEqual(axis.max, 16)
def testGetDependentIndependentAxes(self):
c = self.chart
self.assertEqual([c.left, c.right], c.GetDependentAxes())
self.assertEqual([c.top, c.bottom], c.GetIndependentAxes())
right2 = c.AddAxis(common.AxisPosition.RIGHT, common.Axis())
bottom2 = c.AddAxis(common.AxisPosition.BOTTOM, common.Axis())
self.assertEqual([c.left, c.right, right2], c.GetDependentAxes())
self.assertEqual([c.top, c.bottom, bottom2], c.GetIndependentAxes())
# TODO: remove once AddSeries is deleted
def testAddSeries(self):
warnings.filterwarnings('ignore')
chart = common.BaseChart()
chart.AddSeries(points=[1, 2, 3], style='foo',
markers='markers', label='label')
series = chart.data[0]
self.assertEqual(series.data, [1, 2, 3])
self.assertEqual(series.style, 'foo')
self.assertEqual(series.markers, 'markers')
self.assertEqual(series.label, 'label')
# TODO: remove once the deprecation warning is removed
def testDataSeriesStyles(self):
# Deprecated approach
warnings.filterwarnings('error')
self.assertRaises(DeprecationWarning, common.DataSeries, [1, 2, 3],
color='0000FF')
warnings.filterwarnings('ignore')
d = common.DataSeries([1, 2, 3], color='0000FF')
self.assertEqual('0000FF', d.color)
d.color = 'F00'
self.assertEqual('F00', d.color)
# TODO: remove once the deprecation warning is removed
def testDataSeriesArgumentOrder(self):
# Deprecated approach
warnings.filterwarnings('error')
self.assertRaises(DeprecationWarning, common.DataSeries, [1, 2, 3],
'0000FF', 'style')
# New order
style = common._BasicStyle('0000FF')
d = common.DataSeries([1, 2, 3], 'label', style)
self.assertEqual('label', d.label)
self.assertEqual(style, d.style)
if __name__ == '__main__':
graphy_test.main()
graphy-1.0+dfsg/graphy/line_chart_test.py 0000644 0000000 0000000 00000005256 11203124512 015463 0 ustar #!/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.
"""Tests for line_chart.py."""
import warnings
from graphy import common
from graphy import line_chart
from graphy import graphy_test
# TODO: All the different charts are expected to support a similar API (like
# having a display object, having a list of data series, axes, etc.). Add some
# tests that run against all the charts to make sure they conform to the API.
class LineChartTest(graphy_test.GraphyTest):
def tearDown(self):
warnings.resetwarnings()
# TODO: remove once AddSeries is deleted
def testAddSeries(self):
warnings.filterwarnings('ignore')
chart = line_chart.LineChart()
chart.AddSeries(points=[1, 2, 3], style=line_chart.LineStyle.solid,
markers='markers', label='label')
series = chart.data[0]
self.assertEqual(series.data, [1, 2, 3])
self.assertEqual(series.style.width, line_chart.LineStyle.solid.width)
self.assertEqual(series.style.on, line_chart.LineStyle.solid.on)
self.assertEqual(series.style.off, line_chart.LineStyle.solid.off)
self.assertEqual(series.markers, 'markers')
self.assertEqual(series.label, 'label')
# TODO: remove once the deprecation warning is removed
def testAddLineArgumentOrder(self):
x = common.Marker(common.Marker.x, '0000ff', 5)
# Deprecated approach
chart = line_chart.LineChart()
warnings.filterwarnings("error")
self.assertRaises(DeprecationWarning, chart.AddLine, [1, 2, 3],
'label', [x], 'color')
# New order
chart = line_chart.LineChart()
chart.AddLine([1, 2, 3], 'label', 'color', markers=[x])
self.assertEqual('label', chart.data[0].label)
self.assertEqual([x], chart.data[0].markers)
self.assertEqual('color', chart.data[0].style.color)
class LineStyleTest(graphy_test.GraphyTest):
def testPresets(self):
"""Test selected traits from the preset line styles."""
self.assertEqual(0, line_chart.LineStyle.solid.off)
self.assert_(line_chart.LineStyle.dashed.off > 0)
self.assert_(line_chart.LineStyle.solid.width <
line_chart.LineStyle.thick_solid.width)
if __name__ == '__main__':
graphy_test.main()
graphy-1.0+dfsg/graphy/formatters_test.py 0000644 0000000 0000000 00000006626 11203124512 015543 0 ustar #!/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.
"""Tests for the formatters."""
from graphy import common
from graphy import formatters
from graphy import graphy_test
from graphy.backends import google_chart_api
class InlineLegendTest(graphy_test.GraphyTest):
def setUp(self):
self.chart = google_chart_api.LineChart()
self.chart.formatters.append(formatters.InlineLegend)
self.chart.AddLine([1, 2, 3], label='A')
self.chart.AddLine([4, 5, 6], label='B')
self.chart.auto_scale.buffer = 0
def testLabelsAdded(self):
self.assertEqual(self.Param('chxl'), '0:|A|B')
def testLabelPositionedCorrectly(self):
self.assertEqual(self.Param('chxp'), '0,3,6')
self.assertEqual(self.Param('chxr'), '0,1,6')
def testRegularLegendSuppressed(self):
self.assertRaises(KeyError, self.Param, 'chdl')
class AutoScaleTest(graphy_test.GraphyTest):
def setUp(self):
self.chart = google_chart_api.LineChart([1, 2, 3])
self.auto_scale = formatters.AutoScale(buffer=0)
def testNormalCase(self):
self.auto_scale(self.chart)
self.assertEqual(1, self.chart.left.min)
self.assertEqual(3, self.chart.left.max)
def testKeepsDataAwayFromEdgesByDefault(self):
self.auto_scale = formatters.AutoScale()
self.auto_scale(self.chart)
self.assertTrue(1 > self.chart.left.min)
self.assertTrue(3 < self.chart.left.max)
def testDoNothingIfNoData(self):
self.chart.data = []
self.auto_scale(self.chart)
self.assertEqual(None, self.chart.left.min)
self.assertEqual(None, self.chart.left.max)
self.chart.AddLine([])
self.auto_scale(self.chart)
self.assertEqual(None, self.chart.left.min)
self.assertEqual(None, self.chart.left.max)
def testKeepMinIfSet(self):
self.chart.left.min = -10
self.auto_scale(self.chart)
self.assertEqual(-10, self.chart.left.min)
self.assertEqual(3, self.chart.left.max)
def testKeepMaxIfSet(self):
self.chart.left.max = 9
self.auto_scale(self.chart)
self.assertEqual(1, self.chart.left.min)
self.assertEqual(9, self.chart.left.max)
def testOtherDependentAxesAreAlsoSet(self):
self.chart.AddAxis(common.AxisPosition.LEFT, common.Axis())
self.chart.AddAxis(common.AxisPosition.RIGHT, common.Axis())
self.assertEqual(4, len(self.chart.GetDependentAxes()))
self.auto_scale(self.chart)
for axis in self.chart.GetDependentAxes():
self.assertEqual(1, axis.min)
self.assertEqual(3, axis.max)
def testRightSetsLeft(self):
"""If user sets min/max on right but NOT left, they are copied to left.
(Otherwise the data will be scaled differently from the right-axis labels,
which is bad).
"""
self.chart.right.min = 18
self.chart.right.max = 19
self.auto_scale(self.chart)
self.assertEqual(18, self.chart.left.min)
self.assertEqual(19, self.chart.left.max)
if __name__ == '__main__':
graphy_test.main()
graphy-1.0+dfsg/graphy/common.py 0000644 0000000 0000000 00000034420 11203124512 013577 0 ustar #!/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
from graphy import util
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
class _BasicStyle(object):
"""Basic style object. Used internally."""
def __init__(self, color):
self.color = color
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).
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.
style: A chart-type-specific style object. (LineStyle for LineChart,
BarsStyle for BarChart, etc.)
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).
color: DEPRECATED
"""
# TODO: Should we require the points list to be non-empty ?
# TODO: Do markers belong here? They are really only used for LineCharts
def __init__(self, points, label=None, style=None, markers=None, color=None):
"""Construct a DataSeries. See class docstring for details on args."""
if label is not None and util._IsColor(label):
warnings.warn('Your code may be broken! Label is a hex triplet. Maybe '
'it is a color? The old argument order (color & style '
'before label) is deprecated.', DeprecationWarning,
stacklevel=2)
if color is not None:
warnings.warn('Passing color is deprecated. Pass a style object '
'instead.', DeprecationWarning, stacklevel=2)
# Attempt to fix it for them. If they also passed a style, honor it.
if style is None:
style = _BasicStyle(color)
if style is not None and isinstance(style, basestring):
warnings.warn('Your code is broken! Style is a string, not an object. '
'Maybe you are passing a color? Passing color is '
'deprecated; pass a style object instead.',
DeprecationWarning, stacklevel=2)
if style is None:
style = _BasicStyle(None)
self.data = points
self.style = style
self.markers = markers or []
self.label = label
def _GetColor(self):
warnings.warn('DataSeries.color is deprecated, use '
'DataSeries.style.color instead.', DeprecationWarning,
stacklevel=2)
return self.style.color
def _SetColor(self, color):
warnings.warn('DataSeries.color is deprecated, use '
'DataSeries.style.color instead.', DeprecationWarning,
stacklevel=2)
self.style.color = color
color = property(_GetColor, _SetColor)
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=color, style=style, markers=markers,
label=label)
self.data.append(series)
return series
def GetDependentAxes(self):
"""Return any dependent axes ('left' and 'right' by default for LineCharts,
although bar charts would use 'bottom' and 'top').
"""
return self._axes[AxisPosition.LEFT] + self._axes[AxisPosition.RIGHT]
def GetIndependentAxes(self):
"""Return any independent axes (normally top & bottom, although horizontal
bar charts use left & right by default).
"""
return self._axes[AxisPosition.TOP] + self._axes[AxisPosition.BOTTOM]
def GetDependentAxis(self):
"""Return this chart's main dependent axis (often 'left', but
horizontal bar-charts use 'bottom').
"""
return self.left
def GetIndependentAxis(self):
"""Return this chart's main 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""")
graphy-1.0+dfsg/graphy/__init__.py 0000644 0000000 0000000 00000000022 11203124512 014035 0 ustar __version__='1.0'
graphy-1.0+dfsg/graphy/pie_chart.py 0000644 0000000 0000000 00000014074 11203124512 014250 0 ustar #!/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
from graphy import util
class Segment(common.DataSeries):
"""A single segment of the pie chart.
Object attributes:
size: relative size of the segment
label: label of the segment (if any)
color: color of the segment (if any)
"""
def __init__(self, size, label=None, color=None):
if label is not None and util._IsColor(label):
warnings.warn('Your code may be broken! '
'Label looks like a hex triplet; it might be a color. '
'The old argument order (color before label) is '
'deprecated.',
DeprecationWarning, stacklevel=2)
style = common._BasicStyle(color)
super(Segment, self).__init__([size], label=label, style=style)
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.""")
# Since Segments are so simple, provide color for convenience.
def _GetColor(self):
return self.style.color
def _SetColor(self, color):
self.style.color = color
color = property(_GetColor, _SetColor,
doc = """The color of this pie segment.""")
class PieChart(common.BaseChart):
"""Represents a pie chart.
The pie chart consists of a single "pie" by default, but additional pies
may be added using the AddPie method. The Google Chart API will display
the pies as concentric circles, with pie #0 on the inside; other backends
may display the pies differently.
"""
def __init__(self, points=None, labels=None, colors=None):
"""Constructor for PieChart objects.
Creates a pie chart with a single pie.
Args:
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). If there are less colors than pie
segments, the Google Chart API will attempt to produce a smooth
color transition between segments by spreading the colors across
them.
"""
super(PieChart, self).__init__()
self.formatters = []
self._colors = None
if points:
self.AddPie(points, labels, colors)
def AddPie(self, points, labels=None, colors=None):
"""Add a whole pie to the chart.
Args:
points: A list of pie segment sizes
labels: A list of labels for the pie segments
colors: A list of colors for the segments. Missing colors will be chosen
automatically.
Return:
The index of the newly added pie.
"""
num_colors = len(colors or [])
num_labels = len(labels or [])
pie_index = len(self.data)
self.data.append([])
for i, pt in enumerate(points):
label = None
if i < num_labels:
label = labels[i]
color = None
if i < num_colors:
color = colors[i]
self.AddSegment(pt, label=label, color=color, pie_index=pie_index)
return pie_index
def AddSegments(self, points, labels, colors):
"""DEPRECATED."""
warnings.warn('PieChart.AddSegments is deprecated. Call AddPie instead. ',
DeprecationWarning, stacklevel=2)
num_colors = len(colors or [])
for i, pt in enumerate(points):
assert pt >= 0
label = labels[i]
color = None
if i < num_colors:
color = colors[i]
self.AddSegment(pt, label=label, color=color)
def AddSegment(self, size, label=None, color=None, pie_index=0):
"""Add a pie segment to this chart, and return the segment.
size: The size of the segment.
label: The label for the segment.
color: The color of the segment, or None to automatically choose the color.
pie_index: The index of the pie that will receive the new segment.
By default, the chart has one pie (pie #0); use the AddPie method to
add more pies.
"""
if isinstance(size, Segment):
warnings.warn("AddSegment(segment) is deprecated. Use AddSegment(size, "
"label, color) instead", DeprecationWarning, stacklevel=2)
segment = size
else:
segment = Segment(size, label=label, color=color)
assert segment.size >= 0
if pie_index == 0 and not self.data:
# Create the default pie
self.data.append([])
assert (pie_index >= 0 and pie_index < len(self.data))
self.data[pie_index].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=color, label=label))
def SetColors(self, *colors):
"""Change the colors of this chart to the specified list of colors.
Note that this will completely override the individual colors specified
in the pie segments. Missing colors will be interpolated, so that the
list of colors covers all segments in all the pies.
"""
self._colors = colors
graphy-1.0+dfsg/graphy/bar_chart_test.py 0000644 0000000 0000000 00000005476 11203124512 015304 0 ustar #!/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.
"""Tests for bar_chart.py."""
import warnings
from graphy import common
from graphy import bar_chart
from graphy import graphy_test
from graphy.backends import google_chart_api
class BarChartTest(graphy_test.GraphyTest):
def setUp(self):
self.chart = google_chart_api.BarChart()
def tearDown(self):
warnings.resetwarnings()
# TODO: remove once the deprecation warning is removed
def testBarStyleStillExists(self):
warnings.filterwarnings('ignore')
x = bar_chart.BarStyle(None, None, None)
# TODO: remove once the deprecation warning is removed
def testAddBarArgumentOrder(self):
# Deprecated approach
chart = bar_chart.BarChart()
warnings.filterwarnings('error')
self.assertRaises(DeprecationWarning, chart.AddBars, [1, 2, 3],
'0000FF', 'label')
# New order
chart = bar_chart.BarChart()
chart.AddBars([1, 2, 3], 'label', '0000FF')
self.assertEqual('label', chart.data[0].label)
self.assertEqual('0000FF', chart.data[0].style.color)
def testGetDependentIndependentAxes(self):
c = self.chart
c.vertical = True
self.assertEqual([c.left, c.right], c.GetDependentAxes())
self.assertEqual([c.top, c.bottom], c.GetIndependentAxes())
c.vertical = False
self.assertEqual([c.top, c.bottom], c.GetDependentAxes())
self.assertEqual([c.left, c.right], c.GetIndependentAxes())
right2 = c.AddAxis(common.AxisPosition.RIGHT, common.Axis())
bottom2 = c.AddAxis(common.AxisPosition.BOTTOM, common.Axis())
c.vertical = True
self.assertEqual([c.left, c.right, right2], c.GetDependentAxes())
self.assertEqual([c.top, c.bottom, bottom2], c.GetIndependentAxes())
c.vertical = False
self.assertEqual([c.top, c.bottom, bottom2], c.GetDependentAxes())
self.assertEqual([c.left, c.right, right2], c.GetIndependentAxes())
def testDependentIndependentAxis(self):
self.chart.vertical = True
self.assertTrue(self.chart.left is self.chart.GetDependentAxis())
self.assertTrue(self.chart.bottom is self.chart.GetIndependentAxis())
self.chart.vertical = False
self.assertTrue(self.chart.bottom, self.chart.GetDependentAxis())
self.assertTrue(self.chart.left, self.chart.GetIndependentAxis())
if __name__ == '__main__':
graphy_test.main()
graphy-1.0+dfsg/graphy/formatters.py 0000644 0000000 0000000 00000015264 11203124512 014502 0 ustar #!/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.style.color is None:
index += 1
if index >= len(self.colors):
index = 0
series.style.color = self.colors[index]
class AutoScale(object):
"""If you don't set min/max on the dependent axes, this fills them in
automatically by calculating min/max dynamically from the data.
You can set just min or just max and this formatter will fill in the other
value for you automatically. For example, if you only set min then this will
set max automatically, but leave min untouched.
Charts can have multiple dependent axes (chart.left & chart.right, for
example.) If you set min/max on some axes but not others, then this formatter
copies your min/max to the un-set axes. For example, if you set up min/max on
only the right axis then your values will be automatically copied to the left
axis. (if you use different min/max values for different axes, the
precendence is undefined. So don't do that.)
"""
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.
# Honor user's choice, if they've picked min/max.
for axis in chart.GetDependentAxes():
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.
for axis in chart.GetDependentAxes():
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.
graphy-1.0+dfsg/graphy/bar_chart.py 0000644 0000000 0000000 00000013211 11203124512 014227 0 ustar #!/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
import warnings
from graphy import common
from graphy import util
class BarsStyle(object):
"""Style of a series of bars in a BarChart
Object Attributes:
color: Hex string, like '00ff00' for green
"""
def __init__(self, color):
self.color = color
class BarChartStyle(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, or as a fraction of bar thickness
if use_fractional_gap_spacing is True.
group_gap: The gap between groups of bars, in pixels, or as a fraction of
bar thickness if use_fractional_gap_spacing is True.
use_fractional_gap_spacing: if True, bar_gap and group_gap specify gap
sizes as a fraction of bar width. Default is False.
"""
_DEFAULT_GROUP_GAP = 8
_DEFAULT_BAR_GAP = 4
def __init__(self, bar_thickness=None,
bar_gap=_DEFAULT_BAR_GAP, group_gap=_DEFAULT_GROUP_GAP,
use_fractional_gap_spacing=False):
"""Create a new BarChartStyle.
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
self.use_fractional_gap_spacing = use_fractional_gap_spacing
class BarStyle(BarChartStyle):
def __init__(self, *args, **kwargs):
warnings.warn('BarStyle is deprecated. Use BarChartStyle.',
DeprecationWarning, stacklevel=2)
super(BarStyle, self).__init__(*args, **kwargs)
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.
style: The BarChartStyle for all bars on this chart, specifying bar
thickness and gaps between bars.
"""
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
self.style = BarChartStyle(None, None, None) # full auto
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.
"""
if label is not None and util._IsColor(label):
warnings.warn('Your code may be broken! '
'Label is a hex triplet. Maybe it is a color? The '
'old argument order (color before label) is deprecated.',
DeprecationWarning, stacklevel=2)
style = BarsStyle(color)
series = common.DataSeries(points, label=label, style=style)
self.data.append(series)
return series
def GetDependentAxes(self):
"""Get the dependendant axes, which depend on orientation."""
if self.vertical:
return (self._axes[common.AxisPosition.LEFT] +
self._axes[common.AxisPosition.RIGHT])
else:
return (self._axes[common.AxisPosition.TOP] +
self._axes[common.AxisPosition.BOTTOM])
def GetIndependentAxes(self):
"""Get the independendant axes, which depend on orientation."""
if self.vertical:
return (self._axes[common.AxisPosition.TOP] +
self._axes[common.AxisPosition.BOTTOM])
else:
return (self._axes[common.AxisPosition.LEFT] +
self._axes[common.AxisPosition.RIGHT])
def GetDependentAxis(self):
"""Get the main dependendant axis, which depends on orientation."""
if self.vertical:
return self.left
else:
return self.bottom
def GetIndependentAxis(self):
"""Get the main 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
graphy-1.0+dfsg/graphy/backends/ 0000755 0000000 0000000 00000000000 11320654667 013527 5 ustar graphy-1.0+dfsg/graphy/backends/google_chart_api/ 0000755 0000000 0000000 00000000000 11320654667 017015 5 ustar graphy-1.0+dfsg/graphy/backends/google_chart_api/util_test.py 0000644 0000000 0000000 00000011271 11203124512 021362 0 ustar #!/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.
"""Unittest for Graphy and Google Chart API backend."""
import string
import unittest
from graphy import graphy_test
from graphy.backends.google_chart_api import util
class SimpleEncoderTest(graphy_test.GraphyTest):
def setUp(self):
self.simple = util.SimpleDataEncoder()
def testEmpty(self):
self.assertEqual('', self.simple.Encode([]))
def testSingle(self):
self.assertEqual('A', self.simple.Encode([0]))
def testFull(self):
full = string.ascii_uppercase + string.ascii_lowercase + string.digits
self.assertEqual(full, self.simple.Encode(range(0, 62)))
def testRoundingError(self):
"""Scaling might give us some rounding error. Make sure that the encoder
deals with it properly.
"""
a = [-1, 0, 0, 1, 60, 61, 61, 62]
b = [-0.999999, -0.00001, 0.00001, 0.99998,
60.00001, 60.99999, 61.00001, 61.99998]
self.assertEqual(self.simple.Encode(a), self.simple.Encode(b))
def testFloats(self):
ints = [1, 2, 3, 4]
floats = [1.1, 2.1, 3.1, 4.1]
self.assertEqual(self.simple.Encode(ints), self.simple.Encode(floats))
def testOutOfRangeDropped(self):
"""Confirm that values outside of min/max are left blank."""
nums = [-79, -1, 0, 1, 61, 62, 1012]
self.assertEqual('__AB9__', self.simple.Encode(nums))
def testNoneDropped(self):
"""Confirm that the value None is left blank."""
self.assertEqual('_JI_H', self.simple.Encode([None, 9, 8, None, 7]))
class EnhandedEncoderTest(graphy_test.GraphyTest):
def setUp(self):
self.encoder = util.EnhancedDataEncoder()
def testEmpty(self):
self.assertEqual('', self.encoder.Encode([]))
def testFull(self):
full = ''.join(self.encoder.code)
self.assertEqual(full, self.encoder.Encode(range(0, 4096)))
def testOutOfRangeDropped(self):
nums = [-79, -1, 0, 1, 61, 4096, 10012]
self.assertEqual('____AAABA9____', self.encoder.Encode(nums))
def testNoneDropped(self):
self.assertEqual('__AJAI__AH', self.encoder.Encode([None, 9, 8, None, 7]))
class ScaleTest(graphy_test.GraphyTest):
"""Test scaling."""
def testScaleIntegerData(self):
scale = util.ScaleData
# Identity
self.assertEqual([1, 2, 3], scale([1, 2, 3], 1, 3, 1, 3))
self.assertEqual([-1, 0, 1], scale([-1, 0, 1], -1, 1, -1, 1))
# Translate
self.assertEqual([4, 5, 6], scale([1, 2, 3], 1, 3, 4, 6))
self.assertEqual([-3, -2, -1], scale([1, 2, 3], 1, 3, -3, -1))
# Scale
self.assertEqual([1, 3.5, 6], scale([1, 2, 3], 1, 3, 1, 6))
self.assertEqual([-6, 0, 6], scale([1, 2, 3], 1, 3, -6, 6))
# Scale and Translate
self.assertEqual([100, 200, 300], scale([1, 2, 3], 1, 3, 100, 300))
def testScaleDataWithDifferentMinMax(self):
scale = util.ScaleData
self.assertEqual([1.5, 2, 2.5], scale([1, 2, 3], 0, 4, 1, 3))
self.assertEqual([-2, 2, 6], scale([0, 2, 4], 1, 3, 0, 4))
def testScaleFloatingPointData(self):
scale = util.ScaleData
data = [-3.14, -2.72, 0, 2.72, 3.14]
scaled_e = 5 + 5 * 2.72 / 3.14
expected_data = [0, 10 - scaled_e, 5, scaled_e, 10]
actual_data = scale(data, -3.14, 3.14, 0, 10)
for expected, actual in zip(expected_data, actual_data):
self.assertAlmostEqual(expected, actual)
def testScaleDataOverRealRange(self):
scale = util.ScaleData
self.assertEqual([0, 30.5, 61], scale([1, 2, 3], 1, 3, 0, 61))
def testScalingLotsOfData(self):
data = range(0, 100)
expected = range(-100, 100, 2)
actual = util.ScaleData(data, 0, 100, -100, 100)
self.assertEqual(expected, actual)
class NameTest(graphy_test.GraphyTest):
"""Test long/short parameter names."""
def testLongNames(self):
params = dict(size='S', data='D', chg='G')
params = util.ShortenParameterNames(params)
self.assertEqual(dict(chs='S', chd='D', chg='G'), params)
def testCantUseBothLongAndShortName(self):
"""Make sure we don't let the user specify both the long and the short
version of a parameter. (If we did, which one would we pick?)
"""
params = dict(size='long', chs='short')
self.assertRaises(KeyError, util.ShortenParameterNames, params)
if __name__ == '__main__':
unittest.main()
graphy-1.0+dfsg/graphy/backends/google_chart_api/pie_chart_test.py 0000644 0000000 0000000 00000013404 11203124512 022343 0 ustar #!/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.
"""Unittest for Graphy and Google Chart API backend."""
import warnings
from graphy import graphy_test
from graphy import pie_chart
from graphy.backends import google_chart_api
from graphy.backends.google_chart_api import base_encoder_test
# Extend BaseChartTest so that we pick up & repeat all the line tests which
# Pie Charts should continue to satisfy
class PieChartTest(base_encoder_test.BaseChartTest):
def tearDown(self):
warnings.resetwarnings()
super(PieChartTest, self).tearDown()
def GetChart(self, *args, **kwargs):
return google_chart_api.PieChart(*args, **kwargs)
def AddToChart(self, chart, points, color=None, label=None):
return chart.AddSegment(points[0], color=color, label=label)
def testCanRemoveDefaultFormatters(self):
# Override this test, as pie charts don't have default formatters.
pass
def testChartType(self):
self.chart.display.is3d = False
self.assertEqual(self.Param('cht'), 'p')
self.chart.display.is3d = True
self.assertEqual(self.Param('cht'), 'p3')
def testEmptyChart(self):
self.assertEqual(self.Param('chd'), 's:')
self.assertEqual(self.Param('chco'), '')
self.assertEqual(self.Param('chl'), '')
def testChartCreation(self):
self.chart = self.GetChart([1,2,3], ['Mouse', 'Cat', 'Dog'])
self.assertEqual(self.Param('chd'), 's:Up9')
self.assertEqual(self.Param('chl'), 'Mouse|Cat|Dog')
self.assertEqual(self.Param('cht'), 'p')
# TODO: Get 'None' labels to work and test them
def testAddSegment(self):
self.chart = self.GetChart([1,2,3], ['Mouse', 'Cat', 'Dog'])
self.chart.AddSegment(4, label='Horse')
self.assertEqual(self.Param('chd'), 's:Pfu9')
self.assertEqual(self.Param('chl'), 'Mouse|Cat|Dog|Horse')
# TODO: Remove this when AddSegments is removed
def testAddMultipleSegments(self):
warnings.filterwarnings('ignore')
self.chart.AddSegments([1,2,3],
['Mouse', 'Cat', 'Dog'],
['ff0000', '00ff00', '0000ff'])
self.assertEqual(self.Param('chd'), 's:Up9')
self.assertEqual(self.Param('chl'), 'Mouse|Cat|Dog')
self.assertEqual(self.Param('chco'), 'ff0000,00ff00,0000ff')
# skip two colors
self.chart.AddSegments([4,5,6], ['Horse', 'Moose', 'Elephant'], ['cccccc'])
self.assertEqual(self.Param('chd'), 's:KUfpz9')
self.assertEqual(self.Param('chl'), 'Mouse|Cat|Dog|Horse|Moose|Elephant')
self.assertEqual(self.Param('chco'), 'ff0000,00ff00,0000ff,cccccc')
def testMultiplePies(self):
self.chart.AddPie([1,2,3],
['Mouse', 'Cat', 'Dog'],
['ff0000', '00ff00', '0000ff'])
self.assertEqual(self.Param('chd'), 's:Up9')
self.assertEqual(self.Param('chl'), 'Mouse|Cat|Dog')
self.assertEqual(self.Param('chco'), 'ff0000,00ff00,0000ff')
self.assertEqual(self.Param('cht'), 'p')
# skip two colors
self.chart.AddPie([4,5,6], ['Horse', 'Moose', 'Elephant'], ['cccccc'])
self.assertEqual(self.Param('chd'), 's:KUf,pz9')
self.assertEqual(self.Param('chl'), 'Mouse|Cat|Dog|Horse|Moose|Elephant')
self.assertEqual(self.Param('chco'), 'ff0000,00ff00,0000ff,cccccc')
self.assertEqual(self.Param('cht'), 'pc')
def testMultiplePiesNo3d(self):
chart = self.GetChart([1,2,3], ['Mouse', 'Cat', 'Dog'])
chart.AddPie([4,5,6], ['Horse', 'Moose', 'Elephant'])
chart.display.is3d = True
warnings.filterwarnings('error')
self.assertRaises(RuntimeWarning, chart.display.Url, 320, 240)
def testAddSegmentByIndex(self):
self.chart = self.GetChart([1,2,3], ['Mouse', 'Cat', 'Dog'])
self.chart.AddSegment(4, 'Horse', pie_index=0)
self.assertEqual(self.Param('chd'), 's:Pfu9')
self.assertEqual(self.Param('chl'), 'Mouse|Cat|Dog|Horse')
self.chart.AddPie([4,5], ['Apple', 'Orange'], [])
self.chart.AddSegment(6, 'Watermelon', pie_index=1)
self.assertEqual(self.Param('chd'), 's:KUfp,pz9')
def testSetColors(self):
self.assertEqual(self.Param('chco'), '')
self.chart.AddSegment(1, label='Mouse')
self.chart.AddSegment(5, label='Moose')
self.chart.SetColors('000033', '0000ff')
self.assertEqual(self.Param('chco'), '000033,0000ff')
self.chart.AddSegment(6, label='Elephant')
self.assertEqual(self.Param('chco'), '000033,0000ff')
def testHugeSegmentSizes(self):
self.chart = self.GetChart([1000000000000000L,3000000000000000L],
['Big', 'Uber'])
self.assertEqual(self.Param('chd'), 's:U9')
self.chart.display.enhanced_encoding = True
self.assertEqual(self.Param('chd'), 'e:VV..')
def testSetSegmentSize(self):
segment1 = self.chart.AddSegment(1)
segment2 = self.chart.AddSegment(2)
self.assertEqual(self.Param('chd'), 's:f9')
segment2.size = 3
self.assertEquals(segment1.size, 1)
self.assertEquals(segment2.size, 3)
self.assertEqual(self.Param('chd'), 's:U9')
def testChartAngle(self):
self.assertTrue('chp' not in self.chart.display._Params(self.chart))
self.chart.display.angle = 3.1415
self.assertEqual(self.Param('chp'), '3.1415')
self.chart.display.angle = 0
self.assertTrue('chp' not in self.chart.display._Params(self.chart))
if __name__ == '__main__':
graphy_test.main()
graphy-1.0+dfsg/graphy/backends/google_chart_api/base_encoder_test.py 0000644 0000000 0000000 00000054057 11203124512 023027 0 ustar #!/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.
"""Test for the base encoder. Also serves as a base class for the
chart-type-specific tests."""
from graphy import common
from graphy import graphy_test
from graphy import formatters
from graphy.backends.google_chart_api import encoders
from graphy.backends.google_chart_api import util
class TestEncoder(encoders.BaseChartEncoder):
"""Simple implementation of BaseChartEncoder for testing common behavior."""
def _GetType(self, chart):
return {'chart_type': 'TEST_TYPE'}
def _GetDependentAxis(self, chart):
return chart.left
class TestChart(common.BaseChart):
"""Simple implementation of BaseChart for testing common behavior."""
def __init__(self, points=None):
super(TestChart, self).__init__()
if points is not None:
self.AddData(points)
def AddData(self, points, color=None, label=None):
style = common._BasicStyle(color)
series = common.DataSeries(points, style=style, label=label)
self.data.append(series)
return series
class BaseChartTest(graphy_test.GraphyTest):
"""Base class for all chart-specific tests"""
def ExpectAxes(self, labels, positions):
"""Helper to test that the chart axis spec matches the expected values."""
self.assertEqual(self.Param('chxl'), labels)
self.assertEqual(self.Param('chxp'), positions)
def GetChart(self, *args, **kwargs):
"""Get a chart object. Other classes can override to change the
type of chart being tested.
"""
chart = TestChart(*args, **kwargs)
chart.display = TestEncoder(chart)
return chart
def AddToChart(self, chart, points, color=None, label=None):
"""Add data to the chart.
Chart is assumed to be of the same type as returned by self.GetChart().
"""
return chart.AddData(points, color=color, label=label)
def setUp(self):
self.chart = self.GetChart()
def testImgAndUrlUseSameUrl(self):
"""Check that Img() and Url() return the same URL."""
self.assertIn(self.chart.display.Url(500, 100, use_html_entities=True),
self.chart.display.Img(500, 100))
def testImgUsesHtmlEntitiesInUrl(self):
img_tag = self.chart.display.Img(500, 100)
self.assertNotIn('&ch', img_tag)
self.assertIn('&ch', img_tag)
def testParamsAreStrings(self):
"""Test that params are all converted to strings."""
self.chart.display.extra_params['test'] = 32
self.assertEqual(self.Param('test'), '32')
def testExtraParamsOverideDefaults(self):
self.assertNotEqual(self.Param('cht'), 'test') # Sanity check.
self.chart.display.extra_params['cht'] = 'test'
self.assertEqual(self.Param('cht'), 'test')
def testExtraParamsCanUseLongNames(self):
self.chart.display.extra_params['color'] = 'XYZ'
self.assertEqual(self.Param('chco'), 'XYZ')
def testExtraParamsCanUseNewNames(self):
"""Make sure future Google Chart API features can be accessed immediately
through extra_params. (Double-checks that the long-to-short name
conversion doesn't mess up the ability to use new features).
"""
self.chart.display.extra_params['fancy_new_feature'] = 'shiny'
self.assertEqual(self.Param('fancy_new_feature'), 'shiny')
def testEmptyParamsDropped(self):
"""Check that empty parameters don't end up in the URL."""
self.assertEqual(self.Param('chxt'), '')
self.assertNotIn('chxt', self.chart.display.Url(0, 0))
def testSizes(self):
self.assertIn('89x102', self.chart.display.Url(89, 102))
img = self.chart.display.Img(89, 102)
self.assertIn('chs=89x102', img)
self.assertIn('width="89"', img)
self.assertIn('height="102"', img)
def testChartType(self):
self.assertEqual(self.Param('cht'), 'TEST_TYPE')
def testChartSizeConvertedToInt(self):
url = self.chart.display.Url(100.1, 200.2)
self.assertIn('100x200', url)
def testUrlBase(self):
def assertStartsWith(actual_text, expected_start):
message = "[%s] didn't start with [%s]" % (actual_text, expected_start)
self.assert_(actual_text.startswith(expected_start), message)
assertStartsWith(self.chart.display.Url(0, 0),
'http://chart.apis.google.com/chart')
url_base = 'http://example.com/charts'
self.chart.display.url_base = url_base
assertStartsWith(self.chart.display.Url(0, 0), url_base)
def testEnhancedEncoder(self):
self.chart.display.enhanced_encoding = True
self.assertEqual(self.Param('chd'), 'e:')
def testUrlsEscaped(self):
self.AddToChart(self.chart, [1, 2, 3])
url = self.chart.display.Url(500, 100)
self.assertNotIn('chd=s:', url)
self.assertIn('chd=s%3A', url)
def testUrls_DefaultIsWithoutHtmlEntities(self):
self.AddToChart(self.chart, [1, 2, 3])
self.AddToChart(self.chart, [1, 2, 3], label='Ciao&"Mario>Luigi"')
url_default = self.chart.display.Url(500, 100)
url_forced = self.chart.display.Url(500, 100, use_html_entities=False)
self.assertEqual(url_forced, url_default)
def testUrls_HtmlEntities(self):
self.AddToChart(self.chart, [1, 2, 3])
self.AddToChart(self.chart, [1, 2, 3], label='Ciao&"Mario>Luigi"')
url = self.chart.display.Url(500, 100, use_html_entities=True)
self.assertNotIn('&ch', url)
self.assertIn('&ch', url)
self.assertIn('%7CCiao%26%22Mario%3ELuigi%22', url)
def testUrls_NoEscapeWithHtmlEntities(self):
self.AddToChart(self.chart, [1, 2, 3])
self.AddToChart(self.chart, [1, 2, 3], label='Ciao&"Mario>Luigi"')
self.chart.display.escape_url = False
url = self.chart.display.Url(500, 100, use_html_entities=True)
self.assertNotIn('&ch', url)
self.assertIn('&ch', url)
self.assertIn('Ciao&"Mario>Luigi"', url)
def testUrls_NoHtmlEntities(self):
self.AddToChart(self.chart, [1, 2, 3])
self.AddToChart(self.chart, [1, 2, 3], label='Ciao&"Mario>Luigi"')
url = self.chart.display.Url(500, 100, use_html_entities=False)
self.assertIn('&ch', url)
self.assertNotIn('&ch', url)
self.assertIn('%7CCiao%26%22Mario%3ELuigi%22', url)
def testCanRemoveDefaultFormatters(self):
self.assertEqual(3, len(self.chart.formatters))
# I don't know why you'd want to remove the default formatters like this.
# It is just a proof that we can manipulate the default formatters
# through their aliases.
self.chart.formatters.remove(self.chart.auto_color)
self.chart.formatters.remove(self.chart.auto_legend)
self.chart.formatters.remove(self.chart.auto_scale)
self.assertEqual(0, len(self.chart.formatters))
def testFormattersWorkOnCopy(self):
"""Make sure formatters can't modify the user's chart."""
self.AddToChart(self.chart, [1])
# By making sure our point is at the upper boundry, we make sure that both
# line, pie, & bar charts encode it as a '9' in the simple encoding.
self.chart.left.max = 1
self.chart.left.min = 0
# Sanity checks before adding a formatter.
self.assertEqual(self.Param('chd'), 's:9')
self.assertEqual(len(self.chart.data), 1)
def MaliciousFormatter(chart):
chart.data.pop() # Modify a mutable chart attribute
self.chart.AddFormatter(MaliciousFormatter)
self.assertEqual(self.Param('chd'), 's:', "Formatter wasn't used.")
self.assertEqual(len(self.chart.data), 1,
"Formatter was able to modify original chart.")
self.chart.formatters.remove(MaliciousFormatter)
self.assertEqual(self.Param('chd'), 's:9',
"Chart changed even after removing the formatter")
class XYChartTest(BaseChartTest):
"""Base class for charts that display lines or points in 2d.
Pretty much anything but the pie chart.
"""
def testImgAndUrlUseSameUrl(self):
"""Check that Img() and Url() return the same URL."""
super(XYChartTest, self).testImgAndUrlUseSameUrl()
self.AddToChart(self.chart, range(0, 100))
self.assertIn(self.chart.display.Url(500, 100, use_html_entities=True),
self.chart.display.Img(500, 100))
self.chart = self.GetChart([-1, 0, 1])
self.assertIn(self.chart.display.Url(500, 100, use_html_entities=True),
self.chart.display.Img(500, 100))
# TODO: Once the deprecated AddSeries is removed, revisit
# whether we need this test.
def testAddSeries(self):
self.chart.auto_scale.buffer = 0 # Buffer causes trouble for testing.
self.assertEqual(self.Param('chd'), 's:')
self.AddToChart(self.chart, (1, 2, 3))
self.assertEqual(self.Param('chd'), 's:Af9')
self.AddToChart(self.chart, (4, 5, 6))
self.assertEqual(self.Param('chd'), 's:AMY,lx9')
# TODO: Once the deprecated AddSeries is removed, revisit
# whether we need this test.
def testAddSeriesReturnsValue(self):
points = (1, 2, 3)
series = self.AddToChart(self.chart, points, '#000000')
self.assertTrue(series is not None)
self.assertEqual(series.data, points)
self.assertEqual(series.style.color, '#000000')
def testFlatSeries(self):
"""Make sure we handle scaling of a flat data series correctly (there are
div by zero issues).
"""
self.AddToChart(self.chart, [5, 5, 5])
self.assertEqual(self.Param('chd'), 's:AAA')
self.chart.left.min = 0
self.chart.left.max = 5
self.assertEqual(self.Param('chd'), 's:999')
self.chart.left.min = 5
self.chart.left.max = 15
self.assertEqual(self.Param('chd'), 's:AAA')
def testEmptyPointsStillCreatesSeries(self):
"""If we pass an empty list for points, we expect to get an empty data
series, not nothing. This way we can add data points later."""
chart = self.GetChart()
self.assertEqual(0, len(chart.data))
data = []
chart = self.GetChart(data)
self.assertEqual(1, len(chart.data))
self.assertEqual(0, len(chart.data[0].data))
# This is the use case we are trying to serve: adding points later.
data.append(0)
self.assertEqual(1, len(chart.data[0].data))
def testEmptySeriesDroppedFromParams(self):
"""By the time we make parameters, we don't want empty series to be
included because it will mess up the indexes of other things like colors
and makers. They should be dropped instead."""
self.chart.auto_scale.buffer = 0
# Check just an empty series.
self.AddToChart(self.chart, [], color='eeeeee')
self.assertEqual(self.Param('chd'), 's:')
# Now check when there are some real series in there too.
self.AddToChart(self.chart, [1], color='111111')
self.AddToChart(self.chart, [], color='FFFFFF')
self.AddToChart(self.chart, [2], color='222222')
self.assertEqual(self.Param('chd'), 's:A,9')
self.assertEqual(self.Param('chco'), '111111,222222')
def testDataSeriesCorrectlyConverted(self):
# To avoid problems caused by floating-point errors, the input in this test
# is carefully chosen to avoid 0.5 boundries (1.5, 2.5, 3.5, ...).
chart = self.GetChart()
chart.auto_scale.buffer = 0 # The buffer makes testing difficult.
self.assertEqual(self.Param('chd', chart), 's:')
chart = self.GetChart(range(0, 10))
chart.auto_scale.buffer = 0
self.assertEqual(self.Param('chd', chart), 's:AHOUbipv29')
chart = self.GetChart(range(-10, 0))
chart.auto_scale.buffer = 0
self.assertEqual(self.Param('chd', chart), 's:AHOUbipv29')
chart = self.GetChart((-1.1, 0.0, 1.1, 2.2))
chart.auto_scale.buffer = 0
self.assertEqual(self.Param('chd', chart), 's:AUp9')
def testSeriesColors(self):
self.AddToChart(self.chart, [1, 2, 3], '000000')
self.AddToChart(self.chart, [4, 5, 6], 'FFFFFF')
self.assertEqual(self.Param('chco'), '000000,FFFFFF')
def testSeriesCaption_NoCaptions(self):
self.AddToChart(self.chart, [1, 2, 3])
self.AddToChart(self.chart, [4, 5, 6])
self.assertRaises(KeyError, self.Param, 'chdl')
def testSeriesCaption_SomeCaptions(self):
self.AddToChart(self.chart, [1, 2, 3])
self.AddToChart(self.chart, [4, 5, 6], label='Label')
self.AddToChart(self.chart, [7, 8, 9])
self.assertEqual(self.Param('chdl'), '|Label|')
def testThatZeroIsPreservedInCaptions(self):
"""Test that a 0 caption becomes '0' and not ''.
(This makes sure that the logic to rewrite a label of None to '' doesn't
also accidentally rewrite 0 to '').
"""
self.AddToChart(self.chart, [], label=0)
self.AddToChart(self.chart, [], label=1)
self.assertEqual(self.Param('chdl'), '0|1')
def testSeriesCaption_AllCaptions(self):
self.AddToChart(self.chart, [1, 2, 3], label='Its')
self.AddToChart(self.chart, [4, 5, 6], label='Me')
self.AddToChart(self.chart, [7, 8, 9], label='Mario')
self.assertEqual(self.Param('chdl'), 'Its|Me|Mario')
def testDefaultColorsApplied(self):
self.AddToChart(self.chart, [1, 2, 3])
self.AddToChart(self.chart, [4, 5, 6])
self.assertEqual(self.Param('chco'), '0000ff,ff0000')
def testShowingAxes(self):
self.assertEqual(self.Param('chxt'), '')
self.chart.left.min = 3
self.chart.left.max = 5
self.assertEqual(self.Param('chxt'), '')
self.chart.left.labels = ['a']
self.assertEqual(self.Param('chxt'), 'y')
self.chart.right.labels = ['a']
self.assertEqual(self.Param('chxt'), 'y,r')
self.chart.left.labels = [] # Set back to the original state.
self.assertEqual(self.Param('chxt'), 'r')
def testAxisRanges(self):
self.chart.left.labels = ['a']
self.chart.bottom.labels = ['a']
self.assertEqual(self.Param('chxr'), '')
self.chart.left.min = -5
self.chart.left.max = 10
self.assertEqual(self.Param('chxr'), '0,-5,10')
self.chart.bottom.min = 0.5
self.chart.bottom.max = 0.75
self.assertEqual(self.Param('chxr'), '0,-5,10|1,0.5,0.75')
def testAxisLabels(self):
self.ExpectAxes('', '')
self.chart.left.labels = [10, 20, 30]
self.ExpectAxes('0:|10|20|30', '')
self.chart.left.label_positions = [0, 50, 100]
self.ExpectAxes('0:|10|20|30', '0,0,50,100')
self.chart.right.labels = ['cow', 'horse', 'monkey']
self.chart.right.label_positions = [3.7, 10, -22.9]
self.ExpectAxes('0:|10|20|30|1:|cow|horse|monkey',
'0,0,50,100|1,3.7,10,-22.9')
def testGridBottomAxis(self):
self.chart.bottom.min = 0
self.chart.bottom.max = 20
self.chart.bottom.grid_spacing = 10
self.assertEqual(self.Param('chg'), '50,0,1,0')
self.chart.bottom.grid_spacing = 2
self.assertEqual(self.Param('chg'), '10,0,1,0')
def testGridFloatingPoint(self):
"""Test that you can get decimal grid values in chg."""
self.chart.bottom.min = 0
self.chart.bottom.max = 8
self.chart.bottom.grid_spacing = 1
self.assertEqual(self.Param('chg'), '12.5,0,1,0')
self.chart.bottom.max = 3
self.assertEqual(self.Param('chg'), '33.3,0,1,0')
def testGridLeftAxis(self):
self.chart.auto_scale.buffer = 0
self.AddToChart(self.chart, (0, 20))
self.chart.left.grid_spacing = 5
self.assertEqual(self.Param('chg'), '0,25,1,0')
def testLabelGridBottomAxis(self):
self.AddToChart(self.chart, [0, 20, 40])
self.chart.bottom.label_gridlines = True
self.chart.bottom.labels = ['Apple', 'Banana', 'Coconut']
self.chart.bottom.label_positions = [1.5, 5, 8.5]
self.chart.display._width = 320
self.chart.display._height = 240
self.assertEqual(self.Param('chxtc'), '0,-320')
def testLabelGridLeftAxis(self):
self.AddToChart(self.chart, [0, 20, 40])
self.chart.left.label_gridlines = True
self.chart.left.labels = ['Few', 'Some', 'Lots']
self.chart.left.label_positions = [5, 20, 35]
self.chart.display._width = 320
self.chart.display._height = 240
self.assertEqual(self.Param('chxtc'), '0,-320')
def testLabelGridBothAxes(self):
self.AddToChart(self.chart, [0, 20, 40])
self.chart.left.label_gridlines = True
self.chart.left.labels = ['Few', 'Some', 'Lots']
self.chart.left.label_positions = [5, 20, 35]
self.chart.bottom.label_gridlines = True
self.chart.bottom.labels = ['Apple', 'Banana', 'Coconut']
self.chart.bottom.label_positions = [1.5, 5, 8.5]
self.chart.display._width = 320
self.chart.display._height = 240
self.assertEqual(self.Param('chxtc'), '0,-320|1,-320')
def testDefaultDataScalingNotPersistant(self):
"""The auto-scaling shouldn't permanantly set the scale."""
self.chart.auto_scale.buffer = 0 # Buffer just makes the math tricky here.
# This data should scale to the simple encoding's min/middle/max values
# (A, f, 9).
self.AddToChart(self.chart, [1, 2, 3])
self.assertEqual(self.Param('chd'), 's:Af9')
# Different data that maintains the same relative spacing *should* scale
# to the same min/middle/max.
self.chart.data[0].data = [10, 20, 30]
self.assertEqual(self.Param('chd'), 's:Af9')
def FakeScale(self, data, old_min, old_max, new_min, new_max):
self.min = old_min
self.max = old_max
return data
def testDefaultDataScaling(self):
"""If you don't set min/max, it should use the data's min/max."""
orig_scale = util.ScaleData
util.ScaleData = self.FakeScale
try:
self.AddToChart(self.chart, [2, 3, 5, 7, 11])
self.chart.auto_scale.buffer = 0
# This causes scaling to happen & calls FakeScale.
self.chart.display.Url(0, 0)
self.assertEqual(2, self.min)
self.assertEqual(11, self.max)
finally:
util.ScaleData = orig_scale
def testDefaultDataScalingAvoidsCropping(self):
"""The default scaling should give a little buffer to avoid cropping."""
orig_scale = util.ScaleData
util.ScaleData = self.FakeScale
try:
self.AddToChart(self.chart, [1, 6])
# This causes scaling to happen & calls FakeScale.
self.chart.display.Url(0, 0)
buffer = 5 * self.chart.auto_scale.buffer
self.assertEqual(1 - buffer, self.min)
self.assertEqual(6 + buffer, self.max)
finally:
util.ScaleData = orig_scale
def testExplicitDataScaling(self):
"""If you set min/max, data should be scaled to this."""
orig_scale = util.ScaleData
util.ScaleData = self.FakeScale
try:
self.AddToChart(self.chart, [2, 3, 5, 7, 11])
self.chart.left.min = -7
self.chart.left.max = 49
# This causes scaling to happen & calls FakeScale.
self.chart.display.Url(0, 0)
self.assertEqual(-7, self.min)
self.assertEqual(49, self.max)
finally:
util.ScaleData = orig_scale
def testImplicitMinValue(self):
"""min values should be filled in if they are not set explicitly."""
orig_scale = util.ScaleData
util.ScaleData = self.FakeScale
try:
self.AddToChart(self.chart, [0, 10])
self.chart.auto_scale.buffer = 0
self.chart.display.Url(0, 0) # This causes a call to FakeScale.
self.assertEqual(0, self.min)
self.chart.left.min = -5
self.chart.display.Url(0, 0) # This causes a call to FakeScale.
self.assertEqual(-5, self.min)
finally:
util.ScaleData = orig_scale
def testImplicitMaxValue(self):
"""max values should be filled in if they are not set explicitly."""
orig_scale = util.ScaleData
util.ScaleData = self.FakeScale
try:
self.AddToChart(self.chart, [0, 10])
self.chart.auto_scale.buffer = 0
self.chart.display.Url(0, 0) # This causes a call to FakeScale.
self.assertEqual(10, self.max)
self.chart.left.max = 15
self.chart.display.Url(0, 0) # This causes a call to FakeScale.
self.assertEqual(15, self.max)
finally:
util.ScaleData = orig_scale
def testNoneCanAppearInData(self):
"""None should be a valid value in a data series. (It means "no data at
this point")
"""
# Buffer makes comparison difficult because min/max aren't A & 9
self.chart.auto_scale.buffer = 0
self.AddToChart(self.chart, [1, None, 3])
self.assertEqual(self.Param('chd'), 's:A_9')
def testResolveLabelCollision(self):
self.chart.auto_scale.buffer = 0
self.AddToChart(self.chart, [500, 1000])
self.AddToChart(self.chart, [100, 999])
self.AddToChart(self.chart, [200, 900])
self.AddToChart(self.chart, [200, -99])
self.AddToChart(self.chart, [100, -100])
self.chart.right.max = 1000
self.chart.right.min = -100
self.chart.right.labels = [1000, 999, 900, 0, -99, -100]
self.chart.right.label_positions = self.chart.right.labels
separation = formatters.LabelSeparator(right=40)
self.chart.AddFormatter(separation)
self.assertEqual(self.Param('chxp'), '0,1000,960,900,0,-60,-100')
# Try to force a greater spacing than possible
separation.right = 300
self.assertEqual(self.Param('chxp'), '0,1000,780,560,340,120,-100')
# Cluster some values around the lower and upper threshold to verify
# that order is preserved.
self.chart.right.labels = [1000, 901, 900, 899, 10, 1, -50, -100]
self.chart.right.label_positions = self.chart.right.labels
separation.right = 100
self.assertEqual(self.Param('chxp'), '0,1000,900,800,700,200,100,0,-100')
self.assertEqual(self.Param('chxl'), '0:|1000|901|900|899|10|1|-50|-100')
# Try to adjust a single label
self.chart.right.labels = [1000]
self.chart.right.label_positions = self.chart.right.labels
self.assertEqual(self.Param('chxp'), '0,1000')
self.assertEqual(self.Param('chxl'), '0:|1000')
def testAdjustSingleLabelDoesNothing(self):
"""Make sure adjusting doesn't bork the single-label case."""
self.AddToChart(self.chart, (5, 6, 7))
self.chart.left.labels = ['Cutoff']
self.chart.left.label_positions = [3]
def CheckExpectations():
self.assertEqual(self.Param('chxl'), '0:|Cutoff')
self.assertEqual(self.Param('chxp'), '0,3')
CheckExpectations() # Check without adjustment
self.chart.AddFormatter(formatters.LabelSeparator(right=15))
CheckExpectations() # Make sure adjustment hasn't changed anything
if __name__ == '__main__':
graphy_test.main()
graphy-1.0+dfsg/graphy/backends/google_chart_api/line_chart_test.py 0000644 0000000 0000000 00000011150 11203124512 022511 0 ustar #!/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.
"""Unittest for Graphy and Google Chart API backend."""
from graphy import common
from graphy import graphy_test
from graphy import line_chart
from graphy.backends import google_chart_api
from graphy.backends.google_chart_api import base_encoder_test
# Extend XYChartTest so that we pick up & repeat all the basic tests which
# LineCharts should continue to satisfy
class LineChartTest(base_encoder_test.XYChartTest):
def GetChart(self, *args, **kwargs):
return google_chart_api.LineChart(*args, **kwargs)
def AddToChart(self, chart, points, color=None, label=None):
return chart.AddLine(points, color=color, label=label)
def testChartType(self):
self.assertEqual(self.Param('cht'), 'lc')
def testMarkers(self):
x = common.Marker('x', '0000FF', 5)
o = common.Marker('o', '00FF00', 5)
line = common.Marker('V', 'dddddd', 1)
self.chart.AddLine([1, 2, 3], markers=[(1, x), (2, o), (3, x)])
self.chart.AddLine([4, 5, 6], markers=[(x, line) for x in range(3)])
x = 'x,0000FF,0,%s,5'
o = 'o,00FF00,0,%s,5'
V = 'V,dddddd,1,%s,1'
actual = self.Param('chm')
expected = [m % i for i, m in zip([1, 2, 3, 0, 1, 2], [x, o, x, V, V, V])]
expected = '|'.join(expected)
error_msg = '\n%s\n!=\n%s' % (actual, expected)
self.assertEqual(actual, expected, error_msg)
def testLinePatterns(self):
self.chart.AddLine([1, 2, 3])
self.chart.AddLine([4, 5, 6], pattern=line_chart.LineStyle.DASHED)
self.assertEqual(self.Param('chls'), '1,1,0|1,8,4')
def testMultipleAxisLabels(self):
self.ExpectAxes('', '')
left_axis = self.chart.AddAxis(common.AxisPosition.LEFT,
common.Axis())
left_axis.labels = [10, 20, 30]
left_axis.label_positions = [0, 50, 100]
self.ExpectAxes('0:|10|20|30', '0,0,50,100')
bottom_axis = self.chart.AddAxis(common.AxisPosition.BOTTOM,
common.Axis())
bottom_axis.labels = ['A', 'B', 'c', 'd']
bottom_axis.label_positions = [0, 33, 66, 100]
sub_axis = self.chart.AddAxis(common.AxisPosition.BOTTOM,
common.Axis())
sub_axis.labels = ['CAPS', 'lower']
sub_axis.label_positions = [0, 50]
self.ExpectAxes('0:|10|20|30|1:|A|B|c|d|2:|CAPS|lower',
'0,0,50,100|1,0,33,66,100|2,0,50')
self.chart.AddAxis(common.AxisPosition.RIGHT, left_axis)
self.ExpectAxes('0:|10|20|30|1:|10|20|30|2:|A|B|c|d|3:|CAPS|lower',
'0,0,50,100|1,0,50,100|2,0,33,66,100|3,0,50')
self.assertEqual(self.Param('chxt'), 'y,r,x,x')
def testAxisProperties(self):
self.ExpectAxes('', '')
self.chart.top.labels = ['cow', 'horse', 'monkey']
self.chart.top.label_positions = [3.7, 10, -22.9]
self.ExpectAxes('0:|cow|horse|monkey', '0,3.7,10,-22.9')
self.chart.left.labels = [10, 20, 30]
self.chart.left.label_positions = [0, 50, 100]
self.ExpectAxes('0:|10|20|30|1:|cow|horse|monkey',
'0,0,50,100|1,3.7,10,-22.9')
self.assertEqual(self.Param('chxt'), 'y,t')
sub_axis = self.chart.AddAxis(common.AxisPosition.BOTTOM,
common.Axis())
sub_axis.labels = ['CAPS', 'lower']
sub_axis.label_positions = [0, 50]
self.ExpectAxes('0:|10|20|30|1:|CAPS|lower|2:|cow|horse|monkey',
'0,0,50,100|1,0,50|2,3.7,10,-22.9')
self.assertEqual(self.Param('chxt'), 'y,x,t')
self.chart.bottom.labels = ['A', 'B', 'C']
self.chart.bottom.label_positions = [0, 33, 66]
self.ExpectAxes('0:|10|20|30|1:|A|B|C|2:|CAPS|lower|3:|cow|horse|monkey',
'0,0,50,100|1,0,33,66|2,0,50|3,3.7,10,-22.9')
self.assertEqual(self.Param('chxt'), 'y,x,x,t')
# Extend LineChartTest so that we pick up & repeat all the line tests which
# Sparklines should continue to satisfy
class SparklineTest(LineChartTest):
def GetChart(self, *args, **kwargs):
return google_chart_api.Sparkline(*args, **kwargs)
def testChartType(self):
self.assertEqual(self.Param('cht'), 'lfi')
if __name__ == '__main__':
graphy_test.main()
graphy-1.0+dfsg/graphy/backends/google_chart_api/__init__.py 0000644 0000000 0000000 00000003731 11203124512 021107 0 ustar #!/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."""
from graphy import line_chart
from graphy import bar_chart
from graphy import pie_chart
from graphy.backends.google_chart_api import encoders
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).
# TODO: fix these so they have nice docs in ipython (give them __doc__)
LineChart = _GetChartFactory(line_chart.LineChart, encoders.LineChartEncoder)
Sparkline = _GetChartFactory(line_chart.Sparkline, encoders.SparklineEncoder)
BarChart = _GetChartFactory(bar_chart.BarChart, encoders.BarChartEncoder)
PieChart = _GetChartFactory(pie_chart.PieChart, encoders.PieChartEncoder)
graphy-1.0+dfsg/graphy/backends/google_chart_api/bar_chart_test.py 0000644 0000000 0000000 00000016747 11203124512 022347 0 ustar #!/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.
"""Unittest for Graphy and Google Chart API backend."""
import math
from graphy import graphy_test
from graphy import bar_chart
from graphy.backends import google_chart_api
from graphy.backends.google_chart_api import base_encoder_test
# Extend XYChartTest so that we pick up & repeat all the basic tests which
# BarCharts should continue to satisfy
class BarChartTest(base_encoder_test.XYChartTest):
def GetChart(self, *args, **kwargs):
return google_chart_api.BarChart(*args, **kwargs)
def AddToChart(self, chart, points, color=None, label=None):
return chart.AddBars(points, color=color, label=label)
def testChartType(self):
def Check(vertical, stacked, expected_type):
self.chart.vertical = vertical
self.chart.stacked = stacked
self.assertEqual(self.Param('cht'), expected_type)
Check(vertical=True, stacked=True, expected_type='bvs')
Check(vertical=True, stacked=False, expected_type='bvg')
Check(vertical=False, stacked=True, expected_type='bhs')
Check(vertical=False, stacked=False, expected_type='bhg')
def testSingleBarCase(self):
"""Test that we can handle a bar chart with only a single bar."""
self.AddToChart(self.chart, [1])
self.assertEqual(self.Param('chd'), 's:A')
def testHorizontalScaling(self):
"""Test the scaling works correctly on horizontal bar charts (which have
min/max on a different axis than other charts).
"""
self.AddToChart(self.chart, [3])
self.chart.vertical = False
self.chart.bottom.min = 0
self.chart.bottom.max = 3
self.assertEqual(self.Param('chd'), 's:9') # 9 is far right edge.
self.chart.bottom.max = 6
self.assertEqual(self.Param('chd'), 's:f') # f is right in the middle.
def testZeroPoint(self):
self.AddToChart(self.chart, [-5, 0, 5])
self.assertEqual(self.Param('chp'), str(.5)) # Auto scaling.
self.chart.left.min = 0
self.chart.left.max = 5
self.assertRaises(KeyError, self.Param, 'chp') # No negative values.
self.chart.left.min = -5
self.assertEqual(self.Param('chp'), str(.5)) # Explicit scaling.
self.chart.left.max = 15
self.assertEqual(self.Param('chp'), str(.25)) # Different zero point.
self.chart.left.max = -1
self.assertEqual(self.Param('chp'), str(1)) # Both negative values.
def testLabelsInCorrectOrder(self):
"""Test that we reverse labels for horizontal bar charts
(Otherwise they are backwards from what you would expect)
"""
self.chart.left.labels = [1, 2, 3]
self.chart.vertical = True
self.assertEqual(self.Param('chxl'), '0:|1|2|3')
self.chart.vertical = False
self.assertEqual(self.Param('chxl'), '0:|3|2|1')
def testLabelRangeDefaultsToDataScale(self):
"""Test that if you don't set axis ranges, they default to the data
scale.
"""
self.chart.auto_scale.buffer = 0 # Buffer causes trouble for testing.
self.AddToChart(self.chart, [1, 5])
self.chart.left.labels = (1, 5)
self.chart.left.labels_positions = (1, 5)
self.assertEqual(self.Param('chxr'), '0,1,5')
def testCanOverrideChbh(self):
self.chart.style = bar_chart.BarChartStyle(10, 3, 6)
self.AddToChart(self.chart, [1, 2, 3])
self.assertEqual(self.Param('chbh'), '10,3,6')
self.chart.display.extra_params['chbh'] = '5,5,2'
self.assertEqual(self.Param('chbh'), '5,5,2')
def testDefaultBarChartStyle(self):
self.assertNotIn('chbh', self.chart.display._Params(self.chart))
self.chart.style = bar_chart.BarChartStyle(None, None, None)
self.assertNotIn('chbh', self.chart.display._Params(self.chart))
self.chart.style = bar_chart.BarChartStyle(10, 3, 6)
self.assertNotIn('chbh', self.chart.display._Params(self.chart))
self.AddToChart(self.chart, [1, 2, 3])
self.assertEqual(self.Param('chbh'), '10,3,6')
self.chart.style = bar_chart.BarChartStyle(10)
self.assertEqual(self.Param('chbh'), '10,4,8')
def testAutoBarSizing(self):
self.AddToChart(self.chart, [1, 2, 3])
self.AddToChart(self.chart, [4, 5, 6])
self.chart.style = bar_chart.BarChartStyle(None, 3, 6)
self.chart.display._width = 100
self.chart.display._height = 1000
self.chart.stacked = False
self.assertEqual(self.Param('chbh'), 'a,3,6')
self.chart.stacked = True
self.assertEqual(self.Param('chbh'), 'a,3')
self.chart.vertical = False
self.chart.stacked = False
self.assertEqual(self.Param('chbh'), 'a,3,6')
self.chart.stacked = True
self.assertEqual(self.Param('chbh'), 'a,3')
self.chart.display._height = 1
self.assertEqual(self.Param('chbh'), 'a,3')
def testAutoBarSpacing(self):
self.AddToChart(self.chart, [1, 2, 3])
self.AddToChart(self.chart, [4, 5, 6])
self.chart.style = bar_chart.BarChartStyle(10, 1, None)
self.assertEqual(self.Param('chbh'), '10,1,2')
self.chart.style = bar_chart.BarChartStyle(10, None, 2)
self.assertEqual(self.Param('chbh'), '10,1,2')
self.chart.style = bar_chart.BarChartStyle(10, None, 1)
self.assertEqual(self.Param('chbh'), '10,0,1')
def testFractionalAutoBarSpacing(self):
self.AddToChart(self.chart, [1, 2, 3])
self.AddToChart(self.chart, [4, 5, 6])
self.chart.style = bar_chart.BarChartStyle(10, 0.1, None,
use_fractional_gap_spacing=True)
self.assertEqual(self.Param('chbh'), '10,1,2')
self.chart.style = bar_chart.BarChartStyle(10, None, 0.2,
use_fractional_gap_spacing=True)
self.assertEqual(self.Param('chbh'), '10,1,2')
self.chart.style = bar_chart.BarChartStyle(10, None, 0.1,
use_fractional_gap_spacing=True)
self.assertEqual(self.Param('chbh'), '10,0,1')
self.chart.style = bar_chart.BarChartStyle(None, 0.1, 0.2,
use_fractional_gap_spacing=True)
self.assertEqual(self.Param('chbh'), 'r,0.1,0.2')
self.chart.style = bar_chart.BarChartStyle(None, 0.1, None,
use_fractional_gap_spacing=True)
self.assertEqual(self.Param('chbh'), 'r,0.1,0.2')
def testStackedDataScaling(self):
self.AddToChart(self.chart, [10, 20, 30])
self.AddToChart(self.chart, [-5, -10, -15])
self.chart.stacked = True
self.assertEqual(self.Param('chd'), 's:iu6,PJD')
self.chart.stacked = False
self.assertEqual(self.Param('chd'), 's:iu6,PJD')
self.chart = self.GetChart()
self.chart.stacked = True
self.AddToChart(self.chart, [10, 20, 30])
self.AddToChart(self.chart, [5, -10, 15])
self.assertEqual(self.Param('chd'), 's:Xhr,SDc')
self.AddToChart(self.chart, [-15, -10, -45])
self.assertEqual(self.Param('chd'), 's:lrx,iYo,VYD')
# TODO: Figure out how to deal with missing data points, test them
def testNegativeBars(self):
self.chart.stacked = True
self.AddToChart(self.chart, [-10,-20,-30])
self.assertEqual(self.Param('chd'), 's:oVD')
self.AddToChart(self.chart, [-1,-2,-3])
self.assertEqual(self.Param('chd'), 's:pZI,531')
self.chart.stacked = False
self.assertEqual(self.Param('chd'), 's:pWD,642')
if __name__ == '__main__':
graphy_test.main()
graphy-1.0+dfsg/graphy/backends/google_chart_api/util.py 0000644 0000000 0000000 00000014277 11203124512 020334 0 ustar #!/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.
"""Utility functions for working with the Google Chart API.
Not intended for end users, use the methods in __init__ instead."""
import cgi
import string
import urllib
# 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_size='chbh',
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_size=',',
bar_height=',',
)
class SimpleDataEncoder:
"""Encode data using simple encoding. Out-of-range data will
be dropped (encoded as '_').
"""
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, use_html_entities):
"""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:
url = '%s?%s' % (base, '&'.join(real_params))
else:
url = base
if use_html_entities:
url = cgi.escape(url, quote=True)
return url
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)
graphy-1.0+dfsg/graphy/backends/google_chart_api/encoders.py 0000644 0000000 0000000 00000034720 11203124512 021154 0 ustar #!/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.
"""Display objects for the different kinds of charts.
Not intended for end users, use the methods in __init__ instead."""
import warnings
from graphy.backends.google_chart_api import util
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, use_html_entities=False):
"""Get the URL for our graph.
Args:
use_html_entities: If True, reserved HTML characters (&, <, >, ") in the
URL are replaced with HTML entities (&, <, etc.). Default is False.
"""
self._width = width
self._height = height
params = self._Params(self.chart)
return util.EncodeUrl(self.url_base, params, self.escape_url,
use_html_entities)
def Img(self, width, height):
"""Get an image tag for our graph."""
url = self.Url(width, height, use_html_entities=True)
tag = '
'
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._GetColors,
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(util.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 = []
markers = []
for i, series in enumerate(chart.data):
data = series.data
if not data: # Drop empty series.
continue
series_data.append(data)
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 = util.EncodeData(chart, series_data, y_min, y_max, encoder)
result.update(util.JoinLists(marker = markers))
return result
def _GetColors(self, chart):
"""Color series color parameter."""
colors = []
for series in chart.data:
if not series.data:
continue
colors.append(series.style.color)
return util.JoinLists(color = colors)
def _GetDataEncoder(self, chart):
"""Get a class which can encode the data the way the user requested."""
if not self.enhanced_encoding:
return util.SimpleDataEncoder()
return util.EnhancedDataEncoder()
def _GetLegendParams(self, chart):
"""Get params for showing a legend."""
if chart._show_legend:
return util.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 util.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'}
def _GetLineStyles(self, chart):
"""Get LineStyle parameters."""
styles = []
for series in chart.data:
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)
return util.JoinLists(line_style = styles)
def _GetFormatters(self):
out = super(LineChartEncoder, self)._GetFormatters()
out.insert(-2, self._GetLineStyles)
return out
class SparklineEncoder(LineChartEncoder):
"""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."""
__STYLE_DEPRECATION = ('BarChart.display.style is deprecated.' +
' Use BarChart.style, instead.')
def __init__(self, chart, style=None):
"""Construct a new BarChartEncoder.
Args:
style: DEPRECATED. Set style on the chart object itself.
"""
super(BarChartEncoder, self).__init__(chart)
if style is not None:
warnings.warn(self.__STYLE_DEPRECATION, DeprecationWarning, stacklevel=2)
chart.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()
# insert at -2 to allow extra_params to overwrite everything
out.insert(-2, self._ZeroPoint)
out.insert(-2, self._ApplyBarChartStyle)
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 _ApplyBarChartStyle(self, chart):
"""If bar style is specified, fill in the missing data and apply it."""
# sanity checks
if chart.style is None or not chart.data:
return {}
(bar_thickness, bar_gap, group_gap) = (chart.style.bar_thickness,
chart.style.bar_gap,
chart.style.group_gap)
# Auto-size bar/group gaps
if bar_gap is None and group_gap is not None:
bar_gap = max(0, group_gap / 2)
if not chart.style.use_fractional_gap_spacing:
bar_gap = int(bar_gap)
if group_gap is None and bar_gap is not None:
group_gap = max(0, bar_gap * 2)
# Set bar thickness to auto if it is missing
if bar_thickness is None:
if chart.style.use_fractional_gap_spacing:
bar_thickness = 'r'
else:
bar_thickness = 'a'
else:
# Convert gap sizes to pixels if needed
if chart.style.use_fractional_gap_spacing:
if bar_gap:
bar_gap = int(bar_thickness * bar_gap)
if group_gap:
group_gap = int(bar_thickness * group_gap)
# Build a valid spec; ignore group gap if chart is stacked,
# since there are no groups in that case
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 util.JoinLists(bar_size = spec)
def __GetStyle(self):
warnings.warn(self.__STYLE_DEPRECATION, DeprecationWarning, stacklevel=2)
return self.chart.style
def __SetStyle(self, value):
warnings.warn(self.__STYLE_DEPRECATION, DeprecationWarning, stacklevel=2)
self.chart.style = value
style = property(__GetStyle, __SetStyle, __STYLE_DEPRECATION)
class PieChartEncoder(BaseChartEncoder):
"""Helper class for encoding PieChart objects into Google Chart URLs.
Fuzzy frogs frolic in the forest.
Object Attributes:
is3d: if True, draw a 3d pie chart. Default is False.
"""
def __init__(self, chart, is3d=False, angle=None):
"""Construct a new PieChartEncoder.
Args:
is3d: If True, draw a 3d pie chart. Default is False. If the pie chart
includes multiple pies, is3d must be set to False.
angle: Angle of rotation of the pie chart, in radians.
"""
super(PieChartEncoder, self).__init__(chart)
self.is3d = is3d
self.angle = None
def _GetFormatters(self):
"""Add a formatter for the chart angle."""
formatters = super(PieChartEncoder, self)._GetFormatters()
formatters.append(self._GetAngleParams)
return formatters
def _GetType(self, chart):
if len(chart.data) > 1:
if self.is3d:
warnings.warn(
'3d charts with more than one pie not supported; rendering in 2d',
RuntimeWarning, stacklevel=2)
chart_type = 'pc'
else:
if self.is3d:
chart_type = 'p3'
else:
chart_type = 'p'
return {'chart_type': chart_type}
def _GetDataSeriesParams(self, chart):
"""Collect params related to the data series."""
pie_points = []
labels = []
max_val = 1
for pie in chart.data:
points = []
for segment in pie:
if segment:
points.append(segment.size)
max_val = max(max_val, segment.size)
labels.append(segment.label or '')
if points:
pie_points.append(points)
encoder = self._GetDataEncoder(chart)
result = util.EncodeData(chart, pie_points, 0, max_val, encoder)
result.update(util.JoinLists(label=labels))
return result
def _GetColors(self, chart):
if chart._colors:
# Colors were overridden by the user
colors = chart._colors
else:
# Build the list of colors from individual segments
colors = []
for pie in chart.data:
for segment in pie:
if segment and segment.color:
colors.append(segment.color)
return util.JoinLists(color = colors)
def _GetAngleParams(self, chart):
"""If the user specified an angle, add it to the params."""
if self.angle:
return {'chp' : str(self.angle)}
return {}
graphy-1.0+dfsg/graphy/backends/__init__.py 0000644 0000000 0000000 00000000000 11203124512 015603 0 ustar graphy-1.0+dfsg/graphy/line_chart.py 0000644 0000000 0000000 00000007637 11203124512 014431 0 ustar #!/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)
color: Color of the line. A hex string, like 'ff0000' for red. Optional,
AutoColor will fill this in for you automatically if empty.
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, color=None):
"""Construct a LineStyle. See class docstring for details on args."""
self.width = width
self.on = on
self.off = off
self.color = color
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, color=None,
pattern=LineStyle.SOLID, width=LineStyle.THIN, markers=None):
"""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)
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)
markers: List of Marker objects to attach to this line (see DataSeries
for more info)
"""
if color is not None and isinstance(color[0], common.Marker):
warnings.warn('Your code may be broken! '
'You passed a list of Markers instead of a color. The '
'old argument order (markers before color) is deprecated.',
DeprecationWarning, stacklevel=2)
style = LineStyle(width, pattern[0], pattern[1], color=color)
series = common.DataSeries(points, label=label, style=style,
markers=markers)
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 self.AddLine(points, color=color, width=style.width,
pattern=(style.on, style.off), markers=markers,
label=label)
class Sparkline(LineChart):
"""Represent a sparkline. These behave like LineCharts,
mostly, but come without axes.
"""
graphy-1.0+dfsg/graphy/util.py 0000644 0000000 0000000 00000000644 11203124512 013265 0 ustar def _IsColor(color):
"""Try to determine if color is a hex color string.
Labels that look like hex colors will match too, unfortunately."""
if not isinstance(color, basestring):
return False
color = color.strip('#')
if len(color) != 3 and len(color) != 6:
return False
hex_letters = '0123456789abcdefABCDEF'
for letter in color:
if letter not in hex_letters:
return False
return True
graphy-1.0+dfsg/LICENSE 0000644 0000000 0000000 00000026135 11203124512 011454 0 ustar 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.