Take the 2-minute tour ×
TeX - LaTeX Stack Exchange is a question and answer site for users of TeX, LaTeX, ConTeXt, and related typesetting systems. It's 100% free, no registration required.

Is there a (simplistic?) way to draw a 3D elevation grid like this in Tikz or PGFPlots?

3D Elevation Grid

The blue line should be able to move up or down and, eventually, submerge the interior blocks.

I've looked at this example, and follow it well enough that I could probably reconstruct things on my own, but it seems like at least some part of this may already exist as a package.

I would also be willing to use packages aside from TikZ or PGFPlots.

share|improve this question
2  
Does this come close to what you want? tex.stackexchange.com/questions/52987/… –  percusse Mar 23 '13 at 0:04
    
Yes, that's looking close, @percusse. Now, if the tops or entirety of the histogram bars could be individually coloured... –  Richard Mar 23 '13 at 5:32

3 Answers 3

With PSTricks.

enter image description here

\documentclass[pstricks,border=12pt]{standalone}
\usepackage{pst-eucl}

\definecolor{myblue}{RGB}{37,111,197}
\definecolor{mybrown}{RGB}{211,200,134}

\def\rOne{0.75}
\def\rTwo{0.75}
\def\tOne{30}
\def\tTwo{-5}
\def\stripH{0.5}

\psset
{
    PointName=none,
    PointSymbol=none,
    fillstyle=solid,
    linejoin=1,
}

\def\Bar(#1,#2)#3{%
    \rput(!#2 \rTwo\space mul \tTwo\space PtoC){%
        \rput(!#1 \rOne\space mul \tOne\space 180 add PtoC){%
            \pstGeonode{O}(\rOne;\tOne){One}(\rTwo;\tTwo){Two}
            \pstTranslation{O}{Two}{One}[Three]
            \pnode(0,\stripH){O'}
            \pstTranslation{O}{O'}{One,Two,Three}
            \pnode(0,#3){O''}
            \pstTranslation{O'}{O''}{One',Two',Three'}
            \psset{fillcolor=myblue}
                \pspolygon(O')(O)(Two)(Two')
                \pspolygon(Two')(Two)(Three)(Three')
            \psset{fillcolor=mybrown}
                \pspolygon(O'')(O')(Two')(Two'')
                \pspolygon(Two'')(Two')(Three')(Three'')
                \pspolygon(One'')(O'')(Two'')(Three'')
        }%
    }%
}

\newcount\OuterIndex
\def\SaveListContents#1\relax{\def\Contents{#1}}

\def\Picture{%
\begin{pspicture}[showgrid=false](-3,-2)(5,4)
    \psforeach{\row}{%
        {{3,2.8,2.7,3,3.1}},% <=== Only this row must use double curly braces. It is a feature!
        {2.8,1,1.2,2,3},%
        {2.8,1,1.2,2,2.8},%
        {2.6,1.6,1.8,1.9,1.8},%
        {2.4,1.5,1.7,1.9,1.5}%
    }{%
        \expandafter\SaveListContents\row\relax
        \OuterIndex=\psLoopIndex\relax
        \psforeach{\col}{\Contents}{\Bar(\the\OuterIndex,\the\psLoopIndex){\col}}%
        \psLoopIndex=\OuterIndex\relax
    }
\end{pspicture}}

\begin{document}
\Picture
\end{document}

Animation 1

enter image description here

\documentclass[pstricks,border=12pt]{standalone}
\usepackage{pst-eucl}

\definecolor{myblue}{RGB}{37,111,197}
\definecolor{mybrown}{RGB}{211,200,134}

\def\rOne{0.75}
\def\rTwo{0.75}
\def\tOne{30}
\def\tTwo{-15}
\def\stripH{0.5}

\psset
{
    PointName=none,
    PointSymbol=none,
    fillstyle=solid,
    linejoin=1,
}

\def\RandomBar(#1,#2){%
    \rput(!#2 \rTwo\space mul \tTwo\space PtoC){%
        \rput(!#1 \rOne\space mul \tOne\space 180 add PtoC){%
            \pstGeonode{O}(\rOne;\tOne){One}(\rTwo;\tTwo){Two}
            \pstTranslation{O}{Two}{One}[Three]
            \pnode(0,\stripH){O'}
            \pstTranslation{O}{O'}{One,Two,Three}
            \pnode(!0 rand 201 mod 100 div 1.00 add){O''}
            \pstTranslation{O'}{O''}{One',Two',Three'}
            \psset{fillcolor=myblue}
                \pspolygon(O')(O)(Two)(Two')
                \pspolygon(Two')(Two)(Three)(Three')
            \psset{fillcolor=mybrown}
                \pspolygon(O'')(O')(Two')(Two'')
                \pspolygon(Two'')(Two')(Three')(Three'')
                \pspolygon(One'')(O'')(Two'')(Three'')
        }%
    }%
}

\newcount\OuterIndex
\def\SaveListContents#1\relax{\def\Contents{#1}}

\def\Picture{%
\begin{pspicture}[showgrid=false](-2.5,-3)(4.25,2.5)
    \psforeach{\row}{1,2,..,5}{\psforeach{\col}{1,2,..,5}{\RandomBar(\row,\col)}}
\end{pspicture}}

\begin{document}
\psLoop{20}{\Picture}
\end{document}

Animation 2

enter image description here

\documentclass[pstricks,border=12pt]{standalone}
\usepackage{pst-eucl}

\definecolor{myblue}{RGB}{37,111,197}
\definecolor{mybrown}{RGB}{211,200,134}

\def\rOne{0.75}
\def\rTwo{0.75}
\def\tOne{30}
\def\tTwo{-15}
\def\stripH{0.5}

\psset
{
    PointName=none,
    PointSymbol=none,
    fillstyle=solid,
    linejoin=1,
}

\def\RandomBar(#1,#2){%
    \rput(!#2 \rTwo\space mul \tTwo\space PtoC){%
        \rput(!#1 \rOne\space mul \tOne\space 180 add PtoC){%
            \pstGeonode{O}(\rOne;\tOne){One}(\rTwo;\tTwo){Two}
            \pstTranslation{O}{Two}{One}[Three]
            \pnode(0,\stripH){O'}
            \pstTranslation{O}{O'}{One,Two,Three}
            \pnode(!0 rand 201 mod 100 div 1.00 add){O''}
            \pstTranslation{O'}{O''}{One',Two',Three'}
            \psset{fillcolor=myblue}
                \pspolygon(O')(O)(Two)(Two')
                \pspolygon(Two')(Two)(Three)(Three')
            \psset{fillcolor=mybrown}
                \pspolygon(O'')(O')(Two')(Two'')
                \pspolygon(Two'')(Two')(Three')(Three'')
                \pspolygon(One'')(O'')(Two'')(Three'')
        }%
    }%
}

\newcount\OuterIndex
\def\SaveListContents#1\relax{\def\Contents{#1}}

\def\Picture{\psforeach{\row}{1,2,..,5}{\psforeach{\col}{1,2,..,5}{%
\begin{pspicture}[showgrid=false](-2.5,-3)(4.25,2.5)
    \RandomBar(\row,\col)
\end{pspicture}}}}

\begin{document}
\psLoop{1}{\Picture}
\end{document}

Animation 3

enter image description here

\documentclass[pstricks,border=12pt]{standalone}
\usepackage{pst-eucl}
\usepackage[nomessages]{fp}

\definecolor{myblue}{RGB}{37,111,197}
\definecolor{mybrown}{RGB}{211,200,134}

\def\rOne{0.75}
\def\rTwo{0.75}
\def\tOne{20}
\def\tTwo{-45}
\FPset\RowMaxIndex{4}% because zero based index
\FPset\ColMaxIndex{4}% because zero based index


\psset
{
    PointName=none,
    PointSymbol=none,
    fillstyle=solid,
    linejoin=1,
}

\def\Bar(#1,#2)#3{%
    \rput(!#2 \rTwo\space mul \tTwo\space PtoC){%
        \rput(!#1 \rOne\space mul \tOne\space 180 add PtoC){%
            \pstGeonode{O}(\rOne;\tOne){One}(\rTwo;\tTwo){Two}
            \pstTranslation{O}{Two}{One}[Three]
            \pnode(0,\stripH){O'}
            \pstTranslation{O}{O'}{One,Two,Three}
            \pnode(0,#3){O''}
            \pstTranslation{O}{O''}{One,Two,Three}[One'',Two'',Three'']
            \psset{fillcolor=mybrown}
            \pspolygon(O'')(O)(Two)(Two'')
            \pspolygon(Two'')(Two)(Three)(Three'')
            \pspolygon(One'')(O'')(Two'')(Three'')
            \psset{fillcolor=myblue,opacity=0.75,linestyle=none,linewidth=0}
            \FPifeq{#1}{\RowMaxIndex}\pspolygon(O')(O)(Two)(Two')\fi
            \FPifeq{#2}{\ColMaxIndex}\pspolygon(Two')(Two)(Three)(Three')\fi
            \FPiflt{#3}{\stripH}\pspolygon(One')(O')(Two')(Three')\fi
        }%
    }%
}

\newcount\OuterIndex
\def\SaveListContents#1\relax{\def\Contents{#1}}

\def\Picture#1{%
\def\stripH{#1}%
\begin{pspicture}[showgrid=false](-2.5,-3.35)(3.05,3.05)
    \psforeach{\row}{%
        {{3,2.8,2.7,3,3.1}},% <=== Only this row must use double curly braces. It is a feature!
        {2.8,1,1.2,2,3},%
        {2.8,1,1.2,2,2.8},%
        {2.6,1.6,1.8,1.9,1.8},%
        {2.4,1.5,1.7,1.9,1.5}%
    }{%
        \expandafter\SaveListContents\row\relax
        \OuterIndex=\psLoopIndex\relax
        \psforeach{\col}{\Contents}{\Bar(\the\OuterIndex,\the\psLoopIndex){\col}}%
        \psLoopIndex=\OuterIndex\relax
    }
\end{pspicture}}

\begin{document}
\multido{\n=0.0+0.2}{17}{\Picture{\n}}
\end{document}
share|improve this answer
    
Why does your animation look like an earthquake, while the question clearly is more related to flood simulation? –  g.kov Mar 24 '13 at 4:14
    
@g.kov: Because I have not read the question description yet. I am a visual learner and only saw the given diagram. It is my bad! –  stalking is prohibited Mar 24 '13 at 4:43
    
Thank you for this! I'm fine with having an earthquake :-) But I wonder, could you explain how to change the viewing perspective? Or is that hard-coded? –  Richard Mar 24 '13 at 21:34
    
@Richard: you can try modifying \aTwo and \aOne. –  stalking is prohibited Mar 24 '13 at 21:44
1  
@MarcvanDongen: The requested animation has been added to my answer. –  stalking is prohibited Mar 25 '13 at 8:22

My take:

graph

How I did it

Since tikz is not well suited for this kind of graphic (which has to find intersections between planes, decomposing the figure in sub-objects and draw them in the correct order so that the nearest one hide the farthest ones) I used sketch, which can do these things and produce tikz code as result.

Next is the skecth code. The grid is a 5x5x5 cube. The water level is a real between 0 and 5. As for the height of each prism, unfortunately sketch does not provide primitives for reading numbers from files, nor to do list or array manipulation, so I had to draw each bar "one by one", instead of using a loop, and "hardcoded" their height in each case (the second number in the scale operation) :-(

def water_level 2.3 

def prism {
  sweep[fill style=prism]
  { 4<>, rotate(360/4,[0,1,0]) }
  line[fill style=prism](0.5,0,0.5)(0.5,1,0.5)
}

def water {
  sweep[fill style=water]
  { 4<>, rotate(360/4,[0,1,0]) }
  line[fill style=water, style=water](2.52,0,2.52)(2.52, 1, 2.52)
}


def grid {
  put { translate([0, 0, 0]) then scale([1,   5, 1])}{ {prism} }
  put { translate([1, 0, 0]) then scale([1, 4.5, 1])}{ {prism} }
  put { translate([2, 0, 0]) then scale([1, 4.3, 1])}{ {prism} }
  put { translate([3, 0, 0]) then scale([1, 4.8, 1])}{ {prism} }
  put { translate([4, 0, 0]) then scale([1,   5, 1])}{ {prism} }

  put { translate([0, 0, 1]) then scale([1, 4.5, 1])}{ {prism} }
  put { translate([1, 0, 1]) then scale([1,   2, 1])}{ {prism} }
  put { translate([2, 0, 1]) then scale([1, 2.2, 1])}{ {prism} }
  put { translate([3, 0, 1]) then scale([1,   4, 1])}{ {prism} }
  put { translate([4, 0, 1]) then scale([1, 4.9, 1])}{ {prism} }

  put { translate([0, 0, 2]) then scale([1, 4.5, 1])}{ {prism} }
  put { translate([1, 0, 2]) then scale([1,   2, 1])}{ {prism} }
  put { translate([2, 0, 2]) then scale([1, 2.2, 1])}{ {prism} }
  put { translate([3, 0, 2]) then scale([1,   4, 1])}{ {prism} }
  put { translate([4, 0, 2]) then scale([1, 4.5, 1])}{ {prism} }

  put { translate([0, 0, 3]) then scale([1, 4.0, 1])}{ {prism} }
  put { translate([1, 0, 3]) then scale([1, 3.0, 1])}{ {prism} }
  put { translate([2, 0, 3]) then scale([1, 3.2, 1])}{ {prism} }
  put { translate([3, 0, 3]) then scale([1, 3.5, 1])}{ {prism} }
  put { translate([4, 0, 3]) then scale([1, 3.0, 1])}{ {prism} }

  put { translate([0, 0, 4]) then scale([1, 3.5, 1])}{ {prism} }
  put { translate([1, 0, 4]) then scale([1, 2.8, 1])}{ {prism} }
  put { translate([2, 0, 4]) then scale([1, 3.0, 1])}{ {prism} }
  put { translate([3, 0, 4]) then scale([1, 3.5, 1])}{ {prism} }
  put { translate([4, 0, 4]) then scale([1, 2.5, 1])}{ {prism} }

  put { translate([2, 0, 2]) then scale([1, water_level, 1])}{ {water} }
}

put{ view((10,20,20)) }{{grid}}  % Draw it!

global { language tikz }

You save this code in a file named elevation-grid.sketch and compile it with:

$ sketch elevation-grid.sketch > tikzpicture.tex

Then, your main elevation-grid.tex document looks like this:

\documentclass{standalone}
\usepackage{tikz}
\tikzset{
  prism/.style = {draw=black, fill=black!30, opacity=1},
  water/.style = {draw=black, fill=blue, opacity=0.60},
}
\begin{document}
\input{tikzpicture.tex}
\end{document}

Of course this generates the graphic for a paticular water level (2.8 in the example). To generate the animation, you have to repeat the above a lot of times, for different water_level values. In order to automate this process, I removed the water_level definition from the first line of elevation-grid.sketch and wrote this small shell script (it requires convert utility from Imagemagick):

echo "def water_level $1" > aux.sketch
cat elevation-grid.sketch >> aux.sketch
sketch aux.sketch > tikzpicture.tex
pdflatex elevation-grid.tex
convert -density 300 elevation-grid.pdf elevation-grid-$1.png

If you save this code in a file named run, for example, you may call it with:

$ sh run 1.2
$ sh run 1.3
$ sh run 1.4
etc...

and you'll get a set of png files: elevation-grid-1.2.png, elevation-grid-1.3.png, and so on. Of course I also automated the above to generate values between 1.0 and 3.9. When all png figures were finally generated, I converted them to an animated gif with convert again:

$ convert -delay 20 elevation-grid*png animation.gif

Update

Big city

Now the sketch script is generated by a python script. Currently the data for the grid is randomly generated, but it would be trivial to read it from a file. This is the script:

import random

def randomGrid(x,y,z):
    grid = []
    for i in range(y):
        row = []
        for j in range(x):
            row.append(random.random()*z+1)
        grid.append(row)
    return grid

def generateGraphic(grid, water_level):
    code = []
    code.append("def grid {")
    y = 0
    for row in grid:
        x = 0
        for cell in row:
            code.append("  put { translate([%d, 0, %d]) then scale([1,   %f, 1])}{ {prism} }" % \
                    (x, y, cell))
            x = x + 1
        code.append("")
        y = y + 1
    code.append("put { scale([%f, %f, %f]) then  translate([%f, 0, %f])}{ {water} }" % \
            (x+0.01, water_level, y+0.01, x/2.0-0.5, y/2.0-0.5))
    code.append("}")
    return "\n".join(code)

def preamble():
    return """
def prism {
  sweep[fill style=prism]
  { 4<>, rotate(360/4,[0,1,0]) }
  line[fill style=prism](0.5,0,0.5)(0.5,1,0.5)
}

def water {
  sweep[fill style=water]
  { 4<>, rotate(360/4,[0,1,0]) }
  line[fill style=water, style=water](0.5,0,0.5)(0.5, 1, 0.5)
}
    """

def final(camera):
    return """put{ view((%f,%f,%f)) }{{grid}}  %% Draw it!
            global { language tikz }""" % camera

g = randomGrid(20,20,7)  % 20x20 cells, max height =7
print preamble()
print generateGraphic(g, 4.2)  % 4.2 is the water level
print final(camera=(30,60,60)) % 3D position of camera, looking at origin

To be used like this:

$ python generate-sketch.py > elevation-grid.sketch
share|improve this answer
    
Why don't you convert the PDF output to GIF directly (without via PNG)? –  stalking is prohibited Mar 23 '13 at 18:49
    
@Karl'sstudents because I'm not generating a multi-page pdf, but a single pdf for each run. Yes, I could improve that.. but it was only a proof-of-concept. –  JLDiaz Mar 23 '13 at 19:12
    
I see. Thanks you. –  stalking is prohibited Mar 23 '13 at 19:23
    
It is too bad about the lack of loops and arrays, @JLDiaz, because this solution is otherwise so clean. –  Richard Mar 24 '13 at 22:46
    
@Richard Note that the sketch program which generates the figure, can be in turn generated by another program. I updated my answer to include a python script which generates the sketch code. –  JLDiaz Mar 25 '13 at 12:26

An alternative to TikZ or PGFPlots: Asymptote (part of TeXLive2012):

enter image description here

Source file wdem.asy:

size(6cm,0);
int frameNo;
usersetting(); // set frameNo from commandline 
real[][] dem={
{3,4,7,7.5,8},
{5,5,6,6,7.5},
{4.3,4.4,3.9,4.1,6},
{4.7,4.75,2.2,2.3,6.5},
{5,6,6.2,6.2,7.3},
{4.85,7,5.2,6.1,7.2},
};

import bsp;
real u=2.5;
real v=1;
currentprojection=oblique;
path3[] bar={
  plane((1,0,0),(0,1,0),(0,0,0)),
  plane((0,1,0),(0,0,1),(0,0,0)),
  plane((1,0,0),(0,0,1),(0,0,0)),
  plane((-1,0,0),(0,-1,0),(1,1,1)),
  plane((0,-1,0),(0,0,-1),(1,1,1)),
  plane((-1,0,0),(0,0,-1),(1,1,1)),
};  
face[] faces;

int nz=dem[0].length;
int nx=dem.length;

void barPlot(real wl){
  transform3 t;
  real peak;
  for(int i=0;i<nx;++i){ 
    for(int j=0;j<nz;++j){ 
      peak=dem[nx-1-i][nz-1-j];
      if(peak>0){
        t=shift(i,0,j)*scale(1-1e-3,peak,1-1e-3);
        for(int k=0;k<bar.length;++k){ 
          filldraw(faces.push(t*bar[k]),project(t*bar[k]),lightgreen);//+opacity(0.5));
        }
      }
    }
  }
  t=shift(1e-3,0,1e-3)*scale(nx+1e-3,wl,nz+1e-3); 
  for(int k=0;k<bar.length;++k){ 
    filldraw(faces.push(t*bar[k]),project(t*bar[k]),blue+opacity(0.5));
  }
  add(faces);
}

real wlNow;
real wlLow=3.8;
real wlHigh=6.3;
int frames=30; 
assert(frames>1,"At least two frames is a must.");
real dt=1.0/(frames-1);
real t=frameNo*dt;

wlNow=wlLow*(1-t)+wlHigh*t;
barPlot(wlNow);
shipout(format("f%02i",frameNo));

Frames were processed with batch file gen-anim.sh:

#!/bin/bash
frames=30
for((i=0;i<${frames};++i)){
  echo "Frame $i begin"
  asy -noV -tex=none -f pdf -noprc -u frameNo="$i" wdem.asy
  echo "Frame $i end"
}

An animation was created from 30 pdf frames by convert -colors 32 -density 200 -antialias -delay 20 f*pdf fdem0.gif and optimized with gifsicle --colors 16 --resize 236x_ -O3 <fdem0.gif >fdem.gif.

Edit: version with individually coloured bars:

size(6cm,0);
int frameNo;
usersetting(); // set frameNo from commandline 
real[][] dem={
{3,4,7,7.5,8},
{5,5,6,6,7.5},
{4.3,4.4,3.9,4.1,6},
{4.7,4.75,2.2,2.3,6.5},
{5,6,6.2,6.2,7.3},
{4.85,7,5.2,6.1,7.2},
};

int nz=dem[0].length;
int nx=dem.length;

pen[][] demColor=new pen[nx][nz];

srand(56091);
for(int i=0;i<nx;++i){ 
  for(int j=0;j<nz;++j){ 
    demColor[i][j]=rgb(unitrand(),unitrand(),unitrand());
  }
}


import bsp;
real u=2.5;
real v=1;
currentprojection=oblique;
path3[] bar={
  plane((1,0,0),(0,1,0),(0,0,0)),
  plane((0,1,0),(0,0,1),(0,0,0)),
  plane((1,0,0),(0,0,1),(0,0,0)),
  plane((-1,0,0),(0,-1,0),(1,1,1)),
  plane((0,-1,0),(0,0,-1),(1,1,1)),
  plane((-1,0,0),(0,0,-1),(1,1,1)),
};  
face[] faces;


void barPlot(real wl){
  transform3 t;
  real peak;
  for(int i=0;i<nx;++i){ 
    for(int j=0;j<nz;++j){ 
      peak=dem[nx-1-i][nz-1-j];
      if(peak>0){
        t=shift(i,0,j)*scale(1-1e-3,peak,1-1e-3);
        for(int k=0;k<bar.length;++k){ 
          filldraw(faces.push(t*bar[k]),project(t*bar[k]),demColor[nx-1-i][nz-1-j]);//+opacity(0.5));
        }
      }
    }
  }
  t=shift(1e-3,0,1e-3)*scale(nx+1e-3,wl,nz+1e-3); 
  for(int k=0;k<bar.length;++k){ 
    filldraw(faces.push(t*bar[k]),project(t*bar[k]),blue+opacity(0.5));
  }
  add(faces);
}

real wlNow;
real wlLow=3.8;
real wlHigh=6.3;
int frames=30; 
assert(frames>1,"At least two frames is a must.");
real dt=1.0/(frames-1);
real t=frameNo*dt;

wlNow=wlLow*(1-t)+wlHigh*t;
barPlot(wlNow);
shipout(format("f%02i",frameNo));

enter image description here

share|improve this answer
    
Thanks, @g.kov. Sadly, this compiles extremely slowly on my otherwise zippy computer. Any thoughts on how to speed up the process? –  Richard Mar 26 '13 at 4:11

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.