Repository: plotters-rs/plotters (language: rust)

Issue:
[Feature Request] 3D surface/contour/line plotting?
### What is the feature ?
Support for 3D plots (lines, contours, surfaces).

### (Optional) Why this feature is useful and how people would use the feature ?
Many mature plotting libraries provide support for plotting 3D shapes (lines, contours, surfaces). 
* [igiagkiozis/plotly](https://github.com/igiagkiozis/plotly#surface-plot), which is bindings for the plotly.js library, supports 3D lines and surfaces. 
* [siegelord/rustgnuplot](https://github.com/SiegeLord/RustGnuplot#features) supports 3D contours and surfaces. 

Now, I understand these libraries are actually just bindings for plotting libraries written in other languages, but [38/plotters](https://github.com/38/plotters) is the only library (that I have found so far) that natively supports the [EvCxR Jupyter Kernel](https://github.com/google/evcxr/tree/master/evcxr_jupyter). 3D plotting is one thing I think most people expect from a mature plotting library (at least in the data science realm). I'm aware, that 38/plotters is technically a drawing library vs a plotting library. But, even an example of how to generate a 3D plot would be a nice springboard.

Thoughts?


Discussion / hints:
Hi there, thank for using Plotters. You have already realized that the plotters is more like a drawing library vs plotting library. Unfortunately the low level drawing API only support for 2D drawing and 3D drawing is on my backlog for a long time. 

For the roadmap of the 3D plots, I am going to support CPU 3D rendering first which I am planning to support it after the next major release which is happening in this or next month. For hardware accelerated rendering, it will take a longer time. 

Cheers!
 


Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 3029d06c..891a2c19 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -61,7 +61,7 @@ default = [
         "image",
         "deprecated_items",  "all_series", "all_elements"
 ]
-all_series = ["area_series", "line_series", "point_series"]
+all_series = ["area_series", "line_series", "point_series", "surface_series"]
 all_elements = ["errorbar", "candlestick", "boxplot", "histogram"]
 
 # Tier 1 Backends
@@ -80,6 +80,7 @@ histogram = []
 area_series = []
 line_series = []
 point_series = []
+surface_series = []
 
 # Font implemnetation
 ttf = ["font-kit", "rusttype", "lazy_static"]
diff --git a/examples/3d-plot.rs b/examples/3d-plot.rs
new file mode 100644
index 00000000..7d27e1f9
--- /dev/null
+++ b/examples/3d-plot.rs
@@ -0,0 +1,47 @@
+use plotters::prelude::*;
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+    let area = SVGBackend::new("plotters-doc-data/3d-plot.svg", (1024, 760)).into_drawing_area();
+
+    area.fill(&WHITE)?;
+
+    let x_axis = (-3.0..3.0).step(0.1);
+    let z_axis = (-3.0..3.0).step(0.1);
+
+    let mut chart = ChartBuilder::on(&area)
+        .caption(format!("3D Plot Test"), ("sans", 20))
+        .build_cartesian_3d(x_axis.clone(), -3.0..3.0, z_axis.clone())?;
+
+    chart.with_projection(|mut pb| {
+        pb.yaw = 0.5;
+        pb.scale = 0.9;
+        pb.into_matrix()
+    });
+
+    chart.configure_axes().draw()?;
+
+    chart
+        .draw_series(SurfaceSeries::<f64, _, f64>::new(
+            x_axis.values(),
+            z_axis.values(),
+            |&x, &z| (x * x + z * z).cos(),
+            &BLUE.mix(0.2),
+        ))?
+        .label("Surface")
+        .legend(|(x, y)| Rectangle::new([(x + 5, y - 5), (x + 15, y + 5)], BLUE.mix(0.5).filled()));
+
+    chart
+        .draw_series(LineSeries::new(
+            (-100..100)
+                .map(|y| y as f64 / 40.0)
+                .map(|y| ((y * 10.0).sin(), y, (y * 10.0).cos())),
+            &BLACK,
+        ))?
+        .label("Line")
+        .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &BLACK));
+
+    chart
+        .configure_series_labels()
+        .border_style(&BLACK)
+        .draw()?;
+    Ok(())
+}
diff --git a/examples/wasm-demo/src/lib.rs b/examples/wasm-demo/src/lib.rs
index 1e3a38bd..99b270cb 100644
--- a/examples/wasm-demo/src/lib.rs
+++ b/examples/wasm-demo/src/lib.rs
@@ -3,6 +3,7 @@ use web_sys::HtmlCanvasElement;
 
 mod func_plot;
 mod mandelbrot;
+mod plot3d;
 
 #[global_allocator]
 static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
@@ -44,6 +45,11 @@ impl Chart {
         })
     }
 
+    pub fn plot3d(canvas: HtmlCanvasElement, pitch: f64, yaw: f64) -> Result<(), JsValue> {
+        plot3d::draw(canvas, pitch, yaw).map_err(|err| err.to_string())?;
+        Ok(())
+    }
+
     /// This function can be used to convert screen coordinates to
     /// chart coordinates.
     pub fn coord(&self, x: i32, y: i32) -> Option<Point> {
diff --git a/examples/wasm-demo/src/mandelbrot.rs b/examples/wasm-demo/src/mandelbrot.rs
index c6f332ae..6c6bc9ae 100644
--- a/examples/wasm-demo/src/mandelbrot.rs
+++ b/examples/wasm-demo/src/mandelbrot.rs
@@ -1,8 +1,8 @@
 use crate::DrawResult;
 use plotters::prelude::*;
+use plotters_canvas::CanvasBackend;
 use std::ops::Range;
 use web_sys::HtmlCanvasElement;
-use plotters_canvas::CanvasBackend;
 
 /// Draw Mandelbrot set
 pub fn draw(element: HtmlCanvasElement) -> DrawResult<impl Fn((i32, i32)) -> Option<(f64, f64)>> {
diff --git a/examples/wasm-demo/src/plot3d.rs b/examples/wasm-demo/src/plot3d.rs
new file mode 100644
index 00000000..d65bb818
--- /dev/null
+++ b/examples/wasm-demo/src/plot3d.rs
@@ -0,0 +1,42 @@
+use crate::DrawResult;
+use plotters::prelude::*;
+use plotters_canvas::CanvasBackend;
+use web_sys::HtmlCanvasElement;
+
+pub fn draw(canvas: HtmlCanvasElement, pitch: f64, yaw: f64) -> DrawResult<()> {
+    let area = CanvasBackend::with_canvas_object(canvas)
+        .unwrap()
+        .into_drawing_area();
+    area.fill(&WHITE)?;
+
+    let x_axis = (-3.0..3.0).step(0.1);
+    let z_axis = (-3.0..3.0).step(0.1);
+
+    let mut chart =
+        ChartBuilder::on(&area).build_cartesian_3d(x_axis.clone(), -3.0..3.0, z_axis.clone())?;
+
+    chart.with_projection(|mut pb| {
+        pb.yaw = yaw;
+        pb.pitch = pitch;
+        pb.scale = 0.7;
+        pb.into_matrix()
+    });
+
+    chart.configure_axes().draw()?;
+
+    chart.draw_series(SurfaceSeries::<f64, _, f64>::new(
+        x_axis.values(),
+        z_axis.values(),
+        |&x, &z| (x * x + z * z).cos(),
+        &BLUE.mix(0.2),
+    ))?;
+
+    chart.draw_series(LineSeries::new(
+        (-100..100)
+            .map(|y| y as f64 / 40.0)
+            .map(|y| ((y * 10.0).sin(), y, (y * 10.0).cos())),
+        &BLACK,
+    ))?;
+
+    Ok(())
+}
diff --git a/examples/wasm-demo/www/index.html b/examples/wasm-demo/www/index.html
index 30fc1693..0b330278 100644
--- a/examples/wasm-demo/www/index.html
+++ b/examples/wasm-demo/www/index.html
@@ -15,16 +15,21 @@ <h1>Plotters WebAssembly Demo</h1>
         <canvas id="canvas" width="600" height="400"></canvas>
 	    <div id="status">Loading WebAssembly...</div>
 	    <div id="control">
-		<label for="plot-type">Demo: </label>
-		<select id="plot-type">
-		    <option value="0">Graph of y=1</option>
-		    <option value="1">Graph of y=x</option>
-		    <option value="2">Graph of y=x^2</option>
-		    <option value="3">Graph of y=x^3</option>
-		    <option value="4">Graph of y=x^4</option>
-		    <option value="5">Graph of y=x^5</option>
-		    <option value="mandelbrot">Mandelbrot Set</option>
-		</select>
+			<label for="plot-type">Demo: </label>
+			<select id="plot-type">
+				<option value="0">Graph of y=1</option>
+				<option value="1">Graph of y=x</option>
+				<option value="2">Graph of y=x^2</option>
+				<option value="3">Graph of y=x^3</option>
+				<option value="4">Graph of y=x^4</option>
+				<option value="5">Graph of y=x^5</option>
+				<option value="mandelbrot">Mandelbrot Set</option>
+				<option value="3d-plot">3D Plot Demo</option>
+			</select>
+			<div id="3d-control" class="hide">
+				<label for="pitch">Pitch: </label> <input type="range" min="0" max="157" id="pitch" value="10"> <br/>
+				<label for="yaw">Yaw: </label> <input type="range" min="0" max="314" id="yaw" value="50">
+			</div>
 	    </div>
 	</main>
 	<footer>
diff --git a/examples/wasm-demo/www/index.js b/examples/wasm-demo/www/index.js
index f6abb18c..6cd4bd36 100644
--- a/examples/wasm-demo/www/index.js
+++ b/examples/wasm-demo/www/index.js
@@ -5,12 +5,21 @@ class Chart {}
 const canvas = document.getElementById("canvas");
 const coord = document.getElementById("coord");
 const plotType = document.getElementById("plot-type");
+const pitch = document.getElementById("pitch");
+const yaw = document.getElementById("yaw");
+const control = document.getElementById("3d-control");
 const status = document.getElementById("status");
 
 let chart = null;
 
 /** Main entry point */
 export function main() {
+	let hash = location.hash.substr(1);
+	for(var i = 0; i < plotType.options.length; i++) {
+		if(hash == plotType.options[i].value) {
+			plotType.value = hash;
+		}
+	}
     setupUI();
     setupCanvas();
 }
@@ -24,6 +33,10 @@ export function setup(WasmChart) {
 function setupUI() {
     status.innerText = "WebAssembly loaded!";
     plotType.addEventListener("change", updatePlot);
+	yaw.addEventListener("change", updatePlot);
+	pitch.addEventListener("change", updatePlot);
+	yaw.addEventListener("input", updatePlot);
+	pitch.addEventListener("input", updatePlot);
     window.addEventListener("resize", setupCanvas);
     window.addEventListener("mousemove", onMouseMove);
 }
@@ -58,15 +71,33 @@ function onMouseMove(event) {
     }
 }
 
+function updatePlot3d() {
+	let yaw_value = Number(yaw.value) / 100.0;
+	let pitch_value = Number(pitch.value) / 100.0;
+	Chart.plot3d(canvas, pitch_value, yaw_value);
+	coord.innerText = `Pitch:${pitch_value}, Yaw:${yaw_value}`
+}
+
 /** Redraw currently selected plot. */
 function updatePlot() {
     const selected = plotType.selectedOptions[0];
     status.innerText = `Rendering ${selected.innerText}...`;
     chart = null;
     const start = performance.now();
-    chart = (selected.value == "mandelbrot")
-        ? Chart.mandelbrot(canvas)
-        : Chart.power("canvas", Number(selected.value));
+	switch(selected.value) {
+		case "mandelbrot":
+			control.classList.add("hide");
+			chart = Chart.mandelbrot(canvas);
+			break;
+		case "3d-plot": 
+			control.classList.remove("hide");
+			updatePlot3d();
+			break;
+		default:
+			control.classList.add("hide");
+			chart = Chart.power("canvas", Number(selected.value))
+	}
+	
     const end = performance.now();
     status.innerText = `Rendered ${selected.innerText} in ${Math.ceil(end - start)}ms`;
 }
diff --git a/examples/wasm-demo/www/style.css b/examples/wasm-demo/www/style.css
index 135423a0..404d385b 100644
--- a/examples/wasm-demo/www/style.css
+++ b/examples/wasm-demo/www/style.css
@@ -48,3 +48,8 @@ footer {
     font-size: 12px;
     text-align: center;
 }
+
+.hide {
+	visibility: hidden;
+	height: 0px;
+}
diff --git a/src/chart/axes3d.rs b/src/chart/axes3d.rs
new file mode 100644
index 00000000..18494a77
--- /dev/null
+++ b/src/chart/axes3d.rs
@@ -0,0 +1,186 @@
+use std::marker::PhantomData;
+
+use super::ChartContext;
+use crate::coord::cartesian::Cartesian3d;
+use crate::coord::ranged1d::{BoldPoints, LightPoints, Ranged, ValueFormatter};
+use crate::style::colors::{BLACK, TRANSPARENT};
+use crate::style::Color;
+use crate::style::{AsRelative, ShapeStyle, SizeDesc, TextStyle};
+
+use super::Coord3D;
+
+use crate::drawing::DrawingAreaErrorKind;
+
+use plotters_backend::DrawingBackend;
+
+pub struct Axes3dStyle<'a, 'b, X: Ranged, Y: Ranged, Z: Ranged, DB: DrawingBackend> {
+    pub(super) parent_size: (u32, u32),
+    pub(super) target: Option<&'b mut ChartContext<'a, DB, Cartesian3d<X, Y, Z>>>,
+    pub(super) tick_size: i32,
+    pub(super) n_labels: [usize; 3],
+    pub(super) bold_line_style: ShapeStyle,
+    pub(super) light_line_style: ShapeStyle,
+    pub(super) axis_panel_style: ShapeStyle,
+    pub(super) axis_style: ShapeStyle,
+    pub(super) label_style: TextStyle<'b>,
+    pub(super) format_x: &'b dyn Fn(&X::ValueType) -> String,
+    pub(super) format_y: &'b dyn Fn(&Y::ValueType) -> String,
+    pub(super) format_z: &'b dyn Fn(&Z::ValueType) -> String,
+    _phantom: PhantomData<&'a (X, Y, Z)>,
+}
+
+impl<'a, 'b, X, Y, Z, XT, YT, ZT, DB> Axes3dStyle<'a, 'b, X, Y, Z, DB>
+where
+    X: Ranged<ValueType = XT> + ValueFormatter<XT>,
+    Y: Ranged<ValueType = YT> + ValueFormatter<YT>,
+    Z: Ranged<ValueType = ZT> + ValueFormatter<ZT>,
+    DB: DrawingBackend,
+{
+    pub fn tick_size<Size: SizeDesc>(&mut self, size: Size) -> &mut Self {
+        let actual_size = size.in_pixels(&self.parent_size);
+        self.tick_size = actual_size;
+        self
+    }
+
+    pub fn x_labels(&mut self, n: usize) -> &mut Self {
+        self.n_labels[0] = n;
+        self
+    }
+
+    pub fn y_labels(&mut self, n: usize) -> &mut Self {
+        self.n_labels[1] = n;
+        self
+    }
+
+    pub fn z_labels(&mut self, n: usize) -> &mut Self {
+        self.n_labels[2] = n;
+        self
+    }
+
+    pub fn axis_panel_style<S: Into<ShapeStyle>>(&mut self, style: S) -> &mut Self {
+        self.axis_panel_style = style.into();
+        self
+    }
+
+    pub fn bold_grid_style<S: Into<ShapeStyle>>(&mut self, style: S) -> &mut Self {
+        self.bold_line_style = style.into();
+        self
+    }
+
+    pub fn light_grid_style<S: Into<ShapeStyle>>(&mut self, style: S) -> &mut Self {
+        self.light_line_style = style.into();
+        self
+    }
+
+    pub fn label_style<S: Into<TextStyle<'b>>>(&mut self, style: S) -> &mut Self {
+        self.label_style = style.into();
+        self
+    }
+
+    pub fn x_formatter<F: Fn(&X::ValueType) -> String>(&mut self, f: &'b F) -> &mut Self {
+        self.format_x = f;
+        self
+    }
+
+    pub fn y_formatter<F: Fn(&Y::ValueType) -> String>(&mut self, f: &'b F) -> &mut Self {
+        self.format_y = f;
+        self
+    }
+
+    pub fn z_formatter<F: Fn(&Z::ValueType) -> String>(&mut self, f: &'b F) -> &mut Self {
+        self.format_z = f;
+        self
+    }
+
+    pub(crate) fn new(chart: &'b mut ChartContext<'a, DB, Cartesian3d<X, Y, Z>>) -> Self {
+        let parent_size = chart.drawing_area.dim_in_pixel();
+        let base_tick_size = (5u32).percent().max(5).in_pixels(chart.plotting_area());
+        let tick_size = base_tick_size;
+        Self {
+            parent_size,
+            tick_size,
+            n_labels: [10, 10, 10],
+            bold_line_style: Into::<ShapeStyle>::into(&BLACK.mix(0.2)),
+            light_line_style: Into::<ShapeStyle>::into(&TRANSPARENT),
+            axis_panel_style: Into::<ShapeStyle>::into(&BLACK.mix(0.1)),
+            axis_style: Into::<ShapeStyle>::into(&BLACK.mix(0.8)),
+            label_style: ("sans-serf", (12).percent().max(12).in_pixels(&parent_size)).into(),
+            format_x: &X::format,
+            format_y: &Y::format,
+            format_z: &Z::format,
+            _phantom: PhantomData,
+            target: Some(chart),
+        }
+    }
+    pub fn draw(&mut self) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
+    where
+        XT: Clone,
+        YT: Clone,
+        ZT: Clone,
+    {
+        let chart = self.target.take().unwrap();
+        let kps_bold = chart.get_key_points(
+            BoldPoints(self.n_labels[0]),
+            BoldPoints(self.n_labels[1]),
+            BoldPoints(self.n_labels[2]),
+        );
+        let kps_light = chart.get_key_points(
+            LightPoints::new(self.n_labels[0], self.n_labels[0] * 10),
+            LightPoints::new(self.n_labels[1], self.n_labels[1] * 10),
+            LightPoints::new(self.n_labels[2], self.n_labels[2] * 10),
+        );
+
+        let panels = chart.draw_axis_panels(
+            &kps_bold,
+            &kps_light,
+            self.axis_panel_style.clone(),
+            self.bold_line_style.clone(),
+            self.light_line_style.clone(),
+        )?;
+
+        for i in 0..3 {
+            let axis = chart.draw_axis(i, &panels, self.axis_style.clone())?;
+            let labels: Vec<_> = match i {
+                0 => kps_bold
+                    .x_points
+                    .iter()
+                    .map(|x| {
+                        let x_text = (self.format_x)(x);
+                        let mut p = axis[0].clone();
+                        p[0] = Coord3D::X(x.clone());
+                        (p, x_text)
+                    })
+                    .collect(),
+                1 => kps_bold
+                    .y_points
+                    .iter()
+                    .map(|y| {
+                        let y_text = (self.format_y)(y);
+                        let mut p = axis[0].clone();
+                        p[1] = Coord3D::Y(y.clone());
+                        (p, y_text)
+                    })
+                    .collect(),
+                _ => kps_bold
+                    .z_points
+                    .iter()
+                    .map(|z| {
+                        let z_text = (self.format_z)(z);
+                        let mut p = axis[0].clone();
+                        p[2] = Coord3D::Z(z.clone());
+                        (p, z_text)
+                    })
+                    .collect(),
+            };
+            chart.draw_axis_ticks(
+                axis,
+                &labels[..],
+                self.tick_size,
+                self.axis_style.clone(),
+                self.label_style.clone(),
+            )?;
+        }
+
+        Ok(())
+    }
+}
diff --git a/src/chart/builder.rs b/src/chart/builder.rs
index 051148a0..c4afe306 100644
--- a/src/chart/builder.rs
+++ b/src/chart/builder.rs
@@ -1,6 +1,6 @@
 use super::context::ChartContext;
 
-use crate::coord::cartesian::Cartesian2d;
+use crate::coord::cartesian::{Cartesian2d, Cartesian3d};
 use crate::coord::ranged1d::AsRangedCoord;
 use crate::coord::Shift;
 
@@ -306,6 +306,54 @@ impl<'a, 'b, DB: DrawingBackend> ChartBuilder<'a, 'b, DB> {
             ),
         })
     }
+
+    pub fn build_cartesian_3d<X: AsRangedCoord, Y: AsRangedCoord, Z: AsRangedCoord>(
+        &mut self,
+        x_spec: X,
+        y_spec: Y,
+        z_spec: Z,
+    ) -> Result<
+        ChartContext<'a, DB, Cartesian3d<X::CoordDescType, Y::CoordDescType, Z::CoordDescType>>,
+        DrawingAreaErrorKind<DB::ErrorType>,
+    > {
+        let mut drawing_area = DrawingArea::clone(self.root_area);
+
+        if *self.margin.iter().max().unwrap_or(&0) > 0 {
+            drawing_area = drawing_area.margin(
+                self.margin[0] as i32,
+                self.margin[1] as i32,
+                self.margin[2] as i32,
+                self.margin[3] as i32,
+            );
+        }
+
+        let (title_dx, title_dy) = if let Some((ref title, ref style)) = self.title {
+            let (origin_dx, origin_dy) = drawing_area.get_base_pixel();
+            drawing_area = drawing_area.titled(title, style.clone())?;
+            let (current_dx, current_dy) = drawing_area.get_base_pixel();
+            (current_dx - origin_dx, current_dy - origin_dy)
+        } else {
+            (0, 0)
+        };
+
+        let pixel_range = drawing_area.get_pixel_range();
+
+        Ok(ChartContext {
+            x_label_area: [None, None],
+            y_label_area: [None, None],
+            drawing_area: drawing_area.apply_coord_spec(Cartesian3d::new(
+                x_spec,
+                y_spec,
+                z_spec,
+                pixel_range,
+            )),
+            series_anno: vec![],
+            drawing_area_pos: (
+                title_dx + self.margin[2] as i32,
+                title_dy + self.margin[0] as i32,
+            ),
+        })
+    }
 }
 
 #[cfg(test)]
diff --git a/src/chart/context.rs b/src/chart/context.rs
index 4faf6bbf..a7c72585 100644
--- a/src/chart/context.rs
+++ b/src/chart/context.rs
@@ -1,14 +1,16 @@
 use std::borrow::Borrow;
 use std::ops::Range;
 
+use super::axes3d::Axes3dStyle;
 use super::{DualCoordChartContext, MeshStyle, SeriesAnno, SeriesLabelStyle};
 
-use crate::coord::cartesian::{Cartesian2d, MeshLine};
+use crate::coord::cartesian::{Cartesian2d, Cartesian3d, MeshLine};
 use crate::coord::ranged1d::{AsRangedCoord, KeyPointHint, Ranged, ValueFormatter};
+use crate::coord::ranged3d::{ProjectionMatrix, ProjectionMatrixBuilder};
 use crate::coord::{CoordTranslate, ReverseCoordTranslate, Shift};
 
 use crate::drawing::{DrawingArea, DrawingAreaErrorKind};
-use crate::element::{Drawable, PathElement, PointCollection};
+use crate::element::{Drawable, EmptyElement, PathElement, PointCollection, Polygon, Text};
 use crate::style::text_anchor::{HPos, Pos, VPos};
 use crate::style::{ShapeStyle, TextStyle};
 
@@ -529,6 +531,347 @@ impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Cartesia
     }
 }
 
+pub(super) struct KeyPoints3d<X: Ranged, Y: Ranged, Z: Ranged> {
+    pub(super) x_points: Vec<X::ValueType>,
+    pub(super) y_points: Vec<Y::ValueType>,
+    pub(super) z_points: Vec<Z::ValueType>,
+}
+
+#[derive(Clone, Debug)]
+pub(super) enum Coord3D<X, Y, Z> {
+    X(X),
+    Y(Y),
+    Z(Z),
+}
+
+impl<X, Y, Z> Coord3D<X, Y, Z> {
+    fn get_x(&self) -> &X {
+        match self {
+            Coord3D::X(ret) => ret,
+            _ => panic!("Invalid call!"),
+        }
+    }
+    fn get_y(&self) -> &Y {
+        match self {
+            Coord3D::Y(ret) => ret,
+            _ => panic!("Invalid call!"),
+        }
+    }
+    fn get_z(&self) -> &Z {
+        match self {
+            Coord3D::Z(ret) => ret,
+            _ => panic!("Invalid call!"),
+        }
+    }
+
+    fn build_coord([x, y, z]: [&Self; 3]) -> (X, Y, Z)
+    where
+        X: Clone,
+        Y: Clone,
+        Z: Clone,
+    {
+        (x.get_x().clone(), y.get_y().clone(), z.get_z().clone())
+    }
+}
+impl<'a, DB, X, Y, Z, XT, YT, ZT> ChartContext<'a, DB, Cartesian3d<X, Y, Z>>
+where
+    DB: DrawingBackend,
+    X: Ranged<ValueType = XT> + ValueFormatter<XT>,
+    Y: Ranged<ValueType = YT> + ValueFormatter<YT>,
+    Z: Ranged<ValueType = ZT> + ValueFormatter<ZT>,
+{
+    pub fn configure_axes(&mut self) -> Axes3dStyle<'a, '_, X, Y, Z, DB> {
+        Axes3dStyle::new(self)
+    }
+}
+impl<'a, DB, X: Ranged, Y: Ranged, Z: Ranged> ChartContext<'a, DB, Cartesian3d<X, Y, Z>>
+where
+    DB: DrawingBackend,
+{
+    pub fn with_projection<P: FnOnce(ProjectionMatrixBuilder) -> ProjectionMatrix>(
+        &mut self,
+        pf: P,
+    ) -> &mut Self {
+        let (actual_x, actual_y) = self.drawing_area.get_pixel_range();
+        self.drawing_area
+            .as_coord_spec_mut()
+            .set_projection(actual_x, actual_y, pf);
+        self
+    }
+}
+
+impl<'a, DB, X: Ranged, Y: Ranged, Z: Ranged> ChartContext<'a, DB, Cartesian3d<X, Y, Z>>
+where
+    DB: DrawingBackend,
+    X::ValueType: Clone,
+    Y::ValueType: Clone,
+    Z::ValueType: Clone,
+{
+    pub(super) fn get_key_points<XH: KeyPointHint, YH: KeyPointHint, ZH: KeyPointHint>(
+        &self,
+        x_hint: XH,
+        y_hint: YH,
+        z_hint: ZH,
+    ) -> KeyPoints3d<X, Y, Z> {
+        let coord = self.plotting_area().as_coord_spec();
+        let x_points = coord.logic_x.key_points(x_hint);
+        let y_points = coord.logic_y.key_points(y_hint);
+        let z_points = coord.logic_z.key_points(z_hint);
+        KeyPoints3d {
+            x_points,
+            y_points,
+            z_points,
+        }
+    }
+    pub(super) fn draw_axis_ticks(
+        &mut self,
+        axis: [[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3]; 2],
+        labels: &[(
+            [Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3],
+            String,
+        )],
+        tick_size: i32,
+        style: ShapeStyle,
+        font: TextStyle,
+    ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>> {
+        let coord = self.plotting_area().as_coord_spec();
+        let begin = coord.translate(&Coord3D::build_coord([
+            &axis[0][0],
+            &axis[0][1],
+            &axis[0][2],
+        ]));
+        let end = coord.translate(&Coord3D::build_coord([
+            &axis[1][0],
+            &axis[1][1],
+            &axis[1][2],
+        ]));
+        let axis_dir = (end.0 - begin.0, end.1 - begin.1);
+        let (x_range, y_range) = self.plotting_area().get_pixel_range();
+        let x_mid = (x_range.start + x_range.end) / 2;
+        let y_mid = (y_range.start + y_range.end) / 2;
+
+        let x_dir = if begin.0 < x_mid {
+            (-tick_size, 0)
+        } else {
+            (tick_size, 0)
+        };
+
+        let y_dir = if begin.1 < y_mid {
+            (0, -tick_size)
+        } else {
+            (0, tick_size)
+        };
+
+        let x_score = (x_dir.0 * axis_dir.0 + x_dir.1 * axis_dir.1).abs();
+        let y_score = (y_dir.0 * axis_dir.0 + y_dir.1 * axis_dir.1).abs();
+
+        let dir = if x_score < y_score { x_dir } else { y_dir };
+
+        for (pos, text) in labels {
+            let logic_pos = Coord3D::build_coord([&pos[0], &pos[1], &pos[2]]);
+            let mut font = font.clone();
+            if dir.0 < 0 {
+                font.pos = Pos::new(HPos::Right, VPos::Center);
+            } else if dir.0 > 0 {
+                font.pos = Pos::new(HPos::Left, VPos::Center);
+            };
+            if dir.1 < 0 {
+                font.pos = Pos::new(HPos::Center, VPos::Bottom);
+            } else if dir.1 > 0 {
+                font.pos = Pos::new(HPos::Center, VPos::Top);
+            };
+            let element = EmptyElement::at(logic_pos)
+                + PathElement::new(vec![(0, 0), dir], style.clone())
+                + Text::new(text.to_string(), (dir.0 * 2, dir.1 * 2), font.clone());
+            self.plotting_area().draw(&element)?;
+        }
+        Ok(())
+    }
+    pub(super) fn draw_axis(
+        &mut self,
+        idx: usize,
+        panels: &[[[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3]; 2]; 3],
+        style: ShapeStyle,
+    ) -> Result<
+        [[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3]; 2],
+        DrawingAreaErrorKind<DB::ErrorType>,
+    > {
+        let coord = self.plotting_area().as_coord_spec();
+        let x_range = coord.logic_x.range();
+        let y_range = coord.logic_y.range();
+        let z_range = coord.logic_z.range();
+
+        let ranges: [[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 2]; 3] = [
+            [Coord3D::X(x_range.start), Coord3D::X(x_range.end)],
+            [Coord3D::Y(y_range.start), Coord3D::Y(y_range.end)],
+            [Coord3D::Z(z_range.start), Coord3D::Z(z_range.end)],
+        ];
+
+        let (start, end) = {
+            let mut start = [&ranges[0][0], &ranges[1][0], &ranges[2][0]];
+            let mut end = [&ranges[0][1], &ranges[1][1], &ranges[2][1]];
+
+            let mut plan = vec![];
+
+            for i in 0..3 {
+                if i == idx {
+                    continue;
+                }
+                start[i] = &panels[i][0][i];
+                end[i] = &panels[i][0][i];
+                for j in 0..3 {
+                    if i != idx && i != j && j != idx {
+                        for k in 0..2 {
+                            start[j] = &panels[i][k][j];
+                            end[j] = &panels[i][k][j];
+                            plan.push((start, end));
+                        }
+                    }
+                }
+            }
+            plan.into_iter()
+                .min_by_key(|&(s, e)| {
+                    let d = coord.projected_depth(s[0].get_x(), s[1].get_y(), s[2].get_z());
+                    let d = d + coord.projected_depth(e[0].get_x(), e[1].get_y(), e[2].get_z());
+                    let (_, y1) = coord.translate(&Coord3D::build_coord(s));
+                    let (_, y2) = coord.translate(&Coord3D::build_coord(e));
+                    let y = y1 + y2;
+                    (d, y)
+                })
+                .unwrap()
+        };
+
+        self.plotting_area().draw(&PathElement::new(
+            vec![Coord3D::build_coord(start), Coord3D::build_coord(end)],
+            style.clone(),
+        ))?;
+
+        Ok([
+            [start[0].clone(), start[1].clone(), start[2].clone()],
+            [end[0].clone(), end[1].clone(), end[2].clone()],
+        ])
+    }
+    pub(super) fn draw_axis_panels(
+        &mut self,
+        bold_points: &KeyPoints3d<X, Y, Z>,
+        light_points: &KeyPoints3d<X, Y, Z>,
+        panel_style: ShapeStyle,
+        bold_grid_style: ShapeStyle,
+        light_grid_style: ShapeStyle,
+    ) -> Result<
+        [[[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3]; 2]; 3],
+        DrawingAreaErrorKind<DB::ErrorType>,
+    > {
+        let mut r_iter = (0..3).map(|idx| {
+            self.draw_axis_panel(
+                idx,
+                bold_points,
+                light_points,
+                panel_style.clone(),
+                bold_grid_style.clone(),
+                light_grid_style.clone(),
+            )
+        });
+        Ok([
+            r_iter.next().unwrap()?,
+            r_iter.next().unwrap()?,
+            r_iter.next().unwrap()?,
+        ])
+    }
+    fn draw_axis_panel(
+        &mut self,
+        idx: usize,
+        bold_points: &KeyPoints3d<X, Y, Z>,
+        light_points: &KeyPoints3d<X, Y, Z>,
+        panel_style: ShapeStyle,
+        bold_grid_style: ShapeStyle,
+        light_grid_style: ShapeStyle,
+    ) -> Result<
+        [[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3]; 2],
+        DrawingAreaErrorKind<DB::ErrorType>,
+    > {
+        let coord = self.plotting_area().as_coord_spec();
+        let x_range = coord.logic_x.range();
+        let y_range = coord.logic_y.range();
+        let z_range = coord.logic_z.range();
+
+        let ranges: [[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 2]; 3] = [
+            [Coord3D::X(x_range.start), Coord3D::X(x_range.end)],
+            [Coord3D::Y(y_range.start), Coord3D::Y(y_range.end)],
+            [Coord3D::Z(z_range.start), Coord3D::Z(z_range.end)],
+        ];
+
+        let (mut panel, start, end) = {
+            let a = [&ranges[0][0], &ranges[1][0], &ranges[2][0]];
+            let mut b = [&ranges[0][1], &ranges[1][1], &ranges[2][1]];
+            let mut c = a;
+            let d = b;
+
+            b[idx] = &ranges[idx][0];
+            c[idx] = &ranges[idx][1];
+
+            let (a, b) = if coord.projected_depth(a[0].get_x(), a[1].get_y(), a[2].get_z())
+                >= coord.projected_depth(c[0].get_x(), c[1].get_y(), c[2].get_z())
+            {
+                (a, b)
+            } else {
+                (c, d)
+            };
+
+            let mut m = a.clone();
+            m[(idx + 1) % 3] = b[(idx + 1) % 3];
+            let mut n = a.clone();
+            n[(idx + 2) % 3] = b[(idx + 2) % 3];
+
+            (
+                vec![
+                    Coord3D::build_coord(a),
+                    Coord3D::build_coord(m),
+                    Coord3D::build_coord(b),
+                    Coord3D::build_coord(n),
+                ],
+                a,
+                b,
+            )
+        };
+        self.plotting_area()
+            .draw(&Polygon::new(panel.clone(), panel_style.clone()))?;
+        panel.push(panel[0].clone());
+        self.plotting_area()
+            .draw(&PathElement::new(panel, bold_grid_style.clone()))?;
+
+        for (kps, style) in vec![
+            (light_points, light_grid_style),
+            (bold_points, bold_grid_style),
+        ]
+        .into_iter()
+        {
+            for idx in (0..3).filter(|&i| i != idx) {
+                let kps: Vec<_> = match idx {
+                    0 => kps.x_points.iter().map(|x| Coord3D::X(x.clone())).collect(),
+                    1 => kps.y_points.iter().map(|y| Coord3D::Y(y.clone())).collect(),
+                    _ => kps.z_points.iter().map(|z| Coord3D::Z(z.clone())).collect(),
+                };
+                for kp in kps.iter() {
+                    let mut kp_start = start;
+                    let mut kp_end = end;
+                    kp_start[idx] = kp;
+                    kp_end[idx] = kp;
+                    self.plotting_area().draw(&PathElement::new(
+                        vec![Coord3D::build_coord(kp_start), Coord3D::build_coord(kp_end)],
+                        style.clone(),
+                    ))?;
+                }
+            }
+        }
+
+        Ok([
+            [start[0].clone(), start[1].clone(), start[2].clone()],
+            [end[0].clone(), end[1].clone(), end[2].clone()],
+        ])
+    }
+}
+
 #[cfg(test)]
 mod test {
     use crate::prelude::*;
@@ -577,4 +920,35 @@ mod test {
             .draw()
             .expect("Drawing error");
     }
+
+
+    #[test]
+    fn test_chart_context_3d() {
+        let drawing_area = create_mocked_drawing_area(200, 200, |_| {});
+
+        drawing_area.fill(&WHITE).expect("Fill");
+
+        let mut chart = ChartBuilder::on(&drawing_area)
+            .caption("Test Title", ("serif", 10))
+            .x_label_area_size(20)
+            .y_label_area_size(20)
+            .set_label_area_size(LabelAreaPosition::Top, 20)
+            .set_label_area_size(LabelAreaPosition::Right, 20)
+            .build_cartesian_3d(0..10, 0..10, 0..10)
+            .expect("Create chart");
+        
+        chart.with_projection(|mut pb| {
+            pb.yaw = 0.5;
+            pb.pitch = 0.5;
+            pb.scale = 0.5;
+            pb.into_matrix()
+        });
+
+        chart.configure_axes().draw().expect("Drawing axes");
+
+
+        chart
+            .draw_series(std::iter::once(Circle::new((5, 5, 5), 5, &RED)))
+            .expect("Drawing error");
+    }
 }
diff --git a/src/chart/mod.rs b/src/chart/mod.rs
index 44a2d5c8..4a880296 100644
--- a/src/chart/mod.rs
+++ b/src/chart/mod.rs
@@ -12,6 +12,7 @@ In Plotters, a series is abstracted as an iterator of elements.
 detailed description for each struct.
 */
 
+mod axes3d;
 mod builder;
 mod context;
 mod dual_coord;
@@ -25,3 +26,5 @@ pub use dual_coord::{DualCoordChartContext, DualCoordChartState};
 pub use mesh::{MeshStyle, SecondaryMeshStyle};
 pub use series::{SeriesAnno, SeriesLabelPosition, SeriesLabelStyle};
 pub use state::ChartState;
+
+use context::Coord3D;
diff --git a/src/coord/mod.rs b/src/coord/mod.rs
index 4787aecb..5cc17080 100644
--- a/src/coord/mod.rs
+++ b/src/coord/mod.rs
@@ -43,7 +43,12 @@ pub mod types {
 }
 
 mod ranged2d;
-pub use ranged2d::cartesian;
+pub mod ranged3d;
+
+pub mod cartesian {
+    pub use super::ranged2d::cartesian::{Cartesian2d, MeshLine};
+    pub use super::ranged3d::Cartesian3d;
+}
 
 mod translate;
 pub use translate::{CoordTranslate, ReverseCoordTranslate};
diff --git a/src/coord/ranged1d/types/numeric.rs b/src/coord/ranged1d/types/numeric.rs
index 0c10ebe4..22d4f859 100644
--- a/src/coord/ranged1d/types/numeric.rs
+++ b/src/coord/ranged1d/types/numeric.rs
@@ -2,7 +2,8 @@ use std::convert::TryFrom;
 use std::ops::Range;
 
 use crate::coord::ranged1d::{
-    AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, Ranged, ReversibleRanged,
+    AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged,
+    ReversibleRanged, ValueFormatter,
 };
 
 macro_rules! impl_discrete_trait {
@@ -58,7 +59,7 @@ macro_rules! impl_reverse_mapping_trait {
     };
 }
 macro_rules! make_numeric_coord {
-    ($type:ty, $name:ident, $key_points:ident, $doc: expr) => {
+    ($type:ty, $name:ident, $key_points:ident, $doc: expr, $fmt: ident) => {
         #[doc = $doc]
         #[derive(Clone)]
         pub struct $name($type, $type);
@@ -68,7 +69,7 @@ macro_rules! make_numeric_coord {
             }
         }
         impl Ranged for $name {
-            type FormatOption = DefaultFormatting;
+            type FormatOption = $fmt;
             type ValueType = $type;
             #[allow(clippy::float_cmp)]
             fn map(&self, v: &$type, limit: (i32, i32)) -> i32 {
@@ -96,6 +97,9 @@ macro_rules! make_numeric_coord {
             }
         }
     };
+    ($type:ty, $name:ident, $key_points:ident, $doc: expr) => {
+        make_numeric_coord!($type, $name, $key_points, $doc, DefaultFormatting);
+    }
 }
 
 macro_rules! gen_key_points_comp {
@@ -207,16 +211,28 @@ make_numeric_coord!(
     f32,
     RangedCoordf32,
     compute_f32_key_points,
-    "The ranged coordinate for type f32"
+    "The ranged coordinate for type f32",
+    NoDefaultFormatting
 );
 impl_reverse_mapping_trait!(f32, RangedCoordf32);
+impl ValueFormatter<f32> for RangedCoordf32 {
+    fn format(value: &f32) -> String {
+        crate::data::float::pretty_print_float(*value as f64, false)
+    }
+}
 make_numeric_coord!(
     f64,
     RangedCoordf64,
     compute_f64_key_points,
-    "The ranged coordinate for type f64"
+    "The ranged coordinate for type f64",
+    NoDefaultFormatting
 );
 impl_reverse_mapping_trait!(f64, RangedCoordf64);
+impl ValueFormatter<f64> for RangedCoordf64 {
+    fn format(value: &f64) -> String {
+        crate::data::float::pretty_print_float(*value, false)
+    }
+}
 make_numeric_coord!(
     u32,
     RangedCoordu32,
diff --git a/src/coord/ranged3d/cartesian3d.rs b/src/coord/ranged3d/cartesian3d.rs
new file mode 100644
index 00000000..bdee473a
--- /dev/null
+++ b/src/coord/ranged3d/cartesian3d.rs
@@ -0,0 +1,99 @@
+use super::{ProjectionMatrix, ProjectionMatrixBuilder};
+use crate::coord::ranged1d::Ranged;
+use crate::coord::CoordTranslate;
+use plotters_backend::BackendCoord;
+
+use std::ops::Range;
+
+pub struct Cartesian3d<X: Ranged, Y: Ranged, Z: Ranged> {
+    pub(crate) logic_x: X,
+    pub(crate) logic_y: Y,
+    pub(crate) logic_z: Z,
+    coord_size: (i32, i32, i32),
+    projection: ProjectionMatrix,
+}
+
+impl<X: Ranged, Y: Ranged, Z: Ranged> Cartesian3d<X, Y, Z> {
+    fn compute_default_size(actual_x: Range<i32>, actual_y: Range<i32>) -> i32 {
+        (actual_x.end - actual_x.start).min(actual_y.end - actual_y.start) * 4 / 5
+    }
+    fn create_projection<F: FnOnce(ProjectionMatrixBuilder) -> ProjectionMatrix>(
+        actual_x: Range<i32>,
+        actual_y: Range<i32>,
+        f: F,
+    ) -> ProjectionMatrix {
+        let default_size = Self::compute_default_size(actual_x.clone(), actual_y.clone());
+        let center_3d = (default_size / 2, default_size / 2, default_size / 2);
+        let center_2d = (
+            (actual_x.end + actual_x.start) / 2,
+            (actual_y.end + actual_y.start) / 2,
+        );
+        let mut pb = ProjectionMatrixBuilder::new();
+        pb.set_pivot(center_3d, center_2d);
+        f(pb)
+    }
+    pub fn with_projection<
+        SX: Into<X>,
+        SY: Into<Y>,
+        SZ: Into<Z>,
+        F: FnOnce(ProjectionMatrixBuilder) -> ProjectionMatrix,
+    >(
+        logic_x: SX,
+        logic_y: SY,
+        logic_z: SZ,
+        (actual_x, actual_y): (Range<i32>, Range<i32>),
+        build_projection_matrix: F,
+    ) -> Self {
+        let default_size = Self::compute_default_size(actual_x.clone(), actual_y.clone());
+        Self {
+            logic_x: logic_x.into(),
+            logic_y: logic_y.into(),
+            logic_z: logic_z.into(),
+            coord_size: (default_size, default_size, default_size),
+            projection: Self::create_projection(actual_x, actual_y, build_projection_matrix),
+        }
+    }
+    pub fn set_projection<F: FnOnce(ProjectionMatrixBuilder) -> ProjectionMatrix>(
+        &mut self,
+        actual_x: Range<i32>,
+        actual_y: Range<i32>,
+        f: F,
+    ) -> &mut Self {
+        self.projection = Self::create_projection(actual_x, actual_y, f);
+        self
+    }
+
+    pub fn new<SX: Into<X>, SY: Into<Y>, SZ: Into<Z>>(
+        logic_x: SX,
+        logic_y: SY,
+        logic_z: SZ,
+        (actual_x, actual_y): (Range<i32>, Range<i32>),
+    ) -> Self {
+        Self::with_projection(logic_x, logic_y, logic_z, (actual_x, actual_y), |pb| {
+            pb.into_matrix()
+        })
+    }
+    pub fn projection(&self) -> &ProjectionMatrix {
+        &self.projection
+    }
+
+    pub fn map_3d(&self, x: &X::ValueType, y: &Y::ValueType, z: &Z::ValueType) -> (i32, i32, i32) {
+        (
+            self.logic_x.map(x, (0, self.coord_size.0)),
+            self.logic_y.map(y, (0, self.coord_size.1)),
+            self.logic_z.map(z, (0, self.coord_size.2)),
+        )
+    }
+
+    pub fn projected_depth(&self, x: &X::ValueType, y: &Y::ValueType, z: &Z::ValueType) -> i32 {
+        self.projection.projected_depth(self.map_3d(x, y, z))
+    }
+}
+
+impl<X: Ranged, Y: Ranged, Z: Ranged> CoordTranslate for Cartesian3d<X, Y, Z> {
+    type From = (X::ValueType, Y::ValueType, Z::ValueType);
+    fn translate(&self, coord: &Self::From) -> BackendCoord {
+        let pixel_coord_3d = self.map_3d(&coord.0, &coord.1, &coord.2);
+        self.projection * pixel_coord_3d
+    }
+}
diff --git a/src/coord/ranged3d/mod.rs b/src/coord/ranged3d/mod.rs
new file mode 100644
index 00000000..274a70d8
--- /dev/null
+++ b/src/coord/ranged3d/mod.rs
@@ -0,0 +1,5 @@
+mod projection;
+pub use projection::{ProjectionMatrix, ProjectionMatrixBuilder};
+
+mod cartesian3d;
+pub use cartesian3d::Cartesian3d;
diff --git a/src/coord/ranged3d/projection.rs b/src/coord/ranged3d/projection.rs
new file mode 100644
index 00000000..bbd6465d
--- /dev/null
+++ b/src/coord/ranged3d/projection.rs
@@ -0,0 +1,186 @@
+use std::f64::consts::PI;
+use std::ops::Mul;
+
+#[derive(Clone, Debug, Copy)]
+pub struct ProjectionMatrix([[f64; 4]; 4]);
+
+impl AsRef<[[f64; 4]; 4]> for ProjectionMatrix {
+    fn as_ref(&self) -> &[[f64; 4]; 4] {
+        &self.0
+    }
+}
+
+impl AsMut<[[f64; 4]; 4]> for ProjectionMatrix {
+    fn as_mut(&mut self) -> &mut [[f64; 4]; 4] {
+        &mut self.0
+    }
+}
+
+impl From<[[f64; 4]; 4]> for ProjectionMatrix {
+    fn from(data: [[f64; 4]; 4]) -> Self {
+        ProjectionMatrix(data)
+    }
+}
+
+impl Default for ProjectionMatrix {
+    fn default() -> Self {
+        ProjectionMatrix::rotate(PI, 0.0, 0.0)
+    }
+}
+
+impl Mul<ProjectionMatrix> for ProjectionMatrix {
+    type Output = ProjectionMatrix;
+    fn mul(self, other: ProjectionMatrix) -> ProjectionMatrix {
+        let mut ret = ProjectionMatrix::zero();
+        for r in 0..4 {
+            for c in 0..4 {
+                for k in 0..4 {
+                    ret.0[r][c] += other.0[r][k] * self.0[k][c];
+                }
+            }
+        }
+        ret.normalize();
+        ret
+    }
+}
+
+impl Mul<(i32, i32, i32)> for ProjectionMatrix {
+    type Output = (i32, i32);
+    fn mul(self, (x, y, z): (i32, i32, i32)) -> (i32, i32) {
+        let (x, y, z) = (x as f64, y as f64, z as f64);
+        let m = self.0;
+        (
+            (x * m[0][0] + y * m[0][1] + z * m[0][2] + m[0][3]) as i32,
+            (x * m[1][0] + y * m[1][1] + z * m[1][2] + m[1][3]) as i32,
+        )
+    }
+}
+
+impl Mul<(f64, f64, f64)> for ProjectionMatrix {
+    type Output = (i32, i32);
+    fn mul(self, (x, y, z): (f64, f64, f64)) -> (i32, i32) {
+        let m = self.0;
+        (
+            (x * m[0][0] + y * m[0][1] + z * m[0][2] + m[0][3]) as i32,
+            (x * m[1][0] + y * m[1][1] + z * m[1][2] + m[1][3]) as i32,
+        )
+    }
+}
+
+impl ProjectionMatrix {
+    pub fn one() -> Self {
+        ProjectionMatrix([
+            [1.0, 0.0, 0.0, 0.0],
+            [0.0, 1.0, 0.0, 0.0],
+            [0.0, 0.0, 1.0, 0.0],
+            [0.0, 0.0, 0.0, 1.0],
+        ])
+    }
+    pub fn zero() -> Self {
+        ProjectionMatrix([[0.0; 4]; 4])
+    }
+    pub fn shift(x: f64, y: f64, z: f64) -> Self {
+        ProjectionMatrix([
+            [1.0, 0.0, 0.0, x],
+            [0.0, 1.0, 0.0, y],
+            [0.0, 0.0, 1.0, z],
+            [0.0, 0.0, 0.0, 1.0],
+        ])
+    }
+    pub fn rotate(x: f64, y: f64, z: f64) -> Self {
+        let (c, b, a) = (x, y, z);
+        ProjectionMatrix([
+            [
+                a.cos() * b.cos(),
+                a.cos() * b.sin() * c.sin() - a.sin() * c.cos(),
+                a.cos() * b.sin() * c.cos() + a.sin() * c.sin(),
+                0.0,
+            ],
+            [
+                a.sin() * b.cos(),
+                a.sin() * b.sin() * c.sin() + a.cos() * c.cos(),
+                a.sin() * b.sin() * c.cos() - a.cos() * c.sin(),
+                0.0,
+            ],
+            [-b.sin(), b.cos() * c.sin(), b.cos() * c.cos(), 0.0],
+            [0.0, 0.0, 0.0, 1.0],
+        ])
+    }
+    pub fn scale(factor: f64) -> Self {
+        ProjectionMatrix([
+            [1.0, 0.0, 0.0, 0.0],
+            [0.0, 1.0, 0.0, 0.0],
+            [0.0, 0.0, 1.0, 0.0],
+            [0.0, 0.0, 0.0, 1.0 / factor],
+        ])
+    }
+    pub fn normalize(&mut self) {
+        if self.0[3][3] > 1e-20 {
+            for r in 0..4 {
+                for c in 0..4 {
+                    self.0[r][c] /= self.0[3][3];
+                }
+            }
+        }
+    }
+
+    pub fn projected_depth(&self, (x, y, z): (i32, i32, i32)) -> i32 {
+        let r = &self.0[2];
+        (r[0] * x as f64 + r[1] * y as f64 + r[2] * z as f64 + r[3]) as i32
+    }
+}
+
+#[derive(Copy, Clone)]
+pub struct ProjectionMatrixBuilder {
+    pub yaw: f64,
+    pub pitch: f64,
+    pub scale: f64,
+    pivot_before: (i32, i32, i32),
+    pivot_after: (i32, i32),
+}
+
+impl ProjectionMatrixBuilder {
+    pub fn new() -> Self {
+        Self {
+            yaw: 0.5,
+            pitch: 0.15,
+            scale: 1.0,
+            pivot_after: (0, 0),
+            pivot_before: (0, 0, 0),
+        }
+    }
+
+    pub fn set_pivot(&mut self, before: (i32, i32, i32), after: (i32, i32)) -> &mut Self {
+        self.pivot_before = before;
+        self.pivot_after = after;
+        self
+    }
+
+    pub fn into_matrix(self) -> ProjectionMatrix {
+        let mut ret = if self.pivot_before == (0, 0, 0) {
+            ProjectionMatrix::default()
+        } else {
+            let (x, y, z) = self.pivot_before;
+            ProjectionMatrix::shift(-x as f64, -y as f64, -z as f64) * ProjectionMatrix::default()
+        };
+
+        if self.yaw.abs() > 1e-20 {
+            ret = ret * ProjectionMatrix::rotate(0.0, self.yaw, 0.0);
+        }
+
+        if self.pitch.abs() > 1e-20 {
+            ret = ret * ProjectionMatrix::rotate(self.pitch, 0.0, 0.0);
+        }
+
+        if (self.scale - 1.0).abs() > 1e-20 {
+            ret = ret * ProjectionMatrix::scale(self.scale);
+        }
+
+        if self.pivot_after != (0, 0) {
+            let (x, y) = self.pivot_after;
+            ret = ret * ProjectionMatrix::shift(x as f64, y as f64, 0.0);
+        }
+
+        ret
+    }
+}
diff --git a/src/drawing/area.rs b/src/drawing/area.rs
index ae1e1630..511dc08d 100644
--- a/src/drawing/area.rs
+++ b/src/drawing/area.rs
@@ -235,6 +235,14 @@ impl<DB: DrawingBackend, CT: CoordTranslate> DrawingArea<DB, CT> {
         }
     }
 
+    pub fn use_screen_coord(&self) -> DrawingArea<DB, Shift> {
+        DrawingArea {
+            rect: self.rect.clone(),
+            backend: self.backend.clone(),
+            coord: Shift((0, 0)),
+        }
+    }
+
     /// Get the area dimension in pixel
     pub fn dim_in_pixel(&self) -> (u32, u32) {
         (
@@ -517,6 +525,10 @@ impl<DB: DrawingBackend, CT: CoordTranslate> DrawingArea<DB, CT> {
     pub fn as_coord_spec(&self) -> &CT {
         &self.coord
     }
+
+    pub fn as_coord_spec_mut(&mut self) -> &mut CT {
+        &mut self.coord
+    }
 }
 
 #[cfg(test)]
diff --git a/src/lib.rs b/src/lib.rs
index 954129c8..972b8d91 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -738,6 +738,8 @@ pub mod prelude {
     pub use crate::series::LineSeries;
     #[cfg(feature = "point_series")]
     pub use crate::series::PointSeries;
+    #[cfg(feature = "surface_series")]
+    pub use crate::series::SurfaceSeries;
 
     // Styles
     pub use crate::style::{
diff --git a/src/series/mod.rs b/src/series/mod.rs
index 103135e0..a0c8f198 100644
--- a/src/series/mod.rs
+++ b/src/series/mod.rs
@@ -18,6 +18,8 @@ mod histogram;
 mod line_series;
 #[cfg(feature = "point_series")]
 mod point_series;
+#[cfg(feature = "surface_series")]
+mod surface;
 
 #[cfg(feature = "area_series")]
 pub use area_series::AreaSeries;
@@ -27,3 +29,5 @@ pub use histogram::Histogram;
 pub use line_series::LineSeries;
 #[cfg(feature = "point_series")]
 pub use point_series::PointSeries;
+#[cfg(feature = "surface_series")]
+pub use surface::SurfaceSeries;
diff --git a/src/series/surface.rs b/src/series/surface.rs
new file mode 100644
index 00000000..8628429a
--- /dev/null
+++ b/src/series/surface.rs
@@ -0,0 +1,76 @@
+use crate::element::Polygon;
+use crate::style::ShapeStyle;
+pub struct SurfaceSeries<X, Y, Z> {
+    x_data: Vec<X>,
+    y_data: Vec<Y>,
+    z_data: Vec<Z>,
+    style: ShapeStyle,
+    size: usize,
+    state: usize,
+}
+
+impl<X, Y, Z> SurfaceSeries<X, Y, Z> {
+    pub fn new<XS, ZS, YF, S>(xs: XS, zs: ZS, y_func: YF, style: S) -> Self
+    where
+        YF: Fn(&X, &Z) -> Y,
+        XS: Iterator<Item = X>,
+        ZS: Iterator<Item = Z>,
+        S: Into<ShapeStyle>,
+    {
+        let x_data: Vec<_> = xs.collect();
+        let z_data: Vec<_> = zs.collect();
+        let y_data: Vec<_> = x_data
+            .iter()
+            .map(|x| z_data.iter().map(move |z| (x, z)))
+            .flatten()
+            .map(|(x, z)| y_func(x, z))
+            .collect();
+        let size = (x_data.len().max(1) - 1) * (z_data.len().max(1) - 1);
+        Self {
+            x_data,
+            y_data,
+            z_data,
+            style: style.into(),
+            size,
+            state: 0,
+        }
+    }
+
+    fn point_at(&self, x: usize, z: usize) -> (X, Y, Z)
+    where
+        X: Clone,
+        Y: Clone,
+        Z: Clone,
+    {
+        (
+            self.x_data[x].clone(),
+            self.y_data[x * self.z_data.len() + z].clone(),
+            self.z_data[z].clone(),
+        )
+    }
+}
+
+impl<X: Clone, Y: Clone, Z: Clone> Iterator for SurfaceSeries<X, Y, Z> {
+    type Item = Polygon<(X, Y, Z)>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.size <= self.state {
+            return None;
+        }
+
+        let x = self.state / (self.z_data.len() - 1);
+        let z = self.state % (self.z_data.len() - 1);
+
+        self.state += 1;
+
+        Some(Polygon::new(
+            vec![
+                self.point_at(x, z),
+                self.point_at(x, z + 1),
+                self.point_at(x + 1, z + 1),
+                self.point_at(x + 1, z),
+            ],
+            self.style.clone(),
+        ))
+    }
+}


Test patch: