Skip to content

Commit

Permalink
Add support for access functions. Closes #44.
Browse files Browse the repository at this point in the history
  • Loading branch information
onyxfish committed May 20, 2016
1 parent 3d6918f commit 3d4df4e
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 22 deletions.
6 changes: 3 additions & 3 deletions leather/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ def add_series(self, series):
"""
for dim in DIMENSIONS:
if not self._types[dim]:
self._types[dim] = series.types[dim]
elif series.types[dim] is not self._types[dim]:
raise TypeError('Can\'t mix axis-data types: %s and %s' % (series.types[dim], self._types[dim]))
self._types[dim] = series._types[dim]
elif series._types[dim] is not self._types[dim]:
raise TypeError('Can\'t mix axis-data types: %s and %s' % (series._types[dim], self._types[dim]))

self._layers.append(series)

Expand Down
2 changes: 1 addition & 1 deletion leather/scales/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def infer(cls, series_list, dimension, data_type):
data_max = 0

for series in series_list:
if isinstance(series.shape, (Bars, Columns)):
if isinstance(series._shape, (Bars, Columns)):
force_zero = True

data_min = min(data_min, series.min(dimension))
Expand Down
111 changes: 97 additions & 14 deletions leather/series.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,111 @@
#!/usr/bin/env python

from collections import Iterable, Sequence, Mapping

import six

from leather.data_types import DataType
from leather.utils import DIMENSIONS
from leather.utils import DIMENSIONS, X, Y


class Series(object):
class Series(Iterable):
"""
A series of data and its associated metadata.
Series object does not modify or consume the data it is passed.
:param data:
A sequence of 2-item sequences, where each pair contains :code:`(x, y)`
values.
A sequence (rows) of sequences (columns), a.k.a. :func:`csv.reader`
format. If the :code:`x` and :code:`y` are not specified then the first
column is used as the X values and the second column is used for Y.
Or, a sequence of (rows) of dicts (columns), a.k.a.
:class:`csv.DictReader` format. If this format is used then :code:`x`
and :code:`y` arguments must specify the columns to be charted.
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`.
If using dict row data, then this may be either a key name identifying
the X column, or a :func:`.key_function`.
If using a custom data format, then this must be a
:func:`.key_function`.`
:param y:
See :code:`x`.
:param name:
An optional name to be used in labeling this series.
"""
def __init__(self, data, shape, name=None):
self.data = data
self.shape = shape
self.name = name
def __init__(self, data, shape, x=0, y=1, name=None):
self._data = data
self._len = len(data)
self._shape = shape
self._name = name

self.types = [None, None]
self._keys = [
self._make_key(x),
self._make_key(y)
]

if callable(y):
self._keys[Y] = y
else:
self._keys[Y] = lambda row, index: row[y]

self._types = [None, None]

for i in DIMENSIONS:
self.types[i] = self._infer_type(i)
self._types[i] = self._infer_type(i)

def __iter__(self):
"""
Iterate over this data series. Yields :code:`(x, y, row)`.
"""
x = self._keys[X]
y = self._keys[Y]

for i, row in enumerate(self._data):
yield (x(row, i), y(row, i), row)

def _make_key(self, key):
"""
Process a user-specified data key and convert to a function if needed.
"""
if callable(key):
return key
elif isinstance(self._data[0], Sequence) and not isinstance(key, int):
raise TypeError('You must specify an integer index when using sequence data.')
elif isinstance(self._data[0], Mapping) and not isinstance(key, six.string_types):
raise TypeError('You must specify a string index when using dict data.')
else:
return lambda row, index: row[key]

def _infer_type(self, dimension):
"""
Infer the datatype of this column by sampling the data.
"""
key = self._keys[dimension]
i = 0

while self.data[i][dimension] is None:
while key(self._data[i], i) is None:
i += 1

return DataType.infer(self.data[i][dimension])
# TKTK: raise an exception if we hit the end of the data

return DataType.infer(key(self._data[i], i))

def values(self, dimension):
"""
Get a flattened list of values for a given dimension of the data.
"""
return [d[dimension] for d in self.data]
key = self._keys[dimension]

return [key(row, i) for i, row in enumerate(self._data)]

def min(self, dimension):
"""
Expand All @@ -56,4 +123,20 @@ def to_svg(self, width, height, x_scale, y_scale):
"""
Render this series to SVG elements using it's assigned shape.
"""
return self.shape.to_svg(width, height, x_scale, y_scale, self)
return self._shape.to_svg(width, height, x_scale, y_scale, self)


def key_function(row, index):
"""
This example shows how to define a function to extract X and Y values
from custom data.
:param row:
The function will be called with the row data, in whatever format it
was provided to the :class:`.Series`.
:param index:
The row index in the series data will also be provided.
:returns:
The function must return a chartable value.
"""
pass
2 changes: 1 addition & 1 deletion leather/shapes/bars.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def to_svg(self, width, height, x_scale, y_scale, series):
group = ET.Element('g')
group.set('class', 'series bars')

for i, (x, y) in enumerate(series.data):
for i, (x, y, row) in enumerate(series):
if x is None or y is None:
continue

Expand Down
2 changes: 1 addition & 1 deletion leather/shapes/columns.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def to_svg(self, width, height, x_scale, y_scale, series):
group = ET.Element('g')
group.set('class', 'series columns')

for i, (x, y) in enumerate(series.data):
for i, (x, y, row) in enumerate(series):
if x is None or y is None:
continue

Expand Down
2 changes: 1 addition & 1 deletion leather/shapes/dots.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def to_svg(self, width, height, x_scale, y_scale, series):
group = ET.Element('g')
group.set('class', 'series dots')

for i, (x, y) in enumerate(series.data):
for i, (x, y, row) in enumerate(series):
if x is None or y is None:
continue

Expand Down
2 changes: 1 addition & 1 deletion leather/shapes/lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def to_svg(self, width, height, x_scale, y_scale, series):

d = []

for x, y in series.data:
for x, y, row in series:
if x is None or y is None:
if d:
path.set('d', ' '.join(d))
Expand Down
91 changes: 91 additions & 0 deletions tests/test_series.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/usr/bin/env python

try:
import unittest2 as unittest
except ImportError:
import unittest

import leather
from leather.utils import X, Y


class TestSeries(unittest.TestCase):
def setUp(self):
self.shape = leather.Dots('red')

def test_pairs(self):
data = [
('foo', 1),
('bar', 2),
('baz', 3)
]

series = leather.Series(data, self.shape)

self.assertSequenceEqual(series.values(X), ['foo', 'bar', 'baz'])
self.assertSequenceEqual(series.values(Y), [1, 2, 3])

def test_lists(self):
data = [
('foo', 1, 4),
('bar', 2, 5),
('baz', 3, 6)
]

series = leather.Series(data, self.shape)

self.assertSequenceEqual(series.values(X), ['foo', 'bar', 'baz'])
self.assertSequenceEqual(series.values(Y), [1, 2, 3])

series = leather.Series(data, self.shape, x=2, y=0)

self.assertSequenceEqual(series.values(X), [4, 5, 6])
self.assertSequenceEqual(series.values(Y), ['foo', 'bar', 'baz'])

with self.assertRaises(TypeError):
series = leather.Series(data, self.shape, x='words')

def test_dicts(self):
data = [
{'a': 'foo', 'b': 1, 'c': 4},
{'a': 'bar', 'b': 2, 'c': 5},
{'a': 'baz', 'b': 3, 'c': 6}
]

with self.assertRaises(TypeError):
series = leather.Series(data, self.shape)

series = leather.Series(data, self.shape, x='c', y='a')

self.assertSequenceEqual(series.values(X), [4, 5, 6])
self.assertSequenceEqual(series.values(Y), ['foo', 'bar', 'baz'])

def test_custom(self):
class Obj(object):
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c =c

data = [
Obj('foo', 1, 4),
Obj('bar', 2, 5),
Obj('baz', 3, 6)
]

with self.assertRaises(TypeError):
series = leather.Series(data, self.shape)

with self.assertRaises(TypeError):
series = leather.Series(data, self.shape, x='words', y='more')

def get_x(row, i):
return row.b

def get_y(row, i):
return row.c

series = leather.Series(data, self.shape, x=get_x, y=get_y)

self.assertSequenceEqual(series.values(X), [1, 2, 3])
self.assertSequenceEqual(series.values(Y), [4, 5, 6])

0 comments on commit 3d4df4e

Please sign in to comment.