├── .gitignore ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── docs ├── API_Documentation │ ├── arc.md │ ├── circle.md │ ├── ellipse.md │ ├── line.md │ ├── node.md │ ├── plot_coordinates.md │ ├── point.md │ ├── rectangle.md │ ├── scope.md │ └── tikz_picture.md ├── examples.md ├── index.md ├── installation.md ├── png │ ├── PlotCoords_rotate_Example.png │ ├── arc_ex_1.png │ ├── arc_ex_2.png │ ├── barycentric.png │ ├── basic.png │ ├── blowup.png │ ├── blowup_ex.png │ ├── cantor.png │ ├── cauchy_residue_thm_arc_ex.png │ ├── cauchy_residue_thm_ex.png │ ├── circle_center.png │ ├── circle_east.png │ ├── circle_ex_1.png │ ├── circle_ex_2.png │ ├── circle_intersections.png │ ├── circle_intersections_2.png │ ├── circle_north.png │ ├── circle_point_at_arg.png │ ├── circle_south.png │ ├── circle_west.png │ ├── circles.png │ ├── connect_circles.png │ ├── des.png │ ├── dots.png │ ├── draw_segments.png │ ├── ellipse_ex_1.png │ ├── favicon.png │ ├── fully_connected_neural_network.png │ ├── geometry_figure.png │ ├── grid.png │ ├── integration_ex.png │ ├── intersection_blobs.png │ ├── intersection_circles.png │ ├── line_and_two_nodes.png │ ├── line_end.png │ ├── line_ex_1.png │ ├── line_ex_2.png │ ├── line_midpoint.png │ ├── line_pos_a_t.png │ ├── line_start.png │ ├── linear_transformation_ex_1.png │ ├── linear_transformation_ex_2.png │ ├── lorenz_ex.png │ ├── nn_nodes.png │ ├── node_ex_1.png │ ├── node_ex_2.png │ ├── plotcoordinates_ex_1.png │ ├── plotcoordinates_ex_2.png │ ├── plotcoordinates_ex_3.png │ ├── polar.png │ ├── projective_cone.png │ ├── rectangle_ex_1.png │ ├── roots_of_unity.png │ ├── rotate_circles.png │ ├── rotate_plot.png │ ├── shift_plot.png │ ├── sphere_loop.png │ ├── spiral.png │ ├── test_region_and.png │ ├── test_region_not.png │ ├── test_region_or.png │ ├── test_region_xor.png │ ├── tikz_write_ex_1.png │ ├── tikz_write_ex_2.png │ ├── transformer.png │ └── tutorial_imgs │ │ ├── line.png │ │ ├── log_cut_step_1.png │ │ ├── log_cut_step_2.png │ │ ├── log_cut_step_3.png │ │ ├── neural_network_diagram.png │ │ ├── neural_network_step_1.png │ │ ├── neural_network_step_2.png │ │ ├── neural_network_step_3.png │ │ ├── neural_network_step_4.png │ │ └── neural_network_step_5.png ├── stylesheets │ └── extra.css └── tutorials.md ├── examples ├── _run_all_examples.sh ├── barycentric │ └── barycentric.py ├── basic │ └── basic_ex.py ├── blowup │ └── blowup.py ├── cantor │ └── cantor.py ├── cauchy_residue_thm │ ├── cauchy_residue_thm.py │ └── cauchy_residue_thm_arc.py ├── circle_intersections │ └── circle_intersections.py ├── circle_intersections_2 │ └── circle_intersections_2.py ├── circles │ └── circles.py ├── code.tex ├── controls │ └── controls.py ├── des │ └── des.py ├── geometry_figure │ └── geometry_figure.py ├── line_and_two_nodes │ └── line_and_two_nodes.py ├── linear_model │ └── linear_model.py ├── linear_transformations │ └── linear_transformations.py ├── lorenz │ └── lorenz.py ├── neural_network │ └── neural_network.py ├── neural_network_connection │ └── neural_network_connection.py ├── neural_network_simple │ └── neural_network_simple.py ├── polar │ └── polar.py ├── projective_cone │ └── projective_cone.py ├── relu │ └── relu.py ├── roots_of_unity │ └── roots_of_unity.py ├── rotate_circles │ └── rotate_circles.py ├── rotate_plot │ └── rotate_plot.py ├── shift_plot │ └── shift_plot.py ├── sigmoid │ └── sigmoid.py ├── sphere_loop │ └── sphere_loop.py ├── spiral │ └── spiral.py ├── symbolic_integration │ └── integrate_and_plot.py ├── tikz_code.tex ├── transformer │ └── pre_layer_transformer.py └── ven_diagrams │ └── intersections_scope_clip.py ├── mkdocs.yml ├── pyproject.toml ├── requirements.txt ├── src └── tikzpy │ ├── __init__.py │ ├── colors │ ├── __init__.py │ └── colors.py │ ├── drawing_objects │ ├── __init__.py │ ├── arc.py │ ├── circle.py │ ├── drawing_object.py │ ├── drawing_utils.py │ ├── ellipse.py │ ├── line.py │ ├── node.py │ ├── plotcoordinates.py │ ├── point.py │ ├── rectangle.py │ └── xy_plane.py │ ├── styles │ ├── __init__.py │ └── arrows_along_path.py │ ├── templates │ ├── tex_file.py │ └── tikz_code.tex │ ├── tikz_environments │ ├── __init__.py │ ├── clip.py │ ├── scope.py │ ├── tikz_command.py │ ├── tikz_environment.py │ ├── tikz_picture.py │ └── tikz_style.py │ └── utils │ ├── __init__.py │ ├── helpers.py │ └── types.py └── tests ├── drawing_objects ├── test_arc.py ├── test_circle.py ├── test_ellipse.py ├── test_line.py ├── test_node.py ├── test_plot_coordinates.py ├── test_point.py └── test_rectangle.py ├── integration ├── test_basic.py ├── test_cauchy_residue_thm.py ├── test_circles.py ├── test_controls.py ├── test_line_and_two_nodes.py ├── test_relu.py ├── test_shift_plot.py └── test_simple.py ├── test_action_arg.py ├── test_add_node.py ├── test_attribute_assignment.py ├── test_file_creation.py ├── test_helpler_funcs.py └── test_tikz_picture.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw* 2 | 3 | # "Get a Mac!", they said... 4 | *.DS_Store 5 | 6 | # tikz_code 7 | *tex_file.pdf 8 | 9 | # Pycharm 10 | .idea 11 | 12 | ### Python ### 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | 18 | # C extensions 19 | *.so 20 | 21 | # Distribution / packaging 22 | .Python 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | .eggs/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | wheels/ 35 | pip-wheel-metadata/ 36 | share/python-wheels/ 37 | *.egg-info/ 38 | .installed.cfg 39 | *.egg 40 | MANIFEST 41 | 42 | # PyInstaller 43 | # Usually these files are written by a python script from a template 44 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 45 | *.manifest 46 | *.spec 47 | 48 | # Installer logs 49 | pip-log.txt 50 | pip-delete-this-directory.txt 51 | 52 | # Unit test / coverage reports 53 | htmlcov/ 54 | .tox/ 55 | .nox/ 56 | .coverage 57 | .coverage.* 58 | .cache 59 | nosetests.xml 60 | coverage.xml 61 | *.cover 62 | *.py,cover 63 | .hypothesis/ 64 | .pytest_cache/ 65 | pytestdebug.log 66 | 67 | # Translations 68 | *.mo 69 | *.pot 70 | 71 | # Django stuff: 72 | *.log 73 | local_settings.py 74 | db.sqlite3 75 | db.sqlite3-journal 76 | 77 | # Flask stuff: 78 | instance/ 79 | .webassets-cache 80 | 81 | # Scrapy stuff: 82 | .scrapy 83 | 84 | # Sphinx documentation 85 | docs/_build/ 86 | doc/_build/ 87 | 88 | # PyBuilder 89 | target/ 90 | 91 | # Jupyter Notebook 92 | .ipynb_checkpoints 93 | 94 | # IPython 95 | profile_default/ 96 | ipython_config.py 97 | 98 | # pyenv 99 | .python-version 100 | 101 | # pipenv 102 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 103 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 104 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 105 | # install all needed dependencies. 106 | #Pipfile.lock 107 | 108 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 109 | __pypackages__/ 110 | 111 | # Celery stuff 112 | celerybeat-schedule 113 | celerybeat.pid 114 | 115 | # SageMath parsed files 116 | *.sage.py 117 | 118 | # Environments 119 | .env 120 | .venv 121 | env/ 122 | venv/ 123 | ENV/ 124 | env.bak/ 125 | venv.bak/ 126 | pythonenv* 127 | 128 | # Spyder project settings 129 | .spyderproject 130 | .spyproject 131 | 132 | # Rope project settings 133 | .ropeproject 134 | 135 | # mkdocs documentation 136 | /site 137 | 138 | # mypy 139 | .mypy_cache/ 140 | .dmypy.json 141 | dmypy.json 142 | 143 | # Pyre type checker 144 | .pyre/ 145 | 146 | # pytype static type analyzer 147 | .pytype/ 148 | 149 | # profiling data 150 | .prof 151 | 152 | ### TeX ### 153 | ## Core latex/pdflatex auxiliary files: 154 | *.aux 155 | *.lof 156 | *.lot 157 | *.fls 158 | *.out 159 | *.toc 160 | *.fmt 161 | *.fot 162 | *.cb 163 | *.cb2 164 | .*.lb 165 | 166 | ## Intermediate documents: 167 | *.dvi 168 | *.xdv 169 | *-converted-to.* 170 | # these rules might exclude image files for figures etc. 171 | # *.ps 172 | # *.eps 173 | # *.pdf 174 | 175 | ## Generated if empty string is given at "Please type another file name for output:" 176 | .pdf 177 | 178 | ## Bibliography auxiliary files (bibtex/biblatex/biber): 179 | *.bbl 180 | *.bcf 181 | *.blg 182 | *-blx.aux 183 | *-blx.bib 184 | *.run.xml 185 | 186 | ## Build tool auxiliary files: 187 | *.fdb_latexmk 188 | *.synctex 189 | *.synctex(busy) 190 | *.synctex.gz 191 | *.synctex.gz(busy) 192 | *.pdfsync 193 | 194 | ## Build tool directories for auxiliary files 195 | # latexrun 196 | latex.out/ 197 | 198 | ## Auxiliary and intermediate files from other packages: 199 | # algorithms 200 | *.alg 201 | *.loa 202 | 203 | # achemso 204 | acs-*.bib 205 | 206 | # amsthm 207 | *.thm 208 | 209 | # beamer 210 | *.nav 211 | *.pre 212 | *.snm 213 | *.vrb 214 | 215 | # changes 216 | *.soc 217 | 218 | # comment 219 | *.cut 220 | 221 | # cprotect 222 | *.cpt 223 | 224 | # elsarticle (documentclass of Elsevier journals) 225 | *.spl 226 | 227 | # endnotes 228 | *.ent 229 | 230 | # fixme 231 | *.lox 232 | 233 | # feynmf/feynmp 234 | *.mf 235 | *.mp 236 | *.t[1-9] 237 | *.t[1-9][0-9] 238 | *.tfm 239 | 240 | #(r)(e)ledmac/(r)(e)ledpar 241 | *.end 242 | *.?end 243 | *.[1-9] 244 | *.[1-9][0-9] 245 | *.[1-9][0-9][0-9] 246 | *.[1-9]R 247 | *.[1-9][0-9]R 248 | *.[1-9][0-9][0-9]R 249 | *.eledsec[1-9] 250 | *.eledsec[1-9]R 251 | *.eledsec[1-9][0-9] 252 | *.eledsec[1-9][0-9]R 253 | *.eledsec[1-9][0-9][0-9] 254 | *.eledsec[1-9][0-9][0-9]R 255 | 256 | # glossaries 257 | *.acn 258 | *.acr 259 | *.glg 260 | *.glo 261 | *.gls 262 | *.glsdefs 263 | *.lzo 264 | *.lzs 265 | 266 | # uncomment this for glossaries-extra (will ignore makeindex's style files!) 267 | # *.ist 268 | 269 | # gnuplottex 270 | *-gnuplottex-* 271 | 272 | # gregoriotex 273 | *.gaux 274 | *.gtex 275 | 276 | # htlatex 277 | *.4ct 278 | *.4tc 279 | *.idv 280 | *.lg 281 | *.trc 282 | *.xref 283 | 284 | # hyperref 285 | *.brf 286 | 287 | # knitr 288 | *-concordance.tex 289 | # TODO Comment the next line if you want to keep your tikz graphics files 290 | *.tikz 291 | *-tikzDictionary 292 | 293 | # listings 294 | *.lol 295 | 296 | # luatexja-ruby 297 | *.ltjruby 298 | 299 | # makeidx 300 | *.idx 301 | *.ilg 302 | *.ind 303 | 304 | # minitoc 305 | *.maf 306 | *.mlf 307 | *.mlt 308 | *.mtc 309 | *.mtc[0-9]* 310 | *.slf[0-9]* 311 | *.slt[0-9]* 312 | *.stc[0-9]* 313 | 314 | # minted 315 | _minted* 316 | *.pyg 317 | 318 | # morewrites 319 | *.mw 320 | 321 | # nomencl 322 | *.nlg 323 | *.nlo 324 | *.nls 325 | 326 | # pax 327 | *.pax 328 | 329 | # pdfpcnotes 330 | *.pdfpc 331 | 332 | # sagetex 333 | *.sagetex.sage 334 | *.sagetex.py 335 | *.sagetex.scmd 336 | 337 | # scrwfile 338 | *.wrt 339 | 340 | # sympy 341 | *.sout 342 | *.sympy 343 | sympy-plots-for-*.tex/ 344 | 345 | # pdfcomment 346 | *.upa 347 | *.upb 348 | 349 | # pythontex 350 | *.pytxcode 351 | pythontex-files-*/ 352 | 353 | # tcolorbox 354 | *.listing 355 | 356 | # thmtools 357 | *.loe 358 | 359 | # TikZ & PGF 360 | *.dpth 361 | *.md5 362 | *.auxlock 363 | 364 | # todonotes 365 | *.tdo 366 | 367 | # vhistory 368 | *.hst 369 | *.ver 370 | 371 | # easy-todo 372 | *.lod 373 | 374 | # xcolor 375 | *.xcp 376 | 377 | # xmpincl 378 | *.xmpi 379 | 380 | # xindy 381 | *.xdy 382 | 383 | # xypic precompiled matrices and outlines 384 | *.xyc 385 | *.xyd 386 | 387 | # endfloat 388 | *.ttt 389 | *.fff 390 | 391 | # Latexian 392 | TSWLatexianTemp* 393 | 394 | ## Editors: 395 | # WinEdt 396 | *.bak 397 | *.sav 398 | 399 | # Texpad 400 | .texpadtmp 401 | 402 | # LyX 403 | *.lyx~ 404 | 405 | # Kile 406 | *.backup 407 | 408 | # gummi 409 | .*.swp 410 | 411 | # KBibTeX 412 | *~[0-9]* 413 | 414 | # TeXnicCenter 415 | *.tps 416 | 417 | # auto folder when using emacs and auctex 418 | ./auto/* 419 | *.el 420 | 421 | # expex forward references with \gathertags 422 | *-tags.tex 423 | 424 | # standalone packages 425 | *.sta 426 | 427 | # Makeindex log files 428 | *.lpz 429 | 430 | # REVTeX puts footnotes in the bibliography by default, unless the nofootinbib 431 | # option is specified. Footnotes are the stored in a file with suffix Notes.bib. 432 | # Uncomment the next line to have this generated file ignored. 433 | #*Notes.bib 434 | 435 | ### TeX Patch ### 436 | # LIPIcs / OASIcs 437 | *.vtc 438 | 439 | # glossaries 440 | *.glstex 441 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Luke Trujillo 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include tikzpy/templates/tex_file.tex 2 | include tikzpy/templates/tikz_code.tex 3 | 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: default 2 | default: black lint test 3 | 4 | .PHONY: black 5 | black: 6 | black tests src 7 | 8 | .PHONY: lint 9 | lint: 10 | ruff check src/tikzpy/*.py 11 | 12 | .PHONY: test 13 | test: 14 | pytest tests 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tikz-Python 2 | An object-oriented Python approach towards providing a giant wrapper for Tikz code, with the goal of streamlining the process of creating complex figures for TeX documents. 3 | 4 | To install it, run 5 | ``` 6 | pip install tikz-python 7 | ``` 8 | 9 | ## Documentation 10 | 11 | We have documentation now! Please visit the [documentation](https://ltrujello.github.io/Tikz-Python) site. 12 | 13 | ## Examples 14 | Want to see some nice examples of what this package can do? See [here](https://ltrujello.github.io/Tikz-Python/examples/). 15 | 16 | ## How to Use: Basics 17 | An example of this package in action is below. 18 | ```python 19 | from tikzpy import TikzPicture # Import the class TikzPicture 20 | 21 | tikz = TikzPicture() 22 | tikz.circle((0, 0), 3, options="thin, fill=orange!15") 23 | 24 | arc_one = tikz.arc((3, 0), 0, 180, x_radius=3, y_radius=1.5, options="dashed") 25 | arc_two = tikz.arc((-3, 0), 180, 360, x_radius=3, y_radius=1.5) 26 | 27 | tikz.show() # Displays a pdf of the drawing to the user 28 | ``` 29 | which produces 30 | 31 | 32 | We explain line-by-line the above code. 33 | 34 | * `from tikzpy import TikzPicture` imports the `TikzPicture` class from the `tikzpy` package. 35 | 36 | * The second line of code is analogous to the TeX code `\begin{tikzpicture}` and `\end{tikzpicture}`. The variable `tikz` is now a tikz environment, specifically an instance of the class `TikzPicture`, and we can now append drawings to it. 37 | 38 | * The third, fourth, and fifth lines draw a filled circle and two elliptic arcs, which give the illusion of a sphere. 39 | 40 | * In the last line, the call `show()` immediately displays the PDF of the drawing to the user. 41 | 42 | -------------------------------------------------------------------------------- /docs/API_Documentation/arc.md: -------------------------------------------------------------------------------- 1 | # Arc 2 | 3 | ::: tikzpy.drawing_objects.arc.Arc 4 | members: [] 5 | 6 | 7 | ## Example 8 | Here we draw and fill a sequence of arcs. We also demonstrate `draw_from_start` set to `True` and `False`. In the code below, it is by default set to `True`. 9 | ```python 10 | from tikzpy import TikzPicture 11 | from tikzpy.utils import rainbow_colors 12 | 13 | tikz = TikzPicture() 14 | 15 | for i in range(1, 10): 16 | t = 4 / i 17 | arc = tikz.arc((0, 0), 0, 180, radius=t, options=f"fill={rainbow_colors(i)}") 18 | 19 | ``` 20 | This generates the image 21 | 22 | 23 | 24 | If instead we would like these arcs sharing the same center, we can use the same code, but pass in `draw_from_start=False` to achieve 25 | 26 | 27 | 28 | Without this option, if we were forced to specify the point at which each arc should begin drawing, we would have to calculate the x-shift for every arc and apply such a shift to keep the centers aligned. That sounds inefficient and like a waste of time to achieve something so simple, right? 29 | 30 | 31 | ## Methods 32 | `Arc` has access to methods `.shift()`, `.scale()`, `.rotate()`, which behave as one would expect and takes in parameters as described before. 33 | 34 | ## A few comments... 35 | This class not only provides a wrapper to draw arcs, but it also fixes a few things that Tikz's `\draw arc` command simply gets wrong and confuses users with. 36 | 37 | 1. With Tikz in TeX, to draw a circular arc one must specify `start_angle` and `end_angle`. These make sense: they are the start and end angles of the arc relative to the horizontal. To draw an elliptic arc, one must again specify `start_angle` and `end_angle`, but these actually do not represent the starting and end angles of the elliptic arc. They are the parameters `t` which parameterize the ellipse `(a*cos(t), b*sin(t))`. This makes drawing elliptic arcs inconvenient. 38 | 39 | 2. With Tikz in TeX, the position of the arc is specified by where the arc should start drawing. However, it is sometimes easier to specify the *center* of the arc. 40 | 41 | With Tikz-Python, `start_angle` and `end_angle` will always coincide with the starting and end angles, so the user will not get weird unexpected behavior. Additionally, the user can specify the arc position via its center by setting `draw_from_start=False`, but they can also fall back on the default behavior. 42 | 43 | -------------------------------------------------------------------------------- /docs/API_Documentation/circle.md: -------------------------------------------------------------------------------- 1 | # Circle 2 | 3 | ::: tikzpy.drawing_objects.circle.Circle 4 | 5 | ## Examples 6 | Here we create several circles, making use of the `action` parameter. 7 | ```python 8 | from tikzpy import TikzPicture 9 | 10 | tikz = TikzPicture() 11 | tikz.circle((0, 0), 1.25) #action="draw" by default 12 | tikz.line((0, 0), (0, 1.25), options="dashed") 13 | tikz.circle((3, 0), 1, options="thick, fill=red!60", action="filldraw") 14 | tikz.circle((6, 0), 1.25, options="Green!50", action="fill") 15 | tikz.show() 16 | ``` 17 | 18 | 19 | 20 | We can also use circles to create the [Hawaiian Earing](https://en.wikipedia.org/wiki/Hawaiian_earring). 21 | 22 | ```python 23 | from tikzpy import TikzPicture 24 | 25 | tikz = TikzPicture() 26 | 27 | radius = 5 28 | for i in range(1, 60): 29 | n = radius / i 30 | tikz.circle((n, 0), n) 31 | tikz.show() 32 | ``` 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/API_Documentation/ellipse.md: -------------------------------------------------------------------------------- 1 | # Ellipse 2 | 3 | ::: tikzpy.drawing_objects.ellipse.Ellipse 4 | 5 | ## Example 6 | Here we draw and ellipse and define the major and minors axes. 7 | ```python 8 | import tikzpy 9 | 10 | tikz = tikzpy.TikzPicture() 11 | 12 | # x,y axes 13 | tikz.line((-5, 0), (5, 0), options="Gray!40, ->") 14 | tikz.line((0, -5), (0, 5), options="Gray!40, ->") 15 | # Ellipse 16 | ellipse = tikz.ellipse( 17 | (0, 0), 4, 3, options="fill=ProcessBlue!70, opacity=0.4", action="filldraw" 18 | ) 19 | # Labels 20 | h_line = tikz.line((0, 0), (ellipse.x_axis, 0), options="thick, dashed, ->") 21 | v_line = tikz.line((0, 0), (0, ellipse.y_axis), options="thick, dashed, ->") 22 | tikz.node(h_line.midpoint, options="below", text="Major") 23 | tikz.node(v_line.midpoint, options="left", text="Minor") 24 | ``` 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /docs/API_Documentation/line.md: -------------------------------------------------------------------------------- 1 | # Line 2 | 3 | ::: tikzpy.drawing_objects.line.Line 4 | 5 | # Examples 6 | 7 | Here's an example of us using the `Line` class. 8 | ```python 9 | import tikzpy 10 | 11 | tikz = tikzpy.TikzPicture() 12 | tikz.line((0, 0), (4, 0), options="->", control_pts=[(1, 1), (3, -1)] 13 | ``` 14 | which generates 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/API_Documentation/node.md: -------------------------------------------------------------------------------- 1 | # Node 2 | 3 | ::: tikzpy.drawing_objects.node.Node 4 | 5 | ## Examples 6 | Here we use some nodes to label a figure explaining the logarithm branch cut 7 | ```python 8 | import tikzpy 9 | 10 | tikz = tikzpy.TikzPicture() 11 | # x,y axes 12 | tikz.line((-4, 0), (4, 0), options="Gray!40, ->") 13 | tikz.line((0, -4), (0, 4), options="Gray!40, ->") 14 | # Cut 15 | tikz.line((-4, 0), (0, 0), options="thick") 16 | # Line out 17 | tikz.line((0, 0), (1.414, 1.414), options="-o") 18 | tikz.arc((1, 0), 0, 45, radius=1, options="dashed") 19 | 20 | # Labels 21 | tikz.node((3.6, -0.2), text="$x$") 22 | tikz.node((-0.24, 3.53), text="$iy$") 23 | tikz.node((1.3, 0.4), text="$\\theta$") 24 | tikz.node((2.1, 1.7), text="$z = re^{i\\theta}$") 25 | tikz.node((-2, 0.3), text="Cut") 26 | ``` 27 | which produces 28 | 29 | 30 | Here's another example of usings nodes to illustrate the concept of a multivariable function. 31 | ```python 32 | import tikzpy 33 | 34 | tikz = tikzpy.TikzPicture() 35 | 36 | arrow_len = 2 37 | box_width = 2 38 | # Lines and rectangles 39 | input_arrow = tikz.line((0, 0), (arrow_len, 0), options="->") 40 | box = tikz.rectangle_from_west(input_arrow.end, width=box_width, height=1) 41 | output_arrow = tikz.line(box.east, box.east + (arrow_len, 0), options="->") 42 | 43 | # Labels 44 | tikz.node((-1.2, 0), text="$(x_1, \dots, x_n)$") 45 | tikz.node(input_arrow.midpoint() + (0, 0.3), text="input") 46 | tikz.node(box.center, text="$f$") 47 | tikz.node(output_arrow.midpoint() + (0, 0.3), text="output") 48 | tikz.node((7.3, 0), text="$f(x_1, \dots, x_n)$") 49 | tikz.show() 50 | ``` 51 | 52 | 53 | -------------------------------------------------------------------------------- /docs/API_Documentation/plot_coordinates.md: -------------------------------------------------------------------------------- 1 | # PlotCoordinates 2 | 3 | ::: tikzpy.drawing_objects.plotcoordinates.PlotCoordinates 4 | 5 | ## Examples 6 | Introducing examples of `PlotCoordinates` gives us an opportunity to illustrate the optional parameter `action`. By default, `action` is `"draw"` (analogous to `\draw` in Tikz) so the code below 7 | ```python 8 | import tikzpy 9 | 10 | tikz = tikzpy.TikzPicture() 11 | points = [(2, 2), (4, 0), (1, -3), (-2, -1), (-1, 3)] 12 | plot = tikz.plot_coordinates(points) # action="draw" by default 13 | plot.plot_options = "smooth cycle, tension = 0.5" 14 | ``` 15 | produces the image 16 | 17 | 18 | 19 | Alternatively we can set `action = "fill"` (analogous to `\fill` in Tikz) as in the code below 20 | ```python 21 | import tikzpy 22 | 23 | tikz = tikzpy.TikzPicture() 24 | points = [(2, 2), (4, 0), (1, -3), (-2, -1), (-1, 3)] 25 | plot = tikz.plot_coordinates(points, options="Blue", action="fill") 26 | plot.plot_options = "smooth cycle, tension = 0.5" 27 | ``` 28 | to produce the image 29 | 30 | 31 | 32 | If we want both, we can set `action = "filldraw"` (analogous to `\filldraw` in Tikz) 33 | ```python 34 | import tikzpy 35 | 36 | tikz = tikzpy.TikzPicture() 37 | points = [(2, 2), (4, 0), (1, -3), (-2, -1), (-1, 3)] 38 | plot = tikz.plot_coordinates(points, options="Blue", action="filldraw") 39 | plot.options = "fill=ProcessBlue!50" 40 | plot.plot_options = "smooth cycle, tension = 0.5" 41 | ``` 42 | which produces. 43 | 44 | 45 | Finally, we can set `action = "path"` (analogous to `\path` in Tikz), but as one would expect this doesn't draw anything. 46 | 47 | `PlotCoordinates` has methods `.shift()`, `.scale`, and `.rotate`, similar to the class `Line`, and the parameters behave similarly. These methods are more interestingly used on `PlotCoordinates` than on `Line`. For example, the code 48 | ```python 49 | import tikzpy 50 | 51 | tikz = tikzpy.TikzPicture() 52 | points = [(14.4, 3.2), (16.0, 3.6), (16.8, 4.8), (16.0, 6.8), (16.4, 8.8), (13.6, 8.8), (12.4, 7.6), (12.8, 5.6), (12.4, 3.6)] 53 | 54 | for i in range(0, 20): 55 | options = f"fill = {rainbow_colors(i)}, opacity = 0.7" 56 | # Requires \usetikzlibrary{hobby} here 57 | plot_options = "smooth, tension=.5, closed hobby" 58 | plot = tikz.plot_coordinates(points, options, plot_options) 59 | plot.scale((20 - i) / 20) # Shrink it 60 | plot.rotate(15 * i) # Rotate it 61 | ``` 62 | generates the image 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /docs/API_Documentation/point.md: -------------------------------------------------------------------------------- 1 | # Point 2 | 3 | ::: tikzpy.drawing_objects.point.Point 4 | 5 | # Examples 6 | The `Point` class can be instantiated from a tuple or at least two `Number`s. One can also represent a point in 3D with this class. 7 | 8 | ```python 9 | >>> from tikzpy import Point 10 | >>> my_point = Point(-1, 2) 11 | >>> my_point.x 12 | -1 13 | >>> my_point.y 14 | 2 15 | ``` 16 | You can also perform arithmetic with `Point` objects, either with other `Point` objects or with Python tuples. For example, the following are all valid. 17 | ```python 18 | >>> my_point + (1, 1) # Add it to another tuple 19 | Point(0, 3) 20 | >>> my_point + Point(2, 2) # Add it with another point object 21 | Point(1, 4) 22 | >>> 2 * my_point # Can also do my_point * 2 23 | Point(-2, 4) 24 | >>> my_point / 3 25 | Point(-0.33333333, 0.666666666) 26 | ``` 27 | 28 | This allows you to write things like 29 | ```python 30 | >>> circle = tikz.circle((0,0), radius=3) 31 | >>> circle.center += (1, 1) # This is valid 32 | >>> circle.center /= 3 # Also valid 33 | ``` 34 | and this feature becomes quite useful in drawings that are highly complex. 35 | -------------------------------------------------------------------------------- /docs/API_Documentation/rectangle.md: -------------------------------------------------------------------------------- 1 | # Rectangle 2 | 3 | ::: tikzpy.drawing_objects.rectangle.Rectangle 4 | 5 | ## Example 6 | Rectangles are often used as a background to many figures; in this case, 7 | we create a fancy colored background. 8 | 9 | ```python 10 | from tikzpy import TikzPicture, Rectangle 11 | import math 12 | 13 | tikz = TikzPicture(center=True) 14 | 15 | yellow_box: Rectangle = tikz.rectangle_from_center((0, 0), width=7, height=5, options="rounded corners, Yellow!30",action="filldraw") 16 | # Params 17 | r = 2 18 | n_nodes = 7 19 | nodes = [] 20 | # Draw the nodes 21 | for i in range(1, n_nodes + 1): 22 | angle = 2 * math.pi * i / n_nodes 23 | x = r * math.cos(angle) 24 | y = r * math.sin(angle) 25 | node = tikz.node((x, y), text=f"$A_{{{i}}}$") 26 | nodes.append(node) 27 | 28 | # Draw the lines between the nodes 29 | for i in range(len(nodes)): 30 | start = nodes[i].position 31 | end = nodes[(i + 1) % len(nodes)].position 32 | tikz.line(start, end, options="->, shorten >= 10pt, shorten <=10pt") 33 | tikz.show() 34 | ``` 35 | 36 | 37 | -------------------------------------------------------------------------------- /docs/API_Documentation/scope.md: -------------------------------------------------------------------------------- 1 | # Scope 2 | 3 | ::: tikzpy.tikz_environments.scope.Scope 4 | 5 | ::: tikzpy.tikz_environments.clip.Clip -------------------------------------------------------------------------------- /docs/API_Documentation/tikz_picture.md: -------------------------------------------------------------------------------- 1 | # TikzPicture 2 | 3 | ::: tikzpy.tikz_environments.tikz_picture.TikzPicture 4 | options: 5 | inherited_members: true 6 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to Tikz-Python! 2 | 3 | ![Image Title](png/blowup.png){align=right width=40%} 4 | 5 | Tikz-Python is intended to be an easy-to-use, no-nonsense python package meant to serve as an interface to TikZ code. With Tikz-Python, you can have your beautiful vector graphics and eat it too! (erh, maybe). 6 | 7 | 8 | Install it below as follows: 9 | ``` 10 | pip install tikz-python 11 | ``` 12 | 13 | Want to see some nice examples of what this package can do? See [here](examples.md). 14 | 15 | ## Usage 16 | Suppose we want to create a sphere. We can achieve this as follows. 17 | ```python 18 | from tikzpy import TikzPicture 19 | 20 | tikz = TikzPicture() # Initialize empty canvas 21 | tikz.circle((0, 0), 3, options="thin, fill=orange!15") 22 | 23 | # Draw two arcs to give 3d-illusion 24 | tikz.arc((3, 0), 0, 180, x_radius=3, y_radius=1.5, options="dashed") 25 | tikz.arc((-3, 0), 180, 360, x_radius=3, y_radius=1.5) 26 | 27 | tikz.show() # Displays a pdf of the drawing to the user 28 | ``` 29 | which produces 30 | 31 | 32 | ## Why Tikz-Python 33 | 34 | With Tikz-Python, you generate TikZ code by writing python code. And Python is much nicer than TeX. 35 | 36 | * Instead of spending a lot of time tediously writing messy, unreadable TikZ code to generate your desired figure, 37 | you can use TikZ-Python to quickly create your figure *as a Python script*. Your Python code will definitely be much 38 | more modular and extensible than the raw TikZ code you'd end up writing. 39 | 40 | * At any time, you can compile and look at your figure in a sandbox environment by calling `TikzPicture.show()` 41 | 42 | * With a sandbox environment for compilation, compiling will be faster versus you editing and re-compiling your tikz code directly in whatever 100-page document you're working in. 43 | 44 | * Once you're happy with your drawing, you can copy the generated TikZ code and paste it into your LaTeX document. Or, you can 45 | save your code to a file by calling `TikzPicture.write(file_destination)`. 46 | 47 | Additionally, Tikz-Python encodes lines, circles, rectangles, etc. as data structures. These data structures have useful properties and methods 48 | that can be used to create other drawings. 49 | 50 | For example, suppose I want to create a line and two labels at the ends. The code below achieves this 51 | ```python 52 | from tikzpy import TikzPicture 53 | 54 | tikz = TikzPicture() 55 | line = tikz.line((0, 0), (1, 1), options="thick, blue, o-o") 56 | start_node = tikz.node(line.start, options="below", text="Start!") 57 | end_node = tikz.node(line.end, options="above", text="End!") 58 | tikz.show() # Displays a pdf of the drawing to the user 59 | ``` 60 | which produces 61 | 62 | Saving the line as a variable `line` allows us to pass in `line.start` and `line.end` into the node positions, so we don't have to type out the exact coordinates. 63 | This is great, because it makes our code more modular and therefore easier to change. With TikZ alone, you'd need to type out exact coordinates, and update every single one each time you make a minor adjustment to your code. 64 | 65 | If we were working in an interactive python shell with the code above, we would be able to see that these functions return classes with useful attributes: 66 | ```python 67 | >>> line.start 68 | (0,0) 69 | >>> line.end 70 | (1,1) 71 | >>> start_node.text 72 | "Start!" 73 | ``` 74 | Additionally, you can `print` your tikz object to see the code generated 75 | 76 | ```python 77 | >>> print(tikz) 78 | \begin{tikzpicture} 79 | \draw[thick, blue, o-o] (0, 0) to (1, 1); 80 | \node[below] at (0, 0) { Start! }; 81 | \node[above] at (1, 1) { End! }; 82 | \end{tikzpicture} 83 | ``` 84 | which you can then use to export to your project. 85 | 86 | 87 | ## Background 88 | TikZ is a wrapper of the TeX-based graphics package PGF (see [here](https://github.com/pgf-tikz/pgf/blob/master/tex/generic/pgf/frontendlayer/tikz/tikz.code.tex)), and it is commonly used in LaTeX documents to produce beautiful graphics. 89 | However, the power of TikZ comes with a tradeoff: it is extremely tedious to use, learn, understand, and iterate on. 90 | 91 | The main problem with Tikz is that even though Tikz is very powerful, it is often the case that 92 | nonexperts who use TikZ end up producing subpar images. 93 | The reason for this is because of the fact that in order to create beautiful images with TikZ, you also need to deeply understand 94 | LaTeX, TeX, PGF, and the history, bugs, cryptic error messages, and ridiculous quirks (and there are *many* quirks) of these languages. 95 | This takes years of practice and for the average person this is not realistic or desirable. 96 | 97 | This can be seen in research papers; even in high quality research papers, the graphics are usually not that great and it totally makes sense why. 98 | It's probably because the authors are too busy being an expert in their own field of work to sit down and read a 1300 page manual on TikZ. 99 | 100 | ## About 101 | I started this project after realizing 102 | 103 | * most TikZ code is repetitive. 104 | 105 | * TikZ was designed smart; it has an inherent object oriented pattern in its usage which we can exploit and automate. 106 | 107 | * I really hate writing TeX and TikZ code. 108 | 109 | I wrote the first version of this as a math undergraduate while writing a large set of notes. As a heavy user of LaTeX, TikZ, PGF, I knew how to design the project to enable efficient development of TikZ code. 110 | Then I became a professional software engineer, and I refactored the code to a modern python package while maintaining the original desired goals of the package. 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | 4 | To install Tikz-Python, run 5 | ``` 6 | pip install tikz-python 7 | ``` 8 | In order to compile your drawings, you need an installation of `LaTeX`, with PDF and TikZ libraries and latexmk. Unless you decided to customize your LaTeX installation, all of these should already be on your system. 9 | 10 | If you don't have these on your system, you can of course still use Tikz-Python to generate Tikz code, which you can then take to another system with LaTeX installed to compile on. 11 | -------------------------------------------------------------------------------- /docs/png/PlotCoords_rotate_Example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/PlotCoords_rotate_Example.png -------------------------------------------------------------------------------- /docs/png/arc_ex_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/arc_ex_1.png -------------------------------------------------------------------------------- /docs/png/arc_ex_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/arc_ex_2.png -------------------------------------------------------------------------------- /docs/png/barycentric.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/barycentric.png -------------------------------------------------------------------------------- /docs/png/basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/basic.png -------------------------------------------------------------------------------- /docs/png/blowup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/blowup.png -------------------------------------------------------------------------------- /docs/png/blowup_ex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/blowup_ex.png -------------------------------------------------------------------------------- /docs/png/cantor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/cantor.png -------------------------------------------------------------------------------- /docs/png/cauchy_residue_thm_arc_ex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/cauchy_residue_thm_arc_ex.png -------------------------------------------------------------------------------- /docs/png/cauchy_residue_thm_ex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/cauchy_residue_thm_ex.png -------------------------------------------------------------------------------- /docs/png/circle_center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/circle_center.png -------------------------------------------------------------------------------- /docs/png/circle_east.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/circle_east.png -------------------------------------------------------------------------------- /docs/png/circle_ex_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/circle_ex_1.png -------------------------------------------------------------------------------- /docs/png/circle_ex_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/circle_ex_2.png -------------------------------------------------------------------------------- /docs/png/circle_intersections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/circle_intersections.png -------------------------------------------------------------------------------- /docs/png/circle_intersections_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/circle_intersections_2.png -------------------------------------------------------------------------------- /docs/png/circle_north.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/circle_north.png -------------------------------------------------------------------------------- /docs/png/circle_point_at_arg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/circle_point_at_arg.png -------------------------------------------------------------------------------- /docs/png/circle_south.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/circle_south.png -------------------------------------------------------------------------------- /docs/png/circle_west.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/circle_west.png -------------------------------------------------------------------------------- /docs/png/circles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/circles.png -------------------------------------------------------------------------------- /docs/png/connect_circles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/connect_circles.png -------------------------------------------------------------------------------- /docs/png/des.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/des.png -------------------------------------------------------------------------------- /docs/png/dots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/dots.png -------------------------------------------------------------------------------- /docs/png/draw_segments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/draw_segments.png -------------------------------------------------------------------------------- /docs/png/ellipse_ex_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/ellipse_ex_1.png -------------------------------------------------------------------------------- /docs/png/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/favicon.png -------------------------------------------------------------------------------- /docs/png/fully_connected_neural_network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/fully_connected_neural_network.png -------------------------------------------------------------------------------- /docs/png/geometry_figure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/geometry_figure.png -------------------------------------------------------------------------------- /docs/png/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/grid.png -------------------------------------------------------------------------------- /docs/png/integration_ex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/integration_ex.png -------------------------------------------------------------------------------- /docs/png/intersection_blobs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/intersection_blobs.png -------------------------------------------------------------------------------- /docs/png/intersection_circles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/intersection_circles.png -------------------------------------------------------------------------------- /docs/png/line_and_two_nodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/line_and_two_nodes.png -------------------------------------------------------------------------------- /docs/png/line_end.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/line_end.png -------------------------------------------------------------------------------- /docs/png/line_ex_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/line_ex_1.png -------------------------------------------------------------------------------- /docs/png/line_ex_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/line_ex_2.png -------------------------------------------------------------------------------- /docs/png/line_midpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/line_midpoint.png -------------------------------------------------------------------------------- /docs/png/line_pos_a_t.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/line_pos_a_t.png -------------------------------------------------------------------------------- /docs/png/line_start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/line_start.png -------------------------------------------------------------------------------- /docs/png/linear_transformation_ex_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/linear_transformation_ex_1.png -------------------------------------------------------------------------------- /docs/png/linear_transformation_ex_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/linear_transformation_ex_2.png -------------------------------------------------------------------------------- /docs/png/lorenz_ex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/lorenz_ex.png -------------------------------------------------------------------------------- /docs/png/nn_nodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/nn_nodes.png -------------------------------------------------------------------------------- /docs/png/node_ex_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/node_ex_1.png -------------------------------------------------------------------------------- /docs/png/node_ex_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/node_ex_2.png -------------------------------------------------------------------------------- /docs/png/plotcoordinates_ex_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/plotcoordinates_ex_1.png -------------------------------------------------------------------------------- /docs/png/plotcoordinates_ex_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/plotcoordinates_ex_2.png -------------------------------------------------------------------------------- /docs/png/plotcoordinates_ex_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/plotcoordinates_ex_3.png -------------------------------------------------------------------------------- /docs/png/polar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/polar.png -------------------------------------------------------------------------------- /docs/png/projective_cone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/projective_cone.png -------------------------------------------------------------------------------- /docs/png/rectangle_ex_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/rectangle_ex_1.png -------------------------------------------------------------------------------- /docs/png/roots_of_unity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/roots_of_unity.png -------------------------------------------------------------------------------- /docs/png/rotate_circles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/rotate_circles.png -------------------------------------------------------------------------------- /docs/png/rotate_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/rotate_plot.png -------------------------------------------------------------------------------- /docs/png/shift_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/shift_plot.png -------------------------------------------------------------------------------- /docs/png/sphere_loop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/sphere_loop.png -------------------------------------------------------------------------------- /docs/png/spiral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/spiral.png -------------------------------------------------------------------------------- /docs/png/test_region_and.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/test_region_and.png -------------------------------------------------------------------------------- /docs/png/test_region_not.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/test_region_not.png -------------------------------------------------------------------------------- /docs/png/test_region_or.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/test_region_or.png -------------------------------------------------------------------------------- /docs/png/test_region_xor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/test_region_xor.png -------------------------------------------------------------------------------- /docs/png/tikz_write_ex_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/tikz_write_ex_1.png -------------------------------------------------------------------------------- /docs/png/tikz_write_ex_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/tikz_write_ex_2.png -------------------------------------------------------------------------------- /docs/png/transformer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/transformer.png -------------------------------------------------------------------------------- /docs/png/tutorial_imgs/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/tutorial_imgs/line.png -------------------------------------------------------------------------------- /docs/png/tutorial_imgs/log_cut_step_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/tutorial_imgs/log_cut_step_1.png -------------------------------------------------------------------------------- /docs/png/tutorial_imgs/log_cut_step_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/tutorial_imgs/log_cut_step_2.png -------------------------------------------------------------------------------- /docs/png/tutorial_imgs/log_cut_step_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/tutorial_imgs/log_cut_step_3.png -------------------------------------------------------------------------------- /docs/png/tutorial_imgs/neural_network_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/tutorial_imgs/neural_network_diagram.png -------------------------------------------------------------------------------- /docs/png/tutorial_imgs/neural_network_step_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/tutorial_imgs/neural_network_step_1.png -------------------------------------------------------------------------------- /docs/png/tutorial_imgs/neural_network_step_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/tutorial_imgs/neural_network_step_2.png -------------------------------------------------------------------------------- /docs/png/tutorial_imgs/neural_network_step_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/tutorial_imgs/neural_network_step_3.png -------------------------------------------------------------------------------- /docs/png/tutorial_imgs/neural_network_step_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/tutorial_imgs/neural_network_step_4.png -------------------------------------------------------------------------------- /docs/png/tutorial_imgs/neural_network_step_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/docs/png/tutorial_imgs/neural_network_step_5.png -------------------------------------------------------------------------------- /docs/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --md-primary-fg-color: #222b3a; 3 | --md-primary-fg-color--light: #5d6cc0; 4 | --md-primary-fg-color--dark: #303fa1; 5 | --md-primary-bg-color: #fff; 6 | --md-primary-bg-color--light: #ffffffb3; 7 | --md-accent-fg-color: #526cfe; 8 | --md-accent-fg-color--transparent: #526cfe1a; 9 | --md-accent-bg-color: #fff; 10 | --md-accent-bg-color--light: #ffffffb3 11 | } 12 | 13 | 14 | .md-content { 15 | --md-typeset-a-color: #09bfdd; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /examples/_run_all_examples.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Usage: If you want to run, compile, and view all the pdfs: 4 | # ./_run_all_examples.sh show 5 | # If you want to simply run the python code but not compile or view the pdfs 6 | # ./_run_all_examples.sh 7 | 8 | for pyfile in `find . -name "*.py" -type f`; do 9 | if [ "$1" = "show" ] 10 | then 11 | python3 $pyfile || break # Run the .py 12 | else 13 | sed -i "" "s|^tikz.show()|#tikz.show()|g" $pyfile # Comment out tikz.show(). Don't want to see the pdf. 14 | python3 $pyfile || break # Run the .py 15 | sed -i "" "s|^#tikz.show()|tikz.show()|g" $pyfile # Uncomment tikz.show() 16 | fi 17 | done -------------------------------------------------------------------------------- /examples/barycentric/barycentric.py: -------------------------------------------------------------------------------- 1 | #!/bin/bash/python3 2 | import statistics 3 | import queue 4 | from tikzpy import TikzPicture 5 | from tikzpy.colors import xcolors 6 | 7 | 8 | def nth_subdivision(n): 9 | iters = 0 10 | for i in range(n): 11 | tikz = TikzPicture(center=True) 12 | if n > 0: 13 | iters += 6 ** (i) 14 | barycentric_subdivision(iters, tikz) 15 | else: 16 | barycentric_subdivision(0, tikz) 17 | 18 | tikz.show() 19 | 20 | 21 | def barycentric_subdivision(iterations, tikz): 22 | pt_one = (0, 0) 23 | pt_two = (6, 0) 24 | pt_three = (3, 4.24) 25 | 26 | tikz.line(pt_one, pt_two) 27 | tikz.line(pt_two, pt_three) 28 | tikz.line(pt_three, pt_one) 29 | 30 | triangles = queue.Queue() # queue of lists of tuples 31 | triangles.put([pt_one, pt_two, pt_three]) # Put goes to the end [... {]} 32 | 33 | while not triangles.empty() and iterations > 0: 34 | iterations -= 1 35 | coords = triangles.get() # Grabs from the front {[} ...] 36 | new = medians(coords, tikz, iterations) 37 | for tri in new: 38 | triangles.put(tri) 39 | 40 | 41 | def medians(coords, tikz, iteration): 42 | centroid = ( 43 | statistics.mean([x[0] for x in coords]), 44 | statistics.mean([x[1] for x in coords]), 45 | ) 46 | new_triangle = [] # save coords of new triangles 47 | 48 | # get median coordinates for all edges 49 | midpts = [] 50 | for i in range(-len(coords), 0): 51 | ax, ay = coords[i] 52 | bx, by = coords[i + 1] 53 | cx, cy = coords[i + 2] 54 | 55 | # get median coords, draw median 56 | x = statistics.mean([bx, cx]) 57 | y = statistics.mean([by, cy]) 58 | tikz.line( 59 | (ax, ay), 60 | (x, y), 61 | options=f"color={xcolors(iteration)}, line width=0.1mm", 62 | ) 63 | midpts.append((x, y)) 64 | 65 | # get coords of all new triangles created 66 | for i in range(len(coords)): 67 | new_triangle.append([centroid, midpts[i], coords[i - 1]]) 68 | new_triangle.append([centroid, midpts[i], coords[i - 2]]) 69 | 70 | return new_triangle 71 | 72 | if __name__ == "__main__": 73 | nth_subdivision(3) 74 | -------------------------------------------------------------------------------- /examples/basic/basic_ex.py: -------------------------------------------------------------------------------- 1 | #!/bin/bash/python3 2 | from tikzpy import TikzPicture # Import the class TikzPicture 3 | 4 | """ Draws a circle and two ellipses to create the illusion of sphere. 5 | """ 6 | 7 | if __name__ == "__main__": 8 | tikz = TikzPicture() 9 | tikz.circle((0, 0), 3, options="thin, fill=orange!15") 10 | 11 | arc_one = tikz.arc((3, 0), 0, 180, x_radius=3, y_radius=1.5, options=f"dashed") 12 | arc_two = tikz.arc((-3, 0), 180, 360, x_radius=3, y_radius=1.5) 13 | 14 | tikz.show(quiet=True) # Displays a pdf of the drawing to the user 15 | -------------------------------------------------------------------------------- /examples/blowup/blowup.py: -------------------------------------------------------------------------------- 1 | #!/bin/bash/python3 2 | from math import pi, cos, sin, tan, atan 3 | import numpy as np 4 | from tikzpy import TikzPicture 5 | 6 | """ Plots the blowup at a point. 7 | """ 8 | 9 | tikz = TikzPicture(center=True) 10 | tikz.set_tdplotsetmaincoords(65, 25) 11 | tikz.options = "tdplot_main_coords" 12 | 13 | # The blowup 14 | def blowup(r, t): 15 | theta = 2 * atan(t * 2 / pi) 16 | return r * cos(theta), r * sin(theta), 6 * atan(tan(theta) / 6) 17 | 18 | 19 | if __name__ == "__main__": 20 | # Parameters for surface 21 | vmin = -pi / 2 + 0.1 22 | vmax = pi / 2 - 0.1 23 | umin = -5 24 | umax = 5 25 | 26 | # Draws the gray vertical line 27 | for j in np.linspace(umin, umax, 40): 28 | points = [] 29 | for i in np.linspace(vmin, vmax, 50): 30 | points.append(blowup(j, i)) 31 | tikz.plot_coordinates(points, options="gray!10, thin", plot_options="smooth") 32 | 33 | # Draws the main blue vertical lines for visual aid 34 | for j in np.linspace(umin, umax, 10): 35 | points = [] 36 | for i in np.linspace(vmin, vmax, 50): 37 | points.append(blowup(j, i)) 38 | tikz.plot_coordinates(points, options="ProcessBlue!70", plot_options="smooth") 39 | 40 | # Draws the horizontal lines 41 | for i in np.linspace(vmin, vmax, 50): 42 | new_points = [] 43 | for j in np.linspace(umin, umax, 10): 44 | new_points.append(blowup(j, i)) 45 | tikz.plot_coordinates(new_points, options="ProcessBlue!70", plot_options="smooth") 46 | 47 | tikz.show() 48 | -------------------------------------------------------------------------------- /examples/cantor/cantor.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from tikzpy import TikzPicture, R2_Space 3 | 4 | 5 | def cantor(n): 6 | """A wrapper to perform cantor subdivision on [0, 1].""" 7 | return [0] + subdivide(0, 1, n) + [1] 8 | 9 | 10 | def subdivide(x_1, x_2, n): 11 | """Performs the n-th cantor subdivision of the interval (x_1, x_2), a subset [0, 1]""" 12 | if n == 0: 13 | return [] 14 | 15 | new_x_1 = 2 * (x_1 / 3) + x_2 / 3 16 | new_x_2 = x_1 / 3 + 2 * (x_2 / 3) 17 | return ( 18 | subdivide(x_1, new_x_1, n - 1) 19 | + [new_x_1, new_x_2] 20 | + subdivide(new_x_2, x_2, n - 1) 21 | ) 22 | 23 | 24 | if __name__ == "__main__": 25 | tikz = TikzPicture() 26 | s = 4 # Scale 27 | 28 | # Set up xy-plane 29 | xy_plane = R2_Space(x_interval=(0, s), y_interval=(0, s)) 30 | xy_plane.x_axis_options = "Gray!30, ->" 31 | xy_plane.y_axis_options = "Gray!30, ->" 32 | tikz.draw(xy_plane) 33 | 34 | # Collect (x,y) cantor data 35 | x = np.array(cantor(10)) 36 | y = np.cumsum(np.ones(len(x)) / (len(x) - 2)) - 1 / (len(x) - 2) 37 | y[-1] = 1 38 | 39 | # Plot it 40 | points = tikz.plot_coordinates(list(zip(x, y)), options="ProcessBlue") 41 | points.scale_(4) 42 | tikz.show() 43 | -------------------------------------------------------------------------------- /examples/cauchy_residue_thm/cauchy_residue_thm.py: -------------------------------------------------------------------------------- 1 | #!/bin/bash/python3 2 | from tikzpy import TikzPicture 3 | from tikzpy.styles import arrows_along_path_style 4 | 5 | """ Illustrates a use case of Cauchy's Residue Theorem. In this case 6 | we have two singularities that occur within the semicircle. 7 | """ 8 | 9 | if __name__ == "__main__": 10 | tikz = TikzPicture() 11 | 12 | points = [ 13 | (-3, -0.5), 14 | (-1, -1.5), 15 | (2, -1.5), 16 | (4, -1.5), 17 | (5, 0), 18 | (3, 2.4), 19 | (-1, 2.6), 20 | (-3, 2), 21 | ] 22 | 23 | tikz.add_styles(*arrows_along_path_style) 24 | tikz.options = "thick" 25 | 26 | # Draws the main boundary 27 | plot = tikz.plot_coordinates( 28 | options="arrows_along_path=red", 29 | plot_options="smooth, tension=.5, closed hobby", 30 | points=points, 31 | action="draw", 32 | ) 33 | 34 | # Draws the inner circles 35 | singularity_1 = tikz.circle((3, -0.3), 0.7, "arrows_along_path=blue") 36 | singularity_2 = tikz.circle((1.3, 1.3), 0.7, "arrows_along_path=blue") 37 | singularity_3 = tikz.circle((-0.4, -0.2), 0.7, "arrows_along_path=blue") 38 | singularity_4 = tikz.circle((-2, 1.1), 0.7, "arrows_along_path=blue") 39 | 40 | # Draws the paths that connect the circles 41 | tikz.line((3.7, -0.5), (4.6, -1), options="bend left, <-") 42 | tikz.line((1.4, 0.6), (2.3, -0.3), options="bend right, <-") 43 | tikz.line((-0.25, 0.5), (0.6, 1.4), options="bend left, <-") 44 | tikz.line((-1.29, 1.1), (-0.6, 0.46), options="bend left, <-") 45 | tikz.line((-2.5, 1.6), (-2.8, 2.15), options="bend left, ->") 46 | 47 | # Draws and labels the points a_1, a_2, a_3, and a_4. 48 | for ind, singularity in enumerate( 49 | [singularity_1, singularity_2, singularity_3, singularity_4] 50 | ): 51 | tikz.circle(singularity.center, 0.05, action="fill") 52 | tikz.node(singularity.center, options="right", text=f"$a_{ind+1}$") 53 | 54 | tikz.show() 55 | -------------------------------------------------------------------------------- /examples/cauchy_residue_thm/cauchy_residue_thm_arc.py: -------------------------------------------------------------------------------- 1 | #!/bin/bash/python3 2 | from tikzpy import TikzPicture 3 | from tikzpy.styles import arrows_along_path_style 4 | 5 | """ Illustrates Cauchy's Residue Theorem, which is a statement about integrating over 6 | finitely many singularities 7 | """ 8 | 9 | if __name__ == "__main__": 10 | tikz = TikzPicture() 11 | tikz.add_styles(*arrows_along_path_style) 12 | tikz.options = "thick" 13 | 14 | # x and y axes 15 | x_axis = tikz.line((-4, 0), (4, 0), options="->") 16 | y_axis = tikz.line((0, -1), (0, 4), options="->") 17 | tikz.node(x_axis.end, options="below", text="$\\mathbb{R}$") 18 | tikz.node(y_axis.end, options="left", text="$i$") 19 | 20 | # Red Semicircle 21 | arc = tikz.arc( 22 | (0, 0), 23 | 0, 24 | 180, 25 | radius=2, 26 | options="arrows_along_path=red, red", 27 | draw_from_start=False, 28 | ) 29 | # Draws the blue path [-R, R] 30 | bottom_path = tikz.line((-2, 0), (2, 0), options="blue, arrows_along_path=blue") 31 | tikz.node(bottom_path.start - (0, 0.5), text="$R$") 32 | tikz.node(bottom_path.end - (0, 0.5), text="$-R$") 33 | 34 | # % Draws the points z_1, z_2 of interest 35 | circle_1 = tikz.circle((0.7, 0.7), 0.05, action="fill") 36 | circle_2 = tikz.circle((-0.7, 0.7), 0.05, action="fill") 37 | tikz.node(circle_1.center, options="right", text="$z_1$") 38 | tikz.node(circle_2.center, options="left", text="$z_2$") 39 | 40 | tikz.show() 41 | -------------------------------------------------------------------------------- /examples/circle_intersections/circle_intersections.py: -------------------------------------------------------------------------------- 1 | from tikzpy import TikzPicture, Point 2 | from tikzpy.drawing_objects.drawing_utils import calc_intersection 3 | 4 | tikz = TikzPicture(center=True) 5 | 6 | # Draw nodes A, B 7 | A = tikz.node((0,0), options="left", text="$A$") 8 | B = tikz.node((1.25, 0.25), options="right", text="$B$") 9 | 10 | # Draw circle about A, B 11 | AB_dist = A.position.distance(B.position) # Calc distance between A and B 12 | circle_A = tikz.circle(A.position, radius=AB_dist) 13 | circle_B = tikz.circle(B.position, radius=AB_dist) 14 | 15 | # Calculate intersection points between two circles 16 | intersections = calc_intersection(circle_A, circle_B) 17 | 18 | # Draw nodes C, D, E 19 | C = tikz.node(intersections[1], options="above", text="$C$") 20 | D = tikz.node(circle_A.west, text="$D$", options="left") 21 | E = tikz.node(circle_B.east, text="$E$", options="right") 22 | 23 | # Draw triangle 24 | tikz.line(A.position, C.position, options="red") 25 | tikz.line(B.position, C.position, options="red") 26 | tikz.line(A.position, B.position) 27 | tikz.show(quiet=False) 28 | 29 | -------------------------------------------------------------------------------- /examples/circle_intersections_2/circle_intersections_2.py: -------------------------------------------------------------------------------- 1 | from tikzpy import TikzPicture 2 | from tikzpy.drawing_objects.drawing_utils import calc_intersection 3 | 4 | tikz = TikzPicture(center=True) 5 | 6 | # Draw nodes A, B 7 | A = tikz.node((0,0), options="left", text="$A$") 8 | B = tikz.node((1.25, 0.25), options="right", text="$B$") 9 | 10 | # Draw circle about A, B 11 | AB_dist = A.position.distance(B.position) # Calc distance between A and B 12 | circle_A = tikz.circle(A.position, radius=AB_dist) 13 | circle_B = tikz.circle(B.position, radius=AB_dist) 14 | 15 | # Calculate intersection points between two circles 16 | intersections = calc_intersection(circle_A, circle_B) 17 | 18 | # Draw nodes C, D, E 19 | C = tikz.node(intersections[1], options="above", text="$C$") 20 | C_prime = tikz.node(intersections[0], options="below", text="$C'$") 21 | D = tikz.node(circle_A.west, text="$D$", options="left") 22 | E = tikz.node(circle_B.east, text="$E$", options="right") 23 | 24 | # Draw lines 25 | line_a = tikz.line(A.position, B.position) 26 | line_b = tikz.line(C.position, C_prime.position, options="red") 27 | 28 | # Draw intersection 29 | intersections = calc_intersection(line_a, line_b) 30 | tikz.node(intersections[0], options="fill=red,inner sep=1pt") 31 | 32 | tikz.show(quiet=False) 33 | 34 | -------------------------------------------------------------------------------- /examples/circles/circles.py: -------------------------------------------------------------------------------- 1 | #!/bin/bash/python3 2 | import numpy as np 3 | from tikzpy import TikzPicture 4 | 5 | if __name__ == "__main__": 6 | tikz = TikzPicture(center=True) 7 | 8 | for i in np.linspace(0, 1, 30): 9 | point = (np.sin(2 * np.pi * i), np.cos(2 * np.pi * i)) 10 | 11 | # Create four circles of different radii with center located at point 12 | tikz.circle(point, 2, "ProcessBlue") 13 | tikz.circle(point, 2.2, "ForestGreen") 14 | tikz.circle(point, 2.4, "red") # xcolor Red is very ugly 15 | tikz.circle(point, 2.6, "Purple") 16 | 17 | tikz.show() 18 | -------------------------------------------------------------------------------- /examples/controls/controls.py: -------------------------------------------------------------------------------- 1 | #!/bin/bash/python3 2 | from tikzpy import TikzPicture 3 | from tikzpy.colors import rainbow_colors 4 | 5 | """ Draws a series of rainbow lines in a manner such that their control points are shifted. 6 | """ 7 | 8 | tikz = TikzPicture() 9 | for i in range(0, 15): 10 | line = tikz.line((i, 0), (0, 5)) 11 | line.options = f"color={rainbow_colors(i)}" 12 | line.control_pts = [(i - 2, -1), (i + 2, -2)] 13 | 14 | tikz.show() 15 | -------------------------------------------------------------------------------- /examples/des/des.py: -------------------------------------------------------------------------------- 1 | from tikzpy import TikzPicture 2 | 3 | if __name__ == "__main__": 4 | tikz = TikzPicture(center=True, options=">=stealth, thick") 5 | 6 | # Plaintext 7 | plaintext = tikz.rectangle((0, 0), 4, 0.8) 8 | tikz.node(plaintext.center, text="Plaintext") 9 | 10 | to_IP = tikz.line(plaintext.south, plaintext.south - (0, 0.5), options="->") 11 | # IP 12 | IP = tikz.rectangle_from_north(to_IP.end, height=0.7, width=2, options="rounded corners") # Rectangle who's .north position is the end of IP 13 | tikz.node(IP.center, text="IP") 14 | vertical = tikz.line(IP.south, IP.south - (0, 0.8)) 15 | 16 | # Horizontal lines 17 | h1 = tikz.line(vertical.end, vertical.end - (5, 0)) 18 | h2 = tikz.line(vertical.end, vertical.end + (5, 0)) 19 | 20 | # To the blocks 21 | to_left_block = tikz.line(h1.end, h1.end - (0, 0.5), options="->") 22 | to_right_block = tikz.line(h2.end, h2.end - (0, 0.5), options="->") 23 | 24 | def DES_round(to_left_block, to_right_block, left_label, right_label, K_i=None, dotted=False, is_last_block=False): 25 | # Left block 26 | L_0 = tikz.rectangle_from_north(to_left_block.end, height=1, width=5) 27 | tikz.node(L_0.center, text=left_label) 28 | 29 | # Right block 30 | R_0 = tikz.rectangle_from_north(to_right_block.end, height=1, width=5) 31 | tikz.node(R_0.center, text=right_label) 32 | 33 | # Line to the XOR symbol 34 | to_xor = tikz.line(L_0.south, L_0.south - (0, 0.5)) 35 | 36 | # Draw the XOR symbol 37 | XOR = tikz.circle(to_xor.end - (0, 0.25), radius=0.25) 38 | tikz.line(XOR.north, XOR.south) 39 | tikz.line(XOR.west, XOR.east) 40 | 41 | # Line to the dot 42 | to_dot = tikz.line(R_0.south, (R_0.south.x, XOR.center.y)) 43 | # Draw the dot symbol 44 | dot = tikz.circle(to_dot.end, radius=0.1, options="fill") 45 | 46 | # Function f 47 | function_f = tikz.circle((dot.center + XOR.center) / 2 , radius = 0.5) 48 | tikz.node(function_f.center, text="$f$") 49 | 50 | # Line from dot to function f 51 | to_f = tikz.line(dot.west, function_f.east, options="->") 52 | # Line from function f to XOR symbol 53 | from_f = tikz.line(function_f.west, XOR.east, options="->") 54 | 55 | # From XOR to the right block 56 | from_XOR = tikz.line(XOR.south, (XOR.south.x, dot.south.y - 0.5)) 57 | XOR_diagonal = tikz.line(from_XOR.end, (dot.center.x, from_XOR.end.y - 0.75)) 58 | to_next_right_block = tikz.line(XOR_diagonal.end, XOR_diagonal.end - (0, 0.25), options="->") 59 | 60 | # From dot to left block block 61 | from_dot = tikz.line(dot.south, dot.south - (0, 0.5)) 62 | dot_diagonal = tikz.line(from_dot.end, (XOR.center.x, from_dot.end.y - 0.75)) 63 | to_next_left_block = tikz.line(dot_diagonal.end, dot_diagonal.end - (0, 0.25), options="->") 64 | 65 | # Dot the diagonal lines to show there are hidden steps 66 | if dotted: 67 | XOR_diagonal.options = "dotted" 68 | dot_diagonal.options = "dotted" 69 | 70 | # Incoming key 71 | if K_i is not None: 72 | joint = tikz.line(from_dot.midpoint() + (1, -0.2), (to_f.midpoint().x, from_dot.midpoint().y - 0.2)) 73 | tikz.line(joint.end, function_f.point_at_arg(-30), options="->") 74 | tikz.node(joint.start + (0.3, 0), text=K_i) 75 | 76 | # If last block, straighten the diagonals so that they are vertical lines 77 | if is_last_block: 78 | XOR_diagonal.end = (XOR_diagonal.start.x, XOR_diagonal.end.y) 79 | dot_diagonal.end = (dot_diagonal.start.x, dot_diagonal.end.y) 80 | 81 | 82 | return to_next_left_block, to_next_right_block 83 | 84 | 85 | next_left_block, next_right_block = DES_round(to_left_block, to_right_block, "$L_0$", "$R_0$", "$K_1$" ) 86 | next_left_block, next_right_block = DES_round(next_left_block, next_right_block, "$L_1=R_0$", "$R_1 = L_0 \oplus f(R_0, K_1)$", "$K_2$") 87 | next_left_block, next_right_block = DES_round(next_left_block, next_right_block, "$L_2=R_1$", "$R_2 = L_1 \oplus f(R_1, K_2)$", dotted=True) 88 | next_left_block, next_right_block = DES_round(next_left_block, next_right_block, "$L_{15}=R_{14}$", "$R_{15} = L_{14} \oplus f(R_{14}, K_{15})$", "$K_{16}$", is_last_block=True) 89 | 90 | # Left block 91 | L_0 = tikz.rectangle_from_north(next_left_block.end, height=1, width=5) 92 | tikz.node(L_0.center, text="$R_{16} = L_{15}\oplus f(R_{15}, K_{16})$") 93 | 94 | # Right block 95 | R_0 = tikz.rectangle_from_north(next_right_block.end, height=1, width=5) 96 | tikz.node(R_0.center, text="$L_{16} = R_{15}$") 97 | 98 | # Leaving the blocks 99 | from_left_block = tikz.line(L_0.south, L_0.south - (0, 0.5)) 100 | from_right_block = tikz.line(R_0.south, R_0.south - (0, 0.5)) 101 | 102 | # Horizontal lines to the middle 103 | h1 = tikz.line(from_left_block.end, (to_IP.end.x, from_left_block.end.y)) 104 | h2 = tikz.line(from_right_block.end, (to_IP.end.x, from_right_block.end.y)) 105 | 106 | # Line to IP inverse 107 | to_IP_inv = tikz.line(h1.end, h1.end - (0, 0.5), options="->") 108 | IP_inv = tikz.rectangle_from_north(to_IP_inv.end, height=0.7, width=2, options="rounded corners") 109 | 110 | # IP rectangle 111 | tikz.node(IP_inv.center, text="$\\text{IP}^{-1}$") 112 | to_cipher_text = tikz.line(IP_inv.south, IP_inv.south - (0, 0.5), options="->") 113 | 114 | # Ciphertext rectangle 115 | cipher_text = tikz.rectangle_from_north(to_cipher_text.end, height=0.8, width=4) 116 | tikz.node(cipher_text.center, text="Ciphertext") 117 | tikz.show() 118 | 119 | 120 | -------------------------------------------------------------------------------- /examples/geometry_figure/geometry_figure.py: -------------------------------------------------------------------------------- 1 | import math 2 | from tikzpy import TikzPicture, Point, Line 3 | from tikzpy.drawing_objects.drawing_utils import calc_intersection 4 | 5 | 6 | def draw_point(point, text, label_spacing = 0.3): 7 | tikz.circle(point, radius=0.05, options="fill=black") 8 | angle = ref_angle(Point(point)) 9 | x_spacing = label_spacing * math.cos(math.radians(angle)) 10 | y_spacing = label_spacing * math.sin(math.radians(angle)) 11 | tikz.node(Point(point) + (x_spacing, y_spacing), text=text) 12 | 13 | def ref_angle(point): 14 | return math.degrees(math.atan2(point.y, point.x)) 15 | 16 | def angle_between_lines(line_a, line_b): 17 | m1 = line_a.slope() 18 | m2 = line_b.slope() 19 | return math.degrees(math.atan(abs((m2 - m1) / (1 + m1 * m2)))) 20 | 21 | def arc_between_lines(line_a, line_b, radius): 22 | intersection = calc_intersection(line_a, line_b)[0] 23 | a = ref_angle(line_a.end - Point(intersection)) 24 | b = angle_between_lines(line_a, line_b) 25 | tikz.arc(intersection, a, a + b, radius=radius, draw_from_start=False) 26 | 27 | if __name__ == "__main__": 28 | radius = 3 29 | tikz = TikzPicture(center=True) 30 | circle = tikz.circle((0,0), radius) 31 | 32 | # Points on circumference 33 | A = circle.point_at_arg(110) 34 | B = circle.point_at_arg(315) 35 | C = circle.point_at_arg(70) 36 | D = circle.point_at_arg(215) 37 | 38 | lines = tikz.draw_segments([A, B, C, D]) 39 | 40 | X = lines[(D, A)].pos_at_t(0.65) 41 | 42 | # Draw intersections 43 | intersections = calc_intersection(lines[(A, B)], lines[(C, D)]) 44 | M = intersections[0] 45 | 46 | XM_line = tikz.line(X, M) 47 | intersections = calc_intersection(lines[(B, C)], XM_line) 48 | Y = intersections[0] 49 | 50 | MY_line = tikz.line(M, Y) 51 | intersections = calc_intersection(MY_line, circle) 52 | P, Q = intersections[0], intersections[1] 53 | tikz.line(P, X) 54 | tikz.line(Y, Q) 55 | 56 | # Arc between MY and MC 57 | arc_between_lines(MY_line, Line(M, C), 0.55) 58 | arc_between_lines(MY_line, Line(M, C), 0.5) 59 | 60 | # Arc between MA and MX 61 | arc_between_lines(Line(M, A), Line(M, X), 0.35) 62 | 63 | # Arc between MX and MD 64 | arc_between_lines(Line(M, X), Line(M, D), 0.55) 65 | arc_between_lines(Line(M, X), Line(M, D), 0.5) 66 | 67 | # Arc between DM and DX 68 | arc_between_lines(Line(D, M), Line(D, X), 0.55) 69 | arc_between_lines(Line(D, M), Line(D, X), 0.5) 70 | 71 | # Arc between BY and BM 72 | arc_between_lines(Line(B, Y), Line(B, M), 0.55) 73 | arc_between_lines(Line(B, Y), Line(B, M), 0.5) 74 | 75 | # Arc between MB and MY 76 | arc_between_lines(Line(M, B), Line(M, Y), 0.35) 77 | 78 | # Draw points 79 | draw_point(A, "$A$",) 80 | draw_point(B, "$B$",) 81 | draw_point(C, "$C$",) 82 | draw_point(D, "$D$",) 83 | draw_point(X, "$X$",) 84 | draw_point(Y, "$Y$") 85 | draw_point(M, "$M$", label_spacing=0.5) 86 | draw_point(P, "$P$") 87 | draw_point(Q, "$Q$") 88 | 89 | tikz.show() 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /examples/line_and_two_nodes/line_and_two_nodes.py: -------------------------------------------------------------------------------- 1 | #!/bin/bash/python3 2 | from tikzpy import TikzPicture 3 | 4 | """ Draws a line and two nodes. 5 | """ 6 | 7 | if __name__ == "__main__": 8 | tikz = TikzPicture() 9 | line = tikz.line((0, 0), (1, 1), options="thick, blue, o-o") 10 | start_node = tikz.node(line.start, options="below", text="Start!") 11 | end_node = tikz.node(line.end, options="above", text="End!") 12 | 13 | tikz.show() 14 | -------------------------------------------------------------------------------- /examples/linear_model/linear_model.py: -------------------------------------------------------------------------------- 1 | #!/bin/bash/python3 2 | import math 3 | from tikzpy import TikzPicture, Point 4 | tikz = TikzPicture() 5 | radius = 0.25 6 | 7 | if __name__ == "__main__": 8 | x_2 = 6 9 | y_2 = 2 10 | epsilon = 0.2 11 | for i in range(5): 12 | x_1 = 0 13 | y_1 = 4 - i 14 | if y_1 - y_2 == 0: 15 | theta = math.pi/2 16 | else: 17 | theta = math.atan(abs(x_2 - x_1) / abs(y_1 - y_2)) 18 | if y_2 > y_1: 19 | start = (x_1 + radius * math.sin(theta), y_1 + radius* math.cos(theta)) 20 | end = (x_2 - radius * math.sin(theta), y_2 - radius * math.cos(theta)) 21 | else: 22 | start = (x_1 + radius * math.sin(theta), y_1 - radius* math.cos(theta)) 23 | end = (x_2 - radius * math.sin(theta), y_2 + radius * math.cos(theta)) 24 | 25 | line = tikz.line(start, end, options="->") 26 | tikz.circle((x_1, y_1), radius) 27 | # Draw the input x_i 28 | tikz.node((x_1, y_1 + radius + epsilon), text=f"$x_{i}$") 29 | # Draw the weight w_i 30 | tikz.node(line.pos_at_t(0.3), options="above", text=f"$w_{i}$") 31 | 32 | # Draw the output node 33 | tikz.circle((x_2, y_2), radius) 34 | # Draw label for output node 35 | tikz.node((x_2, y_2 + radius + epsilon), text="$y$") 36 | tikz.show() 37 | -------------------------------------------------------------------------------- /examples/linear_transformations/linear_transformations.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from tikzpy import TikzPicture 3 | from tikzpy.colors import rainbow_colors 4 | 5 | """ Plots the image of a 3 x 2 matrix in R^3 acting on R^2. 6 | Vectors in R^3 which lie below the positive Z-axis are drawn with lower opacity to aid the eye. 7 | 8 | Usage: Plug a 3x2 numpy array into plot_linear_transformation. 9 | Ex: 10 | >>> matrix = np.array([[0, 1], [1, 1], [0, 1]]) 11 | >>> plot_linear_transformation(matrix) 12 | 13 | >>> matrix = np.array([[2, 0], [1, 1], [1, 1]]) 14 | >>> plot_linear_transformation(matrix) 15 | 16 | >>> matrix = np.array([[2, 0], [1, 0], [1, 1]]) 17 | >>> plot_linear_transformation(matrix) 18 | etc. 19 | """ 20 | 21 | 22 | def build_scene(): 23 | """Creates a Tikz Environment with two scope environments displayed side by side. 24 | The left scope environment displays R^2, while the right scope environment displays R^3. 25 | """ 26 | tikz = TikzPicture(options=">=stealth") 27 | 28 | # xy plane 29 | xy_plane = tikz.scope(options="xshift=-8cm") # 2D plane 30 | axis_len = 2.5 # 2D axes length 31 | xy_plane.line((-axis_len, 0), (axis_len, 0)).add_node( 32 | options="right, ->", text="$x$" 33 | ) # X axis 34 | xy_plane.line((0, -axis_len), (0, axis_len)).add_node( 35 | options="above, ->", text="$y$" 36 | ) # Y axis 37 | 38 | # xyz space 39 | tikz.set_tdplotsetmaincoords(60, 110) # 3D perspective 40 | xyz_space = tikz.scope("tdplot_main_coords") 41 | O_xyz = (0, 0, 0) # 3D origin 42 | axis_len = 5 # 3D axes length 43 | X = (axis_len, 0, 0) 44 | Y = (0, axis_len - 1, 0) 45 | Z = (0, 0, axis_len - 2) 46 | # X, Y, Z axes 47 | xyz_space.line(O_xyz, X).add_node(options="below, ->", text="$x$") # X axis 48 | xyz_space.line(O_xyz, Y).add_node(options="right, ->", text="$y$") # Y axis 49 | xyz_space.line(O_xyz, Z).add_node(options="above, ->", text="$z$") # Z axis 50 | 51 | return tikz, xy_plane, xyz_space 52 | 53 | 54 | # Main function 55 | def plot_linear_transformation(matrix, num_vecs=40): 56 | """Plots a list of vectors in 2D and then displays their matrix product in 3D.""" 57 | 58 | tikz, xy_plane, xyz_space = build_scene() 59 | O_xy = (0, 0) # 2D origin 60 | O_xyz = (0, 0, 0) # 3D origin 61 | 62 | for i in np.linspace(0, 2 * np.pi, num_vecs): 63 | i_x = 2 * np.cos(i) 64 | i_y = 2 * np.sin(i) 65 | color = "color=" + rainbow_colors(int(i / (2 * np.pi) * num_vecs)) 66 | xy_plane.line(O_xy, (i_x, i_y), options=f"->, {color}") 67 | 68 | M = tuple(np.matmul(matrix, np.array([i_x, i_y]))) # The matrix calculation 69 | 70 | vec = xyz_space.line(O_xyz, M, options=f"->, {color}") 71 | if M[2] < 0: # If the Z-coordinate is negative, color it with low opacity 72 | vec.options += ", opacity=0.2" 73 | 74 | tikz.show() 75 | 76 | if __name__ == "__main__": 77 | matrix = np.array([[2, 0], [1, 1], [1, 1]]) 78 | plot_linear_transformation(matrix) 79 | -------------------------------------------------------------------------------- /examples/lorenz/lorenz.py: -------------------------------------------------------------------------------- 1 | #!/bin/bash/python3 2 | import numpy as np 3 | from scipy.integrate import odeint 4 | from tikzpy import TikzPicture 5 | 6 | """ Plots the Lorenz dynamical system. 7 | 8 | Note: This might not work for some users because of TeX's buffer size parameter, which is a fixed quantity that tells 9 | TeX when to quit a process (even if there are no errors with the program). Such a parameter exists 10 | because TeX was invented in the late 70s, and not during a time period with quad core 16GB ram computers. 11 | 12 | If one has trouble with this, one could increase their buffer size parameter (a good idea because your computer is in 2021+ 13 | and it can definitely handle it) or one could take less iterations in the code below. 14 | """ 15 | 16 | if __name__ == "__main__": 17 | tikz = TikzPicture(center=True) 18 | tikz.set_tdplotsetmaincoords(60, 45) 19 | tikz.options = "tdplot_main_coords" 20 | 21 | # lorenz parameters 22 | rho = 28.0 23 | sigma = 10.0 24 | beta = 8.0 / 3.0 25 | 26 | 27 | # Next state according to the ODEs 28 | def next(*state): 29 | x, y, z = state[0][0], state[0][1], state[0][2] 30 | return sigma * (y - x), x * (rho - z) - y, x * y - beta * z 31 | 32 | 33 | # Set initial conditions and time steps 34 | initial = [1.0, 1.0, 1.0] 35 | t = np.arange( 36 | 0.0, 80.0, 0.02 37 | ) # This might need to be changed, e.g., 0.02 to 0.08, to make the program run. 38 | 39 | # Solve for the next positions, scale them 40 | states = odeint(func=next, y0=initial, t=t) 41 | states = states * 0.25 42 | 43 | # We convert points from np.array to tuple for Tikz 44 | tuple_states = [] 45 | for state in states: 46 | tuple_states.append(tuple(state)) 47 | 48 | # Plot the lorenz system 49 | lorenz_plot = tikz.plot_coordinates( 50 | tuple_states, options="ProcessBlue!70", plot_options="smooth" 51 | ) 52 | # Annotate the initial state 53 | tikz.circle(tuple_states[0], radius=0.1, action="fill") 54 | tikz.node(tuple_states[0], options="below", text="Initial: (1,1,1)") 55 | 56 | tikz.show() 57 | -------------------------------------------------------------------------------- /examples/neural_network/neural_network.py: -------------------------------------------------------------------------------- 1 | from tikzpy import TikzPicture, Point 2 | 3 | node_radius = 0.5 4 | node_sep = 2 5 | layer_sep = 3 6 | 7 | input_layer_pos = (0, 0) 8 | hidden_layer_pos = (layer_sep, 0) 9 | 10 | def network_layer(init_pos, num_nodes, symbol, color): 11 | layer_nodes = [] 12 | for idx, _ in enumerate(range(num_nodes)): 13 | pos = Point(init_pos) + (0, -node_sep * idx) 14 | # Draw the circle 15 | circle = tikz.circle(pos, radius=node_radius, options=f"fill={color}!40") 16 | # Draw the node 17 | tikz.node(pos, text=f"${symbol}_{idx}$") 18 | layer_nodes.append(circle) 19 | return layer_nodes 20 | 21 | 22 | def draw_layer_connection(curr_layer, next_layer): 23 | for curr_node in curr_layer: 24 | for next_node in next_layer: 25 | tikz.connect_circle_edges(curr_node, next_node, "->", dst_delta=0.1) 26 | 27 | def draw_neural_network(layer_sizes): 28 | max_size = max(layer_sizes) 29 | layers = [] 30 | init_pos = Point((0, 0)) 31 | for idx, size in enumerate(layer_sizes): 32 | x_shift = idx * layer_sep 33 | y_shift = - (max_size - size) / 2 * node_sep 34 | pos = init_pos + (x_shift, y_shift) 35 | if idx == 0: 36 | symbol = "x" 37 | color = "green" 38 | elif idx == len(layer_sizes) - 1: 39 | symbol = "y" 40 | color = "red" 41 | else: 42 | symbol = f"h^{{({idx})}}" 43 | color = "blue" 44 | 45 | nodes = network_layer(pos, size, symbol, color) 46 | layers.append(nodes) 47 | 48 | for idx, layer in enumerate(range(len(layers) - 1)): 49 | draw_layer_connection(layers[idx], layers[idx + 1]) 50 | 51 | 52 | if __name__ == "__main__": 53 | tikz = TikzPicture(center=True) 54 | draw_neural_network([4, 5, 3, 4, 3, 2]) 55 | tikz.show() 56 | 57 | -------------------------------------------------------------------------------- /examples/neural_network_connection/neural_network_connection.py: -------------------------------------------------------------------------------- 1 | from tikzpy import TikzPicture 2 | 3 | tikz = TikzPicture(center=True) # center it in the PDF 4 | radius = 0.25 5 | pos_a = (0, 0) 6 | pos_b = (4, 1) 7 | 8 | # Draw the nodes 9 | node_a = tikz.circle(pos_a, radius) 10 | node_b = tikz.circle(pos_b, radius) 11 | 12 | # Draw the line between the nodes 13 | line = tikz.connect_circle_edges(node_a, node_b) 14 | line.options = "->" 15 | 16 | # Annotate the drawing with mathematical variables 17 | h_j = tikz.node(node_a.center + (0.3, 0.75), text="$h_j^{(n-1)}$") 18 | h_i = tikz.node(node_b.center + (0.3, 0.75), text="$h_i^{(n)}$") 19 | w_ij = tikz.node(line.pos_at_t(0.5) + (0, 0.5), text="$w_{ij}^{(n)}$") 20 | 21 | # Add ellipses on each side to illustrate more nodes are present 22 | tikz.node(node_a.center + (0, 1.5), text="\\vdots") 23 | tikz.node(node_a.center + (0, -0.75), text="\\vdots") 24 | tikz.node(node_b.center + (0, 1.5), text="\\vdots") 25 | tikz.node(node_b.center + (0, -0.75), text="\\vdots") 26 | tikz.show() 27 | 28 | -------------------------------------------------------------------------------- /examples/neural_network_simple/neural_network_simple.py: -------------------------------------------------------------------------------- 1 | import math 2 | from tikzpy import TikzPicture 3 | 4 | def calc_start_end_between_nodes(pos_a, rad_a, pos_b, rad_b): 5 | x_1, y_1 = pos_a 6 | x_2, y_2 = pos_b 7 | if y_1 - y_2 == 0: 8 | theta = math.pi/2 9 | else: 10 | theta = math.atan(abs(x_2 - x_1) / abs(y_1 - y_2)) 11 | 12 | if y_2 > y_1: 13 | start = (x_1 + radius * math.sin(theta), y_1 + radius* math.cos(theta)) 14 | end = (x_2 - radius * math.sin(theta), y_2 - radius * math.cos(theta)) 15 | else: 16 | start = (x_1 + radius * math.sin(theta), y_1 - radius* math.cos(theta)) 17 | end = (x_2 - radius * math.sin(theta), y_2 + radius * math.cos(theta)) 18 | return start, end 19 | 20 | if __name__ == "__main__": 21 | tikz = TikzPicture() 22 | layers = [] 23 | horiz_space = 3 24 | vert_space = 2.5 25 | radius=0.25 26 | epsilon = 0.3 27 | 28 | # x_0 node 29 | x_0_node = tikz.circle((0, vert_space), radius) 30 | tikz.node((0, vert_space + radius + epsilon), text="$x_1$") 31 | # x_2 node 32 | x_1_node = tikz.circle((0, 0), radius) 33 | tikz.node((0, 0 + radius + epsilon), text="$x_2$") 34 | # x_2 node 35 | x_2_node = tikz.circle((0, -vert_space), radius) 36 | tikz.node(x_2_node.center + (0, radius + epsilon), text="$1$") 37 | 38 | # h_0 node 39 | h_0_node = tikz.circle((horiz_space, vert_space), radius) 40 | tikz.node((horiz_space, vert_space + radius + epsilon), text="$h_1$") 41 | # h_1 node 42 | h_1_node = tikz.circle((horiz_space, 0), radius) 43 | tikz.node((horiz_space, 0 + radius + epsilon), text="$h_2$") 44 | # h_2 node 45 | h_2_node = tikz.circle((horiz_space, -vert_space), radius) 46 | tikz.node((horiz_space, -vert_space + radius + epsilon), text="$1$") 47 | 48 | # Add the final output layer 49 | x_2 = 1.75*horiz_space 50 | y_2 = 0 51 | 52 | output_node = tikz.circle((x_2, y_2), radius) 53 | tikz.node((x_2, y_2 + radius + epsilon), text="$y$") 54 | 55 | layers = [[x_0_node, x_1_node, x_2_node], [h_0_node, h_1_node, h_2_node], [output_node]] 56 | 57 | # Draw labels for hidden layer 58 | lines = [] 59 | for i in range(len(layers) - 1): 60 | curr_nodes = layers[i] 61 | next_nodes = layers[i + 1] 62 | 63 | for ind, node in enumerate(curr_nodes): 64 | pos_a = node.center 65 | rad_a = node.radius 66 | for j, next_node in enumerate(next_nodes): 67 | if i == 0 and j == len(next_nodes) - 1: 68 | continue 69 | pos_b = next_node.center 70 | rad_b = next_node.radius 71 | start, end = calc_start_end_between_nodes(pos_a, rad_a, pos_b, rad_b) 72 | lines.append(tikz.line(start, end, options="->")) 73 | 74 | # label the weights in the hidden layer 75 | tikz.node(lines[0].pos_at_t(0.5), options="above", text="3") 76 | tikz.node(lines[1].pos_at_t(0.2), options="above", text="4") 77 | tikz.node(lines[2].pos_at_t(0.2), options="above", text="2") 78 | tikz.node(lines[3].pos_at_t(0.3), options="above", text="3") 79 | # label the biases in the first layer 80 | tikz.node(lines[4].pos_at_t(0.2), options="left", text="-2") 81 | tikz.node(lines[5].pos_at_t(0.5), options="below", text="-4") 82 | 83 | # label the weights in the final layer 84 | tikz.node(lines[6].pos_at_t(0.4), options="right", text="5") 85 | tikz.node(lines[7].pos_at_t(0.5), options="above", text="-5") 86 | # label the bias in the final layer 87 | tikz.node(lines[8].pos_at_t(0.5), options="left", text="-2") 88 | 89 | tikz.show() 90 | -------------------------------------------------------------------------------- /examples/polar/polar.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from tikzpy import TikzPicture, PlotCoordinates, Circle 3 | from tikzpy.colors import rainbow_colors 4 | 5 | def to_cartesian(r, ang): 6 | """Returns the cartesian coordinates of a polar point.""" 7 | x = r * np.cos(ang) 8 | y = r * np.sin(ang) 9 | return x, y 10 | 11 | 12 | if __name__ == "__main__": 13 | xy_plane = TikzPicture(options="thick") 14 | polar_plane = TikzPicture(options="thick") 15 | 16 | # XY_plane 17 | xy_plane.line((0, 0), (2 * np.pi, 0)).add_node(options="above", text="$x$") 18 | xy_plane.line((0, 0), (0, 2)).add_node(options="above", text="$y$") 19 | 20 | # Polar Plane: lines and concentric circles 21 | for ang in np.linspace(0, 2 * np.pi, 16): 22 | polar_plane.line((0, 0), to_cartesian(2.1, ang), options="Gray!30") 23 | 24 | for radius in [1, 1.5, 2]: 25 | polar_plane.circle((0, 0), radius=radius, options="Gray!30") 26 | 27 | # Sin curve and Cardioid 28 | sin_curve = xy_plane.plot_coordinates([], plot_options="smooth") 29 | cardioid = polar_plane.plot_coordinates([], plot_options="smooth") 30 | 31 | for ang in np.linspace(0, 2 * np.pi, 200): 32 | x, y = to_cartesian(1 + np.sin(ang), ang) 33 | sin_curve.add_point(ang, 1 + np.sin(ang)) 34 | cardioid.add_point(x, y) 35 | 36 | # Rainbow points 37 | for ang in np.linspace(0, 2 * np.pi, 16): 38 | r = 1 + np.sin(ang) 39 | x, y = to_cartesian(r, ang) 40 | color = rainbow_colors(int(ang / (np.pi / 6))) 41 | xy_plane.circle((ang, r), radius=0.075, options=f"fill={color}", action="fill") 42 | polar_plane.circle((x, y), radius=0.075, options=f"fill={color}", action="fill") 43 | 44 | xy_plane.write() 45 | polar_plane.write() 46 | polar_plane.show() 47 | -------------------------------------------------------------------------------- /examples/projective_cone/projective_cone.py: -------------------------------------------------------------------------------- 1 | #!/bin/bash/python3 2 | import numpy as np 3 | from tikzpy import TikzPicture, PlotCoordinates 4 | 5 | if __name__ == "__main__": 6 | tikz = TikzPicture(center=True, options="tdplot_main_coords") 7 | tikz.set_tdplotsetmaincoords(75, 120) 8 | 9 | HEIGHT = 3.75 # Height of the surace (from the floor) 10 | O = (1, 2, HEIGHT / 2) # The origin 11 | 12 | # Draw the rectangle 13 | floor = tikz.plot_coordinates([(0, 0, 0), (5, 0, 0), (5, 5, 0), (0, 5, 0), (0, 0, 0)]) 14 | floor.options = "shade, left color = NavyBlue!30, right color = NavyBlue, opacity = 0.3" 15 | 16 | # Initialize the top and bottom curves. Note the points are empty. We'll add to them. 17 | top_curve = PlotCoordinates([], options="line width = 0.07mm") 18 | bottom_curve = PlotCoordinates([], options="line width = 0.2mm") 19 | 20 | for t in np.linspace(0, 1, 100): 21 | # Compute points on the top and bottom curves. I guessed this formula in trying to mimic Hartshorne's image. 22 | top_pt = (1.5 + np.sin(10 * t), 3 * t + 0.5, HEIGHT) 23 | bottom_pt = (1.5 + np.sin(10 * t), 3 * t + 0.5, 0) 24 | 25 | # Lines from the origin to the current points on the curves 26 | line_1 = tikz.line(O, top_pt, "gray, opacity = 0.75, line width = 0.05mm") 27 | line_2 = tikz.line(O, bottom_pt, "gray, opacity = 0.75, line width = 0.05mm") 28 | 29 | # Collect points to later draw the top and bottom curves 30 | top_curve.points.append(top_pt) 31 | bottom_curve.points.append(bottom_pt) 32 | 33 | # We now draw the top and bottom curves 34 | tikz.draw(top_curve, bottom_curve) 35 | 36 | # Annotate the origin (O), rectangle (P^2), cone (C(Y)), variety (Y), and ambient space (A^3) 37 | tikz.circle(O, radius=0.03, action="fill") 38 | tikz.node(O, options="left", text="$O$") 39 | tikz.node((3, 3, 0), text="$Y$") 40 | tikz.node((1, 4.5, 0), text="$\mathbf{P}^2$") 41 | tikz.node((0, 2.5, 1), text="$C(Y)$") 42 | tikz.node((0, 4.2, 3.5), text="$\mathbf{A}^3$") 43 | with open("code.tex", "w") as f: 44 | f.write(tikz.code()) 45 | 46 | tikz.show(quiet=True) 47 | -------------------------------------------------------------------------------- /examples/relu/relu.py: -------------------------------------------------------------------------------- 1 | from tikzpy import TikzPicture, R2_Space 2 | 3 | 4 | if __name__ == "__main__": 5 | tikz = TikzPicture() 6 | axes_len = 4 7 | vert_scale = 4.5 8 | 9 | # Set up xy-plane 10 | xy_plane = R2_Space(x_interval=(-axes_len, axes_len), y_interval=(0, vert_scale + .5)) 11 | xy_plane.x_axis_options = "Gray!30, ->" 12 | xy_plane.y_axis_options = "Gray!30, ->" 13 | tikz.draw(xy_plane) 14 | 15 | line = tikz.line((-3.5, 4.5), (4.5, 4.5), options="dashed") 16 | tikz.node(line.start, options="left", text="$y=1$") 17 | # Plot it 18 | origin = (0, 0) 19 | tikz.line((-axes_len, 0), origin, options="ProcessBlue, <-") 20 | tikz.line(origin, (4.5, 4.5), options="ProcessBlue, ->") 21 | 22 | tikz.show() 23 | -------------------------------------------------------------------------------- /examples/roots_of_unity/roots_of_unity.py: -------------------------------------------------------------------------------- 1 | #!/bin/bash/python3 2 | from math import pi, sin, cos 3 | from tikzpy import TikzPicture 4 | 5 | 6 | def roots_of_unity(n, scale=5): 7 | """Creates a diagram for the n-th roots of unity. 8 | n (int) : The number of roots of unity we display 9 | scale (float) : A parameter which controls the size of the image. This should be >5 for larger roots of unity. 10 | """ 11 | tikz = TikzPicture(center=True) 12 | 13 | for i in range(n): 14 | theta = (2 * pi * i) / n 15 | 16 | # Draw line to nth root of unity 17 | line_to_root = tikz.line( 18 | (0, 0), (scale * cos(theta), scale * sin(theta)), options="-o" 19 | ) 20 | 21 | if 0 <= theta <= pi: 22 | node_option = "above" 23 | else: 24 | node_option = "below" 25 | 26 | # Label the nth root of unity 27 | tikz.node( 28 | line_to_root.end, 29 | options=node_option, 30 | text=f"$e^{{ (2 \cdot \pi \cdot {i})/ {n} }}$", 31 | ) 32 | 33 | tikz.show() 34 | 35 | if __name__ == "__main__": 36 | roots_of_unity(10) 37 | -------------------------------------------------------------------------------- /examples/rotate_circles/rotate_circles.py: -------------------------------------------------------------------------------- 1 | #!/bin/bash/python3 2 | from math import pi, sin, cos 3 | from tikzpy import TikzPicture 4 | from tikzpy.colors import rainbow_colors 5 | 6 | if __name__ == "__main__": 7 | tikz = TikzPicture(center=True) 8 | 9 | n = 30 10 | for i in range(n): 11 | point = (sin(2 * pi * i / n), cos(2 * pi * i / n)) 12 | 13 | for j in range(0, 8): 14 | tikz.circle(point, 2 + j * 0.2, options="color=" + rainbow_colors(i + j)).shift_( 15 | 0, -2 16 | ) 17 | 18 | tikz.show() 19 | -------------------------------------------------------------------------------- /examples/rotate_plot/rotate_plot.py: -------------------------------------------------------------------------------- 1 | #!/bin/bash/python3 2 | from math import exp 3 | from tikzpy import TikzPicture 4 | from tikzpy.colors import rainbow_colors 5 | 6 | if __name__ == "__main__": 7 | tikz = TikzPicture() 8 | # points = [(14.4, 3.2), (16.0, 3.6), (16.8, 4.8), (16.0, 6.8), (16.4, 8.8), (13.6, 8.8), (12.4, 7.6), (12.8, 5.6), (12.4, 3.6)] 9 | # points = [(6.6,11.4), (5.3,8.8), (3.6,9.9), (2.8,7.9), (3.7,6.1), (4.5,4), (6.2,4.2), (6.7,5.5), (8.5,4.3), (9.5,6.7), (8.8,8.5), (9.4,11.1), (7.7,11)] 10 | points = [ 11 | (5.6, 11.1), 12 | (5.2, 9.6), 13 | (3.2, 10.6), 14 | (4.3, 7.3), 15 | (3, 4.1), 16 | (5.6, 5.2), 17 | (7.2, 3.9), 18 | (8.4, 5.6), 19 | (10.2, 4.5), 20 | (8.7, 6.9), 21 | (10, 8.6), 22 | (8.1, 8.8), 23 | (9.3, 11.8), 24 | (7.2, 11.1), 25 | (6.2, 12.5), 26 | ] 27 | 28 | for theta in range(0, 540, 5): 29 | # Plot the points 30 | plot = tikz.plot_coordinates( 31 | options=f"fill = {rainbow_colors(theta)}", 32 | plot_options="smooth, tension=.5, closed hobby", 33 | points=points, 34 | ) 35 | # Rotate them 36 | plot.rotate_(theta, about_pt=(0, 0)) 37 | # Scale them 38 | plot.scale_(exp(-1.5 * theta / 180)) 39 | 40 | tikz.show() 41 | -------------------------------------------------------------------------------- /examples/shift_plot/shift_plot.py: -------------------------------------------------------------------------------- 1 | #!/bin/bash/python3 2 | from tikzpy import TikzPicture 3 | from tikzpy.colors import rainbow_colors 4 | 5 | 6 | if __name__ == "__main__": 7 | tikz = TikzPicture() 8 | # points = [(14.4, 3.2), (16.0, 3.6), (16.8, 4.8), (16.0, 6.8), (16.4, 8.8), (13.6, 8.8), (12.4, 7.6), (12.8, 5.6), (12.4, 3.6)] 9 | # points = [(6.6,11.4), (5.3,8.8), (3.6,9.9), (2.8,7.9), (3.7,6.1), (4.5,4), (6.2,4.2), (6.7,5.5), (8.5,4.3), (9.5,6.7), (8.8,8.5), (9.4,11.1), (7.7,11)] 10 | points = [ 11 | (5.6, 11.1), 12 | (5.2, 9.6), 13 | (3.2, 10.6), 14 | (4.3, 7.3), 15 | (3, 4.1), 16 | (5.6, 5.2), 17 | (7.2, 3.9), 18 | (8.4, 5.6), 19 | (10.2, 4.5), 20 | (8.7, 6.9), 21 | (10, 8.6), 22 | (8.1, 8.8), 23 | (9.3, 11.8), 24 | (7.2, 11.1), 25 | (6.2, 12.5), 26 | ] 27 | 28 | for i in range(1, 20): 29 | plot = tikz.plot_coordinates( 30 | options=f"fill = {rainbow_colors(i)}, opacity = 0.5", 31 | plot_options="smooth, tension=.5, closed hobby", 32 | points=points, 33 | ) 34 | plot.shift_(0, i / 5) 35 | plot.rotate_(45, about_pt=plot.center, radians=False) 36 | 37 | tikz.show() 38 | -------------------------------------------------------------------------------- /examples/sigmoid/sigmoid.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from tikzpy import TikzPicture, R2_Space 3 | import math 4 | 5 | vert_scale = 4.5 6 | horiz_scale = 1.5 7 | 8 | def sigmoid(val): 9 | val = horiz_scale * val 10 | return 1/(1 + math.pow(math.e, -val)) 11 | 12 | 13 | if __name__ == "__main__": 14 | tikz = TikzPicture() 15 | xrange = (-3, 3) 16 | 17 | # Set up xy-plane 18 | xy_plane = R2_Space(x_interval=(-4, 4), y_interval=(0, vert_scale + .5)) 19 | xy_plane.x_axis_options = "Gray!30, ->" 20 | xy_plane.y_axis_options = "Gray!30, ->" 21 | tikz.draw(xy_plane) 22 | 23 | # Collect (x,y) data 24 | x = horiz_scale * np.linspace(xrange[0], xrange[1], 200) 25 | y = [vert_scale*sigmoid(val) for val in x] 26 | 27 | line = tikz.line((-3.5, 4.5), (4.5, 4.5), options="dashed") 28 | tikz.node(line.start, options="left", text="$y=1$") 29 | # Plot it 30 | points = tikz.plot_coordinates(list(zip(x, y)), options="ProcessBlue") 31 | tikz.show() 32 | -------------------------------------------------------------------------------- /examples/sphere_loop/sphere_loop.py: -------------------------------------------------------------------------------- 1 | #!/bin/bash/python3 2 | from tikzpy import TikzPicture 3 | from tikzpy.colors import rainbow_colors 4 | import numpy as np 5 | 6 | if __name__ == "__main__": 7 | tikz = TikzPicture() 8 | tikz.circle((0, 0), 3, options="thick, gray!10", action="filldraw") 9 | 10 | x_r = 3 11 | y_r = 1.5 12 | 13 | for t in np.linspace(0, 2 * np.pi, 200): 14 | x_pos = 3 * np.cos(t) 15 | y_pos = 3 * np.sin(t) 16 | 17 | arc_one = tikz.arc( 18 | (x_pos, y_pos), 19 | 0, 20 | 180, 21 | x_radius=x_r, 22 | y_radius=y_r, 23 | options=f"color={rainbow_colors(int(t*180/np.pi))}", 24 | ) 25 | arc_two = tikz.arc( 26 | (x_pos - 2 * x_r, y_pos), 27 | 180, 28 | 360, 29 | x_radius=x_r, 30 | y_radius=y_r, 31 | options=f"color={rainbow_colors(int(t*180/np.pi))}", 32 | ) 33 | 34 | tikz.show() 35 | -------------------------------------------------------------------------------- /examples/spiral/spiral.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from tikzpy import TikzPicture, PlotCoordinates, Circle 3 | 4 | if __name__ == "__main__": 5 | tikz = TikzPicture(center=True) 6 | 7 | for height in [0, 1.25, 2.5, 3.75]: 8 | """Idea: No Tikz solution exists for drawing this spiral such that self intersections are drawn with 9 | a "crossing over" feature. All documented methods (e.g., preaction, decorations, knots library) don't work. 10 | Instead, we : 11 | Create two curves, one before the self intersection and one after. 12 | At the self intersection, we draw a white circle. 13 | We then draw in the order: first curve half, white circle, second curve half. This provides the necessary crossing over feature. 14 | """ 15 | curve_under = PlotCoordinates([], plot_options="smooth") # Curve before cross over 16 | curve_over = PlotCoordinates([], plot_options="smooth") # Curve after cross over 17 | points = [] # Points on the spiral 18 | under = True # Controls if we are drawing curve_under or curve_over 19 | t = 0 20 | while t < 2 * np.pi / 5 + 0.1: 21 | x = 2 * np.cos(5 * t) 22 | y = 2 * np.sin(5 * t) * 0.3 + t + height 23 | 24 | if np.abs(t - 0.1731) < 3.5e-3: # Draw white space at self intersection 25 | under = False 26 | white_space = Circle( 27 | (x, y), radius=0.05, options="fill=white", action="fill" 28 | ) 29 | 30 | else: # Otherwise, collect points on the curve 31 | if under: 32 | curve_under.points.append((x, y)) 33 | else: 34 | curve_over.points.append((x, y)) 35 | t += 2 * np.pi / 500 36 | 37 | # Draw everything in this specific order 38 | tikz.draw(curve_under, white_space, curve_over) 39 | 40 | # Annotations: \\vdots, S^1, x, \mathbb{R}^1 41 | tikz.node((0, -0.4), text="$\\vdots$") 42 | tikz.node((-2.8, -0.2), text="$\\vdots$") 43 | tikz.node((-2.8, 5.3), text="$\\vdots$") 44 | tikz.node((2.4, -2), text="$S^1$") 45 | tikz.node((-1.5, -2.6), text="$x$") 46 | tikz.node((2.4, 0.7), text="$\\mathbb{R}^1$") 47 | # Arrow 48 | tikz.line((0, -0.9), (0, -1.2), options="->, =>Stealth") 49 | # Circles and x's 50 | for i, math in [(0, "x-1"), (1, "x"), (2, "x+1"), (3, "x+2")]: 51 | x = -1.5 52 | y = 0.37 + 1.25 * i 53 | tikz.circle((x, y), radius=0.05, action="fill") 54 | tikz.node((x - 1.3, y + 0.2), text=f"${math}$") 55 | 56 | # S^1 57 | S1 = tikz.ellipse((0, -2), x_axis=2, y_axis=2 * 0.2) 58 | tikz.circle((-1.5, -2.255), radius=0.05, action="fill") 59 | 60 | tikz.show() 61 | -------------------------------------------------------------------------------- /examples/symbolic_integration/integrate_and_plot.py: -------------------------------------------------------------------------------- 1 | #!/bin/bash/python3 2 | import numpy as np 3 | from sympy import * # Without caution, import * can become a very stupid idea. Here it is okay. 4 | from sympy.abc import x # Don't name any function, variable, etc, as 'x'. 5 | from tikzpy import TikzPicture 6 | from tikzpy.colors import rainbow_colors 7 | 8 | """ Uses Sympy to iteratively calculate n-order integrals. These are then used by tikzpy 9 | to plot. 10 | Ex: 11 | >>> integrate_n_times(poly(x**2), 5) 12 | >>> integrate_n_times(poly(x**2)-2, 5) 13 | >>> integrate_n_times(sin(x)+x, 3) 14 | >>> integrate_n_times(log(x), 3, 0.1, 5) 15 | """ 16 | 17 | 18 | def integrate_n_times(func, n, x_start=-2.5, x_end=2.5): 19 | """Given a function func, we integrate and plot each of the i-order integrals of 20 | func, where i = 1, 2, ... , n. 21 | 22 | func (sympy.core.function.FunctionClass) : A Sympy function. Ex: sin(x), x**2, log(x), etc. 23 | n (int) : The number of integrals we would like to see 24 | x_start : The value of x we should begin plotting 25 | x_end : The value of x we should stop plotting 26 | """ 27 | # Create a TikzPicture 28 | tikz = TikzPicture() 29 | # x- and y- axis 30 | tikz.line((-4, 0), (4, 0), options="Gray!40, thick, <->") 31 | tikz.line((0, -4), (0, 4), options="Gray!40, thick, <->") 32 | 33 | integrals = [(func, [])] 34 | for i in range(1, n): 35 | next_int = integrate(integrals[i - 1][0], x) 36 | integrals.append((next_int, [])) 37 | 38 | n_samples = 100 39 | for i in np.linspace(x_start, x_end, 100): # 100 samples from (x_start, x_end) 40 | for integ in integrals: 41 | integ_val = (i, float(integ[0].subs(x, i))) 42 | integ[1].append(integ_val) 43 | 44 | for i, integ in enumerate(integrals): 45 | tikz.plot_coordinates(integ[1], options=f"<->, color= {rainbow_colors(3*i)}") 46 | if i % 2: 47 | pos = integ[1][-1] 48 | options = "right" 49 | else: 50 | pos = integ[1][0] 51 | options = "left" 52 | tikz.node(pos, options=options, text=f"${latex(integ[0])}$") 53 | 54 | tikz.show() 55 | 56 | if __name__ == "__main__": 57 | integrate_n_times(poly(x**2), 5) 58 | 59 | -------------------------------------------------------------------------------- /examples/ven_diagrams/intersections_scope_clip.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from itertools import combinations 3 | from tikzpy import TikzPicture, Circle, PlotCoordinates, Rectangle 4 | from tikzpy.colors import rainbow_colors 5 | 6 | tikz = TikzPicture() 7 | 8 | def ven_diagram(blobs, show_outlines=False): 9 | """A function that takes in an arbitrary number of 2D blobs and intersects and colors every 10 | single intersection area. 11 | """ 12 | # First we fill in the blobs 13 | for blob in blobs: 14 | tikz.draw(blob) 15 | # Next we compute all of the intersections of the blobs we must make. 16 | tuple_indices = [] 17 | for tuple_size in range(2, len(blobs) + 1): 18 | tuple_indices += list(combinations(range(1, len(blobs) + 1), tuple_size)) 19 | 20 | # Now we clip and draw the blobs. 21 | for j, tuple_index in enumerate(tuple_indices): 22 | scope = tikz.scope() 23 | clip_draw_blobs = [] 24 | for ind in tuple_index: 25 | clip_draw_blobs.append(blobs[ind - 1]) 26 | 27 | # We create the scope environment 28 | scope = tikz.scope() 29 | # Now we clip and draw. We clip all the blobs except the last one, which we draw. 30 | for i, blob in enumerate(clip_draw_blobs): 31 | blob_copy = blob.copy() 32 | 33 | if i != len(clip_draw_blobs) - 1: 34 | scope.clip(blob_copy) 35 | else: 36 | blob_copy.options = f"fill = {rainbow_colors(j)}, opacity = 0.7" 37 | scope.append(blob_copy) 38 | 39 | # We're done, draw the outline 40 | if show_outlines: 41 | for blob in blobs: 42 | draw_blob = blob.copy(options="", action="draw") 43 | tikz.draw(draw_blob) 44 | tikz.write() 45 | tikz.show() 46 | 47 | # A set of three plots 48 | pts_one = [ 49 | (-2.1, 1.5), 50 | (-1, 2.5), 51 | (1.5, 1), 52 | (4, 0.5), 53 | (3, -0.5), 54 | (2.5, -3), 55 | (0, -0.2), 56 | (-3, -2.5), 57 | ] 58 | 59 | 60 | pts_two = [ 61 | (-3.5, -0.5), 62 | (-3, -2.5), 63 | (-1.5, -3.5), 64 | (1, -2), 65 | (3.5, -2.5), 66 | (3.5, 0.5), 67 | (2, 2), 68 | (-0.5, -1.5), 69 | (-3, 2), 70 | ] 71 | 72 | pts_three = [(-1.5, 0), (1, 1), (2, 0), (1, -1), (0, -2), (-2, -1)] 73 | 74 | plot_options = "smooth, tension=.7, closed hobby" 75 | blob1 = PlotCoordinates( 76 | pts_one, options="red!80", plot_options=plot_options, action="fill" 77 | ) 78 | blob2 = PlotCoordinates( 79 | pts_two, options="ProcessBlue!80", plot_options=plot_options, action="fill" 80 | ) 81 | blob3 = PlotCoordinates( 82 | pts_three, options="Green!80", plot_options=plot_options, action="fill" 83 | ) 84 | blobs = [blob1, blob2, blob3] 85 | 86 | if __name__ == "__main__": 87 | # Four specific circles 88 | circle1 = Circle((0, 0), 2, options="purple, opacity = 0.7", action="fill") 89 | circle2 = Circle((1, 0), 2, options="ProcessBlue, opacity = 0.7", action="fill") 90 | circle3 = Circle((1, 1), 2, options="Magenta, opacity = 0.7", action="fill") 91 | circle4 = Circle((0, 1), 2, options="ForestGreen, opacity = 0.7", action="fill") 92 | 93 | # 9 rainbow circles 94 | circles = [] 95 | for i in range(1, 4): 96 | for j in range(1, 4): 97 | circ = Circle( 98 | (i, j), 99 | 1.5, 100 | options=f"fill = {rainbow_colors(i*j)}, fill opacity = 0.7", 101 | action="fill", 102 | ) 103 | circles.append(circ) 104 | ven_diagram(circles) 105 | #ven_diagram(blobs) 106 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Tikz-Python 2 | theme: 3 | name: material 4 | features: 5 | - content.code.copy 6 | - navigation.tabs 7 | icon: 8 | repo: fontawesome/brands/github 9 | favicon: png/favicon.png 10 | logo: png/favicon.png 11 | 12 | 13 | plugins: 14 | - search 15 | - mkdocstrings: 16 | handlers: 17 | python: 18 | paths: [src] # search packages in the src folder 19 | options: 20 | allow_inspection: true 21 | show_source: false 22 | show_symbol_type_toc: true 23 | show_symbol_type_heading: true 24 | show_root_heading: true 25 | show_root_full_path: false 26 | separate_signature: true 27 | show_signature_annotations: true 28 | watch: 29 | - src/tikzpy 30 | 31 | 32 | markdown_extensions: 33 | - pymdownx.highlight: 34 | anchor_linenums: true 35 | line_spans: __span 36 | pygments_lang_class: true 37 | - pymdownx.inlinehilite 38 | - pymdownx.snippets 39 | - pymdownx.superfences 40 | - attr_list 41 | 42 | extra_css: 43 | - stylesheets/extra.css 44 | 45 | repo_url: https://github.com/ltrujello/Tikz-Python 46 | 47 | nav: 48 | - Home: index.md 49 | - Tutorials: tutorials.md 50 | - Examples: examples.md 51 | - API Documentation: 52 | - TikzPicture: API_Documentation/tikz_picture.md 53 | - Circle: API_Documentation/circle.md 54 | - Ellipse: API_Documentation/ellipse.md 55 | - Line: API_Documentation/line.md 56 | - Node: API_Documentation/node.md 57 | - PlotCoordinates: API_Documentation/plot_coordinates.md 58 | - Point: API_Documentation/point.md 59 | - Rectangle: API_Documentation/rectangle.md 60 | - Arc: API_Documentation/arc.md 61 | - Scope: API_Documentation/scope.md 62 | - Installation: installation.md -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "tikz_python" 7 | version = "0.0.5" 8 | authors = [ 9 | { name="Luke Trujillo", email="ltrujillo@hmc.edu" }, 10 | ] 11 | description = "A python wrapper for tikz" 12 | readme = "README.md" 13 | requires-python = ">=3.7" 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | ] 19 | 20 | [tool.ruff] 21 | ignore-init-module-imports = true 22 | [project.urls] 23 | "Homepage" = "https://github.com/ltrujello/Tikz-Python/" 24 | "Documentation" = "https://ltrujello.github.io/Tikz-Python/" 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | black==23.7.0 2 | click==8.1.6 3 | iniconfig==2.0.0 4 | mypy-extensions==1.0.0 5 | numpy==1.25.1 6 | packaging==23.1 7 | pathspec==0.11.1 8 | platformdirs==3.9.1 9 | pluggy==1.2.0 10 | pytest==7.4.0 11 | ruff==0.0.280 12 | -------------------------------------------------------------------------------- /src/tikzpy/__init__.py: -------------------------------------------------------------------------------- 1 | from tikzpy.tikz_environments.tikz_picture import TikzPicture 2 | from tikzpy.tikz_environments.scope import Scope 3 | from tikzpy.tikz_environments.clip import Clip 4 | from tikzpy.drawing_objects.line import Line 5 | from tikzpy.drawing_objects.plotcoordinates import PlotCoordinates 6 | from tikzpy.drawing_objects.circle import Circle 7 | from tikzpy.drawing_objects.node import Node 8 | from tikzpy.drawing_objects.rectangle import Rectangle 9 | from tikzpy.drawing_objects.ellipse import Ellipse 10 | from tikzpy.drawing_objects.arc import Arc 11 | from tikzpy.drawing_objects.xy_plane import R2_Space 12 | from tikzpy.drawing_objects.point import Point 13 | 14 | __all__ = [ 15 | TikzPicture, 16 | Scope, 17 | Clip, 18 | Line, 19 | PlotCoordinates, 20 | Circle, 21 | Node, 22 | Rectangle, 23 | Ellipse, 24 | Arc, 25 | R2_Space, 26 | Point, 27 | ] 28 | -------------------------------------------------------------------------------- /src/tikzpy/colors/__init__.py: -------------------------------------------------------------------------------- 1 | from .colors import rgb, rainbow_colors, xcolors 2 | -------------------------------------------------------------------------------- /src/tikzpy/colors/colors.py: -------------------------------------------------------------------------------- 1 | def rgb(r: float, g: float, b: float) -> str: 2 | """A wrapper function that outputs xcolor/Tikz code for coloring via rgb values. 3 | 4 | When calling rgb, it is necessary to specify "color = " or "fill =" right before. 5 | E.g., tikz.line(... options = "color =" + rgb(r,g,b) ...) 6 | 7 | This is an annoying aspect with Tikz. 8 | """ 9 | return f"{{ rgb,255:red, {r}; green, {g}; blue, {b} }}" 10 | 11 | 12 | def rainbow_colors(i: int) -> str: 13 | """A wrapper function for obtaining rainbow colors.""" 14 | return rainbow_cols[i % len(rainbow_cols)] 15 | 16 | 17 | def xcolors(i: int) -> str: 18 | """A wrapper function to obtain xcolors. 19 | Any integer is valid. 20 | """ 21 | return xcols[i % len(xcols)] 22 | 23 | 24 | # Collect xcolor names 25 | xcols = [ 26 | "Apricot", 27 | "Aquamarine", 28 | "Bittersweet", 29 | "Black", 30 | "Blue", 31 | "BlueGreen", 32 | "BlueViolet", 33 | "BrickRed", 34 | "Brown", 35 | "BurntOrange", 36 | "CadetBlue", 37 | "CarnationPink", 38 | "Cerulean", 39 | "CornflowerBlue", 40 | "Cyan", 41 | "Dandelion", 42 | "DarkOrchid", 43 | "Emerald", 44 | "ForestGreen", 45 | "Fuchsia", 46 | "Goldenrod", 47 | "Gray", 48 | "Green", 49 | "GreenYellow", 50 | "JungleGreen", 51 | "Lavender", 52 | "LimeGreen", 53 | "Magenta", 54 | "Mahogany", 55 | "Maroon", 56 | "Melon", 57 | "MidnightBlue", 58 | "Mulberry", 59 | "NavyBlue", 60 | "OliveGreen", 61 | "Orange", 62 | "OrangeRed", 63 | "Orchid", 64 | "Peach", 65 | "Periwinkle", 66 | "PineGreen", 67 | "Plum", 68 | "ProcessBlue", 69 | "Purple", 70 | "RawSienna", 71 | "Red", 72 | "RedOrange", 73 | "RedViolet", 74 | "Rhodamine", 75 | "RoyalBlue", 76 | "RoyalPurple", 77 | "RubineRed", 78 | "Salmon", 79 | "SeaGreen", 80 | "Sepia", 81 | "SkyBlue", 82 | "SpringGreen", 83 | "Tan", 84 | "TealBlue", 85 | "Thistle", 86 | "Turquoise", 87 | "Violet", 88 | "VioletRed", 89 | "White", 90 | "WildStrawberry", 91 | "Yellow", 92 | "YellowGreen", 93 | "YellowOrange", 94 | ] 95 | 96 | rainbow_cols = [ 97 | "{rgb,255:red, 255; green, 0; blue, 0 }", # red 98 | "{rgb,255:red, 255; green, 125; blue, 0 }", # orange 99 | "{rgb,255:red, 255; green, 240; blue, 105 }", # yellow 100 | "{rgb,255:red, 125; green, 255; blue, 0 }", # spring 101 | "{rgb,255:red, 0; green, 255; blue, 0 }", # green 102 | "{rgb,255:red, 0; green, 255; blue, 125 }", # turquoise 103 | "{rgb,255:red, 0; green, 255; blue, 255 }", # cyan 104 | "{rgb,255:red, 0; green, 125; blue, 255 }", # ocean 105 | "{rgb,255:red, 0; green, 0; blue, 255 }", # blue 106 | "{rgb,255:red, 125; green, 0; blue, 255 }", # violet 107 | "{rgb,255:red, 255; green, 0; blue, 12 }", # magenta 108 | "{rgb,255:red, 255; green, 0; blue, 255 }", # raspberry 109 | ] 110 | -------------------------------------------------------------------------------- /src/tikzpy/drawing_objects/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/src/tikzpy/drawing_objects/__init__.py -------------------------------------------------------------------------------- /src/tikzpy/drawing_objects/circle.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import math 3 | from typing import Tuple, Union 4 | from tikzpy.drawing_objects.point import Point 5 | from tikzpy.drawing_objects.drawing_object import DrawingObject 6 | 7 | 8 | class Circle(DrawingObject): 9 | r""" 10 | A class to create circles in the tikz environment. 11 | 12 | Parameters: 13 | center: Pair of floats representing the center of the circle 14 | radius: Length (in cm) of the radius 15 | options: String containing the drawing options (e.g, "Blue") 16 | action: The type of TikZ action to use. Default is "draw". 17 | """ 18 | 19 | def __init__( 20 | self, 21 | center: Union[Tuple[float, float], Point], 22 | radius: float, 23 | options: str = "", 24 | action: str = "draw", 25 | ) -> None: 26 | self._center = Point(center) 27 | self.radius = radius 28 | self.options = options 29 | super().__init__(action, self.options) 30 | 31 | @property 32 | def center(self) -> Point: 33 | """ 34 | Returns a Point object representing the center of the circle. 35 | 36 | ```python 37 | from tikzpy import TikzPicture 38 | tikz = TikzPicture() 39 | circle = tikz.circle((0,0), 1, options="fill=ProcessBlue!30", action="filldraw") 40 | tikz.node(circle.center, text="O") 41 | tikz.show() 42 | ``` 43 | 44 | 45 | """ 46 | return self._center 47 | 48 | @center.setter 49 | def center(self, new_center: Union[tuple, Point]) -> None: 50 | if isinstance(new_center, (tuple, Point)): 51 | self._center = Point(new_center) 52 | else: 53 | raise TypeError(f"Invalid type '{type(new_center)}' for center") 54 | 55 | @property 56 | def north(self) -> Point: 57 | """ 58 | Returns a Point object representing the north point on the circle. 59 | 60 | ```python 61 | from tikzpy import TikzPicture 62 | tikz = TikzPicture() 63 | circle = tikz.circle((0,0), 1, options="fill=ProcessBlue!30", action="filldraw") 64 | tikz.node(circle.north, text="$N$", options="above") 65 | tikz.show() 66 | ``` 67 | 68 | 69 | """ 70 | return self._center + (0, self.radius) 71 | 72 | @property 73 | def east(self) -> Point: 74 | """ 75 | Returns a Point object representing the east point on the circle. 76 | 77 | ```python 78 | from tikzpy import TikzPicture 79 | tikz = TikzPicture() 80 | circle = tikz.circle((0,0), 1, options="fill=ProcessBlue!30", action="filldraw") 81 | tikz.node(circle.east, text="$E$", options="right") 82 | tikz.show() 83 | ``` 84 | 85 | 86 | """ 87 | return self._center + (self.radius, 0) 88 | 89 | @property 90 | def south(self) -> Point: 91 | """ 92 | Returns a Point object representing the south point on the circle. 93 | 94 | ```python 95 | from tikzpy import TikzPicture 96 | tikz = TikzPicture() 97 | circle = tikz.circle((0,0), 1, options="fill=ProcessBlue!30", action="filldraw") 98 | tikz.node(circle.south, text="$S$", options="below") 99 | tikz.show() 100 | ``` 101 | 102 | 103 | """ 104 | return self._center - (0, self.radius) 105 | 106 | @property 107 | def west(self) -> Point: 108 | """ 109 | Returns a Point object representing the west point on the circle. 110 | 111 | ```python 112 | from tikzpy import TikzPicture 113 | tikz = TikzPicture() 114 | circle = tikz.circle((0,0), 1, options="fill=ProcessBlue!30", action="filldraw") 115 | tikz.node(circle.west, text="$W$", options="left") 116 | tikz.show() 117 | ``` 118 | 119 | 120 | """ 121 | return self._center - (self.radius, 0) 122 | 123 | @property 124 | def _command(self) -> str: 125 | return f"{self._center} circle ({self.radius}cm)" 126 | 127 | def point_at_arg(self, theta: float, radians: bool = False) -> tuple: 128 | r"""Returns the point on the circle at angle theta. Both degrees and radians can be specified. 129 | 130 | 131 | ```python 132 | from tikzpy import TikzPicture 133 | tikz = TikzPicture(center=True) 134 | circle = tikz.circle((0,0), 1, options="fill=ProcessBlue!30", action="filldraw") 135 | tikz.node(circle.point_at_arg(45), text="$\pi/2$", options="right") 136 | tikz.node(circle.point_at_arg(135), text="$3\pi/4$", options="left") 137 | tikz.node(circle.point_at_arg(270), text="$3\pi/2$", options="below") 138 | tikz.show() 139 | ``` 140 | 141 | 142 | """ 143 | if not radians: 144 | theta = math.radians(theta) 145 | return self.center.x + self.radius * math.cos( 146 | theta 147 | ), self.center.y + self.radius * math.sin(theta) 148 | 149 | def shift_(self, xshift: float, yshift: float) -> None: 150 | self._center.shift_(xshift, yshift) 151 | 152 | def scale_(self, scale: float) -> None: 153 | self._center.scale_(scale) 154 | self.radius *= scale 155 | 156 | def rotate_( 157 | self, angle: float, about_pt: Tuple[float, float] = None, radians: bool = False 158 | ) -> None: 159 | self._center.rotate_(angle, about_pt, radians) 160 | 161 | def shift(self, xshift: float, yshift: float) -> "Circle": 162 | new_circle = self.copy() 163 | new_circle.shift_(xshift, yshift) 164 | return new_circle 165 | 166 | def scale(self, scale: float) -> "Circle": 167 | new_circle = self.copy() 168 | new_circle.scale_(scale) 169 | return new_circle 170 | 171 | def rotate( 172 | self, angle: float, about_pt: Tuple[float, float] = None, radians: bool = False 173 | ) -> "Circle": 174 | new_circle = self.copy() 175 | new_circle.rotate_(angle, about_pt, radians) 176 | return new_circle 177 | -------------------------------------------------------------------------------- /src/tikzpy/drawing_objects/drawing_object.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from abc import ABC, abstractmethod 3 | from typing import Tuple 4 | from copy import deepcopy 5 | from tikzpy.utils.helpers import brackets 6 | from tikzpy.drawing_objects.node import Node 7 | 8 | 9 | class DrawingObject(ABC): 10 | r"""A generic class for our drawing objects to inherit properties from. 11 | 12 | Attributes : 13 | action (str) : A string containing either "draw", "filldraw", "fill", or "path". This controls 14 | the type of command statement we generate: \draw, \filldraw, \fill, or \path, ... 15 | By default, it is draw. 16 | options (str) : A string of valid Tikz options for the drawing object. 17 | command (str) : A string consisting of the latter half of our tikz code to create the full statement. 18 | node (Node object) : A Node object which can be appended to the end of the statement. 19 | """ 20 | 21 | def __init__(self, action: str = "draw", options: str = "") -> None: 22 | self.action = action 23 | self.options = options 24 | self.node: Node = None 25 | 26 | if not isinstance(self.action, str): 27 | raise TypeError(f"The action argument {self.action} is not of type str") 28 | 29 | if self.action.replace(" ", "") not in ["draw", "fill", "filldraw", "path"]: 30 | raise ValueError( 31 | f"The action {self.action} is not a valid action ('draw', 'fill', 'filldraw', 'path'). Perhaps you mispelled it." 32 | ) 33 | 34 | @property 35 | @abstractmethod 36 | def _command(self) -> str: 37 | r"""The latter half of the Tikz Code for the drawing object. 38 | E.g.: For a Line with code "\draw (0,0) to (1,1), _command corresponds to "(0,0) to (1,1)". 39 | """ 40 | 41 | @abstractmethod 42 | def shift(self, xshift: float, yshift: float) -> "DrawingObject": 43 | """Shift the coordinates of the drawing object by (xshift, yshift)""" 44 | 45 | @abstractmethod 46 | def scale(self, scale: float) -> "DrawingObject": 47 | """Scale the coordinates of the drawing object by amount "scale".""" 48 | 49 | @abstractmethod 50 | def rotate( 51 | self, angle: float, about_pt: Tuple[float, float] = None, radians: bool = False 52 | ) -> "DrawingObject": 53 | """Rotate the coordinates of the drawing object (counterclockwise) by "angle" about the 54 | point "about_pt". 55 | """ 56 | 57 | @property 58 | def code(self) -> str: 59 | """Full Tikz code for this drawing object.""" 60 | draw_cmd = f"\\{self.action}{brackets(self.options)} {self._command}" 61 | if self.node is None: 62 | return f"{draw_cmd};" 63 | else: 64 | return f"{draw_cmd} node{brackets(self.node.options)} {self.node._command};" 65 | 66 | def add_node( 67 | self, position: tuple = None, options: str = "", text: str = "" 68 | ) -> None: 69 | """A method to build a node on a drawing object directly. 70 | This bypasses having to (1) define a Node object and then (2) use node.setter. 71 | """ 72 | new_node = Node(position, options, text) 73 | self.node = new_node 74 | 75 | def __deepcopy__(self, memo: dict) -> DrawingObject: 76 | """Creates a deep copy of a class object. This is useful since in our classes, we chose to set 77 | our methods to modify objects, but not return anything. 78 | """ 79 | cls = self.__class__ 80 | draw_obj = cls.__new__(cls) 81 | memo[id(self)] = draw_obj 82 | for attr, value in self.__dict__.items(): 83 | setattr(draw_obj, attr, deepcopy(value, memo)) 84 | return draw_obj 85 | 86 | def copy(self, **kwargs: dict) -> DrawingObject: 87 | """Allows one to simultaneously make a (deep) copy of a drawing object and modify 88 | attributes of the drawing object in one step. 89 | """ 90 | new_copy = deepcopy(self) 91 | for attr, val in kwargs.items(): 92 | setattr(new_copy, attr, val) 93 | return new_copy 94 | 95 | def __repr__(self) -> str: 96 | return self.code 97 | -------------------------------------------------------------------------------- /src/tikzpy/drawing_objects/drawing_utils.py: -------------------------------------------------------------------------------- 1 | import math 2 | from tikzpy.drawing_objects.line import Line 3 | from tikzpy.drawing_objects.circle import Circle 4 | from tikzpy.drawing_objects.point import Point 5 | 6 | 7 | def line_connecting_circle_edges( 8 | circle_a: Circle, circle_b: Circle, options="", src_delta=0, dst_delta=0 9 | ) -> Line: 10 | """ 11 | Returns a line that connects the outer edges of circle_a 12 | to circle_b. 13 | """ 14 | pos_a = circle_a.center 15 | pos_b = circle_b.center 16 | rad_a = circle_a.radius + src_delta 17 | rad_b = circle_b.radius + dst_delta 18 | 19 | start, end = calc_start_end_between_nodes( 20 | pos_a=pos_a, rad_a=rad_a, pos_b=pos_b, rad_b=rad_b 21 | ) 22 | return Line(start, end, options=options) 23 | 24 | 25 | def calc_start_end_between_nodes(pos_a, rad_a, pos_b, rad_b): 26 | """ 27 | Given two circles A and B with 28 | - coordinates pos_a, pos_b 29 | - radii rad_a, rad_b 30 | return the start and end coordinates of the shortest 31 | line that connects A and B. 32 | """ 33 | x_1, y_1 = pos_a 34 | x_2, y_2 = pos_b 35 | 36 | # Determine the angle between the points 37 | if y_1 - y_2 == 0: 38 | theta = math.pi / 2 39 | else: 40 | theta = math.atan(abs(x_2 - x_1) / abs(y_1 - y_2)) 41 | 42 | # Use the angle and relative sign of the coordinates to calculate x, y positions 43 | if y_2 > y_1: 44 | if x_2 > x_1: 45 | start = (x_1 + rad_a * math.sin(theta), y_1 + rad_a * math.cos(theta)) 46 | end = (x_2 - rad_b * math.sin(theta), y_2 - rad_b * math.cos(theta)) 47 | else: 48 | start = (x_1 - rad_a * math.sin(theta), y_1 + rad_a * math.cos(theta)) 49 | end = (x_2 + rad_b * math.sin(theta), y_2 - rad_b * math.cos(theta)) 50 | else: 51 | if x_2 > x_1: 52 | start = (x_1 + rad_a * math.sin(theta), y_1 - rad_a * math.cos(theta)) 53 | end = (x_2 - rad_b * math.sin(theta), y_2 + rad_b * math.cos(theta)) 54 | else: 55 | start = (x_1 - rad_a * math.sin(theta), y_1 - rad_a * math.cos(theta)) 56 | end = (x_2 + rad_b * math.sin(theta), y_2 + rad_b * math.cos(theta)) 57 | return start, end 58 | 59 | 60 | def draw_segments(tikz, points, circular=True, options=""): 61 | """ 62 | Given a list of points, draw a sequence of line segments between the points. 63 | Returns a dictionary collection of the lines for later modification if necessary. 64 | """ 65 | idx = 0 66 | lines = {} 67 | while idx < len(points): 68 | if idx == len(points) - 1: 69 | if not circular: 70 | break 71 | first_pt = points[-1] 72 | second_pt = points[0] 73 | else: 74 | first_pt = points[idx] 75 | second_pt = points[idx + 1] 76 | 77 | line = tikz.line(first_pt, second_pt, options=options) 78 | lines[(first_pt, second_pt)] = line 79 | idx += 1 80 | return lines 81 | 82 | 83 | def calc_intersection(item_a, item_b): 84 | intersection_map = { 85 | (Circle, Circle): circle_circle_intersection, 86 | (Line, Line): line_line_intersection, 87 | (Line, Circle): line_circle_intersection, 88 | (Circle, Line): circle_line_intersection, 89 | } 90 | 91 | func = intersection_map.get((type(item_a), type(item_b))) 92 | if func: 93 | return func(item_a, item_b) 94 | else: 95 | raise NotImplementedError( 96 | f"No intersection logic for {type(item_a)} and {type(item_b)}" 97 | ) 98 | 99 | 100 | def circle_circle_intersection(circle_a, circle_b): 101 | intersections = _circle_circle_intersection( 102 | circle_a.center.x, 103 | circle_a.center.y, 104 | circle_a.radius, 105 | circle_b.center.x, 106 | circle_b.center.y, 107 | circle_b.radius, 108 | ) 109 | return [Point(pt) for pt in intersections] 110 | 111 | 112 | def _circle_circle_intersection(x1, y1, r1, x2, y2, r2): 113 | # Distance between circle centers 114 | d = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) 115 | 116 | # No solutions if circles are separate or one completely contains the other 117 | if d > r1 + r2 or d < abs(r1 - r2) or d == 0: 118 | return None # No intersection 119 | 120 | # Find a and h for intersection calculations 121 | a = (r1**2 - r2**2 + d**2) / (2 * d) 122 | h = math.sqrt(r1**2 - a**2) 123 | 124 | # Find the point P2, which is the base point of the perpendicular 125 | x3 = x1 + a * (x2 - x1) / d 126 | y3 = y1 + a * (y2 - y1) / d 127 | 128 | # Intersection points 129 | x_int1 = x3 + h * (y2 - y1) / d 130 | y_int1 = y3 - h * (x2 - x1) / d 131 | 132 | x_int2 = x3 - h * (y2 - y1) / d 133 | y_int2 = y3 + h * (x2 - x1) / d 134 | 135 | # One intersection if circles touch at one point, otherwise two 136 | if d == r1 + r2 or d == abs(r1 - r2): 137 | return [(x_int1, y_int1)] 138 | return [(x_int1, y_int1), (x_int2, y_int2)] 139 | 140 | 141 | def line_line_intersection(line_a, line_b): 142 | intersections = _line_line_intersection( 143 | line_a.start.x, 144 | line_a.start.y, 145 | line_a.end.x, 146 | line_a.end.y, 147 | line_b.start.x, 148 | line_b.start.y, 149 | line_b.end.x, 150 | line_b.end.y, 151 | ) 152 | return [Point(pt) for pt in intersections] 153 | 154 | 155 | def _line_line_intersection(x1, y1, x2, y2, x3, y3, x4, y4): 156 | # Compute coefficients A, B, C for both lines in form: Ax + By = C 157 | A1 = y2 - y1 158 | B1 = x1 - x2 159 | C1 = A1 * x1 + B1 * y1 160 | 161 | A2 = y4 - y3 162 | B2 = x3 - x4 163 | C2 = A2 * x3 + B2 * y3 164 | 165 | det = A1 * B2 - A2 * B1 166 | # Parallel lines 167 | if det == 0: 168 | return None 169 | 170 | x = (C1 * B2 - C2 * B1) / det 171 | y = (A1 * C2 - A2 * C1) / det 172 | return [(x, y)] 173 | 174 | 175 | def circle_line_intersection(circle, line): 176 | intersections = line_circle_intersection(line, circle) 177 | return [Point(pt) for pt in intersections] 178 | 179 | 180 | def line_circle_intersection(line, circle): 181 | m = line.slope() 182 | b = line.y_intercept() 183 | h, k = circle.center # Center of the circle 184 | r = circle.radius 185 | intersections = _line_circle_intersection(m, b, h, k, r) 186 | return [Point(pt) for pt in intersections] 187 | 188 | 189 | def _line_circle_intersection(m, b, h, k, r): 190 | # Quadratic equation coefficients (Ax^2 + Bx + C = 0) 191 | A = 1 + m**2 192 | B = 2 * (m * (b - k) - h) 193 | C = h**2 + (b - k) ** 2 - r**2 194 | 195 | # Discriminant 196 | D = B**2 - 4 * A * C 197 | if D < 0: 198 | # No intersection 199 | return None 200 | elif D == 0: 201 | # One intersection (tangent) 202 | x = -B / (2 * A) 203 | y = m * x + b 204 | return [(x, y)] 205 | 206 | sqrt_D = math.sqrt(D) 207 | x_1 = (-B + sqrt_D) / (2 * A) 208 | x_2 = (-B - sqrt_D) / (2 * A) 209 | y_1 = m * x_1 + b 210 | y_2 = m * x_2 + b 211 | 212 | return [(x_1, y_1), (x_2, y_2)] 213 | -------------------------------------------------------------------------------- /src/tikzpy/drawing_objects/ellipse.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Tuple, Union 3 | from tikzpy.drawing_objects.point import Point 4 | from tikzpy.drawing_objects.drawing_object import DrawingObject 5 | 6 | 7 | class Ellipse(DrawingObject): 8 | r""" 9 | A class to create ellipses in the tikz environment. 10 | 11 | The ellipse class handles ellipses in Tikz. It it analagous to the Tikz command 12 | ``` 13 | \draw[options]
ellipse ( and ); 14 | ``` 15 | 16 | Parameters: 17 | center: Position of the center of the ellipse 18 | x_axis: The length (in cm) of the horizontal axis of the ellipse 19 | y_axis: The length (in cm) of the vertical axis of the ellipse 20 | options: TikZ options to draw with 21 | action: The type of TikZ action to use. Default is "draw" 22 | """ 23 | 24 | def __init__( 25 | self, 26 | center: Union[Tuple[float, float], Point], 27 | x_axis: float, 28 | y_axis: float, 29 | options: str = "", 30 | action: str = "draw", 31 | ) -> None: 32 | self._center = Point(center) 33 | self.x_axis = x_axis 34 | self.y_axis = y_axis 35 | self.options = options 36 | super().__init__(action, self.options) 37 | 38 | @property 39 | def center(self): 40 | return self._center 41 | 42 | @center.setter 43 | def center(self, new_center: Union[tuple, Point]) -> None: 44 | if isinstance(new_center, (tuple, Point)): 45 | self._center = Point(new_center) 46 | else: 47 | raise TypeError(f"Invalid type '{type(new_center)}' for center") 48 | 49 | @property 50 | def north(self) -> Point: 51 | """Returns the north point of the ellipse.""" 52 | return self._center + (0, self.y_axis) 53 | 54 | @property 55 | def east(self) -> Point: 56 | """Returns the east point of the ellipse.""" 57 | return self._center + (self.x_axis, 0) 58 | 59 | @property 60 | def south(self) -> Point: 61 | """Returns the south point of the ellipse.""" 62 | return self._center - (0, self.y_axis) 63 | 64 | @property 65 | def west(self) -> Point: 66 | """Returns the west point of the ellipse.""" 67 | return self._center - (self.x_axis, 0) 68 | 69 | @property 70 | def _command(self) -> str: 71 | return f"{self.center} ellipse ({self.x_axis}cm and {self.y_axis}cm)" 72 | 73 | def shift_(self, xshift: float, yshift: float) -> None: 74 | self._center.shift_(xshift, yshift) 75 | 76 | def scale_(self, scale: float) -> None: 77 | self._center.scale_(scale) 78 | self.x_axis *= scale 79 | self.y_axis *= scale 80 | 81 | def rotate_( 82 | self, angle: float, about_pt: Tuple[float, float], radians: bool = False 83 | ) -> None: 84 | self._center.rotate_(angle, about_pt, radians) 85 | 86 | def shift(self, xshift: float, yshift: float) -> "Ellipse": 87 | new_ellipse = self.copy() 88 | new_ellipse.shift_(xshift, yshift) 89 | return new_ellipse 90 | 91 | def scale(self, scale: float) -> "Ellipse": 92 | new_ellipse = self.copy() 93 | new_ellipse.scale_(scale) 94 | return new_ellipse 95 | 96 | def rotate( 97 | self, angle: float, about_pt: Tuple[float, float], radians: bool = False 98 | ) -> "Ellipse": 99 | new_ellipse = self.copy() 100 | new_ellipse.rotate_(angle, about_pt, radians) 101 | return new_ellipse 102 | -------------------------------------------------------------------------------- /src/tikzpy/drawing_objects/line.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import List, Tuple, Union, Optional 3 | from tikzpy.drawing_objects.point import Point 4 | from tikzpy.drawing_objects.drawing_object import DrawingObject 5 | from tikzpy.utils.helpers import brackets 6 | 7 | 8 | class Line(DrawingObject): 9 | r""" 10 | A class to create lines in the tikz environment. 11 | 12 | The `Line` class helps handle the creation of lines in tikz code. It is analagous to the TikZ code 13 | ``` 14 | \draw[] to [] ; 15 | ``` 16 | Lines in TikZ have even more features, like adding control points, and these features are accessible through the Line class. 17 | 18 | 19 | Parameters: 20 | start: Pair of floats representing the start of the line 21 | end: Pair of floats representing the end of the line 22 | options: String containing Tikz drawing options, e.g. "Blue" 23 | control_pts: List of control points for the line 24 | """ 25 | 26 | def __init__( 27 | self, 28 | start: Union[Tuple[float, float], Point], 29 | end: Union[Tuple[float, float], Point], 30 | options: str = "", 31 | to_options: str = "", 32 | control_pts: List[Tuple] = [], 33 | action: str = "draw", 34 | ) -> None: 35 | self._start = Point(start) 36 | self._end = Point(end) 37 | self.options = options 38 | self.to_options = to_options 39 | self._control_pts = [Point(point) for point in control_pts] 40 | 41 | super().__init__(action, self.options) 42 | 43 | @property 44 | def control_pts(self): 45 | return self._control_pts 46 | 47 | @control_pts.setter 48 | def control_pts(self, new_control_pts): 49 | self._control_pts = [Point(point) for point in new_control_pts] 50 | 51 | @property 52 | def _command(self) -> str: 53 | r"""The Tikz code for a line that comes after \draw[self.options]. It is useful for 54 | us to do this breaking-up of the Tikz code, especially for clipping. However, this 55 | serves no use to the user, so we make it private (well, it's just bells and whistles). 56 | """ 57 | if len(self.control_pts) == 0: 58 | return rf"{self.start} to{brackets(self.to_options)} {self.end}" 59 | 60 | else: 61 | control_stmt = ".. controls " 62 | for pt in self.control_pts: 63 | if pt.z is None: 64 | control_stmt += f"{pt.x, pt.y}" + " and " 65 | else: 66 | control_stmt += f"{pt.x, pt.y, pt.z}" + " and " 67 | control_stmt = control_stmt[:-4] + " .." 68 | return f"{self.start} {control_stmt} {self.end}" 69 | 70 | @property 71 | def start(self) -> Point: 72 | """Returns a Point object representing the start of the line. 73 | ```python 74 | from tikzpy import TikzPicture 75 | 76 | tikz = TikzPicture(center=True) 77 | line = tikz.line((0, 0), (4, 3), options="->") 78 | tikz.node(line.start, text="Start", options="below") 79 | tikz.show() 80 | ``` 81 | 82 | 83 | """ 84 | return self._start 85 | 86 | @property 87 | def end(self) -> Point: 88 | """Returns a Point object representing the end of the line. 89 | 90 | ```python 91 | from tikzpy import TikzPicture 92 | 93 | tikz = TikzPicture(center=True) 94 | line = tikz.line((0, 0), (4, 3), options="->") 95 | tikz.node(line.end, text="End", options="above") 96 | tikz.show() 97 | ``` 98 | 99 | 100 | """ 101 | return self._end 102 | 103 | @start.setter 104 | def start(self, new_start: Union[Tuple, Point]) -> None: 105 | if isinstance(new_start, (tuple, Point)): 106 | self._start = Point(new_start) 107 | else: 108 | raise TypeError(f"Invalid type '{type(new_start)}' for start") 109 | 110 | @end.setter 111 | def end(self, new_end: Union[Tuple, Point]) -> None: 112 | if isinstance(new_end, (tuple, Point)): 113 | self._end = Point(new_end) 114 | else: 115 | raise TypeError(f"Invalid type '{type(new_end)}' for end") 116 | 117 | def pos_at_t(self, t: float) -> Point: 118 | """Returns the point on the line that lies at "time t", on a scale from 0 to 1. 119 | 120 | ```python 121 | from tikzpy import TikzPicture 122 | 123 | tikz = TikzPicture(center=True) 124 | line = tikz.line((0, 0), (4, 3), options="->") 125 | tikz.node(line.pos_at_t(0.7), text="0.7", options="above") 126 | tikz.show() 127 | ``` 128 | 129 | 130 | """ 131 | x_1, y_1 = self._start 132 | x_2, y_2 = self._end 133 | return Point(x_1 * (1 - t) + x_2 * t, y_1 * (1 - t) + y_2 * t) 134 | 135 | def midpoint(self) -> Point: 136 | """Returns a Point object representing the middle of the line. 137 | 138 | ```python 139 | from tikzpy import TikzPicture 140 | 141 | tikz = TikzPicture(center=True) 142 | line = tikz.line((0, 0), (4, 3), options="->") 143 | tikz.node(line.midpoint(), text="$M$", options="above") 144 | tikz.show() 145 | ``` 146 | 147 | 148 | """ 149 | return self.pos_at_t(0.5) 150 | 151 | def slope(self) -> Optional[float]: 152 | """Returns the slope of the line""" 153 | if self.end.x == self.start.x: 154 | return None 155 | return (self.end.y - self.start.y) / (self.end.x - self.start.x) 156 | 157 | def y_intercept(self) -> Optional[float]: 158 | slope = self.slope() 159 | if slope is None: 160 | return None 161 | return self.start.y - slope * self.start.x 162 | 163 | def shift_(self, xshift: float, yshift: float) -> None: 164 | """Shift start, end, and control_pts""" 165 | self._start.shift_(xshift, yshift) 166 | self._end.shift_(xshift, yshift) 167 | for point in self.control_pts: 168 | point.shift_(xshift, yshift) 169 | 170 | def scale_(self, scale: float) -> None: 171 | """Scale start, end, and control_pts.""" 172 | self._start.scale_(scale) 173 | self._end.scale_(scale) 174 | for point in self.control_pts: 175 | point.scale_(scale) 176 | 177 | def rotate_( 178 | self, angle: float, about_pt: Tuple[float, float] = None, radians: bool = False 179 | ) -> None: 180 | """Rotate start, end, and control_pts. By default, the rotation is done relative to the midpoint 181 | of the line.""" 182 | if about_pt is None: 183 | about_pt = self.midpoint 184 | self._start.rotate_(angle, about_pt, radians) 185 | self._end.rotate_(angle, about_pt, radians) 186 | 187 | for point in self.control_pts: 188 | point.rotate_(angle, about_pt, radians) 189 | 190 | def shift(self, xshift: float, yshift: float) -> "Line": 191 | """Shift start, end, and control_pts""" 192 | new_line = self.copy() 193 | new_line.shift_(xshift, yshift) 194 | return new_line 195 | 196 | def scale(self, scale: float) -> "Line": 197 | """Scale start, end, and control_pts.""" 198 | new_line = self.copy() 199 | new_line.scale_(scale) 200 | return new_line 201 | 202 | def rotate( 203 | self, angle: float, about_pt: Tuple[float, float] = None, radians: bool = False 204 | ) -> "Line": 205 | """Rotate start, end, and control_pts. By default, the rotation is done relative to the midpoint 206 | of the line.""" 207 | new_line = self.copy() 208 | new_line.rotate_(angle, about_pt, radians) 209 | return new_line 210 | -------------------------------------------------------------------------------- /src/tikzpy/drawing_objects/node.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Tuple, Union 3 | from copy import deepcopy 4 | from tikzpy.drawing_objects.point import Point 5 | from tikzpy.utils.helpers import brackets 6 | 7 | 8 | class Node: 9 | r""" 10 | A class to manage nodes in a tikz environment. 11 | 12 | This class is equivalent to the tikz code 13 | ``` 14 | \node[] at () {}; 15 | ``` 16 | 17 | Parameters: 18 | position (tuple) : Pair of floats representing the location of the node 19 | options (str) : String containing node options (e.g., "above") 20 | text (str): Text that will be displayed with the node; can use dollar signs $ for LaTeX 21 | """ 22 | 23 | def __init__( 24 | self, 25 | position: Union[Tuple[float, float], Point], 26 | options: str = "", 27 | text: str = "", 28 | ) -> None: 29 | self._position = Point(position) if position is not None else None 30 | self.options = options 31 | self.text = text 32 | 33 | @property 34 | def _command(self) -> str: 35 | if self.position is not None: 36 | return rf"at {self.position} {{ {self.text} }}" 37 | else: 38 | return rf"{{ {self.text} }}" 39 | 40 | @property 41 | def position(self): 42 | """ 43 | Returns a Point object representing the position of the node. This attribute is modifiable. 44 | """ 45 | return self._position 46 | 47 | @position.setter 48 | def position(self, new_pos: Union[Tuple[float, float], Point]) -> None: 49 | if isinstance(new_pos, (tuple, Point)): 50 | self._position = Point(new_pos) 51 | else: 52 | raise TypeError(f"Invalid type '{type(new_pos)}' for node position") 53 | 54 | @property 55 | def code(self) -> str: 56 | return rf"\node{brackets(self.options)} {self._command};" 57 | 58 | def shift_(self, xshift: float, yshift: float) -> None: 59 | if self._position is not None: 60 | self._position.shift_(xshift, yshift) 61 | 62 | def scale_(self, scale: float) -> None: 63 | if self._position is not None: 64 | self._position.scale_(scale) 65 | 66 | def rotate_( 67 | self, angle: float, about_pt: Tuple[float, float], radians: bool = False 68 | ) -> None: 69 | if self._position is not None: 70 | self._position.rotate_(angle, about_pt, radians) 71 | 72 | def shift(self, xshift: float, yshift: float) -> "Node": 73 | new_node = self.copy() 74 | new_node.shift_(xshift, yshift) 75 | return new_node 76 | 77 | def scale(self, scale: float) -> "Node": 78 | new_node = self.copy() 79 | new_node.scale_(scale) 80 | return new_node 81 | 82 | def rotate( 83 | self, angle: float, about_pt: Tuple[float, float], radians: bool = False 84 | ) -> "Node": 85 | new_node = self.copy() 86 | new_node.rotate_(angle, about_pt, radians) 87 | return new_node 88 | 89 | def __deepcopy__(self, memo: dict) -> Node: # TODO: Check this works 90 | """Creates a deep copy of a class object. This is useful since in our classes, we chose to set 91 | our methods to modify objects, but not return anything. 92 | """ 93 | draw_obj = Node( 94 | deepcopy(self._position), 95 | deepcopy(self.options), 96 | deepcopy(self.text), 97 | ) 98 | memo[id(self)] = draw_obj 99 | return draw_obj 100 | 101 | def copy(self, **kwargs: dict) -> Node: 102 | """Allows one to simultaneously make a (deep) copy of a drawing object and modify 103 | attributes of the drawing object in one step. 104 | """ 105 | new_copy = deepcopy(self) 106 | for attr, val in kwargs.items(): 107 | setattr(new_copy, attr, val) 108 | return new_copy 109 | 110 | def __repr__(self) -> str: 111 | return self.code 112 | -------------------------------------------------------------------------------- /src/tikzpy/drawing_objects/plotcoordinates.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple, Union 2 | from tikzpy.drawing_objects.point import Point 3 | from tikzpy.drawing_objects.drawing_object import DrawingObject 4 | from tikzpy.utils.helpers import brackets 5 | 6 | 7 | class PlotCoordinates(DrawingObject): 8 | r""" 9 | A class to manage plots in a tikz environment. 10 | 11 | The PlotCoordinates class is used to represent the plot_coordinates 12 | functionality in TikZ. It is analagous to the TikZ command 13 | ``` 14 | \draw plot[] coordinates{ }; 15 | ``` 16 | 17 | Parameters: 18 | options (str) : String containing drawing options (e.g., "Blue") 19 | plot_options (str) : String containing the plot options (e.g., "smooth cycle") 20 | points (list) : A list of points to be drawn 21 | 22 | """ 23 | 24 | def __init__( 25 | self, 26 | points: Union[List[Tuple], Point], 27 | options: str = "", 28 | plot_options: str = "", 29 | action: str = "draw", 30 | ): 31 | self.points = [Point(point) for point in points] 32 | self.options = options 33 | self.plot_options = plot_options 34 | super().__init__(action, self.options) 35 | 36 | @property 37 | def _command(self) -> str: 38 | cmd: str = rf"plot{brackets(self.plot_options)} coordinates {{" 39 | for pt in self.points: 40 | cmd += str(pt) + " " 41 | cmd += "}" 42 | return cmd 43 | 44 | @property 45 | def center(self) -> "Point": 46 | """Calculates the geometric center (centroid) of a collection of points. 47 | 48 | This property computes the arithmetic mean of the x and y coordinates of all points in the collection. 49 | The result is a new Point object representing the centroid of these points. 50 | 51 | Returns: 52 | Point: A Point object representing the geometric center of the collection of points. 53 | """ 54 | 55 | mean_x = 0 56 | mean_y = 0 57 | for pt in self.points: 58 | mean_x += pt.x 59 | mean_y += pt.y 60 | mean_x = mean_x / len(self.points) 61 | mean_y = mean_y / len(self.points) 62 | return Point(mean_x, mean_y) 63 | 64 | def shift_(self, xshift: float, yshift: float) -> None: 65 | for point in self.points: 66 | point.shift_(xshift, yshift) 67 | 68 | def scale_(self, scale: float) -> None: 69 | for point in self.points: 70 | point.scale_(scale) 71 | 72 | def rotate_( 73 | self, 74 | angle: float, 75 | about_pt: Union[Tuple[float, float], None, Point] = None, 76 | radians: bool = False, 77 | ) -> None: 78 | if about_pt is None: 79 | about_pt = self.center 80 | for point in self.points: 81 | point.rotate_(angle, about_pt, radians) 82 | 83 | def shift(self, xshift: float, yshift: float) -> None: 84 | new_plot = self.copy() 85 | new_plot.shift_(xshift, yshift) 86 | return new_plot 87 | 88 | def scale(self, scale: float) -> None: 89 | new_plot = self.copy() 90 | new_plot.scale_(scale) 91 | return new_plot 92 | 93 | def rotate( 94 | self, 95 | angle: float, 96 | about_pt: Union[Tuple[float, float], None, Point] = None, 97 | radians: bool = False, 98 | ) -> None: 99 | new_plot = self.copy() 100 | new_plot.rotate_(angle, about_pt, radians) 101 | return new_plot 102 | 103 | def add_point(self, x, y): 104 | """Adds a new point to the points list. 105 | 106 | This method creates a new Point instance with the specified x and y coordinates, 107 | and appends it to the `points` attribute of the class. 108 | 109 | Args: 110 | x (int/float): The x-coordinate of the point. 111 | y (int/float): The y-coordinate of the point. 112 | 113 | Returns: 114 | None 115 | """ 116 | self.points.append(Point(x, y)) 117 | -------------------------------------------------------------------------------- /src/tikzpy/drawing_objects/xy_plane.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import List, Tuple 3 | from tikzpy.tikz_environments.scope import Scope 4 | from tikzpy.drawing_objects.line import Line 5 | from tikzpy.drawing_objects.node import Node 6 | 7 | 8 | class R2_Space: 9 | def __init__( 10 | self, 11 | x_interval: Tuple[float, float], 12 | y_interval: Tuple[float, float], 13 | origin: Tuple[float, float] = (0, 0), 14 | show_ticks: bool = False, 15 | show_labels: bool = True, 16 | ): 17 | self.x_interval = x_interval # An R^1 interval around 0, e.g., (-2, 3) 18 | self.y_interval = y_interval # An R^1 interval around 0 19 | self.origin = origin 20 | self.x_axis_options = "Gray!30, <->" 21 | self.y_axis_options = "Gray!30, <->" 22 | self.x_label = "$x$" 23 | self.y_label = "$y$" 24 | self.nticks = None # If left to None, no tick marks are shown 25 | self.show_labels = show_labels 26 | 27 | @property 28 | def x_axis(self): 29 | """The x-axis of R^2. The x_axis is specified via self.x_interval.""" 30 | left_pt = self.origin[0] + self.x_interval[0] # self.x_interval[0] <= 0 31 | right_pt = self.origin[0] + self.x_interval[1] # self.x_interval[1] >= 0 32 | x_axis = Line( 33 | (left_pt, self.origin[1]), 34 | (right_pt, self.origin[1]), 35 | options=self.x_axis_options, 36 | ) 37 | return x_axis 38 | 39 | @property 40 | def x_node(self): 41 | return Node(self.x_axis.end, options="below", text=self.x_label) 42 | 43 | @property 44 | def y_axis(self): 45 | """The y-axis of R^2. The y_axis is specified via self.y_interval.""" 46 | down_pt = self.origin[1] + self.y_interval[0] # self.y_interval[0] <= 0 47 | up_pt = self.origin[1] + self.y_interval[1] # self.x_interval[1] >= 0 48 | y_axis = Line( 49 | (self.origin[0], down_pt), 50 | (self.origin[0], up_pt), 51 | options=self.y_axis_options, 52 | ) 53 | return y_axis 54 | 55 | @property 56 | def y_node(self): 57 | return Node(self.y_axis.end, options="left", text=self.y_label) 58 | 59 | @property 60 | def code(self): 61 | xy_plane = Scope() 62 | xy_plane.draw(self.x_axis, self.y_axis) 63 | if self.show_labels: 64 | xy_plane.draw(self.x_node, self.y_node) 65 | return xy_plane.code 66 | 67 | def __repr__(self): 68 | return self.code 69 | -------------------------------------------------------------------------------- /src/tikzpy/styles/__init__.py: -------------------------------------------------------------------------------- 1 | from .arrows_along_path import arrows_along_path_style 2 | -------------------------------------------------------------------------------- /src/tikzpy/styles/arrows_along_path.py: -------------------------------------------------------------------------------- 1 | from tikzpy.tikz_environments.tikz_style import TikzStyle 2 | 3 | """ We implement a style that allows us to draw arrows on paths. This wonderful 4 | design was created by Paul Gaborit here: 5 | https://tex.stackexchange.com/questions/3161/tikz-how-to-draw-an-arrow-in-the-middle-of-the-line/69225#69225 6 | """ 7 | 8 | arrows_along_path = TikzStyle( 9 | "arrows_along_path", "postaction={on each segment={mid arrow=#1}}" 10 | ) 11 | 12 | on_each_segment = TikzStyle( 13 | "on each segment", 14 | """decorate, 15 | decoration={ 16 | show path construction, 17 | moveto code={}, 18 | lineto code={ 19 | \\path [#1] 20 | (\\tikzinputsegmentfirst) -- (\\tikzinputsegmentlast); 21 | }, 22 | curveto code={ 23 | \\path [#1] (\\tikzinputsegmentfirst) 24 | .. controls 25 | (\\tikzinputsegmentsupporta) and (\\tikzinputsegmentsupportb) 26 | .. 27 | (\\tikzinputsegmentlast); 28 | }, 29 | closepath code={ 30 | \\path [#1] 31 | (\\tikzinputsegmentfirst) -- (\\tikzinputsegmentlast); 32 | }, 33 | } 34 | """, 35 | ) 36 | 37 | mid_arrow = TikzStyle( 38 | "mid arrow", 39 | """ 40 | postaction={decorate,decoration={ 41 | markings, 42 | mark=at position .5 with {\\arrow[#1]{stealth}} 43 | }} 44 | """, 45 | ) 46 | 47 | arrows_along_path_style = [arrows_along_path, on_each_segment, mid_arrow] 48 | -------------------------------------------------------------------------------- /src/tikzpy/templates/tex_file.py: -------------------------------------------------------------------------------- 1 | TEX_FILE = r"""%!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 2 | % 3 | % Auto Generated by tikzpy. 4 | % 5 | % 6 | %%!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 7 | 8 | \documentclass[12pt,letterpaper]{article} 9 | \usepackage[left=0.3in, right=0.3in]{geometry} 10 | \usepackage{amsmath, amsfonts, amssymb} 11 | \usepackage[dvipsnames]{xcolor} % Colors, use dvipsnames for more color options 12 | \usepackage{tikz-cd} % Diagrams 13 | \usepackage{tikz} % General purpose graphics 14 | \usepackage{pgfplots} 15 | \usepackage{tikz-3dplot} 16 | 17 | \usetikzlibrary{ 18 | hobby, 19 | decorations.pathreplacing, 20 | decorations.markings 21 | } 22 | 23 | \begin{document} 24 | \pagestyle{empty} % no page numbers 25 | 26 | fillme 27 | 28 | \end{document} 29 | """ 30 | -------------------------------------------------------------------------------- /src/tikzpy/templates/tikz_code.tex: -------------------------------------------------------------------------------- 1 | %!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 2 | % 3 | % Auto Generated by tikzpy. 4 | % 5 | % 6 | %!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 7 | 8 | -------------------------------------------------------------------------------- /src/tikzpy/tikz_environments/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/src/tikzpy/tikz_environments/__init__.py -------------------------------------------------------------------------------- /src/tikzpy/tikz_environments/clip.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Tuple 3 | from tikzpy.drawing_objects.line import Line 4 | from tikzpy.drawing_objects.plotcoordinates import PlotCoordinates 5 | from tikzpy.drawing_objects.circle import Circle 6 | from tikzpy.drawing_objects.node import Node 7 | from tikzpy.drawing_objects.rectangle import Rectangle 8 | from tikzpy.drawing_objects.ellipse import Ellipse 9 | from tikzpy.drawing_objects.arc import Arc 10 | from tikzpy.drawing_objects.drawing_object import DrawingObject 11 | 12 | 13 | class Clip: 14 | r"""A class for a clipping code statement. 15 | 16 | This class is used to clip a single drawing object draw_obj. 17 | It is meant to be used in conjunction with the Scope class. 18 | It is analagous to the tikz code 19 | 20 | ``` 21 | \clip ... # some drawing object 22 | ``` 23 | """ 24 | 25 | def __init__(self, draw_obj: DrawingObject, draw: bool = False) -> None: 26 | if isinstance( 27 | draw_obj, (Line, PlotCoordinates, Circle, Node, Rectangle, Ellipse, Arc) 28 | ): 29 | self.draw_obj = draw_obj 30 | self.draw = draw 31 | else: 32 | raise TypeError( 33 | f"Clip argument {draw_obj} must be an instance of a drawing class." 34 | ) 35 | 36 | @property 37 | def code(self) -> str: 38 | if self.draw: 39 | return rf"\clip[preaction = {{draw, {self.draw_obj.options}}}] {self.draw_obj._command};" 40 | else: 41 | return rf"\clip {self.draw_obj._command};" 42 | 43 | def shift(self, xshift: float, yshift: float) -> None: 44 | self.draw_obj.shift(xshift, yshift) 45 | 46 | def scale(self, scale: float) -> None: 47 | self.draw_obj.scale(scale) 48 | 49 | def rotate( 50 | self, angle: float, about_pt: Tuple[float, float], radians: bool = False 51 | ) -> None: 52 | self.draw_obj.rotate(angle, about_pt, radians) 53 | -------------------------------------------------------------------------------- /src/tikzpy/tikz_environments/scope.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Tuple 3 | from tikzpy.drawing_objects.drawing_object import DrawingObject 4 | from tikzpy.tikz_environments.tikz_environment import TikzEnvironment 5 | from tikzpy.tikz_environments.clip import Clip 6 | from tikzpy.utils.helpers import brackets 7 | 8 | 9 | class Scope(TikzEnvironment): 10 | r"""A class to create a scope environment. 11 | 12 | The `Scope` class is meant to handle the `scope` environment in TikZ. 13 | Scoping is useful as it can be used to nest a set of commands in a TikZ picture, or it can be used in conjunction with the TikZ `clip` command to "clip out" drawings. 14 | 15 | This class is analagous to the TikZ command 16 | ``` 17 | \begin{scope} 18 | ... 19 | \end{scope} 20 | ``` 21 | """ 22 | 23 | def __init__(self, options: str = "") -> None: 24 | super().__init__(options) 25 | 26 | @property 27 | def code(self) -> str: 28 | """A string contaning the drawing_objects in the scope.""" 29 | code = f"\\begin{{scope}}{brackets(self.options)}\n" 30 | for draw_obj in self.drawing_objects: 31 | code += "\t" + draw_obj.code + "\n" 32 | code += "\\end{scope}\n" 33 | return code 34 | 35 | def __repr__(self) -> str: 36 | return self.code 37 | 38 | def append(self, *args: List[DrawingObject]) -> None: 39 | """Append a drawing object to the scope statement""" 40 | for draw_obj in args: 41 | self.drawing_objects.append(draw_obj) 42 | 43 | def clip(self, draw_obj: DrawingObject, draw: bool = False) -> None: 44 | """Clip a drawing object in the scope environment""" 45 | clip = Clip(draw_obj, draw=draw) 46 | self.draw(clip) 47 | 48 | def shift(self, xshift: float, yshift: float) -> None: 49 | for draw_obj in self.drawing_objects: 50 | draw_obj.shift(xshift, yshift) 51 | 52 | def scale(self, scale: float) -> None: 53 | for draw_obj in self.drawing_objects: 54 | draw_obj.scale(scale) 55 | 56 | def rotate( 57 | self, angle: float, about_pt: Tuple[float, float], radians: bool = False 58 | ) -> None: 59 | for draw_obj in self.drawing_objects: 60 | draw_obj.rotate(angle, about_pt, radians) 61 | -------------------------------------------------------------------------------- /src/tikzpy/tikz_environments/tikz_command.py: -------------------------------------------------------------------------------- 1 | class TikzCommand: 2 | """A class to handle manually typed Tikz code.""" 3 | 4 | def __init__(self, code: str) -> None: 5 | self.code = code 6 | 7 | def __repr__(self) -> str: 8 | return self.code 9 | -------------------------------------------------------------------------------- /src/tikzpy/tikz_environments/tikz_picture.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import webbrowser 3 | import tempfile 4 | import re 5 | 6 | from pathlib import Path 7 | import shutil 8 | from typing import List, Optional 9 | from tikzpy.tikz_environments.scope import Scope 10 | from tikzpy.tikz_environments.tikz_environment import TikzEnvironment 11 | from tikzpy.tikz_environments.tikz_style import TikzStyle 12 | from tikzpy.utils.helpers import ( 13 | brackets, 14 | true_posix_path, 15 | extract_error_content, 16 | ) 17 | from tikzpy.utils.types import CompileError 18 | from tikzpy.templates.tex_file import TEX_FILE 19 | 20 | 21 | class TikzPicture(TikzEnvironment): 22 | """ 23 | A class for managing a Tikzpicture environment and associated tex files with tikz code. 24 | 25 | The TikzPicture class acts a canvas in which users can append drawings to. 26 | In the background, the TikzPicture manages the creation of 27 | the tikz code. 28 | 29 | Parameters: 30 | center: True/False if one wants to center their Tikz code 31 | options: A list of options for the Tikz picture 32 | """ 33 | 34 | def __init__( 35 | self, center: bool = False, options: str = "", tikz_code_dir=None 36 | ) -> None: 37 | super().__init__(options) 38 | self._preamble = {} 39 | self._postamble = {} 40 | self.BASE_DIR = None 41 | 42 | if tikz_code_dir is not None: 43 | self.BASE_DIR = Path(tikz_code_dir) 44 | 45 | if center: 46 | self._preamble["center"] = "\\begin{center}\n" 47 | self._postamble["center"] = "\\end{center}\n" 48 | else: 49 | self._preamble["center"] = "" 50 | self._postamble["center"] = "" 51 | 52 | def code(self) -> str: 53 | """Returns a string contaning the generated Tikz code.""" 54 | code = "" 55 | # Add the beginning statement 56 | for stmt in self._preamble.values(): 57 | code += stmt 58 | code += f"\\begin{{tikzpicture}}{brackets(self.options)}\n" 59 | 60 | # Add the main tikz code 61 | for draw_obj in self.drawing_objects: 62 | code += " " + draw_obj.code + "\n" 63 | 64 | # Add the ending statement 65 | code += "\\end{tikzpicture}\n" 66 | for stmt in list(reversed(list(self._postamble.values()))): 67 | code += stmt 68 | return code 69 | 70 | def __repr__(self) -> str: 71 | readable_code = f"\\begin{{tikzpicture}}{brackets(self.options)}\n" 72 | 73 | for draw_obj in self.drawing_objects: 74 | readable_code += " " + draw_obj.code + "\n" 75 | 76 | readable_code += "\\end{tikzpicture}\n" 77 | return readable_code 78 | 79 | def tikzset(self, style_name: str, style_rules: TikzStyle) -> TikzStyle: 80 | """Create and add a TikzStyle object with name "style_name" and tikzset syntax "style_rules" """ 81 | style = TikzStyle(style_name, style_rules) 82 | self.add_styles(style) 83 | return style 84 | 85 | def add_styles(self, *styles: List[TikzStyle]) -> None: 86 | """Add a TikzStyle object to the environment.""" 87 | for style in styles: 88 | self._preamble[f"tikz_style:{style.style_name}"] = style.code 89 | 90 | def set_tdplotsetmaincoords(self, theta: float, phi: float) -> None: 91 | """Specify the viewing angle for 3D. 92 | 93 | theta: The angle (in degrees) through which the coordinate frame is rotated about the x axis. 94 | phi: The angle (in degrees) through which the coordinate frame is rotated about the z axis. 95 | """ 96 | self.tdplotsetmaincoords = (theta, phi) 97 | self._preamble[ 98 | "tdplotsetmaincoords" 99 | ] = f"\\tdplotsetmaincoords{{{theta}}}{{{phi}}}\n" 100 | 101 | def write_tex_file(self, tex_filepath): 102 | tex_code = TEX_FILE 103 | tex_file_contents = re.sub("fillme", lambda x: self.code(), tex_code) 104 | # Update the TeX file 105 | if self.BASE_DIR is not None: 106 | tex_filepath = self.BASE_DIR / tex_filepath 107 | 108 | with open(tex_filepath, "w") as f: 109 | f.write(tex_file_contents) 110 | 111 | def write(self, tikz_code_filepath=None): 112 | if tikz_code_filepath is None: 113 | tikz_code_filepath = "tikz_code.tex" 114 | 115 | base_dir: Path = Path.cwd() 116 | if self.BASE_DIR is not None: 117 | base_dir = self.BASE_DIR 118 | 119 | tikz_code_filepath = base_dir / tikz_code_filepath 120 | with open(tikz_code_filepath, "w") as f: 121 | f.write(self.code()) 122 | 123 | def compile( 124 | self, pdf_destination: Optional[str] = None, quiet: bool = True 125 | ) -> Path: 126 | """Compiles the Tikz code and returns a Path to the final PDF. 127 | If no file path is provided, a default value of "tex_file.pdf" will be used. 128 | 129 | Parameters: 130 | pdf_destination (str): The file path of the compiled pdf. 131 | quiet (bool): Parameter to silence latexmk. 132 | """ 133 | with tempfile.TemporaryDirectory() as tmp_dir: 134 | tex_filepath = Path(tmp_dir) / "tex_file.tex" 135 | self.write_tex_file(tex_filepath) 136 | 137 | tex_file_posix_path = true_posix_path(tex_filepath) 138 | tex_file_parents = true_posix_path(tex_filepath.parent) 139 | options = "" 140 | if quiet: 141 | options += " -quiet " 142 | cmd = ( 143 | f"latexmk -pdf {options} -interaction=nonstopmode -output-directory={tex_file_parents} {tex_file_posix_path}", 144 | ) 145 | completed_process = subprocess.run(cmd, shell=True, capture_output=True) 146 | if completed_process.returncode != 0: 147 | logfile = Path(tmp_dir) / "tex_file.log" 148 | if not logfile.exists(): 149 | raise CompileError( 150 | f"Unexpected compilation error when running {cmd=}. No log file found. Manually compile the tikz code to debug." 151 | f"{completed_process.stderr=}" 152 | ) 153 | # If there's a log file, try to extract the error from it 154 | # and return it to the user. 155 | error_content = extract_error_content( 156 | logfile.read_text().splitlines(keepends=True) 157 | ) 158 | if error_content is None: 159 | raise CompileError( 160 | f"Unexpected compilation error when running {cmd=}. Failed to parse log file. Manually compile the tikz code and check the .log file." 161 | f"{completed_process.stderr=}" 162 | ) 163 | raise CompileError(error_content) 164 | 165 | # We move the compiled PDF into the same folder containing the tikz code. 166 | pdf_file = tex_filepath.with_suffix(".pdf").resolve() 167 | if pdf_destination is None: 168 | if self.BASE_DIR is None: 169 | moved_pdf_file = Path.cwd() / pdf_file.name 170 | else: 171 | moved_pdf_file = self.BASE_DIR / pdf_file.name 172 | else: 173 | moved_pdf_file = Path(pdf_destination) 174 | shutil.move(pdf_file, moved_pdf_file) 175 | return moved_pdf_file.resolve() 176 | 177 | def show(self, quiet: bool = False) -> None: 178 | """Compiles the Tikz code and displays the pdf to the user. Set quiet=True to shut up latexmk. 179 | This should either open the PDF viewer on the user's computer with the graphic, 180 | or open the PDF in the user's browser. 181 | """ 182 | pdf_file = self.compile(quiet=quiet) 183 | webbrowser.open_new(str(pdf_file.as_uri())) 184 | 185 | def scope(self, options: str = "") -> Scope: 186 | scope = Scope(options=options) 187 | self.draw(scope) 188 | return scope 189 | -------------------------------------------------------------------------------- /src/tikzpy/tikz_environments/tikz_style.py: -------------------------------------------------------------------------------- 1 | class TikzStyle: 2 | def __init__(self, style_name: str, style_settings: str) -> None: 3 | self.style_name = style_name 4 | self.style_settings = style_settings 5 | 6 | @property 7 | def code(self) -> str: 8 | return ( 9 | f"\\tikzset{{ {self.style_name}/.style={{ {self.style_settings} }} \n }}\n" 10 | ) 11 | 12 | def __repr__(self) -> str: 13 | return self.code 14 | -------------------------------------------------------------------------------- /src/tikzpy/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/src/tikzpy/utils/__init__.py -------------------------------------------------------------------------------- /src/tikzpy/utils/helpers.py: -------------------------------------------------------------------------------- 1 | import re 2 | import math 3 | from typing import Tuple 4 | from pathlib import Path, WindowsPath 5 | 6 | 7 | def brackets(string: str) -> str: 8 | """Wraps a string with a pair of matching brackets.""" 9 | if len(string) != 0: 10 | return "[" + string + "]" 11 | else: 12 | return "" 13 | 14 | 15 | def true_posix_path(path_obj: Path) -> str: 16 | r"""Given a path_obj, we return a string which represents the "true posix" file path 17 | of the path_obj. 18 | 19 | Long: We need to tell TeX where our tikz_code is. Because TeX's "\input" command expects posix-like paths, 20 | regardless of the machine it is running, we need things like "/Users/user/Desktop..." and not "C:\Users\user\Desktop..." 21 | We'd naturally just do str(path_obj.resolve()), which works on linux. But this will cause an error on windows machines 22 | since such a command returns something like "C:\Users\user\Desktop..." 23 | Since pathlib does not happen to have a method for this, we write one. 24 | """ 25 | full_path = path_obj.resolve() 26 | if isinstance(path_obj, WindowsPath): 27 | drive = full_path.drive # C:, E:, etc. 28 | return "/" + str( 29 | full_path.relative_to(f"{drive}/").as_posix() 30 | ) # Need / so we may obtain /Users/... not Users/... 31 | else: 32 | return str(full_path) 33 | 34 | 35 | def replace_code( 36 | begin_delim: str, end_delim: str, content: str, new_code: str 37 | ) -> Tuple[str, int]: 38 | """Replaces text delimited by `begin_delim` and `end_delim` appearing in `content`, with `new_code`. 39 | Returns new string and number of matches made.""" 40 | return re.subn( 41 | rf"{re.escape(begin_delim)}([\s\S]*?){re.escape(end_delim)}", 42 | new_code.replace( 43 | "\\", "\\\\" 44 | ), # Need to escape backslashes twice for re package 45 | content, 46 | ) 47 | 48 | 49 | def find_image_start_boundary(img_data): 50 | ind = 0 51 | while ind < len(img_data): 52 | row = img_data[ind] 53 | found = False 54 | for col in row: 55 | if col < 255: 56 | found = True 57 | break 58 | if found: 59 | break 60 | ind += 1 61 | return ind 62 | 63 | 64 | def find_image_end_boundary(img_data): 65 | ind = len(img_data) - 1 66 | while ind > 0: 67 | row = img_data[ind] 68 | found = False 69 | for col in row: 70 | if col < 255: 71 | found = True 72 | break 73 | if found: 74 | break 75 | ind -= 1 76 | return ind 77 | 78 | 79 | def extract_error_content(log_lines: list[str]) -> str: 80 | """ 81 | Scans the provided text for LaTeX error messages. 82 | 83 | This function searches the given text for lines that begin with "! " which 84 | typically indicates the start of an error message in LaTeX logs. It then 85 | continues to collect all subsequent lines until it encounters a line that 86 | begins with "? ", which usually indicates the end of the error message. 87 | All collected lines are appended to a list and returned. 88 | 89 | Parameters: 90 | text (str): The input text to scan for error messages. 91 | 92 | Returns: 93 | list: A list of strings containing the lines of the error message. 94 | If no error message is found, the list will be empty. 95 | """ 96 | error_lines = [] 97 | recording = False 98 | 99 | for line in log_lines: 100 | if line.startswith("! "): 101 | recording = True 102 | if recording: 103 | error_lines.append(line) 104 | if line.startswith("?"): 105 | break 106 | 107 | if len(error_lines) == 0: 108 | return None 109 | 110 | return "".join(error_lines) 111 | -------------------------------------------------------------------------------- /src/tikzpy/utils/types.py: -------------------------------------------------------------------------------- 1 | class CompileError(Exception): 2 | def __init__(self, message): 3 | super().__init__(message) 4 | self.message = message 5 | 6 | def __str__(self): 7 | return self.message 8 | -------------------------------------------------------------------------------- /tests/drawing_objects/test_arc.py: -------------------------------------------------------------------------------- 1 | from tikzpy import TikzPicture, Arc, Point 2 | import pytest 3 | 4 | 5 | @pytest.fixture 6 | def arc_from_tikzpicture(): 7 | tikz = TikzPicture() 8 | arc = tikz.arc((0, 0), 20, 90, 4) 9 | return arc 10 | 11 | 12 | @pytest.fixture 13 | def mock_arc(): 14 | arc = Arc((0, 0), 20, 90, 4) 15 | return arc 16 | 17 | 18 | @pytest.mark.parametrize( 19 | "object", 20 | [ 21 | "arc_from_tikzpicture", 22 | "mock_arc", 23 | ], 24 | ) 25 | def test_arc_construction(object, request): 26 | arc = request.getfixturevalue(object) 27 | assert arc.position.x == 0 28 | assert arc.position.y == 0 29 | assert arc.start_angle == 20 30 | assert arc.end_angle == 90 31 | assert arc.radius == 4 32 | assert ( 33 | arc.code 34 | == r"\draw (0, 0) arc [start angle = 20, end angle = 90, radius = 4cm];" 35 | ) 36 | -------------------------------------------------------------------------------- /tests/drawing_objects/test_circle.py: -------------------------------------------------------------------------------- 1 | from tikzpy import TikzPicture, Circle, Point 2 | import pytest 3 | 4 | 5 | @pytest.fixture 6 | def circle_from_tikzpicture(): 7 | tikz = TikzPicture() 8 | circle = tikz.circle((1, 1), 1, options="fill = purple") 9 | return circle 10 | 11 | 12 | @pytest.fixture 13 | def mock_circle(): 14 | circle = Circle((1, 1), 1, options="fill = purple") 15 | return circle 16 | 17 | 18 | @pytest.mark.parametrize( 19 | "object", 20 | [ 21 | "circle_from_tikzpicture", 22 | "mock_circle", 23 | ], 24 | ) 25 | def test_circle_construction(object, request): 26 | circle = request.getfixturevalue(object) 27 | assert circle.center.x == 1 28 | assert circle.center.y == 1 29 | assert circle.radius == 1 30 | assert circle.options == "fill = purple" 31 | assert circle.code == r"\draw[fill = purple] (1, 1) circle (1cm);" 32 | 33 | 34 | def test_center_assignment(mock_circle): 35 | assert mock_circle.center == Point(1, 1) 36 | assert mock_circle.north == Point(1, 2) 37 | assert mock_circle.east == Point(2, 1) 38 | assert mock_circle.south == Point(1, 0) 39 | assert mock_circle.west == Point(0, 1) 40 | 41 | mock_circle.center = (3, 4) 42 | assert mock_circle.center == Point(3, 4) 43 | assert mock_circle.north == Point(3, 5) 44 | assert mock_circle.east == Point(4, 4) 45 | assert mock_circle.south == Point(3, 3) 46 | assert mock_circle.west == Point(2, 4) 47 | 48 | 49 | def test_circle_shift(mock_circle): 50 | new_circle = mock_circle.shift(1, 1) 51 | assert new_circle.center == Point(2, 2) 52 | assert new_circle.north == Point(2, 3) 53 | assert new_circle.east == Point(3, 2) 54 | assert new_circle.south == Point(2, 1) 55 | assert new_circle.west == Point(1, 2) 56 | 57 | 58 | def test_circle_scale(mock_circle): 59 | new_circle = mock_circle.scale(2) 60 | assert new_circle.center == Point(2, 2) 61 | assert new_circle.north == Point(2, 4) 62 | assert new_circle.east == Point(4, 2) 63 | assert new_circle.south == Point(2, 0) 64 | assert new_circle.west == Point(0, 2) 65 | -------------------------------------------------------------------------------- /tests/drawing_objects/test_ellipse.py: -------------------------------------------------------------------------------- 1 | from tikzpy import TikzPicture, Ellipse, Point 2 | import pytest 3 | 4 | 5 | @pytest.fixture 6 | def ellipse_from_tikzpicture(): 7 | tikz = TikzPicture() 8 | ellipse = tikz.ellipse((0, 0), 3, 4, options="fill = purple") 9 | return ellipse 10 | 11 | 12 | @pytest.fixture 13 | def mock_ellipse(): 14 | ellipse = Ellipse((0, 0), 3, 4, options="fill = purple") 15 | return ellipse 16 | 17 | 18 | @pytest.mark.parametrize( 19 | "object", 20 | [ 21 | "ellipse_from_tikzpicture", 22 | "mock_ellipse", 23 | ], 24 | ) 25 | def test_ellipse_construction(object, request): 26 | ellipse = request.getfixturevalue(object) 27 | assert ellipse.center.x == 0 28 | assert ellipse.center.y == 0 29 | assert ellipse.x_axis == 3 30 | assert ellipse.y_axis == 4 31 | assert ellipse.options == "fill = purple" 32 | assert ellipse.code == r"\draw[fill = purple] (0, 0) ellipse (3cm and 4cm);" 33 | 34 | 35 | def test_center_assignment(mock_ellipse): 36 | assert mock_ellipse.center == Point(0, 0) 37 | assert mock_ellipse.north == Point(0, 4) 38 | assert mock_ellipse.east == Point(3, 0) 39 | assert mock_ellipse.south == Point(0, -4) 40 | assert mock_ellipse.west == Point(-3, 0) 41 | 42 | mock_ellipse.center = (1, 1) 43 | assert mock_ellipse.center == Point(1, 1) 44 | assert mock_ellipse.north == Point(1, 5) 45 | assert mock_ellipse.east == Point(4, 1) 46 | assert mock_ellipse.south == Point(1, -3) 47 | assert mock_ellipse.west == Point(-2, 1) 48 | 49 | 50 | def test_ellipse_shift(mock_ellipse): 51 | new_ellipse = mock_ellipse.shift(1, 1) 52 | assert new_ellipse.center == Point(1, 1) 53 | assert new_ellipse.north == Point(1, 5) 54 | assert new_ellipse.east == Point(4, 1) 55 | assert new_ellipse.south == Point(1, -3) 56 | assert new_ellipse.west == Point(-2, 1) 57 | 58 | 59 | def test_ellipse_scale(mock_ellipse): 60 | new_ellipse = mock_ellipse.scale(2) 61 | assert new_ellipse.center == Point(0, 0) 62 | assert new_ellipse.x_axis == 6 63 | assert new_ellipse.y_axis == 8 64 | -------------------------------------------------------------------------------- /tests/drawing_objects/test_line.py: -------------------------------------------------------------------------------- 1 | from tikzpy import TikzPicture, Line, Point 2 | import pytest 3 | 4 | 5 | @pytest.fixture 6 | def tikz_line_from_tikzpicture(): 7 | tikz = TikzPicture() 8 | line = tikz.line( 9 | (0, 0), 10 | (1, 1), 11 | options="thick, blue", 12 | control_pts=[(0.25, 0.25), (0.75, 0.75)], 13 | ) 14 | return line 15 | 16 | 17 | @pytest.fixture 18 | def tikz_line(): 19 | line = Line( 20 | (0, 0), 21 | (1, 1), 22 | options="thick, blue", 23 | control_pts=[(0.25, 0.25), (0.75, 0.75)], 24 | ) 25 | return line 26 | 27 | 28 | @pytest.fixture 29 | def line_simple(): 30 | line = Line((0, 0), (1, 1)) 31 | return line 32 | 33 | 34 | @pytest.mark.parametrize( 35 | "object", 36 | [ 37 | "tikz_line_from_tikzpicture", 38 | "tikz_line", 39 | ], 40 | ) 41 | def test_line_construction(object, request): 42 | line = request.getfixturevalue(object) 43 | assert line.start.x == 0 44 | assert line.start.y == 0 45 | assert line.end.x == 1 46 | assert line.end.y == 1 47 | assert line.options == "thick, blue" 48 | assert line.control_pts[0].x == 0.25 49 | assert line.control_pts[0].y == 0.25 50 | assert line.control_pts[1].x == 0.75 51 | assert line.control_pts[1].y == 0.75 52 | assert ( 53 | line.code 54 | == r"\draw[thick, blue] (0, 0) .. controls (0.25, 0.25) and (0.75, 0.75) .. (1, 1);" 55 | ) 56 | 57 | 58 | def test_line_point_assignment(): 59 | line = Line((0, 0), (1, 1)) 60 | line.start = (1, 2) 61 | line.end = (3, 4) 62 | assert line.start.x == 1 63 | assert line.start.y == 2 64 | assert line.end.x == 3 65 | assert line.end.y == 4 66 | assert line.code == r"\draw (1, 2) to (3, 4);" 67 | 68 | 69 | def test_line_scale(line_simple): 70 | new_line = line_simple.scale(4) 71 | assert new_line.start == Point(0, 0) 72 | assert new_line.end == Point(4, 4) 73 | 74 | 75 | def test_line_scale(line_simple): 76 | new_line = line_simple.shift(1, 1) 77 | assert new_line.start == Point(1, 1) 78 | assert new_line.end == Point(2, 2) 79 | -------------------------------------------------------------------------------- /tests/drawing_objects/test_node.py: -------------------------------------------------------------------------------- 1 | from tikzpy import TikzPicture, Node, Point 2 | import pytest 3 | 4 | 5 | @pytest.fixture 6 | def node_from_tikzpicture(): 7 | tikz = TikzPicture() 8 | node = tikz.node( 9 | position=(3, 3), 10 | text=r"I love $ \sum_{x \in \mathbb{R}} f(x^2)$ !", 11 | options="above", 12 | ) 13 | return node 14 | 15 | 16 | @pytest.fixture 17 | def mock_node(): 18 | node = Node( 19 | position=(3, 3), 20 | text=r"I love $ \sum_{x \in \mathbb{R}} f(x^2)$ !", 21 | options="above", 22 | ) 23 | return node 24 | 25 | 26 | @pytest.mark.parametrize( 27 | "object", 28 | [ 29 | "node_from_tikzpicture", 30 | "mock_node", 31 | ], 32 | ) 33 | def test_node_construction(object, request): 34 | node = request.getfixturevalue(object) 35 | assert node.position.x == 3 36 | assert node.position.y == 3 37 | assert node.text == r"I love $ \sum_{x \in \mathbb{R}} f(x^2)$ !" 38 | assert node.options == "above" 39 | assert ( 40 | node.code 41 | == r"\node[above] at (3, 3) { I love $ \sum_{x \in \mathbb{R}} f(x^2)$ ! };" 42 | ) 43 | 44 | 45 | def test_node_position_assignment(mock_node): 46 | assert mock_node.position == Point(3, 3) 47 | mock_node.position = (4, 4) 48 | assert mock_node.position == Point(4, 4) 49 | 50 | 51 | def test_node_shift(mock_node): 52 | new_node = mock_node.shift(1, 1) 53 | assert new_node.position == Point(4, 4) 54 | 55 | 56 | def test_node_scale(mock_node): 57 | new_node = mock_node.scale(2) 58 | assert new_node.position == Point(6, 6) 59 | -------------------------------------------------------------------------------- /tests/drawing_objects/test_plot_coordinates.py: -------------------------------------------------------------------------------- 1 | from tikzpy import TikzPicture, PlotCoordinates, Point 2 | import pytest 3 | 4 | 5 | @pytest.fixture 6 | def plot_coordinates_from_tikzpicture(): 7 | tikz = TikzPicture() 8 | plot_coordinates = tikz.plot_coordinates( 9 | options="green", 10 | plot_options="smooth ", 11 | points=[(1, 1), (2, 2), (3, 3), (2, -4)], 12 | ) 13 | return plot_coordinates 14 | 15 | 16 | @pytest.fixture 17 | def mock_plot_coordinates(): 18 | plot_coordinates = PlotCoordinates( 19 | options="green", 20 | plot_options="smooth ", 21 | points=[(1, 1), (2, 2), (3, 3), (2, -4)], 22 | ) 23 | return plot_coordinates 24 | 25 | 26 | @pytest.fixture 27 | def plot_relative_coordinates(): 28 | tikz = TikzPicture() 29 | plot_coordinates = tikz.plot_relative_coordinates( 30 | options="green", 31 | plot_options="smooth ", 32 | points=[(0, 0), (0, 1), (1, 0), (1, 1)], 33 | ) 34 | return plot_coordinates 35 | 36 | 37 | @pytest.mark.parametrize( 38 | "object", 39 | [ 40 | "plot_coordinates_from_tikzpicture", 41 | "mock_plot_coordinates", 42 | ], 43 | ) 44 | def test_plot_coordinates_construction(object, request): 45 | plot_coordinates = request.getfixturevalue(object) 46 | assert plot_coordinates.options == "green" 47 | assert plot_coordinates.plot_options == "smooth " 48 | assert plot_coordinates.points[0].x == 1 49 | assert plot_coordinates.points[0].y == 1 50 | assert plot_coordinates.points[1].x == 2 51 | assert plot_coordinates.points[1].y == 2 52 | assert plot_coordinates.points[2].x == 3 53 | assert plot_coordinates.points[2].y == 3 54 | assert plot_coordinates.points[3].x == 2 55 | assert plot_coordinates.points[3].y == -4 56 | assert ( 57 | plot_coordinates.code 58 | == r"\draw[green] plot[smooth ] coordinates {(1, 1) (2, 2) (3, 3) (2, -4) };" 59 | ) 60 | 61 | 62 | def test_plot_relative_coordinates(plot_relative_coordinates): 63 | assert plot_relative_coordinates.points == [ 64 | Point(0, 0), 65 | Point(0, 1), 66 | Point(1, 1), 67 | Point(2, 2), 68 | ] 69 | -------------------------------------------------------------------------------- /tests/drawing_objects/test_point.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from tikzpy import Point 3 | 4 | 5 | @pytest.mark.parametrize( 6 | "point, expected", 7 | [ 8 | (Point(1, 2), (1, 2)), 9 | (Point(1, 2, 3), (1, 2, 3)), 10 | (Point((1, 2)), (1, 2)), 11 | (Point((1, 2, 3)), (1, 2, 3)), 12 | (Point(Point(1, 2)), (1, 2)), 13 | (Point(Point(1, 2, 3)), (1, 2, 3)), 14 | ], 15 | ) 16 | def test_point_instantiation(point, expected): 17 | assert point.x == expected[0] 18 | assert point.y == expected[1] 19 | if len(expected) == 3: 20 | assert point.z == expected[2] 21 | else: 22 | print(point, point.z) 23 | assert point.z is None 24 | 25 | 26 | def test_point_scale(): 27 | point = Point(1, 4) 28 | new_point = point.scale(2) 29 | assert new_point.x == 2 30 | assert new_point.y == 8 31 | 32 | 33 | def test_point_shift(): 34 | point = Point(1, 4) 35 | new_point = point.shift(2, 3) 36 | assert new_point.x == 3 37 | assert new_point.y == 7 38 | 39 | 40 | def test_point_rotate(): 41 | point = Point(1, 0) 42 | about_point = Point(0, 0) 43 | new_point = point.rotate(90, about_point) 44 | assert pytest.approx(new_point.x) == 0 45 | assert pytest.approx(new_point.y) == 1 46 | -------------------------------------------------------------------------------- /tests/drawing_objects/test_rectangle.py: -------------------------------------------------------------------------------- 1 | from tikzpy import TikzPicture, Rectangle, Point 2 | import pytest 3 | 4 | 5 | @pytest.fixture 6 | def tikz_rectangle(): 7 | tikz = TikzPicture() 8 | rectangle = tikz.rectangle((2, 2), 1, 2, options="Blue") 9 | return rectangle 10 | 11 | 12 | @pytest.fixture 13 | def mock_rectangle(): 14 | rectangle = Rectangle((2, 2), 1, 2, options="Blue") 15 | return rectangle 16 | 17 | 18 | @pytest.mark.parametrize( 19 | "object", 20 | [ 21 | "tikz_rectangle", 22 | "mock_rectangle", 23 | ], 24 | ) 25 | def test_rectangle_constructor(object, request): 26 | rectangle = request.getfixturevalue(object) 27 | assert rectangle.left_corner.x == 2 28 | assert rectangle.left_corner.y == 2 29 | assert rectangle.options == "Blue" 30 | assert rectangle.height == 2 31 | assert rectangle.width == 1 32 | 33 | assert rectangle.center == Point(2.5, 3.0) 34 | assert rectangle.north == Point(2.5, 4) 35 | assert rectangle.east == Point(3, 3) 36 | assert rectangle.south == Point(2.5, 2) 37 | assert rectangle.west == Point(2, 3) 38 | 39 | assert rectangle.code == r"\draw[Blue] (2, 2) rectangle (3, 4);" 40 | 41 | 42 | def test_left_corner_assignment(mock_rectangle): 43 | mock_rectangle.left_corner = (0, 0) 44 | assert mock_rectangle.left_corner == Point(0, 0) 45 | assert mock_rectangle.height == 2 46 | assert mock_rectangle.width == 1 47 | assert mock_rectangle.code == r"\draw[Blue] (0, 0) rectangle (1, 2);" 48 | 49 | 50 | def test_rectangle_shift(mock_rectangle): 51 | new_rectangle = mock_rectangle.shift(1, 1) 52 | assert new_rectangle.left_corner == Point(3, 3) 53 | assert new_rectangle.right_corner == Point(4, 5) 54 | assert new_rectangle.code == r"\draw[Blue] (3, 3) rectangle (4, 5);" 55 | 56 | 57 | def test_rectangle_scale(mock_rectangle): 58 | new_rectangle = mock_rectangle.scale(2) 59 | assert new_rectangle.left_corner == Point(4, 4) 60 | assert new_rectangle.right_corner == Point(6, 8) 61 | assert new_rectangle.code == r"\draw[Blue] (4, 4) rectangle (6, 8);" 62 | -------------------------------------------------------------------------------- /tests/integration/test_basic.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/tests/integration/test_basic.py -------------------------------------------------------------------------------- /tests/integration/test_cauchy_residue_thm.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from pathlib import Path 3 | from tikzpy import TikzPicture 4 | from tikzpy.styles import arrows_along_path_style 5 | 6 | code = r"""\begin{tikzpicture}[thick] 7 | \draw[arrows_along_path=red] plot[smooth, tension=.5, closed hobby] coordinates {(-3, -0.5) (-1, -1.5) (2, -1.5) (4, -1.5) (5, 0) (3, 2.4) (-1, 2.6) (-3, 2) }; 8 | \draw[arrows_along_path=blue] (3, -0.3) circle (0.7cm); 9 | \draw[arrows_along_path=blue] (1.3, 1.3) circle (0.7cm); 10 | \draw[arrows_along_path=blue] (-0.4, -0.2) circle (0.7cm); 11 | \draw[arrows_along_path=blue] (-2, 1.1) circle (0.7cm); 12 | \draw[bend left, <-] (3.7, -0.5) to (4.6, -1); 13 | \draw[bend right, <-] (1.4, 0.6) to (2.3, -0.3); 14 | \draw[bend left, <-] (-0.25, 0.5) to (0.6, 1.4); 15 | \draw[bend left, <-] (-1.29, 1.1) to (-0.6, 0.46); 16 | \draw[bend left, ->] (-2.5, 1.6) to (-2.8, 2.15); 17 | \fill (3, -0.3) circle (0.05cm); 18 | \node[right] at (3, -0.3) { $a_1$ }; 19 | \fill (1.3, 1.3) circle (0.05cm); 20 | \node[right] at (1.3, 1.3) { $a_2$ }; 21 | \fill (-0.4, -0.2) circle (0.05cm); 22 | \node[right] at (-0.4, -0.2) { $a_3$ }; 23 | \fill (-2, 1.1) circle (0.05cm); 24 | \node[right] at (-2, 1.1) { $a_4$ }; 25 | \end{tikzpicture} 26 | """ 27 | 28 | 29 | def test_cauchy_residue_example(): 30 | tikz = TikzPicture() 31 | 32 | points = [ 33 | (-3, -0.5), 34 | (-1, -1.5), 35 | (2, -1.5), 36 | (4, -1.5), 37 | (5, 0), 38 | (3, 2.4), 39 | (-1, 2.6), 40 | (-3, 2), 41 | ] 42 | 43 | tikz.add_styles(*arrows_along_path_style) 44 | tikz.options = "thick" 45 | 46 | # Draws the main boundary 47 | plot = tikz.plot_coordinates( 48 | options="arrows_along_path=red", 49 | plot_options="smooth, tension=.5, closed hobby", 50 | points=points, 51 | action="draw", 52 | ) 53 | 54 | # Draws the inner circles 55 | singularity_1 = tikz.circle((3, -0.3), 0.7, "arrows_along_path=blue") 56 | singularity_2 = tikz.circle((1.3, 1.3), 0.7, "arrows_along_path=blue") 57 | singularity_3 = tikz.circle((-0.4, -0.2), 0.7, "arrows_along_path=blue") 58 | singularity_4 = tikz.circle((-2, 1.1), 0.7, "arrows_along_path=blue") 59 | 60 | # Draws the paths that connect the circles 61 | tikz.line((3.7, -0.5), (4.6, -1), options="bend left, <-") 62 | tikz.line((1.4, 0.6), (2.3, -0.3), options="bend right, <-") 63 | tikz.line((-0.25, 0.5), (0.6, 1.4), options="bend left, <-") 64 | tikz.line((-1.29, 1.1), (-0.6, 0.46), options="bend left, <-") 65 | tikz.line((-2.5, 1.6), (-2.8, 2.15), options="bend left, ->") 66 | 67 | # Draws and labels the points a_1, a_2, a_3, and a_4. 68 | for ind, singularity in enumerate( 69 | [singularity_1, singularity_2, singularity_3, singularity_4] 70 | ): 71 | tikz.circle(singularity.center, 0.05, action="fill") 72 | tikz.node(singularity.center, options="right", text=f"$a_{ind+1}$") 73 | 74 | with tempfile.NamedTemporaryFile() as fp: 75 | temp_path = Path(fp.name) 76 | tikz.compile(temp_path, quiet=True) 77 | 78 | assert str(tikz) == code 79 | -------------------------------------------------------------------------------- /tests/integration/test_circles.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltrujello/Tikz-Python/67c0e42d0fe1a0697ada37d1214eb733b9358f3e/tests/integration/test_circles.py -------------------------------------------------------------------------------- /tests/integration/test_controls.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from pathlib import Path 3 | from tikzpy import TikzPicture 4 | from tikzpy.colors import rainbow_colors 5 | 6 | code = r"""\begin{tikzpicture} 7 | \draw[color={rgb,255:red, 255; green, 0; blue, 0 }] (0, 0) .. controls (-2, -1) and (2, -2) .. (0, 5); 8 | \draw[color={rgb,255:red, 255; green, 125; blue, 0 }] (1, 0) .. controls (-1, -1) and (3, -2) .. (0, 5); 9 | \draw[color={rgb,255:red, 255; green, 240; blue, 105 }] (2, 0) .. controls (0, -1) and (4, -2) .. (0, 5); 10 | \draw[color={rgb,255:red, 125; green, 255; blue, 0 }] (3, 0) .. controls (1, -1) and (5, -2) .. (0, 5); 11 | \draw[color={rgb,255:red, 0; green, 255; blue, 0 }] (4, 0) .. controls (2, -1) and (6, -2) .. (0, 5); 12 | \draw[color={rgb,255:red, 0; green, 255; blue, 125 }] (5, 0) .. controls (3, -1) and (7, -2) .. (0, 5); 13 | \draw[color={rgb,255:red, 0; green, 255; blue, 255 }] (6, 0) .. controls (4, -1) and (8, -2) .. (0, 5); 14 | \draw[color={rgb,255:red, 0; green, 125; blue, 255 }] (7, 0) .. controls (5, -1) and (9, -2) .. (0, 5); 15 | \draw[color={rgb,255:red, 0; green, 0; blue, 255 }] (8, 0) .. controls (6, -1) and (10, -2) .. (0, 5); 16 | \draw[color={rgb,255:red, 125; green, 0; blue, 255 }] (9, 0) .. controls (7, -1) and (11, -2) .. (0, 5); 17 | \draw[color={rgb,255:red, 255; green, 0; blue, 12 }] (10, 0) .. controls (8, -1) and (12, -2) .. (0, 5); 18 | \draw[color={rgb,255:red, 255; green, 0; blue, 255 }] (11, 0) .. controls (9, -1) and (13, -2) .. (0, 5); 19 | \draw[color={rgb,255:red, 255; green, 0; blue, 0 }] (12, 0) .. controls (10, -1) and (14, -2) .. (0, 5); 20 | \draw[color={rgb,255:red, 255; green, 125; blue, 0 }] (13, 0) .. controls (11, -1) and (15, -2) .. (0, 5); 21 | \draw[color={rgb,255:red, 255; green, 240; blue, 105 }] (14, 0) .. controls (12, -1) and (16, -2) .. (0, 5); 22 | \end{tikzpicture} 23 | """ 24 | 25 | 26 | def test_controls_example(): 27 | tikz = TikzPicture() 28 | for i in range(0, 15): 29 | line = tikz.line((i, 0), (0, 5)) 30 | line.options = f"color={rainbow_colors(i)}" 31 | line.control_pts = [(i - 2, -1), (i + 2, -2)] 32 | 33 | with tempfile.NamedTemporaryFile() as fp: 34 | temp_path = Path(fp.name) 35 | tikz.compile(temp_path, quiet=True) 36 | 37 | assert str(tikz) == code 38 | -------------------------------------------------------------------------------- /tests/integration/test_line_and_two_nodes.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from pathlib import Path 3 | from tikzpy import TikzPicture 4 | from tikzpy.colors import rainbow_colors 5 | 6 | code = r"""\begin{tikzpicture} 7 | \draw[thick, blue, o-o] (0, 0) to (1, 1); 8 | \node[below] at (0, 0) { Start! }; 9 | \node[above] at (1, 1) { End! }; 10 | \end{tikzpicture} 11 | """ 12 | 13 | 14 | def test_line_and_two_nodes(): 15 | tikz = TikzPicture() 16 | line = tikz.line((0, 0), (1, 1), options="thick, blue, o-o") 17 | start_node = tikz.node(line.start, options="below", text="Start!") 18 | end_node = tikz.node(line.end, options="above", text="End!") 19 | 20 | with tempfile.NamedTemporaryFile() as fp: 21 | temp_path = Path(fp.name) 22 | tikz.compile(temp_path, quiet=True) 23 | 24 | assert str(tikz) == code 25 | -------------------------------------------------------------------------------- /tests/integration/test_relu.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from pathlib import Path 3 | 4 | import numpy as np 5 | from tikzpy import TikzPicture, Point, R2_Space 6 | import math 7 | 8 | code = r"""\begin{tikzpicture} 9 | \begin{scope} 10 | \draw[Gray!30, ->] (-4, 0) to (4, 0); 11 | \draw[Gray!30, ->] (0, 0) to (0, 5.0); 12 | \node[below] at (4, 0) { $x$ }; 13 | \node[left] at (0, 5.0) { $y$ }; 14 | \end{scope} 15 | 16 | \draw[dashed] (-3.5, 4.5) to (4.5, 4.5); 17 | \node[left] at (-3.5, 4.5) { $y=1$ }; 18 | \draw[ProcessBlue, <-] (-4, 0) to (0, 0); 19 | \draw[ProcessBlue, ->] (0, 0) to (4.5, 4.5); 20 | \end{tikzpicture} 21 | """ 22 | 23 | 24 | def test_relu_example(): 25 | tikz = TikzPicture() 26 | xrange = (-3, 3) 27 | vert_scale = 4.5 28 | horiz_scale = 1.5 29 | 30 | # Set up xy-plane 31 | xy_plane = R2_Space(x_interval=(-4, 4), y_interval=(0, vert_scale + 0.5)) 32 | xy_plane.x_axis_options = "Gray!30, ->" 33 | xy_plane.y_axis_options = "Gray!30, ->" 34 | tikz.draw(xy_plane) 35 | 36 | line = tikz.line((-3.5, 4.5), (4.5, 4.5), options="dashed") 37 | tikz.node(line.start, options="left", text="$y=1$") 38 | # Plot it 39 | tikz.line((-4, 0), (0, 0), options="ProcessBlue, <-") 40 | tikz.line((0, 0), (4.5, 4.5), options="ProcessBlue, ->") 41 | 42 | with tempfile.NamedTemporaryFile() as fp: 43 | temp_path = Path(fp.name) 44 | tikz.compile(temp_path, quiet=True) 45 | 46 | assert str(tikz) == code 47 | -------------------------------------------------------------------------------- /tests/integration/test_shift_plot.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from pathlib import Path 3 | from tikzpy import TikzPicture 4 | from tikzpy.colors import rainbow_colors 5 | 6 | 7 | def test_shift_plot(): 8 | tikz = TikzPicture() 9 | # points = [(14.4, 3.2), (16.0, 3.6), (16.8, 4.8), (16.0, 6.8), (16.4, 8.8), (13.6, 8.8), (12.4, 7.6), (12.8, 5.6), (12.4, 3.6)] 10 | # points = [(6.6,11.4), (5.3,8.8), (3.6,9.9), (2.8,7.9), (3.7,6.1), (4.5,4), (6.2,4.2), (6.7,5.5), (8.5,4.3), (9.5,6.7), (8.8,8.5), (9.4,11.1), (7.7,11)] 11 | points = [ 12 | (5.6, 11.1), 13 | (5.2, 9.6), 14 | (3.2, 10.6), 15 | (4.3, 7.3), 16 | (3, 4.1), 17 | (5.6, 5.2), 18 | (7.2, 3.9), 19 | (8.4, 5.6), 20 | (10.2, 4.5), 21 | (8.7, 6.9), 22 | (10, 8.6), 23 | (8.1, 8.8), 24 | (9.3, 11.8), 25 | (7.2, 11.1), 26 | (6.2, 12.5), 27 | ] 28 | 29 | for i in range(1, 20): 30 | plot = tikz.plot_coordinates( 31 | options=f"fill = {rainbow_colors(i)}, opacity = 0.5", 32 | plot_options="smooth, tension=.5, closed hobby", 33 | points=points, 34 | ) 35 | plot.shift(0, i / 5) 36 | plot.rotate(45, about_pt=plot.center, radians=False) 37 | 38 | with tempfile.NamedTemporaryFile() as fp: 39 | temp_path = Path(fp.name) 40 | tikz.compile(temp_path, quiet=True) 41 | -------------------------------------------------------------------------------- /tests/integration/test_simple.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from pathlib import Path 3 | from tikzpy import TikzPicture 4 | 5 | code = r"""\begin{tikzpicture} 6 | \draw[thin, fill=orange!15] (0, 0) circle (3cm); 7 | \draw[dashed] (3, 0) arc [start angle = 0.0, end angle = 179.99999999999997, x radius = 3cm, y radius = 1.5cm]; 8 | \draw (-3, 0) arc [start angle = 179.99999999999997, end angle = 359.99999999999994, x radius = 3cm, y radius = 1.5cm]; 9 | \end{tikzpicture} 10 | """ 11 | 12 | 13 | def test_basic_circle_example(): 14 | tikz = TikzPicture() 15 | tikz.circle((0, 0), 3, options="thin, fill=orange!15") 16 | 17 | arc_one = tikz.arc((3, 0), 0, 180, x_radius=3, y_radius=1.5, options=f"dashed") 18 | arc_two = tikz.arc((-3, 0), 180, 360, x_radius=3, y_radius=1.5) 19 | 20 | with tempfile.NamedTemporaryFile() as fp: 21 | temp_path = Path(fp.name) 22 | tikz.compile(temp_path, quiet=True) 23 | 24 | assert str(tikz) == code 25 | -------------------------------------------------------------------------------- /tests/test_add_node.py: -------------------------------------------------------------------------------- 1 | import tikzpy 2 | 3 | tikz = tikzpy.TikzPicture() 4 | # Line 5 | line = tikz.line( 6 | (0, 0), (1, 1), options="thin, Green", control_pts=[(0.5, 0.9), (1, -2)] 7 | ) 8 | # Plot 9 | plot = tikz.plot_coordinates( 10 | points=[(1, 1), (2, 2), (3, 3), (2, -4)], 11 | options="purple", 12 | plot_options="smooth cycle", 13 | ) 14 | # Circle 15 | circle = tikz.circle((1, 1), 1, options="fill=red") 16 | # Rectangle 17 | rectangle = tikz.rectangle((2, 2), 1, 2, options="thick") 18 | # Ellipse 19 | ellipse = tikz.ellipse((0, 0), 3, 4, options="fill=Blue") 20 | # Arc 21 | arc = tikz.arc((0, 0), 20, 90, 4, options="fill=purple") 22 | 23 | 24 | def test_add_node(): 25 | # Add nodes 26 | line.add_node(options="below", text="$\\int$!") 27 | plot.add_node(options="above", text="$\\partial$!") 28 | circle.add_node(options="left", text="hello") 29 | rectangle.add_node(options="right", text="Wow!") 30 | ellipse.add_node(options="right", text="Wow!") 31 | arc.add_node(options="left") 32 | 33 | assert ( 34 | line.code 35 | == r"\draw[thin, Green] (0, 0) .. controls (0.5, 0.9) and (1, -2) .. (1, 1) node[below] { $\int$! };" 36 | ) 37 | assert ( 38 | plot.code 39 | == r"\draw[purple] plot[smooth cycle] coordinates {(1, 1) (2, 2) (3, 3) (2, -4) } node[above] { $\partial$! };" 40 | ) 41 | assert circle.code == r"\draw[fill=red] (1, 1) circle (1cm) node[left] { hello };" 42 | assert ( 43 | rectangle.code == r"\draw[thick] (2, 2) rectangle (3, 4) node[right] { Wow! };" 44 | ) 45 | assert ( 46 | ellipse.code 47 | == r"\draw[fill=Blue] (0, 0) ellipse (3cm and 4cm) node[right] { Wow! };" 48 | ) 49 | assert ( 50 | arc.code 51 | == r"\draw[fill=purple] (0, 0) arc [start angle = 20, end angle = 90, radius = 4cm] node[left] { };" 52 | ) 53 | 54 | 55 | def test_add_and_update(): 56 | # Add nodes 57 | line.add_node(options="below", text="$\\int$!") 58 | plot.add_node(options="above", text="$\\partial$!") 59 | circle.add_node(options="left", text="hello") 60 | rectangle.add_node(options="right", text="Wow!") 61 | ellipse.add_node(options="right", text="Wow!") 62 | arc.add_node(options="left") 63 | 64 | line.node.position = (3, 3) 65 | line.node.options = "above" 66 | line.node.text = "Replace" 67 | assert ( 68 | line.code 69 | == r"\draw[thin, Green] (0, 0) .. controls (0.5, 0.9) and (1, -2) .. (1, 1) node[above] at (3, 3) { Replace };" 70 | ) 71 | plot.node.position = (3, 3) 72 | plot.node.options = "above" 73 | plot.node.text = "Replace" 74 | assert ( 75 | plot.code 76 | == r"\draw[purple] plot[smooth cycle] coordinates {(1, 1) (2, 2) (3, 3) (2, -4) } node[above] at (3, 3) { Replace };" 77 | ) 78 | circle.node.position = (3, 3) 79 | circle.node.options = "above" 80 | circle.node.text = "Replace" 81 | assert ( 82 | circle.code 83 | == r"\draw[fill=red] (1, 1) circle (1cm) node[above] at (3, 3) { Replace };" 84 | ) 85 | rectangle.node.position = (3, 3) 86 | rectangle.node.options = "above" 87 | rectangle.node.text = "Replace" 88 | assert ( 89 | rectangle.code 90 | == r"\draw[thick] (2, 2) rectangle (3, 4) node[above] at (3, 3) { Replace };" 91 | ) 92 | ellipse.node.position = (3, 3) 93 | ellipse.node.options = "above" 94 | ellipse.node.text = "Replace" 95 | assert ( 96 | ellipse.code 97 | == r"\draw[fill=Blue] (0, 0) ellipse (3cm and 4cm) node[above] at (3, 3) { Replace };" 98 | ) 99 | arc.node.position = (3, 3) 100 | arc.node.options = "above" 101 | arc.node.text = "Replace" 102 | assert ( 103 | arc.code 104 | == r"\draw[fill=purple] (0, 0) arc [start angle = 20, end angle = 90, radius = 4cm] node[above] at (3, 3) { Replace };" 105 | ) 106 | 107 | 108 | def test_add_nodes_manually(): 109 | pass 110 | -------------------------------------------------------------------------------- /tests/test_attribute_assignment.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from tikzpy import TikzPicture, Point 3 | 4 | """ #2 5 | We test that reassignment of attributes returns the correct Tikz code. 6 | """ 7 | 8 | tikz = TikzPicture() 9 | # Line 10 | line = tikz.line((0, 0), (1, 1)) 11 | line.start = (-1, 2) 12 | line.end = (4, 4) 13 | line.options = "thick, blue" 14 | line.to_options = "bend right = 45" 15 | line.control_pts = [(0.5, 0.9), (1, -2)] 16 | # Plot 17 | plot = tikz.plot_coordinates(points=[(1, 1), (2, 2), (3, 3), (2, -4)]) 18 | plot.points = [Point(0, 0), Point(1, 0), Point(1, 1), Point(0, 1)] 19 | plot.options = "fill=green" 20 | plot.plot_options = "smooth cycle" 21 | # Circle 22 | circle = tikz.circle((1, 1), 1) 23 | circle.center = (2, 2) 24 | circle.radius = 5 25 | circle.options = "fill=purple" 26 | # Node 27 | node = tikz.node(position=(3, 3)) 28 | node.position = (4, 4) 29 | node.text = r"Don't forget $+ C$ when $\int$!" 30 | node.options = "below" 31 | # Rectangle 32 | rectangle = tikz.rectangle((2, 2), (3, 4)) 33 | rectangle.left_corner = (1, 1) 34 | rectangle.width = 4 35 | rectangle.height = 4 36 | rectangle.options = "fill=purple!50" 37 | # Ellipse 38 | ellipse = tikz.ellipse((0, 0), 3, 4) 39 | ellipse.center = (-1, 2) 40 | ellipse.x_axis = 5 41 | ellipse.y_axis = 3 42 | # Arc 43 | arc = tikz.arc((0, 0), 20, 90, 4) 44 | arc.position = (1, 1) 45 | arc.start_angle = 45 46 | arc.end_angle = 70 47 | arc.radius = 2 48 | arc.options = "fill = Blue!80" 49 | arc.radians = False 50 | 51 | 52 | @pytest.mark.order(2) 53 | def test_attribute_assignment(): 54 | """#2 : Test that we may reset attributes of already existing class objects properly.""" 55 | # Line 56 | assert line.start.x == -1 57 | assert line.start.y == 2 58 | assert line.end.x == 4 59 | assert line.end.y == 4 60 | assert line.options == "thick, blue" 61 | assert line.to_options == "bend right = 45" 62 | assert line.control_pts[0].x == 0.5 63 | assert line.control_pts[0].y == 0.9 64 | assert line.control_pts[1].x == 1 65 | assert line.control_pts[1].y == -2 66 | assert ( 67 | line.code 68 | == r"\draw[thick, blue] (-1, 2) .. controls (0.5, 0.9) and (1, -2) .. (4, 4);" 69 | ) 70 | # Plot 71 | assert plot.points[0].x == 0 72 | assert plot.points[0].y == 0 73 | assert plot.points[1].x == 1 74 | assert plot.points[1].y == 0 75 | assert plot.points[2].x == 1 76 | assert plot.points[2].y == 1 77 | assert plot.points[3].x == 0 78 | assert plot.points[3].y == 1 79 | assert plot.options == "fill=green" 80 | assert plot.plot_options == "smooth cycle" 81 | assert ( 82 | plot.code 83 | == r"\draw[fill=green] plot[smooth cycle] coordinates {(0, 0) (1, 0) (1, 1) (0, 1) };" 84 | ) 85 | # Circle 86 | assert circle.center.x == 2 87 | assert circle.center.y == 2 88 | assert circle.radius == 5 89 | assert circle.options == "fill=purple" 90 | assert circle.code == r"\draw[fill=purple] (2, 2) circle (5cm);" 91 | # Node 92 | assert node.position.x == 4 93 | assert node.position.y == 4 94 | assert node.text == r"Don't forget $+ C$ when $\int$!" 95 | assert node.options == "below" 96 | assert node.code == r"\node[below] at (4, 4) { Don't forget $+ C$ when $\int$! };" 97 | # Rectangle 98 | assert rectangle.left_corner.x == 1 99 | assert rectangle.left_corner.y == 1 100 | assert rectangle.width == 4 101 | assert rectangle.height == 4 102 | assert rectangle.options == "fill=purple!50" 103 | assert rectangle.code == r"\draw[fill=purple!50] (1, 1) rectangle (5, 5);" 104 | # Ellipse 105 | assert ellipse.center.x == -1 106 | assert ellipse.center.y == 2 107 | assert ellipse.x_axis == 5 108 | assert ellipse.y_axis == 3 109 | assert ellipse.code == r"\draw (-1, 2) ellipse (5cm and 3cm);" 110 | # Arc 111 | assert arc.position.x == 1 112 | assert arc.position.y == 1 113 | assert arc.start_angle == 45 114 | assert arc.end_angle == 70 115 | assert arc.radius == 2 116 | assert arc.options == "fill = Blue!80" 117 | assert arc.radians == False 118 | assert ( 119 | arc.code 120 | == r"\draw[fill = Blue!80] (1, 1) arc [start angle = 45, end angle = 70, radius = 2cm];" 121 | ) 122 | -------------------------------------------------------------------------------- /tests/test_file_creation.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import re 3 | import tempfile 4 | from pathlib import Path 5 | from tikzpy import TikzPicture 6 | from tikzpy.utils.helpers import true_posix_path 7 | 8 | TEX_FILE = r"""%!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 9 | % 10 | % Auto Generated by tikzpy. 11 | % 12 | % 13 | %%!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 14 | 15 | \documentclass[12pt,letterpaper]{article} 16 | \usepackage[left=0.3in, right=0.3in]{geometry} 17 | \usepackage{amsmath, amsfonts, amssymb} 18 | \usepackage[dvipsnames]{xcolor} % Colors, use dvipsnames for more color options 19 | \usepackage{tikz-cd} % Diagrams 20 | \usepackage{tikz} % General purpose graphics 21 | \usepackage{pgfplots} 22 | \usepackage{tikz-3dplot} 23 | 24 | \usetikzlibrary{ 25 | hobby, 26 | decorations.pathreplacing, 27 | decorations.markings 28 | } 29 | 30 | \begin{document} 31 | \pagestyle{empty} % no page numbers 32 | 33 | \begin{tikzpicture} 34 | \draw (1, 1) arc [start angle = 90, end angle = 180, radius = 3cm]; 35 | \end{tikzpicture} 36 | 37 | 38 | \end{document} 39 | """ 40 | 41 | 42 | def test_pdf_creation(): 43 | """Test that the pdf file is generated as tikz_code/tex_file.pdf.""" 44 | with tempfile.TemporaryDirectory() as tmp_dir: 45 | tikz = TikzPicture(tikz_code_dir=tmp_dir) 46 | # Line 47 | tikz.line( 48 | (0, 0), 49 | (1, 1), 50 | options="thick, blue", 51 | control_pts=[(0.25, 0.25), (0.75, 0.75)], 52 | ) 53 | tikz.write() 54 | pdf_location = tikz.compile() 55 | correct_pdf_file = Path(tmp_dir, "tex_file.pdf") 56 | assert correct_pdf_file.exists() 57 | assert pdf_location.resolve() == correct_pdf_file.resolve() 58 | 59 | 60 | def test_tikz_file_creation(): 61 | """Test that the tikz code file is generated as tikz_code/tikz_code.tex.""" 62 | with tempfile.TemporaryDirectory() as tmp_dir: 63 | tikz = TikzPicture(tikz_code_dir=tmp_dir) 64 | tikz.circle((1, 1), 7) 65 | tikz.write() 66 | correct_tikz_file = Path(tmp_dir, "tikz_code.tex") 67 | assert correct_tikz_file.exists() 68 | 69 | 70 | def test_tex_file_creation(): 71 | """Test that the tex file is generated as tikz_code/tex/tex_file directory""" 72 | with tempfile.TemporaryDirectory() as tmp_dir: 73 | tikz = TikzPicture(tikz_code_dir=tmp_dir) 74 | tikz.arc((1, 1), 90, 180, 3) 75 | tikz.write_tex_file("tex_file.tex") 76 | correct_tex_file = Path(tmp_dir, "tex_file.tex") 77 | assert correct_tex_file.exists() 78 | assert correct_tex_file.read_text() == TEX_FILE 79 | -------------------------------------------------------------------------------- /tests/test_helpler_funcs.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from tikzpy.utils.helpers import brackets, replace_code, extract_error_content 3 | 4 | 5 | @pytest.fixture 6 | def mock_latex_error_msg(): 7 | return """\ 8 | ! Package pgfkeys Error: I do not know the key '/tikz/dashed meow' and I am goi 9 | ng to ignore it. Perhaps you misspelled it. 10 | 11 | See the pgfkeys package documentation for explanation. 12 | Type H for immediate help. 13 | ... 14 | 15 | l.28 \draw[dashed meow] 16 | (3, 0) arc [start angle = 0.0, end angle = 179.9... 17 | 18 | ? 19 | """ 20 | 21 | 22 | def test_brackets(): 23 | """Test the brackets function to correctly surround nonempty text with brackets.""" 24 | string = "This is \nnot empty" 25 | empty_string = "" 26 | assert brackets(string) == "[This is \nnot empty]" 27 | assert brackets(empty_string) == "" 28 | 29 | 30 | def test_replace_code(): 31 | """Test the function replace_code and its update functionality. Check that it correctly replaces text surrounded 32 | by delimiters.""" 33 | delimiter_pairs = [ 34 | ("start", "end"), 35 | ("<", ">"), 36 | ("%__begin__@TikzPy__#id__==__(0)", "%__end__@TikzPy__#id__==__(0)"), 37 | ("%__begin__@TikzPy__#id__==__(10)", "%__end__@TikzPy__#id__==__(10)"), 38 | ] 39 | for delimiter_pair in delimiter_pairs: 40 | begin, end = delimiter_pair 41 | content = f""" 42 | {begin} This ! @ is $ % a 43 | ^ test, & * I'm 44 | going to ( ~ ` 45 | get ) = _ - replaced. 46 | {end} 47 | It doesn't grab this end, right? 48 | """ 49 | replacement_text = f"{begin} Hi! {end}" 50 | updated_text, num_matches = replace_code(begin, end, content, replacement_text) 51 | assert ( 52 | updated_text 53 | == f""" 54 | {begin} Hi! {end} 55 | It doesn't grab this end, right? 56 | """ 57 | ) 58 | 59 | 60 | def test_extract_error_content(mock_latex_error_msg): 61 | new_mock_latex_error_msg = mock_latex_error_msg + "\nFoo Bar" 62 | lines = new_mock_latex_error_msg.splitlines(keepends=True) 63 | assert mock_latex_error_msg == extract_error_content(lines) 64 | 65 | 66 | def test_extract_error_content_no_question_mark(mock_latex_error_msg): 67 | new_mock_latex_error_msg = mock_latex_error_msg + "\nFoo bar" 68 | new_mock_latex_error_msg = new_mock_latex_error_msg.replace("?", "") 69 | lines = new_mock_latex_error_msg.splitlines(keepends=True) 70 | assert new_mock_latex_error_msg == extract_error_content(lines) 71 | 72 | 73 | def test_extract_error_content_no_error_lines(mock_latex_error_msg): 74 | mock_latex_error_msg = mock_latex_error_msg.replace("!", "") 75 | lines = mock_latex_error_msg.splitlines(keepends=True) 76 | assert extract_error_content(lines) is None 77 | -------------------------------------------------------------------------------- /tests/test_tikz_picture.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import pytest 3 | import tikzpy 4 | 5 | from pathlib import Path 6 | from tikzpy import TikzPicture 7 | from tikzpy.utils.types import CompileError 8 | from unittest.mock import Mock 9 | 10 | 11 | def completed_process_factory(returncode=0): 12 | return Mock(returncode=returncode) 13 | 14 | 15 | def test_tikz_picture_write_tex_file(): 16 | filename = "foobar.tex" 17 | with tempfile.TemporaryDirectory() as tmp_dir: 18 | tmp_dir_path = Path(tmp_dir) 19 | tikz = TikzPicture(tikz_code_dir=tmp_dir) 20 | tex_file_path = tmp_dir_path / filename 21 | tikz.write_tex_file(str(tex_file_path)) 22 | assert tex_file_path.exists() 23 | 24 | 25 | def test_compile_smoke(mocker): 26 | # Spy on the subprocess call in the compile method 27 | spy = mocker.spy( 28 | tikzpy.tikz_environments.tikz_picture.subprocess, 29 | "run", 30 | ) 31 | 32 | with tempfile.TemporaryDirectory() as tmp_dir: 33 | pdf_dest = Path(tmp_dir) / "pdf_file.pdf" 34 | tikz = TikzPicture() 35 | tikz.circle((0, 0), 3, options="thin, fill=orange!15") 36 | assert pdf_dest.resolve() == tikz.compile(pdf_dest) 37 | assert spy.spy_return.returncode == 0 38 | 39 | 40 | def test_compile_error_no_log_file(mocker): 41 | # Mock the subprocess call to simulate failure 42 | mock_completed_process = mocker.patch( 43 | "tikzpy.tikz_environments.tikz_picture.subprocess.run", 44 | completed_process_factory(-1), 45 | ) 46 | with tempfile.TemporaryDirectory() as tmp_dir: 47 | tikz = TikzPicture() 48 | tikz.circle((0, 0), 3, options="thin, fill=orange!15") 49 | with pytest.raises(CompileError) as e: 50 | tikz.compile() 51 | assert "No log file found" in e.value.message 52 | 53 | 54 | def test_compile_error_log_file_parsing_failed(mocker): 55 | # Mock the subprocess call to simulate failure 56 | mock_completed_process = mocker.patch( 57 | "tikzpy.tikz_environments.tikz_picture.subprocess.run", 58 | completed_process_factory(-1), 59 | ) 60 | with tempfile.TemporaryDirectory() as tmp_dir: 61 | tikz = TikzPicture() 62 | tikz.circle((0, 0), 3, options="thin, fill=orange!15") 63 | 64 | # Pretend the log file exists 65 | mocker.patch( 66 | "tikzpy.tikz_environments.tikz_picture.Path.exists", 67 | return_value=True, 68 | ) 69 | # Make fake log file have no text 70 | mocker.patch( 71 | "tikzpy.tikz_environments.tikz_picture.Path.read_text", 72 | return_value="", 73 | ) 74 | with pytest.raises(CompileError) as e: 75 | tikz.compile() 76 | assert "Failed to parse log file" in e.value.message 77 | 78 | 79 | def test_compile_compile_error_log_file_parsing(mocker): 80 | # Spy on the subprocess call in the compile method 81 | spy = mocker.spy( 82 | tikzpy.tikz_environments.tikz_picture.subprocess, 83 | "run", 84 | ) 85 | 86 | with tempfile.TemporaryDirectory() as tmp_dir: 87 | tikz = TikzPicture() 88 | tikz.circle((0, 0), 3, options="thin, fill=orange!15, meow") 89 | with pytest.raises(CompileError) as e: 90 | tikz.compile() 91 | assert ( 92 | "! Package pgfkeys Error: I do not know the key '/tikz/meow'" 93 | in e.value.message 94 | ) 95 | assert spy.spy_return.returncode != 0 96 | --------------------------------------------------------------------------------