Skip to content

Commit

Permalink
Adding GroupedColumns. wireservice#26
Browse files Browse the repository at this point in the history
  • Loading branch information
nbedi committed Jun 16, 2016
1 parent 4465593 commit 5f1ce06
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 7 deletions.
4 changes: 4 additions & 0 deletions examples/charts/grouped_columns.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions examples/grouped_columns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import leather
from datetime import date

data = [
('Hello', 1, 'first'),
('World', 5, 'first'),
('Hello', 7, 'second'),
('Goodbye', 4, 'second'),
('Hello', 7, 'third'),
('Goodbye', 3, 'third'),
('Yellow', 4, 'third')
]

chart = leather.Chart('Columns')
chart.add_grouped_columns(data)
chart.to_svg('examples/charts/grouped_columns.svg')
2 changes: 1 addition & 1 deletion leather/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
from leather.lattice import Lattice
from leather.scales import Scale, Linear, Ordinal, Temporal
from leather.series import Series, CategorySeries, key_function
from leather.shapes import Shape, Bars, Columns, Dots, Line, GroupedBars, style_function
from leather.shapes import Shape, Bars, Columns, Dots, Line, GroupedBars, GroupedColumns, style_function
from leather.testcase import LeatherTestCase
from leather import theme
12 changes: 11 additions & 1 deletion leather/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from leather.data_types import Date, DateTime
from leather.scales import Scale, Linear, Temporal
from leather.series import Series, CategorySeries
from leather.shapes import Bars, Columns, Dots, Line, GroupedBars
from leather.shapes import Bars, Columns, Dots, Line, GroupedBars, GroupedColumns
import leather.svg as svg
from leather import theme
from leather.utils import X, Y, Z, DIMENSION_NAMES, Box, IPythonSVG, warn
Expand Down Expand Up @@ -176,6 +176,16 @@ def add_grouped_bars(self, data, x=None, y=None, z=None, name=None, fill_color=N
GroupedBars(fill_color)
)

def add_grouped_columns(self, data, x=None, y=None, z=None, name=None, fill_color=None):
"""
Create and add a :class:`.CategorySeries` rendered with
:class:`.GroupedColumns`.
"""
self.add_series(
CategorySeries(data, x=x, y=y, z=z, name=name),
GroupedColumns(fill_color)
)

def _validate_dimension(self, dimension):
"""
Validates that the given scale and axis are valid for the data that
Expand Down
9 changes: 7 additions & 2 deletions leather/series/category.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,20 @@ def __init__(self, data, x=None, y=None, z=None, name=None):
self._infer_type(self._keys[Z])
]

def data(self):
def data(self, reverse=False):
"""
Return data for this series grouped for rendering.
"""
x = self._keys[X]
y = self._keys[Y]
z = self._keys[Z]

for i, row in enumerate(self._data):
if reverse:
increment = -1
else:
increment = 1

for i, row in enumerate(self._data[::increment]):
yield Datum(i, x(row, i), y(row, i), z(row, i), row)

def categories(self):
Expand Down
1 change: 1 addition & 0 deletions leather/shapes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
from leather.shapes.dots import Dots
from leather.shapes.line import Line
from leather.shapes.grouped_bars import GroupedBars
from leather.shapes.grouped_columns import GroupedColumns
4 changes: 1 addition & 3 deletions leather/shapes/grouped_bars.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@ def to_svg(self, width, height, x_scale, y_scale, series, palette):
category_counts = {c: series.values(Z).count(c) for c in categories}
seen_counts = {c: 0 for c in categories}

# Bars display "top-down"
for i, d in enumerate(series.data()):
for d in series.data():
if d.x is None or d.y is None:
continue

Expand All @@ -72,7 +71,6 @@ def to_svg(self, width, height, x_scale, y_scale, series, palette):

if callable(fill_color):
color = fill_color(d)
print(color)
else:
color = dict(label_colors)[d.y]

Expand Down
87 changes: 87 additions & 0 deletions leather/shapes/grouped_columns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/usr/bin/env python

import xml.etree.ElementTree as ET

import six

from leather.series import CategorySeries
from leather.shapes.category import CategoryShape
from leather.utils import X, Z
from leather import theme


class GroupedColumns(CategoryShape):
"""
Render a categorized series of data as grouped columns.
:param fill_color:
A sequence of colors to fill the columns. The sequence must have length
greater than or equal to the number of unique values in all categories.
You may also specify a :func:`.style_function`.
"""
def __init__(self, fill_color=None):
self._fill_color = fill_color
self._legend_dimension = X

def validate_series(self, series):
"""
Verify this shape can be used to render a given series.
"""
if not isinstance(series, CategorySeries):
raise ValueError('GroupedColumns can only be used to render CategorySeries.')

def to_svg(self, width, height, x_scale, y_scale, series, palette):
"""
Render columns to SVG elements.
"""
group = ET.Element('g')
group.set('class', 'series grouped-columns')

zero_y = y_scale.project(0, height, 0)

if self._fill_color:
fill_color = self._fill_color
else:
fill_color = list(palette)

label_colors = self.legend_labels(series, fill_color)

categories = series.categories()
category_counts = {c: series.values(Z).count(c) for c in categories}
seen_counts = {c: 0 for c in categories}

for d in series.data():
if d.x is None or d.y is None:
continue

x1, x2 = x_scale.project_interval(d.z, 0, width)

group_width = (x1 - x2) / category_counts[d.z]
x1 = x2 + (group_width * (seen_counts[d.z] + 1)) + 1
x2 = x2 + (group_width * seen_counts[d.z])

proj_y = y_scale.project(d.y, height, 0)

if d.y < 0:
column_y = zero_y
column_height = proj_y - zero_y
else:
column_y = proj_y
column_height = zero_y - proj_y

if callable(fill_color):
color = fill_color(d)
else:
color = dict(label_colors)[d.x]

seen_counts[d.z] += 1

group.append(ET.Element('rect',
x=six.text_type(x1),
y=six.text_type(column_y),
width=six.text_type(x2 - x1),
height=six.text_type(column_height),
fill=color
))

return group
72 changes: 72 additions & 0 deletions tests/test_shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,75 @@ def test_nulls(self):
self.assertEqual(len(rects), 2)
self.assertEqual(float(rects[0].get('x')), 0)
self.assertEqual(float(rects[0].get('width')), 0)

class TestGroupedColumns(leather.LeatherTestCase):
def setUp(self):
self.shape = leather.GroupedColumns()
self.linear = leather.Linear(0, 10)
self.ordinal = leather.Ordinal(['first', 'second', 'third'])
self.palette = (color for color in ['red', 'white', 'blue', 'yellow'])
self.rows = [
('foo', 1, 'first'),
('bar', 5, 'first'),
('foo', 7, 'second'),
('bing', 4, 'second'),
('foo', 7, 'third'),
('bar', 3, 'third'),
('buzz', 4, 'third')
]

def test_to_svg(self):
series = leather.CategorySeries(self.rows)

group = self.shape.to_svg(100, 200, self.ordinal, self.linear, series, self.palette)
rects = list(group)

self.assertEqual(len(rects), 7)
self.assertEqual(float(rects[1].get('y')), 100)
self.assertEqual(float(rects[1].get('height')), 100)
self.assertEqual(float(rects[3].get('y')), 120)
self.assertEqual(float(rects[3].get('height')), 80)
self.assertEqual(rects[1].get('fill'), 'white')

def test_invalid_fill_color(self):
series = leather.CategorySeries(self.rows)

with self.assertRaises(ValueError):
group = self.shape.to_svg(100, 200, self.ordinal, self.linear, series, ['one', 'two'])

with self.assertRaises(ValueError):
shape = leather.GroupedColumns('red')
shape.to_svg(100, 100, self.ordinal, self.linear, series, self.palette)

def test_style_function(self):
def color_code(d):
if d.x == 'foo':
return 'green'
else:
return 'black'

shape = leather.GroupedColumns(color_code)
series = leather.CategorySeries(self.rows)

group = shape.to_svg(100, 200, self.ordinal, self.linear, series, self.palette)
rects = list(group)

self.assertEqual(rects[0].get('fill'), 'green')
self.assertEqual(rects[1].get('fill'), 'black')
self.assertEqual(rects[2].get('fill'), 'green')
self.assertEqual(rects[3].get('fill'), 'black')
self.assertEqual(rects[4].get('fill'), 'green')

def test_nulls(self):
series = leather.CategorySeries([
('foo', 0, 'first'),
(None, None, None),
('bing', 10, 'third')
])

group = self.shape.to_svg(100, 200, self.ordinal, self.linear, series, self.palette)
rects = list(group)

self.assertEqual(len(rects), 2)
self.assertEqual(float(rects[1].get('y')), 0)
self.assertEqual(float(rects[1].get('height')), 200)

0 comments on commit 5f1ce06

Please sign in to comment.