pax_global_header00006660000000000000000000000064130177114240014512gustar00rootroot0000000000000052 comment=e85dd30cc20270180c26c56fd895aff61e84f741 leather-0.3.3/000077500000000000000000000000001301771142400131415ustar00rootroot00000000000000leather-0.3.3/.gitignore000066400000000000000000000001551301771142400151320ustar00rootroot00000000000000.DS_Store *.pyc *.swp *.swo .tox *.egg-info docs/_build dist .coverage build .proof .ipynb_checkpoints .idea leather-0.3.3/.travis.yml000066400000000000000000000004531301771142400152540ustar00rootroot00000000000000language: python python: - "2.7" - "3.3" - "3.4" - "3.5" # command to install dependencies install: - if [[ $TRAVIS_PYTHON_VERSION == 3* ]]; then pip install -r requirements-py3.txt; else pip install -r requirements-py2.txt; fi # command to run tests script: nosetests tests sudo: false leather-0.3.3/AUTHORS.rst000066400000000000000000000002231301771142400150150ustar00rootroot00000000000000The following individuals have contributed code, documentation, or expertise to leather: * `Christopher Groskopf `_ leather-0.3.3/CHANGELOG.rst000066400000000000000000000041071301771142400151640ustar00rootroot000000000000000.3.3 - November 30, 2016 ------------------------- * Fix examples that used invalid column data. (#81) * lxml is no longer imported by default. (#83) * Ordinal scales can now display data from multiple series with different values. (#76) * Better error handling for data types supported by different shapes. 0.3.2 - November 11, 2016 ------------------------- * Fix trove classifiers. 0.3.1 - November 11, 2016 ------------------------- * Fix unicode rendering issue in Python2.7 and PyPy. (#74) 0.3.0 - November 11, 2016 ------------------------- * Add examples for many more use-cases. (#11) * Fixed bars so that data are displayed top-down when using :meth:`.Chart.add_bars`. (#72) * Changed default colors. (#51) * Fixed a rare file handling bug when saving SVG files. * Leather will now issue a warning if you attempt to render a chart with data exceeding the scale domain. (#42) * Linear scales will now default to the domain :code:`[0, 1]` if no values are provided. (#66) * Axis no longer takes a number of ticks as an argument. Instead pass a list of custom tick values. * Scales :code:`tick` methods no longer take a number of ticks as an argument. (They should self-optimize.) * Scales that cross :code:`0` will now always have a tick at :code:`0`. (#54) * Implemented auto-ticking. (#23) * :func:`.style_function` now takes a :class:`.Datum` instances, rather than a list of arguments. * Renamed the :code:`Lines` class to :class:`.Line` to be more accurate. * Implemented :class:`.CategorySeries`. * Implemented a more elegant pattern for colorizing series. * Refactored :class:`.Series` so :class:`.Shape` is no longer a parameter. * Tick values can now be overridden with the :code:`tick_values` argument. (#56) * Added methods to customize scales and axes for :class:`.Lattice` charts. (#17) * Expanded unit tests for :class:`.Scale` subclasses. * 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. 0.2.0 ----- * Initial prototype 0.1.0 ----- * Never released leather-0.3.3/COPYING000066400000000000000000000021131301771142400141710ustar00rootroot00000000000000The MIT License Copyright (c) 2016 Christopher Groskopf and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. leather-0.3.3/README.rst000066400000000000000000000021731301771142400146330ustar00rootroot00000000000000.. image:: https://travis-ci.org/wireservice/leather.png :target: https://travis-ci.org/wireservice/leather :alt: Build status .. image:: https://img.shields.io/pypi/dw/leather.svg :target: https://pypi.python.org/pypi/leather :alt: PyPI downloads .. image:: https://img.shields.io/pypi/v/leather.svg :target: https://pypi.python.org/pypi/leather :alt: Version .. image:: https://img.shields.io/pypi/l/leather.svg :target: https://pypi.python.org/pypi/leather :alt: License .. image:: https://img.shields.io/pypi/pyversions/leather.svg :target: https://pypi.python.org/pypi/leather :alt: Support Python versions Leather is the Python charting library for those who need charts *now* and don't care if they're perfect. Leather isn't picky. It's rough. It gets dirty. It looks sexy just hanging on the back of a chair. Leather doesn't need your accessories. Leather is how Snake Plissken would make charts. Get it? Important links: * Documentation: http://leather.rtfd.io * Repository: https://github.com/wireservice/leather * Issues: https://github.com/wireservice/leather/issues leather-0.3.3/docs/000077500000000000000000000000001301771142400140715ustar00rootroot00000000000000leather-0.3.3/docs/Makefile000066400000000000000000000107621301771142400155370ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/leather.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/leather.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/leather" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/leather" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." leather-0.3.3/docs/about.rst000066400000000000000000000010651301771142400157370ustar00rootroot00000000000000============= About leather ============= Why leather? ============ * A readable and user-friendly API. * Optimized for exploratory charting. * Produces scale-independent SVG charts. * Completely type-agnostic. Chart your data, whatever it is. * Designed with `iPython `_, `Jupyter `_ and `atom/hydrogen `_ in mind. * Pure Python. No C dependencies to compile. * MIT licensed and free for all purposes. * Zealously `zen `_. * Made with love. leather-0.3.3/docs/api.rst000066400000000000000000000002371301771142400153760ustar00rootroot00000000000000=== API === .. toctree:: :maxdepth: 1 api/chart api/grid api/lattice api/scales api/axis api/series api/shapes api/theme leather-0.3.3/docs/api/000077500000000000000000000000001301771142400146425ustar00rootroot00000000000000leather-0.3.3/docs/api/axis.rst000066400000000000000000000001331301771142400163350ustar00rootroot00000000000000==== Axis ==== .. automodule:: leather.axis :no-members: .. autoclass:: leather.Axis leather-0.3.3/docs/api/chart.rst000066400000000000000000000012661301771142400165020ustar00rootroot00000000000000===== Chart ===== .. automodule:: leather.chart :no-members: .. autosummary:: :nosignatures: leather.Chart Adding data ----------- .. autosummary:: :nosignatures: leather.Chart.add_series leather.Chart.add_bars leather.Chart.add_columns leather.Chart.add_dots leather.Chart.add_line Customizing ----------- .. autosummary:: :nosignatures: leather.Chart.set_x_scale leather.Chart.set_y_scale leather.Chart.set_x_axis leather.Chart.set_y_axis Rendering --------- .. autosummary:: :nosignatures: leather.Chart.to_svg leather.Chart.to_svg_group Detailed list ------------- .. autoclass:: leather.Chart :members: leather-0.3.3/docs/api/grid.rst000066400000000000000000000001331301771142400163160ustar00rootroot00000000000000==== Grid ==== .. automodule:: leather.grid :no-members: .. autoclass:: leather.Grid leather-0.3.3/docs/api/lattice.rst000066400000000000000000000001521301771142400170170ustar00rootroot00000000000000======= Lattice ======= .. automodule:: leather.lattice :no-members: .. autoclass:: leather.Lattice leather-0.3.3/docs/api/scales.rst000066400000000000000000000003041301771142400166430ustar00rootroot00000000000000====== Scales ====== .. automodule:: leather.scales :no-members: .. autoclass:: leather.Scale .. autoclass:: leather.Linear .. autoclass:: leather.Ordinal .. autoclass:: leather.Temporal leather-0.3.3/docs/api/series.rst000066400000000000000000000002151301771142400166640ustar00rootroot00000000000000====== Series ====== .. automodule:: leather.series :no-members: .. autoclass:: leather.Series .. autofunction:: leather.key_function leather-0.3.3/docs/api/shapes.rst000066400000000000000000000004051301771142400166560ustar00rootroot00000000000000====== Shapes ====== .. automodule:: leather.shapes :no-members: .. autoclass:: leather.Shape .. autoclass:: leather.Bars .. autoclass:: leather.Columns .. autoclass:: leather.Dots .. autoclass:: leather.Line .. autofunction:: leather.style_function leather-0.3.3/docs/api/theme.rst000066400000000000000000000001231301771142400164720ustar00rootroot00000000000000===== Theme ===== .. automodule:: leather.theme :members: :undoc-members: leather-0.3.3/docs/changelog.rst000066400000000000000000000000751301771142400165540ustar00rootroot00000000000000========= Changelog ========= .. include:: ../CHANGELOG.rst leather-0.3.3/docs/conf.py000066400000000000000000000017651301771142400154010ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import os import sys # Path munging sys.path.insert(0, os.path.abspath('..')) # Extensions extensions = [ 'sphinx.ext.autosummary', 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx' ] # autodoc_member_order = 'bysource' autodoc_default_flags = ['members', 'show-inheritance'] intersphinx_mapping = { 'python': ('http://docs.python.org/3.5', None) } # Templates templates_path = ['_templates'] master_doc = 'index' # Metadata project = u'leather' copyright = u'2016, Christopher Groskopf' version = '0.3.3' release = '0.3.3' exclude_patterns = ['_build'] pygments_style = 'sphinx' # HTMl theming html_theme = 'default' on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if not on_rtd: # only import and set the theme if we're building docs locally import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] html_static_path = ['_static'] htmlhelp_basename = 'leatherdoc' leather-0.3.3/docs/examples.rst000066400000000000000000000122751301771142400164500ustar00rootroot00000000000000======== Examples ======== Data series =========== Simple pairs ------------ .. literalinclude:: ../examples/simple_pairs.py :language: python .. figure:: ../examples/charts/simple_pairs.svg Table from csv.reader --------------------- Sequence row data, such as is returned by :func:`csv.reader` can be accessed by specifying the indices of the columns containing the :code:`x` and :code:`y` values. Note that leather does not automatically convert numerical strings, such as those stored in a CSV. If you want that you'll need to use a smarter table reader, such as `agate `_ .. literalinclude:: ../examples/csv_reader.py :language: python .. figure:: ../examples/charts/csv_reader.svg Table from csv.DictReader ------------------------- Dict row data, such as is returned by :class:`csv.DictReader` can be accessed by specifying the indices of the columns containing the :code:`x` and :code:`y` values. See previous example for note on strings from CSVs. .. literalinclude:: ../examples/csv_dict_reader.py :language: python .. figure:: ../examples/charts/csv_dict_reader.svg Custom data ----------- Completely custom data formats are also supported via accessor functions. .. literalinclude:: ../examples/custom_data.py :language: python .. figure:: ../examples/charts/custom_data.svg Multiple series --------------- Multiple data series can be displayed on a single chart so long as they all use the same type of :class:`.Scale`. .. literalinclude:: ../examples/multiple_series.py :language: python .. figure:: ../examples/charts/multiple_series.svg Shapes ====== Bars ---- .. literalinclude:: ../examples/bars.py :language: python .. figure:: ../examples/charts/bars.svg Columns ------- .. literalinclude:: ../examples/columns.py :language: python .. figure:: ../examples/charts/columns.svg Dots ---- .. literalinclude:: ../examples/dots.py :language: python .. figure:: ../examples/charts/dots.svg Lines ----- .. literalinclude:: ../examples/lines.py :language: python .. figure:: ../examples/charts/lines.svg Mixing shapes ------------- You can mix different shapes for different series on the same chart. .. literalinclude:: ../examples/mixed_shapes.py :language: python .. figure:: ../examples/charts/mixed_shapes.svg Scales ====== Linear ------ When using numerical data :class:`.Linear` scales are created automatically and by default. You may override the domain by adding a scale manually. .. literalinclude:: ../examples/linear.py :language: python .. figure:: ../examples/charts/linear.svg Ordinal ------- When using text data :class:`.Ordinal` scales are created automatically and by default. It is generally not useful to override these defaults. .. literalinclude:: ../examples/ordinal.py :language: python .. figure:: ../examples/charts/ordinal.svg Temporal -------- When using date/time data :class:`.Temporal` scales are created automatically and by default. You may override the domain by adding a scale manually. .. literalinclude:: ../examples/temporal.py :language: python .. figure:: ../examples/charts/temporal.svg Axes ==== Changing tick values -------------------- You can change the list of ticks that are displayed using :meth:`.Chart.add_x_axis` and :meth:`.Chart.add_y_axis` methods. This will not adjust automatically adjust the scale, so it is possible to pick tick values that are not displayed. .. literalinclude:: ../examples/ticks.py :language: python .. figure:: ../examples/charts/ticks.svg Customizing tick format ----------------------- You can provide a tick formatter method to change how ticks are displayed using the :meth:`.Chart.add_x_axis` and :meth:`.Chart.add_y_axis` methods. .. literalinclude:: ../examples/tick_format.py :language: python .. figure:: ../examples/charts/tick_format.svg Styling ======= Changing theme values --------------------- Chart styles are set using a dead simple :mod:`.theme` system. Leather is meant for making quick and dirty charts. It is neither expected nor recommended for user's to customize these styles. .. literalinclude:: ../examples/theme.py :language: python .. figure:: ../examples/charts/theme.svg Changing series colors ---------------------- More practically, individual default :class:`.Series` colors can be overriden when they are created. .. literalinclude:: ../examples/series_color.py :language: python .. figure:: ../examples/charts/series_color.svg Styling data based on value --------------------------- Style attributes of individual data points can be set by value using a :func:`.style_function`. .. literalinclude:: ../examples/colorized_dots.py :language: python .. figure:: ../examples/charts/colorized_dots.svg Chart grids =========== With mixed scales ----------------- You can add charts of completely different types to a single graphic by using :class:`.Grid`. .. literalinclude:: ../examples/grid.py :language: python .. figure:: ../examples/charts/grid.svg With consistent scales ---------------------- A grid of charts can automatically be synchronized to a consistent view using :class:`.Lattice`. .. literalinclude:: ../examples/lattice.py :language: python .. figure:: ../examples/charts/lattice.svg leather-0.3.3/docs/index.rst000066400000000000000000000021051301771142400157300ustar00rootroot00000000000000================= leather |release| ================= .. include:: ../README.rst .. toctree:: :hidden: :maxdepth: 2 about install examples api changelog release_process license Show me docs ============ * `About `_ - why you should use leather * `Install `_ - how to install for users and developers * `Examples `_ - code + output examples of every feature of leather * `API `_ - technical documentation for the leather API * `Changelog `_ - a record of every change made for each release Show me code ============ .. literalinclude:: ../examples/colorized_dots.py :language: python .. figure:: ../examples/charts/colorized_dots.svg Join us ======= * `Release process `_ - the process for maintainers to publish new releases * `License `_ - a copy of the MIT open source license covering leather Who we are ========== .. include:: ../AUTHORS.rst Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` leather-0.3.3/docs/install.rst000066400000000000000000000017571301771142400163030ustar00rootroot00000000000000============ Installation ============ Users ----- To use leather install it with pip:: pip install leather Developers ---------- If you are a developer that also wants to hack on leather, install it from git:: git clone git://github.com/wireservice/leather.git cd leather mkvirtualenv leather # If running Python 3 (strongly recommended for development) pip install -r requirements-py3.txt # If running Python 2 pip install -r requirements-py2.txt python setup.py develop tox .. note:: To run the leather tests with coverage:: nosetests --with-coverage tests Supported platforms ------------------- leather supports the following versions of Python: * Python 2.7 * Python 3.3+ * `PyPy `_ It is tested primarily on OSX, but due to its minimal dependencies it should work on both Linux and Windows. .. note:: `iPython `_ or `Jupyter `_ user? Leather works great there too. leather-0.3.3/docs/license.rst000066400000000000000000000000611301771142400162420ustar00rootroot00000000000000======= License ======= .. include:: ../COPYING leather-0.3.3/docs/release_process.rst000066400000000000000000000023111301771142400177760ustar00rootroot00000000000000=============== Release process =============== This is the release process for leather: 1. Verify all unit tests pass with fresh environments: ``tox -r``. 2. Check test coverage: ``nosetests --with-coverage tests``. 3. Ensure any new modules have been added to setup.py's ``packages`` list. #. Ensure any new public interfaces have been added to the documentation. #. Make sure the example scripts still work: ``./examples.sh``. #. Ensure ``CHANGELOG.rst`` is up to date. Add the release date and summary. #. Create a release tag: ``git tag -a x.y.z -m "x.y.z release."`` #. Push tags upstream: ``git push --tags`` #. If this is a major release, merge ``master`` into ``stable``: ``git checkout stable; git merge master; git push`` #. Upload to `PyPI `_: ``python setup.py sdist bdist_wheel upload``. #. Flag the release to build on `RTFD `_. #. Update the "default version" on `RTFD `_ to the latest. #. Rev to latest version: ``docs/conf.py``, ``setup.py`` and ``CHANGELOG.rst`` need updates. #. Commit revision: ``git commit -am "Update to version x.y.z for development."``. leather-0.3.3/examples.sh000077500000000000000000000001571301771142400153210ustar00rootroot00000000000000#!/bin/bash export PYTHONPATH="$PYTHONPATH:$(pwd)" for f in examples/*.py; do echo $f python $f done leather-0.3.3/examples/000077500000000000000000000000001301771142400147575ustar00rootroot00000000000000leather-0.3.3/examples/bars.py000066400000000000000000000002711301771142400162600ustar00rootroot00000000000000import leather data = [ (3, 'Hello'), (5, 'How'), (9, 'Are'), (4, 'You') ] chart = leather.Chart('Bars') chart.add_bars(data) chart.to_svg('examples/charts/bars.svg') leather-0.3.3/examples/charts/000077500000000000000000000000001301771142400162435ustar00rootroot00000000000000leather-0.3.3/examples/charts/.placeholder000066400000000000000000000000001301771142400205140ustar00rootroot00000000000000leather-0.3.3/examples/charts/bars.svg000066400000000000000000000051651301771142400177220ustar00rootroot00000000000000 Bars2.557.5100YouAreHowHelloleather-0.3.3/examples/charts/colorized_dots.svg000066400000000000000000000274271301771142400220230ustar00rootroot00000000000000 Colorized dots10020030001002003000leather-0.3.3/examples/charts/columns.svg000066400000000000000000000051241301771142400204460ustar00rootroot00000000000000 ColumnsHelloHowAreYou2.557.5100leather-0.3.3/examples/charts/csv_dict_reader.svg000066400000000000000000000104731301771142400221110ustar00rootroot00000000000000 Data from CSV reader1020300New ZealandCanadaUnited StatesIrelandGermanyNetherlandsDenmarkSwitzerlandAustraliaNorwayleather-0.3.3/examples/charts/csv_reader.svg000066400000000000000000000106361301771142400211070ustar00rootroot00000000000000 Data from CSV reader0.10.20.30New ZealandCanadaUnited StatesIrelandGermanyNetherlandsDenmarkSwitzerlandAustraliaNorwayleather-0.3.3/examples/charts/custom_data.svg000066400000000000000000000051701301771142400212720ustar00rootroot00000000000000 Line246802.557.510leather-0.3.3/examples/charts/dots.svg000066400000000000000000000053311301771142400177370ustar00rootroot00000000000000 Dots246802.557.510leather-0.3.3/examples/charts/grid.svg000066400000000000000000000126651301771142400177230ustar00rootroot00000000000000 Dots246802.557.510Lines345678246810leather-0.3.3/examples/charts/lattice.svg000066400000000000000000000171741301771142400204230ustar00rootroot00000000000000 First2.557.5100246810Second2.557.5100246810Third2.557.5100246810leather-0.3.3/examples/charts/linear.svg000066400000000000000000000051661301771142400202460ustar00rootroot00000000000000 Linear51015200-10-55100leather-0.3.3/examples/charts/lines.svg000066400000000000000000000051701301771142400201010ustar00rootroot00000000000000 Line246802.557.510leather-0.3.3/examples/charts/mixed_shapes.svg000066400000000000000000000063701301771142400214430ustar00rootroot00000000000000 Mixed shapesUnnamed seriesUnnamed series2.557.51002.557.5100leather-0.3.3/examples/charts/multiple_series.svg000066400000000000000000000065421301771142400222000ustar00rootroot00000000000000 Multiple seriesUnnamed seriesUnnamed series24680246810leather-0.3.3/examples/charts/ordinal.svg000066400000000000000000000051241301771142400204160ustar00rootroot00000000000000 OrdinalHelloHowAreYou2.557.5100leather-0.3.3/examples/charts/series_color.svg000066400000000000000000000051311301771142400214540ustar00rootroot00000000000000 Series colorHelloHowAreYou2.557.5100leather-0.3.3/examples/charts/simple_pairs.svg000066400000000000000000000053411301771142400214560ustar00rootroot00000000000000 Simple pairs246802.557.510leather-0.3.3/examples/charts/temporal.svg000066400000000000000000000060701301771142400206120ustar00rootroot00000000000000 Temporal2014-012014-072015-012015-072016-012016-072.557.510leather-0.3.3/examples/charts/theme.svg000066400000000000000000000065141301771142400200740ustar00rootroot00000000000000 ThemeUnnamed seriesUnnamed series2.557.51002.557.5100leather-0.3.3/examples/charts/tick_format.svg000066400000000000000000000052261301771142400212730ustar00rootroot00000000000000 Line2 (1/5)4 (2/5)6 (3/5)8 (4/5)0 (0/5)2.557.510leather-0.3.3/examples/charts/ticks.svg000066400000000000000000000040361301771142400201040ustar00rootroot00000000000000 Line1002.557.510leather-0.3.3/examples/colorized_dots.py000066400000000000000000000004771301771142400203640ustar00rootroot00000000000000import random import leather dot_data = [(random.randint(0, 250), random.randint(0, 250)) for i in range(100)] def colorizer(d): return 'rgb(%i, %i, %i)' % (d.x, d.y, 150) chart = leather.Chart('Colorized dots') chart.add_dots(dot_data, fill_color=colorizer) chart.to_svg('examples/charts/colorized_dots.svg') leather-0.3.3/examples/columns.py000066400000000000000000000003021301771142400170040ustar00rootroot00000000000000import leather data = [ ('Hello', 3), ('How', 5), ('Are', 9), ('You', 4) ] chart = leather.Chart('Columns') chart.add_columns(data) chart.to_svg('examples/charts/columns.svg') leather-0.3.3/examples/csv_dict_reader.py000066400000000000000000000006761301771142400204620ustar00rootroot00000000000000import csv import leather with open('examples/realdata/gii.csv') as f: reader = csv.DictReader(f) data = list(reader)[:10] for row in data: mmr = row['Maternal mortality ratio'] row['Maternal mortality ratio'] = float(mmr) if mmr is not None else None chart = leather.Chart('Data from CSV reader') chart.add_bars(data, x='Maternal mortality ratio', y='Country') chart.to_svg('examples/charts/csv_dict_reader.svg') leather-0.3.3/examples/csv_reader.py000066400000000000000000000005441301771142400174510ustar00rootroot00000000000000import csv import leather with open('examples/realdata/gii.csv') as f: reader = csv.reader(f) next(reader) data = list(reader)[:10] for row in data: row[1] = float(row[1]) if row[1] is not None else None chart = leather.Chart('Data from CSV reader') chart.add_bars(data, x=1, y=0) chart.to_svg('examples/charts/csv_reader.svg') leather-0.3.3/examples/custom_data.py000066400000000000000000000005541301771142400176400ustar00rootroot00000000000000import leather data = [ { 'x': 0, 'q': { 'y': [3] } }, { 'x': 4, 'q': { 'y': [5] } }, { 'x': 7, 'q': { 'y': [9] } }, { 'x': 8, 'q': { 'y': [4] } } ] def x(row, index): return row['x'] def y(row, index): return row['q']['y'][0] chart = leather.Chart('Line') chart.add_line(data, x=x, y=y) chart.to_svg('examples/charts/custom_data.svg') leather-0.3.3/examples/dots.py000066400000000000000000000002471301771142400163050ustar00rootroot00000000000000import leather data = [ (0, 3), (4, 5), (7, 9), (8, 4) ] chart = leather.Chart('Dots') chart.add_dots(data) chart.to_svg('examples/charts/dots.svg') leather-0.3.3/examples/grid.py000066400000000000000000000005401301771142400162550ustar00rootroot00000000000000import leather data1 = [ (0, 3), (4, 5), (7, 9), (8, 4) ] data2 = [ (3, 4), (5, 6), (7, 10), (8, 2) ] chart1 = leather.Chart('Dots') chart1.add_dots(data1) chart2 = leather.Chart('Lines') chart2.add_line(data2) grid = leather.Grid() grid.add_one(chart1) grid.add_one(chart2) grid.to_svg('examples/charts/grid.svg') leather-0.3.3/examples/lattice.py000066400000000000000000000005651301771142400167640ustar00rootroot00000000000000import leather data1 = [ (0, 3), (4, 5), (7, 9), (8, 4) ] data2 = [ (3, 4), (3, 4), (5, 6), (7, 10), (8, 2) ] data3 = [ (2, 4), (3, 5), (6, 2), (8, 3), (10, 5) ] lattice = leather.Lattice() lattice.add_many([data1, data2, data3], titles=['First', 'Second', 'Third']) lattice.to_svg('examples/charts/lattice.svg') leather-0.3.3/examples/linear.py000066400000000000000000000003371301771142400166060ustar00rootroot00000000000000import leather data = [ (0, 3), (4, 5), (7, 9), (8, 4) ] chart = leather.Chart('Linear') chart.add_x_scale(0, 20) chart.add_y_scale(-10, 10) chart.add_line(data) chart.to_svg('examples/charts/linear.svg') leather-0.3.3/examples/lines.py000066400000000000000000000002501301771142400164400ustar00rootroot00000000000000import leather data = [ (0, 3), (4, 5), (7, 9), (8, 4) ] chart = leather.Chart('Line') chart.add_line(data) chart.to_svg('examples/charts/lines.svg') leather-0.3.3/examples/mixed_shapes.py000066400000000000000000000004321301771142400200010ustar00rootroot00000000000000import leather line_data = [ (0, 1), (2, 5), (4, 4), (8, 3) ] dot_data = [ (1, 3), (2, 5), (6, 9), (10, 4) ] chart = leather.Chart('Mixed shapes') chart.add_line(line_data) chart.add_dots(dot_data) chart.to_svg('examples/charts/mixed_shapes.svg') leather-0.3.3/examples/multiple_series.py000066400000000000000000000004241301771142400205360ustar00rootroot00000000000000import leather data1 = [ (0, 3), (4, 5), (7, 9), (8, 4) ] data2 = [ (2, 4), (7, 3), (6, 2), (5, 9) ] chart = leather.Chart('Multiple series') chart.add_dots(data1) chart.add_dots(data2) chart.to_svg('examples/charts/multiple_series.svg') leather-0.3.3/examples/ordinal.py000066400000000000000000000003031301771142400167550ustar00rootroot00000000000000import leather data = [ ('Hello', 3), ('How', 5,), ('Are', 9), ('You', 4) ] chart = leather.Chart('Ordinal') chart.add_columns(data) chart.to_svg('examples/charts/ordinal.svg') leather-0.3.3/examples/realdata/000077500000000000000000000000001301771142400165345ustar00rootroot00000000000000leather-0.3.3/examples/realdata/gii.csv000066400000000000000000000141201301771142400200170ustar00rootroot00000000000000Country,GII Index,GII Rank,Maternal mortality ratio,Adolescent birth rate,Share of seats in parliament Norway,0.067,9,4,7.8,39.6 Australia,0.110,19,6,12.1,30.5 Switzerland,0.028,2,6,1.9,28.5 Denmark,0.048,4,5,5.1,38.0 Netherlands,0.062,7,6,6.2,36.9 Germany,0.041,3,7,3.8,36.9 Ireland,0.113,21,9,8.2,19.9 United States,0.280,55,28,31.0,19.4 Canada,0.129,25,11,14.5,28.2 New Zealand,0.157,32,8,25.3,31.4 Singapore,0.088,13,6,6.0,25.3 "Hong Kong, China (SAR)",,,,3.3, Liechtenstein,,,,,20.0 Sweden,0.055,6,4,6.5,43.6 United Kingdom,0.177,39,8,25.8,23.5 Iceland,0.087,12,4,11.5,41.3 Korea (Republic of),0.125,23,27,2.2,16.3 Israel,0.101,18,2,7.8,22.5 Luxembourg,0.100,17,11,8.3,28.3 Japan,0.133,26,6,5.4,11.6 Belgium,0.063,8,6,6.7,42.4 France,0.088,13,12,5.7,25.7 Austria,0.053,5,4,4.1,30.3 Finland,0.075,11,4,9.2,42.5 Slovenia,0.016,1,7,0.6,27.7 Spain,0.095,16,4,10.6,38.0 Italy,0.068,10,4,4.0,30.1 Czech Republic,0.091,15,5,4.9,18.9 Greece,0.146,29,5,11.9,21.0 Estonia,0.164,33,11,16.8,19.8 Brunei Darussalam,,,27,23.0, Cyprus,0.124,22,10,5.5,12.5 Qatar,0.524,116,6,9.5,0.0 Andorra,,,,,50.0 Slovakia,0.164,33,7,15.9,18.7 Poland,0.138,28,3,12.2,22.1 Lithuania,0.125,23,11,10.6,23.4 Malta,0.227,46,9,18.2,13.0 Saudi Arabia,0.284,56,16,10.2,19.9 Argentina,0.376,75,69,54.4,36.8 United Arab Emirates,0.232,47,8,27.6,17.5 Chile,0.338,65,22,55.3,15.8 Portugal,0.111,20,8,12.6,31.3 Hungary,0.209,42,14,12.1,10.1 Bahrain,0.265,51,22,13.8,15.0 Latvia,0.167,36,13,13.5,18.0 Croatia,0.149,30,13,12.7,25.8 Kuwait,0.387,79,14,14.5,1.5 Montenegro,0.171,37,7,15.2,17.3 Belarus,0.151,31,1,20.6,30.1 Russian Federation,0.276,54,24,25.7,14.5 Oman,0.275,53,11,10.6,9.6 Romania,0.333,64,33,31.0,12.0 Uruguay,0.313,61,14,58.3,11.5 Bahamas,0.298,58,37,28.5,16.7 Kazakhstan,0.267,52,26,29.9,20.1 Barbados,0.357,69,52,48.4,19.6 Antigua and Barbuda,,,,49.3,25.7 Bulgaria,0.212,44,5,35.9,20.4 Palau,,,,,10.3 Panama,0.454,96,85,78.5,19.3 Malaysia,0.209,42,29,5.7,14.2 Mauritius,0.419,88,73,30.9,11.6 Seychelles,,,,56.3,43.8 Trinidad and Tobago,0.371,73,84,34.8,24.7 Serbia,0.176,38,16,16.9,34.0 Cuba,0.356,68,80,43.1,48.9 Lebanon,0.385,78,16,12.0,3.1 Costa Rica,0.349,66,38,60.8,33.3 Iran (Islamic Republic of),0.515,114,23,31.6,3.1 Venezuela (Bolivarian Republic of),0.476,103,110,83.2,17.0 Turkey,0.359,71,20,30.9,14.4 Sri Lanka,0.370,72,29,16.9,5.8 Mexico,0.373,74,49,63.4,37.1 Brazil,0.457,97,69,70.8,9.6 Georgia,0.382,77,41,46.8,11.3 Saint Kitts and Nevis,,,,,6.7 Azerbaijan,0.303,59,26,40.0,15.6 Grenada,,,23,35.4,25.0 Jordan,0.473,102,50,26.5,11.6 The former Yugoslav Republic of Macedonia,0.164,33,7,18.3,33.3 Ukraine,0.286,57,23,25.7,11.8 Algeria,0.413,85,89,10.0,25.7 Peru,0.406,82,89,50.7,22.3 Albania,0.217,45,21,15.3,20.7 Armenia,0.318,62,29,27.1,10.7 Bosnia and Herzegovina,0.201,41,8,15.1,19.3 Ecuador,0.407,83,87,77.0,41.6 Saint Lucia,,,34,56.3,20.7 China,0.191,40,32,8.6,23.6 Fiji,0.418,87,59,42.8,14.0 Mongolia,0.325,63,68,18.7,14.9 Thailand,0.380,76,26,41.0,6.1 Dominica,,,,,21.9 Libya,0.134,27,15,2.5,16.0 Tunisia,0.240,48,46,4.6,31.3 Colombia,0.429,92,83,68.5,20.9 Saint Vincent and the Grenadines,,,45,54.5,13.0 Jamaica,0.430,93,80,70.1,16.7 Tonga,0.666,148,120,18.1,0.0 Belize,0.426,90,45,71.4,13.3 Dominican Republic,0.477,104,100,99.6,19.1 Suriname,0.463,100,130,35.2,11.8 Maldives,0.243,49,31,4.2,5.9 Samoa,0.457,97,58,28.3,6.1 Botswana,0.480,106,170,44.2,9.5 Moldova (Republic of),0.248,50,21,29.3,20.8 Egypt,0.573,131,45,43.0,2.2 Turkmenistan,,,61,18.0,25.8 Gabon,0.514,113,240,103.0,16.2 Indonesia,0.494,110,190,48.3,17.1 Paraguay,0.472,101,110,67.0,16.8 "Palestine, State of",,,,45.8, Uzbekistan,,,36,38.8,16.4 Philippines,0.420,89,120,46.8,27.1 El Salvador,0.427,91,69,76.0,27.4 South Africa,0.407,83,140,50.9,40.7 Viet Nam,0.308,60,49,29.0,24.3 Bolivia (Plurinational State of),0.444,94,200,71.9,51.8 Kyrgyzstan,0.353,67,75,29.3,23.3 Iraq,0.539,123,67,68.7,26.5 Cabo Verde,,,53,70.6,20.8 Micronesia (Federated States of),,,96,18.6,0.0 Guyana,0.515,114,250,88.5,31.3 Nicaragua,0.449,95,100,100.8,39.1 Morocco,0.525,117,120,35.8,11.0 Namibia,0.401,81,130,54.9,37.7 Guatemala,0.533,119,140,97.2,13.3 Tajikistan,0.357,69,44,42.8,15.2 India,0.563,130,190,32.8,12.2 Honduras,0.480,106,120,84.0,25.8 Bhutan,0.457,97,120,40.9,8.3 Timor-Leste,,,270,52.2,38.5 Syrian Arab Republic,0.533,119,49,41.6,12.4 Vanuatu,,,86,44.8,0.0 Congo,0.593,137,410,126.7,11.5 Kiribati,,,130,16.6,8.7 Equatorial Guinea,,,290,112.6,19.7 Zambia,0.587,132,280,125.4,12.7 Ghana,0.554,127,380,58.4,10.9 Lao People's Democratic Republic,,,,65.0,25.0 Bangladesh,0.503,111,170,80.6,20.0 Cambodia,0.477,104,170,44.3,19.0 Sao Tome and Principe,,,210,65.1,18.2 Kenya,0.552,126,400,93.6,20.8 Nepal,0.489,108,190,73.7,29.5 Pakistan,0.536,121,170,27.3,19.7 Myanmar,0.413,85,200,12.1,4.7 Angola,,,460,170.2,36.8 Swaziland,0.557,128,310,72.0,14.7 Tanzania (United Republic of),0.547,125,410,122.7,36.0 Nigeria,,,560,119.6,6.6 Cameroon,0.587,132,590,115.8,27.1 Madagascar,,,440,122.8,20.5 Zimbabwe,0.504,112,470,60.3,35.1 Mauritania,0.610,139,320,73.3,22.2 Solomon Islands,,,130,64.9,2.0 Papua New Guinea,0.611,140,220,62.1,2.7 Comoros,,,350,51.1,3.0 Yemen,0.744,155,270,47.0,0.7 Lesotho,0.541,124,490,89.4,26.8 Togo,0.588,134,450,91.5,17.6 Haiti,0.603,138,380,42.0,3.5 Rwanda,0.400,80,320,33.6,57.5 Uganda,0.538,122,360,126.6,35.0 Benin,0.614,142,340,90.2,8.4 Sudan,0.591,135,360,84.0,23.8 Djibouti,,,230,18.6,12.7 South Sudan,,,730,75.3,24.3 Senegal,0.528,118,320,94.4,42.7 Afghanistan,0.693,152,400,86.8,27.6 Cote d'Ivoire,0.679,151,720,130.3,9.2 Malawi,0.611,140,510,144.8,16.7 Ethiopia,0.558,129,420,78.4,25.5 Gambia,0.622,143,430,115.8,9.4 Congo (Democratic Republic of the),0.673,149,730,135.3,8.2 Liberia,0.651,146,640,117.4,10.7 Guinea-Bissau,,,560,99.3,13.7 Mali,0.677,150,550,175.6,9.5 Mozambique,0.591,135,480,137.8,39.6 Sierra Leone,0.650,145,"1,100",100.7,12.4 Guinea,,,650,131.0,21.9 Burkina Faso,0.631,144,400,115.4,13.3 Burundi,0.492,109,740,30.3,34.9 Chad,0.706,153,980,152.0,14.9 Eritrea,,,380,65.3,22.0 Central African Republic,0.655,147,880,98.3,12.5 Niger,0.713,154,630,204.8,13.3 Korea (Democratic People's Rep. of),,,87,0.6,16.3 Marshall Islands,,,,,3.0 Monaco,,,,,20.8 Nauru,,,,,5.3 San Marino,,,,,16.7 Somalia,,,850,110.4,13.8 Tuvalu,,,,,6.7 leather-0.3.3/examples/realdata/index.md000066400000000000000000000001621301771142400201640ustar00rootroot00000000000000## gii.csv A subset of data taken from the UN's [Gender Inequality Index](http://hdr.undp.org/en/composite/GII). leather-0.3.3/examples/series_color.py000066400000000000000000000003421301771142400200200ustar00rootroot00000000000000import leather data = [ ('Hello', 3), ('How', 5), ('Are', 9), ('You', 4) ] chart = leather.Chart('Series color') chart.add_columns(data, fill_color='#000000') chart.to_svg('examples/charts/series_color.svg') leather-0.3.3/examples/simple_pairs.py000066400000000000000000000002671301771142400200250ustar00rootroot00000000000000import leather data = [ (0, 3), (4, 5), (7, 9), (8, 4) ] chart = leather.Chart('Simple pairs') chart.add_dots(data) chart.to_svg('examples/charts/simple_pairs.svg') leather-0.3.3/examples/temporal.py000066400000000000000000000004741301771142400171610ustar00rootroot00000000000000from datetime import date import leather data = [ (date(2015, 1, 1), 3), (date(2015, 3, 1), 5), (date(2015, 6, 1), 9), (date(2015, 9, 1), 4) ] chart = leather.Chart('Temporal') chart.add_x_scale(date(2014, 1, 1), date(2016, 1, 1)) chart.add_line(data) chart.to_svg('examples/charts/temporal.svg') leather-0.3.3/examples/theme.py000066400000000000000000000007401301771142400164340ustar00rootroot00000000000000import leather line_data = [ (0, 1), (2, 5), (4, 4), (8, 3) ] dot_data = [ (1, 3), (2, 5), (6, 9), (10, 4) ] leather.theme.title_font_family = 'Comic Sans MS' leather.theme.legend_font_family = 'Comic Sans MS' leather.theme.tick_font_family = 'Comic Sans MS' leather.theme.default_series_colors = ['#ff0000', '#00ff00'] chart = leather.Chart('Theme') chart.add_line(line_data) chart.add_dots(dot_data) chart.to_svg('examples/charts/theme.svg') leather-0.3.3/examples/tick_format.py000066400000000000000000000005021301771142400176300ustar00rootroot00000000000000import leather data = [ (0, 3), (4, 5), (7, 9), (8, 4) ] def tick_formatter(value, index, tick_count): return '%i (%i/%i)' % (value, index, tick_count) chart = leather.Chart('Line') chart.add_x_axis(tick_formatter=tick_formatter) chart.add_line(data) chart.to_svg('examples/charts/tick_format.svg') leather-0.3.3/examples/ticks.py000066400000000000000000000003411301771142400164440ustar00rootroot00000000000000import leather data = [ (0, 3), (4, 5), (7, 9), (8, 4) ] chart = leather.Chart('Line') chart.add_x_scale(0, 10) chart.add_x_axis(ticks=[0, 10]) chart.add_line(data) chart.to_svg('examples/charts/ticks.svg') leather-0.3.3/leather/000077500000000000000000000000001301771142400145655ustar00rootroot00000000000000leather-0.3.3/leather/__init__.py000066400000000000000000000007221301771142400166770ustar00rootroot00000000000000#!/usr/bin/env python from leather.axis import Axis from leather.data_types import Number, Text from leather.chart import Chart from leather.grid import Grid 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.testcase import LeatherTestCase from leather import theme leather-0.3.3/leather/axis.py000066400000000000000000000133201301771142400161020ustar00rootroot00000000000000#!/usr/bin/env python import xml.etree.ElementTree as ET import six from leather import svg from leather import theme class Axis(object): """ A horizontal or vertical chart axis. :param ticks: Instead of inferring tick values from the data, use exactly this sequence of ticks values. These will still be passed to the :code:`tick_formatter`. :param tick_formatter: An optional :func:`.tick_format_function`. """ def __init__(self, ticks=None, tick_formatter=None, name=None): self._ticks = ticks self._tick_formatter = tick_formatter self._name = six.text_type(name) if name is not None else None def _estimate_left_tick_width(self, scale): """ Estimate the y axis space used by tick labels. """ tick_values = self._ticks or scale.ticks() tick_count = len(tick_values) tick_formatter = self._tick_formatter or scale.format_tick max_len = 0 for i, value in enumerate(tick_values): max_len = max(max_len, len(tick_formatter(value, i, tick_count))) return max_len * theme.tick_font_char_width def estimate_label_margin(self, scale, orient): """ Estimate the space needed for the tick labels. """ margin = 0 if orient == 'left': margin += self._estimate_left_tick_width(scale) + (theme.tick_size * 2) elif orient == 'bottom': margin += theme.tick_font_char_height + (theme.tick_size * 2) if self._name: margin += theme.axis_title_font_char_height + theme.axis_title_gap return margin def to_svg(self, width, height, scale, orient): """ Render this axis to SVG elements. """ group = ET.Element('g') group.set('class', 'axis ' + orient) # Axis title if self._name is not None: if orient == 'left': title_x = -(self._estimate_left_tick_width(scale) + theme.axis_title_gap) title_y = height / 2 dy='' transform = svg.rotate(270, title_x, title_y) elif orient == 'bottom': title_x = width / 2 title_y = height + theme.tick_font_char_height + (theme.tick_size * 2) + theme.axis_title_gap dy='1em' transform = '' title = ET.Element('text', x=six.text_type(title_x), y=six.text_type(title_y), dy=dy, fill=theme.axis_title_color, transform=transform ) title.set('text-anchor', 'middle') title.set('font-family', theme.axis_title_font_family) title.text = self._name group.append(title) # Ticks if orient == 'left': label_x = -(theme.tick_size * 2) x1 = -theme.tick_size x2 = width range_min = height range_max = 0 elif orient == 'bottom': label_y = height + (theme.tick_size * 2) y1 = 0 y2 = height + theme.tick_size range_min = 0 range_max = width tick_values = self._ticks or scale.ticks() tick_count = len(tick_values) tick_formatter = self._tick_formatter or scale.format_tick zero_tick_group = None for i, value in enumerate(tick_values): # Tick group tick_group = ET.Element('g') tick_group.set('class', 'tick') if value == 0: zero_tick_group = tick_group else: group.append(tick_group) # Tick line projected_value = scale.project(value, range_min, range_max) if value == 0: tick_color = theme.zero_color else: tick_color = theme.tick_color if orient == 'left': y1 = projected_value y2 = projected_value elif orient == 'bottom': x1 = projected_value x2 = projected_value tick = ET.Element('line', x1=six.text_type(x1), y1=six.text_type(y1), x2=six.text_type(x2), y2=six.text_type(y2), stroke=tick_color ) tick.set('stroke-width', six.text_type(theme.tick_width)) tick_group.append(tick) # Tick label if orient == 'left': x = label_x y = projected_value dy = '0.32em' text_anchor = 'end' elif orient == 'bottom': x = projected_value y = label_y dy = '1em' text_anchor = 'middle' label = ET.Element('text', x=six.text_type(x), y=six.text_type(y), dy=dy, fill=theme.label_color ) label.set('text-anchor', text_anchor) label.set('font-family', theme.tick_font_family) value = tick_formatter(value, i, tick_count) label.text = six.text_type(value) tick_group.append(label) if zero_tick_group is not None: group.append(zero_tick_group) return group def tick_format_function(value, index, tick_count): """ This example shows how to define a function to format tick values for display. :param x: The value to be formatted. :param index: The index of the tick. :param tick_count: The total number of ticks being displayed. :returns: A stringified tick value for display. """ return six.text_type(value) leather-0.3.3/leather/chart.py000066400000000000000000000311341301771142400162420ustar00rootroot00000000000000#!/usr/bin/env python from copy import copy import os import xml.etree.ElementTree as ET import six from leather.axis import Axis 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 import leather.svg as svg from leather import theme from leather.utils import X, Y, DIMENSION_NAMES, Box, IPythonSVG, warn class Chart(object): """ Container for all chart types. :param title: An optional title that will be rendered at the top of the chart. """ def __init__(self, title=None): self._title = title self._series_colors = theme.default_series_colors self._layers = [] self._types = [None, None] self._scales = [None, None] self._axes = [None, None] def _palette(self): """ Return a generator for series colors. """ return (color for color in self._series_colors) def set_x_scale(self, scale): """ Set the X :class:`.Scale` for this chart. """ self._scales[X] = scale def set_y_scale(self, scale): """ See :meth:`.Chart.set_x_scale`. """ self._scales[Y] = scale def add_x_scale(self, domain_min, domain_max): """ Create and add a :class:`.Scale`. If the provided domain values are :class:`date` or :class:`datetime` then a :class:`.Temporal` scale will be created, otherwise it will :class:`.Linear`. If you want to set a custom scale class use :meth:`.Chart.set_x_scale` instead. """ scale_type = Linear if isinstance(domain_min, Date.types) or isinstance(domain_min, DateTime.types): scale_type = Temporal self.set_x_scale(scale_type(domain_min, domain_max)) def add_y_scale(self, domain_min, domain_max): """ See :meth:`.Chart.add_x_scale`. """ scale_type = Linear if isinstance(domain_min, Date.types) or isinstance(domain_min, DateTime.types): scale_type = Temporal self.set_y_scale(scale_type(domain_min, domain_max)) def set_x_axis(self, axis): """ Set an :class:`.Axis` class for this chart. """ self._axes[X] = axis def set_y_axis(self, axis): """ See :meth:`.Chart.set_x_axis`. """ self._axes[Y] = axis def add_x_axis(self, ticks=None, tick_formatter=None, name=None): """ Create and add an X :class:`.Axis`. If you want to set a custom axis class use :meth:`.Chart.set_x_axis` instead. """ self._axes[X] = Axis(ticks, tick_formatter, name) def add_y_axis(self, ticks=None, tick_formatter=None, name=None): """ See :meth:`.Chart.add_x_axis`. """ self._axes[Y] = Axis(ticks, tick_formatter, name) def add_series(self, series, shape): """ Add a data :class:`.Series` to the chart. The data types of the new series must be consistent with any series that have already been added. There are several shortcuts for adding different types of data series. See :meth:`.Chart.add_bars`, :meth:`.Chart.add_columns`, :meth:`.Chart.add_dots`, and :meth:`.Chart.add_line`. """ if self._layers and isinstance(self._layers[0][0], CategorySeries): raise RuntimeError('Additional series can not be added to a chart with a CategorySeries.') if isinstance(series, CategorySeries): self._types = series._types else: for dim in [X, Y]: 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])) shape.validate_series(series) self._layers.append(( series, shape )) def add_bars(self, data, x=None, y=None, name=None, fill_color=None): """ Create and add a :class:`.Series` rendered with :class:`.Bars`. Note that when creating bars in this way the order of the series data will be reversed so that the first item in the series is displayed as the top-most bar in the graphic. If you don't want this to happen use :meth:`.Chart.add_series` instead. """ self.add_series( Series(list(reversed(data)), x=x, y=y, name=name), Bars(fill_color) ) def add_columns(self, data, x=None, y=None, name=None, fill_color=None): """ Create and add a :class:`.Series` rendered with :class:`.Columns`. """ self.add_series( Series(data, x=x, y=y, name=name), Columns(fill_color) ) def add_dots(self, data, x=None, y=None, name=None, fill_color=None, radius=None): """ Create and add a :class:`.Series` rendered with :class:`.Dots`. """ self.add_series( Series(data, x=x, y=y, name=name), Dots(fill_color, radius) ) def add_line(self, data, x=None, y=None, name=None, stroke_color=None, width=None): """ Create and add a :class:`.Series` rendered with :class:`.Line`. """ self.add_series( Series(data, x=x, y=y, name=name), Line(stroke_color, width) ) def _validate_dimension(self, dimension): """ Validates that the given scale and axis are valid for the data that has been added to this chart. If a scale or axis has not been set, generates automated ones. """ scale = self._scales[dimension] axis = self._axes[dimension] if not scale: scale = Scale.infer(self._layers, dimension, self._types[dimension]) else: for series, shape in self._layers: if not scale.contains(series.min(dimension)) or not scale.contains(series.max(dimension)): d = DIMENSION_NAMES[dimension] warn('Data contains values outside %s scale domain. All data points may not be visible on the chart.' % d) # Only display once per axis break if not axis: axis = Axis() return (scale, axis) def to_svg_group(self, width=None, height=None): """ Render this chart to an SVG group element. This can then be placed inside an :code:`` tag to make a complete SVG graphic. See :meth:`.Chart.to_svg` for arguments. """ width = width or theme.default_width height = height or theme.default_height if not self._layers: raise ValueError('You must add at least one series to the chart before rendering.') if isinstance(theme.margin, float): default_margin = width * theme.margin margin = Box( top=default_margin, right=default_margin, bottom=default_margin, left=default_margin ) elif isinstance(margin, int): margin = Box(margin, margin, margin, margin) elif not isinstance(margin, Box): margin = Box(*margin) # Root / background root_group = ET.Element('g') root_group.append(ET.Element('rect', x=six.text_type(0), y=six.text_type(0), width=six.text_type(width), height=six.text_type(height), fill=theme.background_color )) # Margins margin_group = ET.Element('g') margin_group.set('transform', svg.translate(margin.left, margin.top)) margin_width = width - (margin.left + margin.right) margin_height = height - (margin.top + margin.bottom) root_group.append(margin_group) # Header header_group = ET.Element('g') header_margin = 0 if self._title: label = ET.Element('text', x=six.text_type(0), y=six.text_type(0), fill=theme.title_color ) label.set('font-family', theme.title_font_family) label.set('font-size', six.text_type(theme.title_font_size)) label.text = six.text_type(self._title) header_group.append(label) header_margin += theme.title_font_char_height + theme.title_gap # Legend if len(self._layers) > 1 or isinstance(self._layers[0][0], CategorySeries): legend_group = ET.Element('g') legend_group.set('transform', svg.translate(0, header_margin)) indent = 0 rows = 1 palette = self._palette() for series, shape in self._layers: for item_group, item_width in shape.legend_to_svg(series, palette): if indent + item_width > width: indent = 0 rows += 1 y = (rows - 1) * (theme.legend_font_char_height + theme.legend_gap) item_group.set('transform', svg.translate(indent, y)) indent += item_width legend_group.append(item_group) legend_height = rows * (theme.legend_font_char_height + theme.legend_gap) header_margin += legend_height header_group.append(legend_group) margin_group.append(header_group) # Body body_group = ET.Element('g') body_group.set('transform', svg.translate(0, header_margin)) body_width = margin_width body_height = margin_height - header_margin margin_group.append(body_group) # Axes 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') canvas_width = body_width - left_margin canvas_height = body_height - bottom_margin axes_group = ET.Element('g') axes_group.set('transform', svg.translate(left_margin, 0)) axes_group.append(x_axis.to_svg(canvas_width, canvas_height, x_scale, 'bottom')) axes_group.append(y_axis.to_svg(canvas_width, canvas_height, y_scale, 'left')) header_group.set('transform', svg.translate(left_margin, 0)) body_group.append(axes_group) # Series series_group = ET.Element('g') palette = self._palette() for series, shape in self._layers: series_group.append(shape.to_svg(canvas_width, canvas_height, x_scale, y_scale, series, palette)) axes_group.append(series_group) return root_group def to_svg(self, path=None, width=None, height=None): """ Render this chart to an SVG document. The :code:`width` and :code:`height` are specified in SVG's "unitless" units, however, it is usually convenient to specify them as though they were pixels. :param path: Filepath or file-like object to write to. If omitted then the SVG will be returned as a string. If running within IPython, then this will return a SVG object to be displayed. :param width: The output width, in SVG user units. Defaults to :data:`.theme.default_chart_width`. :param height: The output height, in SVG user units. Defaults to :data:`.theme.default_chart_height`. """ width = width or theme.default_chart_width height = height or theme.default_chart_height root = ET.Element('svg', width=six.text_type(width), height=six.text_type(height), version='1.1', xmlns='http://www.w3.org/2000/svg' ) group = self.to_svg_group(width, height) root.append(group) svg_text = svg.stringify(root) close = True if path: f = None try: if hasattr(path, 'write'): f = path close = False else: dirpath = os.path.dirname(path) if dirpath and not os.path.exists(dirpath): os.makedirs(dirpath) f = open(path, 'w') f.write(svg.HEADER) f.write(svg_text) finally: if close and f is not None: f.close() else: return IPythonSVG(svg_text) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/data_types.py�����������������������������������������������������������������0000664�0000000�0000000�00000001463�13017711424�0017300�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python from datetime import date, datetime from decimal import Decimal import six class DataType(object): """ Base class for :class:`.Series` data types. """ @classmethod def infer(cls, v): for t in [DateTime, Date, Number, Text]: if isinstance(v, t.types): return t raise TypeError('No data type available for %s' % type(v)) class Date(DataType): """ Data representing dates. """ types = (date,) class DateTime(DataType): """ Data representing dates with times. """ types = (datetime,) class Number(DataType): """ Data representing numbers. """ types = (int, float, Decimal) class Text(DataType): """ Data representing text/strings. """ types = six.string_types �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/grid.py�����������������������������������������������������������������������0000664�0000000�0000000�00000006167�13017711424�0016076�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python import math import os import xml.etree.ElementTree as ET import six import leather.svg as svg from leather import theme from leather.utils import IPythonSVG class Grid(object): """ A container for a set of :class:`.Chart` instances that are rendered in a grid layout. """ def __init__(self): self._charts = [] def add_one(self, chart): """ Add a :class:`.Chart` to the grid. """ self._charts.append(chart) def add_many(self, charts): """ Add a sequence of charts to this grid. """ self._charts.extend(charts) def to_svg(self, path=None, width=None, height=None): """ Render the grid to an SVG. The :code:`width` and :code:`height` arguments refer to the size of the entire grid. The size of individual charts will be inferred automatically. See :meth:`.Chart.to_svg` for arguments. """ if not width or not height: count = len(self._charts) columns = math.ceil(math.sqrt(count)) rows = math.ceil(count / columns) width = columns * theme.default_chart_width height = rows * theme.default_chart_height root = ET.Element('svg', width=six.text_type(width), height=six.text_type(height), version='1.1', xmlns='http://www.w3.org/2000/svg' ) # Root / background root_group = ET.Element('g') root_group.append(ET.Element('rect', x=six.text_type(0), y=six.text_type(0), width=six.text_type(width), height=six.text_type(height), fill=theme.background_color )) root.append(root_group) # Charts grid_group = ET.Element('g') chart_count = len(self._charts) grid_width = math.ceil(math.sqrt(chart_count)) grid_height = math.ceil(chart_count / grid_width) chart_width = width / grid_width chart_height = height / grid_height for i, chart in enumerate(self._charts): x = (i % grid_width) * chart_width y = math.floor(i / grid_width) * chart_height group = ET.Element('g') group.set('transform', svg.translate(x, y)) chart = chart.to_svg_group(chart_width, chart_height) group.append(chart) grid_group.append(group) root_group.append(grid_group) svg_text = svg.stringify(root) close = True if path: f = None try: if hasattr(path, 'write'): f = path close = False else: dirpath = os.path.dirname(path) if dirpath and not os.path.exists(dirpath): os.makedirs(dirpath) f = open(path, 'w') f.write(svg.HEADER) f.write(svg_text) finally: if close and f is not None: f.close() else: return IPythonSVG(svg_text) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/lattice.py��������������������������������������������������������������������0000664�0000000�0000000�00000012233�13017711424�0016565�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python from leather.axis import Axis from leather.chart import Chart from leather.data_types import Date, DateTime from leather.grid import Grid from leather.scales import Scale, Linear from leather.series import Series from leather.shapes import Line from leather import theme from leather.utils import X, Y class Lattice(object): """ A grid of charts with synchronized shapes, scales, and axes. Lattice only supports graphing a single series of data. :param shape: An instance of :class:`.Shape` to use to render all series. Defaults to :class:`.Line` if not specified. """ def __init__(self, shape=None): self._shape = shape or Line() self._series = [] self._types = [None, None] self._scales = [None, None] self._axes = [None, None] def set_x_scale(self, scale): """ Set the X :class:`.Scale` for this lattice. """ self._scales[X] = scale def set_y_scale(self, scale): """ See :meth:`.Lattice.set_x_scale`. """ self._scales[Y] = scale def add_x_scale(self, domain_min, domain_max): """ Create and add a :class:`.Scale`. If the provided domain values are :class:`date` or :class:`datetime` then a :class:`.Temporal` scale will be created, otherwise it will :class:`.Linear`. If you want to set a custom scale class use :meth:`.Lattice.set_x_scale` instead. """ scale_type = Linear if isinstance(domain_min, Date.types) or isinstance(domain_min, DateTime.types): scale_type = Temporal self.set_x_scale(scale_type(domain_min, domain_max)) def add_y_scale(self, domain_min, domain_max): """ See :meth:`.Lattice.add_x_scale`. """ scale_type = Linear if isinstance(domain_min, Date.types) or isinstance(domain_min, DateTime.types): scale_type = Temporal self.set_y_scale(scale_type(domain_min, domain_max)) def set_x_axis(self, axis): """ Set an :class:`.Axis` class for this lattice. """ self._axes[X] = axis def set_y_axis(self, axis): """ See :meth:`.Lattice.set_x_axis`. """ self._axes[Y] = axis def add_x_axis(self, ticks=None, tick_formatter=None, name=None): """ Create and add an X :class:`.Axis`. If you want to set a custom axis class use :meth:`.Lattice.set_x_axis` instead. """ self._axes[X] = Axis(ticks=ticks, tick_formatter=tick_formatter, name=name) def add_y_axis(self, ticks=None, tick_formatter=None, name=None): """ See :meth:`.Lattice.add_x_axis`. """ self._axes[Y] = Axis(ticks=ticks, tick_formatter=tick_formatter, name=name) def add_one(self, data, x=None, y=None, title=None): """ Add a data series to this lattice. :param data: A sequence of data suitable for constructing a :class:`.Series`, or a sequence of such objects. :param x: See :class:`.Series`. :param y: See :class:`.Series`. :param title: A title to render above this chart. """ series = Series(data, x=x, y=y, name=title) for dimension in [X, Y]: if self._types[dimension]: if series._types[dimension] is not self._types[dimension]: raise TypeError('All data series must have the same data types.') else: self._types[dimension] = series._types[dimension] self._shape.validate_series(series) self._series.append(series) def add_many(self, data, x=None, y=None, titles=None): """ Same as :meth:`.Lattice.add_one` except :code:`data` is a list of data series to be added simultaneously. See :meth:`.Lattice.add_one` for other arguments. Note that :code:`titles` is a sequence of titles that must be the same length as :code:`data`. """ for i, d in enumerate(data): title = titles[i] if titles else None self.add_one(d, x=x, y=y, title=title) def to_svg(self, path=None, width=None, height=None): """ Render the lattice to an SVG. See :class:`.Grid` for additional documentation. """ layers = [(s, self._shape) for s in self._series] if not self._scales[X]: self._scales[X] = Scale.infer(layers, X, self._types[X]) if not self._scales[Y]: self._scales[Y]= Scale.infer(layers, Y, self._types[Y]) if not self._axes[X]: self._axes[X] = Axis() if not self._axes[Y]: self._axes[Y] = Axis() grid = Grid() for i, series in enumerate(self._series): chart = Chart(title=series.name) chart.set_x_scale(self._scales[X]) chart.set_y_scale(self._scales[Y]) chart.set_x_axis(self._axes[X]) chart.set_y_axis(self._axes[Y]) chart.add_series(series, self._shape) grid.add_one(chart) return grid.to_svg(path, width, height) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/scales/�����������������������������������������������������������������������0000775�0000000�0000000�00000000000�13017711424�0016037�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/scales/__init__.py������������������������������������������������������������0000664�0000000�0000000�00000000276�13017711424�0020155�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python from leather.scales.base import Scale from leather.scales.linear import Linear from leather.scales.ordinal import Ordinal from leather.scales.temporal import Temporal ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/scales/base.py����������������������������������������������������������������0000664�0000000�0000000�00000010622�13017711424�0017324�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python from datetime import date, datetime import six from leather.data_types import Date, DateTime, Number, Text from leather.shapes import Bars, Columns class Scale(object): """ Base class for various kinds of scale objects. """ @classmethod def infer(cls, layers, dimension, data_type): """ Infer's an appropriate default scale for a given sequence of :class:`.Series`. :param chart_series: A sequence of :class:`.Series` instances :param dimension: The dimension, :code:`X` or :code:`Y` of the data to infer for. :param data_type: The type of data contained in the series dimension. """ from leather.scales.linear import Linear from leather.scales.ordinal import Ordinal from leather.scales.temporal import Temporal # Default Time scale is Temporal if data_type is Date: data_min = date.max data_max = date.min for series, shape in layers: data_min = min(data_min, series.min(dimension)) data_max = max(data_max, series.max(dimension)) scale = Temporal(data_min, data_max) elif data_type is DateTime: data_min = datetime.max data_max = datetime.min for series, shape in layers: data_min = min(data_min, series.min(dimension)) data_max = max(data_max, series.max(dimension)) scale = Temporal(data_min, data_max) # Default Number scale is Linear elif data_type is Number: force_zero = False data_min = None data_max = None for series, shape in layers: if isinstance(shape, (Bars, Columns)): force_zero = True if data_min is None: data_min = series.min(dimension) else: data_min = min(data_min, series.min(dimension)) if data_max is None: data_max = series.max(dimension) else: data_max = max(data_max, series.max(dimension)) if force_zero: if data_min > 0: data_min = 0 if data_max < 0: data_max = 0 scale = Linear(data_min, data_max) # Default Text scale is Ordinal elif data_type is Text: scale_values = None # First case: a single set of ordinal labels if len(layers) == 1: scale_values = layers[0][0].values(dimension) else: first_series = set(layers[0][0].values(dimension)) data_series = [series.values(dimension) for series, shape in layers] all_same = True for series in data_series: if set(series) != first_series: all_same = False break # Second case: multiple identical sets of ordinal labels if all_same: scale_values = layers[0][0].values(dimension) # Third case: multiple different sets of ordinal labels else: scale_values = sorted(list(set().union(*data_series))) scale = Ordinal(scale_values) return scale def contains(self, v): """ Return :code:`True` if a given value is contained within this scale's displayed domain. """ raise NotImplementedError def project(self, value, range_min, range_max): """ Project a value in this scale's domain to a target range. """ raise NotImplementedError 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`. """ raise NotImplementedError def ticks(self): """ 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) ��������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/scales/linear.py��������������������������������������������������������������0000664�0000000�0000000�00000003520�13017711424�0017663�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python from decimal import Decimal from leather.scales.base import Scale from leather.ticks.score import ScoreTicker class Linear(Scale): """ A scale that linearly maps values from a domain to a range. :param domain_min: The minimum value of the input domain. :param domain_max: The maximum value of the input domain. """ def __init__(self, domain_min, domain_max): if domain_min > domain_max: raise ValueError('Inverted domains are not currently supported.') elif domain_min == domain_max: # Default to unit scale self._data_min = Decimal(0) self._data_max = Decimal(1) else: self._data_min = Decimal(domain_min) self._data_max = Decimal(domain_max) self._ticker = ScoreTicker(self._data_min, self._data_max) def contains(self, v): """ Return :code:`True` if a given value is contained within this scale's domain. """ return self._data_min <= v <= self._data_max def project(self, value, range_min, range_max): """ Project a value in this scale's domain to a target range. """ value = Decimal(value) range_min = Decimal(range_min) range_max = Decimal(range_max) pos = (value - self._ticker.min) / (self._ticker.max - self._ticker.min) return ((range_max - range_min) * pos) + range_min 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`. """ raise NotImplementedError def ticks(self): """ Generate a series of ticks for this scale. """ return self._ticker.ticks ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/scales/ordinal.py�������������������������������������������������������������0000664�0000000�0000000�00000003557�13017711424�0020053�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python from decimal import Decimal from leather.scales.base import Scale class Ordinal(Scale): """ A scale that maps individual values (e.g. strings) to a range. """ def __init__(self, domain): self._domain = domain def contains(self, v): """ Return :code:`True` if a given value is contained within this scale's displayed domain. """ return v in self._domain def project(self, value, range_min, range_max): """ Project a value in this scale's domain to a target range. """ range_min = Decimal(range_min) range_max = Decimal(range_max) segments = len(self._domain) segment_size = (range_max - range_min) / segments try: pos = range_min + (self._domain.index(value) * segment_size) + (segment_size / 2) except ValueError: raise ValueError('Value "%s" is not present in Ordinal scale domain' % value) return pos 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`. """ range_min = Decimal(range_min) range_max = Decimal(range_max) segments = len(self._domain) segment_size = (range_max - range_min) / segments gap = segment_size / Decimal(20) try: a = range_min + (self._domain.index(value) * segment_size) + gap b = range_min + ((self._domain.index(value) + 1) * segment_size) - gap except ValueError: raise ValueError('Value "%s" is not present in Ordinal scale domain' % value) return (a, b) def ticks(self): """ Generate a series of ticks for this scale. """ return self._domain �������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/scales/temporal.py������������������������������������������������������������0000664�0000000�0000000�00000004517�13017711424�0020243�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python from datetime import datetime import six from leather.scales.base import Scale from leather.ticks.score_time import ScoreTimeTicker class Temporal(Scale): """ A scale that linearly maps date/datetime values from a domain to a range. :param domain_min: The minimum date/datetime of the input domain. :param domain_max: The maximum date/datetime of the input domain. """ def __init__(self, domain_min, domain_max): if domain_min >= domain_max: raise ValueError('Domain minimum must be less than domain maximum. Inverted domains are not currently supported.') self._data_min = domain_min self._data_max = domain_max self._ticker = ScoreTimeTicker(self._data_min, self._data_max) def contains(self, v): """ Return :code:`True` if a given value is contained within this scale's domain. """ return self._data_min <= v <= self._data_max def project(self, value, range_min, range_max): """ Project a value in this scale's domain to a target range. """ numerator = value - self._ticker.min denominator = self._ticker.max - self._ticker.min # Python 2 does not support timedelta division if six.PY2: if isinstance(self._ticker.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 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`. """ raise NotImplementedError def ticks(self): """ Generate a series of ticks for this scale. """ return self._ticker.ticks 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 self._ticker.format_tick(value) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/series/�����������������������������������������������������������������������0000775�0000000�0000000�00000000000�13017711424�0016057�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/series/__init__.py������������������������������������������������������������0000664�0000000�0000000�00000000177�13017711424�0020175�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python from leather.series.base import Series, key_function from leather.series.category import CategorySeries �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/series/base.py����������������������������������������������������������������0000664�0000000�0000000�00000007723�13017711424�0017354�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python from collections import Iterable, Sequence, Mapping from functools import partial import six from leather.data_types import DataType from leather.utils import DIMENSION_NAMES, X, Y, Datum class Series(object): """ A series of data and its associated metadata. Series object does not modify the data it is passed. :param data: 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 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. This will be used as the chart title if rendered in a :class:`.Lattice`. """ def __init__(self, data, x=None, y=None, name=None): self._data = data self._name = name self._keys = [ self._make_key(x if x is not None else X), self._make_key(y if y is not None else Y) ] self._types = [ self._infer_type(X), self._infer_type(Y) ] def _make_key(self, key): """ Process a user-specified data key and convert to a function if needed. """ if callable(key): return key 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] for i, row in enumerate(self._data): v = key(row, i) if v is not None: break if v is None: raise ValueError('All values in %s dimension are null.' % DIMENSION_NAMES[dimension]) return DataType.infer(v) @property def name(self): return self._name def data_type(self, dimension): """ Return the data type for a dimension of this series. """ return self._types[dimension] def data(self): """ Return data for this series. """ x = self._keys[X] y = self._keys[Y] for i, row in enumerate(self._data): yield Datum(i, x(row, i), y(row, i), None, row) def values(self, dimension): """ Get a flattened list of values for a given dimension of the data. """ key = self._keys[dimension] return [key(row, i) for i, row in enumerate(self._data)] def min(self, dimension): """ Compute the minimum value of a given dimension. """ return min(v for v in self.values(dimension) if v is not None) def max(self, dimension): """ Compute the minimum value of a given dimension. """ return max(v for v in self.values(dimension) if v is not None) 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 ���������������������������������������������leather-0.3.3/leather/series/category.py������������������������������������������������������������0000664�0000000�0000000�00000005263�13017711424�0020254�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python from collections import Iterable, Sequence, Mapping from functools import partial import six from leather.data_types import DataType from leather.series.base import Series from leather.utils import X, Y, Z, Datum class CategorySeries(Series): """ A series of categorized data and its associated metadata. Series object does not modify the data it is passed. :param data: 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 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 z: See :code:`y`. This variable identifies the category/sub-series of each row. :param name: An optional name to be used in labeling this series. This will be used as the chart title if rendered in a :class:`.Lattice`. """ def __init__(self, data, x=None, y=None, z=None, name=None): self._data = data self._name = name self._keys = [ self._make_key(x if x is not None else X), self._make_key(y if y is not None else Y), self._make_key(z if z is not None else Z) ] self._types = [ self._infer_type(X), self._infer_type(Y), self._infer_type(Z) ] def data(self): """ 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): 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 ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/shapes/�����������������������������������������������������������������������0000775�0000000�0000000�00000000000�13017711424�0016050�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/shapes/__init__.py������������������������������������������������������������0000664�0000000�0000000�00000000347�13017711424�0020165�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python from leather.shapes.base import Shape, style_function from leather.shapes.bars import Bars from leather.shapes.columns import Columns from leather.shapes.dots import Dots from leather.shapes.line import Line �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/shapes/bars.py����������������������������������������������������������������0000664�0000000�0000000�00000004260�13017711424�0017353�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python import xml.etree.ElementTree as ET import six from leather.data_types import Number, Text from leather.series import CategorySeries from leather.shapes.base import Shape from leather import theme from leather.utils import X, Y class Bars(Shape): """ Render a series of data as bars. :param fill_color: The color to fill the bars. You may also specify a :func:`.style_function`. """ def __init__(self, fill_color=None): self._fill_color = fill_color def validate_series(self, series): """ Verify this shape can be used to render a given series. """ if isinstance(series, CategorySeries): raise ValueError('Bars can not be used to render CategorySeries.') if series.data_type(X) is not Number: raise ValueError('Bars only support Number values for the Y axis.') if series.data_type(Y) is not Text: raise ValueError('Bars only support Text values for the X axis.') def to_svg(self, width, height, x_scale, y_scale, series, palette): """ Render bars to SVG elements. """ group = ET.Element('g') group.set('class', 'series bars') zero_x = x_scale.project(0, 0, width) if self._fill_color: fill_color = self._fill_color else: fill_color = next(palette) for d in series.data(): if d.x is None or d.y is None: continue y1, y2 = y_scale.project_interval(d.y, height, 0) proj_x = x_scale.project(d.x, 0, width) if d.x < 0: bar_x = proj_x bar_width = zero_x - proj_x else: bar_x = zero_x bar_width = proj_x - zero_x if callable(fill_color): color = fill_color(d) else: color = fill_color group.append(ET.Element('rect', x=six.text_type(bar_x), y=six.text_type(y2), width=six.text_type(bar_width), height=six.text_type(y1 - y2), fill=color )) return group ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/shapes/base.py����������������������������������������������������������������0000664�0000000�0000000�00000005461�13017711424�0017342�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python import six import xml.etree.ElementTree as ET from leather import theme class Shape(object): """ Base class for shapes that can be used to render data :class:`.Series`. """ def validate_series(self, series): """ Verify this shape can be used to render a given series. """ raise NotImplementedError def to_svg(self, width, height, x_scale, y_scale, series, palette): """ Render this shape to an SVG. """ raise NotImplementedError def legend_to_svg(self, series, palette): """ Render the legend entries for these shapes. """ if hasattr(self, '_fill_color'): if self._fill_color: if callable(self._fill_color): # TODO fill_color = 'black' else: fill_color = self._fill_color else: fill_color = next(palette) else: fill_color = None 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 text = six.text_type(series.name) if series.name is not None else 'Unnamed series' 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) return [(item_group, item_width)] def style_function(datum): """ This example shows how to define a function to specify style values for individual data points. :param datum: A :class:`.Datum` instance for the data row. """ pass ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/shapes/columns.py�������������������������������������������������������������0000664�0000000�0000000�00000004276�13017711424�0020113�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python import xml.etree.ElementTree as ET import six from leather.data_types import Number, Text from leather.series import CategorySeries from leather.shapes.base import Shape from leather.utils import X, Y class Columns(Shape): """ Render a series of data as columns. :param fill_color: The color to fill the columns. You may also specify a :func:`.style_function`. """ def __init__(self, fill_color=None): self._fill_color = fill_color def validate_series(self, series): """ Verify this shape can be used to render a given series. """ if isinstance(series, CategorySeries): raise ValueError('Columns can not be used to render CategorySeries.') if series.data_type(X) is not Text: raise ValueError('Bars only support Text values for the X axis.') if series.data_type(Y) is not Number: raise ValueError('Bars only support Number values for the Y axis.') 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 columns') zero_y = y_scale.project(0, height, 0) if self._fill_color: fill_color = self._fill_color else: fill_color = next(palette) for d in series.data(): if d.x is None or d.y is None: continue x1, x2 = x_scale.project_interval(d.x, 0, width) 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 = fill_color 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 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/shapes/dots.py����������������������������������������������������������������0000664�0000000�0000000�00000005165�13017711424�0017402�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python from collections import defaultdict import xml.etree.ElementTree as ET import six from leather.data_types import Text from leather.series import CategorySeries from leather.shapes.base import Shape from leather import theme from leather.utils import DummySeries, X, Y class Dots(Shape): """ Render a series of data as dots. :param fill_color: The color to fill the dots. You may also specify a :func:`.style_function`. If not specified, default chart colors will be used. :param radius: The radius of the rendered dots. Defaults to :data:`.theme.default_dot_radius`. You may also specify a :func:`.style_function`. """ def __init__(self, fill_color=None, radius=None): self._fill_color = fill_color self._radius = radius or theme.default_dot_radius def validate_series(self, series): """ Verify this shape can be used to render a given series. """ if series.data_type(X) is Text or series.data_type(Y) is Text: raise ValueError('Dots do not support Text values.') return True def to_svg(self, width, height, x_scale, y_scale, series, palette): """ Render dots to SVG elements. """ group = ET.Element('g') group.set('class', 'series dots') default_colors = defaultdict(lambda: next(palette)) for d in series.data(): if d.x is None or d.y is None: continue proj_x = x_scale.project(d.x, 0, width) proj_y = y_scale.project(d.y, height, 0) if callable(self._fill_color): fill_color = self._fill_color(d) elif self._fill_color: fill_color = self._fill_color else: fill_color = default_colors[d.z] if callable(self._radius): radius = self._radius(d) else: radius = self._radius group.append(ET.Element('circle', cx=six.text_type(proj_x), cy=six.text_type(proj_y), r=six.text_type(radius), fill=fill_color )) return group def legend_to_svg(self, series, palette): """ Render the legend entries for these shapes. """ items = [] if isinstance(series, CategorySeries): for category in series.categories(): items.extend(Shape.legend_to_svg(self, DummySeries(category), palette)) else: items.extend(Shape.legend_to_svg(self, series, palette)) return items �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/shapes/line.py����������������������������������������������������������������0000664�0000000�0000000�00000004761�13017711424�0017361�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python import xml.etree.ElementTree as ET import six from leather.data_types import Text from leather.series import CategorySeries from leather.shapes.base import Shape from leather import theme from leather.utils import X, Y class Line(Shape): """ Render a series of data as a line. :param stroke_color: The color to stroke the lines. If not provided, default chart colors will be used. :param width: The width of the lines. Defaults to :data:`.theme.default_line_width`. """ def __init__(self, stroke_color=None, width=None): self._stroke_color = stroke_color self._width = width or theme.default_line_width def validate_series(self, series): """ Verify this shape can be used to render a given series. """ if isinstance(series, CategorySeries): raise ValueError('Line can not be used to render CategorySeries.') if series.data_type(X) is Text or series.data_type(Y) is Text: raise ValueError('Line does not support Text values.') def _new_path(self, stroke_color): """ Start a new path. """ path = ET.Element('path', stroke=stroke_color, fill='none' ) path.set('stroke-width', six.text_type(self._width)) return path def to_svg(self, width, height, x_scale, y_scale, series, palette): """ Render lines to SVG elements. """ group = ET.Element('g') group.set('class', 'series lines') if self._stroke_color: stroke_color = self._stroke_color else: stroke_color = next(palette) path = self._new_path(stroke_color) path_d = [] for d in series.data(): if d.x is None or d.y is None: if path_d: path.set('d', ' '.join(path_d)) group.append(path) path_d = [] path = self._new_path(stroke_color) continue proj_x = x_scale.project(d.x, 0, width) proj_y = y_scale.project(d.y, height, 0) if not path_d: command = 'M' else: command = 'L' path_d.extend([ command, six.text_type(proj_x), six.text_type(proj_y) ]) if path_d: path.set('d', ' '.join(path_d)) group.append(path) return group ���������������leather-0.3.3/leather/svg.py������������������������������������������������������������������������0000664�0000000�0000000�00000001663�13017711424�0015744�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python """ Helpers for working with SVG. """ import xml.etree.ElementTree as ET import six HEADER = '\n' + \ '\n' def stringify(root): """ Convert an SVG XML tree to a unicode string. """ if six.PY3: return ET.tostring(root, encoding='unicode') else: return ET.tostring(root, encoding='utf-8') def save(f, root): """ Save an SVG XML tree to a file. """ f.write(HEADER) f.write(stringify(root)) def translate(x, y): """ Generate an SVG transform statement representing a simple translation. """ return 'translate(%i %i)' % (x, y) def rotate(deg, x, y): """ Generate an SVG transform statement representing rotation around a given point. """ return 'rotate(%i %i %i)' % (deg, x, y) �����������������������������������������������������������������������������leather-0.3.3/leather/testcase.py�������������������������������������������������������������������0000664�0000000�0000000�00000001735�13017711424�0016760�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python try: import unittest2 as unittest except ImportError: import unittest import six class LeatherTestCase(unittest.TestCase): """ Unittest case for quickly asserting logic about charts. """ def render_chart(self, chart): """ Verify the column names in the given table match what is expected. """ svg = chart.to_svg() return self.parse_svg(svg) def parse_svg(self, text): from lxml import etree text = text.replace(' xmlns="http://www.w3.org/2000/svg"', '') if six.PY3: text = text.encode('utf-8') return etree.fromstring(text) def assertElementCount(self, svg, selector, count): series = svg.cssselect(selector) self.assertEqual(len(series), count) def assertTickLabels(self, svg, orient, compare): ticks = [t.text for t in svg.cssselect('.%s .tick text' % orient)] self.assertSequenceEqual(ticks, compare) �����������������������������������leather-0.3.3/leather/theme.py����������������������������������������������������������������������0000664�0000000�0000000�00000004544�13017711424�0016250�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python """ This module contains all style configuration for rendering charts. Setting any of these variables will change how charts are rendered. """ # CHART #: Default chart width default_chart_width = 800 #: Default chart height default_chart_height = 600 #: Chart background color background_color = '#f9f9f9' #: Chart margin as a percent of chart width margin = 0.05 # CHART TITLE #: Chart title text color title_color = '#333' #: Chart title font title_font_family = 'Monaco' #: Chart title font size title_font_size = 16 #: Approximate glyph height of the title font title_font_char_height = 16 #: Approximate glyph width of the title font title_font_char_width = 9 #: Gap between title and rest of chart title_gap = 4 # LEGEND #: Chart legend text color legend_color = '#666' #: Chart legend font legend_font_family = 'Monaco' #: Chart legend font size legend_font_size = 14 #: Approximate glyph height of the legend font legend_font_char_height = 14 #: Approximate glyph width of the legend font legend_font_char_width = 8 #: Gap between legend and rest of chart legend_gap = 4 #: Size of the bubble next to an legend item legend_bubble_size = 10 #: Offset from the top of the glyph legend_bubble_offset = 4 # AXIS #: Axis title text color axis_title_color = '#666' #: Axis title font axis_title_font_family = 'Monaco' #: Axis title font size axis_title_font_size = 14 #: Approximate glyph height of the axis title font axis_title_font_char_height = 14 #: Approximate glyph width of the axis title font axis_title_font_char_width = 8 #: Gap between axis title and rest of chart axis_title_gap = 16 # TICKS #: Width of a tick mark tick_width = 1 #: Length of a tick mark tick_size = 4 #: Color of tick marks tick_color = '#eee' #: Color of the zero tick mark zero_color = '#a8a8a8' # TICK LABELS #: Color of tick label text label_color = '#9c9c9c' #: Tick label font tick_font_family = 'Monaco' #: Tick label font size tick_font_size = 14 #: Approximate glyph height of the tick label font tick_font_char_height = 14 #: Approximate glyph width of the tick label font tick_font_char_width = 8 # SERIES #: Default sequence of :class:`.Shape` colors default_series_colors = ['#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00'] #: Default :class:`.Dots` radius default_dot_radius = 3 #: Default :class:`.Line` width default_line_width = 2 ������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/ticks/������������������������������������������������������������������������0000775�0000000�0000000�00000000000�13017711424�0015702�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/ticks/__init__.py�������������������������������������������������������������0000664�0000000�0000000�00000000236�13017711424�0020014�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python from leather.ticks.base import Ticker from leather.ticks.score import ScoreTicker from leather.ticks.score_time import ScoreTimeTicker ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/ticks/base.py�����������������������������������������������������������������0000664�0000000�0000000�00000000466�13017711424�0017174�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python class Ticker(object): """ Base class for ticker implementations. """ @property def ticks(self): raise NotImplementedError @property def min(self): raise NotImplementedError @property def max(self): raise NotImplementedError ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/ticks/score.py����������������������������������������������������������������0000664�0000000�0000000�00000012016�13017711424�0017367�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python from decimal import Decimal, ROUND_CEILING, ROUND_FLOOR import math import sys from leather.ticks.base import Ticker from leather.utils import isclose # Shorthand ZERO = Decimal('0') TEN = Decimal('10') #: Normalized intervals to be tested for ticks INTERVALS = [ Decimal('0.1'), Decimal('0.15'), Decimal('0.2'), Decimal('0.25'), Decimal('0.5'), Decimal('1.0') ] #: The default number of ticks to produce DEFAULT_TICKS = 5 #: The minimum length of a viable tick sequence MIN_TICK_COUNT = 4 #: The maximum length of a viable tick sequence MAX_TICK_COUNT = 10 #: Most preferred tick intervals BEST_INTERVALS = [Decimal('0.1'), Decimal('1.0')] #: Least preferred tick intervals WORST_INTERVALS = [Decimal('0.15')] class ScoreTicker(Ticker): """ Attempt to find an optimal series of ticks by generating many possible sequences and scoring them based on several criteria. Only the best tick sequence is returned. Based an algorithm described by Austin Clemens: http://austinclemens.com/blog/2016/01/09/an-algorithm-for-creating-a-graphs-axes/ See :meth:`.ScoreTicker.score` for scoring implementation. :param domain_min: Minimum value of the data series. :param domain_max: Maximum value of the data series. """ def __init__(self, domain_min, domain_max): self._domain_min = domain_min self._domain_max = domain_max self._ticks = self._find_ticks() self._min = self._ticks[0] self._max = self._ticks[-1] @property def ticks(self): return self._ticks @property def min(self): return self._min @property def max(self): return self._max def _find_ticks(self): """ Implements the tick-finding algorithm. """ force_zero = self._domain_min < ZERO and self._domain_max > ZERO interval_guess = abs(self._domain_max - self._domain_min) / (DEFAULT_TICKS - 1) magnitude = interval_guess.log10().to_integral_exact(rounding=ROUND_CEILING) candidate_intervals = [] for interval in INTERVALS: candidate_intervals.append((interval, interval * pow(TEN, magnitude))) candidate_intervals.append((interval, interval * pow(TEN, magnitude - 1))) candidate_intervals.append((interval, interval * pow(TEN, magnitude + 1))) candidate_ticks = [] for base_interval, interval in candidate_intervals: ticks = [] if force_zero: min_steps = (abs(self._domain_min) / interval).to_integral_exact(rounding=ROUND_CEILING) ticks.append(self._round_tick(-min_steps * interval)) else: ticks.append(self._round_tick((self._domain_min / interval).to_integral_exact(rounding=ROUND_FLOOR) * interval)) tick_num = 1 while ticks[tick_num - 1] < self._domain_max: t = self._round_tick(ticks[0] + (interval * tick_num)) ticks.append(t) tick_num += 1 # Throw out sequences that are too short or too long if len(ticks) < MIN_TICK_COUNT or len(ticks) > MAX_TICK_COUNT: continue candidate_ticks.append({ 'base_interval': base_interval, 'interval': interval, 'ticks': ticks, 'score': self._score(base_interval, interval, ticks) }) # Order by best score, using number of ticks as a tie-breaker best = sorted(candidate_ticks, key=lambda c: (c['score']['total'], len(c['ticks']))) return best[0]['ticks'] def _score(self, base_interval, interval, ticks): """ Score a given tick sequence based on several criteria. This method returns discrete scoring components for easier debugging. """ s = { 'pct_waste': 0, 'interval_penalty': 0, 'len_penalty': 0, 'total': 0 } # Penalty for wasted scale space waste = (self._domain_min - ticks[0]) + (ticks[-1] - self._domain_max) pct_waste = waste / (self._domain_max - self._domain_min) s['pct_waste'] = pow(10, pct_waste) # Penalty for choosing less optimal tick intervals if base_interval in BEST_INTERVALS: pass elif base_interval in WORST_INTERVALS: s['interval_penalty'] = 2 else: s['interval_penalty'] = 1 # Penalty for too many ticks if len(ticks) > 5: s['len_penalty'] = (len(ticks) - 5) s['total'] = s['pct_waste'] + s['interval_penalty'] + s['len_penalty'] return s def _round_tick(self, t): """ Round a tick to 0-3 decimal places, if the remaining digits do not appear to be significant. """ for r in range(0, 4): exp = pow(Decimal(10), Decimal(-r)) quantized = t.quantize(exp) if isclose(t, quantized): return quantized return t ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/ticks/score_time.py�����������������������������������������������������������0000664�0000000�0000000�00000012004�13017711424�0020402�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python from datetime import date, datetime, timedelta from decimal import Decimal from functools import partial import math import sys from leather.ticks.score import ScoreTicker from leather import utils #: The default number of ticks to produce DEFAULT_TICKS = 5 #: The minimum length of a viable tick sequence MIN_TICK_COUNT = 4 #: The maximum length of a viable tick sequence MAX_TICK_COUNT = 10 #: The minimum units of the interval needed to use that interval ("4 years") MIN_UNITS = 4 #: The possible intervals as (to_function, from_function, overlap_tick_formatter, simple_tick_formatter) INTERVALS = [ (utils.to_year_count, utils.from_year_count, None, '%Y'), (utils.to_month_count, utils.from_month_count, '%Y-%m', '%m'), (utils.to_day_count, utils.from_day_count, '%m-%d', '%d'), (utils.to_hour_count, utils.from_hour_count, '%d-%H', '%H'), (utils.to_minute_count, utils.from_minute_count, '%H:%M', '%M'), (utils.to_second_count, utils.from_second_count, '%H:%M:%S', '%S'), (utils.to_microsecond_count, utils.from_microsecond_count, '%S-%f', '%f'), ] class ScoreTimeTicker(ScoreTicker): """ A variation on :class:`.ScoreTicker` that generates sequences of dates or datetimes. :param domain_min: Minimum value of the data series. :param domain_max: Maximum value of the data series. """ def __init__(self, domain_min, domain_max): self._domain_min = domain_min self._domain_max = domain_max if isinstance(self._domain_min, datetime): self._type = datetime else: self._type = date # Identify appropriate interval unit self._to_unit = None self._from_unit = None self._fmt = None previous_delta = 0 for to_func, from_func, overlap_fmt, simple_fmt in INTERVALS: delta = to_func(self._domain_max) - to_func(self._domain_min) if delta >= MIN_UNITS or to_func is utils.to_microsecond_count: self._to_unit = to_func self._from_unit = partial(from_func, t=self._type) if previous_delta >= 1: self._fmt = overlap_fmt else: self._fmt = simple_fmt break previous_delta = delta # Compute unit min and max self._unit_min = self._to_unit(self._domain_min) self._unit_max = self._to_unit(self._domain_max) if (self._domain_max - self._from_unit(self._unit_max)).total_seconds() > 0: self._unit_max += 1 self._ticks = self._find_ticks() self._min = self._ticks[0] self._max = self._ticks[-1] def _find_ticks(self): """ Implements the tick-finding algorithm. """ delta = self._unit_max - self._unit_min interval_guess = int(math.ceil(delta / (DEFAULT_TICKS - 1))) candidate_intervals = [] candidate_intervals.append(interval_guess) candidate_intervals.append(interval_guess - 1) candidate_intervals.append(interval_guess + 1) if 0 in candidate_intervals: candidate_intervals.remove(0) candidate_ticks = [] for interval in candidate_intervals: ticks = [] ticks.append(int(math.floor((self._unit_min / interval))) * interval) tick_num = 1 while ticks[tick_num - 1] < self._unit_max: t = ticks[0] + (interval * tick_num) ticks.append(t) tick_num += 1 # Throw out sequences that are too short or too long if len(ticks) < MIN_TICK_COUNT or len(ticks) > MAX_TICK_COUNT: continue candidate_ticks.append({ 'interval': interval, 'ticks': ticks, 'score': self._score(interval, ticks) }) # Order by best score, using number of ticks as a tie-breaker best = sorted(candidate_ticks, key=lambda c: (c['score']['total'], len(c['ticks']))) ticks = best[0]['ticks'] return [self._from_unit(t) for t in ticks] def _score(self, interval, ticks): """ Score a given tick sequence based on several criteria. This method returns discrete scoring components for easier debugging. """ s = { 'pct_waste': 0, 'interval_penalty': 0, 'len_penalty': 0, 'total': 0 } # Penalty for wasted scale space waste = (self._unit_min - ticks[0]) + (ticks[-1] - self._unit_max) pct_waste = waste / (self._unit_max - self._unit_min) s['pct_waste'] = pow(10, pct_waste) # Penalty for too many ticks if len(ticks) > 5: s['len_penalty'] = (len(ticks) - 5) s['total'] = s['pct_waste'] + s['interval_penalty'] + s['len_penalty'] return s def format_tick(self, tick): """ Format a tick using the inferred time formatting. """ return tick.strftime(self._fmt) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/leather/utils.py����������������������������������������������������������������������0000664�0000000�0000000�00000007455�13017711424�0016312�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python from collections import namedtuple from datetime import date, datetime, timedelta from decimal import Decimal import math import sys import warnings import six try: __IPYTHON__ from IPython.display import SVG as IPythonSVG except (NameError, ImportError): IPythonSVG = lambda x: x # Shorthand ZERO = Decimal('0') NINE_PLACES = Decimal('1e-9') #: X data dimension index X = 0 #: Y data dimension index Y = 1 #: Z data dimension index Z = 2 DIMENSION_NAMES = ['X', 'Y', 'Z'] #: Data structure for representing margins or other CSS-edge like properties Box = namedtuple('Box', ['top', 'right', 'bottom', 'left']) #: Data structure for a single series data point Datum = namedtuple('Datum', ['i', 'x', 'y', 'z', 'row']) #: Dummy object used in place of a series when rendering legends for categories DummySeries = namedtuple('DummySeries', ['name']) formatwarning_orig = warnings.formatwarning warnings.formatwarning = lambda message, category, filename, lineno, line=None: \ formatwarning_orig(message, category, filename, lineno, line='') warn = warnings.warn warnings.resetwarnings() warnings.simplefilter('always') # In Python 3.5 use builtin C implementation of `isclose` if sys.version_info >= (3, 5): from math import isclose else: def isclose(a, b, rel_tol=NINE_PLACES, abs_tol=ZERO): """ Test if two floating points numbers are close enough to be considered equal. Via: https://github.com/PythonCHB/close_pep/blob/master/isclose.py Verified against final CPython 3.5 implemenation. :param a: The first number to check. :param b: The second number to check. :param rel_tol: Relative tolerance. The amount of error allowed, relative to the larger input value. Defaults to nine decimal places of accuracy. :param abs_tol: Absolute minimum tolerance. Disabled by default. """ if a == b: return True if rel_tol < ZERO or abs_tol < ZERO: raise ValueError('Tolerances must be non-negative') if math.isinf(abs(a)) or math.isinf(abs(b)): return False diff = abs(b - a) return (((diff <= abs(rel_tol * b)) or (diff <= abs(rel_tol * a))) or (diff <= abs_tol)) def to_year_count(d): """ date > n years """ return d.year def from_year_count(n, t=date): """ n years > date """ return t(n, 1, 1) def to_month_count(d): """ date > n months """ return (d.year * 12) + d.month def from_month_count(n, t=date): """ n months > date """ return t(n // 12, (n % 12) + 1, 1) def to_day_count(d): """ date > n days """ return (d - type(d).min).days def from_day_count(n, t=date): """ n days > date """ return t.min + timedelta(days=n) def to_hour_count(d): """ date > n hours """ return (d - datetime.min).total_seconds() / (60 * 60) def from_hour_count(n, t=datetime): """ n hours > date """ return t.min + timedelta(hours=n) def to_minute_count(d): """ date > n minutes """ return (d - datetime.min).total_seconds() / 60 def from_minute_count(n, t=datetime): """ n minutes > date """ return t.min + timedelta(minutes=n) def to_second_count(d): """ date > n seconds """ return (d - datetime.min).total_seconds() def from_second_count(n, t=datetime): """ n seconds > date """ return t.min + timedelta(seconds=n) def to_microsecond_count(d): """ date > n microseconds """ return (d - datetime.min).total_seconds() * 1000 def from_microsecond_count(n, t=datetime): """ n microseconds > date """ return t.min + timedelta(microseconds=n) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/requirements-py2.txt������������������������������������������������������������������0000664�0000000�0000000�00000000223�13017711424�0017132�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������six>=1.10.0 nose>=1.3.7 wheel>=0.24.0 lxml>=3.6.0 cssselect>=0.9.1 tox>=1.3 Sphinx>=1.2.2 coverage>=3.7.1 sphinx_rtd_theme>=0.1.9 unittest2>=0.5.1 �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/requirements-py3.txt������������������������������������������������������������������0000664�0000000�0000000�00000000202�13017711424�0017130�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������six>=1.10.0 nose>=1.3.7 wheel>=0.24.0 lxml>=3.6.0 cssselect>=0.9.1 tox>=1.3 Sphinx>=1.2.2 coverage>=3.7.1 sphinx_rtd_theme>=0.1.9 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/setup.py������������������������������������������������������������������������������0000664�0000000�0000000�00000003107�13017711424�0014654�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python from setuptools import setup install_requires = [ 'six>=1.6.1' ] setup( name='leather', version='0.3.3', description='Python charting for 80% of humans.', long_description=open('README.rst').read(), author='Christopher Groskopf', author_email='chrisgroskopf@gmail.com', url='http://leather.readthedocs.io/', license='MIT', classifiers=[ 'Development Status :: 3 - Alpha', 'Framework :: IPython', 'Intended Audience :: Developers', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: MIT License', 'Topic :: Multimedia :: Graphics', 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Scientific/Engineering :: Information Analysis', 'Topic :: Scientific/Engineering :: Visualization', 'Topic :: Software Development :: Libraries :: Python Modules', ], packages=[ 'leather', 'leather.scales', 'leather.series', 'leather.shapes', 'leather.ticks' ], install_requires=install_requires ) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/test.py�������������������������������������������������������������������������������0000664�0000000�0000000�00000000655�13017711424�0014500�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -*- coding: utf8 -*- import datetime import leather # data1 = [ # (2, 'foo'), # (6, 'bar'), # (9, 'bing') # ] # # data2 = [ # (3, 'foo'), # (5, 'bar'), # (7, 'bing') # ] # # lattice = leather.Lattice(shape=leather.Bars()) # lattice.add_many([data1, data2]) # lattice.to_svg('test.svg') data1 = [ (2, None), (3, None) ] chart = leather.Chart() chart.add_bars(data1) chart.to_svg('test.svg') �����������������������������������������������������������������������������������leather-0.3.3/tests/��������������������������������������������������������������������������������0000775�0000000�0000000�00000000000�13017711424�0014303�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/tests/__init__.py���������������������������������������������������������������������0000664�0000000�0000000�00000000000�13017711424�0016402�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/tests/test_axis.py��������������������������������������������������������������������0000664�0000000�0000000�00000001636�13017711424�0016666�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python import leather class TestChart(leather.LeatherTestCase): def setUp(self): self.data = [ (0, 3), (4, 5), (7, 9), (10, 4) ] def test_ticks(self): chart = leather.Chart() chart.add_dots(self.data) axis = leather.Axis(ticks=[-12, 0, 17, 44, 87, 99]) chart.set_x_axis(axis) svg = self.render_chart(chart) self.assertTickLabels(svg, 'bottom', ['-12', '17', '44', '87', '99', '0']) def test_tick_formatter(self): chart = leather.Chart() chart.add_dots(self.data) def test_formatter(value, i, count): return '%i+' % (value * 10) axis = leather.Axis(tick_formatter=test_formatter) chart.set_x_axis(axis) svg = self.render_chart(chart) self.assertTickLabels(svg, 'bottom', ['25+', '50+', '75+', '100+', '0+']) ��������������������������������������������������������������������������������������������������leather-0.3.3/tests/test_chart.py�������������������������������������������������������������������0000664�0000000�0000000�00000007171�13017711424�0017023�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python # -*- coding: utf8 -*- import os import warnings import leather TEST_SVG = '.test.svg' class TestChart(leather.LeatherTestCase): def setUp(self): self.data1 = [ (0, 3), (4, 5), (7, 9), (8, 4) ] self.data2 = [ (0, 4), (1, 3), (2, 5), (5, 6), (9, 10) ] def tearDown(self): if os.path.exists(TEST_SVG): os.remove(TEST_SVG) def test_single_series(self): chart = leather.Chart() chart.add_dots(self.data1) svg = self.render_chart(chart) self.assertElementCount(svg, '.axis', 2) self.assertElementCount(svg, '.series', 1) self.assertElementCount(svg, '.dots', 1) self.assertElementCount(svg, 'circle', 4) def test_multiple_series(self): chart = leather.Chart() chart.add_dots(self.data1) chart.add_dots(self.data2) svg = self.render_chart(chart) self.assertElementCount(svg, '.axis', 2) self.assertElementCount(svg, '.series', 2) self.assertElementCount(svg, '.dots', 2) self.assertElementCount(svg, 'circle', 9) def test_unicode(self): chart = leather.Chart() chart.add_bars([(1, u'👍')]) svg = self.render_chart(chart) self.assertElementCount(svg, '.axis', 2) self.assertElementCount(svg, '.series', 1) def test_set_scales(self): chart = leather.Chart() chart.set_x_scale(leather.Linear(0, 20)) chart.set_y_scale(leather.Linear(0, 20)) chart.add_dots(self.data1) svg = self.render_chart(chart) self.assertTickLabels(svg, 'left', ['5', '10', '15', '20', '0']) self.assertTickLabels(svg, 'bottom', ['5', '10', '15', '20', '0']) def test_add_scales(self): chart = leather.Chart() chart.add_x_scale(0, 20) chart.add_y_scale(0, 20) chart.add_dots(self.data1) svg = self.render_chart(chart) self.assertTickLabels(svg, 'left', ['5', '10', '15', '20', '0']) self.assertTickLabels(svg, 'bottom', ['5', '10', '15', '20', '0']) def test_scale_domain_warning(self): chart = leather.Chart() chart.add_x_scale(4, 7) chart.add_y_scale(0, 20) chart.add_dots(self.data1) with warnings.catch_warnings(): warnings.simplefilter('error') with self.assertRaises(UserWarning): self.render_chart(chart) def test_set_axes(self): chart = leather.Chart() chart.set_x_axis(leather.Axis(ticks=[0, 4, 8])) chart.set_y_axis(leather.Axis(ticks=[3, 6, 9])) chart.add_dots(self.data1) svg = self.render_chart(chart) self.assertTickLabels(svg, 'left', ['3', '6', '9']) self.assertTickLabels(svg, 'bottom', ['4', '8', '0']) def test_add_axes(self): chart = leather.Chart() chart.add_x_axis(ticks=[0, 4, 8]) chart.add_y_axis(ticks=[3, 6, 9]) chart.add_dots(self.data1) svg = self.render_chart(chart) self.assertTickLabels(svg, 'left', ['3', '6', '9']) self.assertTickLabels(svg, 'bottom', ['4', '8', '0']) def test_to_svg_file_name(self): chart = leather.Chart() chart.add_dots(self.data1) chart.to_svg('.test.svg') self.assertTrue(os.path.exists(TEST_SVG)) def test_to_svg_file_handle(self): chart = leather.Chart() chart.add_dots(self.data1) with open('.test.svg', 'w') as f: chart.to_svg(f) self.assertTrue(os.path.exists(TEST_SVG)) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/tests/test_grid.py��������������������������������������������������������������������0000664�0000000�0000000�00000004206�13017711424�0016643�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python import os import leather TEST_SVG = '.test.svg' class TestGrid(leather.LeatherTestCase): def setUp(self): self.data1 = [ (0, 3), (4, 5), (7, 9), (8, 4) ] self.data2 = [ (0, 4), (1, 3), (2, 5), (5, 6), (9, 10) ] def tearDown(self): if os.path.exists(TEST_SVG): os.remove(TEST_SVG) def test_add_one(self): chart1 = leather.Chart() chart1.add_dots(self.data1) chart2 = leather.Chart() chart2.add_dots(self.data2) grid = leather.Grid() grid.add_one(chart1) grid.add_one(chart2) svg = self.render_chart(grid) self.assertElementCount(svg, '.axis', 4) self.assertElementCount(svg, '.series', 2) self.assertElementCount(svg, '.dots', 2) self.assertElementCount(svg, 'circle', 9) def test_add_many(self): chart1 = leather.Chart() chart1.add_dots(self.data1) chart2 = leather.Chart() chart2.add_dots(self.data2) grid = leather.Grid() grid.add_many([chart1, chart2, chart1]) svg = self.render_chart(grid) self.assertElementCount(svg, '.axis', 6) self.assertElementCount(svg, '.series', 3) self.assertElementCount(svg, '.dots', 3) self.assertElementCount(svg, 'circle', 13) def test_to_svg_file_name(self): chart1 = leather.Chart() chart1.add_dots(self.data1) chart2 = leather.Chart() chart2.add_dots(self.data2) grid = leather.Grid() grid.add_many([chart1, chart2, chart1]) grid.to_svg('.test.svg') self.assertTrue(os.path.exists(TEST_SVG)) def test_to_svg_file_handle(self): chart1 = leather.Chart() chart1.add_dots(self.data1) chart2 = leather.Chart() chart2.add_dots(self.data2) grid = leather.Grid() grid.add_many([chart1, chart2, chart1]) with open('.test.svg', 'w') as f: grid.to_svg(f) self.assertTrue(os.path.exists(TEST_SVG)) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/tests/test_lattice.py�����������������������������������������������������������������0000664�0000000�0000000�00000003776�13017711424�0017356�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python import leather class TestLattice(leather.LeatherTestCase): def setUp(self): self.data1 = [ (0, 3), (4, 5), (7, 9), (8, 4) ] self.data2 = [ (0, 4), (1, 3), (2, 5), (5, 6), (9, 10) ] def test_add_one(self): lattice = leather.Lattice() lattice.add_one(self.data1) lattice.add_one(self.data2) svg = self.render_chart(lattice) self.assertElementCount(svg, '.axis', 4) self.assertElementCount(svg, '.series', 2) self.assertElementCount(svg, '.lines', 2) def test_add_many(self): lattice = leather.Lattice() lattice.add_many([self.data1, self.data2, self.data1]) svg = self.render_chart(lattice) self.assertElementCount(svg, '.axis', 6) self.assertElementCount(svg, '.series', 3) self.assertElementCount(svg, '.lines', 3) def test_bars(self): data1 = [ (2, 'foo'), (6, 'bar'), (9, 'bing') ] data2 = [ (3, 'foo'), (5, 'bar'), (7, 'bing') ] lattice = leather.Lattice(shape=leather.Bars()) lattice.add_many([data1, data2]) svg = self.render_chart(lattice) self.assertElementCount(svg, '.axis', 4) self.assertElementCount(svg, '.series', 2) self.assertElementCount(svg, '.bars', 2) def test_bars_different(self): data1 = [ (3, 'foo'), (5, 'bar'), (9, 'bing') ] data2 = [ (3, 'foo'), (5, 'bar'), (9, 'baz') ] lattice = leather.Lattice(shape=leather.Bars()) lattice.add_many([data1, data2]) svg = self.render_chart(lattice) self.assertElementCount(svg, '.axis', 4) self.assertElementCount(svg, '.series', 2) self.assertElementCount(svg, '.bars', 2) ��leather-0.3.3/tests/test_scales.py������������������������������������������������������������������0000664�0000000�0000000�00000011250�13017711424�0017165�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python from datetime import date, datetime from decimal import Decimal import leather class TestLinear(leather.LeatherTestCase): def test_project(self): scale = leather.Linear(0, 10) self.assertEqual(scale.project(2, 0, 20), 4) self.assertEqual(scale.project(10, 0, 40), 40) self.assertEqual(scale.project(5, 10, 40), 25) self.assertEqual(scale.project(5, 10, 41), 25.5) scale = leather.Linear(10, 40) self.assertEqual(scale.project(25, 0, 10), 5) self.assertEqual(scale.project(4, 0, 20), -4) scale = leather.Linear(-10, 10) self.assertEqual(scale.project(0, 0, 10), 5) self.assertEqual(scale.project(-10, -5, 10), -5) scale = leather.Linear(-20, -10) self.assertEqual(scale.project(-15, 0, 10), 5) self.assertEqual(scale.project(-10, -5, 10), 10) with self.assertRaises(ValueError): leather.Linear(10, 0) def test_no_spread(self): scale = leather.Linear(0, 0) self.assertEqual(scale.project(0, 0, 10), 0) self.assertEqual(scale.project(1, 0, 10), 10) def test_ticks(self): scale = leather.Linear(0, 10) self.assertEqual(scale.ticks(), [0, 2.5, 5, 7.5, 10]) def test_decimal(self): scale = leather.Linear(Decimal(0), Decimal(10)) self.assertEqual(scale.project(Decimal(2), Decimal(0), Decimal(20)), Decimal(4)) self.assertEqual(scale.project(Decimal(10), Decimal(0), Decimal(40)), Decimal(40)) self.assertEqual(scale.project(Decimal(5), Decimal(10), Decimal(40)), Decimal(25)) self.assertEqual(scale.project(Decimal(5), Decimal(10), Decimal(41)), Decimal(25.5)) self.assertEqual(scale.ticks()[1], Decimal(2.5)) def test_contains(self): scale = leather.Linear(-5, 5) self.assertTrue(scale.contains(-5)) self.assertTrue(scale.contains(0)) self.assertTrue(scale.contains(5)) self.assertFalse(scale.contains(-6)) self.assertFalse(scale.contains(6)) class TestOrdinal(leather.LeatherTestCase): 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(), ['a', 'b', 'c', 'd']) def test_contains(self): scale = leather.Ordinal(['a', 'b', 'c', 'd']) self.assertTrue(scale.contains('a')) self.assertFalse(scale.contains('aa')) self.assertFalse(scale.contains('e')) self.assertFalse(scale.contains(5)) class TestTemporal(leather.LeatherTestCase): """ 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() self.assertEqual(ticks[0], date(2010, 1, 1)) self.assertEqual(ticks[-1], date(2014, 1, 1)) def test_contains(self): scale = leather.Temporal(date(2010, 1, 1), date(2014, 1, 1)) self.assertTrue(scale.contains(date(2010, 1, 1))) self.assertTrue(scale.contains(date(2012, 6, 3))) self.assertTrue(scale.contains(date(2014, 1, 1))) self.assertFalse(scale.contains(date(2009, 12, 31))) self.assertFalse(scale.contains(date(2014, 1, 2))) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/tests/test_series.py������������������������������������������������������������������0000664�0000000�0000000�00000005174�13017711424�0017215�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python import leather from leather.utils import X, Y, Z class TestSeries(leather.LeatherTestCase): def test_pairs(self): data = [ ('foo', 1), ('bar', 2), ('baz', 3) ] series = leather.Series(data) 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.assertSequenceEqual(series.values(X), ['foo', 'bar', 'baz']) self.assertSequenceEqual(series.values(Y), [1, 2, 3]) series = leather.Series(data, 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, 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(KeyError): series = leather.Series(data) series = leather.Series(data, 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) with self.assertRaises(TypeError): series = leather.Series(data, x='words', y='more') def get_x(row, i): return row.b def get_y(row, i): return row.c series = leather.Series(data, x=get_x, y=get_y) self.assertSequenceEqual(series.values(X), [1, 2, 3]) self.assertSequenceEqual(series.values(Y), [4, 5, 6]) class TestCategorySeries(leather.LeatherTestCase): def test_triples(self): data = [ ('foo', 1, 'a'), ('bar', 2, 'a'), ('baz', 3, 'b') ] series = leather.CategorySeries(data) self.assertSequenceEqual(series.values(X), ['foo', 'bar', 'baz']) self.assertSequenceEqual(series.values(Y), [1, 2, 3]) self.assertSequenceEqual(series.values(Z), ['a', 'a', 'b']) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/tests/test_shapes.py������������������������������������������������������������������0000664�0000000�0000000�00000015050�13017711424�0017200�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python import leather class TestBars(leather.LeatherTestCase): def setUp(self): self.shape = leather.Bars('red') self.linear = leather.Linear(0, 10) self.ordinal = leather.Ordinal(['foo', 'bar', 'bing']) self.palette = (color for color in ['red', 'white', 'blue']) def test_to_svg(self): series = leather.Series([ (0, 'foo'), (5, 'bar'), (10, 'bing') ]) group = self.shape.to_svg(200, 100, self.linear, self.ordinal, series, self.palette) rects = list(group) self.assertEqual(len(rects), 3) self.assertEqual(float(rects[2].get('x')), 0) self.assertEqual(float(rects[2].get('width')), 200) def test_nulls(self): series = leather.Series([ (0, 'foo'), (None, None), (10, 'bing') ]) group = self.shape.to_svg(200, 100, self.linear, self.ordinal, series, self.palette) rects = list(group) self.assertEqual(len(rects), 2) self.assertEqual(float(rects[1].get('x')), 0) self.assertEqual(float(rects[1].get('width')), 200) def test_zeros(self): series = leather.Series([ (0, 'foo'), (0, None), (0, 'bing') ]) linear = leather.Linear(0, 0) group = self.shape.to_svg(200, 100, linear, self.ordinal, series, self.palette) rects = list(group) self.assertEqual(len(rects), 2) self.assertEqual(float(rects[1].get('x')), 0) self.assertEqual(float(rects[1].get('width')), 0) def test_validate(self): series = leather.Series([ (1, 'foo') ]) self.shape.validate_series(series) series = leather.Series([ ('foo', 1) ]) with self.assertRaises(ValueError): self.shape.validate_series(series) class TestColumns(leather.LeatherTestCase): def setUp(self): self.shape = leather.Columns('red') self.linear = leather.Linear(0, 10) self.ordinal = leather.Ordinal(['foo', 'bar', 'bing']) self.palette = (color for color in ['red', 'white', 'blue']) def test_to_svg(self): series = leather.Series([ ('foo', 0), ('bar', 5), ('bing', 10) ]) group = self.shape.to_svg(200, 100, self.ordinal, self.linear, series, self.palette) rects = list(group) self.assertEqual(len(rects), 3) self.assertEqual(float(rects[1].get('y')), 50) self.assertEqual(float(rects[1].get('height')), 50) def test_nulls(self): series = leather.Series([ ('foo', 0), (None, None), ('bing', 10) ]) group = self.shape.to_svg(200, 100, 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')), 100) def test_validate(self): series = leather.Series([ ('foo', 1) ]) self.shape.validate_series(series) series = leather.Series([ (1, 'foo') ]) with self.assertRaises(ValueError): self.shape.validate_series(series) class TestDots(leather.LeatherTestCase): def setUp(self): self.shape = leather.Dots('red') self.linear = leather.Linear(0, 10) self.ordinal = leather.Ordinal(['foo', 'bar', 'bing']) self.palette = (color for color in ['red', 'white', 'blue']) def test_linear(self): series = leather.Series([ (0, 0), (5, 5), (10, 10) ]) group = self.shape.to_svg(200, 100, self.linear, self.linear, series, self.palette) circles = list(group) self.assertEqual(len(circles), 3) self.assertEqual(float(circles[1].get('cx')), 100) self.assertEqual(float(circles[1].get('cy')), 50) def test_ordinal(self): series = leather.Series([ ('foo', 0), ('bar', 5), ('bing', 10) ]) group = self.shape.to_svg(200, 100, self.ordinal, self.linear, series, self.palette) circles = list(group) self.assertEqual(len(circles), 3) self.assertEqual(float(circles[1].get('cx')), 100) self.assertEqual(float(circles[1].get('cy')), 50) def test_nulls(self): series = leather.Series([ (0, 0), (None, None), (10, 10) ]) group = self.shape.to_svg(200, 100, self.linear, self.linear, series, self.palette) circles = list(group) self.assertEqual(len(circles), 2) self.assertEqual(float(circles[1].get('cx')), 200) self.assertEqual(float(circles[1].get('cy')), 0) def test_validate(self): series = leather.Series([ (1, 1) ]) self.shape.validate_series(series) series = leather.Series([ (1, 'foo') ]) with self.assertRaises(ValueError): self.shape.validate_series(series) class TestLine(leather.LeatherTestCase): def setUp(self): self.shape = leather.Line('red') self.linear = leather.Linear(0, 10) self.ordinal = leather.Ordinal(['foo', 'bar', 'bing']) self.palette = (color for color in ['red', 'white', 'blue']) def test_linear(self): series = leather.Series([ (0, 0), (5, 5), (10, 10) ]) group = self.shape.to_svg(200, 100, self.linear, self.linear, series, self.palette) paths = list(group) self.assertEqual(len(paths), 1) def test_ordinal(self): series = leather.Series([ ('foo', 0), ('bar', 5), ('bing', 10) ]) group = self.shape.to_svg(200, 100, self.ordinal, self.linear, series, self.palette) paths = list(group) self.assertEqual(len(paths), 1) def test_nulls(self): series = leather.Series([ (0, 0), (None, None), (10, 10) ]) group = self.shape.to_svg(200, 100, self.linear, self.linear, series, self.palette) paths = list(group) self.assertEqual(len(paths), 2) def test_validate(self): series = leather.Series([ (1, 1) ]) self.shape.validate_series(series) series = leather.Series([ (1, 'foo') ]) with self.assertRaises(ValueError): self.shape.validate_series(series) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/tests/test_ticks.py�������������������������������������������������������������������0000664�0000000�0000000�00000011170�13017711424�0017031�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python from datetime import date, datetime from decimal import Decimal import leather from leather.ticks.score_time import ScoreTicker, ScoreTimeTicker from leather import utils class TestScoreTicker(leather.LeatherTestCase): def test_years(self): ticker = ScoreTicker(Decimal(0), Decimal(10)) self.assertIsInstance(ticker.ticks[0], Decimal) class TestScoreTimeTicker(leather.LeatherTestCase): def test_years(self): ticker = ScoreTimeTicker( date(2010, 1, 1), date(2015, 1, 1) ) self.assertIsInstance(ticker.ticks[0], date) self.assertIs(ticker._to_unit, utils.to_year_count) def test_years_datetime(self): ticker = ScoreTimeTicker( datetime(2011, 1, 1), datetime(2015, 1, 1) ) self.assertIsInstance(ticker.ticks[0], datetime) self.assertIs(ticker._to_unit, utils.to_year_count) def test_months(self): ticker = ScoreTimeTicker( date(2011, 3, 1), date(2011, 7, 1) ) self.assertIsInstance(ticker.ticks[0], date) self.assertIs(ticker._to_unit, utils.to_month_count) def test_months_datetime(self): ticker = ScoreTimeTicker( datetime(2011, 3, 1), datetime(2011, 7, 1) ) self.assertIsInstance(ticker.ticks[0], datetime) self.assertIs(ticker._to_unit, utils.to_month_count) def test_months_for_years(self): ticker = ScoreTimeTicker( date(2011, 1, 1), date(2013, 1, 1) ) self.assertIsInstance(ticker.ticks[0], date) self.assertIs(ticker._to_unit, utils.to_month_count) def test_days(self): ticker = ScoreTimeTicker( date(2011, 3, 5), date(2011, 3, 10) ) self.assertIsInstance(ticker.ticks[0], date) self.assertIs(ticker._to_unit, utils.to_day_count) def test_days_datetime(self): ticker = ScoreTimeTicker( datetime(2011, 3, 5), datetime(2011, 3, 10) ) self.assertIsInstance(ticker.ticks[0], datetime) self.assertIs(ticker._to_unit, utils.to_day_count) def test_days_for_months(self): ticker = ScoreTimeTicker( date(2011, 3, 1), date(2011, 5, 1) ) self.assertIsInstance(ticker.ticks[0], date) self.assertIs(ticker._to_unit, utils.to_day_count) def test_hours(self): ticker = ScoreTimeTicker( datetime(2011, 3, 5, 2), datetime(2011, 3, 5, 10) ) self.assertIsInstance(ticker.ticks[0], datetime) self.assertIs(ticker._to_unit, utils.to_hour_count) def test_hours_for_days(self): ticker = ScoreTimeTicker( datetime(2011, 3, 5), datetime(2011, 3, 6) ) self.assertIsInstance(ticker.ticks[0], datetime) self.assertIs(ticker._to_unit, utils.to_hour_count) def test_minutes(self): ticker = ScoreTimeTicker( datetime(2011, 3, 5, 2, 15), datetime(2011, 3, 5, 2, 45) ) self.assertIsInstance(ticker.ticks[0], datetime) self.assertIs(ticker._to_unit, utils.to_minute_count) def test_minutes_for_hours(self): ticker = ScoreTimeTicker( datetime(2011, 3, 5, 2), datetime(2011, 3, 5, 3) ) self.assertIsInstance(ticker.ticks[0], datetime) self.assertIs(ticker._to_unit, utils.to_minute_count) def test_seconds(self): ticker = ScoreTimeTicker( datetime(2011, 3, 5, 2, 15, 15), datetime(2011, 3, 5, 2, 15, 45) ) self.assertIsInstance(ticker.ticks[0], datetime) self.assertIs(ticker._to_unit, utils.to_second_count) def test_seconds_for_minutes(self): ticker = ScoreTimeTicker( datetime(2011, 3, 5, 2, 15), datetime(2011, 3, 5, 2, 18) ) self.assertIsInstance(ticker.ticks[0], datetime) self.assertIs(ticker._to_unit, utils.to_second_count) def test_microseconds(self): ticker = ScoreTimeTicker( datetime(2011, 3, 5, 2, 15, 15, 1000), datetime(2011, 3, 5, 2, 15, 15, 5000) ) self.assertIsInstance(ticker.ticks[0], datetime) self.assertIs(ticker._to_unit, utils.to_microsecond_count) def test_microseconds_for_seconds(self): ticker = ScoreTimeTicker( datetime(2011, 3, 5, 2, 15, 15), datetime(2011, 3, 5, 2, 15, 17) ) self.assertIsInstance(ticker.ticks[0], datetime) self.assertIs(ticker._to_unit, utils.to_microsecond_count) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������leather-0.3.3/tox.ini�������������������������������������������������������������������������������0000664�0000000�0000000�00000001160�13017711424�0014452�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[tox] envlist = py27,py33,py34,py35,pypy [testenv] deps= nose>=1.1.2 six>=1.6.1 lxml>=3.6.0 cssselect>=0.9.1 commands=nosetests tests [testenv:py27] deps= {[testenv]deps} [testenv:py33] deps= {[testenv]deps} [testenv:py34] deps= {[testenv:py33]deps} [testenv:py35] deps= {[testenv:py33]deps} [testenv:pypy] deps= {[testenv:py27]deps} [flake8] ignore=E128,E402,E501,F403 # E128 continuation line under-indented for visual indent # E402 module level import not at top of file # E501 line too long (X > 79 characters) # F403 'from xyz import *' used; unable to detect undefined names ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������