├── .gitignore ├── .python-version ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── examples │ ├── legends.py │ ├── minimal.py │ ├── minimal_hires.py │ └── scatter.py └── images │ ├── screenshot-minimal-hires.png │ ├── screenshot-minimal.png │ ├── screenshot-moving-sines.png │ ├── screenshot-scatter.png │ └── screenshot-spectrum.png ├── justfile ├── pyproject.toml ├── src └── textual_plot │ ├── __init__.py │ ├── demo.py │ ├── plot_widget.py │ ├── py.typed │ ├── resources │ └── morning-spectrum.csv │ └── ticks.py ├── tests └── test_transformations.py └── uv.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # Virtual environments 10 | .venv 11 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | 12 | - Add a plot legend, which you can target using CSS (#9). 13 | 14 | ## [0.6.1] - 2025-05-24 15 | 16 | ### Fixed 17 | 18 | - Truncate the y-axis tick label to avoid the leftmost (most significant) digit being clipped (#4). 19 | 20 | ## [0.6.0] - 2025-05-24 21 | 22 | ### Changed 23 | 24 | - Lower Python version requirements back to 3.10. 25 | 26 | ### Fixed 27 | 28 | - Zooming the plot no longer scrolls the container the plot is in. 29 | 30 | ## [0.5.0] - 2025-05-01 31 | 32 | ### Fixed 33 | 34 | - Fix crash on newer textual / textual-hires-canvas when render() runs before all variables are initialised. 35 | 36 | ## [0.4.0] - 2025-03-16 37 | 38 | ### Fixed 39 | 40 | - Fix inter-tick spacing becoming an order of magnitude too large due to negative indexing. 41 | - Improve zooming performance by delaying refresh (batching render calls) and 42 | needs_rerender flag, performing the render only once in a batch. 43 | - Fix invisible plot on first focus event. 44 | 45 | ## [0.3.0] - 2025-03-11 46 | 47 | ### Added 48 | 49 | - `PlotWidget` now has name, classes and disabled parameters. 50 | - Added `allow_pan_and_zoom` parameter to allow or disable panning and zooming the plot. 51 | - Added setting x and y ticks to specific values, or an empty list to hide the ticks. 52 | 53 | ## [0.2.0] - 2025-03-01 54 | 55 | ### Added 56 | 57 | - Post a ScaleChanged message when the user zooms, pans, or resets the scale. 58 | 59 | ### Fixed 60 | 61 | - Fix crash when data contained NaN of Inf values. 62 | - Fix crash when y_min and y_max are identical, i.e. when plotting a constant value. 63 | 64 | ## [0.1.1] - 2025-02-14 65 | 66 | ### Fixed 67 | 68 | - Fix missing csv file for demo. 69 | 70 | ## [0.1.0] - 2025-02-14 71 | 72 | Initial release. 📈🎉 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 David Fokkema 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A native plotting widget for Textual apps 2 | 3 | [Textual](https://www.textualize.io/) is an excellent Python framework for building applications in the terminal, or on the web. This library provides a plot widget which your app can use to plot all kinds of quantitative data. So, no pie charts, sorry. The widget support scatter plots and line plots, and can also draw using _high-resolution_ characters like unicode half blocks, quadrants and 8-dot Braille characters. It may still be apparent that these are drawn using characters that take up a full block in the terminal, especially when plot series overlap. However, the use of these characters can reduce the line thickness and improve the resolution tremendously. 4 | 5 | ## Screenshots 6 | 7 | ![screenshot of day-time spectrum](docs/images/screenshot-spectrum.png) 8 | 9 | ![screenshot of moving sines](docs/images/screenshot-moving-sines.png) 10 | 11 | ![video of plot demo](https://github.com/user-attachments/assets/dd725fdc-e182-4bed-8951-5899bdb99a20) 12 | 13 | The _daytime spectrum_ dataset shows the visible-light spectrum recorded by an Ocean Optics USB2000+ spectrometer using the [DeadSea Optics](https://github.com/davidfokkema/deadsea-optics) software. It was taken in the morning while the detector was facing my office window. 14 | 15 | ## Features 16 | 17 | - Line plots 18 | - Scatter plots 19 | - Automatic scaling and tick placement at _nice_ intervals (1, 2, 5, etc.) 20 | - Axes labels 21 | - High-resolution modes using unicode half blocks (1x2), quadrants (2x2) and braille (2x8) characters 22 | - Mouse support for _zooming_ (mouse scrolling) and _panning_ (mouse dragging) 23 | - Horizontal- or vertical-only zooming and panning when the mouse cursor is in the plot margins 24 | 25 | ## Running the demo / installation 26 | 27 | Using [uv](https://astral.sh/uv/): 28 | ```console 29 | uvx textual-plot 30 | ``` 31 | 32 | Using [pipx](https://pipx.pypa.io/): 33 | ```console 34 | pipx run textual-plot 35 | ``` 36 | 37 | Install the package with either 38 | ```console 39 | uv tool install textual-plot 40 | ``` 41 | or 42 | ```console 43 | pipx install textual-plot 44 | ``` 45 | Alternatively, install the package with `pip` (please, use virtual environments) and run the demo: 46 | ```console 47 | pip install textual-plot 48 | ``` 49 | 50 | In all cases, you can run the demo with 51 | ```console 52 | textual-plot 53 | ``` 54 | 55 | ## Tutorial 56 | 57 | A minimal example is shown below: 58 | ![screenshot of minimal example](docs/images/screenshot-minimal.png) 59 | ```python 60 | from textual.app import App, ComposeResult 61 | 62 | from textual_plot import PlotWidget 63 | 64 | 65 | class MinimalApp(App[None]): 66 | def compose(self) -> ComposeResult: 67 | yield PlotWidget() 68 | 69 | def on_mount(self) -> None: 70 | plot = self.query_one(PlotWidget) 71 | plot.plot(x=[0, 1, 2, 3, 4], y=[0, 1, 4, 9, 16]) 72 | 73 | 74 | MinimalApp().run() 75 | ``` 76 | You include a `PlotWidget` in your compose method and after your UI has finished composing, you can start plotting data. The `plot()` method takes `x` and `y` data which should be array-like. It can be lists, or NumPy arrays, or really anything that can be turned into a NumPy array which is what's used internally. The `plot()` method further accepts a `line_style` argument which accepts Textual styles like `"white"`, `"red on blue3"`, etc. For standard low-resolution plots, it does not make much sense to specify a background color since the text character used for plotting is a full block filling an entire cell. 77 | 78 | ### High-resolution plotting 79 | 80 | The plot widget supports high-resolution plotting where the character does not take up the full cell: 81 | 82 | ![screenshot of minimal hires example](docs/images/screenshot-minimal-hires.png) 83 | 84 | ```python 85 | from textual.app import App, ComposeResult 86 | 87 | from textual_plot import HiResMode, PlotWidget 88 | 89 | 90 | class MinimalApp(App[None]): 91 | def compose(self) -> ComposeResult: 92 | yield PlotWidget() 93 | 94 | def on_mount(self) -> None: 95 | plot = self.query_one(PlotWidget) 96 | plot.plot( 97 | x=[0, 1, 2, 3, 4], 98 | y=[0, 1, 4, 9, 16], 99 | hires_mode=HiResMode.BRAILLE, 100 | line_style="bright_yellow on blue3", 101 | ) 102 | 103 | 104 | MinimalApp().run() 105 | ``` 106 | Admittedly, you'll be mostly plotting with foreground colors only. The plot widget supports four high-resolution modes: `Hires.BRAILLE` (2x8), `HiRes.HALFBLOCK` (1x2) and `HiRes.QUADRANT` (2x2) where the size between brackets is the number of 'pixels' inside a single cell. 107 | 108 | ### Scatter plots 109 | 110 | To create scatter plots, use the `scatter()` method, which accepts a `marker` argument which can be any unicode character (as long as it is one cell wide, which excludes many emoji characters and non-Western scripts): 111 | ![screenshot of scatter plot](docs/images/screenshot-scatter.png) 112 | ```python 113 | import numpy as np 114 | from textual.app import App, ComposeResult 115 | 116 | from textual_plot import PlotWidget 117 | 118 | 119 | class MinimalApp(App[None]): 120 | def compose(self) -> ComposeResult: 121 | yield PlotWidget() 122 | 123 | def on_mount(self) -> None: 124 | rng = np.random.default_rng(seed=4) 125 | plot = self.query_one(PlotWidget) 126 | 127 | x = np.linspace(0, 10, 21) 128 | y = 0.2 * x - 1 + rng.normal(loc=0.0, scale=0.2, size=len(x)) 129 | plot.scatter(x, y, marker="⦿") 130 | 131 | 132 | MinimalApp().run() 133 | ``` 134 | 135 | ### The full demo code 136 | 137 | Finally, the code of the demo is given below, showing how you can handle multiple plots and updating 'live' data: 138 | ```python 139 | import importlib.resources 140 | import itertools 141 | 142 | import numpy as np 143 | from textual.app import App, ComposeResult 144 | from textual.containers import Container 145 | from textual.widgets import Footer, Header, TabbedContent, TabPane 146 | from textual_hires_canvas import HiResMode 147 | 148 | from textual_plot import PlotWidget 149 | 150 | 151 | class SpectrumPlot(Container): 152 | BINDINGS = [("m", "cycle_modes", "Cycle Modes")] 153 | 154 | _modes = itertools.cycle( 155 | [HiResMode.QUADRANT, HiResMode.BRAILLE, None, HiResMode.HALFBLOCK] 156 | ) 157 | mode = next(_modes) 158 | 159 | def compose(self) -> ComposeResult: 160 | yield PlotWidget() 161 | 162 | def on_mount(self) -> None: 163 | # Read CSV data included with this package 164 | self.spectrum_csv = importlib.resources.read_text( 165 | "textual_plot.resources", "morning-spectrum.csv" 166 | ).splitlines() 167 | 168 | # plot the spectrum and set ymin limit once 169 | self.plot_spectrum() 170 | self.query_one(PlotWidget).set_ylimits(ymin=0) 171 | 172 | def plot_spectrum(self) -> None: 173 | x, y = np.genfromtxt( 174 | self.spectrum_csv, 175 | delimiter=",", 176 | names=True, 177 | unpack=True, 178 | ) 179 | 180 | plot = self.query_one(PlotWidget) 181 | plot.clear() 182 | plot.plot(x, y, hires_mode=self.mode) 183 | plot.set_xlabel("Wavelength (nm)") 184 | plot.set_ylabel("Intensity") 185 | 186 | def action_cycle_modes(self) -> None: 187 | self.mode = next(self._modes) 188 | self.plot_spectrum() 189 | 190 | 191 | class SinePlot(Container): 192 | _phi: float = 0.0 193 | 194 | def compose(self) -> ComposeResult: 195 | yield PlotWidget() 196 | 197 | def on_mount(self) -> None: 198 | self._timer = self.set_interval(1 / 24, self.plot_moving_sines, pause=True) 199 | 200 | def on_show(self) -> None: 201 | self._timer.resume() 202 | 203 | def on_hide(self) -> None: 204 | self._timer.pause() 205 | 206 | def plot_moving_sines(self) -> None: 207 | plot = self.query_one(PlotWidget) 208 | plot.clear() 209 | x = np.linspace(0, 10, 41) 210 | y = x**2 / 3.5 211 | plot.scatter( 212 | x, 213 | y, 214 | marker_style="blue", 215 | # marker="*", 216 | hires_mode=HiResMode.QUADRANT, 217 | ) 218 | x = np.linspace(0, 10, 200) 219 | plot.plot( 220 | x=x, 221 | y=10 + 10 * np.sin(x + self._phi), 222 | line_style="blue", 223 | hires_mode=None, 224 | ) 225 | 226 | plot.plot( 227 | x=x, 228 | y=10 + 10 * np.sin(x + self._phi + 1), 229 | line_style="red3", 230 | hires_mode=HiResMode.HALFBLOCK, 231 | ) 232 | plot.plot( 233 | x=x, 234 | y=10 + 10 * np.sin(x + self._phi + 2), 235 | line_style="green", 236 | hires_mode=HiResMode.QUADRANT, 237 | ) 238 | plot.plot( 239 | x=x, 240 | y=10 + 10 * np.sin(x + self._phi + 3), 241 | line_style="yellow", 242 | hires_mode=HiResMode.BRAILLE, 243 | ) 244 | 245 | self._phi += 0.1 246 | 247 | 248 | class DemoApp(App[None]): 249 | AUTO_FOCUS = "PlotWidget" 250 | 251 | def compose(self) -> ComposeResult: 252 | yield Header() 253 | yield Footer() 254 | with TabbedContent(): 255 | with TabPane("Daytime spectrum"): 256 | yield SpectrumPlot() 257 | with TabPane("Moving sines"): 258 | yield SinePlot() 259 | 260 | 261 | def main(): 262 | app = DemoApp() 263 | app.run() 264 | 265 | 266 | if __name__ == "__main__": 267 | main() 268 | ``` 269 | 270 | ## List of important plot widget methods 271 | 272 | - `clear()`: clear the plot. 273 | - `plot(x, y, line_style, hires_mode, label)`: plot a dataset with a line using the specified linestyle and high-resolution mode. 274 | - `scatter(x, y, marker, marker_style, hires_mode, label)`: plot a dataset with markers using the specified marker, marker style and high-resolution mode. 275 | - `set_xlimits(xmin, xmax)`: set the x-axis limits. `None` means autoscale. 276 | - `set_ylimits(xmin, xmax)`: set the y-axis limits. `None` means autoscale. 277 | - `set_xticks(ticks)`: manually specify x-axis tick locations. 278 | - `set_yticks(ticks)`: manually specify y-axis tick locations. 279 | - `set_xlabel(label)`: set the x-axis label. 280 | - `set_ylabel(label)`: set the y-axis label. 281 | - `show_legend(location, is_visible)`: show or hide the plot legend. 282 | 283 | Various other methods exist, mostly for coordinate transformations and handling UI events to zoom and pan the plot. 284 | 285 | ## Alternatives 286 | 287 | [Textual-plotext](https://github.com/Textualize/textual-plotext) uses the [plotext](https://github.com/piccolomo/plotext) library which has more features than this library. However, it does not support interactive zooming or panning and the tick placement isn't as nice since it simply divides up the axes range into a fixed number of intervals giving values like 0, 123.4, 246.8, etc. 288 | 289 | ## Roadmap 290 | 291 | The performance can be much improved, but we're working on that. Next, we'll work on adding some features like date axes. This will (probably) not turn into a general do-it-all plotting library. We focus first on handling quantitative data in the context of physics experiments. If you'd like to see features added, do let us know. And if a PR is of good quality and is a good fit for the API, we'd love to handle more use cases beyond physics. And who knows, maybe this _will_ turn into a general plotting library! -------------------------------------------------------------------------------- /docs/examples/legends.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from textual.app import App, ComposeResult 3 | 4 | from textual_plot import HiResMode, LegendLocation, PlotWidget 5 | 6 | 7 | class LegendsApp(App[None]): 8 | BINDINGS = [("t", "toggle_legend", "Toggle legend")] 9 | 10 | show_legend: bool = True 11 | 12 | def compose(self) -> ComposeResult: 13 | yield PlotWidget() 14 | 15 | def on_mount(self) -> None: 16 | rng = np.random.default_rng(seed=4) 17 | plot = self.query_one(PlotWidget) 18 | 19 | x = np.linspace(0, 10, 71) 20 | y = 0.5 * x - 1 + rng.normal(loc=0.0, scale=0.2, size=len(x)) 21 | plot.scatter(x, y, marker="⦿", label="Series 1") 22 | plot.scatter( 23 | x, y + 0.5, marker="⦿", label="Series 1.5", hires_mode=HiResMode.QUADRANT 24 | ) 25 | plot.scatter(x, y + 1, label="Series 2", marker_style="bold italic green") 26 | plot.plot(x, y + 2, label="Series 3", line_style="red") 27 | plot.plot( 28 | x, 29 | y + 3, 30 | label="Series 3", 31 | line_style="blue", 32 | hires_mode=HiResMode.BRAILLE, 33 | ) 34 | plot.plot( 35 | x, 36 | y + 4, 37 | # label="Series 4", 38 | hires_mode=HiResMode.HALFBLOCK, 39 | ) 40 | plot.plot( 41 | x, 42 | y + 5, 43 | label="Series 5", 44 | line_style="bold italic cyan", 45 | hires_mode=HiResMode.QUADRANT, 46 | ) 47 | plot.show_legend(location=LegendLocation.TOPLEFT) 48 | 49 | def action_toggle_legend(self) -> None: 50 | plot = self.query_one(PlotWidget) 51 | self.show_legend = not self.show_legend 52 | plot.show_legend(is_visible=self.show_legend) 53 | 54 | 55 | LegendsApp().run() 56 | -------------------------------------------------------------------------------- /docs/examples/minimal.py: -------------------------------------------------------------------------------- 1 | from textual.app import App, ComposeResult 2 | 3 | from textual_plot import PlotWidget 4 | 5 | 6 | class MinimalApp(App[None]): 7 | def compose(self) -> ComposeResult: 8 | yield PlotWidget() 9 | 10 | def on_mount(self) -> None: 11 | plot = self.query_one(PlotWidget) 12 | plot.plot(x=[0, 1, 2, 3, 4], y=[0, 1, 4, 9, 16]) 13 | 14 | 15 | MinimalApp().run() 16 | -------------------------------------------------------------------------------- /docs/examples/minimal_hires.py: -------------------------------------------------------------------------------- 1 | from textual.app import App, ComposeResult 2 | 3 | from textual_plot import HiResMode, PlotWidget 4 | 5 | 6 | class MinimalApp(App[None]): 7 | def compose(self) -> ComposeResult: 8 | yield PlotWidget() 9 | 10 | def on_mount(self) -> None: 11 | plot = self.query_one(PlotWidget) 12 | plot.plot( 13 | x=[0, 1, 2, 3, 4], 14 | y=[0, 1, 4, 9, 16], 15 | hires_mode=HiResMode.BRAILLE, 16 | line_style="bright_yellow on blue3", 17 | ) 18 | 19 | 20 | MinimalApp().run() 21 | -------------------------------------------------------------------------------- /docs/examples/scatter.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from textual.app import App, ComposeResult 3 | 4 | from textual_plot import PlotWidget 5 | 6 | 7 | class MinimalApp(App[None]): 8 | def compose(self) -> ComposeResult: 9 | yield PlotWidget() 10 | 11 | def on_mount(self) -> None: 12 | rng = np.random.default_rng(seed=4) 13 | plot = self.query_one(PlotWidget) 14 | 15 | x = np.linspace(0, 10, 21) 16 | y = 0.2 * x - 1 + rng.normal(loc=0.0, scale=0.2, size=len(x)) 17 | plot.scatter(x, y, marker="⦿") 18 | 19 | 20 | MinimalApp().run() 21 | -------------------------------------------------------------------------------- /docs/images/screenshot-minimal-hires.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfokkema/textual-plot/99680c4aa1f8667c98bd88c700885f7bf4e85b73/docs/images/screenshot-minimal-hires.png -------------------------------------------------------------------------------- /docs/images/screenshot-minimal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfokkema/textual-plot/99680c4aa1f8667c98bd88c700885f7bf4e85b73/docs/images/screenshot-minimal.png -------------------------------------------------------------------------------- /docs/images/screenshot-moving-sines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfokkema/textual-plot/99680c4aa1f8667c98bd88c700885f7bf4e85b73/docs/images/screenshot-moving-sines.png -------------------------------------------------------------------------------- /docs/images/screenshot-scatter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfokkema/textual-plot/99680c4aa1f8667c98bd88c700885f7bf4e85b73/docs/images/screenshot-scatter.png -------------------------------------------------------------------------------- /docs/images/screenshot-spectrum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfokkema/textual-plot/99680c4aa1f8667c98bd88c700885f7bf4e85b73/docs/images/screenshot-spectrum.png -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | demo: 2 | uv run textual run textual_plot.demo:DemoApp 3 | 4 | typecheck: 5 | uv run mypy -p textual_plot --strict 6 | 7 | test: 8 | uv run pytest 9 | 10 | format: 11 | uvx ruff format 12 | 13 | fix: 14 | uvx ruff check --fix 15 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "textual-plot" 3 | version = "0.6.1" 4 | description = "A native plotting widget for Textual apps" 5 | readme = "README.md" 6 | authors = [{ name = "David Fokkema", email = "davidfokkema@icloud.com" }] 7 | license = "MIT" 8 | license-files = ["LICENSE"] 9 | requires-python = ">=3.10" 10 | dependencies = ["numpy>=2.2.1", "textual>=1.0.0", "textual-hires-canvas>=0.7.0"] 11 | 12 | [build-system] 13 | requires = ["hatchling"] 14 | build-backend = "hatchling.build" 15 | 16 | [dependency-groups] 17 | dev = [ 18 | "mypy>=1.14.1", 19 | "pytest>=8.3.4", 20 | "rust-just>=1.38.0", 21 | "textual-dev>=1.7.0", 22 | ] 23 | 24 | [project.scripts] 25 | textual-plot = "textual_plot.demo:main" 26 | 27 | [tool.mypy] 28 | python_version = "3.10" 29 | plugins = "numpy.typing.mypy_plugin" 30 | -------------------------------------------------------------------------------- /src/textual_plot/__init__.py: -------------------------------------------------------------------------------- 1 | from textual_plot.plot_widget import HiResMode, LegendLocation, PlotWidget 2 | 3 | __all__ = ["HiResMode", "LegendLocation", "PlotWidget"] 4 | -------------------------------------------------------------------------------- /src/textual_plot/demo.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import importlib.resources 4 | import itertools 5 | 6 | import numpy as np 7 | from textual.app import App, ComposeResult 8 | from textual.containers import Container 9 | from textual.widgets import Footer, Header, TabbedContent, TabPane 10 | from textual_hires_canvas import HiResMode 11 | 12 | from textual_plot import PlotWidget 13 | 14 | 15 | class SpectrumPlot(Container): 16 | BINDINGS = [("m", "cycle_modes", "Cycle Modes")] 17 | 18 | _modes = itertools.cycle( 19 | [HiResMode.QUADRANT, HiResMode.BRAILLE, None, HiResMode.HALFBLOCK] 20 | ) 21 | mode = next(_modes) 22 | 23 | def compose(self) -> ComposeResult: 24 | yield PlotWidget() 25 | 26 | def on_mount(self) -> None: 27 | # Read CSV data included with this package 28 | self.spectrum_csv = importlib.resources.read_text( 29 | "textual_plot.resources", "morning-spectrum.csv" 30 | ).splitlines() 31 | 32 | # plot the spectrum and set ymin limit once 33 | self.plot_spectrum() 34 | self.query_one(PlotWidget).set_ylimits(ymin=0) 35 | 36 | def plot_spectrum(self) -> None: 37 | x, y = np.genfromtxt( 38 | self.spectrum_csv, 39 | delimiter=",", 40 | names=True, 41 | unpack=True, 42 | ) 43 | 44 | plot = self.query_one(PlotWidget) 45 | plot.clear() 46 | plot.plot(x, y, hires_mode=self.mode) 47 | plot.set_xlabel("Wavelength (nm)") 48 | plot.set_ylabel("Intensity") 49 | 50 | def action_cycle_modes(self) -> None: 51 | self.mode = next(self._modes) 52 | self.plot_spectrum() 53 | 54 | 55 | class SinePlot(Container): 56 | _phi: float = 0.0 57 | 58 | def compose(self) -> ComposeResult: 59 | yield PlotWidget() 60 | 61 | def on_mount(self) -> None: 62 | self._timer = self.set_interval(1 / 24, self.plot_moving_sines, pause=True) 63 | 64 | def on_show(self) -> None: 65 | self._timer.resume() 66 | 67 | def on_hide(self) -> None: 68 | self._timer.pause() 69 | 70 | def plot_moving_sines(self) -> None: 71 | plot = self.query_one(PlotWidget) 72 | plot.clear() 73 | x = np.linspace(0, 10, 41) 74 | y = x**2 / 3.5 75 | plot.scatter( 76 | x, 77 | y, 78 | marker_style="blue", 79 | # marker="*", 80 | hires_mode=HiResMode.QUADRANT, 81 | ) 82 | x = np.linspace(0, 10, 200) 83 | plot.plot( 84 | x=x, 85 | y=10 + 10 * np.sin(x + self._phi), 86 | line_style="blue", 87 | hires_mode=None, 88 | ) 89 | 90 | plot.plot( 91 | x=x, 92 | y=10 + 10 * np.sin(x + self._phi + 1), 93 | line_style="red3", 94 | hires_mode=HiResMode.HALFBLOCK, 95 | ) 96 | plot.plot( 97 | x=x, 98 | y=10 + 10 * np.sin(x + self._phi + 2), 99 | line_style="green", 100 | hires_mode=HiResMode.QUADRANT, 101 | ) 102 | plot.plot( 103 | x=x, 104 | y=10 + 10 * np.sin(x + self._phi + 3), 105 | line_style="yellow", 106 | hires_mode=HiResMode.BRAILLE, 107 | ) 108 | 109 | self._phi += 0.1 110 | 111 | 112 | class DemoApp(App[None]): 113 | AUTO_FOCUS = "PlotWidget" 114 | 115 | def compose(self) -> ComposeResult: 116 | yield Header() 117 | yield Footer() 118 | with TabbedContent(): 119 | with TabPane("Daytime spectrum"): 120 | yield SpectrumPlot() 121 | with TabPane("Moving sines"): 122 | yield SinePlot() 123 | 124 | 125 | def main(): 126 | app = DemoApp() 127 | app.run() 128 | 129 | 130 | if __name__ == "__main__": 131 | main() 132 | -------------------------------------------------------------------------------- /src/textual_plot/plot_widget.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import enum 4 | import sys 5 | from dataclasses import dataclass 6 | from math import ceil, floor, log10 7 | from typing import Iterable 8 | 9 | from rich.text import Text 10 | 11 | if sys.version_info >= (3, 11): 12 | from typing import Self 13 | else: 14 | from typing_extensions import Self 15 | 16 | import numpy as np 17 | from numpy.typing import ArrayLike, NDArray 18 | from textual import on 19 | from textual._box_drawing import BOX_CHARACTERS, combine_quads 20 | from textual.app import ComposeResult 21 | from textual.containers import Grid 22 | from textual.events import MouseMove, MouseScrollDown, MouseScrollUp 23 | from textual.geometry import Offset, Region 24 | from textual.message import Message 25 | from textual.widget import Widget 26 | from textual.widgets import Static 27 | from textual_hires_canvas import Canvas, HiResMode, TextAlign 28 | 29 | ZOOM_FACTOR = 0.05 30 | 31 | LEGEND_LINE = { 32 | None: "███", 33 | HiResMode.BRAILLE: "⠒⠒⠒", 34 | HiResMode.HALFBLOCK: "▀▀▀", 35 | HiResMode.QUADRANT: "▀▀▀", 36 | } 37 | 38 | LEGEND_MARKER = { 39 | HiResMode.BRAILLE: "⠂", 40 | HiResMode.HALFBLOCK: "▀", 41 | HiResMode.QUADRANT: "▘", 42 | } 43 | 44 | 45 | class LegendLocation(enum.Enum): 46 | """An enum to specify the location of the legend in the plot widget.""" 47 | 48 | TOPLEFT = enum.auto() 49 | TOPRIGHT = enum.auto() 50 | BOTTOMLEFT = enum.auto() 51 | BOTTOMRIGHT = enum.auto() 52 | 53 | 54 | @dataclass 55 | class DataSet: 56 | x: NDArray[np.floating] 57 | y: NDArray[np.floating] 58 | hires_mode: HiResMode | None 59 | 60 | 61 | @dataclass 62 | class LinePlot(DataSet): 63 | line_style: str 64 | 65 | 66 | @dataclass 67 | class ScatterPlot(DataSet): 68 | marker: str | None 69 | marker_style: str 70 | 71 | 72 | class PlotWidget(Widget, can_focus=True): 73 | """A plot widget for Textual apps. 74 | 75 | This widget supports high-resolution line and scatter plots, has nice ticks 76 | at 1, 2, 5, 10, 20, 50, etc. intervals and supports zooming and panning with 77 | your pointer device. 78 | """ 79 | 80 | @dataclass 81 | class ScaleChanged(Message): 82 | plot: "PlotWidget" 83 | x_min: float 84 | x_max: float 85 | y_min: float 86 | y_max: float 87 | 88 | DEFAULT_CSS = """ 89 | PlotWidget { 90 | layers: plot legend; 91 | 92 | Grid { 93 | layer: plot; 94 | grid-size: 2 3; 95 | 96 | #top-margin, #bottom-margin { 97 | column-span: 2; 98 | } 99 | } 100 | 101 | #legend { 102 | layer: legend; 103 | width: auto; 104 | border: solid white; 105 | } 106 | } 107 | """ 108 | 109 | BINDINGS = [("r", "reset_scales", "Reset scales")] 110 | 111 | _datasets: list[DataSet] 112 | _labels: list[str] 113 | 114 | _user_x_min: float | None = None 115 | _user_x_max: float | None = None 116 | _user_y_min: float | None = None 117 | _user_y_max: float | None = None 118 | _auto_x_min: bool = True 119 | _auto_x_max: bool = True 120 | _auto_y_min: bool = True 121 | _auto_y_max: bool = True 122 | _x_min: float = 0.0 123 | _x_max: float = 1.0 124 | _y_min: float = 0.0 125 | _y_max: float = 1.0 126 | 127 | _x_ticks: Iterable[float] | None = None 128 | _y_ticks: Iterable[float] | None = None 129 | 130 | _margin_top: int = 2 131 | _margin_bottom: int = 3 132 | _margin_left: int = 10 133 | _legend_location: LegendLocation = LegendLocation.TOPRIGHT 134 | 135 | _x_label: str = "" 136 | _y_label: str = "" 137 | 138 | _allow_pan_and_zoom: bool = True 139 | _is_dragging: bool = False 140 | _needs_rerender: bool = False 141 | 142 | def __init__( 143 | self, 144 | allow_pan_and_zoom: bool = True, 145 | name: str | None = None, 146 | id: str | None = None, 147 | classes: str | None = None, 148 | *, 149 | disabled: bool = False, 150 | ) -> None: 151 | """Initializes the plot widget with basic widget parameters. 152 | 153 | Params: 154 | allow_pan_and_zoom: Whether to allow panning and zooming the plot. 155 | Defaults to True. 156 | name: The name of the widget. 157 | id: The ID of the widget in the DOM. 158 | classes: The CSS classes for the widget. 159 | disabled: Whether the widget is disabled or not. 160 | """ 161 | super().__init__( 162 | name=name, 163 | id=id, 164 | classes=classes, 165 | disabled=disabled, 166 | ) 167 | self._datasets = [] 168 | self._labels = [] 169 | self._allow_pan_and_zoom = allow_pan_and_zoom 170 | 171 | def compose(self) -> ComposeResult: 172 | with Grid(): 173 | yield Canvas(1, 1, id="top-margin") 174 | yield Canvas(1, 1, id="left-margin") 175 | yield Canvas(1, 1, id="plot") 176 | yield Canvas(1, 1, id="bottom-margin") 177 | yield Static(id="legend") 178 | 179 | def on_mount(self) -> None: 180 | self._update_margin_sizes() 181 | self.set_xlimits(None, None) 182 | self.set_ylimits(None, None) 183 | self.clear() 184 | 185 | def _on_canvas_resize(self, event: Canvas.Resize) -> None: 186 | event.canvas.reset(size=event.size) 187 | self._needs_rerender = True 188 | self.call_later(self.refresh) 189 | 190 | def _update_margin_sizes(self) -> None: 191 | grid = self.query_one(Grid) 192 | grid.styles.grid_columns = f"{self._margin_left} 1fr" 193 | grid.styles.grid_rows = f"{self._margin_top} 1fr {self._margin_bottom}" 194 | 195 | def clear(self) -> None: 196 | """Clear the plot canvas.""" 197 | self._datasets = [] 198 | self._labels = [] 199 | self._needs_rerender = True 200 | self.refresh() 201 | 202 | def plot( 203 | self, 204 | x: ArrayLike, 205 | y: ArrayLike, 206 | line_style: str = "white", 207 | hires_mode: HiResMode | None = None, 208 | label: str | None = None, 209 | ) -> None: 210 | """Graph dataset using a line plot. 211 | 212 | If you supply hires_mode, the dataset will be plotted using one of the 213 | available high-resolution modes like 1x2, 2x2 or 2x8 pixel-per-cell 214 | characters. 215 | 216 | Args: 217 | x: An ArrayLike with the data values for the horizontal axis. 218 | y: An ArrayLike with the data values for the vertical axis. 219 | line_style: A string with the style of the line. Defaults to 220 | "white". 221 | hires_mode: A HiResMode enum or None to plot with full-height 222 | blocks. Defaults to None. 223 | label: A string with the label for the dataset. Defaults to None. 224 | """ 225 | x, y = drop_nans_and_infs(np.array(x), np.array(y)) 226 | self._datasets.append( 227 | LinePlot( 228 | x=x, 229 | y=y, 230 | line_style=line_style, 231 | hires_mode=hires_mode, 232 | ) 233 | ) 234 | self._labels.append(label) 235 | self._needs_rerender = True 236 | self.call_later(self.refresh) 237 | 238 | def scatter( 239 | self, 240 | x: ArrayLike, 241 | y: ArrayLike, 242 | marker: str = "o", 243 | marker_style: str = "white", 244 | hires_mode: HiResMode | None = None, 245 | label: str | None = None, 246 | ) -> None: 247 | """Graph dataset using a scatter plot. 248 | 249 | If you supply hires_mode, the dataset will be plotted using one of the 250 | available high-resolution modes like 1x2, 2x2 or 2x8 pixel-per-cell 251 | characters. 252 | 253 | Args: 254 | x: An ArrayLike with the data values for the horizontal axis. 255 | y: An ArrayLike with the data values for the vertical axis. 256 | marker: A string with the character to print as the marker. 257 | marker_style: A string with the style of the marker. Defaults to 258 | "white". 259 | hires_mode: A HiResMode enum or None to plot with the supplied 260 | marker. Defaults to None. 261 | label: A string with the label for the dataset. Defaults to None. 262 | """ 263 | x, y = drop_nans_and_infs(np.array(x), np.array(y)) 264 | self._datasets.append( 265 | ScatterPlot( 266 | x=x, 267 | y=y, 268 | marker=marker, 269 | marker_style=marker_style, 270 | hires_mode=hires_mode, 271 | ) 272 | ) 273 | self._labels.append(label) 274 | self._needs_rerender = True 275 | self.call_later(self.refresh) 276 | 277 | def set_xlimits(self, xmin: float | None = None, xmax: float | None = None) -> None: 278 | """Set the limits of the x axis. 279 | 280 | Args: 281 | xmin: A float with the minimum x value or None for autoscaling. 282 | Defaults to None. 283 | xmax: A float with the maximum x value or None for autoscaling. 284 | Defaults to None. 285 | """ 286 | self._user_x_min = xmin 287 | self._user_x_max = xmax 288 | self._auto_x_min = xmin is None 289 | self._auto_x_max = xmax is None 290 | self._x_min = xmin if xmin is not None else 0.0 291 | self._x_max = xmax if xmax is not None else 1.0 292 | self._needs_rerender = True 293 | self.refresh() 294 | 295 | def set_ylimits(self, ymin: float | None = None, ymax: float | None = None) -> None: 296 | """Set the limits of the y axis. 297 | 298 | Args: 299 | xmin: A float with the minimum y value or None for autoscaling. 300 | Defaults to None. 301 | xmax: A float with the maximum y value or None for autoscaling. 302 | Defaults to None. 303 | """ 304 | self._user_y_min = ymin 305 | self._user_y_max = ymax 306 | self._auto_y_min = ymin is None 307 | self._auto_y_max = ymax is None 308 | self._y_min = ymin if ymin is not None else 0.0 309 | self._y_max = ymax if ymax is not None else 1.0 310 | self._needs_rerender = True 311 | self.refresh() 312 | 313 | def set_xlabel(self, label: str) -> None: 314 | """Set a label for the x axis. 315 | 316 | Args: 317 | label: A string with the label text. 318 | """ 319 | self._x_label = label 320 | 321 | def set_ylabel(self, label: str) -> None: 322 | """Set a label for the y axis. 323 | 324 | Args: 325 | label: A string with the label text. 326 | """ 327 | self._y_label = label 328 | 329 | def set_xticks(self, ticks: Iterable[float] | None = None) -> None: 330 | """Set the x axis ticks. 331 | 332 | Use None for autoscaling, an empty list to hide the ticks. 333 | 334 | Args: 335 | ticks: An iterable with the tick values. 336 | """ 337 | self._x_ticks = ticks 338 | 339 | def set_yticks(self, ticks: Iterable[float] | None = None) -> None: 340 | """Set the y axis ticks. 341 | 342 | Use None for autoscaling, an empty list to hide the ticks. 343 | 344 | Args: 345 | ticks: An iterable with the tick values. 346 | """ 347 | self._y_ticks = ticks 348 | 349 | def show_legend( 350 | self, 351 | location: LegendLocation = LegendLocation.TOPLEFT, 352 | is_visible: bool = True, 353 | ) -> None: 354 | """Show or hide the legend for the datasets in the plot. 355 | 356 | Args: 357 | is_visible: A boolean indicating whether to show the legend. 358 | Defaults to True. 359 | """ 360 | self.query_one("#legend", Static).display = is_visible 361 | if not is_visible: 362 | return 363 | 364 | legend_lines = [] 365 | if isinstance(location, LegendLocation): 366 | self._legend_location = location 367 | else: 368 | raise TypeError( 369 | f"Expected LegendLocation, got {type(location).__name__} instead." 370 | ) 371 | 372 | for label, dataset in zip(self._labels, self._datasets): 373 | if label is not None: 374 | if isinstance(dataset, ScatterPlot): 375 | marker = ( 376 | dataset.marker 377 | if dataset.hires_mode is None 378 | else LEGEND_MARKER[dataset.hires_mode] 379 | ).center(3) 380 | style = dataset.marker_style 381 | elif isinstance(dataset, LinePlot): 382 | marker = LEGEND_LINE[dataset.hires_mode] 383 | style = dataset.line_style 384 | else: 385 | # unsupported dataset type 386 | continue 387 | text = Text(marker) 388 | text.stylize(style) 389 | text.append(f" {label}") 390 | legend_lines.append(text.markup) 391 | self.query_one("#legend", Static).update( 392 | Text.from_markup("\n".join(legend_lines)) 393 | ) 394 | 395 | def _position_legend(self) -> None: 396 | """Position the legend in the plot widget using absolute offsets.""" 397 | 398 | canvas = self.query_one("#plot", Canvas) 399 | legend = self.query_one("#legend", Static) 400 | 401 | labels = [label for label in self._labels if label is not None] 402 | # markers and lines in the legend are 3 characters wide, plus a space, so 4 403 | max_length = 4 + max((len(s) for s in labels), default=0) 404 | 405 | if self._legend_location in (LegendLocation.TOPLEFT, LegendLocation.BOTTOMLEFT): 406 | x0 = self._margin_left + 1 407 | elif self._legend_location in ( 408 | LegendLocation.TOPRIGHT, 409 | LegendLocation.BOTTOMRIGHT, 410 | ): 411 | x0 = self._margin_left + canvas.size.width - 1 - max_length 412 | # leave room for the border 413 | x0 -= legend.styles.border.spacing.left + legend.styles.border.spacing.right 414 | if self._legend_location in (LegendLocation.TOPLEFT, LegendLocation.TOPRIGHT): 415 | y0 = self._margin_top + 1 416 | else: 417 | y0 = self._margin_top + canvas.size.height - 1 - len(labels) 418 | # leave room for the border 419 | y0 -= legend.styles.border.spacing.top + legend.styles.border.spacing.bottom 420 | legend.absolute_offset = Offset(x0, y0) 421 | 422 | def refresh( 423 | self, 424 | *regions: Region, 425 | repaint: bool = True, 426 | layout: bool = False, 427 | recompose: bool = False, 428 | ) -> Self: 429 | """Refresh the widget.""" 430 | self._render_plot() 431 | return super().refresh( 432 | *regions, repaint=repaint, layout=layout, recompose=recompose 433 | ) 434 | 435 | def _render_plot(self) -> None: 436 | if (canvas := self.query_one("#plot", Canvas))._canvas_size is None: 437 | return 438 | 439 | if self._needs_rerender: 440 | self._needs_rerender = False 441 | # clear canvas 442 | canvas.reset() 443 | 444 | # determine axis limits 445 | if self._datasets: 446 | xs = [dataset.x for dataset in self._datasets] 447 | ys = [dataset.y for dataset in self._datasets] 448 | if self._auto_x_min: 449 | self._x_min = min(np.min(x) for x in xs) 450 | if self._auto_x_max: 451 | self._x_max = max(np.max(x) for x in xs) 452 | if self._auto_y_min: 453 | self._y_min = min(np.min(y) for y in ys) 454 | if self._auto_y_max: 455 | self._y_max = max(np.max(y) for y in ys) 456 | 457 | if self._x_min == self._x_max: 458 | self._x_min -= 1e-6 459 | self._x_max += 1e-6 460 | if self._y_min == self._y_max: 461 | self._y_min -= 1e-6 462 | self._y_max += 1e-6 463 | 464 | # render datasets 465 | for dataset in self._datasets: 466 | if isinstance(dataset, ScatterPlot): 467 | self._render_scatter_plot(dataset) 468 | elif isinstance(dataset, LinePlot): 469 | self._render_line_plot(dataset) 470 | 471 | self._position_legend() 472 | 473 | # render axis, ticks and labels 474 | canvas.draw_rectangle_box( 475 | 0, 0, canvas.size.width - 1, canvas.size.height - 1, thickness=2 476 | ) 477 | self._render_x_ticks() 478 | self._render_y_ticks() 479 | self._render_x_label() 480 | self._render_y_label() 481 | 482 | def _render_scatter_plot(self, dataset: ScatterPlot) -> None: 483 | canvas = self.query_one("#plot", Canvas) 484 | assert canvas.scale_rectangle is not None 485 | if dataset.hires_mode: 486 | pixels = [ 487 | self.get_hires_pixel_from_coordinate(xi, yi) 488 | for xi, yi in zip(dataset.x, dataset.y) 489 | ] 490 | canvas.set_hires_pixels( 491 | pixels, style=dataset.marker_style, hires_mode=dataset.hires_mode 492 | ) 493 | else: 494 | pixels = [ 495 | self.get_pixel_from_coordinate(xi, yi) 496 | for xi, yi in zip(dataset.x, dataset.y) 497 | ] 498 | for pixel in pixels: 499 | assert dataset.marker is not None 500 | canvas.set_pixel( 501 | *pixel, char=dataset.marker, style=dataset.marker_style 502 | ) 503 | 504 | def _render_line_plot(self, dataset: LinePlot) -> None: 505 | canvas = self.query_one("#plot", Canvas) 506 | assert canvas.scale_rectangle is not None 507 | 508 | if dataset.hires_mode: 509 | pixels = [ 510 | self.get_hires_pixel_from_coordinate(xi, yi) 511 | for xi, yi in zip(dataset.x, dataset.y) 512 | ] 513 | coordinates = [(*pixels[i - 1], *pixels[i]) for i in range(1, len(pixels))] 514 | canvas.draw_hires_lines( 515 | coordinates, style=dataset.line_style, hires_mode=dataset.hires_mode 516 | ) 517 | else: 518 | pixels = [ 519 | self.get_pixel_from_coordinate(xi, yi) 520 | for xi, yi in zip(dataset.x, dataset.y) 521 | ] 522 | for i in range(1, len(pixels)): 523 | canvas.draw_line(*pixels[i - 1], *pixels[i], style=dataset.line_style) 524 | 525 | def _render_x_ticks(self) -> None: 526 | canvas = self.query_one("#plot", Canvas) 527 | assert canvas.scale_rectangle is not None 528 | bottom_margin = self.query_one("#bottom-margin", Canvas) 529 | bottom_margin.reset() 530 | 531 | if self._x_ticks is None: 532 | x_ticks, x_labels = self.get_ticks_between(self._x_min, self._x_max) 533 | else: 534 | x_ticks = self._x_ticks 535 | x_labels = self.get_labels_for_ticks(x_ticks) 536 | for tick, label in zip(x_ticks, x_labels): 537 | if tick < self._x_min or tick > self._x_max: 538 | continue 539 | align = TextAlign.CENTER 540 | # only interested in the x-coordinate, set y to 0.0 541 | x, _ = self.get_pixel_from_coordinate(tick, 0.0) 542 | if tick == self._x_min: 543 | x -= 1 544 | elif tick == self._x_max: 545 | align = TextAlign.RIGHT 546 | for y, quad in [ 547 | # put ticks at top and bottom of scale rectangle 548 | (0, (2, 0, 0, 0)), 549 | (canvas.scale_rectangle.bottom, (0, 0, 2, 0)), 550 | ]: 551 | new_pixel = self.combine_quad_with_pixel(quad, canvas, x, y) 552 | canvas.set_pixel(x, y, new_pixel) 553 | bottom_margin.write_text(x + self._margin_left, 0, label, align) 554 | 555 | def _render_y_ticks(self) -> None: 556 | canvas = self.query_one("#plot", Canvas) 557 | assert canvas.scale_rectangle is not None 558 | left_margin = self.query_one("#left-margin", Canvas) 559 | left_margin.reset() 560 | 561 | if self._y_ticks is None: 562 | y_ticks, y_labels = self.get_ticks_between(self._y_min, self._y_max) 563 | else: 564 | y_ticks = self._y_ticks 565 | y_labels = self.get_labels_for_ticks(y_ticks) 566 | # truncate y-labels to the left margin width 567 | y_labels = [label[: self._margin_left - 1] for label in y_labels] 568 | align = TextAlign.RIGHT 569 | for tick, label in zip(y_ticks, y_labels): 570 | if tick < self._y_min or tick > self._y_max: 571 | continue 572 | # only interested in the y-coordinate, set x to 0.0 573 | _, y = self.get_pixel_from_coordinate(0.0, tick) 574 | if tick == self._y_min: 575 | y += 1 576 | for x, quad in [ 577 | # put ticks at left and right of scale rectangle 578 | (0, (0, 0, 0, 2)), 579 | (canvas.scale_rectangle.right, (0, 2, 0, 0)), 580 | ]: 581 | new_pixel = self.combine_quad_with_pixel(quad, canvas, x, y) 582 | canvas.set_pixel(x, y, new_pixel) 583 | left_margin.write_text(self._margin_left - 2, y, label, align) 584 | 585 | def _render_x_label(self) -> None: 586 | canvas = self.query_one("#plot", Canvas) 587 | margin = self.query_one("#bottom-margin", Canvas) 588 | margin.write_text( 589 | canvas.size.width // 2 + self._margin_left, 590 | 2, 591 | self._x_label, 592 | TextAlign.CENTER, 593 | ) 594 | 595 | def _render_y_label(self) -> None: 596 | margin = self.query_one("#top-margin", Canvas) 597 | margin.write_text( 598 | self._margin_left - 2, 599 | 0, 600 | self._y_label, 601 | TextAlign.CENTER, 602 | ) 603 | 604 | def get_ticks_between( 605 | self, min_: float, max_: float, max_ticks: int = 8 606 | ) -> tuple[list[float], list[str]]: 607 | delta_x = max_ - min_ 608 | tick_spacing = delta_x / 5 609 | power = floor(log10(tick_spacing)) 610 | approx_interval = tick_spacing / 10**power 611 | intervals = np.array([1, 2, 5, 10]) 612 | 613 | idx = intervals.searchsorted(approx_interval) 614 | interval = (intervals[idx - 1] if idx > 0 else intervals[0]) * 10**power 615 | if delta_x // interval > max_ticks: 616 | interval = intervals[idx] * 10**power 617 | ticks = [ 618 | float(t * interval) 619 | for t in np.arange(ceil(min_ / interval), max_ // interval + 1) 620 | ] 621 | decimals = -min(0, power) 622 | tick_labels = self.get_labels_for_ticks(ticks, decimals) 623 | return ticks, tick_labels 624 | 625 | def get_labels_for_ticks( 626 | self, ticks: list[float], decimals: int | None = None 627 | ) -> list[str]: 628 | """Generate formatted labels for given tick values. 629 | 630 | Args: 631 | ticks: A list of tick values to be formatted. 632 | decimals: The number of decimal places for formatting the tick values. 633 | 634 | Returns: 635 | A list of formatted tick labels as strings. 636 | """ 637 | if not ticks: 638 | return [] 639 | if decimals is None: 640 | if len(ticks) >= 2: 641 | power = floor(log10(ticks[1] - ticks[0])) 642 | else: 643 | power = 0 644 | decimals = -min(0, power) 645 | tick_labels = [f"{tick:.{decimals}f}" for tick in ticks] 646 | return tick_labels 647 | 648 | def combine_quad_with_pixel( 649 | self, quad: tuple[int, int, int, int], canvas: Canvas, x: int, y: int 650 | ) -> str: 651 | pixel = canvas.get_pixel(x, y)[0] 652 | for current_quad, v in BOX_CHARACTERS.items(): 653 | if v == pixel: 654 | break 655 | new_quad = combine_quads(current_quad, quad) 656 | return BOX_CHARACTERS[new_quad] 657 | 658 | def get_pixel_from_coordinate( 659 | self, x: float | np.floating, y: float | np.floating 660 | ) -> tuple[int, int]: 661 | assert ( 662 | scale_rectangle := self.query_one("#plot", Canvas).scale_rectangle 663 | ) is not None 664 | return map_coordinate_to_pixel( 665 | x, 666 | y, 667 | self._x_min, 668 | self._x_max, 669 | self._y_min, 670 | self._y_max, 671 | region=scale_rectangle, 672 | ) 673 | 674 | def get_hires_pixel_from_coordinate( 675 | self, x: float | np.floating, y: float | np.floating 676 | ) -> tuple[float | np.floating, float | np.floating]: 677 | assert ( 678 | scale_rectangle := self.query_one("#plot", Canvas).scale_rectangle 679 | ) is not None 680 | return map_coordinate_to_hires_pixel( 681 | x, 682 | y, 683 | self._x_min, 684 | self._x_max, 685 | self._y_min, 686 | self._y_max, 687 | region=scale_rectangle, 688 | ) 689 | 690 | def get_coordinate_from_pixel(self, x: int, y: int) -> tuple[float, float]: 691 | assert ( 692 | scale_rectangle := self.query_one("#plot", Canvas).scale_rectangle 693 | ) is not None 694 | return map_pixel_to_coordinate( 695 | x, 696 | y, 697 | self._x_min, 698 | self._x_max, 699 | self._y_min, 700 | self._y_max, 701 | region=scale_rectangle, 702 | ) 703 | 704 | def _zoom(self, event: MouseScrollDown | MouseScrollUp, factor: float) -> None: 705 | if not self._allow_pan_and_zoom: 706 | return 707 | if (offset := event.get_content_offset(self)) is not None: 708 | widget, _ = self.screen.get_widget_at(event.screen_x, event.screen_y) 709 | canvas = self.query_one("#plot", Canvas) 710 | assert canvas.scale_rectangle is not None 711 | if widget.id == "bottom-margin": 712 | offset = event.screen_offset - self.screen.get_offset(canvas) 713 | x, y = self.get_coordinate_from_pixel(offset.x, offset.y) 714 | if widget.id in ("plot", "bottom-margin"): 715 | self._auto_x_min = False 716 | self._auto_x_max = False 717 | self._x_min = (self._x_min + factor * x) / (1 + factor) 718 | self._x_max = (self._x_max + factor * x) / (1 + factor) 719 | if widget.id in ("plot", "left-margin"): 720 | self._auto_y_min = False 721 | self._auto_y_max = False 722 | self._y_min = (self._y_min + factor * y) / (1 + factor) 723 | self._y_max = (self._y_max + factor * y) / (1 + factor) 724 | self.post_message( 725 | self.ScaleChanged( 726 | self, self._x_min, self._x_max, self._y_min, self._y_max 727 | ) 728 | ) 729 | self._needs_rerender = True 730 | self.call_later(self.refresh) 731 | 732 | @on(MouseScrollDown) 733 | def zoom_in(self, event: MouseScrollDown) -> None: 734 | event.stop() 735 | self._zoom(event, ZOOM_FACTOR) 736 | 737 | @on(MouseScrollUp) 738 | def zoom_out(self, event: MouseScrollUp) -> None: 739 | event.stop() 740 | self._zoom(event, -ZOOM_FACTOR) 741 | 742 | @on(MouseMove) 743 | def pan_plot(self, event: MouseMove) -> None: 744 | if not self._allow_pan_and_zoom: 745 | return 746 | if event.button == 0: 747 | # If no button is pressed, don't drag. 748 | return 749 | 750 | x1, y1 = self.get_coordinate_from_pixel(1, 1) 751 | x2, y2 = self.get_coordinate_from_pixel(2, 2) 752 | dx, dy = x2 - x1, y1 - y2 753 | 754 | assert event.widget is not None 755 | if event.widget.id in ("plot", "bottom-margin"): 756 | self._auto_x_min = False 757 | self._auto_x_max = False 758 | self._x_min -= dx * event.delta_x 759 | self._x_max -= dx * event.delta_x 760 | if event.widget.id in ("plot", "left-margin"): 761 | self._auto_y_min = False 762 | self._auto_y_max = False 763 | self._y_min += dy * event.delta_y 764 | self._y_max += dy * event.delta_y 765 | self.post_message( 766 | self.ScaleChanged(self, self._x_min, self._x_max, self._y_min, self._y_max) 767 | ) 768 | self._needs_rerender = True 769 | self.call_later(self.refresh) 770 | 771 | def action_reset_scales(self) -> None: 772 | self.set_xlimits(self._user_x_min, self._user_x_max) 773 | self.set_ylimits(self._user_y_min, self._user_y_max) 774 | self.post_message( 775 | self.ScaleChanged(self, self._x_min, self._x_max, self._y_min, self._y_max) 776 | ) 777 | self.refresh() 778 | 779 | 780 | def map_coordinate_to_pixel( 781 | x: float | np.floating, 782 | y: float | np.floating, 783 | xmin: float, 784 | xmax: float, 785 | ymin: float, 786 | ymax: float, 787 | region: Region, 788 | ) -> tuple[int, int]: 789 | x = floor(linear_mapper(x, xmin, xmax, region.x, region.right)) 790 | # positive y direction is reversed 791 | y = ceil(linear_mapper(y, ymin, ymax, region.bottom - 1, region.y - 1)) 792 | return x, y 793 | 794 | 795 | def map_coordinate_to_hires_pixel( 796 | x: float | np.floating, 797 | y: float | np.floating, 798 | xmin: float, 799 | xmax: float, 800 | ymin: float, 801 | ymax: float, 802 | region: Region, 803 | ) -> tuple[float | np.floating, float | np.floating]: 804 | x = linear_mapper(x, xmin, xmax, region.x, region.right) 805 | # positive y direction is reversed 806 | y = linear_mapper(y, ymin, ymax, region.bottom, region.y) 807 | return x, y 808 | 809 | 810 | def map_pixel_to_coordinate( 811 | px: int, 812 | py: int, 813 | xmin: float, 814 | xmax: float, 815 | ymin: float, 816 | ymax: float, 817 | region: Region, 818 | ) -> tuple[float, float]: 819 | x = linear_mapper(px + 0.5, region.x, region.right, xmin, xmax) 820 | # positive y direction is reversed 821 | y = linear_mapper(py + 0.5, region.bottom, region.y, ymin, ymax) 822 | return float(x), float(y) 823 | 824 | 825 | def linear_mapper( 826 | x: float | np.floating | int, 827 | a: float | int, 828 | b: float | int, 829 | a_prime: float | int, 830 | b_prime: float | int, 831 | ) -> float | np.floating: 832 | return a_prime + (x - a) * (b_prime - a_prime) / (b - a) 833 | 834 | 835 | def drop_nans_and_infs(x: np.ndarray, y: np.ndarray) -> tuple[np.ndarray, np.ndarray]: 836 | """Drop NaNs and Infs from x and y arrays. 837 | 838 | Args: 839 | x: An array with the data values for the horizontal axis. 840 | y: An array with the data values for the vertical axis. 841 | 842 | Returns: 843 | A tuple of arrays with NaNs and Infs removed. 844 | """ 845 | mask = ~np.isnan(x) & ~np.isnan(y) & ~np.isinf(x) & ~np.isinf(y) 846 | return x[mask], y[mask] 847 | -------------------------------------------------------------------------------- /src/textual_plot/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfokkema/textual-plot/99680c4aa1f8667c98bd88c700885f7bf4e85b73/src/textual_plot/py.typed -------------------------------------------------------------------------------- /src/textual_plot/resources/morning-spectrum.csv: -------------------------------------------------------------------------------- 1 | Wavelength (nm),Intensity 2 | 196.2914015765968,61.166 3 | 196.75309374600835,93.9335 4 | 197.21473619557435,74.273 5 | 197.6763289233602,170.391 6 | 198.1378719274313,65.535 7 | 198.59936520585316,113.594 8 | 199.0608087566912,148.546 9 | 199.52220257801085,89.5645 10 | 199.98354666787762,211.8965 11 | 200.44484102435695,157.284 12 | 200.9060856455142,176.9445 13 | 201.36728052941493,176.9445 14 | 201.8284256741245,229.3725 15 | 202.2895210777084,200.974 16 | 202.7505667382321,192.236 17 | 203.21156265376098,205.343 18 | 203.67250882236056,242.47949999999997 19 | 204.13340524209622,211.8965 20 | 204.59425191103347,190.05149999999998 21 | 205.0550488272377,242.47949999999997 22 | 205.5157959887744,220.6345 23 | 205.976493393709,277.43149999999997 24 | 206.43714104010695,262.14 25 | 206.89773892603372,229.3725 26 | 207.35828704955475,220.6345 27 | 207.81878540873544,246.8485 28 | 208.27923400164127,283.985 29 | 208.7396328263377,244.664 30 | 209.19998188089016,200.974 31 | 209.66028116336415,249.033 32 | 210.12053067182498,297.092 33 | 210.58073040433825,209.712 34 | 211.04088035896936,187.867 35 | 211.50098053378375,240.295 36 | 211.9610309268468,166.022 37 | 212.4210315362241,190.05149999999998 38 | 212.88098235998095,183.498 39 | 213.3408833961829,176.9445 40 | 213.80073464289538,255.5865 41 | 214.2605360981838,262.14 42 | 214.72028776011362,240.295 43 | 215.17998962675028,163.83749999999998 44 | 215.6396416961593,211.8965 45 | 216.099243966406,218.45 46 | 216.55879643555596,229.3725 47 | 217.0182991016745,229.3725 48 | 217.47775196282723,227.188 49 | 217.93715501707942,270.878 50 | 218.3965082624966,249.033 51 | 218.85581169714425,205.343 52 | 219.3150653190878,131.07 53 | 219.77426912639268,244.664 54 | 220.23342311712432,181.31349999999998 55 | 220.6925272893482,168.2065 56 | 221.15158164112972,257.77099999999996 57 | 221.6105861705344,200.974 58 | 222.0695408756276,200.974 59 | 222.5284457544749,152.915 60 | 222.9873008051416,198.78949999999998 61 | 223.4461060256932,146.36149999999998 62 | 223.90486141419524,235.926 63 | 224.363566968713,144.177 64 | 224.82222268731203,124.5165 65 | 225.2808285680578,257.77099999999996 66 | 225.73938460901573,253.402 67 | 226.19789080825123,231.557 68 | 226.6563471638298,194.4205 69 | 227.1147536738168,216.26549999999997 70 | 227.5731103362778,174.76 71 | 228.03141714927818,176.9445 72 | 228.4896741108834,131.07 73 | 228.9478812191589,187.867 74 | 229.40603847217014,179.129 75 | 229.86414586798256,259.9555 76 | 230.32220340466156,163.83749999999998 77 | 230.78021108027266,203.1585 78 | 231.2381688928813,286.16949999999997 79 | 231.6960768405529,155.09949999999998 80 | 232.15393492135294,216.26549999999997 81 | 232.61174313334683,249.033 82 | 233.0695014746,172.57549999999998 83 | 233.52720994317795,281.8005 84 | 233.98486853714616,181.31349999999998 85 | 234.44247725456992,172.57549999999998 86 | 234.90003609351487,190.05149999999998 87 | 235.35754505204633,222.819 88 | 235.8150041282298,207.52749999999997 89 | 236.2724133201307,229.3725 90 | 236.72977262581452,179.129 91 | 237.18708204334666,216.26549999999997 92 | 237.64434157079262,211.8965 93 | 238.10155120621775,227.188 94 | 238.55871094768764,227.188 95 | 239.0158207932676,238.1105 96 | 239.4728807410232,268.6935 97 | 239.9298907890198,255.5865 98 | 240.38685093532285,225.00349999999997 99 | 240.8437611779978,235.926 100 | 241.3006215151102,257.77099999999996 101 | 241.7574319447254,249.033 102 | 242.21419246490882,161.653 103 | 242.67090307372595,159.4685 104 | 243.12756376924227,238.1105 105 | 243.5841745495232,325.4905 106 | 244.04073541263415,176.9445 107 | 244.49724635664063,242.47949999999997 108 | 244.95370737960803,196.605 109 | 245.4101184796019,246.8485 110 | 245.86647965468754,235.926 111 | 246.3227909029305,179.129 112 | 246.7790522223962,203.1585 113 | 247.2352636111501,246.8485 114 | 247.69142506725763,187.867 115 | 248.14753658878422,216.26549999999997 116 | 248.60359817379538,211.8965 117 | 249.05960982035648,161.653 118 | 249.51557152653302,216.26549999999997 119 | 249.97148329039047,257.77099999999996 120 | 250.42734510999418,139.808 121 | 250.88315698340966,192.236 122 | 251.3389189087024,225.00349999999997 123 | 251.79463088393783,242.47949999999997 124 | 252.25029290718126,187.867 125 | 252.70590497649835,231.557 126 | 253.16146708995439,200.974 127 | 253.61697924561494,290.5385 128 | 254.07244144154538,168.2065 129 | 254.52785367581112,192.236 130 | 254.98321594647769,211.8965 131 | 255.4385282516105,220.6345 132 | 255.89379058927503,286.16949999999997 133 | 256.34900295753664,238.1105 134 | 256.80416535446085,259.9555 135 | 257.25927777811313,196.605 136 | 257.7143402265589,214.081 137 | 258.16935269786364,190.05149999999998 138 | 258.6243151900926,229.3725 139 | 259.07922770131154,187.867 140 | 259.53409022958573,174.76 141 | 259.98890277298057,176.9445 142 | 260.44366532956155,214.081 143 | 260.89837789739425,194.4205 144 | 261.353040474544,218.45 145 | 261.80765305907624,266.509 146 | 262.2622156490565,198.78949999999998 147 | 262.71672824255,183.498 148 | 263.1711908376225,172.57549999999998 149 | 263.6256034323393,174.76 150 | 264.0799660247657,170.391 151 | 264.5342786129674,179.129 152 | 264.9885411950098,251.21749999999997 153 | 265.4427537689582,198.78949999999998 154 | 265.8969163328782,255.5865 155 | 266.3510288848352,240.295 156 | 266.8050914228946,251.21749999999997 157 | 267.2591039451219,249.033 158 | 267.7130664495825,264.3245 159 | 268.1669789343419,242.47949999999997 160 | 268.6208413974655,238.1105 161 | 269.0746538370188,159.4685 162 | 269.5284162510672,235.926 163 | 269.98212863767617,255.5865 164 | 270.4357909949112,220.6345 165 | 270.88940332083763,227.188 166 | 271.342965613521,251.21749999999997 167 | 271.79647787102675,168.2065 168 | 272.2499400914202,235.926 169 | 272.703352272767,259.9555 170 | 273.15671441313253,207.52749999999997 171 | 273.6100265105822,259.9555 172 | 274.0632885631814,205.343 173 | 274.5165005689957,266.509 174 | 274.9696625260905,218.45 175 | 275.4227744325311,270.878 176 | 275.8758362863833,220.6345 177 | 276.32884808571214,225.00349999999997 178 | 276.78180982858333,216.26549999999997 179 | 277.2347215130623,200.974 180 | 277.68758313721446,227.188 181 | 278.1403946991052,242.47949999999997 182 | 278.5931561968,211.8965 183 | 279.04586762836436,209.712 184 | 279.4985289918636,253.402 185 | 279.9511402853633,172.57549999999998 186 | 280.4037015069289,168.2065 187 | 280.8562126546258,209.712 188 | 281.3086737265195,244.664 189 | 281.76108472067534,157.284 190 | 282.21344563515885,240.295 191 | 282.6657564680354,257.77099999999996 192 | 283.1180172173706,308.0145 193 | 283.57022788122975,205.343 194 | 284.02238845767835,244.664 195 | 284.4744989447818,227.188 196 | 284.92655934060565,229.3725 197 | 285.37856964321526,233.74149999999997 198 | 285.83052985067616,196.605 199 | 286.2824399610537,222.819 200 | 286.73429997241334,270.878 201 | 287.1861098828206,209.712 202 | 287.6378696903408,211.8965 203 | 288.0895793930395,166.022 204 | 288.5412389889822,262.14 205 | 288.9928484762342,310.19899999999996 206 | 289.44440785286105,229.3725 207 | 289.89591711692816,203.1585 208 | 290.34737626650093,174.76 209 | 290.7987852996449,242.47949999999997 210 | 291.25014421442546,246.8485 211 | 291.70145300890806,183.498 212 | 292.15271168115817,174.76 213 | 292.6039202292413,205.343 214 | 293.05507865122274,192.236 215 | 293.50618694516805,222.819 216 | 293.95724510914266,207.52749999999997 217 | 294.40825314121196,246.8485 218 | 294.8592110394415,214.081 219 | 295.3101188018967,176.9445 220 | 295.76097642664286,211.8965 221 | 296.21178391174567,157.284 222 | 296.6625412552704,262.14 223 | 297.1132484552826,231.557 224 | 297.56390550984764,159.4685 225 | 298.01451241703097,200.974 226 | 298.4650691748981,209.712 227 | 298.9155757815144,249.033 228 | 299.3660322349454,192.236 229 | 299.81643853325653,266.509 230 | 300.26679467451316,207.52749999999997 231 | 300.7171006567809,229.3725 232 | 301.167356478125,220.6345 233 | 301.617562136611,233.74149999999997 234 | 302.06771763030446,262.14 235 | 302.51782295727054,176.9445 236 | 302.967878115575,244.664 237 | 303.4178831032831,218.45 238 | 303.8678379184603,238.1105 239 | 304.31774255917213,231.557 240 | 304.767597023484,187.867 241 | 305.21740130946137,233.74149999999997 242 | 305.6671554151696,190.05149999999998 243 | 306.11685933867426,229.3725 244 | 306.5665130780407,194.4205 245 | 307.01611663133446,235.926 246 | 307.46566999662093,161.653 247 | 307.91517317196553,211.8965 248 | 308.36462615543377,262.14 249 | 308.8140289450911,222.819 250 | 309.2633815390029,229.3725 251 | 309.7126839352347,275.24699999999996 252 | 310.16193613185175,238.1105 253 | 310.6111381269198,227.188 254 | 311.06028991850417,205.343 255 | 311.5093915046702,187.867 256 | 311.95844288348343,253.402 257 | 312.4074440530094,192.236 258 | 312.85639501131334,246.8485 259 | 313.3052957564609,242.47949999999997 260 | 313.7541462865174,207.52749999999997 261 | 314.2029465995484,205.343 262 | 314.65169669361916,207.52749999999997 263 | 315.1003965667954,181.31349999999998 264 | 315.54904621714223,216.26549999999997 265 | 315.9976456427254,233.74149999999997 266 | 316.4461948416103,214.081 267 | 316.8946938118622,281.8005 268 | 317.34314255154675,233.74149999999997 269 | 317.79154105872925,205.343 270 | 318.23988933147524,179.129 271 | 318.6881873678501,231.557 272 | 319.13643516591935,216.26549999999997 273 | 319.5846327237484,286.16949999999997 274 | 320.0327800394027,262.14 275 | 320.4808771109478,209.712 276 | 320.92892393644894,253.402 277 | 321.3769205139717,220.6345 278 | 321.82486684158147,181.31349999999998 279 | 322.27276291734375,259.9555 280 | 322.72060873932395,275.24699999999996 281 | 323.16840430558756,253.402 282 | 323.61614961419997,190.05149999999998 283 | 324.0638446632267,172.57549999999998 284 | 324.51148945073317,198.78949999999998 285 | 324.9590839747848,192.236 286 | 325.40662823344707,211.8965 287 | 325.8541222247854,297.092 288 | 326.30156594686514,192.236 289 | 326.748959397752,181.31349999999998 290 | 327.1963025755111,198.78949999999998 291 | 327.64359547820817,257.77099999999996 292 | 328.0908381039086,190.05149999999998 293 | 328.53803045067775,211.8965 294 | 328.9851725165811,227.188 295 | 329.4322642996841,257.77099999999996 296 | 329.87930579805214,233.74149999999997 297 | 330.32629700975076,225.00349999999997 298 | 330.77323793284546,218.45 299 | 331.2201285654014,238.1105 300 | 331.6669689054844,187.867 301 | 332.1137589511597,176.9445 302 | 332.5604987004928,211.8965 303 | 333.00718815154914,264.3245 304 | 333.45382730239413,242.47949999999997 305 | 333.9004161510932,218.45 306 | 334.34695469571193,229.3725 307 | 334.7934429343156,174.76 308 | 335.2398808649698,264.3245 309 | 335.6862684857399,200.974 310 | 336.13260579469136,209.712 311 | 336.57889278988966,297.092 312 | 337.0251294694002,222.819 313 | 337.4713158312885,198.78949999999998 314 | 337.9174518736198,257.77099999999996 315 | 338.3635375944599,244.664 316 | 338.8095729918739,209.712 317 | 339.2555580639275,225.00349999999997 318 | 339.70149280868594,238.1105 319 | 340.14737722421484,231.557 320 | 340.59321130857967,235.926 321 | 341.03899505984566,273.0625 322 | 341.48472847607843,268.6935 323 | 341.93041155534337,235.926 324 | 342.3760442957059,275.24699999999996 325 | 342.82162669523154,323.306 326 | 343.26715875198573,209.712 327 | 343.71264046403394,277.43149999999997 328 | 344.15807182944155,277.43149999999997 329 | 344.60345284627397,246.8485 330 | 345.04878351259674,288.354 331 | 345.49406382647527,266.509 332 | 345.939293785975,253.402 333 | 346.3844733891614,281.8005 334 | 346.8296026340999,229.3725 335 | 347.27468151885597,216.26549999999997 336 | 347.7197100414951,323.306 337 | 348.16468820008254,283.985 338 | 348.60961599268404,312.38349999999997 339 | 349.05449341736477,218.45 340 | 349.49932047219033,229.3725 341 | 349.94409715522613,353.889 342 | 350.3888234645376,266.509 343 | 350.8334993981902,242.47949999999997 344 | 351.2781249542494,303.64549999999997 345 | 351.72270013078065,351.7045 346 | 352.1672249258494,301.461 347 | 352.61169933752103,318.937 348 | 353.0561233638611,325.4905 349 | 353.5004970029349,275.24699999999996 350 | 353.944820252808,334.2285 351 | 354.3890931115458,395.3945 352 | 354.83331557721374,338.59749999999997 353 | 355.2774876478774,366.996 354 | 355.72160932160205,349.52 355 | 356.1656805964532,353.889 356 | 356.60970147049636,375.734 357 | 357.05367194179695,308.0145 358 | 357.4975920084202,332.044 359 | 357.94146166843194,318.937 360 | 358.3852809198973,316.7525 361 | 358.8290497608819,332.044 362 | 359.2727681894512,349.52 363 | 359.7164362036705,366.996 364 | 360.16005380160544,439.0845 365 | 360.6036209813213,395.3945 366 | 361.04713774088356,366.996 367 | 361.4906040783577,329.85949999999997 368 | 361.93401999180924,386.6565 369 | 362.3773854793035,397.57899999999995 370 | 362.82070053890595,404.1325 371 | 363.2639651686821,417.23949999999996 372 | 363.7071793666974,397.57899999999995 373 | 364.1503431310173,428.162 374 | 364.5934564597071,432.53099999999995 375 | 365.03651935083235,456.5605 376 | 365.4795318024586,493.697 377 | 365.9224938126511,465.2985 378 | 366.3654053794755,526.4644999999999 379 | 366.80826650099715,576.708 380 | 367.25107717528147,535.2025 381 | 367.69383740039393,484.95899999999995 382 | 368.1365471744,502.43499999999995 383 | 368.5792064953651,415.05499999999995 384 | 369.02181536135464,570.1545 385 | 369.4643737704342,605.1065 386 | 369.9068817206691,526.4644999999999 387 | 370.3493392101248,524.28 388 | 370.79174623686686,561.4164999999999 389 | 371.2341027989606,552.6785 390 | 371.6764088944715,565.7855 391 | 372.118664521465,530.8335 392 | 372.5608696780066,607.2909999999999 393 | 373.0030243621617,635.6895 394 | 373.4451285719958,565.7855 395 | 373.88718230557424,522.0955 396 | 374.32918556096257,572.3389999999999 397 | 374.7711383362262,559.232 398 | 375.2130406294307,559.232 399 | 375.6548924386413,609.4755 400 | 376.0966937619236,513.3575 401 | 376.53844459734296,688.1175 402 | 376.9801449429648,644.4275 403 | 377.4217947968547,694.6709999999999 404 | 377.863394157078,618.2135 405 | 378.30494302170024,696.8555 406 | 378.74644138878676,723.0695 407 | 379.18788925640314,609.4755 408 | 379.6292866226147,633.505 409 | 380.07063348548695,602.922 410 | 380.5119298430853,624.7669999999999 411 | 380.95317569347526,640.0585 412 | 381.39437103472216,648.7964999999999 413 | 381.8355158648916,646.612 414 | 382.27661018204896,626.9515 415 | 382.71765398425964,581.077 416 | 383.1586472695892,611.66 417 | 383.599590036103,522.0955 418 | 384.0404822818665,539.5715 419 | 384.4813240049451,589.8149999999999 420 | 384.92211520340436,683.7484999999999 421 | 385.3628558753096,668.457 422 | 385.80354601872637,677.1949999999999 423 | 386.2441856317201,685.933 424 | 386.6847747123562,716.516 425 | 387.1253132587002,753.6524999999999 426 | 387.5658012688175,740.5455 427 | 388.0062387407734,790.789 428 | 388.44662567263356,699.04 429 | 388.88696206246334,760.2059999999999 430 | 389.3272479083282,731.8075 431 | 389.7674832082936,830.1099999999999 432 | 390.207667960425,893.4604999999999 433 | 390.6478021627878,906.5675 434 | 391.08788581344743,921.8589999999999 435 | 391.5279189104694,941.5195 436 | 391.96790145191915,974.2869999999999 437 | 392.4078334358621,865.0619999999999 438 | 392.8477148603637,889.0915 439 | 393.28754572348936,847.586 440 | 393.7273260233046,882.538 441 | 394.1670557578749,729.6229999999999 442 | 394.6067349252656,841.0324999999999 443 | 395.0463635235422,978.656 444 | 395.4859415507702,1017.977 445 | 395.92546900501486,969.9179999999999 446 | 396.36494588434186,865.0619999999999 447 | 396.80437218681647,956.8109999999999 448 | 397.2437479105043,991.7629999999999 449 | 397.6830730534707,1074.774 450 | 398.1223476137811,1063.8515 451 | 398.561571589501,1155.6005 452 | 399.0007449786958,1317.2535 453 | 399.43986777943104,1404.6335 454 | 399.87893998977194,1478.9064999999998 455 | 400.31796160778424,1509.4895 456 | 400.7569326315332,1575.0245 457 | 401.19585305908436,1566.2865 458 | 401.6347228885031,1658.0355 459 | 402.0735421178549,1719.2015 460 | 402.51231074520524,1697.3564999999999 461 | 402.95102876861955,1682.0649999999998 462 | 403.38969618616323,1664.589 463 | 403.82831299590174,1649.2975 464 | 404.2668791959006,1638.375 465 | 404.70539478422506,1865.5629999999999 466 | 405.14385975894083,1817.504 467 | 405.5822741181132,1747.6 468 | 406.0206378598077,1834.98 469 | 406.45895098208973,1867.7475 470 | 406.89721348302476,1832.7955 471 | 407.3354253606782,1920.1754999999998 472 | 407.77358661311536,1911.4375 473 | 408.21169723840205,1976.9724999999999 474 | 408.6497572346034,2014.109 475 | 409.08776659978497,1987.895 476 | 409.52572533201226,2079.644 477 | 409.9636334293507,2103.6735 478 | 410.40149088986567,2177.9465 479 | 410.83929771162263,2197.607 480 | 411.277053892687,2145.179 481 | 411.7147594311243,2252.2194999999997 482 | 412.15241432499994,2335.2304999999997 483 | 412.59001857237945,2374.5515 484 | 413.0275721713282,2302.4629999999997 485 | 413.4650751199116,2361.4445 486 | 413.9025274161952,2416.057 487 | 414.33992905824437,2485.961 488 | 414.7772800441245,2490.33 489 | 415.2145803719012,2457.5625 490 | 415.6518300396398,2564.603 491 | 416.08902904540577,2562.4184999999998 492 | 416.5261773872646,2612.662 493 | 416.9632750632817,2654.1675 494 | 417.4003220715225,2551.496 495 | 417.8373184100525,2728.4404999999997 496 | 418.27426407693713,2778.6839999999997 497 | 418.7111590702418,2706.5955 498 | 419.14800338803195,2852.957 499 | 419.58479702837315,2804.8979999999997 500 | 420.0215399893306,2763.3925 501 | 420.45823226897005,2780.8685 502 | 420.8948738653568,2809.267 503 | 421.3314647765563,2955.6285 504 | 421.768005000634,3001.5029999999997 505 | 422.2044945356553,3104.1744999999996 506 | 422.6409333796857,2999.3185 507 | 423.0773215307906,3141.3109999999997 508 | 423.51365898703557,3099.8055 509 | 423.9499457464859,3115.0969999999998 510 | 424.3861818072072,3204.6614999999997 511 | 424.8223671672648,3246.167 512 | 425.2585018247242,3198.1079999999997 513 | 425.69458577765084,3145.68 514 | 426.1306190241101,3252.7205 515 | 426.5666015621675,3265.8275 516 | 427.00253338988847,3261.4584999999997 517 | 427.43841450533847,3309.5175 518 | 427.8742449065829,3243.9824999999996 519 | 428.3100245916873,3143.4955 520 | 428.74575355871707,3154.4179999999997 521 | 429.1814318057376,3195.9235 522 | 429.6170593308144,3178.4474999999998 523 | 430.05263613201294,3032.086 524 | 430.48816220739855,2892.278 525 | 430.9236375550368,3043.0085 526 | 431.3590621729931,3189.37 527 | 431.7944360593329,3182.8165 528 | 432.2297592121216,3281.1189999999997 529 | 432.6650316294248,3462.4325 530 | 433.10025330930773,3512.676 531 | 433.53542424983607,3497.3844999999997 532 | 433.970544449075,3431.8495 533 | 434.40561390509015,3440.5874999999996 534 | 434.84063261594696,3610.9784999999997 535 | 435.2756005797108,3488.6465 536 | 435.7105177944472,3639.377 537 | 436.1453842582216,3796.6609999999996 538 | 436.58019996909934,3591.3179999999998 539 | 437.01496492514605,3713.6499999999996 540 | 437.44967912442695,3654.6684999999998 541 | 437.8843425650076,3746.4175 542 | 438.3189552449536,3680.8824999999997 543 | 438.7535171623302,3763.8934999999997 544 | 439.18802831520287,3698.3585 545 | 439.6224887016371,3667.7754999999997 546 | 440.0568983196983,3676.5135 547 | 440.49125716745203,3698.3585 548 | 440.9255652429636,3835.982 549 | 441.35982254429854,4034.7715 550 | 441.7940290695222,3901.517 551 | 442.22818481670015,4082.8304999999996 552 | 442.66228978389785,4006.3729999999996 553 | 443.09634396918057,3991.0815 554 | 443.53034737061387,3999.8194999999996 555 | 443.96429998626326,4093.7529999999997 556 | 444.3982018141941,4089.384 557 | 444.8320528524719,4109.0445 558 | 445.265853099162,4109.0445 559 | 445.69960255232996,4019.4799999999996 560 | 446.13330121004117,3969.2365 561 | 446.5669490703611,4176.764 562 | 447.0005461313551,4141.812 563 | 447.4340923910889,4272.882 564 | 447.86758784762765,4202.978 565 | 448.3010324990369,4253.2215 566 | 448.73442634338215,4432.3505 567 | 449.1677693787287,4406.1365 568 | 449.60106160314217,4476.0405 569 | 450.0343030146879,4465.1179999999995 570 | 450.4674936114314,4626.771 571 | 450.900633391438,4441.0885 572 | 451.3337223527734,4500.07 573 | 451.7667604935028,4644.246999999999 574 | 452.19974781169174,4663.907499999999 575 | 452.6326843054056,4576.5275 576 | 453.0655699727099,4491.331999999999 577 | 453.4984048116702,4681.3835 578 | 453.9311888203517,4613.664 579 | 454.36392199682,4438.9039999999995 580 | 454.7966043391405,4670.460999999999 581 | 455.2292358453787,4578.7119999999995 582 | 455.6618165136,4777.501499999999 583 | 456.0943463418699,4749.103 584 | 456.5268253282537,4812.4535 585 | 456.95925347081703,4987.2135 586 | 457.39163076762526,4930.416499999999 587 | 457.82395721674385,5157.6044999999995 588 | 458.25623281623825,4884.5419999999995 589 | 458.6884575641738,5002.505 590 | 459.1206314586161,4906.387 591 | 459.55275449763053,5179.4495 592 | 459.9848266792826,4801.531 593 | 460.4168480016377,4871.4349999999995 594 | 460.8488184627612,4947.8925 595 | 461.2807380607187,4982.8445 596 | 461.7126067935756,5192.5565 597 | 462.14442465939726,5201.2945 598 | 462.57619165624925,5089.884999999999 599 | 463.0079077821969,5308.335 600 | 463.4395730353058,5151.0509999999995 601 | 463.8711874136413,5102.992 602 | 464.3027509152688,5127.0215 603 | 464.73426353825386,5135.7595 604 | 465.16572528066183,5063.670999999999 605 | 465.5971361405583,5096.438499999999 606 | 466.0284961160085,5006.874 607 | 466.4598052050781,4921.6785 608 | 466.8910634058325,4912.9405 609 | 467.32227071633696,4893.28 610 | 467.7534271346571,5024.349999999999 611 | 468.1845326588584,4982.8445 612 | 468.6155872870062,5192.5565 613 | 469.046591017166,5120.468 614 | 469.47754384740324,5068.04 615 | 469.90844577578326,4862.697 616 | 470.33929680037176,5201.2945 617 | 470.77009691923394,5168.527 618 | 471.2008461304354,5236.246499999999 619 | 471.6315444320415,5153.2355 620 | 472.06219182211777,5435.036 621 | 472.4927882987296,5244.9845 622 | 472.92333385994243,5424.1134999999995 623 | 473.3538285038217,5721.2055 624 | 473.7842722284329,5568.2905 625 | 474.21466503184143,5570.474999999999 626 | 474.6450069121128,5721.2055 627 | 475.07529786731243,5660.0395 628 | 475.50553789550577,5834.7995 629 | 475.93572699475817,5710.282999999999 630 | 476.36586516313525,5930.9175 631 | 476.7959523987024,6007.375 632 | 477.225988699525,6206.1645 633 | 477.65597406366857,6042.326999999999 634 | 478.0859084891985,6138.445 635 | 478.5157919741803,6553.5 636 | 478.94562451667935,6573.1605 637 | 479.375406114761,6693.308 638 | 479.805136766491,6667.094 639 | 480.23481646993463,6852.7765 640 | 480.66444522315726,7138.946 641 | 481.09402302422444,7012.245 642 | 481.52354987120157,7058.1195 643 | 481.95302576215414,7103.994 644 | 482.3824506951476,7106.1785 645 | 482.8118246682473,7396.717 646 | 483.2411476795188,7460.067499999999 647 | 483.67041972702754,7178.267 648 | 484.0996408088389,7505.942 649 | 484.5288109230184,7200.111999999999 650 | 484.9579300676313,7298.4145 651 | 485.38699824074337,7071.2265 652 | 485.81601544041985,7152.053 653 | 486.2449816647262,6892.0975 654 | 486.6738969117278,7018.7985 655 | 487.10276117949024,7003.507 656 | 487.531574466079,7114.916499999999 657 | 487.9603367695594,7396.717 658 | 488.3890480879969,7503.7575 659 | 488.81770841945695,7501.572999999999 660 | 489.2463177620051,7829.248 661 | 489.6748761137067,7881.6759999999995 662 | 490.10338347262723,8001.8234999999995 663 | 490.53183983683203,7984.3475 664 | 490.96024520438675,7938.473 665 | 491.3885995733567,8021.4839999999995 666 | 491.8169029418074,8056.436 667 | 492.2451553078042,7964.687 668 | 492.6733566694126,7969.056 669 | 493.10150702469815,7993.085499999999 670 | 493.5296063717261,7984.3475 671 | 493.9576547085621,8038.959999999999 672 | 494.3856520332714,8093.572499999999 673 | 494.8135983439196,8244.303 674 | 495.2414936385721,8093.572499999999 675 | 495.66933791529425,8119.786499999999 676 | 496.0971311721516,8176.5835 677 | 496.52487340720967,8036.7755 678 | 496.95256461853376,8224.6425 679 | 497.3802048041894,8115.4175 680 | 497.80779396224204,8218.089 681 | 498.2353320907571,8054.251499999999 682 | 498.6628191878,8126.339999999999 683 | 499.09025525143625,7973.424999999999 684 | 499.51764027973127,7997.4545 685 | 499.94497427075044,7896.9675 686 | 500.3722572225593,7949.3955 687 | 500.7994891332233,7923.1815 688 | 501.22667000080787,7829.248 689 | 501.65379982337834,7779.0045 690 | 502.08087859900047,7730.9455 691 | 502.50790632573927,7840.170499999999 692 | 502.9348830016607,7754.974999999999 693 | 503.3618086248296,7787.742499999999 694 | 503.78868319331195,7833.616999999999 695 | 504.2155067051729,8006.192499999999 696 | 504.64227915847806,7960.317999999999 697 | 505.0690005512928,8056.436 698 | 505.49567088168243,7997.4545 699 | 505.92229014771283,8082.65 700 | 506.34885834744887,8091.388 701 | 506.7753754789565,8108.864 702 | 507.20184154030073,8076.0965 703 | 507.6282565295474,8012.745999999999 704 | 508.0546204447618,8170.03 705 | 508.4809332840093,7903.521 706 | 508.9071950453555,8052.067 707 | 509.3334057268655,8191.875 708 | 509.75956532660535,8073.911999999999 709 | 510.18567384263997,8095.757 710 | 510.6117312730351,8305.469 711 | 511.037737615856,8261.779 712 | 511.4636928691682,8432.17 713 | 511.88959703103717,8316.3915 714 | 512.3154500995283,8475.859999999999 715 | 512.7412520727072,8351.343499999999 716 | 513.1670029486389,8392.849 717 | 513.5927027253895,8148.1849999999995 718 | 514.0183514010239,8233.3805 719 | 514.4439489736078,8353.528 720 | 514.8694954412065,8130.709 721 | 515.2949908018855,8154.7384999999995 722 | 515.7204350537104,8095.757 723 | 516.1458281947465,7929.735 724 | 516.5711702230592,7925.366 725 | 516.996461136714,7813.956499999999 726 | 517.4217009337766,7789.927 727 | 517.8468896123119,7615.1669999999995 728 | 518.272027170386,7737.499 729 | 518.6971136060637,7962.5025 730 | 519.1221489174109,7912.259 731 | 519.5471331024927,8261.779 732 | 519.972066159375,8126.339999999999 733 | 520.396948086123,8342.6055 734 | 520.821778880802,8576.347 735 | 521.2465585414777,8521.734499999999 736 | 521.6712870662153,8488.966999999999 737 | 522.0959644530805,8624.405999999999 738 | 522.5205907001387,8488.966999999999 739 | 522.9451658054553,8532.657 740 | 523.3696897670957,8545.764 741 | 523.7941625831253,8722.708499999999 742 | 524.2185842516096,8818.8265 743 | 524.6429547706141,8866.8855 744 | 525.0672741382044,8919.3135 745 | 525.4915423524455,8679.0185 746 | 525.9157594114032,8460.5685 747 | 526.339925313143,8353.528 748 | 526.7640400557302,8213.72 749 | 527.1881036372301,8290.1775 750 | 527.6121160557084,8231.196 751 | 528.0360773092306,8537.026 752 | 528.4599873958617,8528.288 753 | 528.8838463136677,8713.9705 754 | 529.3076540607137,8687.7565 755 | 529.7314106350653,8722.708499999999 756 | 530.1551160347879,8829.749 757 | 530.5787702579469,8949.896499999999 758 | 531.0023733026078,9000.14 759 | 531.425925166836,8777.321 760 | 531.849425848697,8637.512999999999 761 | 532.2728753462562,8648.4355 762 | 532.6962736575792,8371.003999999999 763 | 533.1196207807312,8329.4985 764 | 533.5429167137779,8432.17 765 | 533.9661614547846,8543.5795 766 | 534.3893550018167,8689.940999999999 767 | 534.8124973529397,8462.752999999999 768 | 535.235588506219,8646.251 769 | 535.6586284597204,8565.4245 770 | 536.0816172115087,8491.1515 771 | 536.5045547596501,8458.384 772 | 536.9274411022094,8493.336 773 | 537.3502762372524,8301.1 774 | 537.7730601628444,8395.0335 775 | 538.1957928770508,8309.838 776 | 538.6184743779373,8373.1885 777 | 539.0411046635691,8495.520499999999 778 | 539.463683732012,8200.613 779 | 539.8862115813307,8062.9895 780 | 540.3086882095917,8023.6685 781 | 540.7311136148595,8060.804999999999 782 | 541.1534877951999,8117.602 783 | 541.5758107486787,8049.8825 784 | 541.9980824733607,8124.1555 785 | 542.420302967312,8034.590999999999 786 | 542.8424722285973,8014.9304999999995 787 | 543.2645902552829,7986.531999999999 788 | 543.6866570454335,8132.893499999999 789 | 544.1086725971152,8025.852999999999 790 | 544.5306369083927,7949.3955 791 | 544.9525499773321,7940.657499999999 792 | 545.3744118019986,8017.115 793 | 545.7962223804576,8006.192499999999 794 | 546.2179817107747,8062.9895 795 | 546.6396897910151,8010.5615 796 | 547.0613466192447,7875.1224999999995 797 | 547.4829521935283,7768.081999999999 798 | 547.9045065119319,7881.6759999999995 799 | 548.3260095725205,7846.723999999999 800 | 548.7474613733601,8062.9895 801 | 549.1688619125157,8023.6685 802 | 549.5902111880529,7966.871499999999 803 | 550.0115091980371,8049.8825 804 | 550.4327559405336,8028.037499999999 805 | 550.8539514136085,8036.7755 806 | 551.2750956153263,7831.4325 807 | 551.6961885437531,7809.5875 808 | 552.1172301969542,8062.9895 809 | 552.538220572995,7993.085499999999 810 | 552.959159669941,7951.58 811 | 553.3800474858575,7881.6759999999995 812 | 553.8008840188103,8019.299499999999 813 | 554.2216692668642,8091.388 814 | 554.6424032280855,7918.8125 815 | 555.0630859005389,7872.937999999999 816 | 555.4837172822903,7910.0745 817 | 555.9042973714049,7816.141 818 | 556.3248261659484,7720.022999999999 819 | 556.745303663986,7912.259 820 | 557.1657298635832,7807.402999999999 821 | 557.5861047628056,7792.1115 822 | 558.0064283597183,7593.321999999999 823 | 558.4267006523874,7591.1375 824 | 558.8469216388775,7420.746499999999 825 | 559.2670913172547,7538.7095 826 | 559.6872096855841,7612.982499999999 827 | 560.1072767419314,7510.311 828 | 560.527292484362,7516.8645 829 | 560.947256910941,7403.2705 830 | 561.3671700197343,7436.038 831 | 561.787031808807,7647.934499999999 832 | 562.2068422762251,7527.786999999999 833 | 562.6266014200532,7623.905 834 | 563.0463092383576,7578.0305 835 | 563.465965729203,7405.455 836 | 563.8855708906555,7429.4845 837 | 564.3051247207801,7536.525 838 | 564.7246272176424,7473.174499999999 839 | 565.1440783793079,7298.4145 840 | 565.5634782038419,7433.853499999999 841 | 565.9828266893102,7261.277999999999 842 | 566.4021238337776,7425.1155 843 | 566.8213696353101,7353.027 844 | 567.240564091973,7390.1635 845 | 567.6597072018318,7427.299999999999 846 | 568.0787989629519,7280.938499999999 847 | 568.4978393733985,7342.1044999999995 848 | 568.9168284312375,7379.241 849 | 569.335766134534,7383.61 850 | 569.7546524813537,7141.130499999999 851 | 570.1734874697618,7211.0345 852 | 570.5922710978238,7136.7615 853 | 571.0110033636054,7051.566 854 | 571.4296842651717,7123.6545 855 | 571.8483138005884,7114.916499999999 856 | 572.2668919679206,7001.322499999999 857 | 572.6854187652344,7239.433 858 | 573.1038941905946,7215.403499999999 859 | 573.5223182420672,7066.8575 860 | 573.9406909177169,6942.340999999999 861 | 574.3590122156098,7069.0419999999995 862 | 574.7772821338112,6820.009 863 | 575.1955006703864,6586.2675 864 | 575.613667823401,6948.894499999999 865 | 576.0317835909203,6669.278499999999 866 | 576.44984797101,6597.19 867 | 576.8678609617352,6601.558999999999 868 | 577.2858225611617,6439.906 869 | 577.7037327673545,6490.1494999999995 870 | 578.1215915783796,6520.7325 871 | 578.539398992302,6474.857999999999 872 | 578.9571550071875,6365.633 873 | 579.3748596211011,6551.3155 874 | 579.7925128321086,6339.419 875 | 580.2101146382755,6573.1605 876 | 580.6276650376669,6422.429999999999 877 | 581.0451640283486,6356.8949999999995 878 | 581.4626116083858,6380.9245 879 | 581.8800077758442,6308.835999999999 880 | 582.297352528789,6306.6515 881 | 582.7146458652858,6398.4005 882 | 583.1318877834001,6466.12 883 | 583.5490782811969,6339.419 884 | 583.9662173567424,6313.205 885 | 584.3833050081013,6514.179 886 | 584.8003412333394,6208.348999999999 887 | 585.2173260305224,6151.552 888 | 585.6342593977153,6147.183 889 | 586.0511413329837,5874.1205 890 | 586.4679718343931,6160.29 891 | 586.884750900009,6134.076 892 | 587.3014785278966,6027.0355 893 | 587.7181547161217,5865.3825 894 | 588.1347794627494,5662.224 895 | 588.5513527658453,5572.6595 896 | 588.9678746234749,5579.213 897 | 589.3843450337038,5240.6155 898 | 589.8007639945971,5308.335 899 | 590.2171315042203,5465.619 900 | 590.6334475606392,5609.795999999999 901 | 591.0497121619188,5627.272 902 | 591.4659253061249,5511.4935 903 | 591.8820869913226,5577.028499999999 904 | 592.2981972155776,5533.3385 905 | 592.7142559769553,5542.0765 906 | 593.1302632735211,5563.9214999999995 907 | 593.5462191033406,5559.5525 908 | 593.9621234644791,5555.1835 909 | 594.3779763550021,5522.415999999999 910 | 594.7937777729749,5511.4935 911 | 595.2095277164632,5550.8144999999995 912 | 595.6252261835322,5653.486 913 | 596.0408731722475,5592.32 914 | 596.4564686806746,5535.523 915 | 596.8720127068788,5714.652 916 | 597.2875052489256,5649.116999999999 917 | 597.7029463048804,5710.282999999999 918 | 598.1183358728089,5721.2055 919 | 598.5336739507761,5701.545 920 | 598.9489605368481,5839.1685 921 | 599.3641956290895,5769.264499999999 922 | 599.7793792255665,5852.2755 923 | 600.194511324344,5788.924999999999 924 | 600.6095919234878,5791.1095 925 | 601.0246210210634,5987.7145 926 | 601.4395986151358,5981.161 927 | 601.854524703771,6031.4045 928 | 602.269399285034,5935.286499999999 929 | 602.6842223569905,6024.851 930 | 603.0989939177058,6064.172 931 | 603.5137139652455,5885.043 932 | 603.928382497675,6118.7845 933 | 604.3429995130597,6120.969 934 | 604.757565009465,5930.9175 935 | 605.1720789849564,6024.851 936 | 605.5865414375995,5944.0244999999995 937 | 606.0009523654595,5880.674 938 | 606.4153117666021,5915.625999999999 939 | 606.8296196390924,5974.6075 940 | 607.2438759809962,5926.5485 941 | 607.6580807903788,5889.411999999999 942 | 608.0722340653057,5972.423 943 | 608.4863358038422,5817.3234999999995 944 | 608.9003860040536,5714.652 945 | 609.3143846640061,5535.523 946 | 609.7283317817642,5611.9805 947 | 610.142227355394,5507.1245 948 | 610.5560713829607,5566.106 949 | 610.9698638625297,5402.2685 950 | 611.3836047921667,5382.608 951 | 611.7972941699369,5389.1615 952 | 612.2109319939058,5362.947499999999 953 | 612.6245182621388,5295.228 954 | 613.0380529727016,5321.442 955 | 613.4515361236593,5236.246499999999 956 | 613.8649677130776,5247.169 957 | 614.2783477390218,5172.896 958 | 614.6916761995575,5124.8369999999995 959 | 615.1049530927501,5153.2355 960 | 615.5181784166649,5072.409 961 | 615.9313521693674,4980.66 962 | 616.3444743489232,4995.9515 963 | 616.7575449533977,4805.9 964 | 617.1705639808561,4987.2135 965 | 617.5835314293641,4960.9995 966 | 617.9964472969873,4912.9405 967 | 618.4093115817907,4827.745 968 | 618.8221242818402,4834.2985 969 | 619.2348853952006,4781.8705 970 | 619.6475949199383,4779.686 971 | 620.060252854118,4847.4055 972 | 620.4728591958055,4709.782 973 | 620.8854139430659,4794.9775 974 | 621.2979170939649,4727.258 975 | 621.7103686465681,4639.878 976 | 622.1227685989406,4620.2175 977 | 622.5351169491482,4620.2175 978 | 622.9474136952559,4626.771 979 | 623.3596588353297,4445.4574999999995 980 | 623.7718523674346,4414.8745 981 | 624.1839942896362,4458.5644999999995 982 | 624.5960845999999,4373.369 983 | 625.0081232965913,4279.4355 984 | 625.4201103774758,4288.1735 985 | 625.8320458407187,4189.871 986 | 626.2439296843856,4211.715999999999 987 | 626.6557619065417,4157.1035 988 | 627.0675425052528,4135.2585 989 | 627.4792714785842,4128.705 990 | 627.8909488246015,4015.111 991 | 628.3025745413697,3936.4689999999996 992 | 628.7141486269546,3951.7605 993 | 629.1256710794215,3916.8084999999996 994 | 629.5371418968361,3929.9154999999996 995 | 629.9485610772635,3962.683 996 | 630.3599286187693,4019.4799999999996 997 | 630.7712445194193,3956.1295 998 | 631.1825087772783,3958.314 999 | 631.5937213904123,3984.528 1000 | 632.0048823568862,3768.2625 1001 | 632.4159916747659,3953.9449999999997 1002 | 632.8270493421168,3820.6904999999997 1003 | 633.2380553570041,3801.0299999999997 1004 | 633.6490097174935,3988.897 1005 | 634.0599124216503,3818.506 1006 | 634.4707634675401,3960.4984999999997 1007 | 634.881562853228,3888.41 1008 | 635.2923105767801,3964.8675 1009 | 635.7030066362611,3750.7864999999997 1010 | 636.1136510297367,3811.9525 1011 | 636.5242437552727,3796.6609999999996 1012 | 636.9347848109342,3956.1295 1013 | 637.3452741947867,3842.5355 1014 | 637.7557119048956,3868.7495 1015 | 638.1660979393266,3792.292 1016 | 638.5764322961448,3750.7864999999997 1017 | 638.9867149734159,3763.8934999999997 1018 | 639.3969459692053,3805.399 1019 | 639.8071252815784,3667.7754999999997 1020 | 640.2172529086007,3737.6794999999997 1021 | 640.6273288483375,3742.0485 1022 | 641.0373530988544,3691.805 1023 | 641.4473256582166,3742.0485 1024 | 641.8572465244902,3624.0854999999997 1025 | 642.2671156957399,3565.104 1026 | 642.6769331700315,3610.9784999999997 1027 | 643.0866989454304,3512.676 1028 | 643.4964130200021,3506.1225 1029 | 643.9060753918119,3488.6465 1030 | 644.3156860589254,3497.3844999999997 1031 | 644.725245019408,3499.569 1032 | 645.1347522713249,3545.4435 1033 | 645.5442078127421,3584.7644999999998 1034 | 645.9536116417245,3412.189 1035 | 646.3629637563379,3405.6355 1036 | 646.7722641546476,3377.2369999999996 1037 | 647.181512834719,3307.3329999999996 1038 | 647.5907097946177,3333.547 1039 | 647.999855032409,3394.7129999999997 1040 | 648.4089485461586,3270.1965 1041 | 648.8179903339314,3364.1299999999997 1042 | 649.2269803937936,3318.2554999999998 1043 | 649.63591872381,3298.595 1044 | 650.0448053220464,3158.787 1045 | 650.4536401865681,3189.37 1046 | 650.8624233154408,3193.739 1047 | 651.2711547067296,3307.3329999999996 1048 | 651.6798343584999,3139.1265 1049 | 652.0884622688177,3268.0119999999997 1050 | 652.4970384357478,3265.8275 1051 | 652.9055628573561,3377.2369999999996 1052 | 653.3140355317078,3263.643 1053 | 653.7224564568685,3219.953 1054 | 654.1308256309035,3128.2039999999997 1055 | 654.5391430518783,3123.835 1056 | 654.9474087178586,3016.7945 1057 | 655.3556226269093,2957.8129999999996 1058 | 655.7637847770965,3062.669 1059 | 656.171895166485,2861.6949999999997 1060 | 656.5799537931408,2852.957 1061 | 656.987960655129,2949.075 1062 | 657.3959157505152,2907.5695 1063 | 657.8038190773648,2949.075 1064 | 658.2116706337432,3036.455 1065 | 658.619470417716,3082.3295 1066 | 659.0272184273484,3143.4955 1067 | 659.4349146607063,3075.776 1068 | 659.8425591158546,3112.9125 1069 | 660.250151790859,3152.2335 1070 | 660.657692683785,3112.9125 1071 | 661.065181792698,3086.6985 1072 | 661.4726191156634,3160.9714999999997 1073 | 661.8800046507467,3093.252 1074 | 662.2873383960134,3158.787 1075 | 662.6946203495287,3053.931 1076 | 663.1018505093583,3097.6209999999996 1077 | 663.5090288735677,3086.6985 1078 | 663.9161554402222,2977.4735 1079 | 664.323230207387,3023.348 1080 | 664.7302531731281,2935.968 1081 | 665.1372243355106,2959.9975 1082 | 665.5441436926001,3008.0564999999997 1083 | 665.9510112424618,3040.8239999999996 1084 | 666.3578269831612,2959.9975 1085 | 666.7645909127641,2970.92 1086 | 667.1713030293356,2962.182 1087 | 667.5779633309413,2968.7355 1088 | 667.9845718156466,2870.433 1089 | 668.391128481517,2839.85 1090 | 668.7976333266178,3018.979 1091 | 669.2040863490145,2938.1524999999997 1092 | 669.6104875467727,2815.8205 1093 | 670.0168369179574,2800.529 1094 | 670.4231344606349,2783.053 1095 | 670.8293801728697,2866.064 1096 | 671.2355740527281,2769.946 1097 | 671.6417160982749,2872.6175 1098 | 672.0478063075756,2824.5584999999996 1099 | 672.453844678696,2708.7799999999997 1100 | 672.8598312097013,2780.8685 1101 | 673.2657658986573,2798.3444999999997 1102 | 673.6716487436287,2807.0825 1103 | 674.0774797426817,2693.4885 1104 | 674.4832588938814,2732.8095 1105 | 674.8889861952935,2697.8575 1106 | 675.294661644983,2756.839 1107 | 675.7002852410154,2741.5474999999997 1108 | 676.1058569814569,2645.4294999999997 1109 | 676.511376864372,2730.625 1110 | 676.9168448878268,2636.6915 1111 | 677.3222610498863,2630.138 1112 | 677.7276253486164,2601.7394999999997 1113 | 678.1329377820819,2551.496 1114 | 678.5381983483489,2527.4665 1115 | 678.9434070454823,2490.33 1116 | 679.348563871548,2564.603 1117 | 679.7536688246115,2536.2045 1118 | 680.1587219027377,2547.127 1119 | 680.5637231039926,2536.2045 1120 | 680.9686724264413,2457.5625 1121 | 681.3735698681495,2518.7284999999997 1122 | 681.7784154271824,2547.127 1123 | 682.1832091016057,2488.1455 1124 | 682.5879508894844,2378.9204999999997 1125 | 682.9926407888845,2383.2895 1126 | 683.3972787978712,2402.95 1127 | 683.8018649145099,2309.0164999999997 1128 | 684.2063991368661,2350.522 1129 | 684.6108814630053,2348.3375 1130 | 685.015311890993,2319.939 1131 | 685.4196904188942,2230.3745 1132 | 685.8240170447751,2169.2084999999997 1133 | 686.2282917667004,2136.441 1134 | 686.6325145827359,1942.0204999999999 1135 | 687.0366854909473,1970.4189999999999 1136 | 687.4408044893996,1850.2714999999998 1137 | 687.8448715761587,1845.9025 1138 | 688.2488867492895,1747.6 1139 | 688.6528500068578,1834.98 1140 | 689.0567613469287,1778.183 1141 | 689.4606207675685,1795.6589999999999 1142 | 689.8644282668416,1922.36 1143 | 690.2681838428139,1924.5445 1144 | 690.6718874935511,1887.408 1145 | 691.0755392171183,1915.8065 1146 | 691.4791390115813,1961.6809999999998 1147 | 691.882686875005,1913.6219999999998 1148 | 692.2861828054554,1922.36 1149 | 692.6896268009974,1931.098 1150 | 693.093018859697,1922.36 1151 | 693.4963589796193,2014.109 1152 | 693.8996471588297,1950.7585 1153 | 694.302883395394,1976.9724999999999 1154 | 694.7060676873774,1968.2344999999998 1155 | 695.1092000328454,2031.5849999999998 1156 | 695.5122804298634,2068.7215 1157 | 695.915308876497,2134.2565 1158 | 696.3182853708113,2062.1679999999997 1159 | 696.7212099108722,2031.5849999999998 1160 | 697.1240824947447,1981.3415 1161 | 697.5269031204945,2029.4005 1162 | 697.9296717861873,1996.6329999999998 1163 | 698.3323884898879,2020.6625 1164 | 698.7350532296624,1961.6809999999998 1165 | 699.1376660035759,2108.0425 1166 | 699.5402268096939,1942.0204999999999 1167 | 699.9427356460817,1924.5445 1168 | 700.345192510805,1909.253 1169 | 700.7475974019292,1933.2824999999998 1170 | 701.1499503175196,1983.5259999999998 1171 | 701.552251255642,1885.2234999999998 1172 | 701.9545002143612,1924.5445 1173 | 702.3566971917435,1939.8359999999998 1174 | 702.7588421858536,2014.109 1175 | 703.1609351947575,1907.0684999999999 1176 | 703.56297621652,2016.2935 1177 | 703.9649652492071,1946.3895 1178 | 704.3669022908841,1950.7585 1179 | 704.7687873396164,1994.4485 1180 | 705.1706203934697,2003.1864999999998 1181 | 705.572401450509,2040.3229999999999 1182 | 705.9741305088,2011.9244999999999 1183 | 706.3758075664081,2022.847 1184 | 706.7774326213988,1992.264 1185 | 707.1790056718376,2027.216 1186 | 707.5805267157896,2003.1864999999998 1187 | 707.9819957513208,1992.264 1188 | 708.3834127764962,2057.799 1189 | 708.7847777893816,2049.0609999999997 1190 | 709.186090788042,1950.7585 1191 | 709.5873517705433,1955.1274999999998 1192 | 709.9885607349506,2003.1864999999998 1193 | 710.3897176793297,1983.5259999999998 1194 | 710.7908226017455,2003.1864999999998 1195 | 711.191875500264,2022.847 1196 | 711.5928763729504,2025.0314999999998 1197 | 711.9938252178702,2029.4005 1198 | 712.394722033089,1955.1274999999998 1199 | 712.7955668166717,2090.5665 1200 | 713.1963595666846,2014.109 1201 | 713.5971002811923,1942.0204999999999 1202 | 713.9977889582609,2022.847 1203 | 714.3984255959552,1931.098 1204 | 714.7990101923413,1996.6329999999998 1205 | 715.1995427454843,2001.002 1206 | 715.6000232534498,2016.2935 1207 | 716.0004517143032,1832.7955 1208 | 716.4008281261097,1717.0169999999998 1209 | 716.8011524869352,1804.397 1210 | 717.2014247948446,1745.4154999999998 1211 | 717.601645047904,1599.0539999999999 1212 | 718.0018132441783,1592.5004999999999 1213 | 718.401929381733,1522.5964999999999 1214 | 718.801993458634,1443.9544999999998 1215 | 719.2020054729461,1522.5964999999999 1216 | 719.6019654227355,1627.4524999999999 1217 | 720.001873306067,1596.8695 1218 | 720.4017291210064,1555.364 1219 | 720.8015328656188,1583.7624999999998 1220 | 721.2012845379702,1673.327 1221 | 721.6009841361254,1815.3194999999998 1222 | 722.0006316581503,1758.5224999999998 1223 | 722.4002271021103,1717.0169999999998 1224 | 722.7997704660706,1710.4634999999998 1225 | 723.1992617480972,1732.3084999999999 1226 | 723.5987009462549,1557.5484999999999 1227 | 723.9980880586095,1601.2385 1228 | 724.3974230832262,1727.9395 1229 | 724.7967060181709,1666.7735 1230 | 725.1959368615085,1688.6184999999998 1231 | 725.5951156113047,1647.1129999999998 1232 | 725.994242265625,1544.4415 1233 | 726.3933168225348,1688.6184999999998 1234 | 726.7923392800997,1686.434 1235 | 727.1913096363846,1684.2495 1236 | 727.590227889456,1682.0649999999998 1237 | 727.989094037378,1642.744 1238 | 728.387908078217,1710.4634999999998 1239 | 728.7866700100384,1636.1905 1240 | 729.1853798309071,1688.6184999999998 1241 | 729.5840375388892,1644.9285 1242 | 729.9826431320496,1738.8619999999999 1243 | 730.381196608454,1679.8805 1244 | 730.7796979661679,1817.504 1245 | 731.1781472032567,1800.0279999999998 1246 | 731.5765443177856,1800.0279999999998 1247 | 731.9748893078204,1797.8435 1248 | 732.3731821714266,1832.7955 1249 | 732.7714229066692,2005.3709999999999 1250 | 733.1696115116141,1924.5445 1251 | 733.5677479843264,1939.8359999999998 1252 | 733.9658323228718,1968.2344999999998 1253 | 734.3638645253155,1863.3784999999998 1254 | 734.7618445897234,1994.4485 1255 | 735.1597725141603,2066.537 1256 | 735.5576482966922,2046.8764999999999 1257 | 735.9554719353844,1981.3415 1258 | 736.353243428302,1966.05 1259 | 736.7509627735112,2040.3229999999999 1260 | 737.1486299690765,2016.2935 1261 | 737.5462450130642,2066.537 1262 | 737.9438079035392,1981.3415 1263 | 738.3413186385673,2022.847 1264 | 738.7387772162135,2068.7215 1265 | 739.1361836345436,2064.3525 1266 | 739.533537891623,2092.7509999999997 1267 | 739.9308399855171,2035.954 1268 | 740.3280899142916,2022.847 1269 | 740.7252876760114,2053.43 1270 | 741.1224332687425,2051.2455 1271 | 741.5195266905499,2105.8579999999997 1272 | 741.9165679394995,2020.6625 1273 | 742.3135570136562,1985.7105 1274 | 742.7104939110858,2084.013 1275 | 743.107378629854,2145.179 1276 | 743.5042111680257,2053.43 1277 | 743.9009915236667,2066.537 1278 | 744.2977196948423,2182.3154999999997 1279 | 744.694395679618,1983.5259999999998 1280 | 745.0910194760592,2046.8764999999999 1281 | 745.4875910822315,2153.917 1282 | 745.8841104962,2049.0609999999997 1283 | 746.2805777160304,2081.8285 1284 | 746.6769927397884,2101.489 1285 | 747.0733555655388,2129.8875 1286 | 747.4696661913479,2088.382 1287 | 747.8659246152803,2075.275 1288 | 748.262130835402,2140.81 1289 | 748.6582848497782,2094.9355 1290 | 749.0543866564744,2009.7399999999998 1291 | 749.4504362535561,2014.109 1292 | 749.8464336390886,2079.644 1293 | 750.2423788111375,2007.5555 1294 | 750.6382717677682,2062.1679999999997 1295 | 751.0341125070463,2057.799 1296 | 751.4299010270369,2016.2935 1297 | 751.8256373258059,2081.8285 1298 | 752.2213214014182,2169.2084999999997 1299 | 752.6169532519398,2094.9355 1300 | 753.0125328754357,2138.6255 1301 | 753.4080602699714,2035.954 1302 | 753.8035354336129,2140.81 1303 | 754.1989583644248,2097.12 1304 | 754.5943290604733,1998.8174999999999 1305 | 754.9896475198234,2156.1014999999998 1306 | 755.3849137405407,2088.382 1307 | 755.7801277206906,2035.954 1308 | 756.1752894583385,2070.906 1309 | 756.5703989515501,2079.644 1310 | 756.9654561983905,2035.954 1311 | 757.3604611969255,2110.227 1312 | 757.7554139452201,2020.6625 1313 | 758.1503144413404,1854.6405 1314 | 758.545162683351,1756.338 1315 | 758.9399586693182,1526.9655 1316 | 759.3347023973067,1428.663 1317 | 759.7293938653823,1146.8625 1318 | 760.1240330716107,1048.56 1319 | 760.518620014057,889.0915 1320 | 760.9131546907869,801.7115 1321 | 761.3076370998654,823.5564999999999 1322 | 761.7020672393585,790.789 1323 | 762.0964451073311,705.5935 1324 | 762.4907707018492,694.6709999999999 1325 | 762.8850440209778,771.1284999999999 1326 | 763.2792650627824,768.944 1327 | 763.6734338253289,747.0989999999999 1328 | 764.0675503066823,843.217 1329 | 764.4616145049081,943.704 1330 | 764.8556264180719,917.49 1331 | 765.2495860442391,1044.191 1332 | 765.643493381475,1066.036 1333 | 766.0373484278452,1225.5045 1334 | 766.4311511814151,1197.106 1335 | 766.8249016402501,1295.4085 1336 | 767.218599802416,1384.973 1337 | 767.6122456659774,1242.9805 1338 | 768.0058392290009,1450.5079999999998 1339 | 768.3993804895509,1502.936 1340 | 768.7928694456936,1553.1795 1341 | 769.1863060954938,1481.091 1342 | 769.5796904370177,1529.1499999999999 1343 | 769.9730224683301,1542.2569999999998 1344 | 770.3663021874966,1583.7624999999998 1345 | 770.7595295925828,1570.6554999999998 1346 | 771.1527046816541,1625.268 1347 | 771.545827452776,1522.5964999999999 1348 | 771.9388979040139,1483.2755 1349 | 772.3319160334331,1564.1019999999999 1350 | 772.7248818390992,1651.482 1351 | 773.1177953190777,1526.9655 1352 | 773.5106564714338,1522.5964999999999 1353 | 773.9034652942331,1513.8584999999998 1354 | 774.2962217855412,1533.519 1355 | 774.6889259434233,1599.0539999999999 1356 | 775.0815777659451,1472.3529999999998 1357 | 775.4741772511718,1411.187 1358 | 775.8667243971692,1513.8584999999998 1359 | 776.2592192020021,1498.567 1360 | 776.6516616637367,1470.1685 1361 | 777.0440517804377,1461.4305 1362 | 777.436389550171,1542.2569999999998 1363 | 777.8286749710024,1402.4489999999998 1364 | 778.2209080409965,1487.6444999999999 1365 | 778.6130887582195,1457.0615 1366 | 779.0052171207363,1437.4009999999998 1367 | 779.3972931266128,1526.9655 1368 | 779.789316773914,1345.652 1369 | 780.1812880607058,1315.069 1370 | 780.5732069850532,1297.5929999999998 1371 | 780.9650735450218,1457.0615 1372 | 781.3568877386774,1317.2535 1373 | 781.748649564085,1402.4489999999998 1374 | 782.1403590193103,1369.6815 1375 | 782.5320161024185,1382.7884999999999 1376 | 782.9236208114755,1422.1095 1377 | 783.3151731445462,1334.7295 1378 | 783.7066730996962,1376.235 1379 | 784.0981206749914,1304.1464999999998 1380 | 784.4895158684966,1358.759 1381 | 784.8808586782777,1262.6409999999998 1382 | 785.2721491024,1299.7775 1383 | 785.6633871389289,1341.283 1384 | 786.0545727859298,1312.8845 1385 | 786.4457060414685,1280.117 1386 | 786.8367869036098,1277.9325 1387 | 787.2278153704198,1194.9215 1388 | 787.6187914399636,1162.154 1389 | 788.0097151103067,1170.8919999999998 1390 | 788.4005863795147,1240.7959999999998 1391 | 788.7914052456528,1280.117 1392 | 789.1821717067867,1183.999 1393 | 789.5728857609815,1170.8919999999998 1394 | 789.9635474063031,1153.416 1395 | 790.3541566408164,1162.154 1396 | 790.7447134625874,1199.2904999999998 1397 | 791.1352178696812,1212.3975 1398 | 791.5256698601634,1120.6485 1399 | 791.9160694320996,1151.2314999999999 1400 | 792.3064165835547,1087.8809999999999 1401 | 792.6967113125947,1109.7259999999999 1402 | 793.0869536172847,1100.9879999999998 1403 | 793.4771434956906,1159.9695 1404 | 793.8672809458772,1107.5415 1405 | 794.2573659659104,1096.619 1406 | 794.6473985538555,1061.667 1407 | 795.0373787077781,1127.202 1408 | 795.4273064257435,1061.667 1409 | 795.817181705817,1153.416 1410 | 796.2070045460646,1092.25 1411 | 796.5967749445509,1125.0175 1412 | 796.9864928993424,1072.5895 1413 | 797.3761584085034,1009.2389999999999 1414 | 797.7657714701,1013.608 1415 | 798.1553320821979,1129.3864999999998 1416 | 798.5448402428619,1022.346 1417 | 798.934295950158,1055.1135 1418 | 799.3236992021511,1135.94 1419 | 799.7130499969073,1074.774 1420 | 800.1023483324915,1046.3754999999999 1421 | 800.4915942069695,1057.298 1422 | 800.8807876184064,1035.453 1423 | 801.2699285648678,1042.0065 1424 | 801.6590170444194,963.3644999999999 1425 | 802.0480530551263,1020.1614999999999 1426 | 802.4370365950543,954.6265 1427 | 802.8259676622685,954.6265 1428 | 803.2148462548345,985.2094999999999 1429 | 803.6036723708175,1068.2205 1430 | 803.9924460082835,996.132 1431 | 804.3811671652974,1046.3754999999999 1432 | 804.7698358399249,1033.2685 1433 | 805.1584520302316,989.5785 1434 | 805.5470157342826,983.025 1435 | 805.9355269501436,926.228 1436 | 806.3239856758798,952.442 1437 | 806.7123919095571,983.025 1438 | 807.1007456492404,956.8109999999999 1439 | 807.4890468929957,1033.2685 1440 | 807.8772956388879,985.2094999999999 1441 | 808.2654918849826,980.8404999999999 1442 | 808.6536356293456,915.3054999999999 1443 | 809.041726870042,950.2574999999999 1444 | 809.4297656051374,902.1985 1445 | 809.817751832697,1055.1135 1446 | 810.2056855507867,952.442 1447 | 810.5935667574714,880.3534999999999 1448 | 810.981395450817,921.8589999999999 1449 | 811.3691716288889,934.9659999999999 1450 | 811.7568952897522,893.4604999999999 1451 | 812.1445664314728,910.9364999999999 1452 | 812.5321850521157,893.4604999999999 1453 | 812.9197511497468,858.5084999999999 1454 | 813.3072647224312,867.2465 1455 | 813.6947257682345,878.169 1456 | 814.082134285222,758.0215 1457 | 814.4694902714592,764.5749999999999 1458 | 814.8567937250119,766.7595 1459 | 815.244044643945,803.896 1460 | 815.6312430263246,736.1764999999999 1461 | 816.0183888702153,801.7115 1462 | 816.4054821736833,753.6524999999999 1463 | 816.7925229347934,758.0215 1464 | 817.1795111516118,696.8555 1465 | 817.5664468222033,692.4865 1466 | 817.9533299446335,646.612 1467 | 818.3401605169682,683.7484999999999 1468 | 818.7269385372724,659.7189999999999 1469 | 819.1136640036119,685.933 1470 | 819.5003369140518,766.7595 1471 | 819.8869572666579,699.04 1472 | 820.2735250594953,753.6524999999999 1473 | 820.6600402906299,749.2835 1474 | 821.0465029581266,795.1579999999999 1475 | 821.4329130600511,766.7595 1476 | 821.8192705944691,701.2244999999999 1477 | 822.2055755594456,723.0695 1478 | 822.5918279530464,738.361 1479 | 822.9780277733366,751.468 1480 | 823.3641750183822,696.8555 1481 | 823.7502696862481,723.0695 1482 | 824.1363117750001,723.0695 1483 | 824.5223012827033,753.6524999999999 1484 | 824.9082382074233,758.0215 1485 | 825.2941225472258,747.0989999999999 1486 | 825.6799543001758,762.3905 1487 | 826.0657334643394,786.42 1488 | 826.4514600377812,688.1175 1489 | 826.8371340185676,733.992 1490 | 827.222755404763,797.3425 1491 | 827.6083241944336,679.3795 1492 | 827.9938403856446,699.04 1493 | 828.3793039764614,668.457 1494 | 828.7647149649497,705.5935 1495 | 829.1500733491747,664.088 1496 | 829.535379127202,760.2059999999999 1497 | 829.9206322970966,736.1764999999999 1498 | 830.3058328569248,716.516 1499 | 830.6909808047513,657.5345 1500 | 831.076076138642,701.2244999999999 1501 | 831.4611188566619,707.778 1502 | 831.8461089568767,688.1175 1503 | 832.2310464373521,655.3499999999999 1504 | 832.615931296153,755.837 1505 | 833.0007635313455,672.826 1506 | 833.3855431409944,705.5935 1507 | 833.7702701231657,690.3019999999999 1508 | 834.1549444759244,705.5935 1509 | 834.5395661973363,677.1949999999999 1510 | 834.9241352854665,694.6709999999999 1511 | 835.3086517383806,694.6709999999999 1512 | 835.6931155541442,716.516 1513 | 836.0775267308226,747.0989999999999 1514 | 836.4618852664813,729.6229999999999 1515 | 836.8461911591857,723.0695 1516 | 837.2304444070013,683.7484999999999 1517 | 837.6146450079934,753.6524999999999 1518 | 837.9987929602277,703.409 1519 | 838.3828882617696,755.837 1520 | 838.7669309106842,733.992 1521 | 839.1509209050374,696.8555 1522 | 839.5348582428943,744.9145 1523 | 839.9187429223207,692.4865 1524 | 840.3025749413817,718.7004999999999 1525 | 840.6863542981431,653.1655 1526 | 841.0700809906699,727.4385 1527 | 841.4537550170279,707.778 1528 | 841.8373763752825,755.837 1529 | 842.2209450634989,827.9254999999999 1530 | 842.604461079743,716.516 1531 | 842.9879244220798,727.4385 1532 | 843.371335088575,714.3315 1533 | 843.7546930772938,672.826 1534 | 844.1379983863022,694.6709999999999 1535 | 844.521251013665,716.516 1536 | 844.9044509574479,666.2724999999999 1537 | 845.2875982157166,683.7484999999999 1538 | 845.670692786536,677.1949999999999 1539 | 846.0537346679722,709.9625 1540 | 846.4367238580901,681.564 1541 | 846.8196603549555,683.7484999999999 1542 | 847.2025441566335,712.1469999999999 1543 | 847.5853752611902,644.4275 1544 | 847.96815366669,688.1175 1545 | 848.3508793711992,661.9035 1546 | 848.7335523727832,650.981 1547 | 849.1161726695069,690.3019999999999 1548 | 849.4987402594364,683.7484999999999 1549 | 849.8812551406367,611.66 1550 | 850.2637173111734,677.1949999999999 1551 | 850.646126769112,583.2615 1552 | 851.0284835125178,616.029 1553 | 851.4107875394564,622.5825 1554 | 851.793038847993,609.4755 1555 | 852.1752374361935,607.2909999999999 1556 | 852.5573833021227,581.077 1557 | 852.9394764438468,576.708 1558 | 853.3215168594307,591.9995 1559 | 853.7035045469403,574.5235 1560 | 854.0854395044403,578.8924999999999 1561 | 854.4673217299969,565.7855 1562 | 854.8491512216751,565.7855 1563 | 855.2309279775407,543.9404999999999 1564 | 855.6126519956589,692.4865 1565 | 855.994323274095,581.077 1566 | 856.375941810915,602.922 1567 | 856.7575076041837,596.3684999999999 1568 | 857.139020651967,602.922 1569 | 857.5204809523301,629.136 1570 | 857.9018885033387,565.7855 1571 | 858.2832433030578,616.029 1572 | 858.6645453495532,581.077 1573 | 859.0457946408907,602.922 1574 | 859.4269911751348,626.9515 1575 | 859.8081349503518,596.3684999999999 1576 | 860.1892259646067,567.97 1577 | 860.5702642159653,552.6785 1578 | 860.9512497024924,565.7855 1579 | 861.3321824222544,681.564 1580 | 861.7130623733157,611.66 1581 | 862.0938895537424,552.6785 1582 | 862.4746639616001,675.0105 1583 | 862.8553855949535,530.8335 1584 | 863.236054451869,581.077 1585 | 863.6166705304112,537.387 1586 | 863.9972338286461,618.2135 1587 | 864.3777443446388,611.66 1588 | 864.7582020764552,548.3095 1589 | 865.13860702216,591.9995 1590 | 865.5189591798193,570.1545 1591 | 865.8992585474983,563.601 1592 | 866.2795051232624,557.0475 1593 | 866.6596989051775,508.9885 1594 | 867.0398398913085,543.9404999999999 1595 | 867.4199280797211,526.4644999999999 1596 | 867.7999634684803,524.28 1597 | 868.1799460556522,535.2025 1598 | 868.5598758393021,587.6305 1599 | 868.939752817495,550.4939999999999 1600 | 869.319576988297,508.9885 1601 | 869.699348349773,530.8335 1602 | 870.0790668999888,460.92949999999996 1603 | 870.4587326370096,493.697 1604 | 870.8383455589011,471.852 1605 | 871.2179056637284,476.221 1606 | 871.5974129495572,530.8335 1607 | 871.9768674144531,436.9 1608 | 872.3562690564811,522.0955 1609 | 872.7356178737073,441.269 1610 | 873.1149138641963,480.59 1611 | 873.4941570260142,504.61949999999996 1612 | 873.8733473572262,543.9404999999999 1613 | 874.2524848558979,537.387 1614 | 874.6315695200943,489.328 1615 | 875.0106013478813,517.7265 1616 | 875.3895803373246,476.221 1617 | 875.7685064864888,450.00699999999995 1618 | 876.1473797934402,552.6785 1619 | 876.5262002562437,500.2505 1620 | 876.904967872965,489.328 1621 | 877.2836826416693,526.4644999999999 1622 | 877.6623445604225,489.328 1623 | 878.0409536272895,467.48299999999995 1624 | 878.419509840336,526.4644999999999 1625 | 878.7980131976277,508.9885 1626 | 879.1764636972296,535.2025 1627 | 879.5548613372074,554.8629999999999 1628 | 879.9332061156265,528.649 1629 | 880.3114980305525,456.5605 1630 | 880.6897370800505,543.9404999999999 1631 | 881.0679232621862,458.745 1632 | 881.446056575025,528.649 1633 | 881.8241370166322,482.7745 1634 | 882.2021645850737,519.911 1635 | 882.5801392784144,474.0365 1636 | 882.95806109472,443.45349999999996 1637 | 883.335930032056,491.5125 1638 | 883.7137460884878,460.92949999999996 1639 | 884.0915092620808,463.114 1640 | 884.4692195509006,500.2505 1641 | 884.8468769530123,467.48299999999995 1642 | 885.2244814664815,452.19149999999996 1643 | 885.602033089374,415.05499999999995 1644 | 885.9795318197547,454.376 1645 | 886.3569776556895,508.9885 1646 | 886.7343705952435,493.697 1647 | 887.1117106364826,445.638 1648 | 887.4889977774717,432.53099999999995 1649 | 887.8662320162766,467.48299999999995 1650 | 888.2434133509626,460.92949999999996 1651 | 888.620541779595,482.7745 1652 | 888.9976173002398,441.269 1653 | 889.3746399109618,465.2985 1654 | 889.751609609827,450.00699999999995 1655 | 890.1285263949005,493.697 1656 | 890.5053902642479,445.638 1657 | 890.8822012159343,465.2985 1658 | 891.2589592480258,480.59 1659 | 891.6356643585872,445.638 1660 | 892.0123165456841,450.00699999999995 1661 | 892.3889158073825,458.745 1662 | 892.7654621417471,439.0845 1663 | 893.141955546844,515.5419999999999 1664 | 893.518396020738,401.948 1665 | 893.8947835614949,452.19149999999996 1666 | 894.2711181671801,460.92949999999996 1667 | 894.6473998358591,484.95899999999995 1668 | 895.0236285655975,439.0845 1669 | 895.3998043544603,478.40549999999996 1670 | 895.7759272005134,487.14349999999996 1671 | 896.1519971018218,469.66749999999996 1672 | 896.5280140564514,469.66749999999996 1673 | 896.9039780624673,487.14349999999996 1674 | 897.2798891179353,484.95899999999995 1675 | 897.6557472209204,364.81149999999997 1676 | 898.0315523694884,408.50149999999996 1677 | 898.4073045617047,386.6565 1678 | 898.7830037956345,364.81149999999997 1679 | 899.1586500693436,410.686 1680 | 899.5342433808971,493.697 1681 | 899.9097837283609,369.1805 1682 | 900.2852711098,423.793 1683 | 900.6607055232802,377.9185 1684 | 901.0360869668664,380.10299999999995 1685 | 901.4114154386245,369.1805 1686 | 901.7866909366201,419.424 1687 | 902.1619134589182,377.9185 1688 | 902.5370830035847,353.889 1689 | 902.9121995686845,332.044 1690 | 903.2872631522837,371.365 1691 | 903.662273752447,410.686 1692 | 904.0372313672407,417.23949999999996 1693 | 904.4121359947294,375.734 1694 | 904.786987632979,366.996 1695 | 905.1617862800551,401.948 1696 | 905.5365319340227,412.8705 1697 | 905.9112245929479,388.841 1698 | 906.2858642548953,386.6565 1699 | 906.660450917931,323.306 1700 | 907.03498458012,410.686 1701 | 907.4094652395283,412.8705 1702 | 907.7838928942208,340.782 1703 | 908.1582675422632,329.85949999999997 1704 | 908.5325891817209,360.4425 1705 | 908.9068578106594,393.21 1706 | 909.2810734271443,391.02549999999997 1707 | 909.6552360292405,360.4425 1708 | 910.0293456150142,297.092 1709 | 910.4034021825302,314.568 1710 | 910.7774057298543,415.05499999999995 1711 | 911.1513562550518,347.33549999999997 1712 | 911.5252537561881,358.258 1713 | 911.899098231329,382.28749999999997 1714 | 912.2728896785394,358.258 1715 | 912.6466280958854,329.85949999999997 1716 | 913.0203134814318,366.996 1717 | 913.3939458332445,325.4905 1718 | 913.7675251493887,336.413 1719 | 914.14105142793,222.819 1720 | 914.5145246669338,338.59749999999997 1721 | 914.8879448644652,353.889 1722 | 915.2613120185905,356.07349999999997 1723 | 915.6346261273742,318.937 1724 | 916.0078871888825,323.306 1725 | 916.3810952011802,301.461 1726 | 916.7542501623334,325.4905 1727 | 917.1273520704068,334.2285 1728 | 917.5004009234667,369.1805 1729 | 917.8733967195778,290.5385 1730 | 918.2463394568059,342.9665 1731 | 918.6192291332165,321.12149999999997 1732 | 918.9920657468749,419.424 1733 | 919.3648492958467,279.616 1734 | 919.7375797781972,353.889 1735 | 920.1102571919919,288.354 1736 | 920.4828815352961,338.59749999999997 1737 | 920.8554528061755,347.33549999999997 1738 | 921.2279710026955,334.2285 1739 | 921.6004361229213,329.85949999999997 1740 | 921.9728481649188,308.0145 1741 | 922.3452071267529,270.878 1742 | 922.7175130064896,283.985 1743 | 923.0897658021939,318.937 1744 | 923.4619655119316,270.878 1745 | 923.8341121337677,334.2285 1746 | 924.2062056657679,364.81149999999997 1747 | 924.578246105998,229.3725 1748 | 924.9502334525229,310.19899999999996 1749 | 925.3221677034084,318.937 1750 | 925.6940488567196,299.2765 1751 | 926.0658769105224,305.83 1752 | 926.4376518628817,371.365 1753 | 926.8093737118636,356.07349999999997 1754 | 927.181042455533,255.5865 1755 | 927.5526580919554,386.6565 1756 | 927.9242206191967,334.2285 1757 | 928.2957300353218,283.985 1758 | 928.6671863383966,222.819 1759 | 929.0385895264861,277.43149999999997 1760 | 929.4099395976563,415.05499999999995 1761 | 929.781236549972,294.90749999999997 1762 | 930.1524803814993,235.926 1763 | 930.523671090303,281.8005 1764 | 930.894808674449,218.45 1765 | 931.2658931320028,321.12149999999997 1766 | 931.6369244610294,229.3725 1767 | 932.0079026595947,262.14 1768 | 932.3788277257639,152.915 1769 | 932.7496996576026,205.343 1770 | 933.1205184531759,279.616 1771 | 933.4912841105498,259.9555 1772 | 933.8619966277894,235.926 1773 | 934.2326560029601,253.402 1774 | 934.6032622341277,273.0625 1775 | 934.973815319357,249.033 1776 | 935.3443152567141,235.926 1777 | 935.714762044264,299.2765 1778 | 936.0851556800727,183.498 1779 | 936.455496162205,225.00349999999997 1780 | 936.8257834887268,229.3725 1781 | 937.1960176577032,185.6825 1782 | 937.5661986672,222.819 1783 | 937.9363265152824,257.77099999999996 1784 | 938.3064012000159,238.1105 1785 | 938.6764227194661,233.74149999999997 1786 | 939.0463910716982,235.926 1787 | 939.4163062547779,192.236 1788 | 939.7861682667702,220.6345 1789 | 940.1559771057414,286.16949999999997 1790 | 940.5257327697559,211.8965 1791 | 940.8954352568799,205.343 1792 | 941.2650845651787,277.43149999999997 1793 | 941.6346806927173,231.557 1794 | 942.004223637562,209.712 1795 | 942.3737133977772,314.568 1796 | 942.7431499714295,305.83 1797 | 943.1125333565833,240.295 1798 | 943.4818635513046,310.19899999999996 1799 | 943.8511405536588,255.5865 1800 | 944.2203643617111,288.354 1801 | 944.5895349735274,240.295 1802 | 944.9586523871727,249.033 1803 | 945.327716600713,216.26549999999997 1804 | 945.6967276122128,240.295 1805 | 946.0656854197387,255.5865 1806 | 946.434590021355,262.14 1807 | 946.803441415128,259.9555 1808 | 947.172239599123,279.616 1809 | 947.540984571405,200.974 1810 | 947.9096763300402,203.1585 1811 | 948.2783148730933,246.8485 1812 | 948.6469001986303,257.77099999999996 1813 | 949.0154323047161,152.915 1814 | 949.3839111894167,185.6825 1815 | 949.7523368507972,301.461 1816 | 950.120709286923,222.819 1817 | 950.48902849586,266.509 1818 | 950.8572944756731,233.74149999999997 1819 | 951.2255072244282,264.3245 1820 | 951.5936667401903,229.3725 1821 | 951.9617730210255,255.5865 1822 | 952.3298260649983,235.926 1823 | 952.6978258701752,181.31349999999998 1824 | 953.0657724346207,238.1105 1825 | 953.4336657564007,251.21749999999997 1826 | 953.801505833581,277.43149999999997 1827 | 954.1692926642263,259.9555 1828 | 954.5370262464027,218.45 1829 | 954.9047065781753,286.16949999999997 1830 | 955.2723336576096,242.47949999999997 1831 | 955.6399074827709,214.081 1832 | 956.0074280517251,222.819 1833 | 956.3748953625371,301.461 1834 | 956.7423094132726,235.926 1835 | 957.1096702019972,220.6345 1836 | 957.4769777267761,238.1105 1837 | 957.8442319856752,249.033 1838 | 958.2114329767592,266.509 1839 | 958.5785806980941,209.712 1840 | 958.9456751477451,209.712 1841 | 959.3127163237779,259.9555 1842 | 959.6797042242576,297.092 1843 | 960.0466388472498,255.5865 1844 | 960.4135201908202,268.6935 1845 | 960.7803482530339,203.1585 1846 | 961.1471230319565,275.24699999999996 1847 | 961.5138445256534,240.295 1848 | 961.8805127321903,314.568 1849 | 962.2471276496321,235.926 1850 | 962.6136892760449,209.712 1851 | 962.9801976094935,244.664 1852 | 963.3466526480438,277.43149999999997 1853 | 963.7130543897612,183.498 1854 | 964.0794028327107,244.664 1855 | 964.4456979749586,225.00349999999997 1856 | 964.8119398145695,255.5865 1857 | 965.1781283496094,227.188 1858 | 965.5442635781434,220.6345 1859 | 965.9103454982373,227.188 1860 | 966.2763741079561,262.14 1861 | 966.6423494053656,200.974 1862 | 967.0082713885312,190.05149999999998 1863 | 967.3741400555181,209.712 1864 | 967.7399554043923,290.5385 1865 | 968.1057174332185,229.3725 1866 | 968.4714261400628,229.3725 1867 | 968.8370815229902,207.52749999999997 1868 | 969.2026835800666,211.8965 1869 | 969.5682323093569,301.461 1870 | 969.9337277089267,246.8485 1871 | 970.2991697768418,270.878 1872 | 970.6645585111673,290.5385 1873 | 971.029893909969,242.47949999999997 1874 | 971.3951759713118,244.664 1875 | 971.7604046932616,279.616 1876 | 972.1255800738838,249.033 1877 | 972.4907021112435,218.45 1878 | 972.8557708034068,327.67499999999995 1879 | 973.2207861484384,203.1585 1880 | 973.5857481444043,273.0625 1881 | 973.9506567893696,190.05149999999998 1882 | 974.3155120814,279.616 1883 | 974.6803140185608,233.74149999999997 1884 | 975.0450625989175,264.3245 1885 | 975.4097578205354,266.509 1886 | 975.7743996814802,301.461 1887 | 976.1389881798173,227.188 1888 | 976.5035233136119,238.1105 1889 | 976.86800508093,255.5865 1890 | 977.2324334798362,264.3245 1891 | 977.5968085083967,251.21749999999997 1892 | 977.9611301646765,259.9555 1893 | 978.3253984467415,253.402 1894 | 978.6896133526565,266.509 1895 | 979.0537748804875,249.033 1896 | 979.4178830282999,242.47949999999997 1897 | 979.7819377941586,216.26549999999997 1898 | 980.1459391761299,192.236 1899 | 980.5098871722785,262.14 1900 | 980.8737817806705,281.8005 1901 | 981.2376229993705,253.402 1902 | 981.6014108264449,257.77099999999996 1903 | 981.9651452599584,273.0625 1904 | 982.3288262979767,257.77099999999996 1905 | 982.6924539385656,240.295 1906 | 983.0560281797899,266.509 1907 | 983.4195490197156,290.5385 1908 | 983.7830164564078,253.402 1909 | 984.1464304879323,257.77099999999996 1910 | 984.509791112354,292.72299999999996 1911 | 984.8730983277391,209.712 1912 | 985.2363521321522,229.3725 1913 | 985.5995525236592,242.47949999999997 1914 | 985.9626995003257,246.8485 1915 | 986.3257930602168,235.926 1916 | 986.6888332013984,268.6935 1917 | 987.0518199219354,283.985 1918 | 987.4147532198938,303.64549999999997 1919 | 987.7776330933383,218.45 1920 | 988.1404595403352,244.664 1921 | 988.5032325589493,266.509 1922 | 988.8659521472463,203.1585 1923 | 989.2286183032919,231.557 1924 | 989.5912310251509,270.878 1925 | 989.9537903108895,251.21749999999997 1926 | 990.3162961585726,225.00349999999997 1927 | 990.678748566266,262.14 1928 | 991.0411475320348,301.461 1929 | 991.4034930539448,190.05149999999998 1930 | 991.765785130061,235.926 1931 | 992.1280237584492,139.808 1932 | 992.490208937175,192.236 1933 | 992.8523406643034,238.1105 1934 | 993.2144189379003,303.64549999999997 1935 | 993.5764437560306,231.557 1936 | 993.9384151167604,249.033 1937 | 994.3003330181544,262.14 1938 | 994.6621974582789,314.568 1939 | 995.0240084351987,216.26549999999997 1940 | 995.3857659469793,275.24699999999996 1941 | 995.7474699916867,174.76 1942 | 996.1091205673855,194.4205 1943 | 996.470717672142,231.557 1944 | 996.8322613040209,279.616 1945 | 997.1937514610883,268.6935 1946 | 997.5551881414091,275.24699999999996 1947 | 997.9165713430489,251.21749999999997 1948 | 998.2779010640735,218.45 1949 | 998.639177302548,240.295 1950 | 999.000400056538,214.081 1951 | 999.3615693241087,231.557 1952 | 999.7226851033258,262.14 1953 | 1000.0837473922546,246.8485 1954 | 1000.4447561889609,251.21749999999997 1955 | 1000.8057114915096,222.819 1956 | 1001.1666132979663,231.557 1957 | 1001.5274616063969,262.14 1958 | 1001.8882564148663,225.00349999999997 1959 | 1002.2489977214402,168.2065 1960 | 1002.609685524184,255.5865 1961 | 1002.9703198211632,148.546 1962 | 1003.3309006104431,277.43149999999997 1963 | 1003.6914278900895,233.74149999999997 1964 | 1004.0519016581673,216.26549999999997 1965 | 1004.4123219127422,229.3725 1966 | 1004.77268865188,200.974 1967 | 1005.1330018736456,244.664 1968 | 1005.4932615761049,220.6345 1969 | 1005.853467757323,209.712 1970 | 1006.2136204153655,174.76 1971 | 1006.5737195482977,190.05149999999998 1972 | 1006.9337651541855,264.3245 1973 | 1007.2937572310937,270.878 1974 | 1007.653695777088,240.295 1975 | 1008.0135807902343,259.9555 1976 | 1008.3734122685975,238.1105 1977 | 1008.7331902102432,253.402 1978 | 1009.0929146132366,303.64549999999997 1979 | 1009.4525854756439,240.295 1980 | 1009.8122027955297,231.557 1981 | 1010.1717665709601,229.3725 1982 | 1010.5312768,292.72299999999996 1983 | 1010.8907334807151,308.0145 1984 | 1011.250136611171,255.5865 1985 | 1011.6094861894329,249.033 1986 | 1011.9687822135664,377.9185 1987 | 1012.3280246816369,266.509 1988 | 1012.6872135917098,259.9555 1989 | 1013.0463489418504,218.45 1990 | 1013.4054307301247,264.3245 1991 | 1013.7644589545974,238.1105 1992 | 1014.1234336133346,233.74149999999997 1993 | 1014.4823547044014,303.64549999999997 1994 | 1014.8412222258632,192.236 1995 | 1015.2000361757858,238.1105 1996 | 1015.5587965522343,259.9555 1997 | 1015.9175033532744,216.26549999999997 1998 | 1016.2761565769711,216.26549999999997 1999 | 1016.6347562213905,244.664 2000 | 1016.9933022845975,225.00349999999997 2001 | 1017.3517947646576,233.74149999999997 2002 | 1017.7102336596367,216.26549999999997 2003 | 1018.0686189676,209.712 2004 | 1018.4269506866128,231.557 2005 | 1018.7852288147405,227.188 2006 | 1019.143453350049,240.295 2007 | 1019.5016242906031,240.295 2008 | 1019.859741634469,220.6345 2009 | 1020.2178053797113,259.9555 2010 | 1020.5758155243958,179.129 2011 | 1020.9337720665885,176.9445 2012 | 1021.2916750043541,192.236 2013 | 1021.6495243357585,192.236 2014 | 1022.0073200588668,192.236 2015 | 1022.3650621717449,196.605 2016 | 1022.7227506724577,194.4205 2017 | 1023.0803855590709,214.081 2018 | 1023.4379668296501,216.26549999999997 2019 | 1023.7954944822606,229.3725 2020 | 1024.1529685149678,231.557 2021 | 1024.5103889258373,240.295 2022 | 1024.8677557129345,238.1105 2023 | 1025.2250688743247,238.1105 2024 | 1025.5823284080734,242.47949999999997 2025 | 1025.9395343122462,286.16949999999997 2026 | 1026.296686584908,209.712 2027 | 1026.6537852241254,227.188 2028 | 1027.0108302279627,227.188 2029 | 1027.367821594486,227.188 2030 | -------------------------------------------------------------------------------- /src/textual_plot/ticks.py: -------------------------------------------------------------------------------- 1 | from math import ceil, floor, log10 2 | 3 | import numpy as np 4 | 5 | MAX_TICKS = 8 6 | 7 | x_min = 0 8 | x_max = 3177 9 | 10 | delta_x = x_max - x_min 11 | tick_spacing = delta_x / 5 12 | power = floor(log10(tick_spacing)) 13 | approx_interval = tick_spacing / 10**power 14 | intervals = np.array([1, 2, 5, 10]) 15 | 16 | idx = intervals.searchsorted(approx_interval) 17 | interval = intervals[idx - 1] * 10**power 18 | if delta_x // interval > MAX_TICKS: 19 | interval = intervals[idx] * 10**power 20 | ticks = [ 21 | float(t * interval) 22 | for t in np.arange(ceil(x_min / interval), x_max // interval + 1) 23 | ] 24 | decimals = -min(0, power) 25 | tick_labels = [f"{tick:.{decimals}f}" for tick in ticks] 26 | 27 | print(f"{interval=:f}, {power=}") 28 | print(f"{ticks=}") 29 | print(f"{decimals=}") 30 | print(f"{tick_labels=}") 31 | -------------------------------------------------------------------------------- /tests/test_transformations.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from textual.geometry import Region 3 | 4 | from textual_plot.plot_widget import ( 5 | map_coordinate_to_hires_pixel, 6 | map_coordinate_to_pixel, 7 | map_pixel_to_coordinate, 8 | ) 9 | 10 | 11 | class TestImplementation: 12 | @pytest.mark.parametrize( 13 | "x, y, xmin, xmax, ymin, ymax, region,expected", 14 | [ 15 | (0.0, 0.0, 0.0, 10.0, 0.0, 20.0, Region(0, 0, 4, 4), (0, 3)), 16 | (1.0, 1.0, 0.0, 10.0, 0.0, 20.0, Region(0, 0, 4, 4), (0, 3)), 17 | (4.99, 4.99, 0.0, 10.0, 0.0, 20.0, Region(0, 0, 4, 4), (1, 3)), 18 | (1.0, 1.0, 0.0, 10.0, 0.0, 20.0, Region(2, 3, 4, 4), (2, 6)), 19 | (10.0, 20.0, 0.0, 10.0, 0.0, 20.0, Region(0, 0, 4, 4), (4, -1)), 20 | ], 21 | ) 22 | def test_map_coordinate_to_pixel( 23 | self, x, y, xmin, xmax, ymin, ymax, region, expected 24 | ): 25 | assert map_coordinate_to_pixel(x, y, xmin, xmax, ymin, ymax, region) == expected 26 | 27 | @pytest.mark.parametrize( 28 | "x, y, xmin, xmax, ymin, ymax, region,expected", 29 | [ 30 | (0.0, 0.0, 0.0, 10.0, 0.0, 20.0, Region(0, 0, 4, 4), (0.0, 4.0)), 31 | (1.0, 1.0, 0.0, 10.0, 0.0, 20.0, Region(0, 0, 4, 4), (0.4, 3.8)), 32 | (5.0, 5.0, 0.0, 10.0, 0.0, 20.0, Region(0, 0, 4, 4), (2.0, 3.0)), 33 | (1.0, 1.0, 0.0, 10.0, 0.0, 20.0, Region(2, 3, 4, 4), (2.4, 6.8)), 34 | (10.0, 20.0, 0.0, 10.0, 0.0, 20.0, Region(0, 0, 4, 4), (4.0, 0.0)), 35 | ], 36 | ) 37 | def test_map_coordinate_to_hires_pixel( 38 | self, x, y, xmin, xmax, ymin, ymax, region, expected 39 | ): 40 | assert ( 41 | map_coordinate_to_hires_pixel(x, y, xmin, xmax, ymin, ymax, region) 42 | == expected 43 | ) 44 | 45 | @pytest.mark.parametrize( 46 | "px, py, xmin, xmax, ymin, ymax, region,expected", 47 | [ 48 | (0, 3, 0.0, 10.0, 0.0, 20.0, Region(0, 0, 4, 4), (1.25, 2.5)), 49 | (2, 2, 0.0, 10.0, 0.0, 20.0, Region(0, 0, 4, 4), (6.25, 7.5)), 50 | (1, 1, 0.0, 10.0, 0.0, 20.0, Region(0, 0, 4, 4), (3.75, 12.5)), 51 | (2, 3, 0.0, 10.0, 0.0, 20.0, Region(2, 3, 4, 4), (1.25, 17.5)), 52 | ], 53 | ) 54 | def test_map_pixel_to_coordinate( 55 | self, px, py, xmin, xmax, ymin, ymax, region, expected 56 | ): 57 | assert ( 58 | map_pixel_to_coordinate(px, py, xmin, xmax, ymin, ymax, region) == expected 59 | ) 60 | --------------------------------------------------------------------------------