{"id":1794,"date":"2025-02-12T07:02:30","date_gmt":"2025-02-12T07:02:30","guid":{"rendered":"https:\/\/mailitics.com\/index.php\/2025\/02\/12\/4-dimensional-data-visualization-time-in-bubble-charts\/"},"modified":"2025-02-12T07:02:30","modified_gmt":"2025-02-12T07:02:30","slug":"4-dimensional-data-visualization-time-in-bubble-charts","status":"publish","type":"post","link":"https:\/\/mailitics.com\/index.php\/2025\/02\/12\/4-dimensional-data-visualization-time-in-bubble-charts\/","title":{"rendered":"4-Dimensional Data Visualization: Time in Bubble Charts"},"content":{"rendered":"<p>    4-Dimensional Data Visualization: Time in Bubble Charts<br \/>\n \t<BR><br \/>\n<BR><\/BR><br \/>\n    <!-- no image --><br \/>\n \t<BR><br \/>\n<BR><\/BR><\/p>\n<div>\n<p class=\"wp-block-paragraph\" id=\"2751\">Bubble <a href=\"https:\/\/towardsdatascience.com\/tag\/charts\/\" title=\"Charts\">Charts<\/a> elegantly compress large amounts of information into a single visualization, with bubble size adding a third dimension. However, comparing \u201cbefore\u201d and \u201cafter\u201d states is often crucial. To address this, we propose adding a transition between these states, creating an intuitive user experience.<\/p>\n<p class=\"wp-block-paragraph\" id=\"4d40\">Since we couldn\u2019t find a ready-made solution, we developed our own. The challenge turned out to be fascinating and required refreshing some mathematical concepts.<\/p>\n<p class=\"wp-block-paragraph\" id=\"e307\">Without a doubt, the most challenging part of the visualization is the transition between two circles \u2014 before and after states. To simplify, we focus on solving a single case, which can then be extended in a loop to generate the necessary number of transitions.<\/p>\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" data-dominant-color=\"daecf2\" data-has-transparency=\"false\" style=\"--dominant-color: #daecf2;\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"401\" src=\"https:\/\/i0.wp.com\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_TyAz_zQ1lNA5JZG_KoDsUQ-1024x401.webp?resize=1024%2C401&#038;ssl=1\" alt=\"\" class=\"wp-image-597683 not-transparent\" srcset=\"https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_TyAz_zQ1lNA5JZG_KoDsUQ-1024x401.webp 1024w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_TyAz_zQ1lNA5JZG_KoDsUQ-300x117.webp 300w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_TyAz_zQ1lNA5JZG_KoDsUQ-768x301.webp 768w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_TyAz_zQ1lNA5JZG_KoDsUQ.webp 1400w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\"><figcaption class=\"wp-element-caption\">Base element, image by Author<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\" id=\"0056\">To build such a figure, let\u2019s first decompose it into three parts: two circles and a polygon that connects them (in gray).<\/p>\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" data-dominant-color=\"fcd8d9\" data-has-transparency=\"false\" style=\"--dominant-color: #fcd8d9;\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"349\" src=\"https:\/\/i0.wp.com\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_wyP7PbQ_ZtDccYErT0WXOA-2-1024x349.png?resize=1024%2C349&#038;ssl=1\" alt=\"\" class=\"wp-image-597685 not-transparent\" srcset=\"https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_wyP7PbQ_ZtDccYErT0WXOA-2-1024x349.png 1024w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_wyP7PbQ_ZtDccYErT0WXOA-2-300x102.png 300w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_wyP7PbQ_ZtDccYErT0WXOA-2-768x262.png 768w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_wyP7PbQ_ZtDccYErT0WXOA-2.png 1374w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\"><figcaption class=\"wp-element-caption\">Base element decomposition, image by Author<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\" id=\"bb26\">Building two circles is quite simple \u2014 we know their centers and radii. The remaining task is to construct a quadrilateral polygon, which has the following form:<\/p>\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" data-dominant-color=\"fbfbfb\" data-has-transparency=\"false\" style=\"--dominant-color: #fbfbfb;\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"358\" src=\"https:\/\/i0.wp.com\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_-E6c9_pSpxmJulxWeKQ86w-3-1024x358.png?resize=1024%2C358&#038;ssl=1\" alt=\"\" class=\"wp-image-597686 not-transparent\" srcset=\"https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_-E6c9_pSpxmJulxWeKQ86w-3-1024x358.png 1024w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_-E6c9_pSpxmJulxWeKQ86w-3-300x105.png 300w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_-E6c9_pSpxmJulxWeKQ86w-3-768x269.png 768w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_-E6c9_pSpxmJulxWeKQ86w-3.png 1360w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\"><figcaption class=\"wp-element-caption\">Polygon, image by Author<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\" id=\"1c00\">The construction of this polygon reduces to finding the coordinates of its vertices. This is the most interesting task, and we will solve it further.<\/p>\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" data-dominant-color=\"f8f6f6\" data-has-transparency=\"false\" style=\"--dominant-color: #f8f6f6;\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"574\" src=\"https:\/\/i0.wp.com\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_378emyKwZrOdETtMTAHLhw-4-1024x574.png?resize=1024%2C574&#038;ssl=1\" alt=\"\" class=\"wp-image-597687 not-transparent\" srcset=\"https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_378emyKwZrOdETtMTAHLhw-4-1024x574.png 1024w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_378emyKwZrOdETtMTAHLhw-4-300x168.png 300w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_378emyKwZrOdETtMTAHLhw-4-768x431.png 768w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_378emyKwZrOdETtMTAHLhw-4.png 1334w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\"><figcaption class=\"wp-element-caption\">From polygon to tangent lines, image by Author<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\" id=\"b98f\">To calculate the distance from a point\u00a0<em>(x1, y1)<\/em>\u00a0to the line\u00a0<em>ax+y+b=0<\/em>, the formula is:<\/p>\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" data-dominant-color=\"fbfbfb\" data-has-transparency=\"false\" style=\"--dominant-color: #fbfbfb;\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"120\" src=\"https:\/\/i0.wp.com\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_7LlMwZ1FJzH9IpKrXrjLQg-1024x120.png?resize=1024%2C120&#038;ssl=1\" alt=\"\" class=\"wp-image-597688 not-transparent\" srcset=\"https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_7LlMwZ1FJzH9IpKrXrjLQg-1024x120.png 1024w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_7LlMwZ1FJzH9IpKrXrjLQg-300x35.png 300w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_7LlMwZ1FJzH9IpKrXrjLQg-768x90.png 768w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_7LlMwZ1FJzH9IpKrXrjLQg.png 1208w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\"><figcaption class=\"wp-element-caption\">Distance from point to a line, image by Author<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\" id=\"11f5\">In our case, distance (<em>d<\/em>) is equal to circle radius (<em>r<\/em>). Hence,<\/p>\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" data-dominant-color=\"fbfbfb\" data-has-transparency=\"false\" style=\"--dominant-color: #fbfbfb;\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"107\" src=\"https:\/\/i0.wp.com\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_vzCO6MRF29ZWhbKJ7i6LJw-1024x107.png?resize=1024%2C107&#038;ssl=1\" alt=\"\" class=\"wp-image-597692 not-transparent\" srcset=\"https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_vzCO6MRF29ZWhbKJ7i6LJw-1024x107.png 1024w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_vzCO6MRF29ZWhbKJ7i6LJw-300x31.png 300w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_vzCO6MRF29ZWhbKJ7i6LJw-768x80.png 768w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_vzCO6MRF29ZWhbKJ7i6LJw.png 1260w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\"><figcaption class=\"wp-element-caption\">Distance to radius, image by Author<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\" id=\"f882\">After multiplying both sides of the equation by\u00a0<em>a**2+1<\/em>, we get:<\/p>\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" data-dominant-color=\"f9f9f9\" data-has-transparency=\"false\" style=\"--dominant-color: #f9f9f9;\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"81\" src=\"https:\/\/i0.wp.com\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_Yaqm6hDTKJJ9mjTzM8GbXQ-1024x81.png?resize=1024%2C81&#038;ssl=1\" alt=\"\" class=\"wp-image-597695 not-transparent\" srcset=\"https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_Yaqm6hDTKJJ9mjTzM8GbXQ-1024x81.png 1024w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_Yaqm6hDTKJJ9mjTzM8GbXQ-300x24.png 300w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_Yaqm6hDTKJJ9mjTzM8GbXQ-768x61.png 768w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_Yaqm6hDTKJJ9mjTzM8GbXQ.png 1192w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\"><figcaption class=\"wp-element-caption\">Base math, image by Author<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\" id=\"bc73\">After moving everything to one side and setting the equation equal to zero, we get:<\/p>\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" data-dominant-color=\"f8f8f8\" data-has-transparency=\"false\" style=\"--dominant-color: #f8f8f8;\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"67\" src=\"https:\/\/i0.wp.com\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1__8W473CVXk3AnuQSy9_kwg-1024x67.png?resize=1024%2C67&#038;ssl=1\" alt=\"\" class=\"wp-image-597696 not-transparent\" srcset=\"https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1__8W473CVXk3AnuQSy9_kwg-1024x67.png 1024w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1__8W473CVXk3AnuQSy9_kwg-300x20.png 300w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1__8W473CVXk3AnuQSy9_kwg-768x50.png 768w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1__8W473CVXk3AnuQSy9_kwg.png 1314w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\"><figcaption class=\"wp-element-caption\">Base math, image by Author<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\" id=\"914f\">Since we have two circles and need to find a tangent to both, we have the following system of equations:<\/p>\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" data-dominant-color=\"f6f6f6\" data-has-transparency=\"false\" style=\"--dominant-color: #f6f6f6;\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"201\" src=\"https:\/\/i0.wp.com\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_KPXXWjdFqg4RAFvk2QvC9w-1024x201.png?resize=1024%2C201&#038;ssl=1\" alt=\"\" class=\"wp-image-597697 not-transparent\" srcset=\"https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_KPXXWjdFqg4RAFvk2QvC9w-1024x201.png 1024w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_KPXXWjdFqg4RAFvk2QvC9w-300x59.png 300w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_KPXXWjdFqg4RAFvk2QvC9w-768x151.png 768w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_KPXXWjdFqg4RAFvk2QvC9w.png 1362w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\"><figcaption class=\"wp-element-caption\">System of equations, image by Author<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\" id=\"bd89\">This works great, but the problem is that we have 4 possible tangent lines in reality:<\/p>\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" data-dominant-color=\"faf6f6\" data-has-transparency=\"false\" style=\"--dominant-color: #faf6f6;\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"569\" src=\"https:\/\/i0.wp.com\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_ekrFkIirIgr5ulKo_RHFYw-1024x569.png?resize=1024%2C569&#038;ssl=1\" alt=\"\" class=\"wp-image-597700 not-transparent\" srcset=\"https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_ekrFkIirIgr5ulKo_RHFYw-1024x569.png 1024w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_ekrFkIirIgr5ulKo_RHFYw-300x167.png 300w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_ekrFkIirIgr5ulKo_RHFYw-768x427.png 768w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_ekrFkIirIgr5ulKo_RHFYw.png 1400w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\"><figcaption class=\"wp-element-caption\">All possible tangent lines, image by Author<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\" id=\"314c\">And we need to choose just 2 of them \u2014 external ones.<\/p>\n<p class=\"wp-block-paragraph\" id=\"dc9a\">To do this we need to check each tangent and each circle center and determine if the line is above or below the point:<\/p>\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" data-dominant-color=\"f3f3f3\" data-has-transparency=\"false\" style=\"--dominant-color: #f3f3f3;\" loading=\"lazy\" decoding=\"async\" width=\"1002\" height=\"244\" src=\"https:\/\/i0.wp.com\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_K_idhYKkJFmCZgUduNesTg.png?resize=1002%2C244&#038;ssl=1\" alt=\"\" class=\"wp-image-597703 not-transparent\" srcset=\"https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_K_idhYKkJFmCZgUduNesTg.png 1002w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_K_idhYKkJFmCZgUduNesTg-300x73.png 300w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_K_idhYKkJFmCZgUduNesTg-768x187.png 768w\" sizes=\"auto, (max-width: 1002px) 100vw, 1002px\"><figcaption class=\"wp-element-caption\">Check if line is above or below the point, image by Author<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\" id=\"9e07\">We need the two lines that both pass above or both pass below the centers of the circles.<\/p>\n<p class=\"wp-block-paragraph\" id=\"19ba\">Now, let\u2019s translate all these steps into code:<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">import matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport sympy as sp\nfrom scipy.spatial import ConvexHull\nimport math\nfrom matplotlib import rcParams\nimport matplotlib.patches as patches\n\ndef check_position_relative_to_line(a, b, x0, y0):\n    y_line = a * x0 + b\n    \n    if y0 &gt; y_line:\n        return 1 # line is above the point\n    elif y0 &lt; y_line:\n        return -1\n\n    \ndef find_tangent_equations(x1, y1, r1, x2, y2, r2):\n    a, b = sp.symbols('a b')\n\n    tangent_1 = (a*x1 + b - y1)**2 - r1**2 * (a**2 + 1)  \n    tangent_2 = (a*x2 + b - y2)**2 - r2**2 * (a**2 + 1) \n\n    eqs_1 = [tangent_2, tangent_1]\n    solution = sp.solve(eqs_1, (a, b))\n    parameters = [(float(e[0]), float(e[1])) for e in solution]\n\n    # filter just external tangents\n    parameters_filtered = []\n    for tangent in parameters:\n        a = tangent[0]\n        b = tangent[1]\n        if abs(check_position_relative_to_line(a, b, x1, y1) + check_position_relative_to_line(a, b, x2, y2)) == 2:\n            parameters_filtered.append(tangent)\n\n    return parameters_filtered<\/code><\/pre>\n<p class=\"wp-block-paragraph\" id=\"bdb9\">Now, we just need to find the intersections of the tangents with the circles. These 4 points will be the vertices of the desired polygon.<\/p>\n<p class=\"wp-block-paragraph\" id=\"e44d\">Circle equation:<\/p>\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" data-dominant-color=\"f5f5f5\" data-has-transparency=\"false\" style=\"--dominant-color: #f5f5f5;\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"60\" src=\"https:\/\/i0.wp.com\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_uoLemi2JuOlaejqIsYDz9A-1024x60.png?resize=1024%2C60&#038;ssl=1\" alt=\"\" class=\"wp-image-597715 not-transparent\" srcset=\"https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_uoLemi2JuOlaejqIsYDz9A-1024x60.png 1024w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_uoLemi2JuOlaejqIsYDz9A-300x18.png 300w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_uoLemi2JuOlaejqIsYDz9A-768x45.png 768w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_uoLemi2JuOlaejqIsYDz9A.png 1400w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\"><figcaption class=\"wp-element-caption\">Circle equation, image by Author<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\" id=\"5d95\">Substitute the line equation<em>\u00a0y=ax+b<\/em>\u00a0into the circle equation:<\/p>\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" data-dominant-color=\"f7f7f7\" data-has-transparency=\"false\" style=\"--dominant-color: #f7f7f7;\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"63\" src=\"https:\/\/i0.wp.com\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_rkv72OOFPOnTVS4TIscWxw-1024x63.png?resize=1024%2C63&#038;ssl=1\" alt=\"\" class=\"wp-image-597716 not-transparent\" srcset=\"https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_rkv72OOFPOnTVS4TIscWxw-1024x63.png 1024w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_rkv72OOFPOnTVS4TIscWxw-300x18.png 300w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_rkv72OOFPOnTVS4TIscWxw-768x47.png 768w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_rkv72OOFPOnTVS4TIscWxw.png 1336w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\"><figcaption class=\"wp-element-caption\">Base math, image by Author<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\" id=\"0d4b\">Solution of the equation is the\u00a0<em>x<\/em>\u00a0of the intersection.<\/p>\n<p class=\"wp-block-paragraph\" id=\"c334\">Then, calculate\u00a0<em>y<\/em>\u00a0from the line equation:<\/p>\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" data-dominant-color=\"fcfcfc\" data-has-transparency=\"false\" style=\"--dominant-color: #fcfcfc;\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"64\" src=\"https:\/\/i0.wp.com\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1__rBBcVpu31BvYkV1u24pMA-1024x64.png?resize=1024%2C64&#038;ssl=1\" alt=\"\" class=\"wp-image-597718 not-transparent\" srcset=\"https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1__rBBcVpu31BvYkV1u24pMA-1024x64.png 1024w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1__rBBcVpu31BvYkV1u24pMA-300x19.png 300w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1__rBBcVpu31BvYkV1u24pMA-768x48.png 768w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1__rBBcVpu31BvYkV1u24pMA.png 1118w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\"><figcaption class=\"wp-element-caption\">Calculating y, image by Author<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\" id=\"b2b5\">How it translates to the code:<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">def find_circle_line_intersection(circle_x, circle_y, circle_r, line_a, line_b):\n    x, y = sp.symbols('x y')\n    circle_eq = (x - circle_x)**2 + (y - circle_y)**2 - circle_r**2\n    intersection_eq = circle_eq.subs(y, line_a * x + line_b)\n\n    sol_x_raw = sp.solve(intersection_eq, x)[0]\n    try:\n        sol_x = float(sol_x_raw)\n    except:\n        sol_x = sol_x_raw.as_real_imag()[0]\n    sol_y = line_a * sol_x + line_b\n    return sol_x, sol_y<\/code><\/pre>\n<p class=\"wp-block-paragraph\" id=\"442d\">Now we want to generate sample data to demonstrate the whole chart compositions.<\/p>\n<p class=\"wp-block-paragraph\" id=\"e7bc\">Imagine we have 4 users on our platform. We know how many purchases they made, generated revenue and activity on the platform. All these metrics are calculated for 2 periods (let\u2019s call them pre and post period).<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\"># data generation\ndf = pd.DataFrame({'user': ['Emily', 'Emily', 'James', 'James', 'Tony', 'Tony', 'Olivia', 'Olivia'],\n                   'period': ['pre', 'post', 'pre', 'post', 'pre', 'post', 'pre', 'post'],\n                   'num_purchases': [10, 9, 3, 5, 2, 4, 8, 7],\n                   'revenue': [70, 60, 80, 90, 20, 15, 80, 76],\n                   'activity': [100, 80, 50, 90, 210, 170, 60, 55]})<\/code><\/pre>\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" data-dominant-color=\"e6e6e6\" data-has-transparency=\"false\" style=\"--dominant-color: #e6e6e6;\" loading=\"lazy\" decoding=\"async\" width=\"744\" height=\"224\" src=\"https:\/\/i0.wp.com\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_iFY7DSEzgaFFq51e5Hu21w.png?resize=744%2C224&#038;ssl=1\" alt=\"\" class=\"wp-image-597724 not-transparent\" srcset=\"https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_iFY7DSEzgaFFq51e5Hu21w.png 744w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_iFY7DSEzgaFFq51e5Hu21w-300x90.png 300w\" sizes=\"auto, (max-width: 744px) 100vw, 744px\"><figcaption class=\"wp-element-caption\">Data sample, image by Author<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\" id=\"9de0\">Let\u2019s assume that \u201cactivity\u201d is the area of the bubble. Now, let\u2019s convert it into the radius of the bubble. We will also scale the y-axis.<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">def area_to_radius(area):\n    radius = math.sqrt(area \/ math.pi)\n    return radius\n\nx_alias, y_alias, a_alias = 'num_purchases', 'revenue', 'activity'\n\n# scaling metrics\nradius_scaler = 0.1\ndf['radius'] = df[a_alias].apply(area_to_radius) * radius_scaler\ndf['y_scaled'] = df[y_alias] \/ df[x_alias].max()<\/code><\/pre>\n<p class=\"wp-block-paragraph\" id=\"5072\">Now let\u2019s build the chart \u2014 2 circles and the polygon.<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">def draw_polygon(plt, points):\n    hull = ConvexHull(points)\n    convex_points = [points[i] for i in hull.vertices]\n\n    x, y = zip(*convex_points)\n    x += (x[0],)\n    y += (y[0],)\n\n    plt.fill(x, y, color='#99d8e1', alpha=1, zorder=1)\n\n# bubble pre\nfor _, row in df[df.period=='pre'].iterrows():\n    x = row[x_alias]\n    y = row.y_scaled\n    r = row.radius\n    circle = patches.Circle((x, y), r, facecolor='#99d8e1', edgecolor='none', linewidth=0, zorder=2)\n    plt.gca().add_patch(circle)\n\n# transition area\nfor user in df.user.unique():\n    user_pre = df[(df.user==user) &amp; (df.period=='pre')]\n    x1, y1, r1 = user_pre[x_alias].values[0], user_pre.y_scaled.values[0], user_pre.radius.values[0]\n    user_post = df[(df.user==user) &amp; (df.period=='post')]\n    x2, y2, r2 = user_post[x_alias].values[0], user_post.y_scaled.values[0], user_post.radius.values[0]\n\n    tangent_equations = find_tangent_equations(x1, y1, r1, x2, y2, r2)\n    circle_1_line_intersections = [find_circle_line_intersection(x1, y1, r1, eq[0], eq[1]) for eq in tangent_equations]\n    circle_2_line_intersections = [find_circle_line_intersection(x2, y2, r2, eq[0], eq[1]) for eq in tangent_equations]\n\n    polygon_points = circle_1_line_intersections + circle_2_line_intersections\n    draw_polygon(plt, polygon_points)\n\n# bubble post\nfor _, row in df[df.period=='post'].iterrows():\n    x = row[x_alias]\n    y = row.y_scaled\n    r = row.radius\n    label = row.user\n    circle = patches.Circle((x, y), r, facecolor='#2d699f', edgecolor='none', linewidth=0, zorder=2)\n    plt.gca().add_patch(circle)\n\n    plt.text(x, y - r - 0.3, label, fontsize=12, ha='center')<\/code><\/pre>\n<p class=\"wp-block-paragraph\" id=\"acdb\">The output looks as expected:<\/p>\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" data-dominant-color=\"e4eff2\" data-has-transparency=\"true\" style=\"--dominant-color: #e4eff2;\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"708\" src=\"https:\/\/i0.wp.com\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_gOYxIZenDrxsaNlHl-N79w-1024x708.png?resize=1024%2C708&#038;ssl=1\" alt=\"\" class=\"wp-image-597729 has-transparency\" srcset=\"https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_gOYxIZenDrxsaNlHl-N79w-1024x708.png 1024w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_gOYxIZenDrxsaNlHl-N79w-300x207.png 300w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_gOYxIZenDrxsaNlHl-N79w-768x531.png 768w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_gOYxIZenDrxsaNlHl-N79w.png 1400w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\"><figcaption class=\"wp-element-caption\">Output, image by Author<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\" id=\"4a9f\">Now we want to add some styling:<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\"># plot parameters\nplt.subplots(figsize=(10, 10))\nrcParams['font.family'] = 'DejaVu Sans'\nrcParams['font.size'] = 14\nplt.grid(color=\"gray\", linestyle=(0, (10, 10)), linewidth=0.5, alpha=0.6, zorder=1)\nplt.axvline(x=0, color='white', linewidth=2)\nplt.gca().set_facecolor('white')\nplt.gcf().set_facecolor('white')\n\n# spines formatting\nplt.gca().spines[\"top\"].set_visible(False)\nplt.gca().spines[\"right\"].set_visible(False)\nplt.gca().spines[\"bottom\"].set_visible(False)\nplt.gca().spines[\"left\"].set_visible(False)\nplt.gca().tick_params(axis=\"both\", which=\"both\", length=0)\n\n# plot labels\nplt.xlabel(\"Number purchases\") \nplt.ylabel(\"Revenue, $\")\nplt.title(\"Product users performance\", fontsize=18, color=\"black\")\n\n# axis limits\naxis_lim = df[x_alias].max() * 1.2\nplt.xlim(0, axis_lim)\nplt.ylim(0, axis_lim)<\/code><\/pre>\n<p class=\"wp-block-paragraph\" id=\"76f5\">Pre-post legend in the right bottom corner to give viewer a hint, how to read the chart:<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">## pre-post legend \n# circle 1\nlegend_position, r1 = (11, 2.2), 0.3\nx1, y1 = legend_position[0], legend_position[1]\ncircle = patches.Circle((x1, y1), r1, facecolor='#99d8e1', edgecolor='none', linewidth=0, zorder=2)\nplt.gca().add_patch(circle)\nplt.text(x1, y1 + r1 + 0.15, 'Pre', fontsize=12, ha='center', va='center')\n# circle 2\nx2, y2 = legend_position[0], legend_position[1] - r1*3\nr2 = r1*0.7\ncircle = patches.Circle((x2, y2), r2, facecolor='#2d699f', edgecolor='none', linewidth=0, zorder=2)\nplt.gca().add_patch(circle)\nplt.text(x2, y2 - r2 - 0.15, 'Post', fontsize=12, ha='center', va='center')\n# tangents\ntangent_equations = find_tangent_equations(x1, y1, r1, x2, y2, r2)\ncircle_1_line_intersections = [find_circle_line_intersection(x1, y1, r1, eq[0], eq[1]) for eq in tangent_equations]\ncircle_2_line_intersections = [find_circle_line_intersection(x2, y2, r2, eq[0], eq[1]) for eq in tangent_equations]\npolygon_points = circle_1_line_intersections + circle_2_line_intersections\ndraw_polygon(plt, polygon_points)\n# small arrow\nplt.annotate('', xytext=(x1, y1), xy=(x2, y1 - r1*2), arrowprops=dict(edgecolor='black', arrowstyle='-&gt;', lw=1))<\/code><\/pre>\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" data-dominant-color=\"eff6f8\" data-has-transparency=\"false\" style=\"--dominant-color: #eff6f8;\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"1012\" src=\"https:\/\/i0.wp.com\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_BaFPVRvXMIu5jZdDJ7nAjQ-1024x1012.png?resize=1024%2C1012&#038;ssl=1\" alt=\"\" class=\"wp-image-597735 not-transparent\" srcset=\"https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_BaFPVRvXMIu5jZdDJ7nAjQ-1024x1012.png 1024w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_BaFPVRvXMIu5jZdDJ7nAjQ-300x296.png 300w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_BaFPVRvXMIu5jZdDJ7nAjQ-768x759.png 768w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_BaFPVRvXMIu5jZdDJ7nAjQ.png 1400w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\"><figcaption class=\"wp-element-caption\">Adding styling and legend, image by Author<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\" id=\"440e\">And finally bubble-size legend:<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\"># bubble size legend\nlegend_areas_original = [150, 50]\nlegend_position = (11, 10.2)\nfor i in legend_areas_original:\n    i_r = area_to_radius(i) * radius_scaler\n    circle = plt.Circle((legend_position[0], legend_position[1] + i_r), i_r, color='black', fill=False, linewidth=0.6, facecolor='none')\n    plt.gca().add_patch(circle)\n    plt.text(legend_position[0], legend_position[1] + 2*i_r, str(i), fontsize=12, ha='center', va='center',\n              bbox=dict(facecolor='white', edgecolor='none', boxstyle='round,pad=0.1'))\nlegend_label_r = area_to_radius(np.max(legend_areas_original)) * radius_scaler\nplt.text(legend_position[0], legend_position[1] + 2*legend_label_r + 0.3, 'Activity, hours', fontsize=12, ha='center', va='center')<\/code><\/pre>\n<p class=\"wp-block-paragraph\" id=\"69e3\">Our final chart looks like this:<\/p>\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" data-dominant-color=\"eff6f8\" data-has-transparency=\"false\" style=\"--dominant-color: #eff6f8;\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"976\" src=\"https:\/\/i0.wp.com\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_4A-JpivQCt7Q2CZ99ulDnA-1024x976.png?resize=1024%2C976&#038;ssl=1\" alt=\"\" class=\"wp-image-597737 not-transparent\" srcset=\"https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_4A-JpivQCt7Q2CZ99ulDnA-1024x976.png 1024w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_4A-JpivQCt7Q2CZ99ulDnA-300x286.png 300w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_4A-JpivQCt7Q2CZ99ulDnA-768x732.png 768w, https:\/\/towardsdatascience.com\/wp-content\/uploads\/2025\/02\/1_4A-JpivQCt7Q2CZ99ulDnA.png 1400w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\"><figcaption class=\"wp-element-caption\">Adding second legend, image by Author<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\" id=\"8322\">The visualization looks very stylish and concentrates quite a lot of information in a compact form.<\/p>\n<p class=\"wp-block-paragraph\" id=\"955a\">Here is the full code for the graph:<\/p>\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">import matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport sympy as sp\nfrom scipy.spatial import ConvexHull\nimport math\nfrom matplotlib import rcParams\nimport matplotlib.patches as patches\n\ndef check_position_relative_to_line(a, b, x0, y0):\n    y_line = a * x0 + b\n    \n    if y0 &gt; y_line:\n        return 1 # line is above the point\n    elif y0 &lt; y_line:\n        return -1\n\n    \ndef find_tangent_equations(x1, y1, r1, x2, y2, r2):\n    a, b = sp.symbols('a b')\n\n    tangent_1 = (a*x1 + b - y1)**2 - r1**2 * (a**2 + 1)  \n    tangent_2 = (a*x2 + b - y2)**2 - r2**2 * (a**2 + 1) \n\n    eqs_1 = [tangent_2, tangent_1]\n    solution = sp.solve(eqs_1, (a, b))\n    parameters = [(float(e[0]), float(e[1])) for e in solution]\n\n    # filter just external tangents\n    parameters_filtered = []\n    for tangent in parameters:\n        a = tangent[0]\n        b = tangent[1]\n        if abs(check_position_relative_to_line(a, b, x1, y1) + check_position_relative_to_line(a, b, x2, y2)) == 2:\n            parameters_filtered.append(tangent)\n\n    return parameters_filtered\n\ndef find_circle_line_intersection(circle_x, circle_y, circle_r, line_a, line_b):\n    x, y = sp.symbols('x y')\n    circle_eq = (x - circle_x)**2 + (y - circle_y)**2 - circle_r**2\n    intersection_eq = circle_eq.subs(y, line_a * x + line_b)\n\n    sol_x_raw = sp.solve(intersection_eq, x)[0]\n    try:\n        sol_x = float(sol_x_raw)\n    except:\n        sol_x = sol_x_raw.as_real_imag()[0]\n    sol_y = line_a * sol_x + line_b\n    return sol_x, sol_y\n\ndef draw_polygon(plt, points):\n    hull = ConvexHull(points)\n    convex_points = [points[i] for i in hull.vertices]\n\n    x, y = zip(*convex_points)\n    x += (x[0],)\n    y += (y[0],)\n\n    plt.fill(x, y, color='#99d8e1', alpha=1, zorder=1)\n\ndef area_to_radius(area):\n    radius = math.sqrt(area \/ math.pi)\n    return radius\n\n# data generation\ndf = pd.DataFrame({'user': ['Emily', 'Emily', 'James', 'James', 'Tony', 'Tony', 'Olivia', 'Olivia', 'Oliver', 'Oliver', 'Benjamin', 'Benjamin'],\n                   'period': ['pre', 'post', 'pre', 'post', 'pre', 'post', 'pre', 'post', 'pre', 'post', 'pre', 'post'],\n                   'num_purchases': [10, 9, 3, 5, 2, 4, 8, 7, 6, 7, 4, 6],\n                   'revenue': [70, 60, 80, 90, 20, 15, 80, 76, 17, 19, 45, 55],\n                   'activity': [100, 80, 50, 90, 210, 170, 60, 55, 30, 20, 200, 120]})\n\nx_alias, y_alias, a_alias = 'num_purchases', 'revenue', 'activity'\n\n# scaling metrics\nradius_scaler = 0.1\ndf['radius'] = df[a_alias].apply(area_to_radius) * radius_scaler\ndf['y_scaled'] = df[y_alias] \/ df[x_alias].max()\n\n# plot parameters\nplt.subplots(figsize=(10, 10))\nrcParams['font.family'] = 'DejaVu Sans'\nrcParams['font.size'] = 14\nplt.grid(color=\"gray\", linestyle=(0, (10, 10)), linewidth=0.5, alpha=0.6, zorder=1)\nplt.axvline(x=0, color='white', linewidth=2)\nplt.gca().set_facecolor('white')\nplt.gcf().set_facecolor('white')\n\n# spines formatting\nplt.gca().spines[\"top\"].set_visible(False)\nplt.gca().spines[\"right\"].set_visible(False)\nplt.gca().spines[\"bottom\"].set_visible(False)\nplt.gca().spines[\"left\"].set_visible(False)\nplt.gca().tick_params(axis=\"both\", which=\"both\", length=0)\n\n# plot labels\nplt.xlabel(\"Number purchases\") \nplt.ylabel(\"Revenue, $\")\nplt.title(\"Product users performance\", fontsize=18, color=\"black\")\n\n# axis limits\naxis_lim = df[x_alias].max() * 1.2\nplt.xlim(0, axis_lim)\nplt.ylim(0, axis_lim)\n\n# bubble pre\nfor _, row in df[df.period=='pre'].iterrows():\n    x = row[x_alias]\n    y = row.y_scaled\n    r = row.radius\n    circle = patches.Circle((x, y), r, facecolor='#99d8e1', edgecolor='none', linewidth=0, zorder=2)\n    plt.gca().add_patch(circle)\n\n# transition area\nfor user in df.user.unique():\n    user_pre = df[(df.user==user) &amp; (df.period=='pre')]\n    x1, y1, r1 = user_pre[x_alias].values[0], user_pre.y_scaled.values[0], user_pre.radius.values[0]\n    user_post = df[(df.user==user) &amp; (df.period=='post')]\n    x2, y2, r2 = user_post[x_alias].values[0], user_post.y_scaled.values[0], user_post.radius.values[0]\n\n    tangent_equations = find_tangent_equations(x1, y1, r1, x2, y2, r2)\n    circle_1_line_intersections = [find_circle_line_intersection(x1, y1, r1, eq[0], eq[1]) for eq in tangent_equations]\n    circle_2_line_intersections = [find_circle_line_intersection(x2, y2, r2, eq[0], eq[1]) for eq in tangent_equations]\n\n    polygon_points = circle_1_line_intersections + circle_2_line_intersections\n    draw_polygon(plt, polygon_points)\n\n# bubble post\nfor _, row in df[df.period=='post'].iterrows():\n    x = row[x_alias]\n    y = row.y_scaled\n    r = row.radius\n    label = row.user\n    circle = patches.Circle((x, y), r, facecolor='#2d699f', edgecolor='none', linewidth=0, zorder=2)\n    plt.gca().add_patch(circle)\n\n    plt.text(x, y - r - 0.3, label, fontsize=12, ha='center')\n\n# bubble size legend\nlegend_areas_original = [150, 50]\nlegend_position = (11, 10.2)\nfor i in legend_areas_original:\n    i_r = area_to_radius(i) * radius_scaler\n    circle = plt.Circle((legend_position[0], legend_position[1] + i_r), i_r, color='black', fill=False, linewidth=0.6, facecolor='none')\n    plt.gca().add_patch(circle)\n    plt.text(legend_position[0], legend_position[1] + 2*i_r, str(i), fontsize=12, ha='center', va='center',\n              bbox=dict(facecolor='white', edgecolor='none', boxstyle='round,pad=0.1'))\nlegend_label_r = area_to_radius(np.max(legend_areas_original)) * radius_scaler\nplt.text(legend_position[0], legend_position[1] + 2*legend_label_r + 0.3, 'Activity, hours', fontsize=12, ha='center', va='center')\n\n\n## pre-post legend \n# circle 1\nlegend_position, r1 = (11, 2.2), 0.3\nx1, y1 = legend_position[0], legend_position[1]\ncircle = patches.Circle((x1, y1), r1, facecolor='#99d8e1', edgecolor='none', linewidth=0, zorder=2)\nplt.gca().add_patch(circle)\nplt.text(x1, y1 + r1 + 0.15, 'Pre', fontsize=12, ha='center', va='center')\n# circle 2\nx2, y2 = legend_position[0], legend_position[1] - r1*3\nr2 = r1*0.7\ncircle = patches.Circle((x2, y2), r2, facecolor='#2d699f', edgecolor='none', linewidth=0, zorder=2)\nplt.gca().add_patch(circle)\nplt.text(x2, y2 - r2 - 0.15, 'Post', fontsize=12, ha='center', va='center')\n# tangents\ntangent_equations = find_tangent_equations(x1, y1, r1, x2, y2, r2)\ncircle_1_line_intersections = [find_circle_line_intersection(x1, y1, r1, eq[0], eq[1]) for eq in tangent_equations]\ncircle_2_line_intersections = [find_circle_line_intersection(x2, y2, r2, eq[0], eq[1]) for eq in tangent_equations]\npolygon_points = circle_1_line_intersections + circle_2_line_intersections\ndraw_polygon(plt, polygon_points)\n# small arrow\nplt.annotate('', xytext=(x1, y1), xy=(x2, y1 - r1*2), arrowprops=dict(edgecolor='black', arrowstyle='-&gt;', lw=1))\n\n# y axis formatting\nmax_y = df[y_alias].max()\nnearest_power_of_10 = 10 ** math.ceil(math.log10(max_y))\nticks = [round(nearest_power_of_10\/5 * i, 2) for i in range(0, 6)]\nyticks_scaled = ticks \/ df[x_alias].max()\nyticklabels = [str(i) for i in ticks]\nyticklabels[0] = ''\nplt.yticks(yticks_scaled, yticklabels)\n\nplt.savefig(\"plot_with_white_background.png\", bbox_inches='tight', dpi=300)<\/code><\/pre>\n<p class=\"wp-block-paragraph\" id=\"e58e\">Adding a time dimension to bubble charts enhances their ability to convey dynamic data changes intuitively. By implementing smooth transitions between \u201cbefore\u201d and \u201cafter\u201d states, users can better understand trends and comparisons over time.<\/p>\n<p class=\"wp-block-paragraph\" id=\"bcbc\">While no ready-made solutions were available, developing a custom approach proved both challenging and rewarding, requiring mathematical insights and careful animation techniques. The proposed method can be easily extended to various datasets, making it a valuable tool for <a href=\"https:\/\/towardsdatascience.com\/tag\/data-visualization\/\" title=\"Data Visualization\">Data Visualization<\/a> in business, science, and analytics.<\/p>\n<p>The post <a href=\"https:\/\/towardsdatascience.com\/4-dimensional-data-visualization-time-in-bubble-charts\/\">4-Dimensional Data Visualization: Time in Bubble Charts<\/a> appeared first on <a href=\"https:\/\/towardsdatascience.com\/\">Towards Data Science<\/a>.<\/p>\n<\/div>\n<p> \t<BR><br \/>\n <BR><\/BR><br \/>\n    Vladimir Zhyvov<br \/>\n \t<BR><br \/>\n<BR><\/BR><br \/>\n<a href=\"https:\/\/towardsdatascience.com\/4-dimensional-data-visualization-time-in-bubble-charts\/\">Go to original source<\/a><br \/>\n \t<BR><br \/>\n <BR><\/BR><\/p>\n","protected":false},"excerpt":{"rendered":"<p>4-Dimensional Data Visualization: Time in Bubble Charts Bubble Charts elegantly compress large amounts of information into a single visualization, with bubble size adding a third dimension. However, comparing \u201cbefore\u201d and \u201cafter\u201d states is often crucial. To address this, we propose adding a transition between these states, creating an intuitive user experience. Since we couldn\u2019t find [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[62,1034,83,82,166,229,160],"tags":[1712,845,1713],"class_list":["post-1794","post","type-post","status-publish","format-standard","hentry","category-aimldsaimlds","category-charts","category-data-science","category-data-visualization","category-hands-on-tutorials","category-math","category-programming","tag-author","tag-image","tag-polygon"],"_links":{"self":[{"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/posts\/1794"}],"collection":[{"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/comments?post=1794"}],"version-history":[{"count":0,"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/posts\/1794\/revisions"}],"wp:attachment":[{"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/media?parent=1794"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/categories?post=1794"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mailitics.com\/index.php\/wp-json\/wp\/v2\/tags?post=1794"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}