9

I am trying to create a grid with colored squares, multi-line labels within the squares, and labels on the axes. Is there a simple and easy way to achieve this?

This question is the closest I could find to the answer, but I'm not sure how to add the second line of text in each box, or whether there is an easy way to add the rotated y-axis label (or the other labels for that matter). For this reason I am wondering whether a graph might be more suited.

This question is more along the lines of "is this easily possible and with what tool" rather than "what is the exact code to achieve this" (although that would be great as well). If the answer is "it's not easily possible" then that is fine, and I will look at alternative options.

The following image depicts what I am trying to produce:

enter image description here

5
  • Could you please add a fully compilable example of your efforts, if any?
    – Malipivo
    Commented Apr 21, 2014 at 7:56
  • @malipivo - I considered adding code to my question, but my efforts so far have been minimal and consisted mainly of using tabular and its variants (and none have included labelling the axes). I can post these, but my question is more along the lines of "is this easily possible" and "what is the best tool", rather than "how can I update my existing code". If the answer is that there is no easy and quick way, then I might simply produce the diagram in other software (as I have done above) and include it as an image in my document. Thanks.
    – abeverley
    Commented Apr 21, 2014 at 8:25
  • This is a potential duplicate of tex.stackexchange.com/questions/44868/… and/or tex.stackexchange.com/questions/96403/… Commented Apr 21, 2014 at 12:11
  • also related: tex.stackexchange.com/questions/73640/… Commented Apr 21, 2014 at 12:19
  • Wow, what a great selection of answers. It certainly confirmed my suspicion that there is more than one way to achieve the result! I've accepted the answer that I have actually used for now (it was a tough decision), but in the longer term I'd like to go through all of these in more detail. I have no need for it to be repeatable at the moment, but it would be nice to know that I can easily slot one of these into a document in the future. I haven't used LuaLaTeX before (I've been using XeLaTeX) but will certainly investigate (I didn't know that it had the same font flexibility). Thanks.
    – abeverley
    Commented Apr 22, 2014 at 9:42

7 Answers 7

8

Here, I do it with stacks. You can change the length of the box-edge to suit via the length \sqsz.

REVISED so that the color and category label are selected based on the box value (yellow-low <2, orange-med 3-7, red high >7)

\documentclass{article}
\usepackage[usestackEOL]{stackengine}
\usepackage{xcolor, graphicx}
\newlength\sqsz
\sqsz=1.4cm
\newcommand\sq[1]{\protect\sqhelper{#1}}
\newcommand\sqhelper[1]{\sffamily\bfseries\fboxsep=-\fboxrule%
  \ifnum#1>7\def\sqcolor{red}\def\sqcat{High}\else%
    \ifnum#1>2\def\sqcolor{orange}\def\sqcat{Med}\else%
      \def\sqcolor{yellow}\def\sqcat{Low}\fi\fi%
  \fcolorbox{black}{\sqcolor}{\makebox[\sqsz]{%
  \rule[\dimexpr-.5\sqsz+.2\ht\strutbox]{0pt}{\sqsz}\stackanchor[2pt]{#1}{(\sqcat)}}}}
\newcommand\hsq[1]{\protect\hsqhelper{#1}}
\newcommand\hsqhelper[1]{\sffamily\bfseries%
  \makebox[\sqsz]{\rule{0pt}{\dimexpr.5\sqsz+.2\ht\strutbox}#1}}
\newcommand\vsq[1]{\protect\vsqhelper{#1}}
\newcommand\vsqhelper[1]{\sffamily\bfseries%
  \rule[\dimexpr-.5\sqsz+.2\ht\strutbox]{0pt}{\sqsz}#1}
\begin{document}
\raisebox{.5\sqsz}{\rotatebox{-90}{\sffamily\bfseries\makebox[4\sqsz]{Impact of Event}}}~
\setstackgap{S}{0pt}
\Shortunderstack{\vsq{4}\\ \vsq{3}\\ \vsq{2}\\ \vsq{1}}~~
\stackunder[9pt]{%
  \Shortunderstack{
    \sq{4}\sq{8}\sq{12}\sq{16}\\
    \sq{3}\sq{6}\sq{9}\sq{12}\\
    \sq{2}\sq{4}\sq{6}\sq{8}\\
    \sq{1}\sq{2}\sq{3}\sq{4}\\
    \hsq{1}\hsq{2}\hsq{3}\hsq{4}
  }
}{\sffamily\bfseries Probability of event}
\end{document}

enter image description here

15

I think you'll be better off reproducing your image with PGF/TikZ and LuaLaTeX instead of tabular and tables.

Here is my solution:

I would recommend to use LuaLaTeX since you can program things like these and reuse the code to draw other matrices using the same Lua function (you would have to change the input matrix data, and only call print_data with that matrix as its argument).

This may not be the easiest solution but I would stick with this for the previously mentioned reasons.

\documentclass[a4paper]{article}

\usepackage[svgnames]{xcolor}
\usepackage{tikz}
\usepackage{luacode}

\usetikzlibrary{positioning}

\tikzset{%
    cell/.style={%
        font=\bfseries\sffamily\scriptsize,
        draw,
        minimum size=1cm,%
        inner sep=0pt,%
        outer sep=0pt,%
        align=center%
    },%
    low/.style={%
        fill=Gold!50!Yellow%
    },%
    med/.style={%
        fill=DarkOrange%
    },%
    high/.style={%
        fill=Crimson!40!Red%
    },%
    count/.style={%
        minimum size=1cm,%
        inner sep=0pt,%
        outer sep=0pt,%
        align=center%
    },%
    bottomlabel/.style={%
        anchor=north,%
    },%
    leftlabel/.style={%
        anchor=north,%
        rotate=270%
    }
}

\begin{luacode*}
    data = {{4, 8, 12, 16},
        {3, 6, 9, 12},
        {2, 4, 6, 8},
        {1, 2, 3, 4}}

    function print_data(data)
        local cols = #data[1]
        local rows = #data

        tex.sprint([[\begin{tikzpicture}[font=\sffamily] ]])
        tex.sprint([[\begin{scope}[local bounding box=graph] ]])

        for j = 1, cols do
            for i = 1, rows do
                if data[i][j] >= 1 and data[i][j] <= 2 then
                    tex.sprint([[\node[cell, low] at (]])
                elseif data[i][j] >= 3 and data[i][j] <= 6 then
                    tex.sprint([[\node[cell, med] at (]])
                else
                    tex.sprint([[\node[cell, high] at (]])
                end
                tex.sprint(j - 1)
                tex.sprint([[cm,]])
                tex.sprint(-i + 1)
                tex.sprint([[cm){]])

                if data[i][j] >= 1 and data[i][j] <= 2 then
                    tex.sprint(data[i][j])
                    tex.sprint([[\\(Low)]])
                elseif data[i][j] >= 3 and data[i][j] <= 6 then
                    tex.sprint(data[i][j])
                    tex.sprint([[\\(Med)]])
                else
                    tex.sprint(data[i][j])
                    tex.sprint([[\\(High)]])
                end
                tex.sprint([[};]])
            end
        end

        tex.sprint([[\end{scope}]])

        for i = rows, 1, -1 do
            tex.sprint([[\node[count] at (]])
            tex.sprint(-1)
            tex.sprint([[cm,]])
            tex.sprint(-i + 1)
            tex.sprint([[cm){]])
            tex.sprint(rows - i + 1)
            tex.sprint([[};]])
        end

        for j = cols, 1, -1 do
            tex.sprint([[\node[count] at (]])
            tex.sprint(-j + cols)
            tex.sprint([[cm,]])
            tex.sprint(-cols)
            tex.sprint([[cm){]])
            tex.sprint(cols - j + 1)
            tex.sprint([[};]])
        end

        tex.sprint([[\node[below=of graph, bottomlabel] {Probability of event};]])
        tex.sprint([[\node[left=of graph, leftlabel] {Impact of event};]])
        tex.sprint([[\end{tikzpicture}]])
    end
\end{luacode*}

\begin{document}
\luadirect{print_data(data)}
\end{document}
1
  • Well, this seems to be the most popular ;-) I've used the other solution for the time being as it was the most succinct, but I like the way this creates the print_data function, so I'll look into it in the longer term. See also my comment to the question. Thanks.
    – abeverley
    Commented Apr 22, 2014 at 9:45
10

Doesn't generalize that well, but that didn't seem to be a requirement:

\documentclass[tikz,border=5]{standalone}
\renewcommand\familydefault\sfdefault
\tikzset{%
  box/.style={
    align=center, minimum size=1cm, inner sep=0pt, font=\tiny\bfseries,
    draw=black, #1
  },
  Low/.style={fill=yellow},
  Med/.style={fill=orange},
  High/.style={fill=red},
}
\begin{document}
\begin{tikzpicture}
\foreach \i in {1,...,4}
   \foreach \j [evaluate={\k=int(\j*\i); 
     \s=\k<3 ? "Low" : (\k>6 ? "High" : "Med");}] in {1,...,4}{
       \node at (\j, \i) [box=\s]  {\k \\ (\s)};
     \ifnum\i=1 \node at (\j, 0) {\j}; \fi
     \ifnum\j=1 \node at (0, \i) {\i}; \fi
   }
\node [rotate=-90] at (-.75,2.5) {Impact of Event};
\node              at (2.5,-.75) {Probability of Event};
\end{tikzpicture}
\end{document}

enter image description here

1
  • Nice and compact! Commented Apr 21, 2014 at 22:55
9

Here's one LaTeX possibility producing the coloring and the labeling (Low, Med, High) automatically, depending on the numeric value of the entries. The collcell package was the main tool here:

\documentclass{article}
\usepackage[table]{xcolor}
\usepackage{collcell}
\usepackage{hhline}
\usepackage{graphicx}
\usepackage{pgf}

\newcommand\mycolor{blue}
\newcommand\mylabel{a}
\def\Side{1cm}

\newcommand\ColCell[1]{%
  \pgfmathparse{#1<3?1:0}%
  \ifnum\pgfmathresult=0\relax
    \gdef\mycolor{orange!80}
    \gdef\mylabel{Med}
  \else
  \ifnum\pgfmathresult=1\relax
    \gdef\mycolor{yellow}
    \gdef\mylabel{Low}
  \fi \fi 
  \pgfmathparse{#1>6?1:0}%
  \ifnum\pgfmathresult=1\relax
    \gdef\mycolor{red!95!black}      
    \gdef\mylabel{High}
  \fi
  \expandafter\expandafter\cellcolor{\mycolor}%
    \parbox[c][\dimexpr\Side+1.5\tabcolsep\relax][c]{\Side}{%
      \sffamily\centering #1 \\ (\mylabel)
  }%
}

\newcolumntype{E}{>{\centering\arraybackslash\collectcell\ColCell}p{\Side}<{\endcollectcell}}

\begin{document}

\noindent\begin{tabular}{ll*{4}{|E}|}
 \hhline{~~|*4{-}|}
 \raisebox{-1.8cm}[0pt][0pt]{\rotatebox[origin=c]{-90}{%
   \makebox[0pt]{\sffamily Impact of event}}} 
   & 4 & 4 & 8 & 12 & 16 \\
 \hhline{~~|*4{-}|}
 & 3 & 3 & 6 & 9 & 12 \\
 \hhline{~~|*4{-}|}
 & 2 & 2 & 4 & 6 & 8 \\
 \hhline{~~|*4{-}|}
 & 1 & 1 & 2 & 3 & 4 \\
 \hhline{~~|*4{-}|} \\[-1ex]
 & \multicolumn{1}{c}{} & \multicolumn{1}{c}{1} & \multicolumn{1}{c}{2} 
   & \multicolumn{1}{c}{3} & \multicolumn{1}{c}{4} \\
 & \multicolumn{1}{c}{} & \multicolumn{4}{c}{\sffamily Probability of event}  
\end{tabular}

\end{document}

enter image description here

8

I'll explain how to do this with ConTeXt + Lua. To keep the answer simple, I'll start by not considering the the "labels" of the rows and columns. I think that a good interface for this type of table is to be able to type:

\ProbabilityTable
  { { 4, 8, 12, 16 },
    { 3, 6,  9, 12 },
    { 2, 4,  6,  8 },
    { 1, 2,  3,  4 },
  }

and get the desired result. So, lets start with the bare bones implementation so that this interface works. Since there is some flow-control and logic involved, I'll move all the calculations to Lua. So, at the TeX end, we simply need:

\unexpanded\def\ProbabilityTable#1%
    {\ctxcommand{probability_table({#1})}}

where probability_table (actually commands.probability_table) is a Lua function that takes a 2-D Lua table and typesets it. The Lua part of the code is straight forward, so there are only a few comments in the code. Basically, we iterate over the lua-table, and then typeset it as a ConTeXt table.

\startluacode
  -- Function that returns an index (low|medium|high) for an integer
  local index = function(n) 
    local value = ""
    if n <= 2 then
      value = "low"
    elseif n <= 7 then
      value = "medium"
    else
      value = "high"
    end
    return value
  end

  -- Map values to colors
  local colors = {
    ["low"]    = "yellow",
    ["medium"] = "orange",
    ["high"]   = "red",
  }

  -- Map values to labels
  local labels = {
    ["low"]    = "(Low)",
    ["medium"] = "(Med)",
    ["high"]   = "(High)",
  }


  -- Print the probability table
  probability_table = function(values)
    -- Assume that values is a 2-D table:
    context.bTABLE({"setups=probability:table"})
    for row = 1,#values do
      line = values[row]

      context.bTR()
      for col = 1, #line do
        value = line[col]
        idx   = index(value)

        options = { string.format("backgroundcolor=%s", colors[idx]) }
        context.bTD(options)
        context("%s \\crlf %s",  value, labels[idx])
        context.eTD()

      end
      context.eTR()
    end
    context.eTABLE()
  end

  -- Create a public interface
  commands.probability_table = probability_table;

\stopluacode

Combining all this gives:

enter image description here

Now that the basic interface is working, we can take care of the formatting. For ConTeXt tables, this can be done by specifying a setup and adding an interface to it (which is already done by "{setups=probability:table}" line in the lua code).

So, to get colors, add:

\startsetups probability:table
  \setupTABLE[background=color]
\stopsetups

which gives:

enter image description here

Finally, to fix the alignment and formatting, add:

\startsetups probability:table
  \setupTABLE[background=color]
  \setupTABLE[align=middle, style=\ssbf, offset=0.5em]
\stopsetups

which gives

enter image description here

Adding row and column labels is left as a simple exercise for the reader :-). The labels are simply a new row and column with slightly different formatting.

The complete ConTeXt code is:

\startluacode
  -- Function that returns an index (low|medium|high) for an integer
  local index = function(n) 
    local value = ""
    if n <= 2 then
      value = "low"
    elseif n <= 7 then
      value = "medium"
    else
      value = "high"
    end
    return value
  end

  -- Map values to colors
  local colors = {
    ["low"]    = "yellow",
    ["medium"] = "orange",
    ["high"]   = "red",
  }

  -- Map values to labels
  local labels = {
    ["low"]    = "(Low)",
    ["medium"] = "(Med)",
    ["high"]   = "(High)",
  }


  -- Print the probability table
  probability_table = function(values)
    -- Assume that values is a 2-D table:
    context.bTABLE({"setups=probability:table"})
    for row = 1,#values do
      line = values[row]

      context.bTR()
      for col = 1, #line do
        value = line[col]
        idx   = index(value)

        options = { string.format("backgroundcolor=%s", colors[idx]) }
        context.bTD(options)
        context("%s \\crlf %s",  value, labels[idx])
        context.eTD()

      end
      context.eTR()
    end
    context.eTABLE()
  end

  -- Create a public interface
  commands.probability_table = probability_table;

\stopluacode

\unexpanded\def\ProbabilityTable#1%
    {\ctxcommand{probability_table({#1})}}

\startsetups probability:table
  \setupTABLE[background=color]
  \setupTABLE[align=middle, style=\ssbf, offset=0.5em]
\stopsetups

\starttext
\ProbabilityTable
  { { 4, 8, 12, 16 },
    { 3, 6,  9, 12 },
    { 2, 4,  6,  8 },
    { 1, 2,  3,  4 },
  }
\stoptext

Note that:

  1. There is a nice separation between content and presentation. You can easily change the formatting by changing the probability:table setup.

  2. It is easy to change how the result is shown by changing the labels and colors tables. This can be made configurable at the TeX end as well.

  3. I find it easier to program in Lua rather than in TeX.

  4. ConTeXt provides a nice lua interface for the TeX macros. So, you can use context.bTABLE() rather than tex.print("\\bTABLE"), etc.

EDIT: Heiko Oberdiek mentioned that the cell background should not completely fill the cell. To achieve that in ConTeXt, add backgroundoffset=<dimen> to the table settings. For example, with:

\startsetups probability:table
  \setupTABLE[background=color, backgroundoffset=-1pt]
  \setupTABLE[align=middle, style=\ssbf, offset=0.5em]
\stopsetups

you get:

enter image description here

3
  • 1
    Excellent separation of content and presentation; thank for the detailed code. Commented Apr 21, 2014 at 22:54
  • I'm not familiar with ConTeXt, but is the "ConTeXt table" used here similar to a LaTeX table? Commented Jul 22, 2022 at 22:02
  • @FaheemMitha I am not sure what you mean. There are various table mechanisms in latex, and the code here could be translated to latex easily (by someone who knows the lua interface to latex well, ie not me :-)
    – Aditya
    Commented Jul 23, 2022 at 7:03
7

Here is my try with MetaPost and LuaLaTeX. I hope I've correctly understood the underlying logic of the picture. You can change the size of the squares with the u parameter, and their numbers with the n parameter.

I think this kind of picture can be better done with whatever drawing package devised for LaTeX (tikz, pstricks, mfpic, MetaPost, Asymptote and so on), provided that you have a reasonable knowledge of this package. This way it is easier to express the underlying mathematical logic of the picture in the program that draws it.

\documentclass{article}
\usepackage{unicode-math}
\usepackage{luamplib}
  \mplibtextextlabel{enable}
\begin{document}
\begin{mplibcode}
  input mpcolornames;
  u = 1.25cm; n = 4; path mysquare;
  vardef fill_square(expr i, j, str, clr) =
    save mysquare; path mysquare;
    mysquare = ((i-1, j-1) -- (i, j-1) -- (i, j) -- (i-1, j) -- cycle) scaled u;
    fill mysquare withcolor clr;
    label("\begin{tabular}{c}$" & decimal(i*j) & "$\\" & str & "\end{tabular}", 
      u*(i-.5, j-.5));
  enddef;
  beginfig(1);
    for i = 1 upto n:
      for j = 1 upto n:
        if (i+j = n) or (i+j = n+1): fill_square(i, j, "(Mid)", Orange);
        elseif i+j < n: fill_square(i, j, "(Low)", Yellow);
        else: fill_square(i, j, "(High)", red);
        fi;
      endfor;
    endfor;
    for i = 1 upto n:
      label.bot("$" & decimal i & "$", u*(i-.5, 0));
      label.lft("$" & decimal i & "$", u*(0, i-.5));
    endfor;
    labeloffset := 6*labeloffset;
    label.bot(btex Probability of event etex, u*(n/2, 0));
    label.lft(btex Impact of event etex rotatedaround (u*(0, n/2), -90), u*(0, n/2));
    for i = 0 upto n:
      draw (i*u, 0) -- u*(i, n);
      draw (0, i*u) -- u*(n, i);
    endfor;
  endfig;
\end{mplibcode}
\end{document}

enter image description here

2
  • You can avoid using tabluar environment by using label.top(decimal i*j, p) and label.bot("High)", p), etc.
    – Aditya
    Commented Apr 21, 2014 at 18:15
  • @Aditya True, but that would not work so easily if there were more than two lines. That's the reason why I chose a more general way. Commented Apr 21, 2014 at 18:17
5

The grid can also be drawn with the "good old" picture environment.

  • So far it is the only answer, that respects that the cell color does not fill the cell entirely; it leaves a tiny space to the surrounding black lines. Probably the purpose of the space is the increase of the contrast for the lines.

  • The rotated label at the left is done by \rotatebox of the graphicx package.

  • Package multido helps with \multido for setting the cells. Also it calculates the values in the cells. And the cell value is used to select the color and text via macro \ClassifyValue.

\documentclass{article}

\usepackage{graphicx}
\usepackage{color}
\usepackage{multido}

\begin{document}
\setlength{\unitlength}{1cm}
\definecolor{med}{rgb}{1,.4,0}
\newcommand*{\ClassifyValue}[4]{%
  \ifnum#1<3 %
    #2%
  \else
    \ifnum#1<7 %
      #3%
    \else
      #4%
    \fi
  \fi
}
\begin{picture}(5,5)(-1,-1)
  \small
  \sffamily
  \multido{\ni=1+1,\ny=0+1}{4}{%
    \multido{\nv=\ni+\ni,\nx=0+1}{4}{%
      \put(\nx,\ny){%
        \makebox(1,1){%
          \textcolor{\ClassifyValue{\nv}{yellow}{med}{red}}{%
            \addtolength{\unitlength}{-.8pt}% local change
            \rule{\unitlength}{\unitlength}%
          }%
        }%
      }%
      \put(\nx,\ny){%
        \makebox(1,1){%
          \scriptsize
          \bfseries
          \begin{tabular}{@{}c@{}}%
            \nv\\
            (\ClassifyValue{\nv}{Low}{Med}{High})%
          \end{tabular}%
        }%
      }%
    }%
  }%
  \multiput(0,0)(0,1){5}{\line(1,0){4}}%
  \multiput(0,0)(1,0){5}{\line(0,1){4}}%
  \multido{\nx=0+1,\nv=1+1}{4}{%
    \put(\nx,-.35){\makebox(1,0){\nv}}%
  }%
  \put(2,-1){\makebox(0,0)[b]{Probability of event}}%
  \multido{\ny=0+1,\nv=1+1}{4}{%
    \put(-.35,\ny){\makebox(0,1){\nv}}%
  }%
  \put(-1,2){\makebox(0,0)[l]{\rotatebox{270}{Impact of event}}}%
\end{picture}%
\end{document}

Result

1
  • I edited my answer so that the background color does not cover the complete cell.
    – Aditya
    Commented Apr 22, 2014 at 21:39

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .