├── 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 |
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 |
--------------------------------------------------------------------------------