How to create a rowstacked and clustered bar chart in gnuplot?

Viewed 56

Yet another gnuplot bar chart (histogram) question. Starting from a flat list (could be any order), I want to create a normalized rowstacked and clustered bar chart.

  • There is a similar question with an answer, however, with no response of the OP and the suggestion in the answer does not cover my case.

  • Another question to the same topic, however, with answers without the desired solution.

Looking at the gnuplot homepage, gnuplot offers rowstacked histograms and clustered histograms. But apparently, no rowstacked and clustered histogram. Although, there is newhistogram, which however, places a separate bar chart on the x-axis next to the previous one. Not interlaced as I would like it to be.

So, if I use:

  • set style histogram rowstacked clustered, the bar chart will be just clustered (see first graph below)
  • set style histogram clustered rowstacked, the bar chart will continue stacking (see second graph below)

Apparently, the later keyword overwrites the former one. Furthermore, I thought I could use simply set style histogram rowstacked and shift the first bar chart a bit to the left and the second one a bit to the right and use thinner bars, but apparently gnuplot doesn't allow something like:

plot for [COL=2:4] $Histo ($0-0.3):COL:xtic(1) index 0, \
     for [COL=2:4]     '' ($0+0.3):COL:xtic(1) index 1

This will result in an error: Too many columns in using specification

I know that in principle I could draw the bar chart from "scratch" with the plotting style with boxxyerror, but why making things complicated if I maybe just overlooked a simple option?

Script:

### how to create a rowstacked AND clustered bar chart from a flat list
reset session

$Data <<EOD
# Mark  Group    Test    Value
  A     Group1   Test1   23
  B     Group1   Test1   12
  C     Group1   Test1   14
  A     Group2   Test1   23
  B     Group2   Test1    9
  C     Group2   Test1    7
  A     Group1   Test2   11
  B     Group1   Test2   16
  C     Group1   Test2   19
  A     Group2   Test2   13
  B     Group2   Test2   24
  C     Group2   Test2    5
  A     Group1   Test3    4
  B     Group1   Test3   14
  C     Group1   Test3   17
  A     Group2   Test3   15
  B     Group2   Test3    8
  C     Group2   Test3    4
  A     Group1   Test4   10
  B     Group1   Test4   12
  C     Group1   Test4   16
  A     Group2   Test4   19
  B     Group2   Test4   20
  C     Group2   Test4   15
EOD

Marks    = "A B C"
Groups   = "Group1 Group2"
Tests    = "Test1 Test2 Test3 Test4"
Mark(i)  = word(Marks,i)
Group(i) = word(Groups,i)
Test(i)  = word(Tests,i)

myFilter(col1,val1,col2,val2,col3,val3) = (strcol(col1) eq val1) && (strcol(col2) eq val2) && (strcol(col3) eq val3)

set print $Histo
    do for [g=1:words(Groups)] {
        print sprintf("# %s",Group(g))
        print sprintf("X %s Sum",Marks)
        do for [t=1:words(Tests)] {
            Line = Test(t)
            mySum = 0
            do for [m=1:words(Marks)] {
                value = 0
                stats $Data u (myFilter(1,Mark(m),2,Group(g),3,Test(t)) ? value=column(4) : 0) nooutput
                Line = Line.sprintf(" %g",value)
                mySum = mySum + value
            }
            Line = Line.sprintf(" %g",mySum)
            print Line
        }
        print ''; print ''   # two empty lines
    }
set print

print $Histo

set style data histogram
set style histogram clustered rowstacked    # or: rowstacked clustered
set style fill solid 0.5
set yrange [0:]
set format y  "%g%%"
set boxwidth 0.8
set grid y
set key noautotitle out title "Mark"
set offset 0.25,0.25,0,0

plot for [g=1:words(Groups)] for [col=2:words(Marks)+1] $Histo u \
     (column(col)/(column(words(Marks)+2))*100):xtic(1) index g-1 ti columnheader(col)
### end of script

Result:

Datablock $Histo:

# Group1
X A B C Sum
Test1 23 12 14 49
Test2 11 16 19 46
Test3 4 14 17 35
Test4 10 12 16 38


# Group2
X A B C Sum
Test1 23 9 7 39
Test2 13 24 5 42
Test3 15 8 4 27
Test4 19 20 15 54

set style histogram rowstacked clustered

enter image description here

set style histogram clustered rowstacked

enter image description here

Desired output should be something like this:

enter image description here

If I overlooked a simple solution which leads to the last plot, please let me know.

2 Answers

Here's my go at it, using your original flat list format. Script follows, then comments. enter image description here

$Data <<EOD
# Mark  Group    Test    Value
  A     Group1   Test1   23
  B     Group1   Test1   12
  C     Group1   Test1   14
  A     Group2   Test1   23
  B     Group2   Test1    9
  C     Group2   Test1    7
  A     Group1   Test2   11
  B     Group1   Test2   16
  C     Group1   Test2   19
  A     Group2   Test2   13
  B     Group2   Test2   24
  C     Group2   Test2    5
  A     Group1   Test3    4
  B     Group1   Test3   14
  C     Group1   Test3   17
  A     Group2   Test3   15
  B     Group2   Test3    8
  C     Group2   Test3    4
  A     Group1   Test4   10
  B     Group1   Test4   12
  C     Group1   Test4   16
  A     Group2   Test4   19
  B     Group2   Test4   20
  C     Group2   Test4   15
EOD

Marks    = "A B C"
Groups   = "Group1 Group2"
Tests    = "Test1 Test2 Test3 Test4"

set margins 8,4,4,4
set tics nomirror
set border 3
set boxwidth 0.5 absolute
set style fill solid
set style histogram columnstacked title offset 0,-1
set style data histograms
set xtics font ",9"

set xrange [ 0 : 12 ]
set yrange [ 0 : 60 ]

# This part is a hack to make the colors come out right
# There must be a better way??
set for [i=4:9] linetype i lc i-3 

set multiplot

    x0 = 1.0
    do for [test in Tests] {
        plot newhistogram test lt 1 at x0, \
             for [group in Groups] $Data using ((strcol(2) eq group && strcol(3) eq test) ? $4 : NaN) title group

        x0 = x0+3   # advance to next grouping
        unset ytics # so they are written only once
    }

unset multiplot

Comments

My first thought was that a single plot command would be sufficient, since you can nest multiple for loops inside it. But that doesn't work because the newhistogram component breaks the loop structure.

There must be some clean way to restart the color sequence for each bar. I was blanking on how, so I punted by redefining the color sequence to repeat in a cycle of three.

I didn't want to deal with key entries inside the multiplot loop. I would probably add a separate dummy plot with nothing but keyentry elements to compose the key.

I also didn't try to normalize the counts to give percentages. That is a separate issue from that of plot layout.

Updated answer

There was a bug in the treatment of variable colors applied to histograms. It has been fixed in the development version of gnuplot and will be in the 5.4.5 release. The demo below (also in the online collection at histogram_colors) shows how variable color can be used with possibly out-of-order or non-parallel data if a separate category field is present for each data value.

#
# Show how variable color can be used to histogram categorical data even
# when the data sources are not completely parallel.  Each datablock
# contains a column with a category identifier and a column with
# the data value contributing to the histogram.
# However not all categories are present in each block, so they
# are not parallel in that the Nth entry of two input columns may 
# or may not belong to the same category.
# Therefore gnuplot's automatic color assignment would fail.
# Instead we use the category identifier to assign colors.
#
$Set1 << EOD
A 15
B 21
C 12
D 9
G 15
L 12
EOD
$Set2 << EOD
B 14
C 12
D 16
E 8
G 14
H 19
I 20
EOD
$Set3 << EOD
D 13
E 7
F 14
G 11
H 15
I 4
L 11
EOD

unset border
set tics nomirror
unset ytics
set boxwidth 0.8
set key lmargin maxcol 1 invert
set key width 2 offset 2, 0
set key title "  Category  "

# Set up a range of 12 defined linetype colors
set for [i=1:8] linetype 100+i lc i     # copy the default 8 colors
set linetype 109 lc "gray50"            # then add a few more
set linetype 110 lc "white"
set linetype 111 lc "orchid"
set linetype 112 lc "seagreen"

alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
color(category) = 100 + strstrt(alphabet, category)

set style data histogram
set style histogram columns
set style fill solid border lc "black"

set title "Column-stacked histogram colored by data category"

plot for [data in "$Set1 $Set2 $Set3"] \
     data using 2:(color(strcol(1))) lc variable title data, \
     for [i=1:12] keyentry with boxes lc color(alphabet[i:i]) title alphabet[i:i]

enter image description here

Here is a script which does what I wanted: a stacked and clustered bar chart from a flat list. It uses triple for loops, a filter and the plotting style with boxxyerror.

In order to easily change the grouping and focus, you need to define the following: The items from the flat list in strings preceded by the column number.

Marks     = "1 A B C"
Groups    = "2 Group1 Group2"
Tests     = "3 Test1 Test2 Test3 Test4"

If you have 3 quantities you can arrange them in 3!=3*2*1=6 different ways. Now, you can easily assign these strings to the items on x-axis level 0 (X0s) and level 1 (X1s) and items for y-axis (Y0s). Furthermore, you define whether you want to normalize the bars or not.

X0s       = Tests     # x-axis group level 0 
X1s       = Groups    # x-axis group level 1
Y0s       = Marks     # y-axis sum up
Normalize = 1         # 0=no, 1=yes

Script:

### create a stacked AND clustered bar chart from a flat list
reset session

$Data <<EOD
# Mark  Group    Test    Value
  A     Group1   Test1   23
  B     Group1   Test1   12
  C     Group1   Test1   14
  A     Group2   Test1   23
  B     Group2   Test1    9
  C     Group2   Test1    7
  A     Group1   Test2   11
  B     Group1   Test2   16
  C     Group1   Test2   19
  A     Group2   Test2   13
  B     Group2   Test2   24
  C     Group2   Test2    5
  A     Group1   Test3    4
  B     Group1   Test3   14
  C     Group1   Test3   17
  A     Group2   Test3   15
  B     Group2   Test3    8
  C     Group2   Test3    4
  A     Group1   Test4   10
  B     Group1   Test4   12
  C     Group1   Test4   16
  A     Group2   Test4   19
  B     Group2   Test4   20
  C     Group2   Test4   15
EOD

Marks     = "1 A B C"
Groups    = "2 Group1 Group2"
Tests     = "3 Test1 Test2 Test3 Test4"
colV      = 4         # column for values
X0s       = Tests     # x-axis items level 0 
X1s       = Groups    # x-axis items level 1
Y0s       = Marks     # y-axis sum up
Normalize = 1         # 0=no, 1=yes
myColors  = "0xff0000 0x00ff00 0x0000ff 0xff00ff 0xffff00 0x00ffff"

# everthing goes automatic from here on
colX0      = int(word(X0s,1))
colX1      = int(word(X1s,1))
colY0      = int(word(Y0s,1))
X0(i)      = word(X0s,i)
X1(i)      = word(X1s,i)
Y0(i)      = word(Y0s,i)
x0N        = words(X0s)
x1N        = words(X1s)
y0N        = words(Y0s)
myColor(i) = int(word(myColors,i))
myBoxwidth = 0.8
myGap      = 1.0

myFilter(col1,val1,col2,val2,col3,val3) = (strcol(col1) eq val1) && \
                (strcol(col2) eq val2) && (strcol(col3) eq val3)

# create bar chart table
set print $BarChart
    do for [x1i=2:x1N] {
        print sprintf("# %s",X1(x1i))
        print sprintf("Col%s Sum",Y0s)
        do for [x0i=2:x0N] {
            Line = X0(x0i)
            mySum = 0
            do for [y0i=2:y0N] {
                value = 0
                stats $Data u (myFilter(colY0,Y0(y0i),colX0,X0(x0i),colX1,X1(x1i)) ? \
                    value=column(colV) : 0) nooutput
                Line = Line.sprintf(" %g",value)
                mySum = mySum + value
            }
            Line = Line.sprintf(" %g",mySum)
            print Line
        }
        print ''; print ''   # two empty lines
    }
set print
# print $BarChart

set xrange[-1:(x0N-2)*(x1N-1+myGap)+x1N-1]
set format x "\n"
set xtics nomirror
set yrange [0:]
if (Normalize) { set yrange [0:105] }
set format y  (Normalize ? "%g%%" : "%g")
set grid y
set key noautotitle out
set style fill solid 0.5
myX0Pos(i)   = column(0)*(x1N-1+myGap) + (x1N-2)/2. + 1e-6
myX0tic(col) = sprintf("\n%s", X0(int(column(col)+2)))
myTitle(col) = sprintf("%s", x0i==1 && x1i==1 ? Y0(col) : '')

plot for [x0i=1:x0N-1] for [x1i=1:x1N-1] for [y0i=1:y0N-1] $BarChart u \
       ((x0i-1)*(x1N-1+myGap)+x1i-1): \
       (vt=column(y0N+1), v0=column(y0i+1), v1=((sum [i=2:y0i] column(i))+v0/2.)/(Normalize?vt/100:1)): \
       (myBoxwidth/2.):(v0/2./(Normalize?vt/100:1)):xtic(X1(x1i+1)) index x1i-1 every ::x0i::x0i w boxxy \
       lc rgb myColor(y0i) ti myTitle(y0i+1), \
     '+' u (myX0Pos(0)):(NaN):xtic(myX0tic(0)) every ::::x0N-2 w p
### end of script

Result: (all possible 6 permutations merged into one image: left column unnormalized, right column normalized)

enter image description here

Related