graphy-1.0+dfsg/0000755000000000000000000000000011320654610010447 5ustar graphy-1.0+dfsg/README0000644000000000000000000000153311203124512011322 0ustar 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/0000755000000000000000000000000011320654610012265 5ustar graphy-1.0+dfsg/examples/bay_area_population.py0000644000000000000000000000422411203124512016647 0ustar #!/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.py0000644000000000000000000000234411203124512014251 0ustar #!/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.py0000644000000000000000000000160311203124512013753 0ustar #!/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.py0000644000000000000000000000172711203124512016533 0ustar #!/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.py0000644000000000000000000000254711203124512014115 0ustar #!/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.py0000644000000000000000000000202011203124512014464 0ustar #!/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.py0000644000000000000000000000347711203124512017054 0ustar #!/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.py0000644000000000000000000000355011203124512014136 0ustar #!/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.py0000644000000000000000000000263311203124512014622 0ustar #!/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/0000755000000000000000000000000011320654667011755 5ustar graphy-1.0+dfsg/graphy/pie_chart_test.py0000644000000000000000000000706111203124512015305 0ustar #!/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.py0000644000000000000000000000307711203124512014305 0ustar #!/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.py0000644000000000000000000000274511203124512014645 0ustar #!/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.py0000644000000000000000000000715711203124512014645 0ustar #!/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.py0000644000000000000000000000525611203124512015463 0ustar #!/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.py0000644000000000000000000000662611203124512015543 0ustar #!/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.py0000644000000000000000000003442011203124512013577 0ustar #!/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__.py0000644000000000000000000000002211203124512014035 0ustar __version__='1.0' graphy-1.0+dfsg/graphy/pie_chart.py0000644000000000000000000001407411203124512014250 0ustar #!/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.py0000644000000000000000000000547611203124512015304 0ustar #!/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.py0000644000000000000000000001526411203124512014502 0ustar #!/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.py0000644000000000000000000001321111203124512014227 0ustar #!/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/0000755000000000000000000000000011320654667013527 5ustar graphy-1.0+dfsg/graphy/backends/google_chart_api/0000755000000000000000000000000011320654667017015 5ustar graphy-1.0+dfsg/graphy/backends/google_chart_api/util_test.py0000644000000000000000000001127111203124512021362 0ustar #!/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.py0000644000000000000000000001340411203124512022343 0ustar #!/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.py0000644000000000000000000005405711203124512023027 0ustar #!/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.py0000644000000000000000000001115011203124512022511 0ustar #!/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__.py0000644000000000000000000000373111203124512021107 0ustar #!/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.py0000644000000000000000000001674711203124512022347 0ustar #!/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.py0000644000000000000000000001427711203124512020334 0ustar #!/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.py0000644000000000000000000003472011203124512021154 0ustar #!/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 = 'chart' return tag % (url, width, height) def _GetType(self, chart): """Return the correct chart_type param for the chart.""" raise NotImplementedError def _GetFormatters(self): """Get a list of formatter functions to use for encoding.""" formatters = [self._GetLegendParams, self._GetDataSeriesParams, self._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__.py0000644000000000000000000000000011203124512015603 0ustar graphy-1.0+dfsg/graphy/line_chart.py0000644000000000000000000000763711203124512014431 0ustar #!/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.py0000644000000000000000000000064411203124512013265 0ustar 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/LICENSE0000644000000000000000000002613511203124512011454 0ustar 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.