Common
SomeGraphs.Common
—
Module
Common data types and utilities for defining specific graph types.
SomeGraphs.Common.Graph
—
Type
The type of a figure we can display. This is a combination of some
AbstractGraphData
and
AbstractGraphConfiguration
. Accessing the
.figure
property of the graph will return it as a
PlotlyFigure
, which can be displayed in interactive environments (Julia REPL and/or Jupyter notebooks).
You should call
save_graph
to save the graph to a file, instead of calling
savefig
on the
.figure
, because the latter does't obey the graph size, because Plotly.
SomeGraphs.Common.AbstractGraphConfiguration
—
Type
A configuration of a
Graph
specifies how to display the data while being (as much as possible) independent of the data itself. That is, it should be possible to apply the same configuration to multiple sets of data to generate multiple similar graphs. In some cases (e.g., colors) you can specify a default in the configuration and override it for specific entities in the data.
SomeGraphs.Common.AbstractGraphData
—
Type
Some data of a
Graph
specifies what to display the data while being (as much as possible) independent of how to display it. That is, it should be possible to apply multiple sets of data to the same configuration to to generate multiple similar graphs. In some cases (e.g., colors) you can specify a default in the configuration and override it for specific entities in the data.
SomeGraphs.Common.PlotlyFigure
—
Type
The type of a rendered graph which Julia knows how to display.
A plotly figure contains everything needed to display an interactive graph (or generate a static one on disk). It can also be converted to a JSON string for handing it over to a different programming language (e.g., to be used to display the interactive graph in a Python Jupyter notebook, given an appropriate wrapper code).
SomeGraphs.Common.save_graph
—
Function
save_graph(graph::Graph, output_file::AbstractString)::Nothing
Save the graph to a file. Unlike the Plotly
savefig
function, this function will actually obey the
width
and
height
parameters specified in the graph's configuration. The format is deduced from the suffix of the file name.
SomeGraphs.Common.FigureConfiguration
—
Type
@kwdef mutable struct FigureConfiguration <: Validated
margins::MarginsConfiguration = MarginsConfiguration()
width::Maybe{Int} = nothing
height::Maybe{Int} = nothing
grid_color::AbstractString = "lightgrey"
background_color::AbstractString = "white"
paper_color::AbstractString = "white"
colors_scale_offsets::AbstractVector{<:Real} = [1.2, 1.4, 1.6, 1.8, 2.0]
end
Generic configuration that applies to the whole figure. Each complete
AbstractGraphConfiguration
contains a
figure
field of this type.
The optional
width
and
height
are in pixels, that is, 1/96 of an inch. The
margins
are specified in the same units.
You can also manually change the
background_color
(inside the graph's area) and
paper_color
(outside the graph's area, that is, the margins). the axes.
If a graph has both a legend and a color scale, or multiple color scales, then by default, Plotly in its infinite wisdom will happily place them all on top of each other. We therefore need to tell it how to position each and every color scale (except the 1st one if there's no legend) by specifying an explicit offset. Great fun!
These
colors_scale_offsets
are specified in what Plotly calls "paper coordinates" which are singularly unsuitable for this purpose, as they are in a scale where 0 to 1 is the plot area and therefore dependent not only on the width of the preceding legend and/or color scales (which is bad by itself), but also on the overall size of the graph (so scaling an interactive graph will definitely misbehave). We provide a vector of hopefully reasonable default offsets here. For optimal results you will need to manually tweak these to match your specific graph. In 21st century, when "AI" is a thing. Sigh.
We provide "too many" (5) offsets here. Normally a graph displays one color scale, but in theory it can have any number (e.g. when using annotations in bars or heatmap graphs). You will need to extend this array if you have a graph that requires more offsets.
SomeGraphs.Common.Stacking
—
Type
If stacking elements, how to do so:
StackValues
just adds the raw values on top of each other.
StackFractions
normalizes the values so their sum is 1. This can be combined with setting the
percent
field of the relevant
AxisConfiguration
to display percents.
Examples:
Default (serves as a baseline to compare with when modifying options):
using SomeGraphs
graph = distribution_graph(; distribution_values = [0, 0, 1, 1, 1, 3])
using PlotlyDocumenter
to_documenter(graph.figure)
Reduced size:
using SomeGraphs
graph = distribution_graph(; distribution_values = [0, 0, 1, 1, 1, 3])
graph.configuration.figure.width = 300
graph.configuration.figure.height = 300
using PlotlyDocumenter
to_documenter(graph.figure)
Change colors:
using SomeGraphs
graph = distribution_graph(; distribution_values = [0, 0, 1, 1, 1, 3])
graph.configuration.figure.background_color = "lightyellow"
graph.configuration.figure.paper_color = "lightgrey"
using PlotlyDocumenter
to_documenter(graph.figure)
SomeGraphs.Common.MarginsConfiguration
—
Type
@kwdef mutable struct MarginsConfiguration <: Validated
left::Int = 50
bottom::Int = 50
right::Int = 50
top::Int = 50
end
Configure the margins of the graph. Sizes are in pixels (1/96th of an inch). Plotly is uncapable of automatically adjusting the margins to adapt to tick labels, so you may have to tweak this manually to avoid clipping or reduce wasted white space. In the 21st century. Sigh.
Examples:
Increased margins:
using SomeGraphs
graph = distribution_graph(; distribution_values = [0, 0, 1, 1, 1, 3])
graph.configuration.figure.margins.left = 100
graph.configuration.figure.margins.right = 200
graph.configuration.figure.margins.top = 150
graph.configuration.figure.margins.bottom = 250
using PlotlyDocumenter
to_documenter(graph.figure)
SomeGraphs.Common.AxisConfiguration
—
Type
@kwdef mutable struct AxisConfiguration <: Validated
minimum::Maybe{Real} = nothing
maximum::Maybe{Real} = nothing
log_scale::Maybe{LogScale} = nothing
log_regularization::Real = 0
percent::Bool = false
show_ticks::Bool = true
show_grid::Bool = true
grid_color::AbstractString = "lightgrey"
title::Maybe{AbstractString} = nothing
end
Generic configuration for a graph axis. Everything is optional; by default, the
minimum
and
maximum
are computed automatically from the data.
If
log_scale
is specified, then the
log_regularization
is added to the coordinate to avoid zero values, and the axis is shown according to the
LogScale
. Otherwise,
log_regularization
must be 0.
If
percent
is set, then the values are multiplied by 100 and a
%
suffix is added to the tick labels.
The
show_ticks
and/or
show_grid
can be disabled for a cleaner (though less informative) graph appearance. By default the grid lines are shown in
lightgrey
.
The minimum/maximum, data values, color palette values etc. are all in the original scale. That is, you should be able to control log scale and/or percent scaling without changing anything else.
If
title
is specified, it will be shown next to the axis. However, in some cases the correct title depends on the data set, so you can override this in the data.
Examples:
Disable ticks:
using SomeGraphs
graph = distribution_graph(; distribution_values = [0, 0, 1, 1, 1, 3])
graph.configuration.value_axis.show_ticks = false
using PlotlyDocumenter
to_documenter(graph.figure)
Disable grid:
using SomeGraphs
graph = distribution_graph(; distribution_values = [0, 0, 1, 1, 1, 3])
graph.configuration.value_axis.show_grid = false
using PlotlyDocumenter
to_documenter(graph.figure)
Override grid color:
using SomeGraphs
graph = distribution_graph(; distribution_values = [0, 0, 1, 1, 1, 3])
graph.configuration.value_axis.grid_color = "red"
using PlotlyDocumenter
to_documenter(graph.figure)
Override range:
using SomeGraphs
graph = distribution_graph(; distribution_values = [0, 0, 1, 1, 1, 3])
graph.configuration.value_axis.minimum = 1
graph.configuration.value_axis.maximum = 4
using PlotlyDocumenter
to_documenter(graph.figure)
Percent:
using SomeGraphs
graph = distribution_graph(; distribution_values = [0, 0, 1, 1, 1, 3])
graph.configuration.value_axis.percent = 1
using PlotlyDocumenter
to_documenter(graph.figure)
SomeGraphs.Common.LogScale
—
Type
Supported log scales (when log scaling is enabled):
-
Log10Scaleconverts values to their log (base 10). -
Log2Scaleconverts values to their log (base 2).
Examples:
Log 2:
using SomeGraphs
graph = distribution_graph(; distribution_values = [0, 0, 1, 1, 1, 3])
graph.configuration.value_axis.log_scale = Log2Scale
graph.configuration.value_axis.log_regularization = 1
using PlotlyDocumenter
to_documenter(graph.figure)
Log 10:
using SomeGraphs
graph = distribution_graph(; distribution_values = [0, 0, 1, 1, 1, 3])
graph.configuration.value_axis.log_scale = Log10Scale
graph.configuration.value_axis.log_regularization = 1e-5
using PlotlyDocumenter
to_documenter(graph.figure)
SomeGraphs.Common.SizesConfiguration
—
Type
@kwdef mutable struct SizesConfiguration <: Validated
fixed::Maybe{Real} = nothing
minimum::Maybe{Real} = nothing
maximum::Maybe{Real} = nothing
log_scale::Bool = false
log_regularization::Real = 0
smallest::Real = 6
span::Real = 12
end
Configure how to map sizes data to a size in pixels (1/96th of an inch). If
fixed
is specified, it is the size to be used, and none of the other fields should be set (and no sizes data may be specified). Otherwise, sizes data must be specified. The minimal size data value (or any values at most the specified
minimum
) is mapped to the
smallest
size in pixels, and the maximum size data value (or any values at least the specified
maximum
) is mapped to a size with an additional
span
in pixels. If
log_scale
, then we use the log of the data values (and of the specified
minimum
and/or
maximum
, if any), adding the
log_regularization
to avoid a log of zero or negative values.
SomeGraphs.Common.LineConfiguration
—
Type
@kwdef mutable struct LineConfiguration <: Validated
width::Maybe{Real} = nothing
style::Maybe{LineStyle} = SolidLine
is_filled::Bool = false
color::Maybe{AbstractString} = nothing
end
Configure a line in a graph.
If
style
is
nothing
no line is drawn. If
is_filled
then the region defined by the line (below it in a lines graph) is filled. A line with no style and no filled is not drawn but can still be used to define a region (e.g., for a
BandsConfiguration
).
By default, the
color
is chosen automatically.
SomeGraphs.Common.LineStyle
—
Type
Styles of drawing a line
-
SolidLinedraws a solid line (the default). -
DashLinedraws a dashed line. -
DotLinedraws a dotted line. -
DashDotLinedraws a dash-dotted line.
SomeGraphs.Common.ValuesOrientation
—
Type
The orientation of the values axis in a distribution(s) or bars graph:
HorizontalValues
- The values are the X axis
VerticalValues
- The values are the Y axis (the default).
SomeGraphs.Common.BandsConfiguration
—
Type
@kwdef mutable struct BandsConfiguration <: Validated
low::BandConfiguration = BandConfiguration(is_dashed = true)
middle::BandConfiguration = BandConfiguration()
high::BandConfiguration = BandConfiguration(is_dashed = true)
end
Configure the partition of the graph up to three band regions. The
low
and
high
bands are for the "outer" regions (so their lines are at their border, dashed by default) and the
middle
band is for the "inner" region between them (so its line is inside it, solid by default).
If
show_legend
, then a legend showing the bands will be shown.
SomeGraphs.Common.BandConfiguration
—
Type
@kwdef mutable struct BandConfiguration <: Validated
offset::Maybe{Real} = nothing
line::LineConfiguration = LineConfiguration()
end
Configure a region of the graph defined by some band of values.
The
offset
specifies the band's defining line position. We allow up to three bands in a complete
BandsConfiguration
. A band exists only if its
offset
is specified, in which case the
line
specifies how to render its defining line. The low and high bands are defined the region below and above their defining line's
offset
. If both are defined, the middle band
offset
defines the center line of the band; the band can still be filled even if this offset is not specified.
SomeGraphs.Common.BandsData
—
Type
@kwdef mutable struct BandsData
low_offset::Maybe{Real} = nothing
middle_offset::Maybe{Real} = nothing
high_offset::Maybe{Real} = nothing
end
Specify data for bands.
SomeGraphs.Common.ColorsConfiguration
—
Type
@kwdef mutable struct ColorsConfiguration <: Validated
fixed::Maybe{AbstractString} = nothing
palette::Maybe{Union{AbstractString, ContinuousColors, CategoricalColors}} = nothing
axis::AxisConfiguration = AxisConfiguration()
show_legend::Bool = false
title::Maybe{AbstractString} = nothing
end
Configure how to color some data. Supported combinations of configuration and data are:
fixed
|
palette
|
colors data |
axis
|
show_legend
|
Behavior |
|---|---|---|---|---|---|
| color name |
nothing
|
nothing
|
Restricted(A) |
false
|
Named fixed (1) |
nothing
|
nothing
|
nothing
|
Restricted(A) |
false
|
Auto fixed (2) |
nothing
|
nothing
|
str[] | Restricted(A) |
false
|
Named data (3) |
nothing
|
nothing
|
num[] | Any | Any | Auto scale (4) |
nothing
|
palette name | num[] | Any | Any | Named scale (5) |
nothing
|
(value::num, color::str)[] | num[] | Any | Any | Manual scale (6) |
nothing
|
Dict{value::str => color::str} | str[] | Restricted(A) | Any | Categorical (7) |
Any other combination of configuration is not allowed.
Restricted Axis:
The
axis
can't specify
log_scale
,
percent
,
minimum
,
maximum
as they make no sense in this case.
Named fixed (1):
All the data entities will be given the same
fixed
color.
Auto fixed (2): All the data entities will be given the same color, chosen automatically by Plotly.
Named data (3):
The colors data contains explicit color names. An empty color name will prevent the matching data from being plotted. If the
fixed
color is specified, it is ignored.
Auto scale (4):
The colors data (transformed by the
axis
) will be shown in a color scale chosen by Plotly.
Named scale (5):
The colors data (transformed by the
axis
) will be shown using the named standard Plotly
color scale
(see
NAMED_COLOR_SCALES
).
Manual scale (6):
The colors data (transformed by the
axis
) will be shown using the specified palette (whose values will also be transformed by the
axis
). The values must be in non-decreasing order, and the overall range of values must not be empty.
Categorical (7): The colors data contains valid value keys of the categorical colors dictionary. An empty color name in the data or the dictionary will prevent the matching data from being plotted.
If
show_legend
is specified, categorical colors (case 7 above) will be shown in the legend; numerical colors will be shown in a color scale. Plotly is dumb when it comes to positioning color scales next to a legend (or next to each other); see the
colors_scale_offsets
vector of
FigureConfiguration
for details.
If
title
is specified, it will be used when showing the legend (whatever it is). However, in some cases the correct title depends on the data set, so you can override this in the data.
SomeGraphs.Common.ContinuousColors
—
Type
A continuous colors palette, mapping numeric values to colors. We also allow specifying tuples instead of pairs to make it easy to invoke the API from other languages such as Python which do not have the concept of a
Pair
. The
legend_title
is only used if
show_legend
is set in the configuration.
SomeGraphs.Common.CategoricalColors
—
Type
A categorical colors palette, mapping string values to colors. An empty string color means the entity will not be shown (as if it was masked or never included in the data). As a convenience, a named vector of strings can be specified instead of a dictionary.
SomeGraphs.Common.categorical_palette
—
Function
categorical_palette(
values::Union{AbstractSet{<:AbstractString}, AbstractVector{<:AbstractString}},
palette::Union{AbstractString, ContinuousColors} = "HSV",
)::CategoricalColors
Given some string
values
, generate a categorical palette for them based on some continuous
palette
. This is done by giving each unique value an index from 1 to N and then mapping the range 0 to N into the continuous palette (adding 0 allows for safely using cyclical palettes). This palette should have as many distinct colors in it as possible, to maximize legibility of the result;
HSV
is a good default choice.
This is
not
the best way to color categorical data. It is merely an adequate one for quick and dirty results. A manually created palette would be better for a low number of values, and for a high number of values,
HSV
uses only a 1D subset of the 3D color space. It would be nice to offer a robust "generate N colors most different from each other" function... PRs are welcome :-)
SomeGraphs.Common.NAMED_COLOR_SCALES
—
Constant
Builtin color scales from
Plotly
, both linear:
Blackbody
,
Bluered
,
Blues
,
Cividis
,
Earth
,
Electric
,
Greens
,
Greys
,
Hot
,
Jet
,
Picnic
,
Portland
,
Rainbow
,
RdBu
,
Reds
,
Viridis
,
YlGnBu
,
YlOrRd
and cyclical:
Twilight
,
IceFire
,
Edge
,
Phase
,
HSV
,
mrybm
,
mygbm
. In addition,
BuWtRd
which is useful for showing +/- diff values and
WtOrRdDm
is useful for showing absolute values.
You would think we could just give the builtin color scale names to plotly, but it turns out that "builtin" in Python plotly doesn't mean "builtin" in JavaScript plotly, because "reasons". We therefore have to copy their definition here. An upside of having this dictionary is that you are free to insert additional named scale into and gain the convenience of refering to them by name (e.g., for coloring heatmap annotations).
A
_r
suffix specifies reversing the order of the scale.
You can also append a final
_z:<value_fraction>:<color_fraction>
suffix to the name. This will map values in the bottom 0..
value_fraction
of the range to white, and map the rest of the values to the top
color_fraction
..1 range of the scale. For example,
Blues_z:0.3:0.2
will color the bottom 30% of the values in white, and color the top 70% of the values to the top 80% of the
Blues
scale.
A
_c:<value_fraction>:<color_fraction>
works similarly to the
_z
suffix, except that the fractions are centered on 0.5 (the middle) of the values and color ranges. This is meant to be applied when the values range is +/-Diff, and the scale has white in the middle. For example
RdBu_c:0.3:0.2
will color the 30% of the values near the 0 middle in white, and color the rest of the values using the top and bottom 40% of the
RdBu
scale.
An
_o:<value_fraction>:<color>
suffix maps the scale to the range 0..
1-value_fraction
, and all the values above this to the
color
, to denote overflow (too high) values. For example,
Blues_o:0.01:magenta
will color all the values in the bottom 99% of the values range to
Blues
, and the top 1% of the range to magenta. A
_u
suffix works similarly but applies to the bottom range of the values.
You can combine multiple suffixes together, for example
Reds_z:0.2:0.2_o:0.99:magenta
or
RdBu_r_c:0.2:0.2_o:0.01:magenta_u:0.01:darkgreen
.
Palettes with suffixes (including
_r
) are computed on the fly and cached for future use.
The implementation of the suffixes uses
1e-6
as a color difference "too small to matter". Don't use fractions this small in the prefixes or you will have a bad day.
Always hold the
COLOR_SCALES_LOCK
when manually accessing the
NAMED_COLOR_SCALES
, otherwise you
will
regret it at some point.
SomeGraphs.Common.COLOR_SCALES_LOCK
—
Constant
A global lock to use when accessing the
NAMED_COLOR_SCALES
. Always hold the
COLOR_SCALES_LOCK
when manually accessing the
NAMED_COLOR_SCALES
, otherwise you
will
regret it at some point.
SomeGraphs.Common.AnnotationData
—
Type
@kwdef mutable struct AnnotationData <: Validated
title::Maybe{AbstractString} = nothing
values::Union{AbstractVector{<:Real}, AbstractVector{<:AbstractString}} = Float32[]
hovers::Maybe{AbstractVector{<:AbstractString}} = nothing
colors::ColorsConfiguration = ColorsConfiguration()
end
An annotation to attach to an axis. This applies to discrete axes (bars axis for a
BarsGraph
or the rows and/or columns of a
HeatmapGraph
). The number of the
values
and the optional
hovers
must be the same as the number of entries in the axis.
The
colors
configuration is part of the data, because the color of annotations (in particular, categorical ones) is tightly coupled with the data.
SomeGraphs.Common.AnnotationSize
—
Type
@kwdef mutable struct AnnotationSize
size::AbstractFloat = 0.05
gap::AbstractFloat = 0.005
end
Specify the sizes of annotations shown to the side of an axis, with a gap between each other and the heatmap itself. The sizes are in the usual inconvenient units (fraction of the overall graph size), because Plotly.
Index
-
SomeGraphs.Common -
SomeGraphs.Common.COLOR_SCALES_LOCK -
SomeGraphs.Common.NAMED_COLOR_SCALES -
SomeGraphs.Common.AbstractGraphConfiguration -
SomeGraphs.Common.AbstractGraphData -
SomeGraphs.Common.AnnotationData -
SomeGraphs.Common.AnnotationSize -
SomeGraphs.Common.AxisConfiguration -
SomeGraphs.Common.BandConfiguration -
SomeGraphs.Common.BandsConfiguration -
SomeGraphs.Common.BandsData -
SomeGraphs.Common.CategoricalColors -
SomeGraphs.Common.ColorsConfiguration -
SomeGraphs.Common.ContinuousColors -
SomeGraphs.Common.FigureConfiguration -
SomeGraphs.Common.Graph -
SomeGraphs.Common.LineConfiguration -
SomeGraphs.Common.LineStyle -
SomeGraphs.Common.LogScale -
SomeGraphs.Common.MarginsConfiguration -
SomeGraphs.Common.PlotlyFigure -
SomeGraphs.Common.SizesConfiguration -
SomeGraphs.Common.Stacking -
SomeGraphs.Common.ValuesOrientation -
SomeGraphs.Common.categorical_palette -
SomeGraphs.Common.save_graph