3D histogram with gnuplot or octave
Asked Answered
P

6

11

I would like to draw a 3D histogram (with gnuplot or octave) in order to represent my data. lets say that I have a data file in the following form:

2 3 4    
8 4 10    
5 6 7

I'd like to draw nine colored bars (the size of the matrix), in the set [1,3]x[1,3], such that the bar's color is proportional to the bar's height. How can I do this?

Poignant answered 12/6, 2014 at 9:27 Comment(7)
What about using interp2 and nearest neighbours interpolation to resample your data from 3-by-3 to say 300-by-300 and then just use surf?Donnie
@Donnie why have you added the matlab tag?Ultrasonics
@TomFenech Matlab and Octave share the same syntax and functions and Matlab has a larger SO community so it should increase the chances of getting a good solutionDonnie
@Donnie I didn't realise they were so similar. I guess if it's certain that a valid MATLAB answer will also work on Octave, then that's fair enough.Ultrasonics
@TomFenech It's not certain, but it is extremely likelyDonnie
@TomFenech: The Octave developers consider anything that works in MATLAB but not in Octave a bug, so I'd say the MATLAB tag is perfectly valid.Futch
@Rody fair enough, I didn't realise the two were quite so similar. Seems reasonable then :)Ultrasonics
A
19

Below is a function I implemented that acts as a bar3 replacement (partially).

In my version, the bars are rendered by creating a patch graphics object: we build a matrix of vertex coordinates and a list of faces connecting those vertices.

The idea is to first build a single "3d cube" as a template, then replicate it for as many bars as we have. Each bar is shifted and scaled according to its position and height.

The vertices/faces matrices are constructed in a vectorized manner (look ma, no loops!), and the result is a single patch object drawn for all bars, as opposed to multiple patches one per bar (this is more efficient in terms of graphics performance).

The function could have been implemented by specifying coordinates of connected vertices that form polygons, by using the XData, YData, ZData and CData properties instead of the Vertices and Faces properties. In fact this is what bar3 internally does. Such approach usually requires larger data to define the patches (because we cant have shared points across patch faces, although I didn't care much about that in my implementation). Here is a related post where I tried to explain the structure of the data constructed by bar3.

my_bar3.m

function pp = my_bar3(M, width)
    % MY_BAR3  3D bar graph.
    %
    % M     - 2D matrix
    % width - bar width (1 means no separation between bars)
    %
    % See also: bar3, hist3

    %% construct patch
    if nargin < 2, width = 0.8; end
    assert(ismatrix(M), 'Matrix expected.')
    
    % size of matrix
    [ny,nx] = size(M);

    % first we build a "template" column-bar (8 vertices and 6 faces)
    % (bar is initially centered at position (1,1) with width=? and height=1)
    hw = width / 2;    % half width
    [X,Y,Z] = ndgrid([1-hw 1+hw], [1-hw 1+hw], [0 1]);
    v = [X(:) Y(:) Z(:)];
    f = [
        1 2 4 3 ; % bottom
        5 6 8 7 ; % top
        1 2 6 5 ; % front
        3 4 8 7 ; % back
        1 5 7 3 ; % left
        2 6 8 4   % right
    ];

    % replicate vertices of "template" to form nx*ny bars
    [offsetX,offsetY] = meshgrid(0:nx-1,0:ny-1);
    offset = [offsetX(:) offsetY(:)]; offset(:,3) = 0;
    v = bsxfun(@plus, v, permute(offset,[3 2 1]));
    v = reshape(permute(v,[2 1 3]), 3,[]).';

    % adjust bar heights to be equal to matrix values
    v(:,3) = v(:,3) .* kron(M(:), ones(8,1));

    % replicate faces of "template" to form nx*ny bars
    increments = 0:8:8*(nx*ny-1);
    f = bsxfun(@plus, f, permute(increments,[1 3 2]));
    f = reshape(permute(f,[2 1 3]), 4,[]).';

    %% plot
    % prepare plot
    if exist('OCTAVE_VERSION','builtin') > 0
        % If running Octave, select OpenGL backend, gnuplot wont work
        graphics_toolkit('fltk');
        hax = gca;
    else
        hax = newplot();
        set(ancestor(hax,'figure'), 'Renderer','opengl')
    end


    % draw patch specified by faces/vertices
    % (we use a solid color for all faces)
    p = patch('Faces',f, 'Vertices',v, ...
        'FaceColor',[0.75 0.85 0.95], 'EdgeColor','k', 'Parent',hax);
    view(hax,3); grid(hax,'on');
    set(hax, 'XTick',1:nx, 'YTick',1:ny, 'Box','off', 'YDir','reverse', ...
        'PlotBoxAspectRatio',[1 1 (sqrt(5)-1)/2]) % 1/GR (GR: golden ratio)

    % return handle to patch object if requested
    if nargout > 0
        pp = p;
    end
end

Here is an example to compare it against the builtin bar3 function in MATLAB:

subplot(121), bar3(magic(7)), axis tight
subplot(122), my_bar3(magic(7)), axis tight

comparison_bar3

Note that I chose to color all the bars in a single solid color (similar to the output of the hist3 function), while MATLAB emphasizes the columns of the matrix with matching colors.

It is easy to customize the patch though; Here is an example to match bar3 coloring mode by using indexed color mapping (scaled):

M = membrane(1); M = M(1:3:end,1:3:end);
h = my_bar3(M, 1.0);

% 6 faces per bar
fvcd = kron((1:numel(M))', ones(6,1));
set(h, 'FaceVertexCData',fvcd, 'FaceColor','flat', 'CDataMapping','scaled')

colormap hsv; axis tight; view(50,25)
set(h, 'FaceAlpha',0.85)   % semi-transparent bars

bar3_coloring

Or say you wanted to color the bars using gradient according to their heights:

M = 9^2 - spiral(9);
h = my_bar3(M, 0.8);

% use Z-coordinates as vertex colors (indexed color mapping)
v = get(h, 'Vertices');
fvcd = v(:,3);
set(h, 'FaceVertexCData',fvcd, 'FaceColor','interp')

axis tight vis3d; daspect([1 1 10]); view(-40,20)
set(h, 'EdgeColor','k', 'EdgeAlpha',0.1)

gradient_bars_animation

Note that in the last example, the "Renderer" property of the figure will affect the appearance of the gradients. In MATLAB, the 'OpenGL' renderer will interpolate colors along the RGB colorspace, whereas the other two renderers ('Painters' and 'ZBuffer') will interpolate across the colors of the current colormap used (so the histogram bars would look like mini colorbars going through the jet palette, as opposed to a gradient from blue at the base to whatever the color is at the defined height as shown above). See this post for more details.


I've tested the function in Octave 3.6.4 and 3.8.1 both running on Windows, and it worked fine. If you run the examples I showed above, you'll find that some of the advanced 3D features are not yet implemented correctly in Octave (this includes transparency, lighting, and such..). Also I've used functions not available in Octave like membrane and spiral to build sample matrices, but those are not essential to the code, just replace them with your own data :)

octave_my_bar3

Anisotropic answered 4/7, 2014 at 6:2 Comment(4)
Awesome. But why not incorporate the colouring bars by height as a C-data extra parameter to the function?Donnie
@Dan: Do you mean you want to call the function as my_bar3(Z,C) where C (a matrix of same size as Z) is used to color the bars? I guess it could done, but bar3 (which I tried to replicate) doesn't offer such syntax. Plus I didn't want to handle all possible calling options as it would make the code too long (we have to check if C is passed or not, do we assume C=Z if its missing or should we use a single color for all bars? Plus decide whether C should be used to specify gradients, or for bars with flat colors).. At least I sorta showed how to do some of it in the examples :)Anisotropic
I was thinking more like my_bar3(Z, width, C) so that it can still be used just like bar3. I think the default C can be one solid colour as your function creates now. I didn't really think about the gradient vs solid issue...Donnie
@Dan: ok here is one possible way to incorporate a color parameter: pastebin.com/HL9UtY2N (diff/patch file). Now you can call the function in of the following ways: my_bar3(M,w) (default solid color), my_bar3(M,w,rand(1,3)) and my_bar3(M,w,'red') (user-supplied solid color), my_bar3(M,w,rand(size(M))) (one color per bar as gradients using indexed mapping into the current colormap), or my_bar3(M,w,rand(numel(M),3)) (flat colors, one row per bar, denoted by a matrix of true-colors).Anisotropic
E
7

Solution using only functions available in OCTAVE, tested with octave-online

This solution generates a surface in a similar way to the internals of Matlabs hist3d function.

In brief:

  • creates a surface with 4 points with the "height" of each value, which are plotted at each bin edge.
  • Each is surrounded by zeros, which are also plotted at each bin edge.
  • The colour is set to be based on the bin values and is applied to the 4 points and the surrounding zeros. (so that the edges and tops of the 'bars' are coloured to match the "height".)

For data given as a matrix containing bin heights (bin_values in the code):

Code

bin_values=rand(5,4); %some random data

bin_edges_x=[0:size(bin_values,2)]; 
x=kron(bin_edges_x,ones(1,5));
x=x(4:end-2);

bin_edges_y=[0:size(bin_values,1)]; 
y=kron(bin_edges_y,ones(1,5));
y=y(4:end-2);

mask_z=[0,0,0,0,0;0,1,1,0,0;0,1,1,0,0;0,0,0,0,0;0,0,0,0,0];
mask_c=ones(5);
z=kron(bin_values,mask_z);
c=kron(bin_values,mask_c);

surf(x,y,z,c)

Output

3dhist

Etna answered 3/7, 2014 at 15:25 Comment(1)
Amazing. Any chance spaces/padding could get added in between the bars? (not a big deal though)Deluca
D
3

I don't have access to Octave, butI believe this should do the trick:

Z = [2 3 4
     8 4 10
     5 6 7];

[H W] = size(Z);

h = zeros( 1, numel(Z) ); 
ih = 1;
for ix = 1:W
    fx = ix-.45;
    tx = ix+.45;
    for iy = 1:W
        fy = iy-.45;
        ty = iy+.45;      

        vert = [ fx fy 0;...
            fx ty 0;...
            tx fy 0;...
            tx ty 0;...
            fx fy Z(iy,ix);...
            fx ty Z(iy,ix);...
            tx fy Z(iy,ix);...
            tx ty Z(iy,ix)];
        faces = [ 1 3 5;...
            5 3 7;...
            7 3 4;...
            7 8 4;...
            5 6 7;...
            6 7 8;...
            1 2 5;...
            5 6 2;...
            2 4 8;...
            2 6 8];

        h(ih) = patch( 'faces', faces, 'vertices', vert, 'FaceVertexCData', Z(iy,ix),...
            'FaceColor', 'flat', 'EdgeColor','none' );
        ih = ih+1;
    end
end
view( 60, 45 );
colorbar;
Defunct answered 3/7, 2014 at 11:42 Comment(6)
@Donnie - can you check this on Octave?Defunct
I don't have Octave installed either. I normally check here: compileonline.com/execute_matlab_online.php but it doesn't always plot graphs. it doesn't error though so that's promising.Donnie
Can you explain your faces matrix?Donnie
@Donnie a face is a list of vertices, I suspect that octave does not support non-planar faces and therefore all faces here are triangles (3 vertices). Each traiangle face (row of faces) index three rows in vert. Hence the face [1 3 5] is a triangle connecting the first, third and fifth vertices.Defunct
I don't have Octave to check. OK I see so it's 10 triangles per bar - thanks.Donnie
Works fine in octave; tested the script in version 6.4.0 on Ubuntu 22.04Louie
F
3

I think the following should do the trick. I didn't use anything more sophisticated than colormap, surf and patch, which to my knowledge should all work as-is in Octave.

The code:

%# Your data
Z = [2 3 4
    8 4 10
    5 6 7];


%# the "nominal" bar (adjusted from cylinder())
n = 4;
r = [0.5; 0.5];
m = length(r);
theta = (0:n)/n*2*pi + pi/4;

sintheta = sin(theta); sintheta(end) = sqrt(2)/2;

x0 = r * cos(theta);
y0 = r * sintheta;
z0 = (0:m-1)'/(m-1) * ones(1,n+1);

%# get data for current colormap
map = colormap;
Mz = max(Z(:));
mz = min(Z(:));

% Each "bar" is 1 surf and 1 patch
for ii = 1:size(Z,1)
    for jj = 1:size(Z,2)

        % Get color (linear interpolation through current colormap)
        cI = (Z(ii,jj)-mz)*(size(map,1)-1)/(Mz-mz) + 1;
        fC = floor(cI);
        cC = ceil(cI);
        color = map(fC,:) + (map(cC,:) - map(fC,:)) * (cI-fC);

        % Translate and rescale the nominal bar
        x = x0+ii;
        y = y0+jj;
        z = z0*Z(ii,jj);

        % Draw the bar
        surf(x,y,z, 'Facecolor', color)
        patch(x(end,:), y(end,:), z(end,:), color)

    end
end

Result:

enter image description here

How I generate the "nominal bar" is based on code from MATLAB's cylinder(). One cool thing about that is you can very easily make much more funky-looking bars:

enter image description here

This was generated by changing

n = 4;
r = [0.5; 0.5];

into

n = 8;
r = [0.5; 0.45; 0.2; 0.1; 0.2; 0.45; 0.5];
Futch answered 3/7, 2014 at 13:12 Comment(7)
@Dan: can you verify whether this works correctly on Octave (or the online version thereof)?Futch
It doesn't error on the online version, but I can't check if it makes the right plot unfortunately. I would suspect that if it doesn't error then it does work though but I'm afraid I can't really check. Looks like a great solution though!Donnie
fwiw, I got a single big yellow box with this code.Deluca
@StevenLu What MATLAB version do you have?Futch
I don't. Using Octave. I would hope it works perfectly on MATLAB.Deluca
@StevenLu: Ah...What do you think causes this? I don't see why surf, patch or colormap should fail the way you describe...Futch
This fails on Octave because you don't use hold on between calls to surface. The end result is that only the last bar is plotted.Rowles
D
2

Have you looked at this tutorial on bar3?

Adapting it slightly:

Z=[2 3 4
   8 4 10
   5 6 7]; % input data
figure; 
h = bar3(Z); % get handle to graphics
for k=1:numel(h), 
    z=get(h(k),'ZData'); % old data - need for its NaN pattern
    nn = isnan(z); 
    nz = kron( Z(:,k),ones(6,4) ); % map color to height 6 faces per data point 
    nz(nn) = NaN; % used saved NaN pattern for transparent faces 
    set(h(k),'CData', nz); % set the new colors
end
colorbar;

And here's what you get at the end: enter image description here

Defunct answered 3/7, 2014 at 9:35 Comment(1)
Great solution for Matlab - but it looks like bar3 doesn't exist for Octave :(Donnie
H
0

I would like to propose a solution inspired by two references about Scilab and Matlab. For the colors, I have used the code shown in this page by Oldenhuis.

clear -a;
data=[2,3,4;8,4,10;5,6,7];
[nr,nc]=size(data);
m=min(data(:));
M=max(data(:));
w=0.8;
cmap=colormap(jet);

figure(1);
clf;
hold on;
for i=1:nr
    for j=1:nc
        x=[zeros(2,1)+j+(1-w),ones(2,2)*w+j,zeros(2,2)+j+(1-w)];
        y=[zeros(2,2)+i+(1-w),ones(2,2)*w+i,zeros(2,1)+i+(1-w)];
        z=[zeros(1,5);ones(1,5)*data(i,j)];
        index=(data(i,j)-m)*(size(cmap,1)-1)/(M-m)+1;
        f=floor(index);
        c=ceil(index);
        color=cmap(f,:)+(cmap(c,:)-cmap(f,:))*(index-f);
        surf(x,y,z,"facecolor",color,"facealpha",1,"linewidth",0.1);
        patch(x',y',z',color,"facealpha",1,"linewidth",0.1);
    end
end

view(160,30);
xlabel("X label");
ylabel("Y label");
zlabel("Z label");
title("Title");
set(gca,"xtick",1.5:1:3.5,"xticklabel",{"3","2","1"});
set(gca,"ytick",1.5:1:3.5,"yticklabel",{"C","B","A"});
grid on;
hold off;

print -dpng plot_bar3d_test.png

and the result is the final plot.

Hakon answered 9/5, 2023 at 5:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.