Skip to content

Commit

Permalink
Implement Monthly scale and add a bunch of tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
onyxfish committed May 26, 2016
1 parent 492f44f commit 49cb966
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 10 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
0.3.0
-----

* Implement :class:`.Annual` scale type.
* Expanded unit tests for :class:`.Scale` subclasses.
* Implemented :class:`.Monthly` scale type.
* Implemented :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. (#52)
* Refactored the :class:`.Lattice` API.
Expand Down
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, Annual, Linear, Ordinal, Temporal
from leather.scales import Scale, Annual, Linear, Monthly, Ordinal, Temporal
from leather.series import Series, key_function
from leather.shapes import Shape, Bars, Columns, Dots, Lines, style_function
from leather import theme
1 change: 1 addition & 0 deletions leather/scales/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
from leather.scales.annual import Annual
from leather.scales.base import Scale
from leather.scales.linear import Linear
from leather.scales.monthly import Monthly
from leather.scales.ordinal import Ordinal
from leather.scales.temporal import Temporal
23 changes: 19 additions & 4 deletions leather/scales/annual.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
#!/usr/bin/env python

from datetime import datetime, date
from decimal import Decimal
import math

from leather.scales.temporal import Temporal


class Annual(Temporal):
"""
A scale that maps years to a pixel range.
A scale that maps years to a coordinate 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
Expand All @@ -28,7 +30,7 @@ def _value_as_date(self, value):
"""
if isinstance(value, (datetime, date)):
return value
elif isinstance(value, (int, float)):
elif isinstance(value, (int, float, Decimal)):
return date(value, 1, 1)

raise ValueError('Unsupported domain value for Annual scale.')
Expand All @@ -44,7 +46,12 @@ def project(self, value, range_min, range_max):

pos = d.year - self._min.year

return range_min + ((pos + 0.5) * segment_size)
if pos >= 0:
pos += 0.5
else:
pos -= 0.5

return range_min + (pos * segment_size)

def project_interval(self, value, range_min, range_max):
"""
Expand All @@ -68,7 +75,15 @@ 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)]
size = int(math.ceil(float(self._max.year - self._min.year) / count))
values = []

for i in range(count):
years = self._min.year + (i * size)

values.append(date(years, 1, 1))

return values

def format_tick(self, value, i, count):
"""
Expand Down
18 changes: 17 additions & 1 deletion leather/scales/temporal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#!/usr/bin/env python

from datetime import datetime

import six

from leather.scales.base import Scale


Expand All @@ -20,7 +24,19 @@ def project(self, value, range_min, range_max):
"""
Project a value in this scale's domain to a target range.
"""
pos = (value - self._min) / (self._max - self._min)
numerator = value - self._min
denominator = self._max - self._min

# Python 2 does not support timedelta division
if six.PY2:
if isinstance(self._min, datetime):
numerator = numerator.total_seconds()
denominator = denominator.total_seconds()
else:
numerator = float(numerator.days)
denominator = float(denominator.days)

pos = numerator / denominator

return ((range_max - range_min) * pos) + range_min

Expand Down
7 changes: 4 additions & 3 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

data = [
(datetime.date(2010, 1, 1), -1),
(datetime.date(2011, 1, 1), -1),
(datetime.date(2012, 1, 1), 0),
(datetime.date(2010, 6, 1), 0),
(datetime.date(2011, 1, 1), -0.5),
(datetime.date(2012, 1, 1), 0.5),
(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.set_x_scale(leather.Monthly(datetime.date(2010, 1, 1), datetime.date(2014, 1, 1)))
chart.to_svg('test.svg')
147 changes: 147 additions & 0 deletions tests/test_scales.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#!/usr/bin/env python

from datetime import date, datetime

try:
import unittest2 as unittest
except ImportError:
Expand Down Expand Up @@ -43,3 +45,148 @@ def test_ticks(self):

self.assertEqual(scale.ticks(5), [0, 2.5, 5, 7.5, 10])
self.assertEqual(scale.ticks(6), [0, 2, 4, 6, 8, 10])


class TestOrdinal(XMLTest):
def test_project(self):
scale = leather.Ordinal(['a', 'b', 'c', 'd'])

self.assertEqual(scale.project('b', 0, 20), 7.5)

scale = leather.Ordinal(['a', 'd', 'c', 'b'])

self.assertEqual(scale.project('b', 0, 20), 17.5)

def test_project_interval(self):
scale = leather.Ordinal(['a', 'b', 'c', 'd'])

self.assertEqual(scale.project_interval('b', 0, 20), (5.25, 9.75))

scale = leather.Ordinal(['a', 'd', 'c', 'b'])

self.assertEqual(scale.project_interval('b', 0, 20), (15.25, 19.75))

def test_ticks(self):
scale = leather.Ordinal(['a', 'b', 'c', 'd'])

self.assertEqual(scale.ticks(4), ['a', 'b', 'c', 'd'])
self.assertEqual(scale.ticks(5), ['a', 'b', 'c', 'd'])
self.assertEqual(scale.ticks(6), ['a', 'b', 'c', 'd'])


class TestTemporal(XMLTest):
"""
Note: due to leap-year calculations, it's almost impossible to write
exact tests for this scale which are not trivial.
"""
def test_project(self):
scale = leather.Temporal(date(2010, 1, 1), date(2014, 1, 1))

self.assertAlmostEqual(scale.project(date(2011, 1, 1), 0, 20), 5, 1)
self.assertAlmostEqual(scale.project(date(2012, 1, 1), 0, 20), 10, 1)
self.assertAlmostEqual(scale.project(date(2009, 1, 1), 0, 20), -5, 1)

scale = leather.Temporal(datetime(2010, 1, 1), datetime(2014, 1, 1))

self.assertAlmostEqual(scale.project(datetime(2011, 1, 1), 0, 20), 5, 1)
self.assertAlmostEqual(scale.project(datetime(2012, 1, 1), 0, 20), 10, 1)
self.assertAlmostEqual(scale.project(datetime(2009, 1, 1), 0, 20), -5, 1)

def test_project_interval(self):
scale = leather.Temporal(date(2010, 1, 1), date(2014, 1, 1))

with self.assertRaises(NotImplementedError):
scale.project_interval(date(2011, 1, 1), 0, 20)

def test_ticks(self):
scale = leather.Temporal(date(2010, 1, 1), date(2014, 1, 1))

ticks = scale.ticks(5)
self.assertEqual(ticks[0], date(2010, 1, 1))
self.assertEqual(ticks[-1], date(2014, 1, 1))


class TestAnnual(XMLTest):
def test_project(self):
scale = leather.Annual(date(2010, 1, 1), date(2014, 1, 1))

self.assertEqual(scale.project(date(2011, 1, 1), 0, 20), 6)
self.assertEqual(scale.project(date(2012, 1, 1), 0, 20), 10)
self.assertEqual(scale.project(date(2009, 1, 1), 0, 20), -6)

scale = leather.Annual(datetime(2010, 1, 1), datetime(2014, 1, 1))

self.assertEqual(scale.project(datetime(2011, 1, 1), 0, 20), 6)
self.assertEqual(scale.project(datetime(2012, 1, 1), 0, 20), 10)
self.assertEqual(scale.project(datetime(2009, 1, 1), 0, 20), -6)

scale = leather.Annual(2010, 2014)

self.assertEqual(scale.project(2011, 0, 20), 6)
self.assertEqual(scale.project(2012, 0, 20), 10)
self.assertEqual(scale.project(2009, 0, 20), -6)

def test_project_interval(self):
scale = leather.Annual(date(2010, 1, 1), date(2014, 1, 1))

self.assertEqual(scale.project_interval(date(2011, 1, 1), 0, 20), (4.2, 7.8))

def test_ticks(self):
scale = leather.Annual(date(2010, 1, 1), date(2014, 1, 1))

self.assertEqual(scale.ticks(5), [
date(2010, 1, 1),
date(2011, 1, 1),
date(2012, 1, 1),
date(2013, 1, 1),
date(2014, 1, 1)
])


class TestMonthly(XMLTest):
"""
See notes for :class:`.TestTemporal`.
"""
def test_project(self):
scale = leather.Monthly(date(2010, 1, 1), date(2014, 1, 1))

self.assertAlmostEqual(scale.project(date(2011, 1, 1), 0, 48), 12, 0)
self.assertAlmostEqual(scale.project(date(2012, 1, 1), 0, 48), 24, 0)
self.assertAlmostEqual(scale.project(date(2008, 12, 1), 0, 48), -12, 0)

scale = leather.Monthly(datetime(2010, 1, 1), datetime(2014, 1, 1))

self.assertAlmostEqual(scale.project(datetime(2011, 1, 1), 0, 48), 12, 0)
self.assertAlmostEqual(scale.project(datetime(2012, 1, 1), 0, 48), 24, 0)
self.assertAlmostEqual(scale.project(datetime(2008, 12, 1), 0, 48), -12, 0)

with self.assertRaises(ValueError):
scale = leather.Monthly(2010, 2014)

def test_project_interval(self):
scale = leather.Monthly(date(2010, 1, 1), date(2014, 1, 1))

a, b = scale.project_interval(date(2011, 1, 1), 0, 48)
self.assertAlmostEqual(a, 11.5, 0)
self.assertAlmostEqual(b, 12.5, 0)

def test_ticks(self):
scale = leather.Monthly(date(2010, 1, 1), date(2014, 1, 1))

self.assertEqual(scale.ticks(5), [
date(2010, 1, 1),
date(2011, 1, 1),
date(2012, 1, 1),
date(2013, 1, 1),
date(2014, 1, 1)
])

scale = leather.Monthly(date(2010, 1, 1), date(2012, 1, 1))

self.assertEqual(scale.ticks(5), [
date(2010, 1, 1),
date(2010, 7, 1),
date(2011, 1, 1),
date(2011, 7, 1),
date(2012, 1, 1)
])

0 comments on commit 49cb966

Please sign in to comment.