Skip to content

Add Categorical Axis Ordering and Axis Category Array #239

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.10.0] - 2024-10-29
### Added
- [[#239](https://github.com/plotly/plotly.rs/pull/239)] Add Categorical Axis Ordering and Axis Category Array.

### Fixed
- [[#237](https://github.com/plotly/plotly.rs/issues/237)] Add Categorical Axis ordering.

## [0.10.0] - 2024-09-16
### Added
- [[#231](https://github.com/plotly/plotly.rs/pull/231)] Added new `plotly_embed_js` feature to reduce binary sizes by not embedding `plotly.min.js` in the library unless explicitly enabled via the feature flag. Deprecates `use_local_plotly` in favor of explicit opt-in via the feature flag and introduce method `use_cdn_plotly` to allow users to use CDN version even behind the `plotly_embed_js` feature flag.
Expand Down
70 changes: 69 additions & 1 deletion examples/basic_charts/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use plotly::{
ColorScale, ColorScalePalette, DashType, Fill, Font, Line, LineShape, Marker, Mode,
Orientation,
},
layout::{Axis, BarMode, Layout, Legend, TicksDirection, TraceOrder},
layout::{Axis, BarMode, CategoryOrder, Layout, Legend, TicksDirection, TraceOrder},
sankey::{Line as SankeyLine, Link, Node},
traces::table::{Cells, Header},
Bar, Plot, Sankey, Scatter, ScatterPolar, Table,
Expand Down Expand Up @@ -523,6 +523,49 @@ fn filled_lines() {
plot.show();
}

/// Scatter plot showing y axis categories and category ordering.
fn categories_scatter_chart() {
// Categories are ordered on the y axis from bottom to top.
let categories = vec!["Unknown", "Off", "On"];

let x = vec![
"2024-10-30T08:30:05.05Z",
"2024-10-30T08:35:05.05Z",
"2024-10-30T08:50:05.05Z",
"2024-10-30T08:50:20.05Z",
"2024-10-30T09:00:05.05Z",
"2024-10-30T09:05:05.05Z",
"2024-10-30T09:10:05.05Z",
"2024-10-30T09:10:20.05Z",
];
let y = vec![
"On",
"Off",
"Unknown",
"Off",
"On",
"Off",
// Categories that aren't in the category_array follow the Trace order.
"NewCategory",
"Off",
];

let trace = Scatter::new(x, y).line(Line::new().shape(LineShape::Hv));

let layout = Layout::new().y_axis(
Axis::new()
.category_order(CategoryOrder::Array)
.category_array(categories),
);

let mut plot = Plot::new();
plot.add_trace(trace);

plot.set_layout(layout);

plot.show();
}

// Bar Charts
fn basic_bar_chart() {
let animals = vec!["giraffes", "orangutans", "monkeys"];
Expand Down Expand Up @@ -567,6 +610,29 @@ fn stacked_bar_chart() {
plot.show();
}

/// Graph a bar chart that orders the x axis categories by the total number
/// of animals in each category.
fn category_order_bar_chart() {
let animals1 = vec!["giraffes", "orangutans", "monkeys"];
let trace1 = Bar::new(animals1, vec![10, 14, 23]).name("SF Zoo");

let animals2 = vec!["giraffes", "orangutans", "monkeys"];
let trace2 = Bar::new(animals2, vec![12, 18, 29]).name("LA Zoo");

let layout = Layout::new()
.bar_mode(BarMode::Stack)
// Order the x axis categories so the category with the most animals
// appears first.
.x_axis(Axis::new().category_order(CategoryOrder::TotalDescending));

let mut plot = Plot::new();
plot.add_trace(trace1);
plot.add_trace(trace2);
plot.set_layout(layout);

plot.show();
}

// Sankey Diagrams
fn basic_sankey_diagram() {
// https://plotly.com/javascript/sankey-diagram/#basic-sankey-diagram
Expand Down Expand Up @@ -627,6 +693,7 @@ fn main() {
// data_labels_on_the_plot();
// colored_and_styled_scatter_plot();
// large_data_sets();
// categories_scatter_chart();

// Line Charts
// adding_names_to_line_and_scatter_plot();
Expand All @@ -641,6 +708,7 @@ fn main() {
// grouped_bar_chart();
// stacked_bar_chart();
// table_chart();
// category_order_bar_chart();

// Sankey Diagrams
// basic_sankey_diagram();
Expand Down
91 changes: 90 additions & 1 deletion plotly/src/layout/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,10 +418,72 @@ pub enum SpikeSnap {
HoveredData,
}

#[derive(Serialize, Debug, Clone)]
pub enum CategoryOrder {
#[serde(rename = "trace")]
Trace,
#[serde(rename = "category ascending")]
CategoryAscending,
#[serde(rename = "category descending")]
CategoryDescending,
#[serde(rename = "array")]
Array,
#[serde(rename = "total ascending")]
TotalAscending,
#[serde(rename = "total descending")]
TotalDescending,
#[serde(rename = "min ascending")]
MinAscending,
#[serde(rename = "min descending")]
MinDescending,
#[serde(rename = "max ascending")]
MaxAscending,
#[serde(rename = "max descending")]
MaxDescending,
#[serde(rename = "sum ascending")]
SumAscending,
#[serde(rename = "sum descending")]
SumDescending,
#[serde(rename = "mean ascending")]
MeanAscending,
#[serde(rename = "mean descending")]
MeanDescending,
#[serde(rename = "geometric mean ascending")]
GeometricMeanAscending,
#[serde(rename = "geometric mean descending")]
GeometricMeanDescending,
#[serde(rename = "median ascending")]
MedianAscending,
#[serde(rename = "median descending")]
MedianDescending,
}

#[serde_with::skip_serializing_none]
#[derive(Serialize, Debug, Clone, FieldSetter)]
pub struct Axis {
visible: Option<bool>,
/// Sets the order in which categories on this axis appear. Only has an
/// effect if `category_order` is set to [`CategoryOrder::Array`].
/// Used with `category_order`.
#[serde(rename = "categoryarray")]
category_array: Option<NumOrStringCollection>,
/// Specifies the ordering logic for the case of categorical variables.
/// By default, plotly uses [`CategoryOrder::Trace`], which specifies
/// the order that is present in the data supplied. Set `category_order` to
/// [`CategoryOrder::CategoryAscending`] or
/// [`CategoryOrder::CategoryDescending`] if order should be determined
/// by the alphanumerical order of the category names. Set `category_order`
/// to [`CategoryOrder::Array`] to derive the ordering from the attribute
/// `category_array`. If a category is not found in the `category_array`
/// array, the sorting behavior for that attribute will be identical to the
/// [`CategoryOrder::Trace`] mode. The unspecified categories will follow
/// the categories in `category_array`. Set `category_order` to
/// [`CategoryOrder::TotalAscending`] or
/// [`CategoryOrder::TotalDescending`] if order should be determined by the
/// numerical order of the values. Similarly, the order can be determined
/// by the min, max, sum, mean, geometric mean or median of all the values.
#[serde(rename = "categoryorder")]
category_order: Option<CategoryOrder>,
color: Option<Box<dyn Color>>,
title: Option<Title>,
#[field_setter(skip)]
Expand Down Expand Up @@ -2341,6 +2403,29 @@ mod tests {
assert_eq!(to_value(SpikeSnap::HoveredData).unwrap(), json!("hovered data"));
}

#[test]
#[rustfmt::skip]
fn test_serialize_category_order() {
assert_eq!(to_value(CategoryOrder::Trace).unwrap(), json!("trace"));
assert_eq!(to_value(CategoryOrder::CategoryAscending).unwrap(), json!("category ascending"));
assert_eq!(to_value(CategoryOrder::CategoryDescending).unwrap(), json!("category descending"));
assert_eq!(to_value(CategoryOrder::Array).unwrap(), json!("array"));
assert_eq!(to_value(CategoryOrder::TotalAscending).unwrap(), json!("total ascending"));
assert_eq!(to_value(CategoryOrder::TotalDescending).unwrap(), json!("total descending"));
assert_eq!(to_value(CategoryOrder::MinAscending).unwrap(), json!("min ascending"));
assert_eq!(to_value(CategoryOrder::MinDescending).unwrap(), json!("min descending"));
assert_eq!(to_value(CategoryOrder::MaxAscending).unwrap(), json!("max ascending"));
assert_eq!(to_value(CategoryOrder::MaxDescending).unwrap(), json!("max descending"));
assert_eq!(to_value(CategoryOrder::SumAscending).unwrap(), json!("sum ascending"));
assert_eq!(to_value(CategoryOrder::SumDescending).unwrap(), json!("sum descending"));
assert_eq!(to_value(CategoryOrder::MeanAscending).unwrap(), json!("mean ascending"));
assert_eq!(to_value(CategoryOrder::MeanDescending).unwrap(), json!("mean descending"));
assert_eq!(to_value(CategoryOrder::GeometricMeanAscending).unwrap(), json!("geometric mean ascending"));
assert_eq!(to_value(CategoryOrder::GeometricMeanDescending).unwrap(), json!("geometric mean descending"));
assert_eq!(to_value(CategoryOrder::MedianAscending).unwrap(), json!("median ascending"));
assert_eq!(to_value(CategoryOrder::MedianDescending).unwrap(), json!("median descending"));
}

#[test]
fn test_serialize_selector_button() {
let selector_button = SelectorButton::new()
Expand Down Expand Up @@ -2490,7 +2575,9 @@ mod tests {
.position(0.6)
.range_slider(RangeSlider::new())
.range_selector(RangeSelector::new())
.calendar(Calendar::Coptic);
.calendar(Calendar::Coptic)
.category_order(CategoryOrder::Array)
.category_array(vec!["Category0", "Category1"]);

let expected = json!({
"visible": false,
Expand Down Expand Up @@ -2556,6 +2643,8 @@ mod tests {
"rangeslider": {},
"rangeselector": {},
"calendar": "coptic",
"categoryorder": "array",
"categoryarray": ["Category0", "Category1"]
});

assert_eq!(to_value(axis).unwrap(), expected);
Expand Down
Loading