blob: eac4d53bfd10c8ceafede10d09383c2adef06b65 [file] [log] [blame]
use std::borrow::Borrow;
use std::ops::Range;
use super::axes3d::Axes3dStyle;
use super::{DualCoordChartContext, MeshStyle, SeriesAnno, SeriesLabelStyle};
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, EmptyElement, PathElement, PointCollection, Polygon, Text};
use crate::style::text_anchor::{HPos, Pos, VPos};
use crate::style::{ShapeStyle, TextStyle};
use plotters_backend::{BackendCoord, DrawingBackend, FontTransform};
/// The context of the chart. This is the core object of Plotters.
/// Any plot/chart is abstracted as this type, and any data series can be placed to the chart
/// context.
///
/// - To draw a series on a chart context, use [ChartContext::draw_series](struct.ChartContext.html#method.draw_series)
/// - To draw a single element to the chart, you may want to use [ChartContext::plotting_area](struct.ChartContext.html#method.plotting_area)
///
pub struct ChartContext<'a, DB: DrawingBackend, CT: CoordTranslate> {
pub(super) x_label_area: [Option<DrawingArea<DB, Shift>>; 2],
pub(super) y_label_area: [Option<DrawingArea<DB, Shift>>; 2],
pub(super) drawing_area: DrawingArea<DB, CT>,
pub(super) series_anno: Vec<SeriesAnno<'a, DB>>,
pub(super) drawing_area_pos: (i32, i32),
}
impl<'a, DB, XT, YT, X, Y> ChartContext<'a, DB, Cartesian2d<X, Y>>
where
DB: DrawingBackend,
X: Ranged<ValueType = XT> + ValueFormatter<XT>,
Y: Ranged<ValueType = YT> + ValueFormatter<YT>,
{
pub(crate) fn is_overlapping_drawing_area(
&self,
area: Option<&DrawingArea<DB, Shift>>,
) -> bool {
if let Some(area) = area {
let (x0, y0) = area.get_base_pixel();
let (w, h) = area.dim_in_pixel();
let (x1, y1) = (x0 + w as i32, y0 + h as i32);
let (dx0, dy0) = self.drawing_area.get_base_pixel();
let (w, h) = self.drawing_area.dim_in_pixel();
let (dx1, dy1) = (dx0 + w as i32, dy0 + h as i32);
let (ox0, ox1) = (x0.max(dx0), x1.min(dx1));
let (oy0, oy1) = (y0.max(dy0), y1.min(dy1));
ox1 > ox0 && oy1 > oy0
} else {
false
}
}
/// Initialize a mesh configuration object and mesh drawing can be finalized by calling
/// the function `MeshStyle::draw`.
pub fn configure_mesh(&mut self) -> MeshStyle<'a, '_, X, Y, DB> {
MeshStyle::new(self)
}
}
impl<'a, DB: DrawingBackend, CT: ReverseCoordTranslate> ChartContext<'a, DB, CT> {
/// Convert the chart context into an closure that can be used for coordinate translation
pub fn into_coord_trans(self) -> impl Fn(BackendCoord) -> Option<CT::From> {
let coord_spec = self.drawing_area.into_coord_spec();
move |coord| coord_spec.reverse_translate(coord)
}
}
impl<'a, DB: DrawingBackend, CT: CoordTranslate> ChartContext<'a, DB, CT> {
/// Configure the styles for drawing series labels in the chart
pub fn configure_series_labels<'b>(&'b mut self) -> SeriesLabelStyle<'a, 'b, DB, CT>
where
DB: 'a,
{
SeriesLabelStyle::new(self)
}
/// Get a reference of underlying plotting area
pub fn plotting_area(&self) -> &DrawingArea<DB, CT> {
&self.drawing_area
}
/// Cast the reference to a chart context to a reference to underlying coordinate specification.
pub fn as_coord_spec(&self) -> &CT {
self.drawing_area.as_coord_spec()
}
// TODO: All draw_series_impl is overly strict about lifetime, because we don't have stable HKT,
// what we can ensure is for all lifetime 'b the element reference &'b E is a iterator
// of points reference with the same lifetime.
// However, this doesn't work if the coordinate doesn't live longer than the backend,
// this is unnecessarily strict
pub(super) fn draw_series_impl<E, R, S>(
&mut self,
series: S,
) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
where
for<'b> &'b E: PointCollection<'b, CT::From>,
E: Drawable<DB>,
R: Borrow<E>,
S: IntoIterator<Item = R>,
{
for element in series {
self.drawing_area.draw(element.borrow())?;
}
Ok(())
}
pub(super) fn alloc_series_anno(&mut self) -> &mut SeriesAnno<'a, DB> {
let idx = self.series_anno.len();
self.series_anno.push(SeriesAnno::new());
&mut self.series_anno[idx]
}
/// Draw a data series. A data series in Plotters is abstracted as an iterator of elements
pub fn draw_series<E, R, S>(
&mut self,
series: S,
) -> Result<&mut SeriesAnno<'a, DB>, DrawingAreaErrorKind<DB::ErrorType>>
where
for<'b> &'b E: PointCollection<'b, CT::From>,
E: Drawable<DB>,
R: Borrow<E>,
S: IntoIterator<Item = R>,
{
self.draw_series_impl(series)?;
Ok(self.alloc_series_anno())
}
}
impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Cartesian2d<X, Y>> {
/// Get the range of X axis
pub fn x_range(&self) -> Range<X::ValueType> {
self.drawing_area.get_x_range()
}
/// Get range of the Y axis
pub fn y_range(&self) -> Range<Y::ValueType> {
self.drawing_area.get_y_range()
}
/// Maps the coordinate to the backend coordinate. This is typically used
/// with an interactive chart.
pub fn backend_coord(&self, coord: &(X::ValueType, Y::ValueType)) -> BackendCoord {
self.drawing_area.map_coordinate(coord)
}
/// The actual function that draws the mesh lines.
/// It also returns the label that suppose to be there.
#[allow(clippy::type_complexity)]
fn draw_mesh_lines<FmtLabel, YH: KeyPointHint, XH: KeyPointHint>(
&mut self,
(r, c): (YH, XH),
(x_mesh, y_mesh): (bool, bool),
mesh_line_style: &ShapeStyle,
mut fmt_label: FmtLabel,
) -> Result<(Vec<(i32, String)>, Vec<(i32, String)>), DrawingAreaErrorKind<DB::ErrorType>>
where
FmtLabel: FnMut(&MeshLine<X, Y>) -> Option<String>,
{
let mut x_labels = vec![];
let mut y_labels = vec![];
self.drawing_area.draw_mesh(
|b, l| {
let draw;
match l {
MeshLine::XMesh((x, _), _, _) => {
if let Some(label_text) = fmt_label(&l) {
x_labels.push((x, label_text));
}
draw = x_mesh;
}
MeshLine::YMesh((_, y), _, _) => {
if let Some(label_text) = fmt_label(&l) {
y_labels.push((y, label_text));
}
draw = y_mesh;
}
};
if draw {
l.draw(b, mesh_line_style)
} else {
Ok(())
}
},
r,
c,
)?;
Ok((x_labels, y_labels))
}
fn draw_axis(
&self,
area: &DrawingArea<DB, Shift>,
axis_style: Option<&ShapeStyle>,
orientation: (i16, i16),
inward_labels: bool,
) -> Result<Range<i32>, DrawingAreaErrorKind<DB::ErrorType>> {
let (x0, y0) = self.drawing_area.get_base_pixel();
let (tw, th) = area.dim_in_pixel();
let mut axis_range = if orientation.0 == 0 {
self.drawing_area.get_x_axis_pixel_range()
} else {
self.drawing_area.get_y_axis_pixel_range()
};
/* At this point, the coordinate system tells us the pixel range
* after the translation.
* However, we need to use the logic coordinate system for drawing. */
if orientation.0 == 0 {
axis_range.start -= x0;
axis_range.end -= x0;
} else {
axis_range.start -= y0;
axis_range.end -= y0;
}
if let Some(axis_style) = axis_style {
let mut x0 = if orientation.0 > 0 { 0 } else { tw as i32 - 1 };
let mut y0 = if orientation.1 > 0 { 0 } else { th as i32 - 1 };
let mut x1 = if orientation.0 >= 0 { 0 } else { tw as i32 - 1 };
let mut y1 = if orientation.1 >= 0 { 0 } else { th as i32 - 1 };
if inward_labels {
if orientation.0 == 0 {
if y0 == 0 {
y0 = th as i32 - 1;
y1 = th as i32 - 1;
} else {
y0 = 0;
y1 = 0;
}
} else if x0 == 0 {
x0 = tw as i32 - 1;
x1 = tw as i32 - 1;
} else {
x0 = 0;
x1 = 0;
}
}
if orientation.0 == 0 {
x0 = axis_range.start;
x1 = axis_range.end;
} else {
y0 = axis_range.start;
y1 = axis_range.end;
}
area.draw(&PathElement::new(
vec![(x0, y0), (x1, y1)],
axis_style.clone(),
))?;
}
Ok(axis_range)
}
// TODO: consider make this function less complicated
#[allow(clippy::too_many_arguments)]
#[allow(clippy::cognitive_complexity)]
fn draw_axis_and_labels(
&self,
area: Option<&DrawingArea<DB, Shift>>,
axis_style: Option<&ShapeStyle>,
labels: &[(i32, String)],
label_style: &TextStyle,
label_offset: i32,
orientation: (i16, i16),
axis_desc: Option<(&str, &TextStyle)>,
tick_size: i32,
) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>> {
let area = if let Some(target) = area {
target
} else {
return Ok(());
};
let (x0, y0) = self.drawing_area.get_base_pixel();
let (tw, th) = area.dim_in_pixel();
/* This is the minimal distance from the axis to the box of the labels */
let label_dist = tick_size.abs() * 2;
/* Draw the axis and get the axis range so that we can do further label
* and tick mark drawing */
let axis_range = self.draw_axis(area, axis_style, orientation, tick_size < 0)?;
/* To make the right label area looks nice, it's a little bit tricky, since for a that is
* very long, we actually prefer left alignment instead of right alignment.
* Otherwise, the right alignment looks better. So we estimate the max and min label width
* So that we are able decide if we should apply right alignment for the text. */
let label_width: Vec<_> = labels
.iter()
.map(|(_, text)| {
if orientation.0 > 0 && orientation.1 == 0 && tick_size >= 0 {
let ((x0, _), (x1, _)) = label_style
.font
.layout_box(text)
.unwrap_or(((0, 0), (0, 0)));
x1 - x0
} else {
// Don't ever do the layout estimationfor the drawing area that is either not
// the right one or the tick mark is inward.
0
}
})
.collect();
let min_width = *label_width.iter().min().unwrap_or(&1).max(&1);
let max_width = *label_width
.iter()
.filter(|&&x| x < min_width * 2)
.max()
.unwrap_or(&min_width);
let right_align_width = (min_width * 2).min(max_width);
/* Then we need to draw the tick mark and the label */
for ((p, t), w) in labels.iter().zip(label_width.into_iter()) {
/* Make sure we are actually in the visible range */
let rp = if orientation.0 == 0 { *p - x0 } else { *p - y0 };
if rp < axis_range.start.min(axis_range.end)
|| axis_range.end.max(axis_range.start) < rp
{
continue;
}
let (cx, cy, h_pos, v_pos) = if tick_size >= 0 {
match orientation {
// Right
(dx, dy) if dx > 0 && dy == 0 => {
if w >= right_align_width {
(label_dist, *p - y0, HPos::Left, VPos::Center)
} else {
(
label_dist + right_align_width,
*p - y0,
HPos::Right,
VPos::Center,
)
}
}
// Left
(dx, dy) if dx < 0 && dy == 0 => {
(tw as i32 - label_dist, *p - y0, HPos::Right, VPos::Center)
}
// Bottom
(dx, dy) if dx == 0 && dy > 0 => (*p - x0, label_dist, HPos::Center, VPos::Top),
// Top
(dx, dy) if dx == 0 && dy < 0 => {
(*p - x0, th as i32 - label_dist, HPos::Center, VPos::Bottom)
}
_ => panic!("Bug: Invalid orientation specification"),
}
} else {
match orientation {
// Right
(dx, dy) if dx > 0 && dy == 0 => {
(tw as i32 - label_dist, *p - y0, HPos::Right, VPos::Center)
}
// Left
(dx, dy) if dx < 0 && dy == 0 => {
(label_dist, *p - y0, HPos::Left, VPos::Center)
}
// Bottom
(dx, dy) if dx == 0 && dy > 0 => {
(*p - x0, th as i32 - label_dist, HPos::Center, VPos::Bottom)
}
// Top
(dx, dy) if dx == 0 && dy < 0 => (*p - x0, label_dist, HPos::Center, VPos::Top),
_ => panic!("Bug: Invalid orientation specification"),
}
};
let (text_x, text_y) = if orientation.0 == 0 {
(cx + label_offset, cy)
} else {
(cx, cy + label_offset)
};
let label_style = &label_style.pos(Pos::new(h_pos, v_pos));
area.draw_text(&t, label_style, (text_x, text_y))?;
if tick_size != 0 {
if let Some(style) = axis_style {
let xmax = tw as i32 - 1;
let ymax = th as i32 - 1;
let (kx0, ky0, kx1, ky1) = if tick_size > 0 {
match orientation {
(dx, dy) if dx > 0 && dy == 0 => (0, *p - y0, tick_size, *p - y0),
(dx, dy) if dx < 0 && dy == 0 => {
(xmax - tick_size, *p - y0, xmax, *p - y0)
}
(dx, dy) if dx == 0 && dy > 0 => (*p - x0, 0, *p - x0, tick_size),
(dx, dy) if dx == 0 && dy < 0 => {
(*p - x0, ymax - tick_size, *p - x0, ymax)
}
_ => panic!("Bug: Invalid orientation specification"),
}
} else {
match orientation {
(dx, dy) if dx > 0 && dy == 0 => {
(xmax, *p - y0, xmax + tick_size, *p - y0)
}
(dx, dy) if dx < 0 && dy == 0 => (0, *p - y0, -tick_size, *p - y0),
(dx, dy) if dx == 0 && dy > 0 => {
(*p - x0, ymax, *p - x0, ymax + tick_size)
}
(dx, dy) if dx == 0 && dy < 0 => (*p - x0, 0, *p - x0, -tick_size),
_ => panic!("Bug: Invalid orientation specification"),
}
};
let line = PathElement::new(vec![(kx0, ky0), (kx1, ky1)], style.clone());
area.draw(&line)?;
}
}
}
if let Some((text, style)) = axis_desc {
let actual_style = if orientation.0 == 0 {
style.clone()
} else if orientation.0 == -1 {
style.transform(FontTransform::Rotate270)
} else {
style.transform(FontTransform::Rotate90)
};
let (x0, y0, h_pos, v_pos) = match orientation {
// Right
(dx, dy) if dx > 0 && dy == 0 => (tw, th / 2, HPos::Center, VPos::Top),
// Left
(dx, dy) if dx < 0 && dy == 0 => (0, th / 2, HPos::Center, VPos::Top),
// Bottom
(dx, dy) if dx == 0 && dy > 0 => (tw / 2, th, HPos::Center, VPos::Bottom),
// Top
(dx, dy) if dx == 0 && dy < 0 => (tw / 2, 0, HPos::Center, VPos::Top),
_ => panic!("Bug: Invalid orientation specification"),
};
let actual_style = &actual_style.pos(Pos::new(h_pos, v_pos));
area.draw_text(&text, &actual_style, (x0 as i32, y0 as i32))?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(super) fn draw_mesh<FmtLabel, YH: KeyPointHint, XH: KeyPointHint>(
&mut self,
(r, c): (YH, XH),
mesh_line_style: &ShapeStyle,
x_label_style: &TextStyle,
y_label_style: &TextStyle,
fmt_label: FmtLabel,
x_mesh: bool,
y_mesh: bool,
x_label_offset: i32,
y_label_offset: i32,
x_axis: bool,
y_axis: bool,
axis_style: &ShapeStyle,
axis_desc_style: &TextStyle,
x_desc: Option<String>,
y_desc: Option<String>,
x_tick_size: [i32; 2],
y_tick_size: [i32; 2],
) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
where
FmtLabel: FnMut(&MeshLine<X, Y>) -> Option<String>,
{
let (x_labels, y_labels) =
self.draw_mesh_lines((r, c), (x_mesh, y_mesh), mesh_line_style, fmt_label)?;
for idx in 0..2 {
self.draw_axis_and_labels(
self.x_label_area[idx].as_ref(),
if x_axis { Some(axis_style) } else { None },
&x_labels[..],
x_label_style,
x_label_offset,
(0, -1 + idx as i16 * 2),
x_desc.as_ref().map(|desc| (&desc[..], axis_desc_style)),
x_tick_size[idx],
)?;
self.draw_axis_and_labels(
self.y_label_area[idx].as_ref(),
if y_axis { Some(axis_style) } else { None },
&y_labels[..],
y_label_style,
y_label_offset,
(-1 + idx as i16 * 2, 0),
y_desc.as_ref().map(|desc| (&desc[..], axis_desc_style)),
y_tick_size[idx],
)?;
}
Ok(())
}
/// Convert this chart context into a dual axis chart context and attach a second coordinate spec
/// on the chart context. For more detailed information, see documentation for [struct DualCoordChartContext](struct.DualCoordChartContext.html)
///
/// - `x_coord`: The coordinate spec for the X axis
/// - `y_coord`: The coordinate spec for the Y axis
/// - **returns** The newly created dual spec chart context
#[allow(clippy::type_complexity)]
pub fn set_secondary_coord<SX: AsRangedCoord, SY: AsRangedCoord>(
self,
x_coord: SX,
y_coord: SY,
) -> DualCoordChartContext<
'a,
DB,
Cartesian2d<X, Y>,
Cartesian2d<SX::CoordDescType, SY::CoordDescType>,
> {
let mut pixel_range = self.drawing_area.get_pixel_range();
pixel_range.1 = pixel_range.1.end..pixel_range.1.start;
DualCoordChartContext::new(self, Cartesian2d::new(x_coord, y_coord, pixel_range))
}
}
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,
{
/// Override the 3D projection matrix. This function allows to override the default projection
/// matrix.
/// - `pf`: A function that takes the default projection matrix configuration and returns the
/// projection matrix. This function will allow you to adjust the pitch, yaw angle and the
/// centeral point of the projection, etc. You can also build a projection matrix which is not
/// relies on the default configuration as well.
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::*;
#[test]
fn test_chart_context() {
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_2d(0..10, 0..10)
.expect("Create chart")
.set_secondary_coord(0.0..1.0, 0.0..1.0);
chart
.configure_mesh()
.x_desc("X")
.y_desc("Y")
.draw()
.expect("Draw mesh");
chart
.configure_secondary_axes()
.x_desc("X")
.y_desc("Y")
.draw()
.expect("Draw Secondary axes");
chart
.draw_series(std::iter::once(Circle::new((5, 5), 5, &RED)))
.expect("Drawing error");
chart
.draw_secondary_series(std::iter::once(Circle::new((0.3, 0.8), 5, &GREEN)))
.expect("Drawing error")
.label("Test label")
.legend(|(x, y)| Rectangle::new([(x - 10, y - 5), (x, y + 5)], &GREEN));
chart
.configure_series_labels()
.position(SeriesLabelPosition::UpperMiddle)
.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");
}
}