Repository: ManimCommunity/manim

Issue:
z-axis and parametric surface misplaced in ThreeDAxes
## Description of bug / unexpected behavior
<!-- Add a clear and concise description of the problem you encountered. -->
When using `ThreeDAxes` in v0.6.0 to create a plot where the minimum values of the x-axis and y-axis are not 0, the z-axis is not adjusted to go through where the x-axis and y-axis cross.  Meanwhile, any parametric surface added to the plot is also not moved to account for the shift in the x-axis and y-axis.  Here is an example:

https://user-images.githubusercontent.com/79891353/116828508-0c7c3000-ab54-11eb-9bb6-03411961d5df.mp4

## Expected behavior
<!-- Add a clear and concise description of what you expected to happen. -->
The z-axis should automatically adjust, just as the x-axis and y-axis both adjust according to the `x_range` and `y_range` variables.  Likewise, when adding a parametric surface, or any `mobject` to the axes, it too should be adjusted to the correct position.

## How to reproduce the issue
<!-- Provide a piece of code illustrating the undesired behavior. -->

<details><summary>Code for reproducing the problem</summary>

```py
from manim import *
import numpy as np

class ThreeDSurfacePlot(ThreeDScene):
    def construct(self):
        resolution_fa = 42
        self.set_camera_orientation(phi=75 * DEGREES, theta=10 * DEGREES)
        
        axes = ThreeDAxes(
                x_range=(5, 10, 1),
                y_range=(3, 8, 1),
                z_range=(-1, 1, 0.5)
                )

        def param_surface(u, v):
            x = u
            y = v
            z = np.sin(x) * np.cos(y)
            return z

        surface_plane = ParametricSurface(
            lambda u, v: axes.c2p(u, v, param_surface(u, v)),
            resolution=(resolution_fa, resolution_fa),
            v_min=5,
            v_max=10,
            u_min=3,
            u_max=8,
        )

        surface_plane.set_style(fill_opacity=1, stroke_color=GREEN)
        surface_plane.set_fill_by_checkerboard(ORANGE, BLUE, opacity=0.5)
                                    
        self.add(axes, surface_plane)
        
        self.begin_ambient_camera_rotation(rate=-0.3)
        self.wait(10)
```

</details>


## System specifications

<details><summary>System Details</summary>
Manim v0.6.0
</details>


## Additional comments
<!-- Add further context that you think might be relevant for this issue here. -->
I will try to investigate this issue and report back my findings.


Discussion:
It looks like this issue is also happening for 2D graphs using `Axes`, as can be seen here:

![ParametricPlot_ManimCE_v0 6 0](https://user-images.githubusercontent.com/79891353/116948065-3ce6cb80-ac33-11eb-97d0-e5796d16e4b6.png)

Here is the code to reproduce this graph:

```py
from manim import *
import numpy as np

class ParametricPlot(Scene):
    def construct(self):
        axes = Axes(
                x_range=(5, 10, 1),
                y_range=(3, 8, 1)
                )
        
        trig_graph = ParametricFunction(lambda t: axes.c2p(t, 2 * np.sin(t) + 5), color=BLUE, t_range=(5, 10, 1))
        
        VGroup(axes, trig_graph).center()
        VGroup(axes, trig_graph).scale(0.5)
        self.add(axes, trig_graph)
```

The axes get shifted when created in /manim/mobject/coordinate_systems.py:

```py
    def create_axis(self, range_terms, axis_config, length):
        """Creates an axis and dynamically adjusts its position depending on where 0 is located on the line.
        Parameters
        ----------
        range_terms : Union[:class:`list`, :class:`numpy.ndarray`]
            The range of the the axis : `(x_min, x_max, x_step)`.
        axis_config : :class:`dict`
            Additional parameters that are passed to :class:`NumberLine`.
        length : :class:`float`
            The length of the axis.
        """
        new_config = merge_dicts_recursively(self.axis_config, axis_config)
        new_config["length"] = length
        axis = NumberLine(range_terms, **new_config)

        # without the if/elif, graph does not exist when min > 0 or max < 0
        # shifts the axis so that 0 is centered
        if range_terms[0] > 0:
            axis.shift(-axis.number_to_point(range_terms[0]))
        elif range_terms[1] < 0:
            axis.shift(-axis.number_to_point(range_terms[1]))
        else:
            axis.shift(-axis.number_to_point(0))
        return axis
```

But then c2p (coords_to_point) uses 0 as origin, which doesn't take into account the shift:

```py
    def coords_to_point(self, *coords):
        origin = self.x_axis.number_to_point(0)
        result = np.array(origin)
        for axis, coord in zip(self.get_axes(), coords):
            result += axis.number_to_point(coord) - origin
        return result
```

As for the stray z-axis, there isn't any code shifting that one due to the shifts happening in the x and y axes.

I will try to fix these issues and submit a pull request.

Resolving patch:
diff --git a/manim/mobject/coordinate_systems.py b/manim/mobject/coordinate_systems.py
--- a/manim/mobject/coordinate_systems.py
+++ b/manim/mobject/coordinate_systems.py
@@ -5,7 +5,7 @@
 
 import math
 import numbers
-from typing import Iterable, Optional, Sequence
+from typing import Iterable, List, Optional, Sequence
 
 import numpy as np
 
@@ -246,18 +246,13 @@ def create_axis(self, range_terms, axis_config, length):
         new_config["length"] = length
         axis = NumberLine(range_terms, **new_config)
 
-        # without the if/elif, graph does not exist when min > 0 or max < 0
+        # without the call to origin_shift, graph does not exist when min > 0 or max < 0
         # shifts the axis so that 0 is centered
-        if range_terms[0] > 0:
-            axis.shift(-axis.number_to_point(range_terms[0]))
-        elif range_terms[1] < 0:
-            axis.shift(-axis.number_to_point(range_terms[1]))
-        else:
-            axis.shift(-axis.number_to_point(0))
+        axis.shift(-axis.number_to_point(self.origin_shift(range_terms)))
         return axis
 
     def coords_to_point(self, *coords):
-        origin = self.x_axis.number_to_point(0)
+        origin = self.x_axis.number_to_point(self.origin_shift(self.x_range))
         result = np.array(origin)
         for axis, coord in zip(self.get_axes(), coords):
             result += axis.number_to_point(coord) - origin
@@ -367,6 +362,22 @@ def construct(self):
 
         return line_graph
 
+    @staticmethod
+    def origin_shift(axis_range: List[float]) -> float:
+        """Determines how to shift graph mobjects to compensate when 0 is not on the axis.
+
+        Parameters
+        ----------
+        axis_range
+            The range of the axis : ``(x_min, x_max, x_step)``.
+        """
+        if axis_range[0] > 0:
+            return axis_range[0]
+        if axis_range[1] < 0:
+            return axis_range[1]
+        else:
+            return 0
+
 
 class ThreeDAxes(Axes):
     """A 3-dimensional set of axes.
@@ -444,7 +455,7 @@ def __init__(
         z_axis = self.create_axis(self.z_range, self.z_axis_config, self.z_length)
         z_axis.rotate_about_zero(-PI / 2, UP)
         z_axis.rotate_about_zero(angle_of_vector(self.z_normal))
-        z_axis.shift(self.x_axis.n2p(0))
+        z_axis.shift(self.x_axis.number_to_point(self.origin_shift(x_range)))
 
         self.axes.add(z_axis)
         self.add(z_axis)


Test patch:
diff --git a/tests/test_axes_shift.py b/tests/test_axes_shift.py
--- a/tests/test_axes_shift.py
+++ b/tests/test_axes_shift.py
@@ -1,6 +1,7 @@
 import numpy as np
 import pytest
 
+from manim.mobject.coordinate_systems import Axes
 from manim.scene.graph_scene import GraphScene
 
 
@@ -42,3 +43,9 @@ def test_axes_with_xy_shift():
     G.setup_axes()
     assert all(np.isclose(G.graph_origin, G.x_axis.n2p(1)))
     assert all(np.isclose(G.graph_origin, G.y_axis.n2p(-5)))
+
+
+def test_axes_origin_shift():
+    ax = Axes(x_range=(5, 10, 1), y_range=(40, 45, 0.5))
+    assert np.allclose(ax.coords_to_point(5, 40), ax.x_axis.number_to_point(5))
+    assert np.allclose(ax.coords_to_point(5, 40), ax.y_axis.number_to_point(40))