├── pyd3 ├── tests │ ├── __init__.py │ ├── test_interpolate_rgb.py │ ├── test_interpolate_list.py │ ├── test_interpolate_round.py │ ├── test_interpolate_number.py │ ├── test_interpolate_dict.py │ ├── test_interpolate_string.py │ └── test_scale_linear.py ├── __init__.py ├── color.py ├── interpolate.py └── scale.py ├── setup.py ├── LICENSE.txt ├── README.md └── README-interpolate.md /pyd3/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) 2016, Nicolas P. Rougier. All rights reserved. 3 | # Distributed under the terms of the new BSD License. 4 | # ----------------------------------------------------------------------------- 5 | -------------------------------------------------------------------------------- /pyd3/__init__.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) 2016, Nicolas P. Rougier. All rights reserved. 3 | # Distributed under the terms of the new BSD License. 4 | # ----------------------------------------------------------------------------- 5 | 6 | 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #----------------------------------------------------------------------------- 2 | # Copyright (c) 2016 Nicolas P. Rougier 3 | # Distributed under the terms of the BSD License. 4 | #----------------------------------------------------------------------------- 5 | from setuptools import setup 6 | 7 | if __name__ == "__main__": 8 | setup(name='pyd3', 9 | version='0.1', 10 | description='D3 modules', 11 | author='Nicolas P. Rougier', 12 | author_email='Nicolas.Rougier@inria.fr', 13 | url='', 14 | packages=['pyd3', 'pyd3.tests'], 15 | ) 16 | -------------------------------------------------------------------------------- /pyd3/tests/test_interpolate_rgb.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) 2016, Nicolas P. Rougier. All rights reserved. 3 | # Distributed under the terms of the new BSD License. 4 | # ----------------------------------------------------------------------------- 5 | import unittest 6 | from pyd3.color import Color 7 | from pyd3 import interpolate 8 | 9 | 10 | class test_number(unittest.TestCase): 11 | 12 | def test_1(self): 13 | """ 14 | interpolate_rgb(a, b) converts a and b to RGB colors 15 | """ 16 | i = interpolate.rgb( Color("steelblue"), Color("brown")) 17 | self.assertEqual(i(0), Color("steelblue")) 18 | self.assertEqual(i(1), Color("brown")) 19 | 20 | def test_2(self): 21 | """ 22 | interpolate_rgb(a, b) interpolates in RGB and returns a hexadecimal string 23 | """ 24 | i = interpolate.rgb(Color("steelblue"), Color("#f00")) 25 | self.assertEqual(i(.2), Color("#6b6890")) 26 | 27 | if __name__ == "__main__": 28 | unittest.main() 29 | -------------------------------------------------------------------------------- /pyd3/tests/test_interpolate_list.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) 2016, Nicolas P. Rougier. All rights reserved. 3 | # Distributed under the terms of the new BSD License. 4 | # ----------------------------------------------------------------------------- 5 | import unittest 6 | from pyd3 import interpolate 7 | 8 | class test_number(unittest.TestCase): 9 | 10 | def test_1(self): 11 | """ 12 | interpolate_list(a, b) interpolates defined elements in a and b 13 | """ 14 | 15 | i = interpolate.list([2, 12], [4, 24]) 16 | self.assertEqual(i(.5), [3, 18]) 17 | 18 | def test_2(self): 19 | """ 20 | interpolate_list(a, b) nested objects and arrays 21 | """ 22 | i = interpolate.list([[2, 12]], [[4, 24]]) 23 | self.assertEqual(i(.5), [[3, 18]]) 24 | 25 | 26 | def test_3(self): 27 | """ 28 | interpolate_list(a, b) merges non-shared elements 29 | """ 30 | i = interpolate.list([[2, 12]], [[4, 24,12]]) 31 | self.assertEqual(i(.5), [[3, 18, 12]]) 32 | 33 | i = interpolate.list([[2, 12,12]], [[4, 24,12]]) 34 | self.assertEqual(i(.5), [[3, 18, 12]]) 35 | 36 | if __name__ == "__main__": 37 | unittest.main() 38 | -------------------------------------------------------------------------------- /pyd3/tests/test_interpolate_round.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) 2016, Nicolas P. Rougier. All rights reserved. 3 | # Distributed under the terms of the new BSD License. 4 | # ----------------------------------------------------------------------------- 5 | import unittest 6 | from pyd3 import interpolate 7 | 8 | class test_number(unittest.TestCase): 9 | 10 | def test_1(self): 11 | """ 12 | interpolate_round(a, b) interpolates between two numbers a and b, and then 13 | rounds. 14 | """ 15 | 16 | i = interpolate.round(10, 42) 17 | self.assertEqual(i(0.0), 10) 18 | self.assertEqual(i(0.1), 13) 19 | self.assertEqual(i(0.2), 16) 20 | self.assertEqual(i(0.3), 20) 21 | self.assertEqual(i(0.4), 23) 22 | self.assertEqual(i(0.5), 26) 23 | self.assertEqual(i(0.6), 29) 24 | self.assertEqual(i(0.7), 32) 25 | self.assertEqual(i(0.8), 36) 26 | self.assertEqual(i(0.9), 39) 27 | self.assertEqual(i(1.0), 42) 28 | 29 | def test_2(self): 30 | """ round(a, b) does not pre-round a and b """ 31 | i = interpolate.round(2.6, 3.6) 32 | self.assertEqual(i(0.6), 3) 33 | 34 | if __name__ == "__main__": 35 | unittest.main() 36 | -------------------------------------------------------------------------------- /pyd3/tests/test_interpolate_number.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) 2016, Nicolas P. Rougier. All rights reserved. 3 | # Distributed under the terms of the new BSD License. 4 | # ----------------------------------------------------------------------------- 5 | import unittest 6 | from pyd3 import interpolate 7 | 8 | class test_number(unittest.TestCase): 9 | 10 | def test_1(self): 11 | """ 12 | interpolate_number(a, b) interpolates between two numbers a and b. 13 | """ 14 | 15 | i = interpolate.number(10, 42) 16 | self.assertAlmostEqual(i(0.0), 10.0, delta=1e-6) 17 | self.assertAlmostEqual(i(0.1), 13.2, delta=1e-6) 18 | self.assertAlmostEqual(i(0.2), 16.4, delta=1e-6) 19 | self.assertAlmostEqual(i(0.3), 19.6, delta=1e-6) 20 | self.assertAlmostEqual(i(0.4), 22.8, delta=1e-6) 21 | self.assertAlmostEqual(i(0.5), 26.0, delta=1e-6) 22 | self.assertAlmostEqual(i(0.6), 29.2, delta=1e-6) 23 | self.assertAlmostEqual(i(0.7), 32.4, delta=1e-6) 24 | self.assertAlmostEqual(i(0.8), 35.6, delta=1e-6) 25 | self.assertAlmostEqual(i(0.9), 38.8, delta=1e-6) 26 | self.assertAlmostEqual(i(1.0), 42.0, delta=1e-6) 27 | 28 | if __name__ == "__main__": 29 | unittest.main() 30 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Nicolas P. Rougier. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 13 | * Neither the name of Nicolas P. Rougier nor the names of its 14 | contributors may be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 18 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 19 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 20 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 21 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 22 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 23 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 24 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 25 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 26 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /pyd3/tests/test_interpolate_dict.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) 2016, Nicolas P. Rougier. All rights reserved. 3 | # Distributed under the terms of the new BSD License. 4 | # ----------------------------------------------------------------------------- 5 | import unittest 6 | from pyd3 import interpolate 7 | 8 | class test_number(unittest.TestCase): 9 | 10 | def test_1(self): 11 | """ 12 | interpolate_dict(a, b) interpolates defined elements in a and b 13 | """ 14 | 15 | i = interpolate.dict({"value": 2}, {"value": 4}) 16 | self.assertEqual(i(.5), {"value": 3}) 17 | 18 | def test_2(self): 19 | """ 20 | interpolate_dict(a, b) merges non-shared properties 21 | """ 22 | 23 | i = interpolate.dict({"foo": 2 }, 24 | {"foo": 4, "bar":4}) 25 | self.assertEqual(i(.5), {"foo": 3, "bar": 4}) 26 | 27 | i = interpolate.dict({"foo": 2, "bar":4}, 28 | {"foo": 4 }) 29 | self.assertEqual(i(.5), {"foo": 3, "bar": 4}) 30 | 31 | def test_3(self): 32 | """ 33 | interpolate_dict(a, b) interpolates nested dicts and list 34 | """ 35 | i = interpolate.dict( {"foo": [2, 12]}, 36 | {"foo": [4, 24]}) 37 | self.assertEqual(i(.5), {"foo": [3, 18]}) 38 | 39 | i = interpolate.dict( {"foo": {"bar": [2, 12]}}, 40 | {"foo": {"bar": [4, 24]}}) 41 | self.assertEqual(i(.5), {"foo": {"bar": [3, 18]}}) 42 | 43 | def test_4(self): 44 | """ 45 | interpolate_dict(a, b) interpolates color properties as rgb 46 | """ 47 | 48 | i = interpolate.dict({"bg": "red"}, {"bg": "green"}) 49 | self.assertEqual(i(.5), {"bg": "#804000"}) 50 | 51 | 52 | if __name__ == "__main__": 53 | unittest.main() 54 | -------------------------------------------------------------------------------- /pyd3/tests/test_interpolate_string.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) 2016, Nicolas P. Rougier. All rights reserved. 3 | # Distributed under the terms of the new BSD License. 4 | # ----------------------------------------------------------------------------- 5 | import unittest 6 | from pyd3 import interpolate 7 | 8 | class test_string(unittest.TestCase): 9 | 10 | def test_1(self): 11 | """ 12 | string(a, b) interpolates matching numbers in a and b. 13 | """ 14 | i = interpolate.string(" 10/20 30", "50/10 100 ") 15 | self.assertEqual( i(0.2), "18/18 44 ") 16 | self.assertEqual( i(0.4), "26/16 58 ") 17 | 18 | def test_3(self): 19 | """ 20 | string(a, b) preserves non-numbers in string b. 21 | """ 22 | i = interpolate.string(" 10/20 30", "50/10 foo ") 23 | self.assertEqual( i(0.2), "18/18 foo ") 24 | self.assertEqual( i(0.4), "26/16 foo ") 25 | 26 | def test_4(self): 27 | """ 28 | string(a, b) preserves non-matching numbers in string b. 29 | """ 30 | i = interpolate.string(" 10/20 foo", "50/10 100 ") 31 | self.assertEqual( i(0.2), "18/18 100 ") 32 | self.assertEqual( i(0.4), "26/16 100 ") 33 | 34 | def test_5(self): 35 | """ 36 | string(a, b) preserves equal-value numbers in both strings. 37 | """ 38 | i = interpolate.string(" 10/20 100 20", "50/10 100, 20 ") 39 | self.assertEqual( i(0.2), "18/18 100, 20 ") 40 | self.assertEqual( i(0.4), "26/16 100, 20 ") 41 | 42 | def test_6(self): 43 | """ 44 | string(a, b) interpolates decimal notation correctly. 45 | """ 46 | i = interpolate.string("1.", "2.") 47 | self.assertEqual( i(0.5), "1.5") 48 | 49 | def test_7(self): 50 | """ 51 | string(a, b) interpolates exponent notation correctly. 52 | """ 53 | self.assertEqual(interpolate.string("1e+3", "1e+4")(0.5), "5500") 54 | self.assertEqual(interpolate.string("1e-3", "1e-4")(0.5), "0.00055") 55 | self.assertEqual(interpolate.string("1.e-3", "1.e-4")(0.5), "0.00055") 56 | self.assertEqual(interpolate.string("-1.e-3", "-1.e-4")(0.5), "-0.00055") 57 | self.assertEqual(interpolate.string("+1.e-3", "+1.e-4")(0.5), "0.00055") 58 | self.assertEqual(interpolate.string(".1e-2", ".1e-3")(0.5), "0.00055") 59 | 60 | def test_8(self): 61 | """ 62 | string(a, b) with no numbers, returns the target string. 63 | """ 64 | self.assertEqual(interpolate.string("foo", "bar")(.5), "bar") 65 | self.assertEqual(interpolate.string("foo", "")(.5), "") 66 | self.assertEqual(interpolate.string("", "bar")(.5), "bar") 67 | self.assertEqual(interpolate.string("", "")(.5), "") 68 | 69 | def test_9(self): 70 | """ 71 | string(a, b) with two numerically-equivalent numbers, returns the default 72 | format. 73 | """ 74 | self.assertEqual(interpolate.string("top: 1000px;", "top: 1e3px;")(.5), 75 | "top: 1000px;") 76 | self.assertEqual(interpolate.string("top: 1e3px;", "top: 1000px;")(.5), 77 | "top: 1000px;") 78 | 79 | if __name__ == "__main__": 80 | unittest.main() 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pyd3 is a python translation of some modules of the [D3](https://github.com/d3) 2 | library. 3 | 4 | **Content** 5 | 6 | * [color]() — Color spaces! RGB, HSL, Cubehelix, Lab (CIELAB) and HCL (CIELCH). 7 | * [interpolate](#interpolate) — Interpolate numbers, colors, strings, arrays, objects, whatever! 8 | * [scale](#scale) — Encodings that map abstract data to visual representation. 9 | * [continuous](#continuous) — map a continuous, quantitative input domain to a continuous output range. 10 | 11 | #interpolate 12 | 13 | This module provides a variety of interpolation methods for blending between 14 | two values. Values may be numbers, colors, strings, arrays, or even 15 | deeply-nested objects. For example: 16 | 17 | ```python 18 | i = interpolate.number(10, 20) 19 | i(0.0) # 10 20 | i(0.2) # 12 21 | i(0.5) # 15 22 | i(1.0) # 20 23 | ``` 24 | 25 | The returned function `i` is called an *interpolator*. Given a starting value 26 | *a* and an ending value *b*, it takes a parameter *t* in the domain [0, 1] and 27 | returns the corresponding interpolated value between *a* and *b*. An 28 | interpolator typically returns a value equivalent to *a* at *t* = 0 and a value 29 | equivalent to *b* at *t* = 1. 30 | 31 | You can interpolate more than just numbers. To find the perceptual midpoint 32 | between steelblue and brown: 33 | 34 | ```python 35 | interpolate.rgb("white", "black")(0.5) # "#808080 36 | ``` 37 | 38 | Here’s a more elaborate example demonstrating type inference used by value: 39 | 40 | ```python 41 | i = interpolate.value({"colors": ["red", "blue"]}, 42 | {"colors": ["white", "black"]}) 43 | i(0.0) # {'colors': ["#ff0000", "#0000ff"]} 44 | i(0.5) # {'colors': ["#ff8080", "#000080"]} 45 | i(1.0) # {'colors': ["#ffffff", "#000000"]} 46 | ``` 47 | 48 | Note that the generic value interpolator detects not only nested objects and 49 | lists, but also color strings and numbers embedded in strings! 50 | 51 | 52 | #scale 53 | 54 | Scales are a convenient abstraction for a fundamental task in 55 | visualization: mapping a dimension of abstract data to a visual 56 | representation. Although most often used for position-encoding quantitative 57 | data, such as mapping a measurement in meters to a position in pixels for dots 58 | in a scatterplot, scales can represent virtually any visual encoding, such as 59 | diverging colors, stroke widths, or symbol size. Scales can also be used with 60 | virtually any type of data, such as named categorical data or discrete data 61 | that requires sensible breaks. 62 | 63 | For continuous quantitative data, you typically want a linear scale. (For time 64 | series data, a time scale.) If the distribution calls for it, consider 65 | transforming data using a power or log scale. A quantize scale may aid 66 | differentiation by rounding continuous data to a fixed set of discrete values; 67 | similarly, a quantile scale computes quantiles from a sample population, and a 68 | threshold scale allows you to specify arbitrary breaks in continuous 69 | data. Several built-in sequential color scales are also provided. (If you don’t 70 | like these palettes, try ColorBrewer.) 71 | 72 | For discrete ordinal (ordered) or categorical (unordered) data, an ordinal 73 | scale specifies an explicit mapping from a set of data values to a 74 | corresponding set of visual attributes (such as colors). The related band and 75 | point scales are useful for position-encoding ordinal data, such as bars in a 76 | bar chart or dots in an categorical scatterplot. Several built-in categorical 77 | color scales are also provided. 78 | 79 | Scales have no intrinsic visual representation. However, most scales can 80 | generate and format ticks for reference marks to aid in the construction of 81 | axes. 82 | 83 | #continuous 84 | 85 | Given a value from the domain, returns the corresponding value from the 86 | range. If the given value is outside the domain, and clamping is not 87 | enabled, the mapping may be extrapolated such that the returned value is 88 | outside the range. For example, to apply a position encoding: 89 | 90 | ```python 91 | x = scale.linear(domain=[10, 130], range=[0, 960]) 92 | x(20) # 80 93 | x(50) # 320 94 | ``` 95 | 96 | Or to apply a color encoding: 97 | 98 | ```python 99 | color = scale.linear(domain=[10, 100], range=["brown", "steelblue"]) 100 | color(20) # "#9a3439" 101 | color(50) # "#7b5167" 102 | ``` 103 | -------------------------------------------------------------------------------- /README-interpolate.md: -------------------------------------------------------------------------------- 1 | # pyd3.interpolate 2 | 3 | pyd3.interpolate is a python translation of of the 4 | [D3-interpolate](https://github.com/d3/d3-interpolate) module. 5 | 6 | 7 | This module provides a variety of interpolation methods for blending between 8 | two values. Values may be numbers, colors, strings, arrays, or even 9 | deeply-nested objects. For example: 10 | 11 | ```python 12 | i = pyd3.interpolate.number(10, 20) 13 | i(0.0) # 10 14 | i(0.2) # 12 15 | i(0.5) # 15 16 | i(1.0) # 20 17 | ``` 18 | 19 | The returned function `i` is called an *interpolator*. Given a starting value 20 | *a* and an ending value *b*, it takes a parameter *t* in the domain [0, 1] and 21 | returns the corresponding interpolated value between *a* and *b*. An 22 | interpolator typically returns a value equivalent to *a* at *t* = 0 and a value 23 | equivalent to *b* at *t* = 1. 24 | 25 | You can interpolate more than just numbers. To find the perceptual midpoint 26 | between steelblue and brown: 27 | 28 | ```python 29 | pyd3.interpolate.rgb("steelblue", "brown")(0.5); # "#8e5c6d" 30 | ``` 31 | 32 | Here’s a more elaborate example demonstrating type inference used by 33 | [value](#value): 34 | 35 | ```python 36 | i = pyd3.interpolate.value({colors: ["red", "blue"]}, {colors: ["white", "black"]}) 37 | i(0.0) # {colors: ["#ff0000", "#0000ff"]} 38 | i(0.5) # {colors: ["#ff8080", "#000080"]} 39 | i(1.0) # {colors: ["#ffffff", "#000000"]} 40 | ``` 41 | 42 | Note that the generic value interpolator detects not only nested objects and 43 | arrays, but also color strings and numbers embedded in strings! 44 | 45 | ## API Reference 46 | 47 | # pyd3.interpolate.value(a, b) 48 | 49 | Returns an interpolator between the two arbitrary values *a* and *b*. The 50 | interpolator implementation is based on the type of the end value *b*, using 51 | the following algorithm: 52 | 53 | 1. If *b* is a [color](#color), [rgb](#rgb) is used. 54 | 2. If *b* is a string, [string](#string) is used. 55 | 3. If *b* is an array, [array](#array) is used. 56 | 4. If *b* is a dict, [dict](#dict) is used. 57 | 5. Otherwise, [number](#number) is used. 58 | 59 | 60 | Based on the chosen interpolator, *a* is coerced to a suitable corresponding 61 | type. The behavior of this method may be augmented to support additional types 62 | by pushing custom interpolator factories onto the [values](#values) array. 63 | 64 | # pyd3.interpolate.number(a, b) 65 | 66 | Returns an interpolator between the two numbers *a* and *b*. The returned 67 | interpolator is equivalent to: 68 | 69 | ```python 70 | def interpolate(t): 71 | return a * (1 - t) + b * t 72 | ``` 73 | 74 | Caution: avoid interpolating to or from the number zero when the interpolator 75 | is used to generate a string. When very small values are stringified, they may 76 | be converted to scientific notation, which is an invalid attribute or style 77 | property value. For example, the number `0.0000001` is converted to the string 78 | `"1e-7"`. This is particularly noticeable with interpolating opacity. To avoid 79 | scientific notation, start or end the transition at 1e-6: the smallest value 80 | that is not stringified in scientific notation. 81 | 82 | # pyd3.interpolate.round(a, b) 83 | 84 | Returns an interpolator between the two numbers *a* and *b*; the interpolator 85 | is similar to [number](#number), except it will round the resulting value to 86 | the nearest integer. 87 | 88 | # pyd3.interpolate.string(a, b) 89 | 90 | Returns an interpolator between the two strings *a* and *b*. The string 91 | interpolator finds numbers embedded in *a* and *b*, where each number is of the 92 | form understood by Python. A few examples of numbers that will be detected 93 | within a string: `-1`, `42`, `3.14159`, and `6.0221413e+23`. 94 | 95 | For each number embedded in *b*, the interpolator will attempt to find a 96 | corresponding number in *a*. If a corresponding number is found, a numeric 97 | interpolator is created using [number](#number). The remaining parts of the 98 | string *b* are used as a template: the static parts of the string *b* remain 99 | constant for the interpolation, with the interpolated numeric values embedded 100 | in the template. 101 | 102 | For example, if *a* is `"300 12px sans-serif"`, and *b* is `"500 36px 103 | Comic-Sans"`, two embedded numbers are found. The remaining static parts of the 104 | string are a space between the two numbers (`" "`), and the suffix (`"px 105 | Comic-Sans"`). The result of the interpolator at *t* = .5 is `"400 24px 106 | Comic-Sans"`. 107 | 108 | # pyd3.interpolate.list(a, b) 109 | 110 | Returns an interpolator between the two lists *a* and *b*. Internally, a list 111 | template is created that is the same length in *b*. For each element in *b*, if 112 | there exists a corresponding element in *a*, a generic interpolator is created 113 | for the two elements using [value](#value). If there is no such element, the 114 | static value from *b* is used in the template. Then, for the given parameter 115 | *t*, the template’s embedded interpolators are evaluated. The updated list 116 | template is then returned. 117 | 118 | For example, if *a* is the list `[0, 1]` and *b* is the list `[1, 10, 100]`, 119 | then the result of the interpolator for *t* = .5 is the list `[.5, 5.5, 100]`. 120 | 121 | 122 | # pyd3.interpolate.dict(a, b) 123 | 124 | Returns an interpolator between the two dicts *a* and *b*. Internally, a dict 125 | template is created that has the same properties as *b*. For each property in 126 | *b*, if there exists a corresponding property in *a*, a generic interpolator is 127 | created for the two elements using [value](#value). If there is no such 128 | property, the static value from *b* is used in the template. Then, for the 129 | given parameter *t*, the template's embedded interpolators are evaluated and 130 | the updated object template is then returned. 131 | 132 | For example, if *a* is the dict `{"x": 0, "y": 1}` and *b* is the dict `{"x": 133 | 1, "y": 10, "z": 100}`, the result of the interpolator for *t* = .5 is the dict 134 | `{"x": .5, "y": 5.5, "z": 100}`. 135 | 136 | dict interpolation is particularly useful for *dataspace interpolation*, where 137 | data is interpolated rather than attribute values. 138 | 139 | # pyd3.interpolate.rgb(a, b) 140 | 141 | rgb 142 | 143 | Returns an RGB color space interpolator between the two colors *a* and *b*. The 144 | colors *a* and *b* need not be in RGB; they will be converted to RGB using 145 | [color.rgb](https://github.com/d3/d3-color#rgb). The return value of the 146 | interpolator is a hexadecimal RGB string. 147 | 148 | -------------------------------------------------------------------------------- /pyd3/color.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) 2016, Nicolas P. Rougier. All rights reserved. 3 | # Distributed under the terms of the new BSD License. 4 | # ----------------------------------------------------------------------------- 5 | import re 6 | import colorsys 7 | 8 | __colors__ = { 9 | "aliceblue": "#f0f8ff", 10 | "antiquewhite": "#faebd7", 11 | "aqua": "#00ffff", 12 | "aquamarine": "#7fffd4", 13 | "azure": "#f0ffff", 14 | "beige": "#f5f5dc", 15 | "bisque": "#ffe4c4", 16 | "black": "#000000", 17 | "blanchedalmond": "#ffebcd", 18 | "blue": "#0000ff", 19 | "blueviolet": "#8a2be2", 20 | "brown": "#a52a2a", 21 | "burlywood": "#deb887", 22 | "cadetblue": "#5f9ea0", 23 | "chartreuse": "#7fff00", 24 | "chocolate": "#d2691e", 25 | "coral": "#ff7f50", 26 | "cornflowerblue": "#6495ed", 27 | "cornsilk": "#fff8dc", 28 | "crimson": "#dc143c", 29 | "cyan": "#00ffff", 30 | "darkblue": "#00008b", 31 | "darkcyan": "#008b8b", 32 | "darkgoldenrod": "#b8860b", 33 | "darkgray": "#a9a9a9", 34 | "darkgrey": "#a9a9a9", 35 | "darkgreen": "#006400", 36 | "darkkhaki": "#bdb76b", 37 | "darkmagenta": "#8b008b", 38 | "darkolivegreen": "#556b2f", 39 | "darkorange": "#ff8c00", 40 | "darkorchid": "#9932cc", 41 | "darkred": "#8b0000", 42 | "darksalmon": "#e9967a", 43 | "darkseagreen": "#8fbc8f", 44 | "darkslateblue": "#483d8b", 45 | "darkslategray": "#2f4f4f", 46 | "darkslategrey": "#2f4f4f", 47 | "darkturquoise": "#00ced1", 48 | "darkviolet": "#9400d3", 49 | "deeppink": "#ff1493", 50 | "deepskyblue": "#00bfff", 51 | "dimgray": "#696969", 52 | "dimgrey": "#696969", 53 | "dodgerblue": "#1e90ff", 54 | "firebrick": "#b22222", 55 | "floralwhite": "#fffaf0", 56 | "forestgreen": "#228b22", 57 | "fuchsia": "#ff00ff", 58 | "gainsboro": "#dcdcdc", 59 | "ghostwhite": "#f8f8ff", 60 | "gold": "#ffd700", 61 | "goldenrod": "#daa520", 62 | "gray": "#808080", 63 | "grey": "#808080", 64 | "green": "#008000", 65 | "greenyellow": "#adff2f", 66 | "honeydew": "#f0fff0", 67 | "hotpink": "#ff69b4", 68 | "indianred": "#cd5c5c", 69 | "indigo": "#4b0082", 70 | "ivory": "#fffff0", 71 | "khaki": "#f0e68c", 72 | "lavender": "#e6e6fa", 73 | "lavenderblush": "#fff0f5", 74 | "lawngreen": "#7cfc00", 75 | "lemonchiffon": "#fffacd", 76 | "lightblue": "#add8e6", 77 | "lightcoral": "#f08080", 78 | "lightcyan": "#e0ffff", 79 | "lightgoldenrodyellow": "#fafad2", 80 | "lightgray": "#d3d3d3", 81 | "lightgrey": "#d3d3d3", 82 | "lightgreen": "#90ee90", 83 | "lightpink": "#ffb6c1", 84 | "lightsalmon": "#ffa07a", 85 | "lightseagreen": "#20b2aa", 86 | "lightskyblue": "#87cefa", 87 | "lightslategray": "#778899", 88 | "lightslategrey": "#778899", 89 | "lightsteelblue": "#b0c4de", 90 | "lightyellow": "#ffffe0", 91 | "lime": "#00ff00", 92 | "limegreen": "#32cd32", 93 | "linen": "#faf0e6", 94 | "magenta": "#ff00ff", 95 | "maroon": "#800000", 96 | "mediumaquamarine": "#66cdaa", 97 | "mediumblue": "#0000cd", 98 | "mediumorchid": "#ba55d3", 99 | "mediumpurple": "#9370d8", 100 | "mediumseagreen": "#3cb371", 101 | "mediumslateblue": "#7b68ee", 102 | "mediumspringgreen": "#00fa9a", 103 | "mediumturquoise": "#48d1cc", 104 | "mediumvioletred": "#c71585", 105 | "midnightblue": "#191970", 106 | "mintcream": "#f5fffa", 107 | "mistyrose": "#ffe4e1", 108 | "moccasin": "#ffe4b5", 109 | "navajowhite": "#ffdead", 110 | "navy": "#000080", 111 | "oldlace": "#fdf5e6", 112 | "olive": "#808000", 113 | "olivedrab": "#6b8e23", 114 | "orange": "#ffa500", 115 | "orangered": "#ff4500", 116 | "orchid": "#da70d6", 117 | "palegoldenrod": "#eee8aa", 118 | "palegreen": "#98fb98", 119 | "paleturquoise": "#afeeee", 120 | "palevioletred": "#d87093", 121 | "papayawhip": "#ffefd5", 122 | "peachpuff": "#ffdab9", 123 | "peru": "#cd853f", 124 | "pink": "#ffc0cb", 125 | "plum": "#dda0dd", 126 | "powderblue": "#b0e0e6", 127 | "purple": "#800080", 128 | "red": "#ff0000", 129 | "rosybrown": "#bc8f8f", 130 | "royalblue": "#4169e1", 131 | "saddlebrown": "#8b4513", 132 | "salmon": "#fa8072", 133 | "sandybrown": "#f4a460", 134 | "seagreen": "#2e8b57", 135 | "seashell": "#fff5ee", 136 | "sienna": "#a0522d", 137 | "silver": "#c0c0c0", 138 | "skyblue": "#87ceeb", 139 | "slateblue": "#6a5acd", 140 | "slategray": "#708090", 141 | "slategrey": "#708090", 142 | "snow": "#fffafa", 143 | "springgreen": "#00ff7f", 144 | "steelblue": "#4682b4", 145 | "tan": "#d2b48c", 146 | "teal": "#008080", 147 | "thistle": "#d8bfd8", 148 | "tomato": "#ff6347", 149 | "turquoise": "#40e0d0", 150 | "violet": "#ee82ee", 151 | "wheat": "#f5deb3", 152 | "white": "#ffffff", 153 | "whitesmoke": "#f5f5f5", 154 | "yellow": "#ffff00", 155 | "yellowgreen": "#9acd32" 156 | } 157 | 158 | def web2hex(color): 159 | if color in __colors__.keys(): 160 | return __colors__[color] 161 | raise ValueError('Unknwon color string "%s"' % color) 162 | 163 | def hex2rgb(color): 164 | 165 | hex_color= re.compile( 166 | "\A#[a-fA-F0-9]{6}\Z|\A#[a-fA-F0-9]{3}\Z|\A#[a-fA-F0-9]{1}\Z") 167 | if hex_color.match(color) is None: 168 | raise ValueError('Invalid hex color string "%s"' % color) 169 | 170 | color = color.lstrip('#') 171 | n = len(color) 172 | # 1 single byte component (#x) 173 | if n == 1: 174 | v = int(color,16)*17/255.0 175 | return v,v,v 176 | # 3 single byte components (#xyz) 177 | if n == 3: 178 | return tuple( (int(color[i:i+1], 16)*17)/255.0 for i in range(0,3)) 179 | # 3 double byte components (#xxyyzz) 180 | return tuple(int(color[i:i+n//3], 16)/255.0 for i in range(0,n,n//3)) 181 | 182 | def rgb2hex(r,g,b): 183 | return '#' + ''.join(["%02x" % int(round(v*255)) for v in (r,g,b)]) 184 | 185 | def rgb2hsl(r,g,b): 186 | h,l,s = colorsys.rgb_to_hls(r,g,b) 187 | return h,s,l 188 | 189 | def hsl2rgb(h,s,l): 190 | return colorsys.hls_to_rgb(h,l,s) 191 | 192 | def rgb2hsv(r,g,b): 193 | return colorsys.rgb_to_hsv(r,g,b) 194 | 195 | def hsv2rgb(h,s,v): 196 | return colorsys.hsv_to_rgb(r,g,b) 197 | 198 | def rgb2hsl(r,g,b): 199 | h,l,s = colorsys.rgb_to_hls(r,g,b) 200 | return h,s,l 201 | 202 | hsl2hex = lambda x: rgb2hex(hsl2rgb(x)) 203 | hex2hsl = lambda x: rgb2hsl(hex2rgb(x)) 204 | rgb2web = lambda x: hex2web(rgb2hex(x)) 205 | web2rgb = lambda x: hex2rgb(web2hex(x)) 206 | web2hsl = lambda x: rgb2hsl(web2rgb(x)) 207 | hsl2web = lambda x: rgb2web(hsl2rgb(x)) 208 | 209 | 210 | class Color(object): 211 | """ 212 | color = Color(RGB=(255, 255, 255)) 213 | color = Color(rgb=(1.0, 1.0, 1.0)) 214 | color = Color(hsl=(0.0, 0.0, 1.0)) 215 | color = Color(hsv=(0.0, 0.0, 1.0)) 216 | color = Color("#ffffff") 217 | color = Color("#fff") 218 | color = Color("white") 219 | color = Color(Color("white")) 220 | """ 221 | 222 | def __init__(self, color=None, *args, **kwargs): 223 | if color is not None: 224 | if isinstance(color, str): 225 | if color[0] == '#': 226 | r,g,b = hex2rgb(color) 227 | else: 228 | r,g,b = web2rgb(color) 229 | self.rgb = r,g,b 230 | self.alpha = 1.0 231 | elif isinstance(color, Color): 232 | self.rgb = color.rgb 233 | self.alpha = color.alpha 234 | else: 235 | raise ValueError('Color argument not understood "%s"' % repr(color)) 236 | elif "rgb" in kwargs.keys(): 237 | self.rgb = kwargs["rgb"] 238 | self.alpha= 1.0 239 | elif "rgba" in kwargs.keys(): 240 | r,g,b,a = kwargs["rgb"] 241 | self.rgb = rgb 242 | self.alpha = a 243 | elif "RGB" in kwargs.keys(): 244 | R,G,B = kwargs["RGB"] 245 | self.rgb = R/255.0, G/255.0, B/255.0 246 | self.alpha = 1.0 247 | elif "RGBA" in kwargs.keys(): 248 | R,G,B,A = kwargs["RGB"] 249 | self.rgb = R/255.0, G/255.0, B/255.0 250 | self.alpha = A/255.0 251 | elif "hsl" in kwargs.keys(): 252 | r,g,b = kwargs["rgb"] 253 | self.rgb = hsl2rgb(h,s,l) 254 | self.alpha= 1.0 255 | elif "hsv" in kwargs.keys(): 256 | self.rgb = hsv2rgb(*kwargs["hsv"]) 257 | self.alpha= 1.0 258 | 259 | else: 260 | self.rgb = 1.0, 1.0, 1.0 261 | self.alpha= 1.0 262 | 263 | @property 264 | def hsl(self): 265 | return rgb2hsl(*self.rgb) 266 | 267 | @property 268 | def hsv(self): 269 | return rgb2hsv(*self.rgb) 270 | 271 | def __eq__(self,other): 272 | if isinstance(other,Color): 273 | return rgb2hex(*self.rgb) == rgb2hex(*other.rgb) 274 | elif isinstance(other,str): 275 | return rgb2hex(*self.rgb) == other 276 | elif isinstance(other,tuple): 277 | return self.rgb == other 278 | else: 279 | return False 280 | 281 | def __repr__(self): 282 | return rgb2hex(*self.rgb) 283 | 284 | __all__ = [Color] 285 | -------------------------------------------------------------------------------- /pyd3/interpolate.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) 2016, Nicolas P. Rougier. All rights reserved. 3 | # Distributed under the terms of the new BSD License. 4 | # ----------------------------------------------------------------------------- 5 | """ 6 | This is a python translation of the `d3-interpolate 7 | `_ javascript module. 8 | 9 | This module provides a variety of interpolation methods for blending between 10 | two values. Values may be numbers, colors, strings, arrays, or even 11 | deeply-nested objects. For example:: 12 | 13 | i = interpolate.number(10, 20) 14 | i(0.0) # 10 15 | i(0.2) # 12 16 | i(0.5) # 15 17 | i(1.0) # 20 18 | 19 | The returned function `i` is called an *interpolator*. Given a starting value 20 | *a* and an ending value *b*, it takes a parameter *t* in the domain [0, 1] and 21 | returns the corresponding interpolated value between *a* and *b*. An 22 | interpolator typically returns a value equivalent to *a* at *t* = 0 and a value 23 | equivalent to *b* at *t* = 1. 24 | 25 | You can interpolate more than just numbers. To find the perceptual midpoint 26 | between steelblue and brown:: 27 | 28 | interpolate.rgb("white", "black")(0.5) # "#808080 29 | 30 | Here’s a more elaborate example demonstrating type inference used by value:: 31 | 32 | i = interpolate.value({"colors": ["red", "blue"]}, 33 | {"colors": ["white", "black"]}); 34 | i(0.0); # {'colors': ["#ff0000", "#0000ff"]} 35 | i(0.5); # {'colors': ["#ff8080", "#000080"]} 36 | i(1.0); # {'colors': ["#ffffff", "#000000"]} 37 | 38 | Note that the generic value interpolator detects not only nested objects and 39 | lists, but also color strings and numbers embedded in strings! 40 | """ 41 | import re 42 | from pyd3.color import Color 43 | 44 | # We saved python core objects here because we'll override some of them (see 45 | # end of file) 46 | py_list = list 47 | py_dict = dict 48 | py_tuple = tuple 49 | py_round = round 50 | 51 | 52 | def interpolate_value(a,b): 53 | """ 54 | Returns an interpolator between the two arbitrary values *a* and *b*. The 55 | interpolator implementation is based on the type of the end value *b*, 56 | using the following algorithm: 57 | 58 | 1. If *b* is a color, [rgb](#rgb) is used. 59 | 2. If *b* is a string, [string](#string) is used. 60 | 3. If *b* is a list, [list](#list) is used. 61 | 4. If *b* is a dict, [dict](#dict) is used. 62 | 5. Otherwise, [number](#number) is used. 63 | 64 | Based on the chosen interpolator, *a* is coerced to a suitable 65 | corresponding type. The behavior of this method may be augmented to support 66 | additional types by pushing custom interpolator factories onto the values 67 | array. 68 | """ 69 | 70 | if isinstance(b, str): 71 | try: 72 | a, b = Color(a), Color(b) 73 | except ValueError: 74 | try: 75 | a, b = float(a), float(b) 76 | except ValueError: 77 | return interpolate_string(a,b) 78 | else: 79 | return interpolate_number(float(a),float(b)) 80 | else: 81 | return interpolate_rgb(Color(a),Color(b)) 82 | elif isinstance(b, (py_list,py_tuple)): 83 | return interpolate_list(a,b) 84 | elif isinstance(b, py_dict): 85 | return interpolate_dict(a,b) 86 | else: 87 | return interpolate_number(a,b) 88 | 89 | 90 | def interpolate_number(a, b): 91 | """ 92 | Returns an interpolator between the two numbers *a* and *b*. The returned 93 | interpolator is equivalent to:: 94 | 95 | def interpolate(t): 96 | return a * (1 - t) + b * t 97 | 98 | Caution: avoid interpolating to or from the number zero when the 99 | interpolator is used to generate a string. When very small values are 100 | stringified, they may be converted to scientific notation, which is an 101 | invalid attribute or style property value. For example, the number 102 | `0.0000001` is converted to the string `"1e-7"`. This is particularly 103 | noticeable with interpolating opacity. To avoid scientific notation, start 104 | or end the transition at 1e-6: the smallest value that is not stringified 105 | in scientific notation. 106 | """ 107 | 108 | b = b - a 109 | def _interpolate(t): 110 | return a + t * b 111 | return _interpolate 112 | 113 | 114 | def interpolate_round(a, b): 115 | """ 116 | Returns an interpolator between the two numbers *a* and *b*; the 117 | interpolator is similar to number, except it will round the resulting value 118 | to the nearest integer. 119 | """ 120 | 121 | b = b - a 122 | def _interpolate(t): 123 | return int(py_round(a + b * t)) 124 | return _interpolate 125 | 126 | 127 | def interpolate_rgb(a, b): 128 | """ 129 | Returns an RGB color space interpolator between the two colors a and b. The 130 | colors a and b need not be in RGB; they will be converted to RGB using 131 | color.rgb. The return value of the interpolator is a hexadecimal RGB 132 | string. 133 | """ 134 | 135 | a, b = Color(a), Color(b) 136 | ar, ag, ab = a.rgb 137 | br, bg, bb = b.rgb 138 | br, bg, bb = br-ar, bg-ag, bb-ab 139 | def _interpolate(t): 140 | return Color(rgb=(ar + t*br, ag + t*bg, ab + t*bb)) 141 | return _interpolate 142 | 143 | 144 | def interpolate_string(a, b): 145 | """ 146 | Returns an interpolator between the two strings *a* and *b*. The string 147 | interpolator finds numbers embedded in *a* and *b*, where each number is of 148 | the form understood by JavaScript. A few examples of numbers that will be 149 | detected within a string: `-1`, `42`, `3.14159`, and `6.0221413e+23`. 150 | 151 | For each number embedded in *b*, the interpolator will attempt to find a 152 | corresponding number in *a*. If a corresponding number is found, a numeric 153 | interpolator is created using [number](#number). The remaining parts of the 154 | string *b* are used as a template: the static parts of the string *b* 155 | remain constant for the interpolation, with the interpolated numeric values 156 | embedded in the template. 157 | 158 | For example, if *a* is `"300 12px sans-serif"`, and *b* is `"500 36px 159 | Comic-Sans"`, two embedded numbers are found. The remaining static parts of 160 | the string are a space between the two numbers (`" "`), and the suffix 161 | (`"px Comic-Sans"`). The result of the interpolator at *t* = .5 is `"400 162 | 24px Comic-Sans"`. 163 | """ 164 | 165 | # Regular expression matching any number in decimal notation 166 | number = "[+-]?((\d+\.\d*)|(\d*\.\d+)|(([1-9][0-9]*)|0+))(([eE][-+]?\d+)?)" 167 | 168 | # Get all values from string a 169 | a_values = [] 170 | for match in re.finditer(number, a): 171 | a_values.append(eval(match.group(0))) 172 | 173 | # Get as many values from string b and replace them with "%g" 174 | b_values = [] 175 | def replace(match): 176 | if len(b_values) < len(a_values): 177 | b_values.append(eval(match.group(0))) 178 | return "%g" 179 | else: 180 | return match.group(0) 181 | text = re.sub(number, replace, b) 182 | 183 | # Build individual interpolators 184 | interpolators = [interpolate_number(a_values[i],b_values[i]) 185 | for i in range(len(b_values))] 186 | def _interpolate(t): 187 | return text % tuple([interpolator(t) for interpolator in interpolators]) 188 | return _interpolate 189 | 190 | 191 | def interpolate_list(a, b): 192 | """ 193 | Returns an interpolator between the two lists *a* and *b*. Internally, a 194 | list template is created that is the same length in *b*. For each element 195 | in *b*, if there exists a corresponding element in *a*, a generic 196 | interpolator is created for the two elements using [value](#value). If 197 | there is no such element, the static value from *b* is used in the 198 | template. Then, for the given parameter *t*, the template’s embedded 199 | interpolators are evaluated. The updated list template is then returned. 200 | 201 | For example, if *a* is the list `[0, 1]` and *b* is the list `[1, 10, 202 | 100]`, then the result of the interpolator for *t* = .5 is the list `[.5, 203 | 5.5, 100]`. 204 | """ 205 | 206 | _interpolators = [] 207 | for i in range(len(b)): 208 | if i < len(a): 209 | _interpolators.append(interpolate_value(a[i],b[i])) 210 | else: 211 | _interpolators.append(lambda t: b[i]) 212 | 213 | def _interpolate(t): 214 | return [f(t) for f in _interpolators] 215 | return _interpolate 216 | 217 | 218 | def interpolate_dict(a, b): 219 | """ 220 | Returns an interpolator between the two dicts *a* and *b*. Internally, a 221 | dict template is created that has the same properties as *b*. For each 222 | property in *b*, if there exists a corresponding property in *a*, a generic 223 | interpolator is created for the two elements using [value](#value). If 224 | there is no such property, the static value from *b* is used in the 225 | template. Then, for the given parameter *t*, the template's embedded 226 | interpolators are evaluated and the updated object template is then 227 | returned. 228 | 229 | For example, if *a* is the dict `{"x": 0, "y": 1}` and *b* is the dict 230 | `{"x": 1, "y": 10, "z": 100}`, the result of the interpolator for *t* = .5 231 | is the dict `{"x": .5, "y": 5.5, "z": 100}`. 232 | 233 | dict interpolation is particularly useful for *dataspace interpolation*, 234 | where data is interpolated rather than attribute values. 235 | """ 236 | 237 | _interpolators = {} 238 | 239 | # Values present in a but not in b (not interpolated) 240 | for key in a.keys(): 241 | if key not in b.keys(): 242 | _interpolators[key] = lambda t, k=key: a[k] 243 | 244 | # Values present in a but not in b (not interpolated) 245 | for key in b.keys(): 246 | if key not in a.keys(): 247 | _interpolators[key] = lambda t, k=key: b[k] 248 | else: 249 | _interpolators[key] = interpolate_value(a[key],b[key]) 250 | 251 | def _interpolate(t): 252 | return {key:f(t) for (key,f) in _interpolators.items()} 253 | return _interpolate 254 | 255 | 256 | # Shortcuts to allow convenient notation such as interpolate.string(a,b) 257 | rgb = interpolate_rgb 258 | list = interpolate_list 259 | dict = interpolate_dict 260 | value = interpolate_value 261 | number = interpolate_number 262 | round = interpolate_round 263 | string = interpolate_string 264 | 265 | # We don't want to allow to import everything since it would overrides list, 266 | # dict and round 267 | __all__ = [ interpolate_rgb, interpolate_list, interpolate_dict, 268 | interpolate_value, interpolate_number, interpolate_round, 269 | interpolate_string ] 270 | 271 | -------------------------------------------------------------------------------- /pyd3/scale.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) 2016, Nicolas P. Rougier. All rights reserved. 3 | # Distributed under the terms of the new BSD License. 4 | # ----------------------------------------------------------------------------- 5 | """ 6 | This is a python translation of the `d3-scale 7 | `_ javascript module. 8 | 9 | Scales are a convenient abstraction for a fundamental task in 10 | visualization: mapping a dimension of abstract data to a visual 11 | representation. Although most often used for position-encoding quantitative 12 | data, such as mapping a measurement in meters to a position in pixels for dots 13 | in a scatterplot, scales can represent virtually any visual encoding, such as 14 | diverging colors, stroke widths, or symbol size. Scales can also be used with 15 | virtually any type of data, such as named categorical data or discrete data 16 | that requires sensible breaks. 17 | 18 | For continuous quantitative data, you typically want a linear scale. (For time 19 | series data, a time scale.) If the distribution calls for it, consider 20 | transforming data using a power or log scale. A quantize scale may aid 21 | differentiation by rounding continuous data to a fixed set of discrete values; 22 | similarly, a quantile scale computes quantiles from a sample population, and a 23 | threshold scale allows you to specify arbitrary breaks in continuous 24 | data. Several built-in sequential color scales are also provided. (If you don’t 25 | like these palettes, try ColorBrewer.) 26 | 27 | For discrete ordinal (ordered) or categorical (unordered) data, an ordinal 28 | scale specifies an explicit mapping from a set of data values to a 29 | corresponding set of visual attributes (such as colors). The related band and 30 | point scales are useful for position-encoding ordinal data, such as bars in a 31 | bar chart or dots in an categorical scatterplot. Several built-in categorical 32 | color scales are also provided. 33 | 34 | Scales have no intrinsic visual representation. However, most scales can 35 | generate and format ticks for reference marks to aid in the construction of 36 | axes. 37 | """ 38 | import math 39 | import numpy as np 40 | from pyd3 import interpolate 41 | 42 | py_range = range 43 | 44 | 45 | def interpolate_number(x, xp, yp, clamp=True): 46 | """ 47 | Specialized interpolation for array of scalars 48 | """ 49 | x = np.asarray(x) 50 | 51 | # Specific case for empty domain 52 | if xp[0] == xp[-1] or len(xp)<2: 53 | if len(x.shape) == 0: 54 | return yp[0] 55 | else: 56 | return [yp[0],]*len(x) 57 | 58 | # Extrapolate 59 | if not clamp: 60 | # Single value 61 | if len(x.shape) == 0: 62 | if x < xp[0]: 63 | return yp[ 0] + (x-xp[ 0])*(yp[ 0]-yp[ 1]) / (xp[ 0]-xp[ 1]) 64 | elif x > xp[-1]: 65 | return yp[-1] + (x-xp[-1])*(yp[-1]-yp[-2]) / (xp[-1]-xp[-2]) 66 | else: 67 | return np.interp(x, xp, yp) 68 | # Values list 69 | else: 70 | # Specific case for empty domain 71 | if xp[0] == xp[-1] or len(xp)<2: 72 | return [yp[0],]*len(x) 73 | y = np.interp(x, xp, yp) 74 | y[x < xp[ 0]] = yp[ 0] + (x[x xp[-1]] = yp[-1] + (x[x>xp[-1]]-xp[-1]) * (yp[-1]-yp[-2]) / (xp[-1]-xp[-2]) 76 | return y 77 | 78 | # Interpolate 79 | return np.interp(x, xp, yp) 80 | 81 | 82 | 83 | def interpolate_time(x, xp, yp, clamp=True): 84 | """ 85 | Specialized interpolation for array of datetime values 86 | """ 87 | x = np.asarray(x) 88 | 89 | # Specific case for empty domain 90 | if xp[0] == xp[-1] or len(xp)<2: 91 | if len(x.shape) == 0: 92 | return yp[0] 93 | else: 94 | return [yp[0],]*len(x) 95 | 96 | delta = np.cumsum(yp[1:] - yp[:-1]) 97 | delta = np.insert(delta, 0, 0) 98 | dtype = delta.dtype 99 | 100 | interpolated = np.array(interpolate_number(x, xp, delta.astype(int), clamp)) 101 | return yp[0] + interpolated.astype(delta.dtype) 102 | 103 | 104 | def interpolate_value(x, xp, yp, clamp=True): 105 | """ 106 | Generic interpolation 107 | """ 108 | x = np.asarray(x) 109 | n = len(xp) 110 | 111 | # Specific case for empty domain 112 | if xp[0] == xp[-1] or len(xp)<2: 113 | if len(x.shape) == 0: 114 | return yp[0] 115 | else: 116 | return [yp[0],]*len(x) 117 | 118 | # Build (n-1) interpolators for each interval in yp 119 | interpolators = [] 120 | for i in range(len(yp)-1): 121 | interpolators.append(interpolate.value(yp[i],yp[i+1])) 122 | 123 | # Find corresponding interpolator foreach x value 124 | xi = np.searchsorted(xp,x) 125 | 126 | # Single value 127 | if len(x.shape) == 0: 128 | # Find indices of x within xp 129 | if xi == 0: 130 | xi = 1 # index 0 (= prepend) is invalid 131 | elif xi == n: 132 | xi = n-1 # index n (= append) is invalid 133 | xi -= 1 134 | 135 | # Normalized x values in each interval 136 | v = np.arange(len(xp)) 137 | nx = interpolate_number(x, xp, np.arange(len(xp)), clamp=clamp) - xi 138 | return interpolators[xi](nx) 139 | 140 | # Values list 141 | else: 142 | # Find indices of x within xp 143 | xi[xi==0] = 1 # index 0 (= prepend) is invalid 144 | xi[xi==n] = n-1 # index n (= append) is invalid 145 | xi -= 1 146 | 147 | # Normalized x values in each interval 148 | v = np.arange(len(xp)) 149 | nx = interpolate_number(x, xp, np.arange(len(xp)), clamp=clamp) - xi 150 | 151 | # Get output value for each x 152 | return [interpolators[i](x) for i,x in zip(xi,nx)] 153 | 154 | def tick_step(start, stop, count): 155 | e10 = math.sqrt(50) 156 | e5 = math.sqrt(10) 157 | e2 = math.sqrt(2) 158 | 159 | step0 = abs(stop - start) / max(0, count) 160 | step1 = math.pow(10, math.floor(math.log(step0) / math.log(10))) 161 | error = step0 / step1 162 | if error >= e10: step1 *= 10 163 | elif error >= e5: step1 *= 5 164 | elif error >= e2: step1 *= 2 165 | if stop < start: return -step1 166 | else: return +step1 167 | 168 | def ticks(start, stop, count): 169 | """ 170 | Returns approximately count representative values from the scale’s 171 | domain. If count is not specified, it defaults to 10. The returned tick 172 | values are uniformly spaced, have human-readable values (such as multiples 173 | of powers of 10), and are guaranteed to be within the extent of the 174 | domain. Ticks are often used to display reference lines, or tick marks, in 175 | conjunction with the visualized data. The specified count is only a hint; 176 | the scale may return more or fewer values depending on the domain. 177 | """ 178 | 179 | step = tick_step(start, stop, count) 180 | start = math.ceil(start/step) 181 | stop = math.floor(stop/step) 182 | n = round(abs(stop-start))+1 183 | t = (start + np.arange(n))*step 184 | return t.tolist() 185 | 186 | 187 | class ContinuousScale(object): 188 | """ 189 | Continuous scales map a continuous, quantitative input domain to a 190 | continuous output range. If the range is also numeric, the mapping may be 191 | inverted. A continuous scale is not constructed directly; instead, try a 192 | linear, power, log, identity, time or sequential color scale. 193 | 194 | # continuous(value) 195 | 196 | Given a value from the domain, returns the corresponding value from the 197 | range. If the given value is outside the domain, and clamping is not 198 | enabled, the mapping may be extrapolated such that the returned value is 199 | outside the range. For example, to apply a position encoding:: 200 | 201 | x = scale.linear(domain=[10, 130], range=[0, 960]) 202 | x(20) # 80 203 | x(50) # 320 204 | 205 | Or to apply a color encoding:: 206 | 207 | color = scale.linear(domain=[10, 100], range=["brown", "steelblue"]) 208 | color(20) # "#9a3439" 209 | color(50) # "#7b5167" 210 | """ 211 | 212 | def __init__(self, domain=[0,1], range=[0,1], clamp=False, interpolate=None): 213 | 214 | self._update_domain_range(domain, range) 215 | self._clamp = bool(clamp) 216 | if isinstance(range[0], (int,float)): 217 | self._interpolate = interpolate_number 218 | elif isinstance(range[0], np.datetime64): 219 | self._interpolate = interpolate_time 220 | else: 221 | self._interpolate = interpolate_value 222 | 223 | @property 224 | def clamp(self): 225 | return self._clamp 226 | 227 | @clamp.setter 228 | def clamp(self, clamp): 229 | self._clamp = bool(clamp) 230 | 231 | @property 232 | def domain(self): 233 | return self._domain 234 | 235 | @domain.setter 236 | def domain(self, domain): 237 | self._update_domain_range(domain, self._range) 238 | 239 | @property 240 | def range(self): 241 | """ 242 | Range must contain two or more elements. Unlike the domain, elements in the 243 | given array need not be numbers; any value that is supported by the 244 | underlying interpolator will work, though note that numeric ranges are 245 | required for invert. 246 | """ 247 | 248 | return self._range 249 | 250 | @range.setter 251 | def range(self, range): 252 | self._update_domain_range(self._domain, range) 253 | 254 | 255 | def _update_domain_range(self, domain, range): 256 | 257 | # Store domain and range 258 | self._domain = domain 259 | self._range = range 260 | 261 | # Ensure len(domain) == len(range) 262 | n = min(len(domain), len(range)) 263 | domain = domain[:n] 264 | range = range[:n] 265 | 266 | 267 | # Coerce domain values if necessary 268 | # (domain may have been given as ["1", "2"]) 269 | if not isinstance(domain, np.ndarray): 270 | try: 271 | domain = [float(v) for v in domain] 272 | except: 273 | pass 274 | 275 | # Forward domain & range 276 | # (domain must be sorted in increasing order) 277 | domain = np.asarray(domain) 278 | sorted = np.argsort(domain) 279 | self._forward_domain = domain[sorted] 280 | if isinstance(range, np.ndarray): 281 | self._forward_range = range[sorted] 282 | else: 283 | self._forward_range = [range[i] for i in sorted] 284 | 285 | # Try to convert range to a numpy array if possible 286 | if len(self._forward_range): 287 | if isinstance(self._forward_range[0], (int,float, np.datetime64)): 288 | self._forward_range = np.asarray(self._forward_range) 289 | 290 | # Inverse domain & range 291 | # (range must be sorted in increasing order) 292 | if not isinstance(range, np.ndarray): 293 | try: 294 | range = [float(v) for v in range] 295 | except: 296 | self._inverse_domain = None 297 | self._inverse_range = None 298 | return 299 | range = np.asarray(range) 300 | sorted = np.argsort(range) 301 | self._inverse_range = range[sorted] 302 | self._inverse_domain = domain[sorted] 303 | 304 | 305 | def ticks(self, count=10): 306 | 307 | if not isinstance(count, int) or count < 1: 308 | return [] 309 | domain = self._domain 310 | return ticks(domain[0], domain[-1], count) 311 | 312 | 313 | 314 | class LinearScale(ContinuousScale): 315 | """ 316 | Constructs a new continuous scale with the unit domain [0, 1], the unit 317 | range [0, 1], a value interpolator and clamping disabled. Linear scales are 318 | a good default choice for continuous quantitative data because they 319 | preserve proportional differences. Each range value y can be expressed as a 320 | function of the domain value x: y = mx + b. 321 | """ 322 | 323 | def __init__(self, domain=[0,1], range=[0,1], clamp=False): 324 | ContinuousScale.__init__(self, domain, range, clamp) 325 | 326 | def __call__(self, values): 327 | return self._interpolate(values, self._forward_domain, self._forward_range, self._clamp) 328 | 329 | def invert(self, values): 330 | if self._inverse_range is not None: 331 | return self._interpolate(values, self._inverse_range, self._inverse_domain, self._clamp) 332 | else: 333 | return None 334 | 335 | def nice(self, count = 10): 336 | """ 337 | Extends the domain so that it starts and ends on nice round values. This 338 | method typically modifies the scale’s domain, and may only extend the 339 | bounds to the nearest round value. An optional tick count argument 340 | allows greater control over the step size used to extend the bounds, 341 | guaranteeing that the returned ticks will exactly cover the 342 | domain. Nicing is useful if the domain is computed from data, say using 343 | extent, and may be irregular. For example, for a domain of [0.201479…, 344 | 0.996679…], a nice domain might be [0.2, 1.0]. If the domain has more 345 | than two values, nicing the domain only affects the first and last 346 | value. 347 | """ 348 | 349 | scale = LinearScale(domain=self._domain, range=self._range, clamp=self._clamp) 350 | d = self._domain 351 | n = count 352 | start, stop = d[0], d[-1] 353 | 354 | # Degenerate case 355 | if start == stop: return scale 356 | 357 | step = tick_step(start, stop, n) 358 | if step: 359 | step = tick_step(math.floor(start/step)*step, math.ceil(stop/step)*step, n) 360 | d[0] = math.floor(start / step) * step 361 | d[-1] = math.ceil(stop / step) * step 362 | scale._update_domain_range(d, self._range) 363 | return scale 364 | 365 | 366 | linear = LinearScale 367 | 368 | 369 | 370 | # class QuantizeScale(object): 371 | # def __init__(self, domain=[0,1], range=[0,1], clamp=True): 372 | # self._domain = domain 373 | # self._range = np.linspace(0,len(range)-1,num=len(domain)) 374 | # self._values = list(range) 375 | # self._clamp = clamp 376 | # self._interpolate = interpolate 377 | 378 | # def __call__(self, values): 379 | # indices = np.round(self._interpolate(values, self._domain, self._range)) 380 | # return self._values[indices.astype(int)] 381 | 382 | # def invert(self, values): 383 | # values = np.asarray(values) 384 | # if len(values.shape) == 0: 385 | # index = self._values.index(values) 386 | # return self._interpolate(index, self._range, self._domain) 387 | # else: 388 | # indices = [self._values.index(value) for value in values] 389 | # return self._interpolate(indices, self._range, self._domain) 390 | 391 | 392 | # x = QuantizeScale(domain=[10,100], range=[1,2,4]) 393 | # print(x(20)) 394 | # print(x(50)) 395 | # print(x(80)) 396 | # print(x.invert(4)) 397 | # print(x.invert([1,2,4])) 398 | 399 | # x = LinearScale(domain=[0,100], range=[0,1]) 400 | # print(x.invert(x(np.linspace(0,100,11)))) 401 | #x = LinearScale(domain=[0,100], range=[Color("black"),Color("white")]) 402 | #print(x(100)) 403 | #print(x.invert(x(np.linspace(0,100,11)))) 404 | 405 | # def interpolate_time(x, xp, yp): 406 | # delta = np.cumsum(T[1:] - T[:-1]) 407 | # delta = np.insert(delta, 0, 0) 408 | # dtype = delta.dtype 409 | # return yp[0] + np.interp(x, xp, delta.astype(int)).astype(delta.dtype) 410 | 411 | 412 | # T = np.array(['2005-01-01T00:00', '2005-01-02T00:00', '2005-01-03T00:00'], dtype='datetime64') 413 | # print(interpolate_time( [0], [0,1,2], T)[0]) 414 | # print(interpolate_time( [1], [0,1,2], T)[0]) 415 | # print(interpolate_time( [2], [0,1,2], T)[0]) 416 | # print(interpolate_time( [-1], [0,1,2], T, False)[0]) 417 | 418 | # print(interpolate( -1, [0,1], [0,1], clamp=True)) 419 | # print(interpolate( +2, [0,1], [0,1], clamp=True)) 420 | # print(interpolate( -1, [0,1], [0,1], clamp=False)) 421 | # print(interpolate( +2, [0,1], [0,1], clamp=False)) 422 | 423 | 424 | -------------------------------------------------------------------------------- /pyd3/tests/test_scale_linear.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) 2016, Nicolas P. Rougier. All rights reserved. 3 | # Distributed under the terms of the new BSD License. 4 | # ----------------------------------------------------------------------------- 5 | import unittest 6 | import numpy as np 7 | from pyd3 import scale 8 | 9 | class test_scale_linear(unittest.TestCase): 10 | 11 | def test_1(self): 12 | """ 13 | linear() has the expected defaults 14 | """ 15 | s = scale.linear() 16 | self.assertEqual(s.domain, [0,1]) 17 | self.assertEqual(s.range, [0,1]) 18 | self.assertEqual(s.clamp, False) 19 | 20 | def test_2(self): 21 | """ 22 | linear(x) maps a domain value x to a range value y" 23 | """ 24 | 25 | s = scale.linear(range=[1,2]) 26 | self.assertEqual(s(0.5), 1.5) 27 | 28 | def test_3(self): 29 | """ 30 | linear(x) ignores extra range values if the domain is smaller than the range 31 | """ 32 | s = scale.linear(domain=[-10,0], range=["red", "white", "green"], clamp=True) 33 | self.assertEqual(s(-5), "#ff8080") 34 | self.assertEqual(s(50), "#ffffff") 35 | 36 | def test_4(self): 37 | """ 38 | linear(x) ignores extra domain values if the range is smaller than the domain 39 | """ 40 | s = scale.linear(domain=[-10,0,100], range=["red", "white"], clamp=True) 41 | self.assertEqual(s(-5), "#ff8080") 42 | self.assertEqual(s(50), "#ffffff") 43 | 44 | def test_5(self): 45 | """ 46 | linear(x) maps an empty domain to the range start 47 | """ 48 | s = scale.linear(domain=[0,0], range=[1,2]) 49 | self.assertEqual(s(0), 1) 50 | 51 | s = scale.linear(domain=[0,0], range=[2,1]) 52 | self.assertEqual(s(1), 2) 53 | 54 | def test_6(self): 55 | """ 56 | linear(x) can map a bilinear domain with two values to the corresponding 57 | range 58 | """ 59 | s = scale.linear(domain=[1, 2]) 60 | self.assertEqual(s.domain, [1,2]) 61 | self.assertEqual(s(0.5), -0.5) 62 | self.assertEqual(s(1.0), 0.0) 63 | self.assertEqual(s(1.5), 0.5) 64 | self.assertEqual(s(2.0), 1.0) 65 | self.assertEqual(s(2.5), 1.5) 66 | self.assertEqual(s.invert(-0.5), 0.5) 67 | self.assertEqual(s.invert( 0.0), 1.0) 68 | self.assertEqual(s.invert( 0.5), 1.5) 69 | self.assertEqual(s.invert( 1.0), 2.0) 70 | self.assertEqual(s.invert( 1.5), 2.5) 71 | 72 | def test_7(self): 73 | """ 74 | linear(x) can map a polylinear domain with more than two values to the 75 | corresponding range 76 | """ 77 | s = scale.linear(domain=[-10, 0, 100], range=["red", "white", "green"]) 78 | self.assertEqual(s.domain, [-10, 0, 100]) 79 | self.assertEqual(s(-5), "#ff8080") 80 | self.assertEqual(s(50), "#80c080") 81 | self.assertEqual(s(75), "#40a040") 82 | 83 | s = scale.linear(domain=[4, 2, 1], range=[1, 2, 4]) 84 | self.assertEqual(s(1.5), 3) 85 | self.assertEqual(s(3), 1.5) 86 | self.assertEqual(s.invert(1.5), 3) 87 | self.assertEqual(s.invert(3), 1.5) 88 | 89 | s = scale.linear(domain=[1, 2, 4],range=[4, 2, 1]) 90 | self.assertEqual(s(1.5), 3) 91 | self.assertEqual(s(3), 1.5) 92 | self.assertEqual(s.invert(1.5), 3) 93 | self.assertEqual(s.invert(3), 1.5) 94 | 95 | def test_8(self): 96 | """ 97 | linear.invert(y) maps a range value y to a domain value x 98 | """ 99 | s = scale.linear(range=[1,2]) 100 | self.assertEqual(s.invert(1.5), .5) 101 | 102 | def test_9(self): 103 | """ 104 | linear.invert(y) maps an empty range to the domain start 105 | """ 106 | s = scale.linear(domain=[1,2], range=[0,0]) 107 | self.assertEqual(s.invert(0), 1) 108 | s = scale.linear(domain=[2,1], range=[0,0]) 109 | self.assertEqual(s.invert(1), 2) 110 | 111 | def test_10(self): 112 | """ 113 | linear.invert(y) coerces range values to numbers 114 | """ 115 | s = scale.linear(range=["0", "2"]) 116 | self.assertEqual(s.invert(1), .5) 117 | 118 | s = scale.linear(range=[np.datetime64("1990-01-01"), np.datetime64("1991-01-01")]) 119 | self.assertEqual(s(.5), np.datetime64("1990-07-02")) 120 | 121 | def test_11(self): 122 | """ 123 | linear.invert(y) returns None if the range is not coercible to number 124 | """ 125 | s = scale.linear(range=["#000", "#fff"]) 126 | self.assertEqual(s.invert("#999"), None) 127 | s = scale.linear(range=[0, "#fff"]) 128 | self.assertEqual(s.invert("#999"), None) 129 | 130 | def test_12(self): 131 | """ 132 | linear.domain(domain) accepts an array of numbers 133 | """ 134 | self.assertEqual( scale.linear(domain=[]).domain, []) 135 | self.assertEqual( scale.linear(domain=[1,0]).domain, [1,0]) 136 | self.assertEqual( scale.linear(domain=[1,2,3]).domain, [1,2,3]) 137 | 138 | def test_16(self): 139 | """ 140 | linear.range(range) does not coerce range to numbers 141 | """ 142 | s = scale.linear(range=["0px", "2px"]) 143 | self.assertEqual(s.range, ["0px", "2px"]) 144 | self.assertEqual(s(.5), "1px") 145 | 146 | def test_17(self): 147 | """ 148 | linear.range(range) can accept range values as colors 149 | """ 150 | s = scale.linear(range=["red", "blue"]) 151 | self.assertEqual(s(.5), "#800080") 152 | 153 | s = scale.linear(range=["#ff0000", "#0000ff"]) 154 | self.assertEqual(s(.5), "#800080") 155 | 156 | s = scale.linear(range=["#f00", "#00f"]) 157 | self.assertEqual(s(.5), "#800080") 158 | 159 | def test_18(self): 160 | """ 161 | linear.range(range) can accept range values as arrays or objects 162 | """ 163 | s = scale.linear(range=[{"color": "red"}, {"color": "blue"}]) 164 | self.assertEqual(s(.5), {"color":"#800080"}) 165 | 166 | s = scale.linear(range=[["red"], ["blue"]]) 167 | self.assertEqual(s(.5), ["#800080"]) 168 | 169 | def test_22(self): 170 | """ 171 | linear.clamp() is false by default 172 | """ 173 | self.assertEqual(scale.linear().clamp, False) 174 | self.assertEqual(scale.linear(range=[10, 20])(2), 30) 175 | self.assertEqual(scale.linear(range=[10, 20])(-1), 0) 176 | self.assertEqual(scale.linear(range=[10, 20]).invert(30), 2) 177 | self.assertEqual(scale.linear(range=[10, 20]).invert(0), -1) 178 | 179 | def test_23(self): 180 | """ 181 | linear.clamp(true) restricts output values to the range 182 | """ 183 | self.assertEqual(scale.linear(range=[10, 20],clamp=True)(2), 20) 184 | self.assertEqual(scale.linear(range=[10, 20],clamp=True)(-1), 10) 185 | 186 | def test_24(self): 187 | """ 188 | linear.clamp(true) restricts input values to the domain 189 | """ 190 | self.assertEqual(scale.linear(range=[10, 20],clamp=True).invert(30), 1) 191 | self.assertEqual(scale.linear(range=[10, 20],clamp=True).invert(0), 0) 192 | 193 | def test_25(self): 194 | """ 195 | linear.clamp(clamp) coerces the specified clamp value to a boolean 196 | """ 197 | self.assertEqual(scale.linear(clamp=True).clamp, True) 198 | self.assertEqual(scale.linear(clamp=1).clamp, True) 199 | self.assertEqual(scale.linear(clamp="").clamp, False) 200 | self.assertEqual(scale.linear(clamp=0).clamp, False) 201 | 202 | def test_26(self): 203 | """ 204 | linear.interpolate(interpolate) takes a custom interpolator factory 205 | """ 206 | pass 207 | 208 | def test_27(self): 209 | """ 210 | linear.nice() is an alias for linear.nice(10) 211 | """ 212 | self.assertEqual(scale.linear(domain=[0,.96]).nice().domain, [0, 1]) 213 | self.assertEqual(scale.linear(domain=[0,96]).nice().domain, [0, 100]) 214 | 215 | def test_28(self): 216 | """ 217 | linear.nice(count) extends the domain to match the desired ticks 218 | """ 219 | self.assertEqual(scale.linear(domain=[ 0,.96]).nice().domain, [0, 1]) 220 | self.assertEqual(scale.linear(domain=[ 0, 96]).nice().domain, [0, 100]) 221 | self.assertEqual(scale.linear(domain=[.96, 0]).nice().domain, [1, 0]) 222 | self.assertEqual(scale.linear(domain=[ 96, 0]).nice().domain, [100, 0]) 223 | self.assertEqual(scale.linear(domain=[0,-.96]).nice().domain, [0, -1]) 224 | self.assertEqual(scale.linear(domain=[0, -96]).nice().domain, [0, -100]) 225 | self.assertEqual(scale.linear(domain=[-.96,0]).nice().domain, [-1, 0]) 226 | self.assertEqual(scale.linear(domain=[-96,0]).nice().domain, [-100, 0]) 227 | self.assertEqual(scale.linear(domain=[-0.1,51.1]).nice(8).domain, [-10, 60]) 228 | 229 | def test_29(self): 230 | """ 231 | linear.nice(count) nices the domain, extending it to round numbers 232 | """ 233 | self.assertEqual(scale.linear(domain=[1.1,10.9]).nice(10).domain, [1, 11]) 234 | self.assertEqual(scale.linear(domain=[10.9,1.1]).nice(10).domain, [11, 1]) 235 | self.assertEqual(scale.linear(domain=[.7,11.001]).nice(10).domain, [0, 12]) 236 | self.assertEqual(scale.linear(domain=[123.1,6.7]).nice(10).domain, [130, 0]) 237 | self.assertEqual(scale.linear(domain=[0,.49]).nice(10).domain, [0, .5]) 238 | 239 | def test_30(self): 240 | """ 241 | linear.nice(count) has no effect on degenerate domains 242 | """ 243 | self.assertEqual(scale.linear(domain=[0,0]).nice(10).domain, [0, 0]) 244 | self.assertEqual(scale.linear(domain=[.5,.5]).nice(10).domain, [.5, .5]) 245 | 246 | def test_31(self): 247 | """ 248 | linear.nice(count) nicing a polylinear domain only affects the extent 249 | """ 250 | self.assertEqual(scale.linear(domain=[1.1, 1, 2, 3, 10.9]).nice(10).domain, 251 | [1, 1, 2, 3, 11]) 252 | self.assertEqual(scale.linear(domain=[123.1, 1, 2, 3, -.9]).nice(10).domain, 253 | [130, 1, 2, 3, -10]) 254 | 255 | def test_32(self): 256 | """ 257 | linear.nice(count) accepts a tick count to control nicing step 258 | """ 259 | self.assertEqual(scale.linear(domain=[12,87]).nice(5).domain,[0, 100]) 260 | self.assertEqual(scale.linear(domain=[12,87]).nice(10).domain,[10, 90]) 261 | self.assertEqual(scale.linear(domain=[12,87]).nice(100).domain,[12, 87]) 262 | 263 | def test_33(self): 264 | """ 265 | linear.ticks(count) returns the expected ticks for an ascending domain 266 | """ 267 | 268 | s = scale.linear() 269 | np.testing.assert_almost_equal( 270 | s.ticks(10), [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) 271 | np.testing.assert_almost_equal( 272 | s.ticks(9), [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) 273 | np.testing.assert_almost_equal( 274 | s.ticks(8), [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) 275 | np.testing.assert_almost_equal( 276 | s.ticks(7), [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]) 277 | np.testing.assert_almost_equal( 278 | s.ticks(6), [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]) 279 | np.testing.assert_almost_equal( 280 | s.ticks(5), [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]) 281 | np.testing.assert_almost_equal( 282 | s.ticks(4), [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]) 283 | np.testing.assert_almost_equal( 284 | s.ticks(3), [0.0, 0.5, 1.0]) 285 | np.testing.assert_almost_equal( 286 | s.ticks(2), [0.0, 0.5, 1.0]) 287 | np.testing.assert_almost_equal( 288 | s.ticks(1), [0.0, 1.0]) 289 | 290 | s = scale.linear(domain=[-100,100]) 291 | np.testing.assert_almost_equal( 292 | s.ticks(10), [-100, -80, -60, -40, -20, 0, 20, 40, 60, 80, 100]) 293 | np.testing.assert_almost_equal( 294 | s.ticks(9), [-100, -80, -60, -40, -20, 0, 20, 40, 60, 80, 100]) 295 | np.testing.assert_almost_equal( 296 | s.ticks(8), [-100, -80, -60, -40, -20, 0, 20, 40, 60, 80, 100]) 297 | np.testing.assert_almost_equal( 298 | s.ticks(7), [-100, -80, -60, -40, -20, 0, 20, 40, 60, 80, 100]) 299 | np.testing.assert_almost_equal( 300 | s.ticks(6), [-100, -50, 0, 50, 100]) 301 | np.testing.assert_almost_equal( 302 | s.ticks(5), [-100, -50, 0, 50, 100]) 303 | np.testing.assert_almost_equal( 304 | s.ticks(4), [-100, -50, 0, 50, 100]) 305 | np.testing.assert_almost_equal( 306 | s.ticks(3), [-100, -50, 0, 50, 100]) 307 | np.testing.assert_almost_equal( 308 | s.ticks(2), [-100, 0, 100]) 309 | np.testing.assert_almost_equal( 310 | s.ticks(1), [ 0 ]) 311 | 312 | def test_34(self): 313 | """ 314 | linear.ticks(count) returns the expected ticks for a descending domain 315 | """ 316 | 317 | s = scale.linear(domain=[1,0]) 318 | np.testing.assert_almost_equal( 319 | s.ticks(10), [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0][::-1]) 320 | np.testing.assert_almost_equal( 321 | s.ticks(9), [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0][::-1]) 322 | np.testing.assert_almost_equal( 323 | s.ticks(8), [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0][::-1]) 324 | np.testing.assert_almost_equal( 325 | s.ticks(7), [0.0, 0.2, 0.4, 0.6, 0.8, 1.0][::-1]) 326 | np.testing.assert_almost_equal( 327 | s.ticks(6), [0.0, 0.2, 0.4, 0.6, 0.8, 1.0][::-1]) 328 | np.testing.assert_almost_equal( 329 | s.ticks(5), [0.0, 0.2, 0.4, 0.6, 0.8, 1.0][::-1]) 330 | np.testing.assert_almost_equal( 331 | s.ticks(4), [0.0, 0.2, 0.4, 0.6, 0.8, 1.0][::-1]) 332 | np.testing.assert_almost_equal( 333 | s.ticks(3), [0.0, 0.5, 1.0][::-1]) 334 | np.testing.assert_almost_equal( 335 | s.ticks(2), [0.0, 0.5, 1.0][::-1]) 336 | np.testing.assert_almost_equal( 337 | s.ticks(1), [0.0, 1.0][::-1]) 338 | 339 | s = scale.linear(domain=[+100,-100]) 340 | np.testing.assert_almost_equal( 341 | s.ticks(10), [-100, -80, -60, -40, -20, 0, 20, 40, 60, 80, 100][::-1]) 342 | np.testing.assert_almost_equal( 343 | s.ticks(9), [-100, -80, -60, -40, -20, 0, 20, 40, 60, 80, 100][::-1]) 344 | np.testing.assert_almost_equal( 345 | s.ticks(8), [-100, -80, -60, -40, -20, 0, 20, 40, 60, 80, 100][::-1]) 346 | np.testing.assert_almost_equal( 347 | s.ticks(7), [-100, -80, -60, -40, -20, 0, 20, 40, 60, 80, 100][::-1]) 348 | np.testing.assert_almost_equal( 349 | s.ticks(6), [-100, -50, 0, 50, 100][::-1]) 350 | np.testing.assert_almost_equal( 351 | s.ticks(5), [-100, -50, 0, 50, 100][::-1]) 352 | np.testing.assert_almost_equal( 353 | s.ticks(4), [-100, -50, 0, 50, 100][::-1]) 354 | np.testing.assert_almost_equal( 355 | s.ticks(3), [-100, -50, 0, 50, 100][::-1]) 356 | np.testing.assert_almost_equal( 357 | s.ticks(2), [-100, 0, 100][::-1]) 358 | np.testing.assert_almost_equal( 359 | s.ticks(1), [ 0 ][::-1]) 360 | 361 | def test_35(self): 362 | """ 363 | linear.ticks(count) returns the expected ticks for a polylinear domain 364 | """ 365 | 366 | s = scale.linear(domain=[0, 0.25, 0.9, 1]) 367 | np.testing.assert_almost_equal( 368 | s.ticks(10), [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) 369 | np.testing.assert_almost_equal( 370 | s.ticks(9), [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) 371 | np.testing.assert_almost_equal( 372 | s.ticks(8), [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) 373 | np.testing.assert_almost_equal( 374 | s.ticks(7), [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]) 375 | np.testing.assert_almost_equal( 376 | s.ticks(6), [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]) 377 | np.testing.assert_almost_equal( 378 | s.ticks(5), [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]) 379 | np.testing.assert_almost_equal( 380 | s.ticks(4), [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]) 381 | np.testing.assert_almost_equal( 382 | s.ticks(3), [0.0, 0.5, 1.0]) 383 | np.testing.assert_almost_equal( 384 | s.ticks(2), [0.0, 0.5, 1.0]) 385 | np.testing.assert_almost_equal( 386 | s.ticks(1), [0.0, 1.0]) 387 | 388 | s = scale.linear(domain=[-100,0,100]) 389 | np.testing.assert_almost_equal( 390 | s.ticks(10), [-100, -80, -60, -40, -20, 0, 20, 40, 60, 80, 100]) 391 | np.testing.assert_almost_equal( 392 | s.ticks(9), [-100, -80, -60, -40, -20, 0, 20, 40, 60, 80, 100]) 393 | np.testing.assert_almost_equal( 394 | s.ticks(8), [-100, -80, -60, -40, -20, 0, 20, 40, 60, 80, 100]) 395 | np.testing.assert_almost_equal( 396 | s.ticks(7), [-100, -80, -60, -40, -20, 0, 20, 40, 60, 80, 100]) 397 | np.testing.assert_almost_equal( 398 | s.ticks(6), [-100, -50, 0, 50, 100]) 399 | np.testing.assert_almost_equal( 400 | s.ticks(5), [-100, -50, 0, 50, 100]) 401 | np.testing.assert_almost_equal( 402 | s.ticks(4), [-100, -50, 0, 50, 100]) 403 | np.testing.assert_almost_equal( 404 | s.ticks(3), [-100, -50, 0, 50, 100]) 405 | np.testing.assert_almost_equal( 406 | s.ticks(2), [-100, 0, 100]) 407 | np.testing.assert_almost_equal( 408 | s.ticks(1), [ 0 ]) 409 | 410 | def test_36(self): 411 | """ 412 | linear.ticks(count) returns the empty array if count is not a positive integer 413 | """ 414 | s = scale.linear() 415 | self.assertEqual(s.ticks(None), []) 416 | self.assertEqual(s.ticks(0), []); 417 | self.assertEqual(s.ticks(-1), []); 418 | 419 | def test_37(self): 420 | """ 421 | linear.ticks() is an alias for linear.ticks(10) 422 | """ 423 | s = scale.linear(); 424 | self.assertEqual(s.ticks(), s.ticks(10)) 425 | 426 | 427 | if __name__ == "__main__": 428 | unittest.main() 429 | --------------------------------------------------------------------------------