Skip to content

Commit

Permalink
Improve Shape error handling. Union Ordinal series. Closes #76.
Browse files Browse the repository at this point in the history
  • Loading branch information
onyxfish committed Nov 15, 2016
1 parent e82c4b5 commit fad6a80
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 18 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
0.3.3
-----


* Ordinal scales can now display data from multiple series with different values. (#76)
* Better error handling for data types supported by different shapes.

0.3.2 - November 11, 2016
-------------------------
Expand Down
26 changes: 19 additions & 7 deletions leather/scales/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,25 @@ def infer(cls, layers, dimension, data_type):
elif data_type is Text:
scale_values = None

for series, shape in layers:
if scale_values is None:
scale_values = series.values(dimension)
continue

if series.values(dimension) != scale_values:
raise ValueError('All series must have the same values for scale display.')
# First case: a single set of ordinal labels
if len(layers) == 1:
scale_values = layers[0][0].values(dimension)
else:
first_series = set(layers[0][0].values(dimension))
data_series = [series.values(dimension) for series, shape in layers]
all_same = True

for series in data_series:
if set(series) != first_series:
all_same = False
break

# Second case: multiple identical sets of ordinal labels
if all_same:
scale_values = layers[0][0].values(dimension)
# Third case: multiple different sets of ordinal labels
else:
scale_values = sorted(list(set().union(*data_series)))

scale = Ordinal(scale_values)

Expand Down
10 changes: 7 additions & 3 deletions leather/series/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ class Series(object):
Or, a custom data format, in which case :code:`x` and :code:`y` must
specify :func:`.key_function`.
:param shape:
An instance of :class:`.Shape` to use to render this data.
:param x:
If using sequence row data, then this may be either an integer index
identifying the X column, or a :func:`.key_function`.
Expand Down Expand Up @@ -77,14 +75,20 @@ def _infer_type(self, key):
break

if v is None:
raise ValueError('All values in dimension was null.')
raise ValueError('All values in dimension were null.')

return DataType.infer(v)

@property
def name(self):
return self._name

def data_type(self, dimension):
"""
Return the data type for a dimension of this series.
"""
return self._types[dimension]

def data(self):
"""
Return data for this series.
Expand Down
8 changes: 8 additions & 0 deletions leather/shapes/bars.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

import six

from leather.data_types import Number, Text
from leather.series import CategorySeries
from leather.shapes.base import Shape
from leather import theme
from leather.utils import X, Y


class Bars(Shape):
Expand All @@ -27,6 +29,12 @@ def validate_series(self, series):
if isinstance(series, CategorySeries):
raise ValueError('Bars can not be used to render CategorySeries.')

if series.data_type(X) is not Number:
raise ValueError('Bars only support Number values for the Y axis.')

if series.data_type(Y) is not Text:
raise ValueError('Bars only support Text values for the X axis.')

def to_svg(self, width, height, x_scale, y_scale, series, palette):
"""
Render bars to SVG elements.
Expand Down
8 changes: 8 additions & 0 deletions leather/shapes/columns.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

import six

from leather.data_types import Number, Text
from leather.series import CategorySeries
from leather.shapes.base import Shape
from leather.utils import X, Y


class Columns(Shape):
Expand All @@ -26,6 +28,12 @@ def validate_series(self, series):
if isinstance(series, CategorySeries):
raise ValueError('Columns can not be used to render CategorySeries.')

if series.data_type(X) is not Text:
raise ValueError('Bars only support Text values for the X axis.')

if series.data_type(Y) is not Number:
raise ValueError('Bars only support Number values for the Y axis.')

def to_svg(self, width, height, x_scale, y_scale, series, palette):
"""
Render columns to SVG elements.
Expand Down
6 changes: 5 additions & 1 deletion leather/shapes/dots.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@

import six

from leather.data_types import Text
from leather.series import CategorySeries
from leather.shapes.base import Shape
from leather import theme
from leather.utils import DummySeries
from leather.utils import DummySeries, X, Y


class Dots(Shape):
Expand All @@ -32,6 +33,9 @@ def validate_series(self, series):
"""
Verify this shape can be used to render a given series.
"""
if series.data_type(X) is Text or series.data_type(Y) is Text:
raise ValueError('Dots do not support Text values.')

return True

def to_svg(self, width, height, x_scale, y_scale, series, palette):
Expand Down
5 changes: 5 additions & 0 deletions leather/shapes/line.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

import six

from leather.data_types import Text
from leather.series import CategorySeries
from leather.shapes.base import Shape
from leather import theme
from leather.utils import X, Y


class Line(Shape):
Expand All @@ -30,6 +32,9 @@ def validate_series(self, series):
if isinstance(series, CategorySeries):
raise ValueError('Line can not be used to render CategorySeries.')

if series.data_type(X) is Text or series.data_type(Y) is Text:
raise ValueError('Line does not support Text values.')

def _new_path(self, stroke_color):
"""
Start a new path.
Expand Down
28 changes: 22 additions & 6 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,28 @@

import leather

data = [
(0, 'foo'),
(5, u'👍'),
(10, 'bar')
# data1 = [
# (2, 'foo'),
# (6, 'bar'),
# (9, 'bing')
# ]
#
# data2 = [
# (3, 'foo'),
# (5, 'bar'),
# (7, 'bing')
# ]
#
# lattice = leather.Lattice(shape=leather.Bars())
# lattice.add_many([data1, data2])
# lattice.to_svg('test.svg')

data1 = [
(2, None),
(6, None),
(9, None)
]

chart = leather.Chart('Dots')
chart.add_bars(data)
chart = leather.Chart()
chart.add_bars(data1)
chart.to_svg('test.svg')
44 changes: 44 additions & 0 deletions tests/test_lattice.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,47 @@ def test_add_many(self):
self.assertElementCount(svg, '.axis', 6)
self.assertElementCount(svg, '.series', 3)
self.assertElementCount(svg, '.lines', 3)

def test_bars(self):
data1 = [
(2, 'foo'),
(6, 'bar'),
(9, 'bing')
]

data2 = [
(3, 'foo'),
(5, 'bar'),
(7, 'bing')
]

lattice = leather.Lattice(shape=leather.Bars())
lattice.add_many([data1, data2])

svg = self.render_chart(lattice)

self.assertElementCount(svg, '.axis', 4)
self.assertElementCount(svg, '.series', 2)
self.assertElementCount(svg, '.bars', 2)

def test_bars_different(self):
data1 = [
(3, 'foo'),
(5, 'bar'),
(9, 'bing')
]

data2 = [
(3, 'foo'),
(5, 'bar'),
(9, 'baz')
]

lattice = leather.Lattice(shape=leather.Bars())
lattice.add_many([data1, data2])

svg = self.render_chart(lattice)

self.assertElementCount(svg, '.axis', 4)
self.assertElementCount(svg, '.series', 2)
self.assertElementCount(svg, '.bars', 2)
56 changes: 56 additions & 0 deletions tests/test_shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ def test_zeros(self):
self.assertEqual(float(rects[1].get('x')), 0)
self.assertEqual(float(rects[1].get('width')), 0)

def test_validate(self):
series = leather.Series([
(1, 'foo')
])

self.shape.validate_series(series)

series = leather.Series([
('foo', 1)
])

with self.assertRaises(ValueError):
self.shape.validate_series(series)


class TestColumns(leather.LeatherTestCase):
def setUp(self):
Expand Down Expand Up @@ -90,6 +104,20 @@ def test_nulls(self):
self.assertEqual(float(rects[1].get('y')), 0)
self.assertEqual(float(rects[1].get('height')), 100)

def test_validate(self):
series = leather.Series([
('foo', 1)
])

self.shape.validate_series(series)

series = leather.Series([
(1, 'foo')
])

with self.assertRaises(ValueError):
self.shape.validate_series(series)


class TestDots(leather.LeatherTestCase):
def setUp(self):
Expand Down Expand Up @@ -140,6 +168,20 @@ def test_nulls(self):
self.assertEqual(float(circles[1].get('cx')), 200)
self.assertEqual(float(circles[1].get('cy')), 0)

def test_validate(self):
series = leather.Series([
(1, 1)
])

self.shape.validate_series(series)

series = leather.Series([
(1, 'foo')
])

with self.assertRaises(ValueError):
self.shape.validate_series(series)


class TestLine(leather.LeatherTestCase):
def setUp(self):
Expand Down Expand Up @@ -183,3 +225,17 @@ def test_nulls(self):
paths = list(group)

self.assertEqual(len(paths), 2)

def test_validate(self):
series = leather.Series([
(1, 1)
])

self.shape.validate_series(series)

series = leather.Series([
(1, 'foo')
])

with self.assertRaises(ValueError):
self.shape.validate_series(series)

0 comments on commit fad6a80

Please sign in to comment.