Goal
The goal is to merge the default styles of a few different charts; namely, to have a chart that:
- has the key components of
BoxWhiskerChart
- the outline of the
DistributionChart
'sChartElementFunction
$\rightarrow$SmoothDensity
- has the points of the
DistributionChart
ChartElementFunction
$\rightarrow$PointDensity
Further, I would like to have the ability to:
- color the points based on other data
- add
Callout
s to specified points.
Final outcome addressing all the requirements above should look like:
Example
Sample data
data = {RandomInteger[100, 30], RandomInteger[100, 30]};
Individual chart elements
BoxWhiskerChart[data]
DistributionChart[data]
DistributionChart[data, ChartElementFunction -> "PointDensity"]
External data for color:
With[{perm=Permute[data[[1]]]}, class1 = perm[[1;;Floor@Length@perm/2]]; class2 = perm[[Floor@Length@perm/2+1;;]]]
Such that points belonging to class1 are one color and class2 are another.
Here is an image showing the transformations I would like to make:
Understanding @kglr's code
Kudos to @kglr for being able to do this, but I have very little idea of what is going on in the code. Let's break it down line, by line, and perhaps tweak this a bit for re-use :)
For two dimensional charts, ChartElementFunction
passes region
, values
and metadata
to the function. So let's start by changing the function definition to be more readable:
Options[BoxWhiskerDistributionChart] = {"InternalOpacity" -> 0.7};
BoxWhiskerDistributionChart[{{xmin_, xmax_}, {ymin_, ymax_}}, values_,metadata_, OptionsPattern[]] :=
Module[
{parameters = {{{xmin, xmax}, {ymin, ymax}}, values, metadata}},
{
(*block of code*)
}
]
Yes the definition is longer, but it is more readable to people (like me) who do not dabble with customizing graphic often. The local variable parameters
will take the place of ##
.
Inside the block of code, the first two lines are:
EdgeForm[{Thick, Charting`ChartStyleInformation["Color"]}],
FaceForm[Opacity[.7, Charting`ChartStyleInformation["Color"]]],
I do not understand why we have to call the Charting` context... I believe, we could tweak this to be:
EdgeForm[Thick],
FaceForm[Opacity[OptionValue["InternalOpacity"]]],
Next we have the first With
block:
With[
{dd = #2},
Normal[ChartElementDataFunction["SmoothDensity"][##]] /.
GeometricTransformation[Polygon[x_, y___], tr_] :>
Polygon[DeleteCases[
AffineTransform[tr]@
x, {_, _?((# < Quantile[dd, .25] || # > Quantile[dd, .75]) &)}],
y]
]
First we will apply some changes in variables - according to our new function definition - remove some unneeded code, and rename other variables.
ChartElementDataFunction["SmoothDensity"][parameters] /.
GeometricTransformation[Polygon[x_, y___], transformation_] :>
Polygon[DeleteCases[
AffineTransform[transformation_]@
x, {_, _?((# < Quantile[values, .25] || # > Quantile[values, .75]) &)}],
y]
The first line of this chuck of code:
ChartElementDataFunction["SmoothDensity"][parameters]
Is just the operator form of applying the "SmoothDensity"
ChartElementFunction that we want to our current parameter. The Normal
was not needed.
Because our new function definition gives us access to the values, we can remove the With
block.
My remaining questions on this chuck of the code are as follows:
You have an internally defined function with a delayed rule (see below). What is the transformation function. Where did it come from?
GeometricTransformation[Polygon[x_, y___], tr_] :> etc
2.) I can see what this does (selects only the data within the .25=.75 quantiles), but I have no idea how:
AffineTransform[transformation_]@x, {_, _?((# < Quantile[values, .25] || # > Quantile[values, .75]) &)}],y]
So we apply the mysterious transformation to only the x values for some reason, then pattern match for any pair such that the y value falls in the right quantile.
This entire line has to have a cleaner way of being written (e.g. write out the variables for y_ _ _, etc. I just don't know what that is...
The next line is straight forward. Nothing to change.
Now we add the points with this chuck:
ChartElementDataFunction["PointDensity",
"PointStyle" -> PointSize[Medium]][##] /.
GraphicsGroup[{x_, y__}] :> ({y} /.
Point[p_] :> (({If[MemberQ[class, #[[2]]], col], Point[{#}]} & /@
p)))
Again we can rewrite parts of it:
ChartElementDataFunction["PointDensity",
"PointStyle" -> PointSize[Medium]][parameters] /.
GraphicsGroup[{x_, y__}] :> ({y} /.
Point[p_] :> (({If[MemberQ[class, #[[2]]], col], Point[{#}]} & /@
p)))
This code isolates which points are of interest and changes their color. However I do not find this code straight forward with two delayed rules and replacement.
So if someone can explain this to me? All of it.
The next two lines, are called to overwrite the "PointDensity" Edge and Face Forms.
While I see that it works. Why do we have to recall them? Can't we just assign them prior to PointDensity and only take the Points?
and the last line is best summed up by @kglr:
Finally, SystemBarFunctionDump
boxplot[] produces the basic box-whisker graphics objects in BoxWhiskerChart, and the modification I made is to add the directives EdgeForm[] and FaceForm[]
Although I do not know why we have to call that context... Is there a way to do this without getting into contexts?
Ideal
The following code outlines a more ideal solution, allowing for users to alter various parameters, change the cutoff, and specify points to highlight via the corresponding option.
This does not work. I think it either has something to do with Module
, or the explicit use of the parameters...
Options[BoxWhiskerDistributionChart] = {"InternalOpacity" -> 0.7,
"PointsToHighlight" -> {}, "PointToHighlightColor" -> Black,
"UpperCutoff" -> .75, "LowerCutoff" -> .25,
"EdgeColor" -> Charting`ChartStyleInformation["Color"] };
BoxWhiskerDistributionChart[{{xmin_, xmax_}, {ymin_, ymax_}}, values_,
metadata_, OptionsPattern[]] :=
Module[
{chartParameters = {{{xmin, xmax}, {ymin, ymax}}, values, metadata}},
{
(*Give boxes thick edges with their respective color*)
EdgeForm[{Thick, OptionValue["EdgeColor"]}],
(*Alter boxes internal opacity with their respective color*)
FaceForm[
Opacity[OptionValue["InternalOpacity"],
Charting`ChartStyleInformation["Color"]]],
(*Use smooth density to change box shape,
with given quantile cutoffs*)
ChartElementDataFunction["SmoothDensity"][chartParameters] /.
GeometricTransformation[Polygon[x_, y___], transformation_] :>
Polygon[
DeleteCases[
AffineTransform[transformation]@
x, {_, _?((# <
Quantile[values, OptionValue["LowerCutoff"]] || # >
Quantile[values, OptionValue["UpperCutoff"]]) &)}], y],
Opacity[1],
(*Color points also foudn in the PointsToHighlight option*)
ChartElementDataFunction["PointDensity",
"PointStyle" -> PointSize[Medium]][chartParameters] /.
GraphicsGroup[{x_, y__}] :> ({y} /.
Point[p_] :> (({If[
MemberQ[OptionValue["PointsToHighlight"], #[[2]]],
OptionValue["PointToHighlightColor"]], Point[{#}]} & /@
p))),
(*Remove the default BoxWhiskerChart EdgeForm*)
EdgeForm[],
(*Remove the default BoxWhiskerChart FaceForm*)
FaceForm[],
(*Produce the plot*)
System`BarFunctionDump`boxplot[][chartParameters]
} &;
]
Graphics[Cases[Level[DistributionChart[data],-1],GeometricTransformationBox[PolygonBox[_],___]],AspectRatio->1]
$\endgroup$