Skip to content

Commit 1119c84

Browse files
authored
Merge pull request #239 from alex-bohm-nimbelink/feature/GH-237-axis-category-order
Add Categorical Axis Ordering and Axis Category Array
2 parents e0afa36 + 66a5adc commit 1119c84

File tree

3 files changed

+166
-2
lines changed

3 files changed

+166
-2
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file.
33

44
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).
55

6+
## [0.10.0] - 2024-10-29
7+
### Added
8+
- [[#239](https://github.com/plotly/plotly.rs/pull/239)] Add Categorical Axis Ordering and Axis Category Array.
9+
10+
### Fixed
11+
- [[#237](https://github.com/plotly/plotly.rs/issues/237)] Add Categorical Axis ordering.
12+
613
## [0.10.0] - 2024-09-16
714
### Added
815
- [[#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.

examples/basic_charts/src/main.rs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use plotly::{
77
ColorScale, ColorScalePalette, DashType, Fill, Font, Line, LineShape, Marker, Mode,
88
Orientation,
99
},
10-
layout::{Axis, BarMode, Layout, Legend, TicksDirection, TraceOrder},
10+
layout::{Axis, BarMode, CategoryOrder, Layout, Legend, TicksDirection, TraceOrder},
1111
sankey::{Line as SankeyLine, Link, Node},
1212
traces::table::{Cells, Header},
1313
Bar, Plot, Sankey, Scatter, ScatterPolar, Table,
@@ -523,6 +523,49 @@ fn filled_lines() {
523523
plot.show();
524524
}
525525

526+
/// Scatter plot showing y axis categories and category ordering.
527+
fn categories_scatter_chart() {
528+
// Categories are ordered on the y axis from bottom to top.
529+
let categories = vec!["Unknown", "Off", "On"];
530+
531+
let x = vec![
532+
"2024-10-30T08:30:05.05Z",
533+
"2024-10-30T08:35:05.05Z",
534+
"2024-10-30T08:50:05.05Z",
535+
"2024-10-30T08:50:20.05Z",
536+
"2024-10-30T09:00:05.05Z",
537+
"2024-10-30T09:05:05.05Z",
538+
"2024-10-30T09:10:05.05Z",
539+
"2024-10-30T09:10:20.05Z",
540+
];
541+
let y = vec![
542+
"On",
543+
"Off",
544+
"Unknown",
545+
"Off",
546+
"On",
547+
"Off",
548+
// Categories that aren't in the category_array follow the Trace order.
549+
"NewCategory",
550+
"Off",
551+
];
552+
553+
let trace = Scatter::new(x, y).line(Line::new().shape(LineShape::Hv));
554+
555+
let layout = Layout::new().y_axis(
556+
Axis::new()
557+
.category_order(CategoryOrder::Array)
558+
.category_array(categories),
559+
);
560+
561+
let mut plot = Plot::new();
562+
plot.add_trace(trace);
563+
564+
plot.set_layout(layout);
565+
566+
plot.show();
567+
}
568+
526569
// Bar Charts
527570
fn basic_bar_chart() {
528571
let animals = vec!["giraffes", "orangutans", "monkeys"];
@@ -567,6 +610,29 @@ fn stacked_bar_chart() {
567610
plot.show();
568611
}
569612

613+
/// Graph a bar chart that orders the x axis categories by the total number
614+
/// of animals in each category.
615+
fn category_order_bar_chart() {
616+
let animals1 = vec!["giraffes", "orangutans", "monkeys"];
617+
let trace1 = Bar::new(animals1, vec![10, 14, 23]).name("SF Zoo");
618+
619+
let animals2 = vec!["giraffes", "orangutans", "monkeys"];
620+
let trace2 = Bar::new(animals2, vec![12, 18, 29]).name("LA Zoo");
621+
622+
let layout = Layout::new()
623+
.bar_mode(BarMode::Stack)
624+
// Order the x axis categories so the category with the most animals
625+
// appears first.
626+
.x_axis(Axis::new().category_order(CategoryOrder::TotalDescending));
627+
628+
let mut plot = Plot::new();
629+
plot.add_trace(trace1);
630+
plot.add_trace(trace2);
631+
plot.set_layout(layout);
632+
633+
plot.show();
634+
}
635+
570636
// Sankey Diagrams
571637
fn basic_sankey_diagram() {
572638
// https://plotly.com/javascript/sankey-diagram/#basic-sankey-diagram
@@ -627,6 +693,7 @@ fn main() {
627693
// data_labels_on_the_plot();
628694
// colored_and_styled_scatter_plot();
629695
// large_data_sets();
696+
// categories_scatter_chart();
630697

631698
// Line Charts
632699
// adding_names_to_line_and_scatter_plot();
@@ -641,6 +708,7 @@ fn main() {
641708
// grouped_bar_chart();
642709
// stacked_bar_chart();
643710
// table_chart();
711+
// category_order_bar_chart();
644712

645713
// Sankey Diagrams
646714
// basic_sankey_diagram();

plotly/src/layout/mod.rs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,10 +418,72 @@ pub enum SpikeSnap {
418418
HoveredData,
419419
}
420420

421+
#[derive(Serialize, Debug, Clone)]
422+
pub enum CategoryOrder {
423+
#[serde(rename = "trace")]
424+
Trace,
425+
#[serde(rename = "category ascending")]
426+
CategoryAscending,
427+
#[serde(rename = "category descending")]
428+
CategoryDescending,
429+
#[serde(rename = "array")]
430+
Array,
431+
#[serde(rename = "total ascending")]
432+
TotalAscending,
433+
#[serde(rename = "total descending")]
434+
TotalDescending,
435+
#[serde(rename = "min ascending")]
436+
MinAscending,
437+
#[serde(rename = "min descending")]
438+
MinDescending,
439+
#[serde(rename = "max ascending")]
440+
MaxAscending,
441+
#[serde(rename = "max descending")]
442+
MaxDescending,
443+
#[serde(rename = "sum ascending")]
444+
SumAscending,
445+
#[serde(rename = "sum descending")]
446+
SumDescending,
447+
#[serde(rename = "mean ascending")]
448+
MeanAscending,
449+
#[serde(rename = "mean descending")]
450+
MeanDescending,
451+
#[serde(rename = "geometric mean ascending")]
452+
GeometricMeanAscending,
453+
#[serde(rename = "geometric mean descending")]
454+
GeometricMeanDescending,
455+
#[serde(rename = "median ascending")]
456+
MedianAscending,
457+
#[serde(rename = "median descending")]
458+
MedianDescending,
459+
}
460+
421461
#[serde_with::skip_serializing_none]
422462
#[derive(Serialize, Debug, Clone, FieldSetter)]
423463
pub struct Axis {
424464
visible: Option<bool>,
465+
/// Sets the order in which categories on this axis appear. Only has an
466+
/// effect if `category_order` is set to [`CategoryOrder::Array`].
467+
/// Used with `category_order`.
468+
#[serde(rename = "categoryarray")]
469+
category_array: Option<NumOrStringCollection>,
470+
/// Specifies the ordering logic for the case of categorical variables.
471+
/// By default, plotly uses [`CategoryOrder::Trace`], which specifies
472+
/// the order that is present in the data supplied. Set `category_order` to
473+
/// [`CategoryOrder::CategoryAscending`] or
474+
/// [`CategoryOrder::CategoryDescending`] if order should be determined
475+
/// by the alphanumerical order of the category names. Set `category_order`
476+
/// to [`CategoryOrder::Array`] to derive the ordering from the attribute
477+
/// `category_array`. If a category is not found in the `category_array`
478+
/// array, the sorting behavior for that attribute will be identical to the
479+
/// [`CategoryOrder::Trace`] mode. The unspecified categories will follow
480+
/// the categories in `category_array`. Set `category_order` to
481+
/// [`CategoryOrder::TotalAscending`] or
482+
/// [`CategoryOrder::TotalDescending`] if order should be determined by the
483+
/// numerical order of the values. Similarly, the order can be determined
484+
/// by the min, max, sum, mean, geometric mean or median of all the values.
485+
#[serde(rename = "categoryorder")]
486+
category_order: Option<CategoryOrder>,
425487
color: Option<Box<dyn Color>>,
426488
title: Option<Title>,
427489
#[field_setter(skip)]
@@ -2341,6 +2403,29 @@ mod tests {
23412403
assert_eq!(to_value(SpikeSnap::HoveredData).unwrap(), json!("hovered data"));
23422404
}
23432405

2406+
#[test]
2407+
#[rustfmt::skip]
2408+
fn test_serialize_category_order() {
2409+
assert_eq!(to_value(CategoryOrder::Trace).unwrap(), json!("trace"));
2410+
assert_eq!(to_value(CategoryOrder::CategoryAscending).unwrap(), json!("category ascending"));
2411+
assert_eq!(to_value(CategoryOrder::CategoryDescending).unwrap(), json!("category descending"));
2412+
assert_eq!(to_value(CategoryOrder::Array).unwrap(), json!("array"));
2413+
assert_eq!(to_value(CategoryOrder::TotalAscending).unwrap(), json!("total ascending"));
2414+
assert_eq!(to_value(CategoryOrder::TotalDescending).unwrap(), json!("total descending"));
2415+
assert_eq!(to_value(CategoryOrder::MinAscending).unwrap(), json!("min ascending"));
2416+
assert_eq!(to_value(CategoryOrder::MinDescending).unwrap(), json!("min descending"));
2417+
assert_eq!(to_value(CategoryOrder::MaxAscending).unwrap(), json!("max ascending"));
2418+
assert_eq!(to_value(CategoryOrder::MaxDescending).unwrap(), json!("max descending"));
2419+
assert_eq!(to_value(CategoryOrder::SumAscending).unwrap(), json!("sum ascending"));
2420+
assert_eq!(to_value(CategoryOrder::SumDescending).unwrap(), json!("sum descending"));
2421+
assert_eq!(to_value(CategoryOrder::MeanAscending).unwrap(), json!("mean ascending"));
2422+
assert_eq!(to_value(CategoryOrder::MeanDescending).unwrap(), json!("mean descending"));
2423+
assert_eq!(to_value(CategoryOrder::GeometricMeanAscending).unwrap(), json!("geometric mean ascending"));
2424+
assert_eq!(to_value(CategoryOrder::GeometricMeanDescending).unwrap(), json!("geometric mean descending"));
2425+
assert_eq!(to_value(CategoryOrder::MedianAscending).unwrap(), json!("median ascending"));
2426+
assert_eq!(to_value(CategoryOrder::MedianDescending).unwrap(), json!("median descending"));
2427+
}
2428+
23442429
#[test]
23452430
fn test_serialize_selector_button() {
23462431
let selector_button = SelectorButton::new()
@@ -2490,7 +2575,9 @@ mod tests {
24902575
.position(0.6)
24912576
.range_slider(RangeSlider::new())
24922577
.range_selector(RangeSelector::new())
2493-
.calendar(Calendar::Coptic);
2578+
.calendar(Calendar::Coptic)
2579+
.category_order(CategoryOrder::Array)
2580+
.category_array(vec!["Category0", "Category1"]);
24942581

24952582
let expected = json!({
24962583
"visible": false,
@@ -2556,6 +2643,8 @@ mod tests {
25562643
"rangeslider": {},
25572644
"rangeselector": {},
25582645
"calendar": "coptic",
2646+
"categoryorder": "array",
2647+
"categoryarray": ["Category0", "Category1"]
25592648
});
25602649

25612650
assert_eq!(to_value(axis).unwrap(), expected);

0 commit comments

Comments
 (0)