Skip to content

Commit

Permalink
Implement experimental Annual scale. #35.
Browse files Browse the repository at this point in the history
  • Loading branch information
onyxfish committed May 26, 2016
1 parent bf92490 commit 492f44f
Show file tree
Hide file tree
Showing 10 changed files with 110 additions and 11 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
0.3.0
-----

* Implement :class:`.Annual` scale type.
* Zero lines now render above other tick marks. (#31)
* Fixed rendering of :class:`.Bar` and :class:`.Column` shapes for negative values.
* Fixed rendering of :class:`.Bar` and :class:`.Column` shapes for negative values. (#52)
* Refactored the :class:`.Lattice` API.

0.2.0
Expand Down
2 changes: 2 additions & 0 deletions docs/api/scales.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ Scales
.. autoclass:: leather.Ordinal

.. autoclass:: leather.Temporal

.. autoclass:: leather.Annual
2 changes: 1 addition & 1 deletion leather/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from leather.grid import Grid
from leather.lattice import Lattice
from leather.legend import Legend
from leather.scales import Scale, Linear, Ordinal, Temporal
from leather.scales import Scale, Annual, Linear, Ordinal, Temporal
from leather.series import Series, key_function
from leather.shapes import Shape, Bars, Columns, Dots, Lines, style_function
from leather import theme
10 changes: 6 additions & 4 deletions leather/axis.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class Axis(object):
"""
def __init__(self, ticks=None, tick_formatter=None, name=None):
self._ticks = ticks or theme.default_ticks
self._tick_formatter = tick_formatter or tick_format_function
self._tick_formatter = tick_formatter
self._name = name

def _estimate_left_tick_width(self, scale):
Expand All @@ -29,10 +29,11 @@ def _estimate_left_tick_width(self, scale):
"""
ticks = scale.ticks(self._ticks)
tick_count = len(ticks)
tick_formatter = self._tick_formatter or scale.format_tick
max_len = 0

for i, value in enumerate(ticks):
max_len = max(max_len, len(self._tick_formatter(value, i, tick_count)))
max_len = max(max_len, len(tick_formatter(value, i, tick_count)))

return max_len * theme.tick_font_char_width

Expand Down Expand Up @@ -100,6 +101,7 @@ def to_svg(self, width, height, scale, orient):

tick_values = scale.ticks(self._ticks)
tick_count = len(tick_values)
tick_formatter = self._tick_formatter or scale.format_tick

zero_tick_group = None

Expand Down Expand Up @@ -161,12 +163,12 @@ def to_svg(self, width, height, scale, orient):
label.set('text-anchor', text_anchor)
label.set('font-family', theme.tick_font_family)

value = self._tick_formatter(value, i, tick_count)
value = tick_formatter(value, i, tick_count)
label.text = six.text_type(value)

tick_group.append(label)

if zero_tick_group:
if zero_tick_group is not None:
group.append(zero_tick_group)

return group
Expand Down
1 change: 1 addition & 0 deletions leather/scales/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env python

from leather.scales.annual import Annual
from leather.scales.base import Scale
from leather.scales.linear import Linear
from leather.scales.ordinal import Ordinal
Expand Down
77 changes: 77 additions & 0 deletions leather/scales/annual.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env python

from datetime import datetime, date

from leather.scales.temporal import Temporal


class Annual(Temporal):
"""
A scale that maps years to a pixel range.
This scale takes linear values (dates, datetimes, or numbers), but treats
them as ordinal values for purposes of projection. Thus you can use this
scale to render :class:`.Bars` or :class:`.Columns` for yearly data.
:param domain_min:
The minimum value of the domain.
:param domain_max:
The maximum value of the domain.
"""
def __init__(self, domain_min, domain_max):
self._min = self._value_as_date(domain_min)
self._max = self._value_as_date(domain_max)

def _value_as_date(self, value):
"""
Convert a date or number to a date for consistent logic.
"""
if isinstance(value, (datetime, date)):
return value
elif isinstance(value, (int, float)):
return date(value, 1, 1)

raise ValueError('Unsupported domain value for Annual scale.')

def project(self, value, range_min, range_max):
"""
Project a value in this scale's domain to a target range.
"""
d = self._value_as_date(value)

segments = self._max.year - self._min.year + 1
segment_size = (range_max - range_min) / segments

pos = d.year - self._min.year

return range_min + ((pos + 0.5) * segment_size)

def project_interval(self, value, range_min, range_max):
"""
Project a value in this scale's domain to an interval in the target
range. This is used for places :class:`.Bars` and :class:`.Columns`.
"""
d = self._value_as_date(value)

segments = self._max.year - self._min.year + 1
segment_size = (range_max - range_min) / segments
gap = segment_size * 0.05

pos = d.year - self._min.year

a = range_min + ((pos) * segment_size) + gap
b = range_min + ((pos + 1) * segment_size) - gap

return (a, b)

def ticks(self, count):
"""
Generate a series of ticks for this scale.
"""
return [date(year, 1, 1) for year in range(self._min.year, self._max.year + 1)]

def format_tick(self, value, i, count):
"""
Display only year component.
"""
return value.year
11 changes: 11 additions & 0 deletions leather/scales/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from datetime import date, datetime

import six

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

Expand Down Expand Up @@ -101,3 +103,12 @@ def ticks(self, count):
Generate a series of ticks for this scale.
"""
raise NotImplementedError

def format_tick(self, value, i, count):
"""
Format ticks for display.
This method is used as a default which will be ignored if the user
provides a custom tick formatter to the axis.
"""
return six.text_type(value)
1 change: 1 addition & 0 deletions leather/scales/ordinal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from leather.scales.base import Scale


class Ordinal(Scale):
"""
A scale that maps individual values (e.g. strings) to a range.
Expand Down
2 changes: 0 additions & 2 deletions leather/scales/temporal.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
#!/usr/bin/env python

from datetime import date, datetime, time

from leather.scales.base import Scale


Expand Down
12 changes: 9 additions & 3 deletions test.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import leather

import datetime

data = [
(-1, -1),
(0, 0),
(1, 1)
(datetime.date(2010, 1, 1), -1),
(datetime.date(2011, 1, 1), -1),
(datetime.date(2012, 1, 1), 0),
(datetime.date(2013, 1, 1), 1)
]

chart = leather.Chart()
chart.add_columns(data)
chart.add_lines(data)
chart.add_dots(data)
chart.set_x_scale(leather.Annual(datetime.date(2010, 1, 1), datetime.date(2014, 1, 1)))
chart.to_svg('test.svg')

0 comments on commit 492f44f

Please sign in to comment.