From c9ad6dfdbd94ee542c440d4e5345b10206292ec0 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Sun, 13 Oct 2024 14:20:36 +1300 Subject: [PATCH 1/2] Support `margins`, `margin_cluster_default`, and `margin_node_default`. --- crate/model/src/common/graphviz_attrs.rs | 90 ++++++++++++++++++- .../common/graphviz_attrs/cluster_margin.rs | 61 +++++++++++++ .../model/src/common/graphviz_attrs/margin.rs | 78 ++++++++++++++++ .../src/common/graphviz_attrs/margins.rs | 64 +++++++++++++ .../src/common/graphviz_attrs/node_margin.rs | 60 +++++++++++++ crate/model/src/common/graphviz_dot_theme.rs | 32 ------- .../src/into_graphviz_dot_src/info_graph.rs | 77 ++++++++++------ 7 files changed, 398 insertions(+), 64 deletions(-) create mode 100644 crate/model/src/common/graphviz_attrs/cluster_margin.rs create mode 100644 crate/model/src/common/graphviz_attrs/margin.rs create mode 100644 crate/model/src/common/graphviz_attrs/margins.rs create mode 100644 crate/model/src/common/graphviz_attrs/node_margin.rs diff --git a/crate/model/src/common/graphviz_attrs.rs b/crate/model/src/common/graphviz_attrs.rs index 98f6df6..fa86c32 100644 --- a/crate/model/src/common/graphviz_attrs.rs +++ b/crate/model/src/common/graphviz_attrs.rs @@ -1,17 +1,22 @@ use serde::{Deserialize, Serialize}; pub use self::{ - edge_constraints::EdgeConstraints, edge_dir::EdgeDir, edge_dirs::EdgeDirs, - edge_minlens::EdgeMinlens, fixed_size::FixedSize, node_heights::NodeHeights, - node_widths::NodeWidths, pack_mode::PackMode, pack_mode_flag::PackModeFlag, splines::Splines, + cluster_margin::ClusterMargin, edge_constraints::EdgeConstraints, edge_dir::EdgeDir, + edge_dirs::EdgeDirs, edge_minlens::EdgeMinlens, fixed_size::FixedSize, margin::Margin, + margins::Margins, node_heights::NodeHeights, node_margin::NodeMargin, node_widths::NodeWidths, + pack_mode::PackMode, pack_mode_flag::PackModeFlag, splines::Splines, }; +mod cluster_margin; mod edge_constraints; mod edge_dir; mod edge_dirs; mod edge_minlens; mod fixed_size; +mod margin; +mod margins; mod node_heights; +mod node_margin; mod node_widths; mod pack_mode; mod pack_mode_flag; @@ -69,6 +74,24 @@ pub struct GraphvizAttrs { /// /// [`minlen`]: https://graphviz.org/docs/attrs/minlen/ pub edge_minlens: EdgeMinlens, + /// The default value for each node's [`margin`], defaults to `0.11,0.055`. + /// + /// May be a single float, or two floats separated by a comma. + /// + /// [`margin`]: https://graphviz.org/docs/attrs/margin/ + pub margin_cluster_default: ClusterMargin, + /// The default value for each cluster's [`margin`], defaults to `8.0`. + /// + /// May be a single float, or two floats separated by a comma. + /// + /// [`margin`]: https://graphviz.org/docs/attrs/margin/ + pub margin_node_default: NodeMargin, + /// Each node or cluster's [`margin`]. + /// + /// May be a single float, or two floats separated by a comma. + /// + /// [`margin`]: https://graphviz.org/docs/attrs/margin/ + pub margins: Margins, /// Minimum / initial [`width`] for nodes, defaults to `0.3`. /// /// If `fixedsize` is true, this will be the exact / maximum width for @@ -197,6 +220,37 @@ impl GraphvizAttrs { self } + /// Sets the default value for each node's [`margin`], defaults to + /// `0.11,0.055`. + /// + /// May be a single float, or two floats separated by a comma. + /// + /// [`margin`]: https://graphviz.org/docs/attrs/margin/ + pub fn with_margin_cluster_default(mut self, margin_cluster_default: ClusterMargin) -> Self { + self.margin_cluster_default = margin_cluster_default; + self + } + + /// Sets the default value for each cluster's [`margin`], defaults to `8.0`. + /// + /// May be a single float, or two floats separated by a comma. + /// + /// [`margin`]: https://graphviz.org/docs/attrs/margin/ + pub fn with_margin_node_default(mut self, margin_node_default: NodeMargin) -> Self { + self.margin_node_default = margin_node_default; + self + } + + /// Sets each node or cluster's [`margin`]. + /// + /// May be a single float, or two floats separated by a comma. + /// + /// [`margin`]: https://graphviz.org/docs/attrs/margin/ + pub fn with_margins(mut self, margins: Margins) -> Self { + self.margins = margins; + self + } + /// Sets the minimum / initial [`width`] for nodes, defaults to `0.3`. /// /// If `fixedsize` is true, this will be the exact / maximum width for @@ -323,6 +377,33 @@ impl GraphvizAttrs { &self.edge_minlens } + /// Returns the default value for each node's [`margin`]. + /// + /// May be a single float, or two floats separated by a comma. + /// + /// [`margin`]: https://graphviz.org/docs/attrs/margin/ + pub fn margin_cluster_default(&self) -> ClusterMargin { + self.margin_cluster_default + } + + /// Returns the default value for each cluster's [`margin`]. + /// + /// May be a single float, or two floats separated by a comma. + /// + /// [`margin`]: https://graphviz.org/docs/attrs/margin/ + pub fn margin_node_default(&self) -> NodeMargin { + self.margin_node_default + } + + /// Returns each node or cluster's [`margin`]. + /// + /// May be a single float, or two floats separated by a comma. + /// + /// [`margin`]: https://graphviz.org/docs/attrs/margin/ + pub fn margins(&self) -> &Margins { + &self.margins + } + /// Returns the minimum / initial [`width`] for nodes. /// /// If `fixedsize` is true, this will be the exact / maximum width for @@ -385,6 +466,9 @@ impl Default for GraphvizAttrs { edge_dirs: EdgeDirs::default(), edge_minlen_default: 2, edge_minlens: EdgeMinlens::default(), + margin_cluster_default: ClusterMargin::default(), + margin_node_default: NodeMargin::default(), + margins: Margins::default(), node_width_default: 0.3, node_widths: NodeWidths::default(), node_height_default: 0.1, diff --git a/crate/model/src/common/graphviz_attrs/cluster_margin.rs b/crate/model/src/common/graphviz_attrs/cluster_margin.rs new file mode 100644 index 0000000..1fcc77c --- /dev/null +++ b/crate/model/src/common/graphviz_attrs/cluster_margin.rs @@ -0,0 +1,61 @@ +use std::fmt; + +use serde::{Deserialize, Serialize}; + +use crate::common::graphviz_attrs::Margin; + +/// A node [`margin`], which specifies the space between the nodes in the +/// cluster and the cluster bounding box. +/// +/// Defaults to `Margin::Same(8.0)`; Graphviz default: `8.0`. +/// +/// May be a single float, or two floats separated by a comma. +/// +/// [`margin`]: https://graphviz.org/docs/attrs/margin/ +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct ClusterMargin(pub Margin); + +impl ClusterMargin { + /// Returns a new `ClusterMargin`. + pub fn new() -> Self { + Self::default() + } + + /// Returns the inner `Margin`. + pub fn into_inner(self) -> Margin { + self.0 + } +} + +impl Default for ClusterMargin { + fn default() -> Self { + Self(Margin::Same(8.0)) + } +} + +impl From for ClusterMargin { + fn from(margin: Margin) -> Self { + Self(margin) + } +} + +impl std::ops::DerefMut for ClusterMargin { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl std::ops::Deref for ClusterMargin { + type Target = Margin; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl fmt::Display for ClusterMargin { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} diff --git a/crate/model/src/common/graphviz_attrs/margin.rs b/crate/model/src/common/graphviz_attrs/margin.rs new file mode 100644 index 0000000..fad021c --- /dev/null +++ b/crate/model/src/common/graphviz_attrs/margin.rs @@ -0,0 +1,78 @@ +use std::{fmt, fmt::Display, str::FromStr}; + +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + +/// Node or cluster [`margin`]s. +/// +/// May be a single float, or two floats separated by a comma. +/// +/// [`margin`]: https://graphviz.org/docs/attrs/margin/ +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Margin { + /// Margins on both left/right and top/bottom are the same. + Same(f64), + /// Margins for left/right are different to top/bottom. + Different(f64, f64), +} + +impl FromStr for Margin { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.split_once(',') { + Some((margin_x, margin_y)) => { + let margin_x = margin_x.parse::().map_err(|_e| { + format!( + "Failed to parse `{margin_x}` as a margin. \ + Please use a floating point number such as 0.5." + ) + })?; + let margin_y = margin_y.parse::().map_err(|_e| { + format!( + "Failed to parse `{margin_y}` as a margin. \ + Please use a floating point number such as 0.5." + ) + })?; + + Ok(Self::Different(margin_x, margin_y)) + } + None => { + let margin = s.parse::().map_err(|_e| { + format!( + "Failed to parse `{s}` as a margin. \ + Please use a floating point number such as 0.5." + ) + })?; + Ok(Self::Same(margin)) + } + } + } +} + +impl Display for Margin { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Margin::Same(margin) => margin.fmt(f), + Margin::Different(margin_x, margin_y) => write!(f, "{margin_x},{margin_y}"), + } + } +} + +impl<'de> Deserialize<'de> for Margin { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + FromStr::from_str(&s).map_err(de::Error::custom) + } +} + +impl Serialize for Margin { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} diff --git a/crate/model/src/common/graphviz_attrs/margins.rs b/crate/model/src/common/graphviz_attrs/margins.rs new file mode 100644 index 0000000..4a0d457 --- /dev/null +++ b/crate/model/src/common/graphviz_attrs/margins.rs @@ -0,0 +1,64 @@ +use std::ops::{Deref, DerefMut}; + +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +use crate::common::{graphviz_attrs::Margin, NodeId}; + +/// GraphViz node or cluster margins. `IndexMap` newtype. +/// +/// This is only used for GraphViz dot graphs, which sets the [`margin`] +/// attribute for the node / cluster. +/// +/// If this is unset, nodes will use the default [`NodeMargin`], and clusters +/// will use the default [`ClusterMargin`]. +/// +/// [`margin`]: https://graphviz.org/docs/attrs/margin/ +/// [`NodeMargin`]: crate::common::graphviz_attrs::NodeMargin +/// [`ClusterMargin`]: crate::common::graphviz_attrs::ClusterMargin +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] +pub struct Margins(IndexMap); + +impl Margins { + /// Returns a new `Margins` map. + pub fn new() -> Self { + Self::default() + } + + /// Returns a new `Margins` map with the given preallocated + /// capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self(IndexMap::with_capacity(capacity)) + } + + /// Returns the underlying map. + pub fn into_inner(self) -> IndexMap { + self.0 + } +} + +impl Deref for Margins { + type Target = IndexMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Margins { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From> for Margins { + fn from(inner: IndexMap) -> Self { + Self(inner) + } +} + +impl FromIterator<(NodeId, Margin)> for Margins { + fn from_iter>(iter: I) -> Self { + Self(IndexMap::from_iter(iter)) + } +} diff --git a/crate/model/src/common/graphviz_attrs/node_margin.rs b/crate/model/src/common/graphviz_attrs/node_margin.rs new file mode 100644 index 0000000..4da583f --- /dev/null +++ b/crate/model/src/common/graphviz_attrs/node_margin.rs @@ -0,0 +1,60 @@ +use std::fmt; + +use serde::{Deserialize, Serialize}; + +use crate::common::graphviz_attrs::Margin; + +/// A node [`margin`], which specifies space left around the node's label. +/// +/// Defaults to `Margin::Different(0.04, 0.04)`; Graphviz default: `0.11,0.055`. +/// +/// May be a single float, or two floats separated by a comma. +/// +/// [`margin`]: https://graphviz.org/docs/attrs/margin/ +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct NodeMargin(pub Margin); + +impl NodeMargin { + /// Returns a new `NodeMargin`. + pub fn new() -> Self { + Self::default() + } + + /// Returns the inner `Margin`. + pub fn into_inner(self) -> Margin { + self.0 + } +} + +impl Default for NodeMargin { + fn default() -> Self { + Self(Margin::Different(0.11, 0.055)) + } +} + +impl From for NodeMargin { + fn from(margin: Margin) -> Self { + Self(margin) + } +} + +impl std::ops::DerefMut for NodeMargin { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl std::ops::Deref for NodeMargin { + type Target = Margin; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl fmt::Display for NodeMargin { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} diff --git a/crate/model/src/common/graphviz_dot_theme.rs b/crate/model/src/common/graphviz_dot_theme.rs index c2321fc..43f6e4d 100644 --- a/crate/model/src/common/graphviz_dot_theme.rs +++ b/crate/model/src/common/graphviz_dot_theme.rs @@ -7,18 +7,6 @@ pub struct GraphvizDotTheme { // // pub node_text_color: &'static str, - /// Left and right margin in inches. - /// - /// Default: `0.04`. - /// - /// Graphviz default: `0.11`. - pub node_margin_x: f64, - /// Top and bottom margin in inches. - /// - /// Default: `0.04`. - /// - /// Graphviz default: `0.055`. - pub node_margin_y: f64, pub plain_text_color: &'static str, pub emoji_point_size: u32, /// Default font point size for node labels. @@ -58,16 +46,6 @@ impl GraphvizDotTheme { self } - pub fn with_node_margin_x(mut self, node_margin_x: f64) -> Self { - self.node_margin_x = node_margin_x; - self - } - - pub fn with_node_margin_y(mut self, node_margin_y: f64) -> Self { - self.node_margin_y = node_margin_y; - self - } - pub fn with_plain_text_color(mut self, plain_text_color: &'static str) -> Self { self.plain_text_color = plain_text_color; self @@ -126,14 +104,6 @@ impl GraphvizDotTheme { self.node_text_color } - pub fn node_margin_x(&self) -> f64 { - self.node_margin_x - } - - pub fn node_margin_y(&self) -> f64 { - self.node_margin_y - } - pub fn plain_text_color(&self) -> &str { self.plain_text_color } @@ -180,8 +150,6 @@ impl Default for GraphvizDotTheme { Self { edge_color: "#333333", node_text_color: "#111111", - node_margin_x: 0.04, - node_margin_y: 0.04, plain_text_color: "#222222", emoji_point_size: 14, node_point_size: 10, diff --git a/crate/rt/src/into_graphviz_dot_src/info_graph.rs b/crate/rt/src/into_graphviz_dot_src/info_graph.rs index 3f6e2c4..3ecf3ae 100644 --- a/crate/rt/src/into_graphviz_dot_src/info_graph.rs +++ b/crate/rt/src/into_graphviz_dot_src/info_graph.rs @@ -287,9 +287,8 @@ fn node_attrs( }; let node_text_color = theme.node_text_color(); let node_point_size = theme.node_point_size(); - let node_margin_x = theme.node_margin_x(); - let node_margin_y = theme.node_margin_y(); + let node_margin = graphviz_attrs.margin_node_default(); let node_width = graphviz_attrs.node_width_default(); let node_height = graphviz_attrs.node_height_default(); let fixed_size = graphviz_attrs.fixed_size(); @@ -307,7 +306,7 @@ fn node_attrs( {node_style_and_shape} width = {node_width} height = {node_height} - margin = "{node_margin_x:.3},{node_margin_y:.3}" + margin = "{node_margin}" {fixed_size} ] "# @@ -374,6 +373,8 @@ fn node_cluster_internal( .get(&AnyId::from(node_id.clone())) .map(AsRef::::as_ref) .unwrap_or_default(); + let graphviz_attrs = info_graph.graphviz_attrs(); + let margins = graphviz_attrs.margins(); let node_point_size = theme.node_point_size(); let node_name = node_names.get(node_id).map(String::as_str); @@ -426,31 +427,44 @@ fn node_cluster_internal( // Same thing happens for `{edge_tag_classes}` if node_hierarchy.is_empty() { match graph_style { - GraphStyle::Box => writedoc!( - buffer, - r#" - {node_id} [ - label = < - - {image}{emoji} - - {node_desc} -
{node_label}
> - class = "{OUTLINE_NONE} {node_tailwind_classes}{node_tag_classes}" - {node_width} - {node_height} - ] - "# - )?, + GraphStyle::Box => { + let margin = margins + .get(node_id) + .copied() + .map(|margin| Cow::::Owned(format!(r#"margin = "{margin}""#))) + .unwrap_or_default(); + + writedoc!( + buffer, + r#" + {node_id} [ + label = < + + {image}{emoji} + + {node_desc} +
{node_label}
> + class = "{OUTLINE_NONE} {node_tailwind_classes}{node_tag_classes}" + {node_width} + {node_height} + {margin} + ] + "# + )? + } GraphStyle::Circle => { // `margin` doesn't apply to `plain` shaped nodes, so we use rectangle and use // an invisible colour. - let margin = match graph_dir { + let margin_outer = margins + .get(node_id) + .copied() + .unwrap_or_else(|| graphviz_attrs.margin_node_default().into_inner()); + let margin_inner = match graph_dir { GraphDir::Horizontal => "margin = \"0.11,0.07\"", GraphDir::Vertical => "margin = \"0.13,0.055\"", }; @@ -464,7 +478,7 @@ fn node_cluster_internal( r#" subgraph cluster_{node_id} {{ label = <> - margin = 0.0 + margin = "{margin_outer}" class = "{OUTLINE_NONE}" {node_width} {node_height} @@ -472,12 +486,12 @@ fn node_cluster_internal( {node_id} [ label = "" class = "{OUTLINE_NONE} {node_tailwind_classes}{node_tag_classes}" - {margin} + {margin_inner} ] {node_id}_text [ fillcolor="{no_color}" shape="rectangle" - {margin} + {margin_inner} label = < Date: Sun, 13 Oct 2024 14:21:17 +1300 Subject: [PATCH 2/2] Update `CHANGELOG.md`. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 830d53f..f739fda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## unreleased +* Support `margins` in `GraphvizAttrs`. +* Support `margin_cluster_default` in `GraphvizAttrs`. +* Support `margin_node_default` in `GraphvizAttrs`. * Support `node_width_default` in `GraphvizAttrs`. * Support `node_widths` in `GraphvizAttrs`. * Support `node_height_default` in `GraphvizAttrs`.