Customizing contour plots part 2

A few weeks ago a user posted a question on Matlab’s Answers forum, asking whether it is possible to display contour labels in the same color as their corresponding contour lines. In today’s post I’ll provide some insight that may assist users with similar customizations in other plot types.

Matlab does not provide, for reasons that escape my limited understanding, documented access to the contour plot’s component primitives, namely its contour lines, labels and patch faces. Luckily however, these handles are accessible (in HG2, i.e. R2014b onward) via undocumented hidden properties aptly named EdgePrims, TextPrims and FacePrims, as I explained in a previous post about contour plots customization, two years ago.

Let’s start with a simple contour plot of the peaks function:

[X,Y,Z] = peaks;
[C,hContour] = contour(X,Y,Z, 'ShowText','on', 'LevelStep',1);

The result is the screenshot on the left:

Standard Matlab contour labels

Standard Matlab contour labels

 
Customized Matlab contour labels

Customized Matlab contour labels

In order to update the label colors (to get the screenshot on the right), we create a short updateContours function that updates the TextPrims color to their corresponding EdgePrims color:

The updateContours() function

function updateContours(hContour)
    % Update the text label colors
    drawnow  % very important!
    levels = hContour.LevelList;
    labels = hContour.TextPrims;  % undocumented/unsupported
    lines  = hContour.EdgePrims;  % undocumented/unsupported
    for idx = 1 : numel(labels)
        labelValue = str2double(labels(idx).String);
        lineIdx = find(abs(levels-labelValue)<10*eps, 1);  % avoid FP errors using eps
        labels(idx).ColorData = lines(lineIdx).ColorData;  % update the label color
        %labels(idx).Font.Size = 8;                        % update the label font size
    end
    drawnow  % optional
end

Note that in this function we don’t directly equate the numeric label values to the contour levels’ values: this would work well for integer values but would fail with floating-point ones. Instead I used a very small 10*eps tolerance in the numeric comparison.

Also note that I was careful to call drawnow at the top of the update function, in order to ensure that EdgePrims and TextPrims are updated when the function is called (this might not be the case before the call to drawnow). The final drawnow at the end of the function is optional: it is meant to reduce the flicker caused by the changing label colors, but it can be removed to improve the rendering performance in case of rapidly-changing contour plots.

Finally, note that I added a commented line that shows we can modify other label properties (in this case, the font size from 10 to 8). Feel free to experiment with other label properties.

Putting it all together

The final stage is to call our new updateContours function directly, immediately after creating the contour plot. We also want to call updateContours asynchronously whenever the contour is redrawn, for example, upon a zoom/pan event, or when one of the relevant contour properties (e.g., LevelStep or *Data) changes. To do this, we add a callback listener to the contour object’s [undocumented] MarkedClean event that reruns our updateContours function:

[X,Y,Z] = peaks;
[C,hContour] = contour(X,Y,Z, 'ShowText','on', 'LevelStep',1);
 
% Update the contours immediately, and also whenever the contour is redrawn
updateContours(hContour);
addlistener(hContour, 'MarkedClean', @(h,e)updateContours(hContour));

Contour level values

As noted in my comment reply below, the contour lines (hContour.EdgePrims) correspond to the contour levels (hContour.LevelList).

For example, to make all negative contour lines dotted, you can do the following:

[C,hContour] = contour(peaks, 'ShowText','on', 'LevelStep',1); drawnow
set(hContour.EdgePrims(hContour.LevelList<0), 'LineStyle', 'dotted');

Customized Matlab contour lines

Customized Matlab contour lines

Prediction about forward compatibility

As I noted on my previous post on contour plot customization, I am marking this article as “High risk of breaking in future Matlab versions“, not because of the basic functionality (being important enough I don’t presume it will go away anytime soon) but because of the property names: TextPrims, EdgePrims and FacePrims don’t seem to be very user-friendly property names. So far MathWorks has been very diligent in making its object properties have meaningful names, and so I assume that when the time comes to expose these properties, they will be renamed (perhaps to TextHandles, EdgeHandles and FaceHandles, or perhaps LabelHandles, LineHandles and FillHandles). For this reason, even if you find out in some future Matlab release that TextPrims, EdgePrims and FacePrims don’t exist, perhaps they still exist and simply have different names. Note that these properties have not changed their names or functionality in the past 3 years, so while it could well happen next year, it could also remain unchanged for many years to come. The exact same thing can be said for the MarkedClean event.

Professional assistance anyone?

As shown by this and many other posts on this site, a polished interface and functionality is often composed of small professional touches, many of which are not exposed in the official Matlab documentation for various reasons. So if you need top-quality professional appearance/functionality in your Matlab program, or maybe just a Matlab program that is dependable, robust and highly-performant, consider employing my consulting services.

Categories: Handle graphics, Hidden property, High risk of breaking in future versions, Stock Matlab function

Tags: , , ,

Bookmark and SharePrint Print

9 Responses to Customizing contour plots part 2

  1. Elbio says:

    Hi Yair:

    Using the old matlab utilities it was possible to get the Cdata value associated with each line in the contour plot:

    [c,h]=contourf(peaks,[-9:1:9]);
    h1=get(h,'children');
    hdd=get(h1(1),'Cdata');
     
    hdd=8

    This utility was useful to dash the negative contours of some plot. Apparently there is no “cdata” value in the h.EdgePrims properties.

    How do you think that one can proceed in order to get these values?

    Regards,

    Elbio

    • @Elbio – in HG2 (R2014b or newer) the contour lines (hContour.EdgePrims) correspond to the contour levels (hContour.LevelList). For example, to make all negative contour lines dotted, you can do the following:

      [C,hContour] = contour(peaks, 'ShowText','on', 'LevelStep',1); drawnow
      set(hContour.EdgePrims(hContour.LevelList<0), 'LineStyle', 'dotted');

      I added an explanation and screenshot to the main article text above.

    • Elbio says:

      Hi Yair:

      Many thanks for your helpful answer. I found a problem though when using contourf.

      [c,h] = contourf(peaks,'LevelStep',1); drawnow
      levels = h.LevelList;
      lines  = h.EdgePrims;
      set(lines(levels<0),  'LineStyle', 'dotted');
      set(lines(levels==0), 'LineWidth', 2);

      Instead of highlighting the zero line it is the “1” line the one that appears wider.
      It might be related to the different size of the levels and lines arrays. For example if I tried to change the linestyle using:

      [c,h] = contourf(peaks,[-9:1:9]);
      levels = h.LevelList;
      lines  = h.EdgePrims;
      set(lines(levels<0), 'LineStyle', 'dotted');

      there are dotted lines with positive values.

      Regards,
      Elbio.

    • Unlike contour, the contourf function returns a list of levels that includes the minimum value, which does not correspond to any contour line ([-6.5466,-6,-5,-4,...] rather than [-6,-5,-4,...]). So the solution for contourf is simply to equate levels(2:end) with lines, as follows:

      [c,hContour] = contourf(peaks, 'LevelStep',1, 'ShowText','on'); drawnow
      levels = hContour.LevelList(2:end);
      lines  = hContour.EdgePrims;
      set(lines(levels<0),  'LineStyle', 'dotted');
      set(lines(levels==0), 'LineWidth', 2);

  2. Alon says:

    Hello Yair,
    Note that if there are unused contour lines, as may be the case when hContour.LevelListMode is set to manual, the above code will not work, as hContour.EdgePrims enumerates only the visible contour lines (while not hinting at the contour level they are related to), while hContour.LevelList enumerates them all.
    Instead, you must enumerate the visible contour lines:

    k = 1;
    activecontours = [];
    while k < size(hContour.ContourMatrix,2)
        activecontours = [activecontours hContour.ContourMatrix(1,k)];
        k = k + hContour.ContourMatrix(2,k) + 1;
    end
    levels = unique(activecontours);

    Then use the levels generated in the code you supplied.

    Best regards,
    Alon

    • Peter Cook says:

      @Alon
      This method runs very slow for a complex contour (e.g. not necessarily in size but perhaps one generated from real, noisy, data), which led me to experiment with some other methods. In the case where contour levels are specified to the function, I found a heuristic that seems to work:

      [c,hContour] = contourf(imgaussfilt(noisyData,2),contourLevels);
      hL = hContour.EdgePrims;
      whichLevels = sum(bsxfun(@eq,c(1,:)',contourLevels)) > 10;
       
      >> length(contourLevels)
      ans =
          32
       
      >> length(hL)
      ans =
          24
       
      >> length(contourLevels(whichLevels))
      ans =
          24

      I’m sure this might fail on some other contour plots, but it has worked on several I generated. Thoughts?

  3. Steve says:

    You can also use clabel(c,hContour,'FontSize',10,'Color',[1 1 1])

  4. Shan says:

    Hi,
    Thank you so much for this improvement.
    But when I use the function ax=gca, it will overrun the set(hContour.EdgePrims(hContour.LevelList<0),'LineStyle','dotted');
    here is my code:

    [X,Y,Z] = peaks;
    [C,hContour] = contour(peaks,[-8 -6 -4 -2 -0 2 4 6 8], 'ShowText','on'); drawnow
    set(hContour.EdgePrims(hContour.LevelList<0),'LineStyle','dotted');
    ax=gca;
    ax.YDir = 'reverse';

    So do you have any solution for this?
    Thanks

    • @Shan – The ax.YDir='reverse' command, and any other similar command that repaints the axes, reverts the EdgePrims to their default (documented) behavior. So after any such command, you need to redo the setting of the undocumented functionality (set(hContour.EdgePrims...)

Leave a Reply

Your email address will not be published. Required fields are marked *