# Comparing two groups - stacked bar chart with representation of each data point

I have a dataset comparing the performance of students on two exams, Exam 1 and Exam 2: I would like to create a stacked bar chart where each bar represents the grades on Exam 1 and the stacks in each bar represent the grades on Exam 2. Here's the catch: instead of having solid bars representing the number of students, I want to have a symbol (eg, circle) represent each student. See drawing below: So by counting the number of dots in the A column, I know how many students got an A on Exam 1. By counting the total number of green dots, I know how any students got an A on Exam 2. By counting the number of green dots in column A, I know how many students got As on Exam 1 and Exam 2.

I am, of course, open to any suggestions on how to display data of this type that conveys the same information.

Update: Using a custom ChartElementFunction to represent each data point with a disk:

ClearAll[ceF]
ceF[nc_: 5, sz_: 5, shft_: 5][ clr_: "Pastel"] :=
Module[{nrows, disks, bb = ChartingChartStyleInformation["BoundingBox"][],
ncols = Range[#[[1, 1]], #[[1, 2]], (#[[1, 2]] - #[[1, 1]])/(nc - 1)],
cols = If[Head[clr] === String,
ColorData[clr] /@ Range[0, 1, 1/(Length[#3[]] - 1)], clr]},
nrows = Range[## & @@ (shft+bb), (bb[] - bb[])/Quotient[bb[] - bb[], nc]];
disks = Disk[#, Offset[sz]] & /@ (Reverse /@ Tuples[{nrows, ncols}][[;; #2]]);
Thread[{Join @@ ConstantArray @@@ Thread[{cols, #3[]}], disks}]] &;


Examples:

Use the total of each data group as main data and pass its breakdown as metadata to be processed by the ChartElementFunction to render bars filled with appropriate number of colored disks:

data = {{10, 8, 2}, {12, 15, 4}, {5, 7, 7}};
bcdata = Thread[Total /@ data -> data];

Labeled[BarChart[bcdata, ChartElementFunction -> ceF[][],
BarSpacing -> {1, 0}, ChartLabels -> {Style[#, 16] & /@ {"A", "B", "<=C"}},
Axes -> {True, False},
ChartLegends -> SwatchLegend["Pastel", Style[#, 16] & /@ {"A", "B", "<=C"},
LegendLayout -> "ReversedColumn",
LegendLabel -> Style["Exam 2 Grades", 16, "Panel"]]],
{Rotate[Style["Counts", 16, "Panel"], 90 Degree],
Style["Exam 1", 16, "Panel"]}, {Left, Bottom}] Use ChartElementFunction -> ceF[][{Red, Green, Blue}] with appropriate changes in the legend to get Use ChartElementFunction -> ceF[3, 7][{Red, Green, Blue}] to get larger disks arranged in three columns: etc..

data = {{10, 8, 2}, {12, 15, 4}, {5, 7, 7}};


Using a standard BarChart you can do

Labeled[BarChart[data, ChartLayout -> "Stacked",
ChartLabels -> {Style[#, 16] & /@ {"A", "B", "<=C"}, None},
ChartLegends -> SwatchLegend[Style[#, 16] & /@ {"A", "B", "<=C"},
LegendLayout -> "ReversedColumn",
LegendLabel -> Style["Exam 2 \nGrades", 16, "Panel"]]],
{Rotate[Style["Count", 16, "Panel"], 90 Degree], Style["Exam 1", 16, "Panel"]},
{Left, Bottom}] Use labeleddata

labeleddata = Thread[Labeled[#, StringJoin @@@ Thread[{{"A : ", "B : ", "<=C : "},
ToString /@ #}], Center]] & /@ data;


as the first argument of BarChart` above to get 