Repository: Yatoom/foronoi (language: python)

Issue:
New end-user API's and methods, improved visualization, and observers

AttributeError: 'NoneType' object has no attribute 'edge'
When point number is too large (around 30) Algorithm stops workin, and i can see this error

```
Traceback (most recent call last):
  File "VoronoiGraph.py", line 206, in <module>
    v.testVoronoi()
  File "VoronoiGraph.py", line 184, in testVoronoi
    v.create_diagram(points=points, vis_steps=False,
  File "/usr/local/lib/python3.8/dist-packages/voronoi-0.9-py3.8.egg/voronoi/algorithm.py", line 99, in create_diagram
  File "/usr/local/lib/python3.8/dist-packages/voronoi-0.9-py3.8.egg/voronoi/algorithm.py", line 271, in handle_circle_event
AttributeError: 'NoneType' object has no attribute 'edge'
```

After limiting the number of points graph is computed properly
Some user questions
I am evaluating several voronoi libraries to be used in a Freecad add-on I am working on for generating frame structures based on voronoi patterns. Very early stage now, so far just flat surfaces, later to be mapped onto any surfaces hopefully. Anyway, your library looks like a very good fit for my needs, but I have some questions and a minor issue.

For my use case I need to get for each vertex every edge connected to that vertex. By looking at the examples and the code I did not find any straightforward way to do this. Is there an easy way to get those edges ? Otherwise I can scan all edges for every vertex, but that is very inefficient.

One minor issue I have: to make installation of my addon easy, I don't want any dependencies so I pack all sources into the addon. `voronoi` has some dependencies for the visualization (which i do not need). So I have to comment out the visualization import statements in `alogrithm.py`. Is there a better way to use barebone `voronoi` without visualization and its dependencies ?

And one last question: the way I access edges for drawing lines along them is just taken from the original `visualization/visual.py`:
```
...
v = Voronoi(bounding_poly)
v.create_diagram(points=points)
for edge in v.edges:
        y = -1000
        start = edge.get_origin(y, bounding_poly.max_y)
        end = edge.twin.get_origin(y, bounding_poly.max_y)
```
I do not fully understand (yet) what this does, but it provides the start and endpoint for the lines I am drawing. However the hardcoded `y = -1000` looks a bit scary to me. So if the recommended way to iterate through all edges for line drawing looks different, please let me know.

And thanks for providing this great software


Discussion / hints:

Hi,
I found this package searching for an alternative to the using of Voronoi diagram method from the not-yet-released-version 1.8 of the Shapely package which is based on the Geos package.

With that package, using a list of points like this:
`(20.1273, 18.7303), (26.5107, 18.7303), (20.1273, 23.8437), (26.5107, 23.8437)`
[Those are the vertexes of a rectangular box.]
I had an Exception which made the algorithm not usable for me. Posting on the Issues page of Shapely, a contributor replied that the same could be seen on PostGIS (using Geos too) so the issue is upstream, in the Geos algorithm for Voronoi diagram.

So I searched for an alternative and found this package Yatoom/voronoi, with which I tried the same series of points.
Got the error you posted above.

Only that in this case I only had to make a couple of changes to make it work. BTW, I've tried with 64 points in a grid and it worked after making the simple changes I've mentioned.

I think that this is an ugly hack but it may point the developer in the right direction.

The changes I made were:
In voronoi.algorithm.handle_circle_event() method:
just after "Updating breakpoint" I test if the updated attribute is None and if None I return False.
`
# Update breakpoints
       self.beach_line, updated, removed, left, right = self.update_breakpoints(
           self.beach_line, self.sweep_line, arc_node, predecessor, successor)

        if updated is None:
            return None
If the method reach it's end I return True.

and in  voronoi.algorithm.handle_circle_event() method:
When handling circle events, when handling the event,
instead of 
`
self.handle_circle_event(event, verbose=verbose)`
I replaced it with:
`
Handle the event
res = self.handle_circle_event(event, verbose=verbose)

if res is None:
    continue`

PS: Sorry for the formatting, I'm not used to ...

Best regards,
Marius

LE: Well, while the Matplotlib plot that the create_diagram() method display looks correct for 64 points, in reality with the modification above, after plotting the  actual resulting Voronoi polygons in another plot, some of the polygons are fused together.
So the modifications are not adequate.
https://ibb.co/2npRRy3


Fix patch:
diff --git a/Pipfile b/Pipfile
index 4dce864..6e10c3b 100644
--- a/Pipfile
+++ b/Pipfile
@@ -7,6 +7,7 @@ name = "pypi"
 numpy = "*"
 matplotlib = "*"
 pytest = "*"
+graphviz = "*"
 
 [dev-packages]
 nose = "*"
diff --git a/Pipfile.lock b/Pipfile.lock
index 71205a3..685846f 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "11b36640c08dcb4874b3c32aea059e8dec4043e06ded70ba68f0d5f9c58b6725"
+            "sha256": "079cc89b54f10b15159148043d214ec6cde9f2cffab2c9eb44c4ce2721eeafdb"
         },
         "pipfile-spec": 6,
         "requires": {
@@ -16,19 +16,12 @@
         ]
     },
     "default": {
-        "atomicwrites": {
-            "hashes": [
-                "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
-                "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
-            ],
-            "version": "==1.3.0"
-        },
         "attrs": {
             "hashes": [
-                "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
-                "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
+                "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
+                "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
             ],
-            "version": "==19.1.0"
+            "version": "==20.3.0"
         },
         "cycler": {
             "hashes": [
@@ -37,161 +30,213 @@
             ],
             "version": "==0.10.0"
         },
-        "importlib-metadata": {
+        "graphviz": {
+            "hashes": [
+                "sha256:3cad5517c961090dfc679df6402a57de62d97703e2880a1a46147bb0dc1639eb",
+                "sha256:d2d25af1c199cad567ce4806f0449cb74eb30cf451fd7597251e1da099ac6e57"
+            ],
+            "index": "pypi",
+            "version": "==0.16"
+        },
+        "iniconfig": {
             "hashes": [
-                "sha256:6dfd58dfe281e8d240937776065dd3624ad5469c835248219bd16cf2e12dbeb7",
-                "sha256:cb6ee23b46173539939964df59d3d72c3e0c1b5d54b84f1d8a7e912fe43612db"
+                "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
+                "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
             ],
-            "version": "==0.18"
+            "version": "==1.1.1"
         },
         "kiwisolver": {
             "hashes": [
-                "sha256:05b5b061e09f60f56244adc885c4a7867da25ca387376b02c1efc29cc16bcd0f",
-                "sha256:26f4fbd6f5e1dabff70a9ba0d2c4bd30761086454aa30dddc5b52764ee4852b7",
-                "sha256:3b2378ad387f49cbb328205bda569b9f87288d6bc1bf4cd683c34523a2341efe",
-                "sha256:400599c0fe58d21522cae0e8b22318e09d9729451b17ee61ba8e1e7c0346565c",
-                "sha256:47b8cb81a7d18dbaf4fed6a61c3cecdb5adec7b4ac292bddb0d016d57e8507d5",
-                "sha256:53eaed412477c836e1b9522c19858a8557d6e595077830146182225613b11a75",
-                "sha256:58e626e1f7dfbb620d08d457325a4cdac65d1809680009f46bf41eaf74ad0187",
-                "sha256:5a52e1b006bfa5be04fe4debbcdd2688432a9af4b207a3f429c74ad625022641",
-                "sha256:5c7ca4e449ac9f99b3b9d4693debb1d6d237d1542dd6a56b3305fe8a9620f883",
-                "sha256:682e54f0ce8f45981878756d7203fd01e188cc6c8b2c5e2cf03675390b4534d5",
-                "sha256:79bfb2f0bd7cbf9ea256612c9523367e5ec51d7cd616ae20ca2c90f575d839a2",
-                "sha256:7f4dd50874177d2bb060d74769210f3bce1af87a8c7cf5b37d032ebf94f0aca3",
-                "sha256:8944a16020c07b682df861207b7e0efcd2f46c7488619cb55f65882279119389",
-                "sha256:8aa7009437640beb2768bfd06da049bad0df85f47ff18426261acecd1cf00897",
-                "sha256:939f36f21a8c571686eb491acfffa9c7f1ac345087281b412d63ea39ca14ec4a",
-                "sha256:9733b7f64bd9f807832d673355f79703f81f0b3e52bfce420fc00d8cb28c6a6c",
-                "sha256:a02f6c3e229d0b7220bd74600e9351e18bc0c361b05f29adae0d10599ae0e326",
-                "sha256:a0c0a9f06872330d0dd31b45607197caab3c22777600e88031bfe66799e70bb0",
-                "sha256:acc4df99308111585121db217681f1ce0eecb48d3a828a2f9bbf9773f4937e9e",
-                "sha256:b64916959e4ae0ac78af7c3e8cef4becee0c0e9694ad477b4c6b3a536de6a544",
-                "sha256:d3fcf0819dc3fea58be1fd1ca390851bdb719a549850e708ed858503ff25d995",
-                "sha256:d52e3b1868a4e8fd18b5cb15055c76820df514e26aa84cc02f593d99fef6707f",
-                "sha256:db1a5d3cc4ae943d674718d6c47d2d82488ddd94b93b9e12d24aabdbfe48caee",
-                "sha256:e3a21a720791712ed721c7b95d433e036134de6f18c77dbe96119eaf7aa08004",
-                "sha256:e8bf074363ce2babeb4764d94f8e65efd22e6a7c74860a4f05a6947afc020ff2",
-                "sha256:f16814a4a96dc04bf1da7d53ee8d5b1d6decfc1a92a63349bb15d37b6a263dd9",
-                "sha256:f2b22153870ca5cf2ab9c940d7bc38e8e9089fa0f7e5856ea195e1cf4ff43d5a",
-                "sha256:f790f8b3dff3d53453de6a7b7ddd173d2e020fb160baff578d578065b108a05f"
-            ],
-            "version": "==1.1.0"
+                "sha256:0cd53f403202159b44528498de18f9285b04482bab2a6fc3f5dd8dbb9352e30d",
+                "sha256:1e1bc12fb773a7b2ffdeb8380609f4f8064777877b2225dec3da711b421fda31",
+                "sha256:225e2e18f271e0ed8157d7f4518ffbf99b9450fca398d561eb5c4a87d0986dd9",
+                "sha256:232c9e11fd7ac3a470d65cd67e4359eee155ec57e822e5220322d7b2ac84fbf0",
+                "sha256:31dfd2ac56edc0ff9ac295193eeaea1c0c923c0355bf948fbd99ed6018010b72",
+                "sha256:33449715e0101e4d34f64990352bce4095c8bf13bed1b390773fc0a7295967b3",
+                "sha256:401a2e9afa8588589775fe34fc22d918ae839aaaf0c0e96441c0fdbce6d8ebe6",
+                "sha256:44a62e24d9b01ba94ae7a4a6c3fb215dc4af1dde817e7498d901e229aaf50e4e",
+                "sha256:50af681a36b2a1dee1d3c169ade9fdc59207d3c31e522519181e12f1b3ba7000",
+                "sha256:563c649cfdef27d081c84e72a03b48ea9408c16657500c312575ae9d9f7bc1c3",
+                "sha256:5989db3b3b34b76c09253deeaf7fbc2707616f130e166996606c284395da3f18",
+                "sha256:5a7a7dbff17e66fac9142ae2ecafb719393aaee6a3768c9de2fd425c63b53e21",
+                "sha256:5c3e6455341008a054cccee8c5d24481bcfe1acdbc9add30aa95798e95c65621",
+                "sha256:5f6ccd3dd0b9739edcf407514016108e2280769c73a85b9e59aa390046dbf08b",
+                "sha256:72c99e39d005b793fb7d3d4e660aed6b6281b502e8c1eaf8ee8346023c8e03bc",
+                "sha256:78751b33595f7f9511952e7e60ce858c6d64db2e062afb325985ddbd34b5c131",
+                "sha256:834ee27348c4aefc20b479335fd422a2c69db55f7d9ab61721ac8cd83eb78882",
+                "sha256:8be8d84b7d4f2ba4ffff3665bcd0211318aa632395a1a41553250484a871d454",
+                "sha256:950a199911a8d94683a6b10321f9345d5a3a8433ec58b217ace979e18f16e248",
+                "sha256:a357fd4f15ee49b4a98b44ec23a34a95f1e00292a139d6015c11f55774ef10de",
+                "sha256:a53d27d0c2a0ebd07e395e56a1fbdf75ffedc4a05943daf472af163413ce9598",
+                "sha256:acef3d59d47dd85ecf909c359d0fd2c81ed33bdff70216d3956b463e12c38a54",
+                "sha256:b38694dcdac990a743aa654037ff1188c7a9801ac3ccc548d3341014bc5ca278",
+                "sha256:b9edd0110a77fc321ab090aaa1cfcaba1d8499850a12848b81be2222eab648f6",
+                "sha256:c08e95114951dc2090c4a630c2385bef681cacf12636fb0241accdc6b303fd81",
+                "sha256:c5518d51a0735b1e6cee1fdce66359f8d2b59c3ca85dc2b0813a8aa86818a030",
+                "sha256:c8fd0f1ae9d92b42854b2979024d7597685ce4ada367172ed7c09edf2cef9cb8",
+                "sha256:ca3820eb7f7faf7f0aa88de0e54681bddcb46e485beb844fcecbcd1c8bd01689",
+                "sha256:cf8b574c7b9aa060c62116d4181f3a1a4e821b2ec5cbfe3775809474113748d4",
+                "sha256:d3155d828dec1d43283bd24d3d3e0d9c7c350cdfcc0bd06c0ad1209c1bbc36d0",
+                "sha256:f8d6f8db88049a699817fd9178782867bf22283e3813064302ac59f61d95be05",
+                "sha256:fd34fbbfbc40628200730bc1febe30631347103fc8d3d4fa012c21ab9c11eca9"
+            ],
+            "version": "==1.3.1"
         },
         "matplotlib": {
             "hashes": [
-                "sha256:08d9bc2e2acef42965256acd5015dc2c899cbd53e01bf4214c5510c7ea0efd2d",
-                "sha256:1e0213f87cc0076f7b0c4c251d7e23601e2419cd98691df79edb95517ba06f0c",
-                "sha256:1f31053f660df5f0310118d7f5bd1e8025170e9773f0bebe8fec486d0926adf6",
-                "sha256:399bf6352633aeeb45ca55c6c943fa2738022fb17ae498c32a142ced0b41528d",
-                "sha256:409a5894efb810d630d2512449c7a4394de9a4d15fc6394e26a409b17d9cc18c",
-                "sha256:5c5ef5cf1bc8f483123102e2615644937af7d4c01d100acc72bf74a044a78717",
-                "sha256:d0052be5cdfa27018bb08194b8812c47cb985d60eb682e1809c76e9600839516",
-                "sha256:e7d6620d145ca9f6c3e88248e5734b6fda430e75e70755b887e48f8e9bc1de2a",
-                "sha256:f3d8b6bccc577e4e5ecbd58fdd63cacb8e58f0ed1e97616a7f7a7baaf4b8d036"
+                "sha256:1de0bb6cbfe460725f0e97b88daa8643bcf9571c18ba90bb8e41432aaeca91d6",
+                "sha256:1e850163579a8936eede29fad41e202b25923a0a8d5ffd08ce50fc0a97dcdc93",
+                "sha256:215e2a30a2090221a9481db58b770ce56b8ef46f13224ae33afe221b14b24dc1",
+                "sha256:348e6032f666ffd151b323342f9278b16b95d4a75dfacae84a11d2829a7816ae",
+                "sha256:3d2eb9c1cc254d0ffa90bc96fde4b6005d09c2228f99dfd493a4219c1af99644",
+                "sha256:3e477db76c22929e4c6876c44f88d790aacdf3c3f8f3a90cb1975c0bf37825b0",
+                "sha256:451cc89cb33d6652c509fc6b588dc51c41d7246afdcc29b8624e256b7663ed1f",
+                "sha256:46b1a60a04e6d884f0250d5cc8dc7bd21a9a96c584a7acdaab44698a44710bab",
+                "sha256:5f571b92a536206f7958f7cb2d367ff6c9a1fa8229dc35020006e4cdd1ca0acd",
+                "sha256:672960dd114e342b7c610bf32fb99d14227f29919894388b41553217457ba7ef",
+                "sha256:7310e353a4a35477c7f032409966920197d7df3e757c7624fd842f3eeb307d3d",
+                "sha256:746a1df55749629e26af7f977ea426817ca9370ad1569436608dc48d1069b87c",
+                "sha256:7c155437ae4fd366e2700e2716564d1787700687443de46bcb895fe0f84b761d",
+                "sha256:9265ae0fb35e29f9b8cc86c2ab0a2e3dcddc4dd9de4b85bf26c0f63fe5c1c2ca",
+                "sha256:94bdd1d55c20e764d8aea9d471d2ae7a7b2c84445e0fa463f02e20f9730783e1",
+                "sha256:9a79e5dd7bb797aa611048f5b70588b23c5be05b63eefd8a0d152ac77c4243db",
+                "sha256:a17f0a10604fac7627ec82820439e7db611722e80c408a726cd00d8c974c2fb3",
+                "sha256:a1acb72f095f1d58ecc2538ed1b8bca0b57df313b13db36ed34b8cdf1868e674",
+                "sha256:aa49571d8030ad0b9ac39708ee77bd2a22f87815e12bdee52ecaffece9313ed8",
+                "sha256:c24c05f645aef776e8b8931cb81e0f1632d229b42b6d216e30836e2e145a2b40",
+                "sha256:cf3a7e54eff792f0815dbbe9b85df2f13d739289c93d346925554f71d484be78",
+                "sha256:d738acfdfb65da34c91acbdb56abed46803db39af259b7f194dc96920360dbe4",
+                "sha256:e15fa23d844d54e7b3b7243afd53b7567ee71c721f592deb0727ee85e668f96a",
+                "sha256:ed4a9e6dcacba56b17a0a9ac22ae2c72a35b7f0ef0693aa68574f0b2df607a89",
+                "sha256:f44149a0ef5b4991aaef12a93b8e8d66d6412e762745fea1faa61d98524e0ba9"
             ],
             "index": "pypi",
-            "version": "==3.1.0"
-        },
-        "more-itertools": {
-            "hashes": [
-                "sha256:3ad685ff8512bf6dc5a8b82ebf73543999b657eded8c11803d9ba6b648986f4d",
-                "sha256:8bb43d1f51ecef60d81854af61a3a880555a14643691cc4b64a6ee269c78f09a"
-            ],
-            "markers": "python_version > '2.7'",
-            "version": "==7.1.0"
+            "version": "==3.3.4"
         },
         "numpy": {
             "hashes": [
-                "sha256:0778076e764e146d3078b17c24c4d89e0ecd4ac5401beff8e1c87879043a0633",
-                "sha256:141c7102f20abe6cf0d54c4ced8d565b86df4d3077ba2343b61a6db996cefec7",
-                "sha256:14270a1ee8917d11e7753fb54fc7ffd1934f4d529235beec0b275e2ccf00333b",
-                "sha256:27e11c7a8ec9d5838bc59f809bfa86efc8a4fd02e58960fa9c49d998e14332d5",
-                "sha256:2a04dda79606f3d2f760384c38ccd3d5b9bb79d4c8126b67aff5eb09a253763e",
-                "sha256:3c26010c1b51e1224a3ca6b8df807de6e95128b0908c7e34f190e7775455b0ca",
-                "sha256:52c40f1a4262c896420c6ea1c6fda62cf67070e3947e3307f5562bd783a90336",
-                "sha256:6e4f8d9e8aa79321657079b9ac03f3cf3fd067bf31c1cca4f56d49543f4356a5",
-                "sha256:7242be12a58fec245ee9734e625964b97cf7e3f2f7d016603f9e56660ce479c7",
-                "sha256:7dc253b542bfd4b4eb88d9dbae4ca079e7bf2e2afd819ee18891a43db66c60c7",
-                "sha256:94f5bd885f67bbb25c82d80184abbf7ce4f6c3c3a41fbaa4182f034bba803e69",
-                "sha256:a89e188daa119ffa0d03ce5123dee3f8ffd5115c896c2a9d4f0dbb3d8b95bfa3",
-                "sha256:ad3399da9b0ca36e2f24de72f67ab2854a62e623274607e37e0ce5f5d5fa9166",
-                "sha256:b0348be89275fd1d4c44ffa39530c41a21062f52299b1e3ee7d1c61f060044b8",
-                "sha256:b5554368e4ede1856121b0dfa35ce71768102e4aa55e526cb8de7f374ff78722",
-                "sha256:cbddc56b2502d3f87fda4f98d948eb5b11f36ff3902e17cb6cc44727f2200525",
-                "sha256:d79f18f41751725c56eceab2a886f021d70fd70a6188fd386e29a045945ffc10",
-                "sha256:dc2ca26a19ab32dc475dbad9dfe723d3a64c835f4c23f625c2b6566ca32b9f29",
-                "sha256:dd9bcd4f294eb0633bb33d1a74febdd2b9018b8b8ed325f861fffcd2c7660bb8",
-                "sha256:e8baab1bc7c9152715844f1faca6744f2416929de10d7639ed49555a85549f52",
-                "sha256:ec31fe12668af687b99acf1567399632a7c47b0e17cfb9ae47c098644ef36797",
-                "sha256:f12b4f7e2d8f9da3141564e6737d79016fe5336cc92de6814eba579744f65b0a",
-                "sha256:f58ac38d5ca045a377b3b377c84df8175ab992c970a53332fa8ac2373df44ff7"
+                "sha256:032be656d89bbf786d743fee11d01ef318b0781281241997558fa7950028dd29",
+                "sha256:104f5e90b143dbf298361a99ac1af4cf59131218a045ebf4ee5990b83cff5fab",
+                "sha256:125a0e10ddd99a874fd357bfa1b636cd58deb78ba4a30b5ddb09f645c3512e04",
+                "sha256:12e4ba5c6420917571f1a5becc9338abbde71dd811ce40b37ba62dec7b39af6d",
+                "sha256:13adf545732bb23a796914fe5f891a12bd74cf3d2986eed7b7eba2941eea1590",
+                "sha256:2d7e27442599104ee08f4faed56bb87c55f8b10a5494ac2ead5c98a4b289e61f",
+                "sha256:3bc63486a870294683980d76ec1e3efc786295ae00128f9ea38e2c6e74d5a60a",
+                "sha256:3d3087e24e354c18fb35c454026af3ed8997cfd4997765266897c68d724e4845",
+                "sha256:4ed8e96dc146e12c1c5cdd6fb9fd0757f2ba66048bf94c5126b7efebd12d0090",
+                "sha256:60759ab15c94dd0e1ed88241fd4fa3312db4e91d2c8f5a2d4cf3863fad83d65b",
+                "sha256:65410c7f4398a0047eea5cca9b74009ea61178efd78d1be9847fac1d6716ec1e",
+                "sha256:66b467adfcf628f66ea4ac6430ded0614f5cc06ba530d09571ea404789064adc",
+                "sha256:7199109fa46277be503393be9250b983f325880766f847885607d9b13848f257",
+                "sha256:72251e43ac426ff98ea802a931922c79b8d7596480300eb9f1b1e45e0543571e",
+                "sha256:89e5336f2bec0c726ac7e7cdae181b325a9c0ee24e604704ed830d241c5e47ff",
+                "sha256:89f937b13b8dd17b0099c7c2e22066883c86ca1575a975f754babc8fbf8d69a9",
+                "sha256:9c94cab5054bad82a70b2e77741271790304651d584e2cdfe2041488e753863b",
+                "sha256:9eb551d122fadca7774b97db8a112b77231dcccda8e91a5bc99e79890797175e",
+                "sha256:a1d7995d1023335e67fb070b2fae6f5968f5be3802b15ad6d79d81ecaa014fe0",
+                "sha256:ae61f02b84a0211abb56462a3b6cd1e7ec39d466d3160eb4e1da8bf6717cdbeb",
+                "sha256:b9410c0b6fed4a22554f072a86c361e417f0258838957b78bd063bde2c7f841f",
+                "sha256:c26287dfc888cf1e65181f39ea75e11f42ffc4f4529e5bd19add57ad458996e2",
+                "sha256:c91ec9569facd4757ade0888371eced2ecf49e7982ce5634cc2cf4e7331a4b14",
+                "sha256:ecb5b74c702358cdc21268ff4c37f7466357871f53a30e6f84c686952bef16a9"
             ],
             "index": "pypi",
-            "version": "==1.16.4"
+            "version": "==1.20.1"
         },
         "packaging": {
             "hashes": [
-                "sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af",
-                "sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3"
-            ],
-            "version": "==19.0"
+                "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
+                "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
+            ],
+            "version": "==20.9"
+        },
+        "pillow": {
+            "hashes": [
+                "sha256:15306d71a1e96d7e271fd2a0737038b5a92ca2978d2e38b6ced7966583e3d5af",
+                "sha256:1940fc4d361f9cc7e558d6f56ff38d7351b53052fd7911f4b60cd7bc091ea3b1",
+                "sha256:1f93f2fe211f1ef75e6f589327f4d4f8545d5c8e826231b042b483d8383e8a7c",
+                "sha256:30d33a1a6400132e6f521640dd3f64578ac9bfb79a619416d7e8802b4ce1dd55",
+                "sha256:328240f7dddf77783e72d5ed79899a6b48bc6681f8d1f6001f55933cb4905060",
+                "sha256:46c2bcf8e1e75d154e78417b3e3c64e96def738c2a25435e74909e127a8cba5e",
+                "sha256:5762ebb4436f46b566fc6351d67a9b5386b5e5de4e58fdaa18a1c83e0e20f1a8",
+                "sha256:5a2d957eb4aba9d48170b8fe6538ec1fbc2119ffe6373782c03d8acad3323f2e",
+                "sha256:5cf03b9534aca63b192856aa601c68d0764810857786ea5da652581f3a44c2b0",
+                "sha256:5daba2b40782c1c5157a788ec4454067c6616f5a0c1b70e26ac326a880c2d328",
+                "sha256:63cd413ac52ee3f67057223d363f4f82ce966e64906aea046daf46695e3c8238",
+                "sha256:6efac40344d8f668b6c4533ae02a48d52fd852ef0654cc6f19f6ac146399c733",
+                "sha256:71b01ee69e7df527439d7752a2ce8fb89e19a32df484a308eca3e81f673d3a03",
+                "sha256:71f31ee4df3d5e0b366dd362007740106d3210fb6a56ec4b581a5324ba254f06",
+                "sha256:72027ebf682abc9bafd93b43edc44279f641e8996fb2945104471419113cfc71",
+                "sha256:74cd9aa648ed6dd25e572453eb09b08817a1e3d9f8d1bd4d8403d99e42ea790b",
+                "sha256:81b3716cc9744ffdf76b39afb6247eae754186838cedad0b0ac63b2571253fe6",
+                "sha256:8565355a29655b28fdc2c666fd9a3890fe5edc6639d128814fafecfae2d70910",
+                "sha256:87f42c976f91ca2fc21a3293e25bd3cd895918597db1b95b93cbd949f7d019ce",
+                "sha256:89e4c757a91b8c55d97c91fa09c69b3677c227b942fa749e9a66eef602f59c28",
+                "sha256:8c4e32218c764bc27fe49b7328195579581aa419920edcc321c4cb877c65258d",
+                "sha256:903293320efe2466c1ab3509a33d6b866dc850cfd0c5d9cc92632014cec185fb",
+                "sha256:90882c6f084ef68b71bba190209a734bf90abb82ab5e8f64444c71d5974008c6",
+                "sha256:98afcac3205d31ab6a10c5006b0cf040d0026a68ec051edd3517b776c1d78b09",
+                "sha256:a01da2c266d9868c4f91a9c6faf47a251f23b9a862dce81d2ff583135206f5be",
+                "sha256:aeab4cd016e11e7aa5cfc49dcff8e51561fa64818a0be86efa82c7038e9369d0",
+                "sha256:b07c660e014852d98a00a91adfbe25033898a9d90a8f39beb2437d22a203fc44",
+                "sha256:bead24c0ae3f1f6afcb915a057943ccf65fc755d11a1410a909c1fefb6c06ad1",
+                "sha256:d1d6bca39bb6dd94fba23cdb3eeaea5e30c7717c5343004d900e2a63b132c341",
+                "sha256:e2cd8ac157c1e5ae88b6dd790648ee5d2777e76f1e5c7d184eaddb2938594f34",
+                "sha256:e5739ae63636a52b706a0facec77b2b58e485637e1638202556156e424a02dc2",
+                "sha256:f36c3ff63d6fc509ce599a2f5b0d0732189eed653420e7294c039d342c6e204a",
+                "sha256:f91b50ad88048d795c0ad004abbe1390aa1882073b1dca10bfd55d0b8cf18ec5"
+            ],
+            "version": "==8.1.2"
         },
         "pluggy": {
             "hashes": [
-                "sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc",
-                "sha256:b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c"
+                "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
+                "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
             ],
-            "version": "==0.12.0"
+            "version": "==0.13.1"
         },
         "py": {
             "hashes": [
-                "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa",
-                "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"
+                "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
+                "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
             ],
-            "version": "==1.8.0"
+            "version": "==1.10.0"
         },
         "pyparsing": {
             "hashes": [
-                "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a",
-                "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03"
+                "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
+                "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
             ],
-            "version": "==2.4.0"
+            "version": "==2.4.7"
         },
         "pytest": {
             "hashes": [
-                "sha256:4a784f1d4f2ef198fe9b7aef793e9fa1a3b2f84e822d9b3a64a181293a572d45",
-                "sha256:926855726d8ae8371803f7b2e6ec0a69953d9c6311fa7c3b6c1b929ff92d27da"
+                "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9",
+                "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839"
             ],
             "index": "pypi",
-            "version": "==4.6.3"
+            "version": "==6.2.2"
         },
         "python-dateutil": {
             "hashes": [
-                "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb",
-                "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"
+                "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
+                "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
             ],
-            "version": "==2.8.0"
+            "version": "==2.8.1"
         },
         "six": {
             "hashes": [
-                "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
-                "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
-            ],
-            "version": "==1.12.0"
-        },
-        "wcwidth": {
-            "hashes": [
-                "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
-                "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
+                "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
+                "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
             ],
-            "version": "==0.1.7"
+            "version": "==1.15.0"
         },
-        "zipp": {
+        "toml": {
             "hashes": [
-                "sha256:8c1019c6aad13642199fbe458275ad6a84907634cc9f0989877ccc4a2840139d",
-                "sha256:ca943a7e809cc12257001ccfb99e3563da9af99d52f261725e96dfe0f9275bc3"
+                "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
+                "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
             ],
-            "version": "==0.5.1"
+            "version": "==0.10.2"
         }
     },
     "develop": {
diff --git a/README.md b/README.md
index 24d3a4e..afd6e0a 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 # Voronoi
 [![Build Status](https://travis-ci.org/Yatoom/voronoi.svg?branch=master)](https://travis-ci.org/Yatoom/voronoi)
 
-![](https://raw.githubusercontent.com/Yatoom/voronoi/master/triangle.gif)
+![](voronoi.gif)
 
 A Python implementation of Fortune's algorithm based on the description of "Computational Geometry: Algorithms and Applications" by de Berg et al. The algorithm handles the special cases described in the book. The bounding box is generalized to handle a convex polygon.
 
@@ -19,10 +19,10 @@ Note: you need to use `sudo python3 setup.py install` on most Linux distribution
 
 Example that uses a polygon as a bounding box.
 
-```python
-from voronoi import Voronoi, Polygon
+```python3
+from voronoi import Voronoi, Polygon, Visualizer, VoronoiObserver
 
-# Define a set of points
+# Define some points (a.k.a sites or cell points)
 points = [
     (2.5, 2.5),
     (4, 7.5),
@@ -33,7 +33,7 @@ points = [
     (6, 3),
 ]
 
-# Define a bounding box
+# Define a bounding box / polygon
 polygon = Polygon([
     (2.5, 10),
     (5, 10),
@@ -48,21 +48,39 @@ polygon = Polygon([
 # Initialize the algorithm
 v = Voronoi(polygon)
 
+# Attach a Voronoi Observer that monitors and visualizes the construction of 
+# the Voronoi Diagram step-by-step. See for more information 
+# examples/quickstart.py or examples/observers.py.
+v.attach_observer(VoronoiObserver())
+
 # Create the diagram
-v.create_diagram(points=points, vis_steps=False, verbose=False, vis_result=True, vis_tree=True)
+v.create_diagram(points=points)
 
-# Get properties
+# Get properties. See more examples in examples/quickstart.py
 edges = v.edges
 vertices = v.vertices
 arcs = v.arcs
 points = v.points
+
+# Plotting
+# Note: plot_border_to_site() indicates with dashed line to which site a border 
+# belongs. The site's first edge is colored green.
+Visualizer(voronoi, canvas_offset=1)\
+    .plot_sites(show_labels=True)\
+    .plot_edges(show_labels=False)\
+    .plot_vertices()\
+    .plot_border_to_site()\ 
+    .show()
+
 ```
-![](https://raw.githubusercontent.com/Yatoom/voronoi/master/example.png)
+![image](https://user-images.githubusercontent.com/4205641/111237517-8a609800-85f5-11eb-8095-09001dd7b00e.png)
+
+
 
 ### Calculate the shell size for each point
 ```python
-for point in v.points:
-    print(f"{(point.x, point.y)} \t {point.cell_size()}")
+for point in v.sites:
+    print(f"{point.xy} \t {point.area()}")
 ```
 Output:
 ```
@@ -77,9 +95,11 @@ Output:
 
 More examples can be found in the `voronoi/examples` folder.
 
-### Get coordinates for a point
+### Get coordinates of the cell borders for a point
 ```python
-v.points[0].get_coordinates()
+vertices = v.sites[0].get_vertices()
+coords = [(vertex.x, vertex.y) for vertex in vertices]
+print(coords)
 ```
 Output:
 ```python
diff --git a/example.png b/example.png
deleted file mode 100644
index dcebef1..0000000
Binary files a/example.png and /dev/null differ
diff --git a/example.py b/example.py
deleted file mode 100644
index 78b3e8f..0000000
--- a/example.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# Polygon
-from voronoi.algorithm import Algorithm
-
-from voronoi import BoundingBox
-
-polygon = BoundingBox(-10, 10, -10, 10)
-
-# Points
-points = [
-    (0, 5),
-    (5, 0),
-    (-5, -5)
-]
-
-v = Algorithm(polygon)
-v.create_diagram(points=points, vis_steps=True, verbose=True, vis_result=True, vis_tree=True, vis_before_clipping=True)
\ No newline at end of file
diff --git a/examples/bounding_circle.py b/examples/bounding_circle.py
new file mode 100644
index 0000000..bb4efe2
--- /dev/null
+++ b/examples/bounding_circle.py
@@ -0,0 +1,33 @@
+from voronoi import Voronoi, Visualizer
+from voronoi.contrib import BoundingCircle
+
+# Define a set of points
+points = [
+    (1, 2),
+    (7, 13),
+    (12, 6),
+    (5, 5),
+]
+
+# Define a bounding circle
+bounding_circle = BoundingCircle(5., 5., 9.)
+
+v = Voronoi(bounding_circle)
+
+v.create_diagram(
+    points=points,
+)
+
+edges = v.edges
+vertices = v.vertices
+arcs = v.arcs
+points = v.sites
+
+# Plotting
+Visualizer(v, canvas_offset=1) \
+    .plot_polygon()\
+    .plot_sites(points, show_labels=True) \
+    .plot_edges(edges, show_labels=False) \
+    .plot_vertices(vertices) \
+    .plot_border_to_site() \
+    .show()
diff --git a/examples/bounding_polygon.py b/examples/bounding_polygon.py
new file mode 100644
index 0000000..c276fd6
--- /dev/null
+++ b/examples/bounding_polygon.py
@@ -0,0 +1,71 @@
+import os
+import matplotlib.pyplot as plt
+
+from voronoi import Voronoi, Polygon, Visualizer, TreeVisualizer, VoronoiObserver, TreeObserver
+
+# Define some points (a.k.a sites or cell points)
+points = [
+    (2.5, 2.5), (4, 7.5), (7.5, 2.5), (6, 7.5), (4, 4), (3, 3), (6, 3)
+]
+
+# Define a bounding box / polygon
+polygon = Polygon([
+    (2.5, 10), (5, 10), (10, 5), (10, 2.5), (5, 0), (2.5, 0), (0, 2.5), (0, 5)
+])
+
+# Initialize the algorithm
+v = Voronoi(polygon)
+
+# Optional: attach observer that visualizes Voronoi diagram every step
+v.attach_observer(
+    VoronoiObserver(
+
+        # Settings to put into the visualizer
+        settings=dict(polygon=True, edges=True, vertices=True, sites=True,
+                      outgoing_edges=False, border_to_site=False, scale=1,
+                      edge_labels=False, site_labels=False, triangles=False, arcs=False),
+
+        # Callback that saves the figure every step
+        callback=lambda observer, figure: figure.savefig(f"output/voronoi/{observer.n_messages:02d}.png")
+    )
+)
+
+# Optional: attach observer that visualizes the tree every step
+v.attach_observer(
+    TreeObserver(
+        # Callback that saves the figure every step
+        callback=lambda observer, dot: dot.render(f"output/tree/{observer.n_messages:02d}")
+    )
+)
+
+# Create the output directory if it doesn't exist
+if not os.path.exists("output"):
+    os.mkdir("output")
+
+if not os.path.exists("output/tree/"):
+    os.mkdir("output/tree/")
+
+if not os.path.exists("output/voronoi/"):
+    os.mkdir("output/voronoi/")
+
+# Create the Voronoi diagram
+v.create_diagram(points=points)
+
+# Get properties
+edges = v.edges
+vertices = v.vertices
+arcs = v.arcs
+points = v.sites
+
+# Plotting
+Visualizer(v, canvas_offset=1) \
+    .plot_sites(points, show_labels=True) \
+    .plot_edges(edges, show_labels=False) \
+    .plot_vertices(vertices) \
+    .plot_border_to_site() \
+    .show()
+
+# Visualize the tree
+TreeVisualizer() \
+    .plot(v.beach_line) \
+    .render("output/tree.dot", view=True)
diff --git a/examples/observers.py b/examples/observers.py
new file mode 100644
index 0000000..f9c0b75
--- /dev/null
+++ b/examples/observers.py
@@ -0,0 +1,63 @@
+import os
+
+from voronoi import Polygon, Voronoi, VoronoiObserver, TreeObserver, DebugObserver
+from voronoi.visualization import Presets
+
+# Define some points (a.k.a sites or cell points)
+points = [
+    (2.5, 2.5), (4, 7.5), (7.5, 2.5), (6, 7.5), (4, 4), (3, 3), (6, 3)
+]
+
+# Define a bounding box / polygon
+polygon = Polygon([
+    (2.5, 10), (5, 10), (10, 5), (10, 2.5), (5, 0), (2.5, 0), (0, 2.5), (0, 5)
+])
+
+# Initialize the algorithm
+v = Voronoi(polygon)
+
+# Attach a Voronoi observer that visualizes the Voronoi diagram every step
+v.attach_observer(
+    VoronoiObserver(
+
+        # Settings to pass into the visualizer's plot_all() method.
+        # - By default, the observer uses a set of minimalistic presets that are useful for visualizing during
+        #   construction, clipping and the final result. Have a look at Presets.construction, Presets.clipping and
+        #   Presets.final.
+        # - These settings below will update the default presets used by the observer. For example, by default,
+        #   the arc_labels are not shown, but below we can enable the arc labels. Other parameters can be found in
+        #   the visualizer's plot_all() method.
+        settings=dict(arc_labels=True, site_labels=True),
+
+        # Callback that saves the figure every step
+        # If no callback is provided, it will simply display the figure in a matplotlib window
+        callback=lambda observer, figure: figure.savefig(f"output/voronoi/{observer.n_messages:02d}.png")
+    )
+)
+
+# Attach observer that visualizes the tree every step. This is a binary tree data structure that keeps track of where
+# the arcs and the breakpoints between the arcs are going.
+v.attach_observer(
+    TreeObserver(
+        # Callback that saves the figure every step
+        # If no callback is provided, it will render the figure in a window
+        callback=lambda observer, dot: dot.render(f"output/tree/{observer.n_messages:02d}")
+    )
+)
+
+# Attach a listener that listens to debug messages.
+# If no callback is provided, it will print the messages.
+v.attach_observer(DebugObserver(callback=lambda _: print(_)))
+
+# Create the output directory if it doesn't exist
+if not os.path.exists("output"):
+    os.mkdir("output")
+
+if not os.path.exists("output/tree/"):
+    os.mkdir("output/tree/")
+
+if not os.path.exists("output/voronoi/"):
+    os.mkdir("output/voronoi/")
+
+# Create the Voronoi diagram
+v.create_diagram(points=points)
diff --git a/examples/quickstart.py b/examples/quickstart.py
new file mode 100644
index 0000000..461388d
--- /dev/null
+++ b/examples/quickstart.py
@@ -0,0 +1,61 @@
+from typing import List
+from voronoi import Voronoi, Polygon, Visualizer, Point, VoronoiObserver
+from voronoi.graph import HalfEdge, Vertex
+
+# Define some points (a.k.a sites or cell points)
+points = [
+    (2.5, 2.5), (4, 7.5), (7.5, 2.5), (6, 7.5), (4, 4), (3, 3), (6, 3)
+]
+
+# Define a bounding box / polygon
+polygon = Polygon([
+    (2.5, 10), (5, 10), (10, 5), (10, 2.5), (5, 0), (2.5, 0), (0, 2.5), (0, 5)
+])
+
+# Initialize the algorithm
+v = Voronoi(polygon)
+
+# Optional: visualize the voronoi diagram at every step.
+# You can find more information in the observers.py example file
+# v.attach_observer(
+#     VoronoiObserver()
+# )
+
+# Create the Voronoi diagram
+v.create_diagram(points=points)
+
+# Visualize the Voronoi diagram
+Visualizer(v) \
+    .plot_sites(show_labels=False) \
+    .plot_edges(show_labels=False) \
+    .plot_vertices() \
+    .show()
+
+# Some examples of how to access properties from the Voronoi diagram:
+edges: List[HalfEdge] = v.edges                    # A list of all edges
+vertices: List[Vertex] = v.vertices                # A list of all vertices
+sites: List[Point] = v.sites                       # A list of all cell points (a.k.a. sites)
+
+edge, vertex, site = edges[0], vertices[0], sites[0]
+
+# Edge operations
+origin: Vertex = edge.origin               # The vertex in which the edge originates
+target: Vertex = edge.twin.origin          # The twin is the edge that goes in the other direction
+target_alt: Vertex = edge.target           # Same as above, but more convenient
+twin: HalfEdge = edge.twin                 # Get the twin of this edge
+next_edge: HalfEdge = edge.next            # Get the next edge
+prev_edge: HalfEdge = edge.twin.next       # Get the previous edge
+prev_alt: HalfEdge = edge.prev             # Same as above, but more convenient
+
+# Site operations
+size: float = site.area()                  # The area of the cell
+borders: List[HalfEdge] = site.borders()   # A list of all the borders that surround this cell point
+vertices: List[Vertex] = site.vertices()   # A list of all the vertices around this cell point
+site_x: float = site.x                     # X-coordinate of the site
+site_xy: [float, float] = site.xy          # (x, y)-coordinates of the site
+first_edge: HalfEdge = site.first_edge     # Points to the first edge that is part of the border around the site
+
+# Vertex operations
+connected_edges: List[HalfEdge] = vertex.connected_edges  # A list of all edges that are connected to this vertex
+vertex_x: float = vertex.x                                # x-coordinate of the vertex
+vertex_xy: [float, float] = vertex.xy                     # (x, y)-coordinates of the vertex
diff --git a/examples/random_bounding_triangle.py b/examples/random_bounding_triangle.py
new file mode 100644
index 0000000..1418a2d
--- /dev/null
+++ b/examples/random_bounding_triangle.py
@@ -0,0 +1,76 @@
+import os
+import random
+from voronoi import Voronoi, Polygon, Point, VoronoiObserver
+
+# Width and height of the triangle
+width = 100
+height = 100
+
+# Number of points to generate
+n_points = 10
+
+# Print the randomly generated input points
+print_input = True
+
+# Create the triangle
+triangle = Polygon([
+    (0, height),
+    (width, height),
+    (width / 2, 0)
+])
+
+# Generate the points
+points = []
+while len(points) < n_points:
+    p = Point(random.randint(0, width), random.randint(0, height))
+
+    # Check if the point is inside the triangle
+    if triangle.inside(p):
+        points.append(p)
+
+# Print the input points
+if print_input:
+    print("points = [")
+    for point in points:
+        print(f"    Point({point.xd}, {point.yd}),")
+    print("]")
+
+# Example:
+# points = [
+#     Point(27, 89),
+#     Point(15, 95),
+#     Point(49, 8),
+#     Point(79, 63),
+#     Point(54, 12),
+#     Point(77, 92),
+#     Point(62, 82),
+#     Point(83, 71),
+#     Point(58, 33),
+#     Point(53, 59),
+# ]
+
+# Initialize the algorithm
+v = Voronoi(triangle)
+
+# Optional: attach observer that visualizes Voronoi diagram every step
+v.attach_observer(
+    VoronoiObserver(
+
+        # Settings to put into the visualizer
+        settings=dict(polygon=True, edges=True, vertices=True, sites=True,
+                      outgoing_edges=False, border_to_site=False, scale=1,
+                      edge_labels=False, site_labels=False, triangles=False, arcs=False),
+
+        # Callback that saves the figure every step
+        callback=lambda observer, figure: figure.savefig(f"output/{observer.n_messages:02d}.png")
+    )
+)
+
+# Make the output directory
+if not os.path.exists("output"):
+    os.mkdir("output")
+
+# Start the procedure
+v.create_diagram(
+    points=[(p.xd, p.yd) for p in points],
+)
diff --git a/setup.py b/setup.py
index c0832b2..da1f22e 100644
--- a/setup.py
+++ b/setup.py
@@ -1,7 +1,17 @@
 from setuptools import setup
 setup(
   name='voronoi',
-  packages=['voronoi', 'voronoi.visualization', 'voronoi.tree', 'voronoi.nodes', "voronoi.graph", "voronoi.events", "voronoi.tests"],
+  packages=[
+    'voronoi',
+    'voronoi.visualization',
+    'voronoi.contrib',
+    'voronoi.observers',
+    'voronoi.tree',
+    'voronoi.nodes',
+    "voronoi.graph",
+    "voronoi.events",
+    "voronoi.tests"
+  ],
   version='0.9',
   description="Fortune's algorithm for Voronoi diagrams with polygon bounding boxes.",
   author='Jeroen van Hoof',
@@ -11,6 +21,6 @@
   keywords=['voronoi', 'polygon', 'fortune', 'algorithm'],
   classifiers=[],
   install_requires=[
-    "numpy", "matplotlib",
+    "numpy", "matplotlib", "graphviz"
   ]
 )
\ No newline at end of file
diff --git a/triangle.gif b/triangle.gif
deleted file mode 100644
index e35f176..0000000
Binary files a/triangle.gif and /dev/null differ
diff --git a/voronoi.gif b/voronoi.gif
new file mode 100644
index 0000000..37a9c3d
Binary files /dev/null and b/voronoi.gif differ
diff --git a/voronoi/__init__.py b/voronoi/__init__.py
index fd886eb..24ed1d7 100644
--- a/voronoi/__init__.py
+++ b/voronoi/__init__.py
@@ -1,2 +1,10 @@
 from voronoi.algorithm import Algorithm as Voronoi
-from voronoi.graph import DecimalCoordinate, Polygon, BoundingBox, Point
+from voronoi.graph.coordinate import Coordinate
+from voronoi.graph.bounding_box import BoundingBox
+from voronoi.graph.point import Point
+from voronoi.graph.polygon import Polygon
+from voronoi.visualization import Visualizer
+from voronoi.observers.tree_observer import TreeObserver
+from voronoi.observers.debug_observer import DebugObserver
+from voronoi.observers.voronoi_observer import VoronoiObserver
+from voronoi.visualization.tree_visualizer import TreeVisualizer
\ No newline at end of file
diff --git a/voronoi/algorithm.py b/voronoi/algorithm.py
index 72c22d8..20b568a 100644
--- a/voronoi/algorithm.py
+++ b/voronoi/algorithm.py
@@ -1,24 +1,33 @@
-import warnings
 from queue import PriorityQueue
 
-from voronoi.graph import Point
-from voronoi.graph import HalfEdge, Vertex, Algebra
-from voronoi.graph.bounding_circle import BoundingCircle
-from voronoi.nodes import LeafNode, Arc, Breakpoint, InternalNode
-from voronoi.events import CircleEvent, SiteEvent
-from voronoi.tree import SmartTree, SmartNode
-from voronoi.visualization import vis
-from voronoi.visualization import Tell
-
-
-class Algorithm:
-    def __init__(self, bounding_poly=None):
+from voronoi.observers.message import Message
+from voronoi.observers.subject import Subject
+from voronoi.graph.point import Point
+from voronoi.graph.half_edge import HalfEdge
+from voronoi.graph.vertex import Vertex
+from voronoi.graph.algebra import Algebra
+from voronoi.graph.polygon import Polygon
+from voronoi.nodes.leaf_node import LeafNode
+from voronoi.nodes.arc import Arc
+from voronoi.nodes.breakpoint import Breakpoint
+from voronoi.nodes.internal_node import InternalNode
+from voronoi.events.circle_event import CircleEvent
+from voronoi.events.site_event import SiteEvent
+from voronoi.tree.smart_node import SmartNode
+from voronoi.tree.smart_tree import SmartTree
+
+
+class Algorithm(Subject):
+    def __init__(self, bounding_poly: Polygon = None, remove_zero_length_edges=True):
+        super().__init__()
 
         # The bounding box around the edge
-        self.bounding_poly = bounding_poly
+        self.bounding_poly: Polygon = bounding_poly
+        self.bounding_poly.inherit_observers_from(self)
 
         # Event queue for upcoming site and circle events
         self.event_queue = PriorityQueue()
+        self.event = None
 
         # Root of beach line
         self.beach_line: SmartNode = None
@@ -33,7 +42,7 @@ def __init__(self, bounding_poly=None):
         self.arcs = []
 
         # Store points for visualization
-        self.points = None
+        self.sites = None
 
         # Half edges for visualization
         self.edges = []
@@ -41,10 +50,13 @@ def __init__(self, bounding_poly=None):
         # List of vertices
         self.vertices = []
 
+        # Whether to remove zero length edges
+        self.remove_zero_length_edges = remove_zero_length_edges
+
     def initialize(self, points):
 
         # Store the points for visualization
-        self.points = points
+        self.sites = points
 
         # Initialize event queue with all site events.
         for index, point in enumerate(points):
@@ -54,17 +66,11 @@ def initialize(self, points):
 
         return self.event_queue
 
-    def create_diagram(self, points: list, vis_steps=False, vis_result=False, vis_tree=False,
-                       vis_before_clipping=False, verbose=False):
+    def create_diagram(self, points: list):
         """
         Create the Voronoi diagram.
 
         :param points: (list) The list of cell points to make the diagram for
-        :param vis_steps: (bool) Visualize intermediate steps
-        :param vis_result: (bool) Visualize the final result
-        :param vis_tree: (bool) Visualize the status of the binary tree (text-based)
-        :param vis_before_clipping: Visualize the intermediate final result before clipping
-        :param verbose: (bool) Print debug information
         """
 
         points = [Point(x, y) for x, y in points]
@@ -78,8 +84,6 @@ def create_diagram(self, points: list, vis_steps=False, vis_result=False, vis_tr
 
         while not self.event_queue.empty():
 
-            Tell.print_queue(self.event_queue, verbose)
-
             # Pop the event queue with the highest priority
             event = self.event_queue.get()
 
@@ -89,26 +93,16 @@ def create_diagram(self, points: list, vis_steps=False, vis_result=False, vis_tr
             # Handle circle events
             if isinstance(event, CircleEvent) and event.is_valid:
                 # Update sweep line position
-                self.sweep_line = event.y
+                self.sweep_line = event.yd
 
                 # Debugging
-                Tell.print(
-                    verbose,
-                    f"-> Handle circle event at {event.y} with center {event.center} and arcs {event.point_triple}"
+                self.notify_observers(
+                    Message.DEBUG,
+                    payload=f"# Handle circle event at {event.yd:.3f} with center= {event.center} and arcs= {event.point_triple}"
                 )
 
                 # Handle the event
-                self.handle_circle_event(event, verbose=verbose)
-
-                # Visualization
-                if vis_steps and vis_tree:
-                    self.beach_line.visualize()
-
-                if vis_steps:
-                    vis.visualize(self.sweep_line, current_event=event, bounding_poly=self.bounding_poly,
-                                  points=self.points, vertices=self.vertices, edges=self.edges, arc_list=self.arcs,
-                                  event_queue=self.event_queue)
-
+                self.handle_circle_event(event)
 
             # Handle site events
             elif isinstance(event, SiteEvent):
@@ -118,44 +112,40 @@ def create_diagram(self, points: list, vis_steps=False, vis_result=False, vis_tr
                 index += 1
 
                 # Update sweep line position
-                self.sweep_line = event.y
+                self.sweep_line = event.yd
 
                 # Debugging
-                Tell.print(verbose, f"-> Handle site event at {event.y} with point {event.point}")
+                self.notify_observers(
+                    Message.DEBUG,
+                    payload=f"# Handle site event at y={event.yd:.3f} with point {event.point}"
+                )
 
                 # Handle the event
-                self.handle_site_event(event, verbose=verbose)
+                self.handle_site_event(event)
+            else:
+                # Skip the step if circle event is no longer valid
+                continue
 
-                # Visualization
-                if vis_steps and vis_tree:
-                    self.beach_line.visualize()
+            self.event = event
+            self.notify_observers(Message.STEP_FINISHED)
 
-                if vis_steps:
-                    vis.visualize(y=self.sweep_line, current_event=event, bounding_poly=self.bounding_poly,
-                                  points=self.points, vertices=self.vertices, edges=self.edges, arc_list=self.arcs,
-                                  event_queue=self.event_queue)
-
-        if vis_before_clipping:
-            vis.visualize(y=-1000, current_event="Before clipping", bounding_poly=self.bounding_poly,
-                          points=self.points, vertices=self.vertices, edges=self.edges, arc_list=self.arcs,
-                          event_queue=self.event_queue)
+        self.notify_observers(Message.DEBUG, payload="# Sweep finished")
+        self.notify_observers(Message.SWEEP_FINISHED)
 
         # Finish with the bounding box
         self.edges, polygon_vertices = self.bounding_poly.finish_edges(
-            edges=self.edges, verbose=verbose, vertices=self.vertices, points=self.points, event_queue=self.event_queue
+            edges=self.edges, vertices=self.vertices, points=self.sites, event_queue=self.event_queue
         )
-        self.edges, self.vertices = self.bounding_poly.finish_polygon(self.edges, self.vertices, self.points)
+        self.edges, self.vertices = self.bounding_poly.finish_polygon(self.edges, self.vertices, self.sites)
 
-        # Final visualization
-        if vis_result:
-            # Cell size calculation is not supported for bounding circles
-            calc_cell_sizes = not isinstance(self.bounding_poly, BoundingCircle)
+        if self.remove_zero_length_edges:
+            self.clean_up_zero_length_edges()
 
-            vis.visualize(-1000, current_event="Final result", bounding_poly=self.bounding_poly,
-                          points=self.points, vertices=self.vertices, edges=self.edges, arc_list=self.arcs,
-                          event_queue=self.event_queue, calc_cell_sizes=calc_cell_sizes)
+        # Final visualization
+        self.notify_observers(Message.DEBUG, payload="# Voronoi finished")
+        self.notify_observers(Message.VORONOI_FINISHED)
 
-    def handle_site_event(self, event: SiteEvent, verbose=False):
+    def handle_site_event(self, event: SiteEvent):
 
         # Create a new arc
         new_point = event.point
@@ -168,12 +158,12 @@ def handle_site_event(self, event: SiteEvent, verbose=False):
             return
 
         # 2. Search the beach line tree for the arc above the point
-        arc_node_above_point = SmartTree.find_leaf_node(self.beach_line, key=new_point.x, sweep_line=self.sweep_line)
+        arc_node_above_point = SmartTree.find_leaf_node(self.beach_line, key=new_point.xd, sweep_line=self.sweep_line)
         arc_above_point = arc_node_above_point.get_value()
 
         # 3. Remove potential false alarm
         if arc_above_point.circle_event is not None:
-            arc_above_point.circle_event.remove(verbose=verbose)
+            arc_above_point.circle_event.remove()
 
         # 4. Replace leaf with new sub tree that represents the two new intersections on the arc above the point
         #
@@ -238,14 +228,17 @@ def handle_site_event(self, event: SiteEvent, verbose=False):
         node_a, node_b, node_c = root.left.predecessor, root.left, root.right.left
         node_c, node_d, node_e = node_c, root.right.right, root.right.right.successor
 
-        self.check_circles((node_a, node_b, node_c), (node_c, node_d, node_e), verbose)
+        self.check_circles((node_a, node_b, node_c), (node_c, node_d, node_e))
 
         # 7. Rebalance the tree
         self.beach_line = SmartTree.balance_and_propagate(root)
 
-    def handle_circle_event(self, event: CircleEvent, verbose=False):
+    def handle_circle_event(self, event: CircleEvent):
 
         # 1. Delete the leaf γ that represents the disappearing arc α from T.
+        arc = event.arc_pointer.data
+        if arc in self.arcs:
+            self.arcs.remove(arc)
         arc_node: LeafNode = event.arc_pointer
         predecessor = arc_node.predecessor
         successor = arc_node.successor
@@ -262,6 +255,7 @@ def handle_circle_event(self, event: CircleEvent, verbose=False):
         def remove(neighbor_event):
             if neighbor_event is None:
                 return None
+            self.notify_observers(Message.DEBUG, payload=f"Circle event for {neighbor_event.yd} removed.")
             return neighbor_event.remove()
 
         remove(predecessor.get_value().circle_event)
@@ -276,7 +270,7 @@ def remove(neighbor_event):
         # Note: we only create the new edge if the vertex is still inside the bounding box
         # if self.bounding_poly.inside(event.center):
         # Create a vertex
-        v = Vertex(point=convergence_point)
+        v = Vertex(convergence_point.xd, convergence_point.yd)
         self.vertices.append(v)
 
         # Connect the two old edges to the vertex
@@ -294,9 +288,9 @@ def remove(neighbor_event):
         new_edge = HalfEdge(breakpoint_a, origin=v, twin=HalfEdge(breakpoint_b, origin=updated))
 
         # Set previous and next
-        left.edge.twin.set_next(new_edge)    # yellow
+        left.edge.twin.set_next(new_edge)  # yellow
         right.edge.twin.set_next(left.edge)  # orange
-        new_edge.twin.set_next(right.edge)   # blue
+        new_edge.twin.set_next(right.edge)  # blue
 
         # Add to list for visualization
         self.edges.append(new_edge)
@@ -314,28 +308,26 @@ def remove(neighbor_event):
         node_a, node_b, node_c = former_left.predecessor, former_left, former_left.successor
         node_d, node_e, node_f = former_right.predecessor, former_right, former_right.successor
 
-        self.check_circles((node_a, node_b, node_c), (node_d, node_e, node_f), verbose)
+        self.check_circles((node_a, node_b, node_c), (node_d, node_e, node_f))
 
-    def check_circles(self, triple_left, triple_right, verbose=False):
+    def check_circles(self, triple_left, triple_right):
         node_a, node_b, node_c = triple_left
         node_d, node_e, node_f = triple_right
 
-        left_event = CircleEvent.create_circle_event(node_a, node_b, node_c, sweep_line=self.sweep_line,
-                                                     verbose=verbose)
-        right_event = CircleEvent.create_circle_event(node_d, node_e, node_f, sweep_line=self.sweep_line,
-                                                      verbose=verbose)
+        left_event = CircleEvent.create_circle_event(node_a, node_b, node_c, sweep_line=self.sweep_line)
+        right_event = CircleEvent.create_circle_event(node_d, node_e, node_f, sweep_line=self.sweep_line)
 
         # Check if the circles converge
         if left_event:
             if not Algebra.check_clockwise(node_a.data.origin, node_b.data.origin, node_c.data.origin,
                                            left_event.center):
-                Tell.print(verbose, f"Circle {left_event.point_triple} not clockwise.")
+                self.notify_observers(Message.DEBUG, payload=f"Circle {left_event.point_triple} not clockwise.")
                 left_event = None
 
         if right_event:
             if not Algebra.check_clockwise(node_d.data.origin, node_e.data.origin, node_f.data.origin,
                                            right_event.center):
-                Tell.print(verbose, f"Circle {right_event.point_triple} not clockwise.")
+                self.notify_observers(Message.DEBUG, payload=f"Circle {right_event.point_triple} not clockwise.")
                 right_event = None
 
         if left_event is not None:
@@ -347,9 +339,11 @@ def check_circles(self, triple_left, triple_right, verbose=False):
             node_e.data.circle_event = right_event
 
         if left_event is not None:
-            Tell.print(verbose, f"Left circle event created for {left_event.y}. Arcs: {left_event.point_triple}")
+            self.notify_observers(Message.DEBUG,
+                                  payload=f"Left circle event created for {left_event.yd}. Arcs: {left_event.point_triple}")
         if right_event is not None:
-            Tell.print(verbose, f"Right circle event created for {right_event.y}. Arcs: {right_event.point_triple}")
+            self.notify_observers(Message.DEBUG,
+                                  payload=f"Right circle event created for {right_event.yd}. Arcs: {right_event.point_triple}")
 
         return left_event, right_event
 
@@ -413,3 +407,36 @@ def update_breakpoints(root, sweep_line, arc_node, predecessor, successor):
             right = updated
 
         return root, updated, removed, left, right
+
+    def clean_up_zero_length_edges(self):
+        """
+        Removes zero length edges and vertices with the same coordinate
+        that are produced when two site-events happen at the same time.
+        """
+
+        resulting_edges = []
+        for edge in self.edges:
+            start = edge.get_origin()
+            end = edge.twin.get_origin()
+            if start.xd == end.xd and start.yd == end.yd:
+
+                # Combine the vertices
+                v1: Vertex = edge.origin
+                v2: Vertex = edge.twin.origin
+
+                # Move connected edges from v1 to v2
+                for connected in v1.connected_edges:
+                    connected.origin = v2
+                    v1.connected_edges.remove(connected)
+                    v2.connected_edges.append(connected)
+
+                # Remove vertex v1
+                self.vertices.remove(v1)
+
+                # Delete the edge
+                edge.delete()
+                edge.twin.delete()
+
+            else:
+                resulting_edges.append(edge)
+            self.edges = resulting_edges
diff --git a/voronoi/contrib/__init__.py b/voronoi/contrib/__init__.py
new file mode 100644
index 0000000..662c39c
--- /dev/null
+++ b/voronoi/contrib/__init__.py
@@ -0,0 +1,1 @@
+from voronoi.contrib.bounding_circle import BoundingCircle
diff --git a/voronoi/graph/bounding_circle.py b/voronoi/contrib/bounding_circle.py
similarity index 50%
rename from voronoi/graph/bounding_circle.py
rename to voronoi/contrib/bounding_circle.py
index 587f3e4..6f2af16 100644
--- a/voronoi/graph/bounding_circle.py
+++ b/voronoi/contrib/bounding_circle.py
@@ -1,24 +1,30 @@
 import warnings
-from math import sqrt
+from decimal import Decimal
 
-from voronoi.graph import Polygon, Point, DecimalCoordinate, Vertex
-from voronoi.visualization import vis
+from voronoi import Polygon
+from voronoi.algorithm import Algorithm
+from voronoi.graph import Point, Coordinate, Vertex
 
 DEBUG = False
 
+if DEBUG:
+    from voronoi.visualization import Visualizer
+
 
 class BoundingCircle(Polygon):
 
     def __init__(self, x, y, radius):
-        self.x = x
-        self.y = y
-        self.radius = radius
+        self.xd = Decimal(str(x))
+        self.yd = Decimal(str(y))
+        self.radius = Decimal(str(radius))
         self.polygon_vertices = []
-        self.center = DecimalCoordinate(self.x, self.y)
-        self.max_x = self.x + 2 * self.radius
-        self.min_x = self.x - 2 * self.radius
-        self.max_y = self.y + 2 * self.radius
-        self.min_y = self.y - 2 * self.radius
+        self.center = Coordinate(self.xd, self.yd)
+        self.max_x = self.xd + 2 * self.radius
+        self.min_x = self.xd - 2 * self.radius
+        self.max_y = self.yd + 2 * self.radius
+        self.min_y = self.yd - 2 * self.radius
+        self.voronoi = Algorithm(self)  # A dummy for visualization
+        self.voronoi.sweep_line = self.min_y - abs(self.max_y)
 
         # Important warning about visualization
         warnings.warn("""
@@ -31,23 +37,23 @@ def __init__(self, x, y, radius):
         """)
 
     def inside(self, point):
-        return (self.x - point.x) ** 2 + (self.y - point.y) ** 2 < self.radius ** 2
+        return (self.xd - point.xd) ** 2 + (self.yd - point.yd) ** 2 < self.radius ** 2
 
-    def finish_edges(self, edges, verbose=False, vertices=None, points=None, event_queue=None):
+    def finish_edges(self, edges, vertices=None, points=None, event_queue=None):
         resulting_edges = []
         for edge in edges:
             result = True
             A = edge.get_origin(y=-1000)
             B = edge.twin.get_origin(y=-1000)
+
             if DEBUG:
-                if vertices and points and event_queue:
-                    vis.visualize(y=-1000, current_event="nothing",
-                                  bounding_poly=self,
-                                  points=points,
-                                  vertices=vertices + self.polygon_vertices,
-                                  edges=edges, arc_list=[],
-                                  event_queue=event_queue)
-                vis.highlight_edge(-1000, self, edge)
+                Visualizer(self.voronoi, 1) \
+                    .plot_sites(points) \
+                    .plot_vertices(vertices + self.polygon_vertices) \
+                    .plot_edges(edges) \
+                    .plot_edges([edge], color="green") \
+                    .plot_polygon()\
+                    .show()
 
             if A is None:
                 if B is None:
@@ -59,14 +65,13 @@ def finish_edges(self, edges, verbose=False, vertices=None, points=None, event_q
                 resulting_edges.append(edge)
 
             if DEBUG:
-                if vertices and points and event_queue:
-                    vis.visualize(y=-1000, current_event="nothing",
-                                  bounding_poly=self,
-                                  points=points,
-                                  vertices=vertices + self.polygon_vertices,
-                                  edges=edges, arc_list=[],
-                                  event_queue=event_queue)
-                vis.highlight_edge(-1000, self, edge)
+                Visualizer(self.voronoi, 1) \
+                    .plot_sites(points) \
+                    .plot_vertices(vertices + self.polygon_vertices) \
+                    .plot_edges(edges) \
+                    .plot_edges([edge], color="green") \
+                    .plot_polygon() \
+                    .show()
 
             if B is None or not self.inside(B):
                 result = self.trim_edge(edge.twin)
@@ -74,19 +79,25 @@ def finish_edges(self, edges, verbose=False, vertices=None, points=None, event_q
                     resulting_edges.append(edge.twin)
 
             if DEBUG:
-                if vertices and points and event_queue:
-                    vis.visualize(y=-1000, current_event="nothing", bounding_poly=self,
-                                  points=points, vertices=vertices + self.polygon_vertices, edges=edges, arc_list=[],
-                                  event_queue=event_queue)
+                Visualizer(self.voronoi, 1) \
+                    .plot_sites(points) \
+                    .plot_vertices(vertices + self.polygon_vertices) \
+                    .plot_edges(edges) \
+                    .plot_edges([edge], color="green") \
+                    .plot_polygon() \
+                    .show()
 
         # Re-order polygon vertices
         self.polygon_vertices = self.get_ordered_vertices(self.polygon_vertices)
 
         if DEBUG:
-            if vertices and points and event_queue:
-                vis.visualize(y=-1000, current_event="nothing", bounding_poly=self,
-                              points=points, vertices=vertices + self.polygon_vertices,
-                              edges=edges, arc_list=[], event_queue=event_queue)
+            Visualizer(self.voronoi, 1) \
+                .plot_sites(points) \
+                .plot_vertices(vertices + self.polygon_vertices) \
+                .plot_edges(edges) \
+                .plot_polygon() \
+                .show()
+
         return resulting_edges, self.polygon_vertices
 
     def trim_edge(self, edge, twisted=False):
@@ -96,7 +107,7 @@ def trim_edge(self, edge, twisted=False):
         if point is None:
             return False
         # Create vertex
-        v = Vertex(point=point)
+        v = Vertex(point.x, point.y)
         v.connected_edges.append(edge)
         edge.origin = v
         self.polygon_vertices.append(v)
@@ -104,34 +115,37 @@ def trim_edge(self, edge, twisted=False):
         return True
 
     def get_line(self, A, B):
-        if (B.y - A.y) == 0:
-            a = 0
-            b = 1.
-            c = A.y
-        elif (B.x - A.x) == 0:
-            a = 1
-            b = 0
-            c = A.x
+        if (B.yd - A.yd) == 0:
+            a = Decimal("0")
+            b = Decimal("1")
+            c = A.yd
+        elif (B.xd - A.xd) == 0:
+            a = Decimal("1")
+            b = Decimal("0")
+            c = A.xd
         else:
-            a = -(B.y - A.y) / (B.x - A.x)
-            b = 1.
-            c = A.x * a + A.y
+            a = -(B.yd - A.yd) / (B.xd - A.xd)
+            b = Decimal("1")
+            c = A.xd * a + A.yd
 
         return a, b, c
 
     def get_ray(self, edge):
         A = edge.origin.breakpoint[0]
         B = edge.origin.breakpoint[1]
-        center = Point(x=(A.x + B.x) / 2., y=(A.y + B.y) / 2.)
+        center = Point(x=(A.xd + B.xd) / 2, y=(A.yd + B.yd) / 2)
 
         if edge.twin.get_origin() is None:
             ray_start = center
         else:
-            ray_start = edge.twin.origin.point
+            ray_start = edge.twin.origin
         a, b, c = self.get_line(ray_start, center)
 
         if DEBUG:
-            vis.plot_helper_points(A, B, center, ray_start, a, b, c)
+            Visualizer(self.voronoi, 1) \
+                .plot_sites([A, B, center, ray_start, a, b, c]) \
+                .plot_polygon() \
+                .show()
 
         return ray_start, center, a, b, c
 
@@ -139,8 +153,8 @@ def on_line(self, A, B, C):
         def is_on(a, b, c):
             "Return true iff point c intersects the line segment from a to b."
             # (or the degenerate case that all 3 points are coincident)
-            return ((within(a.x, c.x, b.x) if a.x != b.x else
-                     within(a.y, c.y, b.y)))
+            return ((within(a.xd, c.xd, b.xd) if a.xd != b.xd else
+                     within(a.yd, c.yd, b.yd)))
 
         def within(p, q, r):
             "Return true iff q is between p and r (inclusive)."
@@ -155,7 +169,10 @@ def cut_line(self, edge):
 
         point1, point2 = self.cut_circle(a, b, c)
         if DEBUG:
-            vis.plot_points(point1, point2)
+            Visualizer(self.voronoi, 1) \
+                .plot_sites([point1, point2]) \
+                .plot_polygon() \
+                .show()
         if point1 is None:
             return None
         points = []
@@ -168,26 +185,26 @@ def cut_line(self, edge):
         elif len(points) == 1:
             return points[0]
         else:
-            dist_0 = (A.x - points[0].x) ** 2. + (A.y - points[0].y) ** 2.
-            dist_1 = (A.x - points[1].x) ** 2. + (A.y - points[1].y) ** 2.
+            dist_0 = (A.xd - points[0].xd) ** 2 + (A.yd - points[0].yd) ** 2
+            dist_1 = (A.xd - points[1].xd) ** 2 + (A.yd - points[1].yd) ** 2
             if dist_0 < dist_1:
                 return points[0]
             else:
                 return points[1]
 
     def cut_circle(self, a, b, c):
-        d = c - a * self.x - b * self.y
+        d = c - a * self.xd - b * self.yd
         a_sq_b_sq = a ** 2 + b ** 2
         try:
-            big_sqrt = sqrt(self.radius ** 2 * a_sq_b_sq - d ** 2).real
+            big_sqrt = Decimal.sqrt(self.radius ** 2 * a_sq_b_sq - d ** 2).real
         except ValueError:
             return None, None
 
-        x1 = self.x + (a * d + b * big_sqrt) / a_sq_b_sq
-        y1 = self.y + (b * d - a * big_sqrt) / a_sq_b_sq
+        x1 = self.xd + (a * d + b * big_sqrt) / a_sq_b_sq
+        y1 = self.yd + (b * d - a * big_sqrt) / a_sq_b_sq
         point1 = Point(x=x1, y=y1)
-        x2 = self.x + (a * d - b * big_sqrt) / a_sq_b_sq
-        y2 = self.y + (b * d + a * big_sqrt) / a_sq_b_sq
+        x2 = self.xd + (a * d - b * big_sqrt) / a_sq_b_sq
+        y2 = self.yd + (b * d + a * big_sqrt) / a_sq_b_sq
         point2 = Point(x=x2, y=y2)
 
         return point1, point2
\ No newline at end of file
diff --git a/voronoi/events/circle_event.py b/voronoi/events/circle_event.py
index 4d6731e..cb53e56 100644
--- a/voronoi/events/circle_event.py
+++ b/voronoi/events/circle_event.py
@@ -2,16 +2,15 @@
 from decimal import *
 
 from voronoi.events.event import Event
-from voronoi.graph.coordinate import DecimalCoordinate
+from voronoi.graph.coordinate import Coordinate
 from voronoi.nodes.leaf_node import LeafNode
 from voronoi.nodes.arc import Arc
-from voronoi.visualization import Tell
 
 
 class CircleEvent(Event):
     circle_event = True
 
-    def __init__(self, center: DecimalCoordinate, radius: Decimal, arc_node: LeafNode, point_triple=None, arc_triple=None):
+    def __init__(self, center: Coordinate, radius: Decimal, arc_node: LeafNode, point_triple=None, arc_triple=None):
         """
         Circle event.
 
@@ -26,40 +25,35 @@ def __init__(self, center: DecimalCoordinate, radius: Decimal, arc_node: LeafNod
         self.arc_triple = arc_triple
 
     def __repr__(self):
-        # return f"CircleEvent({self.point_triple}, {round(self.center.y - self.radius, 3)})"
-        return f"CircleEvent({self.point_triple}, y-radius={self.center.y - self.radius}, y={self.center.y}, radius={self.radius})"
+        return f"CircleEvent({self.point_triple}, y-radius={self.center.yd - self.radius:.2f}, y={self.center.yd:.2f}, radius={self.radius:.2f})"
 
     @property
-    def x(self):
-        return self.center.x
+    def xd(self):
+        return self.center.xd
 
     @property
-    def y(self):
-        return self.center.y - self.radius
+    def yd(self):
+        return self.center.yd - self.radius
 
     def get_triangle(self):
         return (
-            (self.point_triple[0].x, self.point_triple[0].y),
-            (self.point_triple[1].x, self.point_triple[1].y),
-            (self.point_triple[2].x, self.point_triple[2].y),
+            (self.point_triple[0].xd, self.point_triple[0].yd),
+            (self.point_triple[1].xd, self.point_triple[1].yd),
+            (self.point_triple[2].xd, self.point_triple[2].yd),
         )
 
-    def remove(self, verbose=False):
-        if verbose:
-            Tell.print(verbose, f"Circle event for {self.y} removed.")
+    def remove(self):
         self.is_valid = False
         return self
 
     @staticmethod
-    def create_circle_event(left_node: LeafNode, middle_node: LeafNode, right_node: LeafNode, sweep_line,
-                            verbose=False) -> "CircleEvent":
+    def create_circle_event(left_node: LeafNode, middle_node: LeafNode, right_node: LeafNode, sweep_line) -> "CircleEvent":
         """
         Checks if the breakpoints converge, and inserts circle event if required.
         :param sweep_line: Y-coordinate of the sweep line
         :param left_node: The node that represents the arc on the left
         :param middle_node: The node that represents the arc on the middle
         :param right_node: The node that represents the arc on the right
-        :param verbose: Flag for printing debugging information
         :return: The circle event or None if no circle event needs to be inserted
         """
 
@@ -81,7 +75,7 @@ def create_circle_event(left_node: LeafNode, middle_node: LeafNode, right_node:
             x, y, radius = CircleEvent.create_circle(a, b, c)
 
             # Return circle event
-            return CircleEvent(center=DecimalCoordinate(x, y), radius=radius, arc_node=middle_node, point_triple=(a, b, c),
+            return CircleEvent(center=Coordinate(x, y), radius=radius, arc_node=middle_node, point_triple=(a, b, c),
                                arc_triple=(left_arc, middle_arc, right_arc))
 
         return None
@@ -90,13 +84,13 @@ def create_circle_event(left_node: LeafNode, middle_node: LeafNode, right_node:
     def create_circle(a, b, c):
 
         # Algorithm from O'Rourke 2ed p. 189
-        A = b.x - a.x
-        B = b.y - a.y
-        C = c.x - a.x
-        D = c.y - a.y
-        E = (b.x - a.x) * (a.x + b.x) + (b.y - a.y) * (a.y + b.y)
-        F = (c.x - a.x) * (a.x + c.x) + (c.y - a.y) * (a.y + c.y)
-        G = 2 * ((b.x - a.x) * (c.y - b.y) - (b.y - a.y) * (c.x - b.x))
+        A = b.xd - a.xd
+        B = b.yd - a.yd
+        C = c.xd - a.xd
+        D = c.yd - a.yd
+        E = (b.xd - a.xd) * (a.xd + b.xd) + (b.yd - a.yd) * (a.yd + b.yd)
+        F = (c.xd - a.xd) * (a.xd + c.xd) + (c.yd - a.yd) * (a.yd + c.yd)
+        G = 2 * ((b.xd - a.xd) * (c.yd - b.yd) - (b.yd - a.yd) * (c.xd - b.xd))
 
         if G == 0:
             # Points are all on one line (collinear), so no circle can be made
@@ -106,6 +100,6 @@ def create_circle(a, b, c):
         x = (D * E - B * F) / G
         y = (A * F - C * E) / G
 
-        radius = Decimal.sqrt((a.x - x) ** 2 + (a.y - y) ** 2)
+        radius = Decimal.sqrt((a.xd - x) ** 2 + (a.yd - y) ** 2)
 
         return x, y, radius
diff --git a/voronoi/events/event.py b/voronoi/events/event.py
index c684892..3f932c3 100644
--- a/voronoi/events/event.py
+++ b/voronoi/events/event.py
@@ -2,27 +2,27 @@ class Event:
     circle_event = False
 
     @property
-    def x(self):
+    def xd(self):
         return 0
 
     @property
-    def y(self):
+    def yd(self):
         return 0
 
     def __lt__(self, other):
-        if self.y == other.y and self.x == other.x:
+        if self.yd == other.yd and self.xd == other.xd:
             return self.circle_event and not other.circle_event
 
-        if self.y == other.y:
-            return self.x < other.x
+        if self.yd == other.yd:
+            return self.xd < other.xd
 
         # Switch y axis
-        return self.y > other.y
+        return self.yd > other.yd
 
     def __eq__(self, other):
         if other is None:
             return None
-        return self.y == other.y and self.x == other.x
+        return self.yd == other.yd and self.xd == other.xd
 
     def __ne__(self, other):
         return not self.__eq__(other)
diff --git a/voronoi/events/site_event.py b/voronoi/events/site_event.py
index 7dca3cf..22aef5f 100644
--- a/voronoi/events/site_event.py
+++ b/voronoi/events/site_event.py
@@ -1,24 +1,26 @@
+from voronoi.graph.point import Point
 from voronoi.events.event import Event
-from voronoi.graph.coordinate import DecimalCoordinate
+from voronoi.observers.subject import Subject
 
 
-class SiteEvent(Event):
+class SiteEvent(Event, Subject):
     circle_event = False
 
-    def __init__(self, point: DecimalCoordinate):
+    def __init__(self, point: Point):
         """
         Site event
         :param point:
         """
+        super().__init__()
         self.point = point
 
     @property
-    def x(self):
-        return self.point.x
+    def xd(self):
+        return self.point.xd
 
     @property
-    def y(self):
-        return self.point.y
+    def yd(self):
+        return self.point.yd
 
     def __repr__(self):
-        return f"SiteEvent(x={self.point.x}, y={self.point.y})"
+        return f"SiteEvent(x={self.point.xd}, y={self.point.yd})"
diff --git a/voronoi/examples/bounding_circle.py b/voronoi/examples/bounding_circle.py
deleted file mode 100644
index a7ae06d..0000000
--- a/voronoi/examples/bounding_circle.py
+++ /dev/null
@@ -1,29 +0,0 @@
-from voronoi import Voronoi
-from voronoi.graph.bounding_circle import BoundingCircle
-
-# Define a set of points
-points = [
-    (1, 2),
-    (7, 13),
-    (12, 6),
-    (5, 5),
-]
-
-# Define a bounding circle
-bounding_circle = BoundingCircle(5., 5., 9.)
-
-v = Voronoi(bounding_circle)
-
-v.create_diagram(
-    points=points,
-    vis_steps=True,            # Visualize intermediate steps
-    vis_result=True,           # Visualize the final result
-    vis_tree=True,             # Print the binary tree at each step
-    vis_before_clipping=True,  # Visualize the intermediate final result before clipping
-    verbose=True               # Print the event queue and events that handled at each step
-)
-
-edges = v.edges
-vertices = v.vertices
-arcs = v.arcs
-points = v.points
diff --git a/voronoi/examples/bounding_polygon.py b/voronoi/examples/bounding_polygon.py
deleted file mode 100644
index ef30d38..0000000
--- a/voronoi/examples/bounding_polygon.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from voronoi import Voronoi, Polygon
-
-# Define a set of points
-points = [
-    (2.5, 2.5),
-    (4, 7.5),
-    (7.5, 2.5),
-    (6, 7.5),
-    (4, 4),
-    (3, 3),
-    (6, 3),
-]
-
-# Define a bounding box
-polygon = Polygon([
-    (2.5, 10),
-    (5, 10),
-    (10, 5),
-    (10, 2.5),
-    (5, 0),
-    (2.5, 0),
-    (0, 2.5),
-    (0, 5),
-])
-
-# Initialize the algorithm
-v = Voronoi(polygon)
-
-# Create the diagram
-v.create_diagram(
-    points=points,
-    vis_steps=True,            # Visualize intermediate steps
-    vis_result=True,           # Visualize the final result
-    vis_tree=True,             # Print the binary tree at each step
-    vis_before_clipping=True,  # Visualize the intermediate final result before clipping
-    verbose=True               # Print the event queue and events that handled at each step
-)
-
-# Get properties
-edges = v.edges
-vertices = v.vertices
-arcs = v.arcs
-points = v.points
-
-# Calculate the sell size for each  point
-for point in v.points:
-    print(f"{(point.x, point.y)} \t {point.cell_size()}")
-
-# for point in v.points:
-#     print([(round(p.x, 2), round(p.y, 2)) for p in point.get_coordinates()])
-
-print(v.points[0].get_coordinates())
\ No newline at end of file
diff --git a/voronoi/examples/random_bounding_triangle.py b/voronoi/examples/random_bounding_triangle.py
deleted file mode 100644
index 8ce0ae3..0000000
--- a/voronoi/examples/random_bounding_triangle.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import random
-
-from voronoi.algorithm import Algorithm
-from voronoi.graph import Polygon, Point
-
-x = 100
-y = 100
-n = 10
-print_input = True
-
-polygon_points = [
-    (0, y),
-    (x, y),
-    (x / 2, 0)
-]
-
-polygon = Polygon(polygon_points)
-
-points = []
-while len(points) < n:
-    p = Point(random.randint(0, x), random.randint(0, y))
-    if polygon.inside(p):
-        points.append(p)
-
-if print_input:
-    print("points = [")
-    for point in points:
-        print(f"    Point({point.x}, {point.y}),")
-    print("]")
-
-v = Algorithm(polygon)
-v.create_diagram(
-    points=[(p.x, p.y) for p in points],
-    vis_steps=True,            # Visualize intermediate steps
-    vis_result=True,           # Visualize the final result
-    vis_tree=True,             # Print the binary tree at each step
-    vis_before_clipping=True,  # Visualize the intermediate final result before clipping
-    verbose=True               # Print the event queue and events that handled at each step
-)
\ No newline at end of file
diff --git a/voronoi/graph/__init__.py b/voronoi/graph/__init__.py
index a2d600d..a62ab8c 100644
--- a/voronoi/graph/__init__.py
+++ b/voronoi/graph/__init__.py
@@ -1,5 +1,5 @@
 from voronoi.graph.half_edge import HalfEdge
-from voronoi.graph.coordinate import DecimalCoordinate
+from voronoi.graph.coordinate import Coordinate
 from voronoi.graph.point import Point
 from voronoi.graph.vertex import Vertex
 from voronoi.graph.polygon import Polygon
diff --git a/voronoi/graph/algebra.py b/voronoi/graph/algebra.py
index 3bc81fe..0d2d184 100644
--- a/voronoi/graph/algebra.py
+++ b/voronoi/graph/algebra.py
@@ -1,14 +1,14 @@
 import numpy as np
-from voronoi.graph import DecimalCoordinate
+from voronoi.graph import Coordinate
 
 
 class Algebra:
     @staticmethod
     def distance(point_a, point_b):
-        x1 = point_a.x
-        x2 = point_b.x
-        y1 = point_a.y
-        y2 = point_b.y
+        x1 = point_a.xd
+        x2 = point_b.xd
+        y1 = point_a.yd
+        y2 = point_b.yd
 
         return np.sqrt((x2 - x1) ** 2 + (y2 - y1)**2)
 
@@ -48,21 +48,21 @@ def line_ray_intersection_point(ray_orig, ray_end, point_1, point_2):
         return []
 
     @staticmethod
-    def get_intersection(orig: DecimalCoordinate, end: DecimalCoordinate, p1: DecimalCoordinate, p2: DecimalCoordinate):
+    def get_intersection(orig: Coordinate, end: Coordinate, p1: Coordinate, p2: Coordinate):
         if not orig or not end:
             return None
 
-        point = Algebra.line_ray_intersection_point([orig.x, orig.y], [end.x, end.y], [p1.x, p1.y], [p2.x, p2.y])
+        point = Algebra.line_ray_intersection_point([orig.xd, orig.yd], [end.xd, end.yd], [p1.xd, p1.yd], [p2.xd, p2.yd])
 
         if len(point) == 0:
             return None
 
-        return DecimalCoordinate(point[0][0], point[0][1])
+        return Coordinate(point[0][0], point[0][1])
 
     @staticmethod
     def calculate_angle(point, center):
-        dx = point.x - center.x
-        dy = point.y - center.y
+        dx = point.xd - center.xd
+        dy = point.yd - center.yd
         return np.math.degrees(np.math.atan2(dy, dx)) % 360
 
     @staticmethod
diff --git a/voronoi/graph/bounding_box.py b/voronoi/graph/bounding_box.py
index 93c2279..f6fa568 100644
--- a/voronoi/graph/bounding_box.py
+++ b/voronoi/graph/bounding_box.py
@@ -1,4 +1,4 @@
-from voronoi.graph import Polygon, DecimalCoordinate
+from voronoi.graph import Polygon, Coordinate
 
 
 class BoundingBox(Polygon):
diff --git a/voronoi/graph/coordinate.py b/voronoi/graph/coordinate.py
index ac3d111..017f856 100644
--- a/voronoi/graph/coordinate.py
+++ b/voronoi/graph/coordinate.py
@@ -2,50 +2,57 @@
 
 
 class Coordinate:
-    def __repr__(self):
-        return f"Coordinate({round(self.x, 3)}, {round(self.y, 3)})"
-
-
-class DecimalCoordinate(Coordinate):
     def __init__(self, x=None, y=None):
         """
         A point in 2D space.
         :param x: (float) The x-coordinate
         :param y: (float) The y-coordinate
         """
-        self._x: Decimal = Decimal(str(x)) if x is not None else Decimal()
-        self._y: Decimal = Decimal(str(y)) if y is not None else Decimal()
+        self._xd: Decimal = Coordinate.to_dec(x)
+        self._yd: Decimal = Coordinate.to_dec(y)
 
     def __sub__(self, other):
-        return DecimalCoordinate(x=self.x - other.x, y=self.y - other.y)
+        return Coordinate(x=self.xd - other.xd, y=self.yd - other.yd)
 
     def __repr__(self):
-        return f"DecimalCoordinate('{self.x}', '{self.y}')"
+        return f"Coord({self.xd:.2f}, {self.yd:.2f})"
+
+    @staticmethod
+    def to_dec(value):
+        return Decimal(str(value)) if value is not None else None
 
     @property
     def x(self):
-        return self._x
-
-    @x.setter
-    def x(self, value: float):
-        self._x = Decimal(str(value))
+        return float(self._xd)
 
     @property
     def y(self):
-        return self._y
+        return float(self._yd)
+
+    @x.setter
+    def x(self, value):
+        self._xd = Coordinate.to_dec(value)
 
     @y.setter
-    def y(self, value: float):
-        self._y = Decimal(str(value))
+    def y(self, value):
+        self._yd = Coordinate.to_dec(value)
+
+    @property
+    def xy(self):
+        return self.x, self.y
 
-    def as_floats(self):
-        return FloatCoordinate(self.x, self.y)
+    @property
+    def xd(self):
+        return self._xd
 
-    def as_float_tuple(self):
-        return float(self.x), float(self.y)
+    @xd.setter
+    def xd(self, value: float):
+        self._xd = Coordinate.to_dec(value)
 
+    @property
+    def yd(self):
+        return self._yd
 
-class FloatCoordinate(Coordinate):
-    def __init__(self, x, y):
-        self.x = float(x)
-        self.y = float(y)
+    @yd.setter
+    def yd(self, value: float):
+        self._yd = Coordinate.to_dec(value)
diff --git a/voronoi/graph/half_edge.py b/voronoi/graph/half_edge.py
index bc464aa..ec3a6e0 100644
--- a/voronoi/graph/half_edge.py
+++ b/voronoi/graph/half_edge.py
@@ -26,12 +26,22 @@ def __repr__(self):
 
     def set_next(self, next):
         if next:
-            next.prev = self
+            next.prev_edge = self
         self.next = next
 
     def get_origin(self, y=None, max_y=None):
+        """
+        Get the point of origin.
+
+        :param y: Sweep line (only used when the Voronoi diagram is under construction and we need to calculate
+                  where it currently is)
+        :param max_y: Bounding box top for clipping infinite breakpoints
+        :return: The point of origin, or None
+        """
         if isinstance(self.origin, Vertex):
-            return self.origin.point
+            if self.origin.xd is None or self.origin.yd is None:
+                return None
+            return self.origin
 
         if y is not None:
             return self.origin.get_intersection(y, max_y=max_y)
@@ -49,3 +59,34 @@ def twin(self, twin):
             twin._twin = self
 
         self._twin = twin
+
+    @property
+    def target(self):
+        if self.twin is None:
+            return None
+        return self.twin.origin
+
+    def delete(self):
+        """
+        Delete this half edge by pointing the previous edge to the next, and removing it from the origin's
+        connected edges list.
+        """
+
+        # Remove the edge from the vertex' connected edges list
+        if isinstance(self.origin, Vertex):
+            self.origin.connected_edges.remove(self)
+
+        # Link previous edge to next edge
+        if self.prev is not None:
+            self.prev.set_next(self.next)
+
+        # If the incident point had a pointer to this edge, we need to point it to a new one
+        if self.incident_point is not None and self.incident_point.first_edge == self:
+
+            # Incident points should remain the same
+            assert (
+                    self.next is None or self.next.incident_point == self.incident_point
+            ), f"Incident points {self.next.incident_point} and {self.incident_point} do not match"
+
+            # Set the new "first edge" pointer
+            self.incident_point.first_edge = self.next
\ No newline at end of file
diff --git a/voronoi/graph/point.py b/voronoi/graph/point.py
index 64d540c..61ace4f 100644
--- a/voronoi/graph/point.py
+++ b/voronoi/graph/point.py
@@ -1,9 +1,10 @@
 import numpy as np
 
-from voronoi.graph.coordinate import DecimalCoordinate
+from voronoi.graph.vertex import Vertex
+from voronoi.graph.coordinate import Coordinate
 
 
-class Point(DecimalCoordinate):
+class Point(Coordinate):
 
     def __init__(self, x=None, y=None, metadata=None, name=None, first_edge=None):
         """
@@ -26,9 +27,9 @@ def __init__(self, x=None, y=None, metadata=None, name=None, first_edge=None):
     def __repr__(self):
         if self.name is not None:
             return f"P{self.name}"
-        return f"Point({round(self.x, 3)}, {round(self.y, 3)})"
+        return f"Point({self.xd:.2f}, {self.xd:.2f})"
 
-    def cell_size(self, digits=None):
+    def area(self, digits=None):
         """
         Calculate cell size if the point is a site.
         :param digits: (int) number of digits to round to
@@ -39,27 +40,28 @@ def cell_size(self, digits=None):
         if digits is not None:
             return round(self._shoelace(x, y), digits)
 
-        return self._shoelace(x, y)
+        return float(self._shoelace(x, y))
 
-    def get_coordinates(self):
-        """
-        Find the coordinates of the associated cell's vertices.
-        """
-        coordinates = []
+    def borders(self):
+        if self.first_edge is None:
+            return None
         edge = self.first_edge
-        start = True
-        while edge != self.first_edge or start:
-            if edge is None or edge.get_origin() is None:
+        edges = [edge]
+        while edge.next != self.first_edge:
+            if edge.next is None:
                 return None
-
-            coordinates.append(edge.get_origin().as_floats())
             edge = edge.next
-            start = False
+            edges.append(edge)
+        return edges
 
-        return coordinates
+    def vertices(self):
+        borders = self.borders()
+        if borders is None:
+            return None
+        return [border.origin for border in borders if isinstance(border.origin, Vertex)]
 
     def _get_xy(self):
-        coordinates = self.get_coordinates()
+        coordinates = self.vertices()
         if coordinates is None:
             return [], []
         x = [coordinate.x for coordinate in coordinates]
@@ -67,7 +69,7 @@ def _get_xy(self):
         return x, y
 
     def __sub__(self, other):
-        return Point(x=self.x-other.x, y=self.y-other.y)
+        return Point(x=self.xd - other.xd, y=self.yd - other.yd)
 
     @staticmethod
     def _shoelace(x, y):
diff --git a/voronoi/graph/polygon.py b/voronoi/graph/polygon.py
index 90d09cc..7e5bdf2 100644
--- a/voronoi/graph/polygon.py
+++ b/voronoi/graph/polygon.py
@@ -1,34 +1,36 @@
-from voronoi.graph import DecimalCoordinate, Vertex, HalfEdge
+from voronoi.graph import Coordinate, Vertex, HalfEdge
 from voronoi.graph.algebra import Algebra
 import numpy as np
 
-from voronoi.visualization import Tell
+from voronoi.observers.message import Message
+from voronoi.observers.subject import Subject
 
 
-class Polygon:
+class Polygon(Subject):
     def __init__(self, tuples):
-        points = [DecimalCoordinate(x, y) for x, y in tuples]
+        super().__init__()
+        points = [Coordinate(x, y) for x, y in tuples]
         self.points = points
-        min_y = min([p.y for p in self.points])
-        min_x = min([p.x for p in self.points])
-        max_y = max([p.y for p in self.points])
-        max_x = max([p.x for p in self.points])
-        center = DecimalCoordinate((max_x + min_x) / 2, (max_y + min_y) / 2)
+        min_y = min([p.yd for p in self.points])
+        min_x = min([p.xd for p in self.points])
+        max_y = max([p.yd for p in self.points])
+        max_x = max([p.xd for p in self.points])
+        center = Coordinate((max_x + min_x) / 2, (max_y + min_y) / 2)
         self.min_y, self.min_x, self.max_y, self.max_x, self.center = min_y, min_x, max_y, max_x, center
 
         self.points = self.order_points(self.points)
         self.polygon_vertices = []
         for point in self.points:
-            self.polygon_vertices.append(Vertex(point=point))
+            self.polygon_vertices.append(Vertex(point.xd, point.yd))
 
     def order_points(self, points):
         clockwise = sorted(points, key=lambda point: (-180 - Algebra.calculate_angle(point, self.center)) % 360)
         return clockwise
 
     def get_ordered_vertices(self, vertices):
-        vertices = [vertex for vertex in vertices if vertex.position is not None]
+        vertices = [vertex for vertex in vertices if vertex.xd is not None]
         clockwise = sorted(vertices,
-                           key=lambda vertex: (-180 - Algebra.calculate_angle(vertex.position, self.center)) % 360)
+                           key=lambda vertex: (-180 - Algebra.calculate_angle(vertex, self.center)) % 360)
         return clockwise
 
     @staticmethod
@@ -40,7 +42,7 @@ def get_closest_point(position, points):
     def finish_polygon(self, edges, existing_vertices, points):
         vertices = self.get_ordered_vertices(self.polygon_vertices)
         vertices = vertices + [vertices[0]]
-        cell = self.get_closest_point(vertices[0].position, points)
+        cell = self.get_closest_point(vertices[0], points)
         previous_edge = None
         for index in range(0, len(vertices) - 1):
 
@@ -77,14 +79,14 @@ def finish_polygon(self, edges, existing_vertices, points):
             # Set previous edge
             previous_edge = edge
 
-        existing_vertices = [i for i in existing_vertices if self.inside(i.position)]
+        existing_vertices = [i for i in existing_vertices if self.inside(i)]
 
         return edges, vertices + existing_vertices
 
     def get_coordinates(self):
-        return [(i.x, i.y) for i in self.points]
+        return [(i.xd, i.yd) for i in self.points]
 
-    def finish_edges(self, edges, verbose=False, **kwargs):
+    def finish_edges(self, edges, **kwargs):
         resulting_edges = []
         for edge in edges:
 
@@ -97,46 +99,30 @@ def finish_edges(self, edges, verbose=False, **kwargs):
             if edge.get_origin() is not None and edge.twin.get_origin() is not None:
                 resulting_edges.append(edge)
             else:
-                self.delete_edge(edge)
-                self.delete_edge(edge.twin)
-                Tell.print(verbose, f"Edges {edge} and {edge.twin} deleted!")
+                edge.delete()
+                edge.twin.delete()
+                self.notify_observers(Message.DEBUG, payload=f"Edges {edge} and {edge.twin} deleted!")
 
         # Re-order polygon vertices
         self.polygon_vertices = self.get_ordered_vertices(self.polygon_vertices)
 
         return resulting_edges, self.polygon_vertices
 
-    @staticmethod
-    def delete_edge(edge):
-
-        # Link previous edge to next edge
-        if edge.prev is not None:
-            edge.prev.set_next(edge.next)
-
-        # If the incident point had a pointer to this edge, we need to point it to a new one
-        if edge.incident_point.first_edge == edge:
-
-            # Incident points should remain the same
-            assert (
-                edge.next is None or edge.next.incident_point == edge.incident_point
-            ), f"Incident points {edge.next.incident_point} and {edge.incident_point} do not match"
-
-            # Set the new "first edge" pointer
-            edge.incident_point.first_edge = edge.next
-
     def finish_edge(self, edge):
+        # Sweep line position
+        sweep_line = self.min_y - abs(self.max_y)
+
         # Start should be a breakpoint
-        start = edge.get_origin(y=2 * (self.min_y - self.max_y), max_y=self.max_y)
-        # TODO: check if this is correct
+        start = edge.get_origin(y=sweep_line, max_y=self.max_y)
 
         # End should be a vertex
-        end = edge.twin.get_origin(y=self.min_y - self.max_y, max_y=self.max_y)
+        end = edge.twin.get_origin(y=sweep_line, max_y=self.max_y)
 
         # Get point of intersection
         point = self.get_intersection_point(end, start)
 
         # Create vertex
-        v = Vertex(point=point)
+        v = Vertex(point.x, point.y) if point is not None else Vertex(None, None)
         v.connected_edges.append(edge)
         edge.origin = v
         self.polygon_vertices.append(v)
@@ -146,10 +132,10 @@ def finish_edge(self, edge):
     def on_edge(self, point):
         vertices = self.points + self.points[0:1]
         for i in range(0, len(vertices) - 1):
-            dxc = point.x - vertices[i].x
-            dyc = point.y - vertices[i].y
-            dx1 = vertices[i + 1].x - vertices[i].x
-            dy1 = vertices[i + 1].y - vertices[i].y
+            dxc = point.xd - vertices[i].xd
+            dyc = point.yd - vertices[i].yd
+            dx1 = vertices[i + 1].xd - vertices[i].xd
+            dy1 = vertices[i + 1].yd - vertices[i].yd
 
             cross = dxc * dy1 - dyc * dx1
 
@@ -167,16 +153,16 @@ def inside(self, point):
 
         vertices = self.points + self.points[0:1]
 
-        x = point.x
-        y = point.y
+        x = point.xd
+        y = point.yd
         inside = False
 
         for i in range(0, len(vertices) - 1):
             j = i + 1
-            xi = vertices[i].x
-            yi = vertices[i].y
-            xj = vertices[j].x
-            yj = vertices[j].y
+            xi = vertices[i].xd
+            yi = vertices[i].yd
+            xj = vertices[j].xd
+            yj = vertices[j].yd
 
             intersect = ((yi > y) != (yj > y)) and (x < (xj - xi) * (y - yi) / (yj - yi) + xi)
             if intersect:
diff --git a/voronoi/graph/vertex.py b/voronoi/graph/vertex.py
index ae3e6a8..98368cf 100644
--- a/voronoi/graph/vertex.py
+++ b/voronoi/graph/vertex.py
@@ -1,15 +1,11 @@
-class Vertex:
-    def __init__(self, connected_edges=None, point=None):
-        if connected_edges is None:
-            connected_edges = []
+from voronoi.graph.coordinate import Coordinate
 
-        self.connected_edges = connected_edges
 
-        self.point = point
+class Vertex(Coordinate):
+    def __init__(self, x, y, connected_edges=None):
+        super().__init__(x, y)
 
-    def __repr__(self):
-        return f"Vertex({self.point})"
+        self.connected_edges = connected_edges or []
 
-    @property
-    def position(self):
-        return self.point
\ No newline at end of file
+    def __repr__(self):
+        return f"Vertex({self.xd:.2f}, {self.yd:.2f})"
diff --git a/voronoi/nodes/arc.py b/voronoi/nodes/arc.py
index 0f19441..b0c3aa9 100644
--- a/voronoi/nodes/arc.py
+++ b/voronoi/nodes/arc.py
@@ -1,6 +1,6 @@
 from decimal import Decimal
 
-from voronoi.graph.coordinate import DecimalCoordinate
+from voronoi.graph.coordinate import Coordinate
 
 
 class Arc:
@@ -10,7 +10,7 @@ class Arc:
     will disappear, or this circle event has not been detected yet.
     """
 
-    def __init__(self, origin: DecimalCoordinate, circle_event=None):
+    def __init__(self, origin: Coordinate, circle_event=None):
         """
         :param origin: The point that caused the arc
         :param circle_event: The pointer to the circle event in which the arc will disappear
@@ -30,7 +30,7 @@ def get_plot(self, x, sweep_line):
         :return: A list of y-values
         """
         sweep_line = float(sweep_line)
-        i = self.origin.as_floats()
+        i = self.origin
 
         if i.y - sweep_line == 0:
             return None
diff --git a/voronoi/nodes/breakpoint.py b/voronoi/nodes/breakpoint.py
index a59b749..b083eb2 100644
--- a/voronoi/nodes/breakpoint.py
+++ b/voronoi/nodes/breakpoint.py
@@ -1,7 +1,7 @@
 import math
 from decimal import Decimal
 
-from voronoi.graph.coordinate import DecimalCoordinate
+from voronoi.graph.coordinate import Coordinate
 
 
 class Breakpoint:
@@ -33,7 +33,7 @@ def tuple_name(self):
 
     def does_intersect(self):
         i, j = self.breakpoint
-        return not (i.y == j.y and j.x < i.x)
+        return not (i.yd == j.yd and j.xd < i.xd)
 
     def get_intersection(self, l, max_y=None):
         """
@@ -49,31 +49,31 @@ def get_intersection(self, l, max_y=None):
         i, j = self.breakpoint
 
         # Initialize the resulting point
-        result = DecimalCoordinate()
-        p: DecimalCoordinate = i
+        result = Coordinate()
+        p: Coordinate = i
 
         # First we replace some stuff to make it easier
-        a = i.x
-        b = i.y
-        c = j.x
-        d = j.y
+        a = i.xd
+        b = i.yd
+        c = j.xd
+        d = j.yd
         u = 2 * (b - l)
         v = 2 * (d - l)
 
         # Handle the case where the two points have the same y-coordinate (breakpoint is in the middle)
-        if i.y == j.y:
-            result.x = (i.x + j.x) / 2
+        if i.yd == j.yd:
+            result.xd = (i.xd + j.xd) / 2
 
-            if j.x < i.x:
-                result.y = max_y or float('inf')
+            if j.xd < i.xd:
+                result.yd = max_y or float('inf')
                 return result
 
         # Handle cases where one point's y-coordinate is the same as the sweep line
-        elif i.y == l:
-            result.x = i.x
+        elif i.yd == l:
+            result.xd = i.xd
             p = j
-        elif j.y == l:
-            result.x = j.x
+        elif j.yd == l:
+            result.xd = j.xd
         else:
             # We now need to solve for x
             # 1/u * (x**2 - 2*a*x + a**2 + b**2 - l**2) = 1/v * (x**2 - 2*c*x + c**2 + d**2 - l**2)
@@ -81,19 +81,19 @@ def get_intersection(self, l, max_y=None):
             x = -(Decimal.sqrt(
                 v * (a ** 2 * u - 2 * a * c * u + b ** 2 * (u - v) + c ** 2 * u) + d ** 2 * u * (v - u) + l ** 2 * (
                     u - v) ** 2) + a * v - c * u) / (u - v)
-            result.x = x
+            result.xd = x
 
         # We have to re-evaluate this, since the point might have been changed
-        a = p.x
-        b = p.y
-        x = result.x
+        a = p.xd
+        b = p.yd
+        x = result.xd
         u = 2 * (b - l)
 
         # Handle degenerate case where parabolas don't intersect
         if u == 0:
-            result.y = float("inf")
+            result.yd = float("inf")
             return result
 
         # And we put everything back in y
-        result.y = 1 / u * (x ** 2 - 2 * a * x + a ** 2 + b ** 2 - l ** 2)
+        result.yd = 1 / u * (x ** 2 - 2 * a * x + a ** 2 + b ** 2 - l ** 2)
         return result
diff --git a/voronoi/nodes/internal_node.py b/voronoi/nodes/internal_node.py
index d2b00d3..351edf2 100644
--- a/voronoi/nodes/internal_node.py
+++ b/voronoi/nodes/internal_node.py
@@ -9,10 +9,10 @@ def __repr__(self):
         return f"Internal({self.data}, left={self.left}, right={self.right})"
 
     def get_key(self, sweep_line=None):
-        return self.data.get_intersection(sweep_line).x
+        return self.data.get_intersection(sweep_line).xd
 
     def get_value(self, **kwargs):
         return self.data
 
     def get_label(self):
-        return f"{self.data.breakpoint[0].name}{self.data.breakpoint[1].name}"
+        return f"{self.data.breakpoint[0].name},{self.data.breakpoint[1].name}"
diff --git a/voronoi/nodes/leaf_node.py b/voronoi/nodes/leaf_node.py
index b703857..5441a84 100644
--- a/voronoi/nodes/leaf_node.py
+++ b/voronoi/nodes/leaf_node.py
@@ -10,7 +10,7 @@ def __repr__(self):
         return f"Leaf({self.data}, left={self.left}, right={self.right})"
 
     def get_key(self, sweep_line=None):
-        return self.data.origin.x
+        return self.data.origin.xd
 
     def get_value(self, **kwargs):
         return self.data
diff --git a/voronoi/observers/__init__.py b/voronoi/observers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/voronoi/observers/debug_observer.py b/voronoi/observers/debug_observer.py
new file mode 100644
index 0000000..8c36f8f
--- /dev/null
+++ b/voronoi/observers/debug_observer.py
@@ -0,0 +1,14 @@
+from abc import ABC
+
+from voronoi.observers.message import Message
+from voronoi.observers.observer import Observer
+from voronoi.observers.subject import Subject
+
+
+class DebugObserver(Observer, ABC):
+    def __init__(self, callback=None):
+        self.callback = callback or (lambda _: print(_))
+
+    def update(self, subject: Subject, message: Message, **kwargs):
+        if message == Message.DEBUG:
+            self.callback(kwargs['payload'])
diff --git a/voronoi/observers/message.py b/voronoi/observers/message.py
new file mode 100644
index 0000000..def6855
--- /dev/null
+++ b/voronoi/observers/message.py
@@ -0,0 +1,10 @@
+from enum import Enum
+
+
+class Message(Enum):
+    STEP_FINISHED = 0
+    SWEEP_FINISHED = 1
+    VORONOI_FINISHED = 2
+    CIRCLE_EVENT = 3
+    SITE_EVENT = 4
+    DEBUG = 5
\ No newline at end of file
diff --git a/voronoi/observers/observer.py b/voronoi/observers/observer.py
new file mode 100644
index 0000000..2a3559d
--- /dev/null
+++ b/voronoi/observers/observer.py
@@ -0,0 +1,22 @@
+from abc import ABC, abstractmethod
+
+from voronoi.observers.message import Message
+
+
+class Observer(ABC):
+    """
+    The Observer interface declares the update method, used by subjects.
+    """
+
+    @abstractmethod
+    def update(self, subject, message: Message, **kwargs) -> None:
+        """
+        Receive update from subject.
+        """
+        pass
+
+
+
+
+
+
diff --git a/voronoi/observers/subject.py b/voronoi/observers/subject.py
new file mode 100644
index 0000000..4af6a6e
--- /dev/null
+++ b/voronoi/observers/subject.py
@@ -0,0 +1,61 @@
+from voronoi.observers.observer import Observer
+
+
+class Subject:
+    def __init__(self):
+        self._observers = []
+        self._children = []
+        self._root_sender = self
+        self._child_sender = self
+
+    def attach_observer(self, observer: Observer):
+        """
+        Attach an observer to the subject.
+        """
+        self._observers.append(observer)
+        self._update_children()
+        return self
+
+    def detach_observer(self, observer: Observer):
+        """
+        Detach an observer from the subject.
+        """
+        self._observers.remove(observer)
+        self._update_children()
+        return self
+
+    def notify_observers(self, message, **kwargs):
+        """
+        Notify all observers about an event.
+        """
+        for observer in self._observers:
+            observer.update(self._root_sender, message, **kwargs)
+        return self
+
+    def get_observers(self):
+        """
+        Getter for observers
+        """
+        return self._observers
+
+    def inherit_observers_from(self, parent):
+        """
+        Make this subject inherit observers from a parent.
+        Set the sender to self.
+        """
+        parent.add_child(self)
+        self._root_sender = parent
+        self._child_sender = self
+
+    def add_child(self, child):
+        """
+        Add child
+        """
+        self._children.append(child)
+
+    def _update_children(self):
+        """
+        Send the observers to the children
+        """
+        for child in self._children:
+            child._observers = self.get_observers()
diff --git a/voronoi/observers/tree_observer.py b/voronoi/observers/tree_observer.py
new file mode 100644
index 0000000..e908d76
--- /dev/null
+++ b/voronoi/observers/tree_observer.py
@@ -0,0 +1,58 @@
+from abc import ABC
+
+from voronoi.algorithm import Algorithm
+from voronoi.observers.message import Message
+from voronoi.observers.observer import Observer
+import uuid
+from graphviz import Digraph
+
+
+class TreeObserver(Observer, ABC):
+    def __init__(self, visualize_steps=True, visualize_before_clipping=False, visualize_result=True, text_based=False, callback=None):
+        self.text_based = text_based
+        self.visualize_steps = visualize_steps
+        self.visualize_before_clipping = visualize_before_clipping
+        self.visualize_result = visualize_result
+        self.callback = callback
+        self.n_messages = 0
+        self.messages = []
+
+    def update(self, subject: Algorithm, message: Message, **kwargs):
+        if (message == Message.STEP_FINISHED and self.visualize_steps) or \
+           (message == Message.VORONOI_FINISHED and self.visualize_result) or \
+           (message == Message.SWEEP_FINISHED and self.visualize_before_clipping):
+            if self.text_based:
+                visualized_in_text = subject.beach_line.visualize()
+                if self.callback is not None:
+                    self.callback(visualized_in_text)
+                else:
+                    print(visualized_in_text)
+            else:
+                self.visualize(subject.beach_line)
+            self.n_messages += 1
+            self.messages.append(message)
+
+    def visualize(self, node):
+        dot = Digraph(comment='Binary Tree', format="png")
+        self._visualize(node, dot, None)
+
+        if self.callback is not None:
+            self.callback(self, dot)
+        else:
+            dot.render('tree/tree.gv', view=True)
+
+    @staticmethod
+    def _visualize(node, dot, parent_id):
+        id = str(uuid.uuid4())
+        label = node.get_label()
+        dot.node(id, label)
+
+        # Check if parent is None
+        if parent_id is not None:
+            dot.edge(parent_id, id)
+
+        if node.left is not None:
+            TreeObserver._visualize(node.left, dot, id)
+
+        if node.right is not None:
+            TreeObserver._visualize(node.right, dot, id)
diff --git a/voronoi/observers/voronoi_observer.py b/voronoi/observers/voronoi_observer.py
new file mode 100644
index 0000000..489db74
--- /dev/null
+++ b/voronoi/observers/voronoi_observer.py
@@ -0,0 +1,54 @@
+from abc import ABC
+
+from voronoi.algorithm import Algorithm
+from voronoi.observers.message import Message
+from voronoi.observers.observer import Observer
+import matplotlib.pyplot as plt
+
+from voronoi.visualization.visualizer import Visualizer, Presets
+
+
+class VoronoiObserver(Observer, ABC):
+    def __init__(self, visualize_steps=True, visualize_before_clipping=False, visualize_result=True, callback=None,
+                 figsize=(8, 8), canvas_offset=1, settings=None):
+        self.canvas_offset = canvas_offset
+        self.figsize = figsize
+        self.visualize_steps = visualize_steps
+        self.visualize_before_clipping = visualize_before_clipping
+        self.visualize_result = visualize_result
+        self.callback = callback or (lambda a, b: plt.show(block=True))
+        self.n_messages = 0
+        self.messages = []
+        self.settings = settings or {}
+
+    def update(self, subject: Algorithm, message: Message, **kwargs):
+
+        if not isinstance(subject, Algorithm):
+            return False
+
+        if message == Message.STEP_FINISHED and self.visualize_steps:
+            vis = Visualizer(subject, canvas_offset=self.canvas_offset)
+            settings = Presets.construction
+            settings.update(self.settings)
+            assert subject.sweep_line == subject.event.yd
+            result = vis.plot_all(**settings)
+            plt.title(str(subject.event) + "\n")
+        elif message == Message.SWEEP_FINISHED and self.visualize_before_clipping:
+            vis = Visualizer(subject, canvas_offset=self.canvas_offset)
+            settings = Presets.clipping
+            settings.update(self.settings)
+            result = vis.plot_all(**settings)
+            plt.title("Sweep finished\n")
+        elif message == Message.VORONOI_FINISHED and self.visualize_result:
+            vis = Visualizer(subject, canvas_offset=self.canvas_offset)
+            settings = Presets.final
+            settings.update(self.settings)
+            result = vis.plot_all(**settings)
+            plt.title("Voronoi completed\n")
+
+        else:
+            return
+
+        self.callback(self, result.get_canvas())
+        self.n_messages += 1
+        self.messages.append(message)
diff --git a/voronoi/tree/smart_node.py b/voronoi/tree/smart_node.py
index 4195190..5937c54 100644
--- a/voronoi/tree/smart_node.py
+++ b/voronoi/tree/smart_node.py
@@ -227,7 +227,7 @@ def replace_leaf(self, replacement, root):
         return root
 
     def visualize(self):
-        print(self._visualize())
+        return self._visualize()
 
     def _visualize(self, depth=0):
         """
diff --git a/voronoi/visualization/__init__.py b/voronoi/visualization/__init__.py
index e9a4c0c..85b981b 100644
--- a/voronoi/visualization/__init__.py
+++ b/voronoi/visualization/__init__.py
@@ -1,4 +1,2 @@
-from voronoi.visualization.tell import Tell
-from voronoi.visualization.visual import Visualization
-
-vis = Visualization()
+from voronoi.visualization.visualizer import Visualizer
+from voronoi.visualization.visualizer import Presets
\ No newline at end of file
diff --git a/voronoi/visualization/tell.py b/voronoi/visualization/tell.py
deleted file mode 100644
index 8dcf14b..0000000
--- a/voronoi/visualization/tell.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from itertools import groupby
-from pprint import pprint
-
-
-class Tell:
-    @staticmethod
-    def print(verbose, *text, sep=' ', end='\n', file=None):
-        if not verbose:
-            return
-        print(*text, sep=sep, end=end, file=file)
-
-    @staticmethod
-    def print_queue(event_queue, verbose):
-        if not verbose:
-            return
-        print("Printing Queue, grouped by y (warning: expensive calculation!)")
-        r = []
-        while not event_queue.empty():
-            r.append(event_queue.get())
-        for i in r:
-            event_queue.put(i)
-        pprint([list(v) for l, v in groupby(r, lambda item: item.y)])
diff --git a/voronoi/visualization/tree_visualizer.py b/voronoi/visualization/tree_visualizer.py
new file mode 100644
index 0000000..ccbdb34
--- /dev/null
+++ b/voronoi/visualization/tree_visualizer.py
@@ -0,0 +1,26 @@
+import uuid
+from graphviz import Digraph
+
+
+class TreeVisualizer:
+
+    def plot(self, node):
+        dot = Digraph(comment='Binary Tree')
+        self._plot(node, dot, None)
+        return dot
+
+    @staticmethod
+    def _plot(node, dot, parent_id):
+        id = str(uuid.uuid4())
+        label = node.get_label()
+        dot.node(id, label)
+
+        # Check if parent is None
+        if parent_id is not None:
+            dot.edge(parent_id, id)
+
+        if node.left is not None:
+            TreeVisualizer._plot(node.left, dot, id)
+
+        if node.right is not None:
+            TreeVisualizer._plot(node.right, dot, id)
\ No newline at end of file
diff --git a/voronoi/visualization/visual.py b/voronoi/visualization/visual.py
deleted file mode 100644
index 13ec6ed..0000000
--- a/voronoi/visualization/visual.py
+++ /dev/null
@@ -1,230 +0,0 @@
-import math
-import time
-from decimal import Decimal
-
-import matplotlib.patches as patches
-import matplotlib.pyplot as plt
-import numpy as np
-from voronoi.events import CircleEvent
-from voronoi.graph.coordinate import Coordinate, DecimalCoordinate
-
-
-class Colors:
-    SWEEP_LINE = "#636e72"
-    CELL_POINTS = "black"
-    BEACH_LINE = "#636e72"
-    EDGE = "#636e72"
-    ARC = "#b2bec3"
-    INCIDENT_POINT_POINTER = "#FDAB81"
-    INCIDENT_POINT_POINTER_TWIN = "#dfe6e9"
-    INVALID_CIRCLE = "#d63031"  # red
-    VALID_CIRCLE = "#0984e3"  # blue
-    VERTICES = "#0984e3"  # blue
-    TRIANGLE = "#00cec9"  # orange
-    BOUNDING_BOX = "black"  # blue
-    TEXT = "#00cec9"  # green
-    HELPER = "#ff0000"
-    HIGH_LIGHT = "#00ff00"
-    EDGE_DIRECTION = "#fdcb6e"
-
-
-class Settings:
-    plot_arcs = False
-    plot_edge_names = False
-
-
-class Visualization(object):
-
-    def visualize(self, y, current_event, bounding_poly, points, vertices, edges, arc_list, event_queue,
-                  calc_cell_sizes=True):
-        self.fig, self.ax = plt.subplots(figsize=(17, 17))
-        self.ax.set_title(str(current_event))
-        scale = (bounding_poly.max_y - bounding_poly.min_y)
-        border = (bounding_poly.max_y - bounding_poly.min_y) / 4
-        plt.ylim((bounding_poly.min_y - border, bounding_poly.max_y + border))
-        plt.xlim((bounding_poly.min_x - border, bounding_poly.max_x + border))
-
-        # Create 1000 equally spaced points between -10 and 10 and setup plot window
-        x = np.linspace(float(bounding_poly.min_x), float(bounding_poly.max_x), 1000)
-        x_full = np.linspace(float(bounding_poly.min_x) - float(border), float(bounding_poly.max_x + border), 1000)
-
-        # Plot the sweep line
-        self.ax.plot([bounding_poly.min_x - border, bounding_poly.max_x + border], [y, y], color=Colors.SWEEP_LINE)
-
-        # Plot all arcs
-        plot_lines = []
-        for arc in arc_list:
-            plot_line = arc.get_plot(x_full, y)
-            if plot_line is None:
-                if Settings.plot_arcs:
-                    self.ax.axvline(x=arc.origin.x)
-            else:
-                if Settings.plot_arcs:
-                    self.ax.plot(x_full, plot_line, linestyle="--", color=Colors.ARC)
-                plot_lines.append(plot_line)
-
-        # Plot the beach line, i.e. the bottom of all the arcs
-        if len(plot_lines) > 0:
-            self.ax.plot(x_full, np.min(plot_lines, axis=0), color=Colors.BEACH_LINE)
-
-        # Plot half-edges
-        for edge in edges:
-            self.plot_edge(self.ax, edge, y, bounding_poly, print_name=Settings.plot_edge_names)
-            self.plot_edge(self.ax, edge.twin, y, bounding_poly, print_name=False)  # Plot edge name only once
-
-        if isinstance(current_event, CircleEvent):
-            self._plot_circle(current_event, self.ax)
-
-        for event in event_queue.queue:
-            if isinstance(event, CircleEvent):
-                self._plot_circle(event, self.ax)
-
-        if hasattr(bounding_poly, 'radius'):
-            # Draw bounding box
-            self.ax.add_patch(
-                patches.Circle((bounding_poly.x, bounding_poly.x), bounding_poly.radius, fill=False,
-                               edgecolor=Colors.BOUNDING_BOX)
-            )
-        else:
-            # Draw bounding box
-            self.ax.add_patch(
-                patches.Polygon(bounding_poly.get_coordinates(), fill=False, edgecolor=Colors.BOUNDING_BOX)
-            )
-
-        # Plot vertices
-        Visualization.plot_vertices(self.ax, vertices)
-
-        # Plot points
-        for point in points:
-            x, y = point.x, point.y
-            self.ax.scatter(x=[x], y=[y], s=50, color=Colors.CELL_POINTS)
-            text = str(point)
-
-            if calc_cell_sizes:
-                text += f" ({point.cell_size(digits=2)})"
-
-            self.ax.text(s=text, x=x + scale / 100, y=y + scale / 100, color=Colors.TEXT)
-
-        plt.show(block=True)
-
-    @staticmethod
-    def plot_vertices(ax, vertices, **kwargs):
-        for vertex in vertices:
-            x, y = vertex.position.x, vertex.position.y
-            ax.scatter(x=[x], y=[y], s=50, color=Colors.VERTICES, **kwargs)
-
-    # Plot circle events
-    @staticmethod
-    def _plot_circle(evt, ax):
-        x, y = evt.center.x, evt.center.y
-        radius = evt.radius
-        color = Colors.VALID_CIRCLE if evt.is_valid else Colors.INVALID_CIRCLE
-
-        # if evt.is_valid:
-        circle = plt.Circle((x, y), radius, fill=False, color=color, linewidth=1.2)
-        triangle = plt.Polygon(evt.get_triangle(), fill=False, color=Colors.TRIANGLE, linewidth=1.2)
-        ax.add_artist(circle)
-        ax.add_artist(triangle)
-
-    def plot_helper_points(self, A, B, center, start_ray, a, b, c):
-        self.ax.scatter(x=[A.x, B.x, center.x, start_ray.x], y=[A.y, B.y, center.y, start_ray.y], s=50,
-                        color=Colors.HELPER)
-        self.ax.plot(
-            [start_ray.x, center.x], [start_ray.y, (c - a * center.x) / b],
-            color=Colors.HELPER
-        )
-        self.fig.show()
-
-    def plot_points(self, A, B):
-        self.ax.scatter(x=[A.x, B.x], y=[A.y, B.y], s=50, color=Colors.HELPER)
-        self.fig.show()
-
-    def highlight_edge(self, y, bounding_poly, edge):
-        # Get start and end of edges
-        start = edge.get_origin(y, bounding_poly.max_y)
-        end = edge.twin.get_origin(y, bounding_poly.max_y)
-
-        # Draw line
-        if start and end:
-            self.ax.plot([start.x, end.x], [start.y, end.y], Colors.HIGH_LIGHT, linewidth=5)
-
-        # Add arrow
-        if start and end and start.y < float('inf'):
-            self.ax.annotate(
-                s='',
-                xy=(end.x, end.y),
-                xytext=(start.x, start.y),
-                arrowprops=dict(
-                    arrowstyle='->',
-                    linewidth=5,
-                    color=Colors.HIGH_LIGHT
-                )
-            )
-
-        self.fig.show()
-
-    @staticmethod
-    def plot_edge_direction(ax, edge, y, bounding_poly, scale=1, **kwargs):
-
-        scale = Decimal(str(scale))
-
-        if edge.incident_point is None:
-            return
-
-        # Get start and end of edges
-        start = edge.get_origin(y, bounding_poly.max_y)
-        end = edge.twin.get_origin(y, bounding_poly.max_y)
-
-        # Direction vector
-        x_diff = end.x - start.x
-        y_diff = end.y - start.y
-        length = Decimal.sqrt(x_diff ** 2 + y_diff ** 2)
-        if length == 0:
-            return
-        direction = (x_diff / length, y_diff / length)
-        new_end = DecimalCoordinate(start.x + direction[0] * scale, start.y + direction[1] * scale)
-        # ax.plot([start.x, new_end.x], [start.y, new_end.y], Colors.EDGE_DIRECTION, linewidth=5)
-
-        # props = {"arrowstyle": "->", "color": Colors.EDGE_DIRECTION, "linewidth": 3}
-        props = dict(arrowstyle="->", color=Colors.EDGE_DIRECTION, linewidth=2, **kwargs)
-        ax.annotate(text='', xy=(new_end.x, new_end.y), xytext=(start.x, start.y), arrowprops=props)
-
-    @staticmethod
-    def plot_edge(ax, edge, y, bounding_poly, print_name=False, **kwargs):
-
-        if edge.incident_point is None:
-            return
-
-        # Get start and end of edges
-        start = edge.get_origin(y, bounding_poly.max_y)
-        end = edge.twin.get_origin(y, bounding_poly.max_y)
-
-        # Draw line
-        if start and end:
-            ax.plot([start.x, end.x], [start.y, end.y], Colors.EDGE)
-            # Add Name
-            if print_name:
-                plt.annotate(
-                    text=str(edge),
-                    xy=((end.x + start.x) / 2, (end.y + start.y) / 2),
-                    **kwargs
-                )
-
-        # Add arrow
-        if start and end and start.y < float('inf'):
-            plt.annotate(text='', xy=(end.x, end.y), xytext=(start.x, start.y), arrowprops=dict(arrowstyle='->', **kwargs))
-
-        # Point to incident point
-        Visualization.draw_pointer_to_incident_point(ax, edge, start, end, Colors.INCIDENT_POINT_POINTER_TWIN)
-        # self.draw_pointer_to_incident_point(edge.twin, end, start, Colors.INCIDENT_POINT_POINTER_TWIN)
-
-    @staticmethod
-    def draw_pointer_to_incident_point(ax, edge, start, end, color):
-        is_first_edge = edge.incident_point is not None and edge.incident_point.first_edge == edge
-        incident_point = edge.incident_point
-        if start and end and incident_point:
-            ax.plot(
-                [(start.x + end.x) / 2, incident_point.x], [(start.y + end.y) / 2, incident_point.y],
-                color="green" if is_first_edge else color,
-                linestyle="--"
-            )
diff --git a/voronoi/visualization/visualizer.py b/voronoi/visualization/visualizer.py
new file mode 100644
index 0000000..bebfba7
--- /dev/null
+++ b/voronoi/visualization/visualizer.py
@@ -0,0 +1,307 @@
+from copy import copy
+from decimal import Decimal
+
+import numpy as np
+from matplotlib import patches
+
+from voronoi import Coordinate
+from voronoi.algorithm import Algorithm
+from voronoi.events import CircleEvent
+import matplotlib.pyplot as plt
+
+
+class Colors:
+    SWEEP_LINE = "#2c3e50"
+    VERTICES = "#34495e"
+    BEACH_LINE = "#f1c40f"
+    EDGE = "#636e72"
+    ARC = "#95a5a6"
+    INCIDENT_POINT_POINTER = "#ecf0f1"
+    INVALID_CIRCLE = "#ecf0f1"  # red
+    VALID_CIRCLE = "#2980b9"  # blue
+    CELL_POINTS = "#bdc3c7"  # blue
+    TRIANGLE = "#e67e22"  # orange
+    BOUNDING_BOX = "black"  # blue
+    TEXT = "#00cec9"  # green
+    HELPER = "#ff0000"
+    HIGH_LIGHT = "#00ff00"
+    EDGE_DIRECTION = "#fdcb6e"
+    FIRST_EDGE = "#2ecc71"
+
+
+class Presets:
+    # A minimalistic preset that is useful during construction
+    construction = dict(polygon=True, events=True, beach_line=True, sweep_line=True)
+
+    # A minimalistic preset that is useful during clipping
+    clipping = dict(polygon=True)
+
+    # A minimalistic preset that is useful to show the final result
+    final = dict()
+
+
+class Visualizer:
+
+    def __init__(self, voronoi, canvas_offset=1, figsize=(8, 8)):
+        self.voronoi = voronoi
+        self.min_x, self.max_x, self.min_y, self.max_y = self.canvas_size(voronoi.bounding_poly, canvas_offset)
+        plt.close("all")  # Prevents previous created plots from showing up
+        fig, ax = plt.subplots(figsize=figsize)
+        self.canvas = ax
+
+    def set_limits(self):
+        self.canvas.set_ylim(self.min_y, self.max_y)
+        self.canvas.set_xlim(self.min_x, self.max_x)
+        return self
+
+    def get_canvas(self):
+        self.set_limits()
+        return self.canvas.figure
+
+    def show(self, block=True, **kwargs):
+        self.set_limits()
+        plt.show(block=block, **kwargs)
+        return self
+
+    def plot_all(self, polygon=False, edges=True, vertices=True, sites=True,
+                 outgoing_edges=False, border_to_site=False, scale=1,
+                 edge_labels=False, site_labels=False, triangles=False, arcs=False, sweep_line=False, events=False,
+                 arc_labels=False, beach_line=False):
+
+        self.plot_sweep_line() if sweep_line else False
+        self.plot_polygon() if polygon else False
+        self.plot_edges(show_labels=edge_labels) if edges else False
+        self.plot_border_to_site() if border_to_site else False
+        self.plot_vertices() if vertices else False
+        self.plot_sites(show_labels=site_labels) if sites else False
+        self.plot_outgoing_edges(scale=scale) if outgoing_edges else False
+        self.plot_event(triangles) if events else False
+        self.plot_arcs(plot_arcs=arcs, show_labels=arc_labels) if beach_line else False
+        self.set_limits()
+        return self
+
+    def plot_polygon(self):
+        if hasattr(self.voronoi.bounding_poly, 'radius'):
+            # Draw bounding box
+            self.canvas.add_patch(
+                patches.Circle((self.voronoi.bounding_poly.xd, self.voronoi.bounding_poly.xd),
+                               self.voronoi.bounding_poly.radius,
+                               fill=False,
+                               edgecolor=Colors.BOUNDING_BOX)
+            )
+        else:
+            # Draw bounding box
+            self.canvas.add_patch(
+                patches.Polygon(self.voronoi.bounding_poly.get_coordinates(), fill=False, edgecolor=Colors.BOUNDING_BOX)
+            )
+
+        return self
+
+    def plot_vertices(self, vertices=None, **kwargs):
+        vertices = vertices or self.voronoi.vertices
+
+        xs = [vertex.xd for vertex in vertices]
+        ys = [vertex.yd for vertex in vertices]
+
+        # Scatter points
+        self.canvas.scatter(xs, ys, s=50, color=Colors.VERTICES, zorder=10, **kwargs)
+
+        return self
+
+    def plot_outgoing_edges(self, vertices=None, scale=0.5, **kwargs):
+        vertices = vertices or self.voronoi.vertices
+        scale = Decimal(str(scale))
+
+        for vertex in vertices:
+            for edge in vertex.connected_edges:
+                start, end = self._origins(edge, None)
+
+                if start is None or end is None:
+                    continue
+
+                # Direction vector
+                x_diff = end.xd - start.xd
+                y_diff = end.yd - start.yd
+                length = Decimal.sqrt(x_diff ** 2 + y_diff ** 2)
+
+                if length == 0:
+                    continue
+
+                direction = (x_diff / length, y_diff / length)
+                new_end = Coordinate(start.xd + direction[0] * scale, start.yd + direction[1] * scale)
+
+                props = dict(arrowstyle="->", color=Colors.EDGE_DIRECTION, linewidth=3, **kwargs)
+                self.canvas.annotate(text='', xy=(new_end.xd, new_end.yd), xytext=(start.xd, start.yd),
+                                     arrowprops=props)
+
+        return self
+
+    def plot_sites(self, points=None, show_labels=True, color=Colors.CELL_POINTS, zorder=10):
+        points = points or self.voronoi.sites
+
+        xs = [point.xd for point in points]
+        ys = [point.yd for point in points]
+
+        # Scatter points
+        self.canvas.scatter(xs, ys, s=50, color=color, zorder=zorder)
+
+        # Add descriptions
+        if show_labels:
+            for point in points:
+                self.canvas.text(point.xd, point.yd, s=f"P{point.name if point.name is not None else ''}", zorder=15)
+
+        return self
+
+    def plot_edges(self, edges=None, sweep_line=None, show_labels=True, color=Colors.EDGE, **kwargs):
+        edges = edges or self.voronoi.edges
+        sweep_line = sweep_line or self.voronoi.sweep_line
+        for edge in edges:
+            self._plot_edge(edge, sweep_line, show_labels, color)
+            self._plot_edge(edge.twin, sweep_line, print_name=False, color=color)
+
+        return self
+
+    def plot_border_to_site(self, edges=None, sweep_line=None):
+        edges = edges or self.voronoi.edges
+        sweep_line = sweep_line or self.voronoi.sweep_line
+        for edge in edges:
+            self._draw_line_from_edge_midpoint_to_incident_point(edge, sweep_line)
+            self._draw_line_from_edge_midpoint_to_incident_point(edge.twin, sweep_line)
+
+        return self
+
+    def plot_arcs(self, arcs=None, sweep_line=None, plot_arcs=False, show_labels=True):
+        arcs = arcs or self.voronoi.arcs
+        sweep_line = sweep_line or self.voronoi.sweep_line
+
+        # Get axis limits
+        min_x, max_x, min_y, max_y = self.min_x, self.max_x, self.min_y, self.max_y
+        sweep_line = max_y if sweep_line is None else sweep_line
+
+        # Create 1000 equally spaced points
+        x = np.linspace(float(min_x), float(max_x), 1000)
+
+        plot_lines = []
+
+        for arc in arcs:
+            plot_line = arc.get_plot(x, sweep_line)
+
+            if plot_line is None:
+                if plot_arcs:
+                    self.canvas.axvline(x=arc.origin.xd, color=Colors.SWEEP_LINE)
+            else:
+                if plot_arcs:
+                    self.canvas.plot(x, plot_line, linestyle="--", color=Colors.ARC)
+
+                plot_lines.append(plot_line)
+
+        # Plot the bottom of all the arcs
+        if len(plot_lines) > 0:
+            bottom = np.min(plot_lines, axis=0)
+            self.canvas.plot(x, bottom, color=Colors.BEACH_LINE)
+
+            if show_labels:
+                self._plot_arc_labels(x, plot_lines, bottom, sweep_line, arcs)
+
+        return self
+
+    def _plot_arc_labels(self, x, plot_lines, bottom, sweep_line, arcs):
+        indices = np.nanargmin(plot_lines, axis=0)
+        unique_indices = np.unique(indices)
+
+        for index in unique_indices:
+            x_mean = np.nanmedian(x[(indices == index) & (bottom < self.max_y)])
+            y = arcs[index].get_plot(x_mean, sweep_line)
+            self.canvas.text(x_mean, y, s=f"{arcs[index].origin.name}", size=12, color=Colors.VALID_CIRCLE, zorder=15)
+
+        return self
+
+    def plot_sweep_line(self, sweep_line=None):
+        sweep_line = sweep_line or self.voronoi.sweep_line
+
+        # Get axis limits
+        min_x, max_x, min_y, max_y = self.min_x, self.max_x, self.min_y, self.max_y
+
+        self.canvas.plot([min_x, max_x], [sweep_line, sweep_line], color=Colors.SWEEP_LINE)
+
+        return self
+
+    def plot_event(self, event=None, triangles=False):
+        event = event or self.voronoi.event
+        if isinstance(event, CircleEvent):
+            self._plot_circle(event, show_triangle=triangles)
+
+        return self
+
+    def _plot_circle(self, evt, show_triangle=False):
+        x, y = evt.center.xd, evt.center.yd
+        radius = evt.radius
+        color = Colors.VALID_CIRCLE if evt.is_valid else Colors.INVALID_CIRCLE
+
+        circle = plt.Circle((x, y), radius, fill=False, color=color, linewidth=2)
+        self.canvas.add_artist(circle)
+
+        if show_triangle:
+            triangle = plt.Polygon(evt.get_triangle(), fill=False, color=Colors.TRIANGLE, linewidth=1)
+            self.canvas.add_artist(triangle)
+
+        points = evt.point_triple
+        self.plot_sites(points, color=Colors.VALID_CIRCLE, show_labels=False, zorder=15)
+
+        return self
+
+    def _plot_edge(self, edge, sweep_line=None, print_name=True, color=Colors.EDGE, **kwargs):
+
+        start, end = self._origins(edge, sweep_line)
+
+        # Return if conditions not met
+        if not (start and end):
+            return self
+
+        # Draw the line
+        self.canvas.plot([start.xd, end.xd], [start.yd, end.yd], color)
+
+        # Add Name
+        if print_name:
+            self.canvas.annotate(
+                text=str(edge),
+                xy=((end.xd + start.xd) / 2, (end.yd + start.yd) / 2),
+                **kwargs
+            )
+
+        # Add arrow
+        # ax.annotate(text='', xy=(end.x, end.y), xytext=(start.x, start.y),
+        #             arrowprops=dict(arrowstyle='->', **kwargs))
+
+        return self
+
+    def _draw_line_from_edge_midpoint_to_incident_point(self, edge, sweep_line=None):
+        start, end = self._origins(edge, sweep_line)
+        is_first_edge = edge.incident_point is not None and edge.incident_point.first_edge == edge
+        incident_point = edge.incident_point
+        if start and end and incident_point:
+            self.canvas.plot(
+                [(start.xd + end.xd) / 2, incident_point.xd], [(start.yd + end.yd) / 2, incident_point.yd],
+                color=Colors.FIRST_EDGE if is_first_edge else Colors.INCIDENT_POINT_POINTER,
+                linestyle="--"
+            )
+        return self.canvas
+
+    def _origins(self, edge, sweep_line=None):
+
+        # Get axis limits
+        max_y = self.max_y
+
+        # Get start and end of edges
+        start = edge.get_origin(sweep_line, max_y)
+        end = edge.twin.get_origin(sweep_line, max_y)
+
+        return start, end
+
+    @staticmethod
+    def canvas_size(bounding_polygon, offset):
+        max_y = bounding_polygon.max_y + offset
+        max_x = bounding_polygon.max_x + offset
+        min_x = bounding_polygon.min_x - offset
+        min_y = bounding_polygon.min_y - offset
+        return min_x, max_x, min_y, max_y


Test patch:
diff --git a/voronoi/tests/line.py b/voronoi/tests/line.py
deleted file mode 100644
index e9a0af8..0000000
--- a/voronoi/tests/line.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from voronoi import Point
-from voronoi.graph.bounding_circle import BoundingCircle
-
-bc = BoundingCircle(5, 6, 12)
-
-p1 = Point(1, 2)
-p2 = Point(7.5, 21.4)
-a = bc.get_line(p1, p2)
-print(a)
-
-p1 = Point(1, 2)
-p2 = Point(7.5, 2)
-a = bc.get_line(p1, p2)
-print(a)
-
-p1 = Point(1, 2)
-p2 = Point(1, 11.2)
-a = bc.get_line(p1, p2)
-print(a)
-
-pass
diff --git a/voronoi/tests/unit.py b/voronoi/tests/unit.py
index af40b27..4225a3b 100644
--- a/voronoi/tests/unit.py
+++ b/voronoi/tests/unit.py
@@ -1,4 +1,4 @@
-from voronoi import DecimalCoordinate
+from voronoi import Coordinate
 from voronoi.algorithm import Algorithm
 from voronoi.graph import Polygon
 from voronoi.graph.bounding_box import BoundingBox
@@ -18,9 +18,9 @@ def _triangle(x, y):
 
 def _execute(polygon, points, sizes):
     v = Algorithm(polygon)
-    v.create_diagram(points=points, vis_steps=False, verbose=False, vis_result=False)
-    calculated = [p.cell_size(2) for p in v.points]
-    assert sizes == calculated, calculated
+    v.create_diagram(points=points)
+    calculated = [p.area(2) for p in v.sites]
+    assert sizes == calculated, f"\nResult=\n{calculated}\n\n Expected=\n{sizes}"
 
 
 # -----------
@@ -445,53 +445,41 @@ def test_large_grid():
     _execute(polygon, points, sizes)
 
 
-def _test_vertices_correct(polygon, points, expected):
+def _test_vertices_correct(polygon, points, expected, remove_zero_length_edges):
     # Initialize the algorithm
-    v = Algorithm(polygon)
+    v = Algorithm(polygon, remove_zero_length_edges=remove_zero_length_edges)
 
     # Create the diagram
     v.create_diagram(points)
 
     result = [
         [
-            [edge.get_origin().as_float_tuple(), edge.twin.get_origin().as_float_tuple()]
+            [edge.get_origin().xy, edge.twin.get_origin().xy]
             for edge in sorted(vertex.connected_edges, key=_sort_edges)
         ]
         for vertex in sorted(v.vertices, key=_sort_vertices)
     ]
 
-    assert result == expected, result
+    assert result == expected, f"\nResult=\n{result}\n\n Expected=\n{expected}"
+
 
 def _sort_vertices(vertex):
-    return vertex.point.y, vertex.point.x
+    return vertex.yd, vertex.xd
 
 
 def _sort_edges(edge):
-    return edge.get_origin().y, edge.get_origin().x
-
-def test_vertices_correct():
-    points = [
-        (2.5, 2.5),
-        (4, 7.5),
-        (7.5, 2.5),
-        (6, 7.5),
-        (4, 4),
-        (3, 3),
-        (6, 3),
-    ]
+    return edge.get_origin().yd, edge.get_origin().xd
 
-    # Define a bounding box
-    polygon = Polygon([
-        (2.5, 10),
-        (5, 10),
-        (10, 5),
-        (10, 2.5),
-        (5, 0),
-        (2.5, 0),
-        (0, 2.5),
-        (0, 5),
-    ])
 
+def test_vertices_correct():
+    points = [(2.5, 2.5), (4, 7.5), (7.5, 2.5), (6, 7.5), (4, 4), (3, 3), (6, 3)]
+    polygon = Polygon([(2.5, 10), (5, 10), (10, 5), (10, 2.5), (5, 0), (2.5, 0), (0, 2.5), (0, 5)])
     expected = [[[(2.5, 0.0), (4.642857142857143, 0.0)], [(2.5, 0.0), (0.0, 2.5)]], [[(4.642857142857143, 0.0), (4.5, 1.0)], [(4.642857142857143, 0.0), (5.0, 0.0)], [(4.642857142857143, 0.0), (2.5, 0.0)]], [[(5.0, 0.0), (6.0, 0.5)], [(5.0, 0.0), (4.642857142857143, 0.0)]], [[(6.0, 0.5), (7.583333333333333, 5.25)], [(6.0, 0.5), (10.0, 2.5)], [(6.0, 0.5), (5.0, 0.0)]], [[(4.5, 1.0), (4.5, 2.5)], [(4.5, 1.0), (0.16666666666666607, 5.333333333333334)], [(4.5, 1.0), (4.642857142857143, 0.0)]], [[(0.0, 2.5), (2.5, 0.0)], [(0.0, 2.5), (0.0, 5.0)]], [[(4.5, 2.5), (5.875, 5.25)], [(4.5, 2.5), (1.25, 5.75)], [(4.5, 2.5), (4.5, 1.0)]], [[(10.0, 2.5), (10.0, 5.0)], [(10.0, 2.5), (6.0, 0.5)]], [[(0.0, 5.0), (0.16666666666666607, 5.333333333333334)], [(0.0, 5.0), (0.0, 2.5)]], [[(0.0, 5.0), (0.16666666666666607, 5.333333333333334)], [(0.0, 5.0), (0.0, 2.5)]], [[(10.0, 5.0), (9.25, 5.75)], [(10.0, 5.0), (10.0, 2.5)]], [[(5.875, 5.25), (5.0, 5.75)], [(5.875, 5.25), (7.583333333333333, 5.25)], [(5.875, 5.25), (4.5, 2.5)]], [[(7.583333333333333, 5.25), (6.0, 0.5)], [(7.583333333333333, 5.25), (5.875, 5.25)], [(7.583333333333333, 5.25), (9.25, 5.75)]], [[(0.16666666666666607, 5.333333333333334), (4.5, 1.0)], [(0.16666666666666607, 5.333333333333334), (0.0, 5.0)], [(0.16666666666666607, 5.333333333333334), (0.4624999999999999, 5.925)]], [[(1.25, 5.75), (4.5, 2.5)], [(1.25, 5.75), (5.0, 5.75)], [(1.25, 5.75), (0.4624999999999999, 5.925)]], [[(5.0, 5.75), (1.25, 5.75)], [(5.0, 5.75), (5.0, 10.0)], [(5.0, 5.75), (5.875, 5.25)]], [[(9.25, 5.75), (7.583333333333333, 5.25)], [(9.25, 5.75), (5.0, 10.0)], [(9.25, 5.75), (10.0, 5.0)]], [[(0.4624999999999999, 5.925), (1.25, 5.75)], [(0.4624999999999999, 5.925), (0.16666666666666607, 5.333333333333334)], [(0.4624999999999999, 5.925), (2.5, 10.0)]], [[(2.5, 10.0), (0.4624999999999999, 5.925)], [(2.5, 10.0), (5.0, 10.0)]], [[(5.0, 10.0), (2.5, 10.0)], [(5.0, 10.0), (5.0, 10.0)]], [[(5.0, 10.0), (5.0, 5.75)], [(5.0, 10.0), (5.0, 10.0)], [(5.0, 10.0), (9.25, 5.75)]]]
+    _test_vertices_correct(polygon, points, expected, False)
+
 
-    _test_vertices_correct(polygon, points, expected)
+def test_vertices_correct_removed():
+    points = [(2.5, 2.5), (4, 7.5), (7.5, 2.5), (6, 7.5), (4, 4), (3, 3), (6, 3)]
+    polygon = Polygon([(2.5, 10), (5, 10), (10, 5), (10, 2.5), (5, 0), (2.5, 0), (0, 2.5), (0, 5)])
+    expected = [[[(2.5, 0.0), (4.642857142857143, 0.0)], [(2.5, 0.0), (0.0, 2.5)]], [[(4.642857142857143, 0.0), (4.5, 1.0)], [(4.642857142857143, 0.0), (5.0, 0.0)], [(4.642857142857143, 0.0), (2.5, 0.0)]], [[(5.0, 0.0), (6.0, 0.5)], [(5.0, 0.0), (4.642857142857143, 0.0)]], [[(6.0, 0.5), (7.583333333333333, 5.25)], [(6.0, 0.5), (10.0, 2.5)], [(6.0, 0.5), (5.0, 0.0)]], [[(4.5, 1.0), (4.5, 2.5)], [(4.5, 1.0), (0.16666666666666607, 5.333333333333334)], [(4.5, 1.0), (4.642857142857143, 0.0)]], [[(0.0, 2.5), (2.5, 0.0)], [(0.0, 2.5), (0.0, 5.0)]], [[(4.5, 2.5), (5.875, 5.25)], [(4.5, 2.5), (1.25, 5.75)], [(4.5, 2.5), (4.5, 1.0)]], [[(10.0, 2.5), (10.0, 5.0)], [(10.0, 2.5), (6.0, 0.5)]], [[(0.0, 5.0), (0.16666666666666607, 5.333333333333334)], [(0.0, 5.0), (0.0, 2.5)]], [[(0.0, 5.0), (0.16666666666666607, 5.333333333333334)], [(0.0, 5.0), (0.0, 2.5)]], [[(10.0, 5.0), (9.25, 5.75)], [(10.0, 5.0), (10.0, 2.5)]], [[(5.875, 5.25), (5.0, 5.75)], [(5.875, 5.25), (7.583333333333333, 5.25)], [(5.875, 5.25), (4.5, 2.5)]], [[(7.583333333333333, 5.25), (6.0, 0.5)], [(7.583333333333333, 5.25), (5.875, 5.25)], [(7.583333333333333, 5.25), (9.25, 5.75)]], [[(0.16666666666666607, 5.333333333333334), (4.5, 1.0)], [(0.16666666666666607, 5.333333333333334), (0.0, 5.0)], [(0.16666666666666607, 5.333333333333334), (0.4624999999999999, 5.925)]], [[(1.25, 5.75), (4.5, 2.5)], [(1.25, 5.75), (5.0, 5.75)], [(1.25, 5.75), (0.4624999999999999, 5.925)]], [[(5.0, 5.75), (1.25, 5.75)], [(5.0, 5.75), (5.0, 10.0)], [(5.0, 5.75), (5.875, 5.25)]], [[(9.25, 5.75), (7.583333333333333, 5.25)], [(9.25, 5.75), (5.0, 10.0)], [(9.25, 5.75), (10.0, 5.0)]], [[(0.4624999999999999, 5.925), (1.25, 5.75)], [(0.4624999999999999, 5.925), (0.16666666666666607, 5.333333333333334)], [(0.4624999999999999, 5.925), (2.5, 10.0)]], [[(2.5, 10.0), (0.4624999999999999, 5.925)], [(2.5, 10.0), (5.0, 10.0)]], [[(5.0, 10.0), (5.0, 5.75)], [(5.0, 10.0), (9.25, 5.75)], [(5.0, 10.0), (2.5, 10.0)]]]
+    _test_vertices_correct(polygon, points, expected, True)