Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Grouped bars 2 #70

Closed
wants to merge 15 commits into from
4 changes: 4 additions & 0 deletions examples/charts/grouped_bars.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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_bars.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import leather
from datetime import date

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

chart = leather.Chart('Bars')
chart.add_grouped_bars(data)
chart.to_svg('examples/charts/grouped_bars.svg')
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, style_function
from leather.shapes import Shape, Bars, Columns, Dots, Line, GroupedBars, GroupedColumns, style_function
from leather.testcase import LeatherTestCase
from leather import theme
42 changes: 35 additions & 7 deletions leather/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
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
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, DIMENSION_NAMES, Box, IPythonSVG, warn
from leather.utils import X, Y, Z, DIMENSION_NAMES, Box, IPythonSVG, warn


class Chart(object):
Expand All @@ -28,9 +28,9 @@ def __init__(self, title=None):
self._series_colors = theme.default_series_colors

self._layers = []
self._types = [None, None]
self._scales = [None, None]
self._axes = [None, None]
self._types = [None, None, None]
self._scales = [None, None, None]
self._axes = [None, None, None]

def _palette(self):
"""
Expand Down Expand Up @@ -166,6 +166,26 @@ def add_line(self, data, x=None, y=None, name=None, stroke_color=None, width=Non
Line(stroke_color, width)
)

def add_grouped_bars(self, data, x=None, y=None, z=None, name=None, fill_color=None):
"""
Create and add a :class:`.CategorySeries` rendered with
:class:`.GroupedBars`.
"""
self.add_series(
CategorySeries(data, x=x, y=y, z=z, name=name),
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 Expand Up @@ -297,8 +317,16 @@ def to_svg_group(self, width=None, height=None):
margin_group.append(body_group)

# Axes
x_scale, x_axis = self._validate_dimension(X)
y_scale, y_axis = self._validate_dimension(Y)
if isinstance(self._layers[0][0], CategorySeries):
if self._layers[0][1].legend_dimension() is X:
x_scale, x_axis = self._validate_dimension(Z)
y_scale, y_axis = self._validate_dimension(Y)
elif self._layers[0][1].legend_dimension() is Y:
x_scale, x_axis = self._validate_dimension(X)
y_scale, y_axis = self._validate_dimension(Z)
else:
x_scale, x_axis = self._validate_dimension(X)
y_scale, y_axis = self._validate_dimension(Y)

bottom_margin = x_axis.estimate_label_margin(x_scale, 'bottom')
left_margin = y_axis.estimate_label_margin(y_scale, 'left')
Expand Down
4 changes: 2 additions & 2 deletions leather/scales/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import six

from leather.data_types import Date, DateTime, Number, Text
from leather.shapes import Bars, Columns
from leather.shapes import Bars, Columns, GroupedBars, GroupedColumns


class Scale(object):
Expand Down Expand Up @@ -55,7 +55,7 @@ def infer(cls, layers, dimension, data_type):
data_max = None

for series, shape in layers:
if isinstance(shape, (Bars, Columns)):
if isinstance(shape, (Bars, Columns, GroupedBars, GroupedColumns)):
force_zero = True

if data_min is None:
Expand Down
3 changes: 2 additions & 1 deletion leather/scales/ordinal.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ class Ordinal(Scale):
A scale that maps individual values (e.g. strings) to a range.
"""
def __init__(self, domain):
self._domain = domain
seen = set()
self._domain = [v for v in domain if v not in seen and not seen.add(v)]

def contains(self, v):
"""
Expand Down
15 changes: 15 additions & 0 deletions leather/series/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,21 @@ def values(self, dimension):

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

def unique_values(self, dimension):
"""
Return all unique values in the category field.
"""
z = self._keys[dimension]
unique_values = []

for i, row in enumerate(self._data):
value = z(row, i)

if value not in unique_values:
unique_values.append(value)

return unique_values

def min(self, dimension):
"""
Compute the minimum value of a given dimension.
Expand Down
20 changes: 8 additions & 12 deletions leather/series/category.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,28 +61,24 @@ 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):
"""
Return all unique values in the category field.
"""
z = self._keys[Z]
categories = []

for i, row in enumerate(self._data):
cat = z(row, i)

if cat not in categories:
categories.append(cat)

return categories
return self.unique_values(Z)
2 changes: 2 additions & 0 deletions leather/shapes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
from leather.shapes.columns import Columns
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
111 changes: 111 additions & 0 deletions leather/shapes/category.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env python

import six
import xml.etree.ElementTree as ET

from leather import theme
from leather.shapes.base import Shape
from leather.utils import issequence


class CategoryShape(Shape):
"""
Base class for shapes that can be used to render data :class:`.CategorySeries`.

Extends the base :class:`.Shape` class.
"""
def legend_dimension(self):
return self._legend_dimension

def legend_labels(self, series, palette):
"""
Generate a list of tuples with labels mappeds to colors for the legend.
"""
if self._fill_color is not None:
colors = self._fill_color
else:
colors = list(palette)

label_colors = []
legend_dimension = self._legend_dimension
legend_values = series.unique_values(legend_dimension)

if issequence(colors):
colors = list(colors)
color_count = len(colors)

for i, value in enumerate(legend_values):
if i >= color_count:
raise ValueError('Fill color must have length greater than or equal to the number of unique values in all categories.')

label_colors.append((value, colors[i]))

elif callable(colors):
# TODO
label_colors = []

else:
raise ValueError('Fill color must be a sequence of strings or a style function.')

return label_colors

def legend_to_svg(self, series, palette):
"""
Render the legend entries for these shapes.
"""
label_colors = self.legend_labels(series, palette)
item_groups = []

if hasattr(self, '_stroke_color'):
if self._stroke_color:
if callable(self._stroke_color):
# TODO
stroke_color = 'black'
else:
stroke_color = self._stroke_color
else:
stroke_color = next(palette)
else:
stroke_color = None

bubble_width = theme.legend_bubble_size + theme.legend_bubble_offset

for label, fill_color in label_colors:
text = six.text_type(label) if label is not None else 'Unnamed label'
text_width = (len(text) + 4) * theme.legend_font_char_width

item_width = text_width + bubble_width

# Group
item_group = ET.Element('g')

# Bubble
bubble = ET.Element('rect',
x=six.text_type(0),
y=six.text_type(-theme.legend_font_char_height + theme.legend_bubble_offset),
width=six.text_type(theme.legend_bubble_size),
height=six.text_type(theme.legend_bubble_size)
)

if fill_color:
bubble.set('fill', fill_color)
elif stroke_color:
bubble.set('fill', stroke_color)

item_group.append(bubble)

# Label
label = ET.Element('text',
x=six.text_type(bubble_width),
y=six.text_type(0),
fill=theme.legend_color
)
label.set('font-family', theme.legend_font_family)
label.set('font-size', six.text_type(theme.legend_font_size))
label.text = text

item_group.append(label)

item_groups.append((item_group, item_width))

return item_groups
Loading