/*******************************************************************************
* Copyright (c) 2010 Haifeng Li
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
package smile.plot;
import java.awt.Color;
import java.util.ArrayList;
import java.util.List;
import smile.math.Math;
/**
* A contour plot is a graphical technique for representing a 3-dimensional
* surface by plotting constant z slices, called contours, on a 2-dimensional
* format. That is, given a value for z, lines are drawn for connecting the
* (x, y) coordinates where that z value occurs. The contour plot is an
* alternative to a 3-D surface plot.
*
* @author Haifeng Li
*/
public class Contour extends Plot {
private static final String DIMENSIONS_XZ_DONT_MATCH = "The dimensions of x and z don't match.";
private static final String DIMENSIONS_YZ_DONT_MATCH = "The dimensions of y and z don't match.";
/**
* The x coordinate of surface.
*/
private double[] x;
/**
* The y coordinate of surface.
*/
private double[] y;
/**
* The two-dimensional surface.
*/
private double[][] z;
/**
* Minimum of data matrix.
*/
private double zMin;
/**
* Maximum of data matrix.
*/
private double zMax;
/**
* Plot contours in logarithmic scale.
*/
private boolean logScale = false;
/**
* The number of contours.
*/
private int numLevels = 10;
/**
* The contour level values.
*/
private double[] levels;
/**
* The contour color.
*/
private Color[] colors;
/**
* True if show the level values.
*/
private boolean showLevelValue = true;
/**
* The set of contours.
*/
private List contours;
/**
* Contour contains a list of segments.
*/
class Isoline {
/**
* The coordinates of points along the contour line.
*/
List points = new ArrayList<>();
/**
* The level value of contour line.
*/
double level;
/**
* The color of contour line.
*/
Color color;
/**
* The label of contour line.
*/
Label label;
/**
* Constructor.
*/
Isoline(double level) {
this.level = level;
}
/**
* Constructor.
*/
Isoline(double level, Color color) {
this.level = level;
this.color = color;
}
/**
* Add a point to the contour line.
*/
void add(double[] point) {
points.add(point);
}
/**
* Add a point to the contour line.
*/
void add(double x, double y) {
double[] point = {x, y};
points.add(point);
}
/**
* Paint the contour line. If the color attribute is null, the level
* value of contour line will be shown along the line.
*/
void paint(Graphics g) {
Color c = g.getColor();
if (color != null) {
g.setColor(color);
}
double angle = 0.0;
double horizontalReference = 0.0;
double verticalReference = 0.0;
double[] coord = null;
if (points.size() > 1) {
double[] x1 = points.get(0);
for (int i = 1; i < points.size(); i++) {
double[] x2 = points.get(i);
g.drawLine(x1, x2);
x1 = x2;
if (label == null) {
if (i == points.size() / 2) {
coord = x1;
int k = i + 1;
if (k >= points.size()) {
k = i;
}
// compute label angle
angle = Math.PI / 2;
double dxx = points.get(k)[0] - points.get(i)[0];
double dyy = points.get(k)[1] - points.get(i)[1];
if (dyy < 0.0) {
angle = -Math.PI / 2;
}
if (dxx != 0.0) {
angle = Math.atan(dyy / dxx) + Math.PI / 2;
}
}
}
}
} else if (points.size() == 1) {
double[] x1 = points.get(0);
g.drawPoint('@', x1);
coord = x1;
horizontalReference = 0.0;
}
if (label == null) {
double[] lb = g.getLowerBound();
double[] ub = g.getUpperBound();
double xrange = ub[0] - lb[0];
double yrange = ub[1] - lb[1];
if (ub[0] - coord[0] < xrange / 10) {
horizontalReference = 1.0;
}
if (ub[1] - coord[1] < yrange / 10) {
horizontalReference = 1.0;
}
if (coord[1] - lb[1] < yrange / 10) {
horizontalReference = 0.0;
}
label = new Label(String.format("%.2G", level), horizontalReference, verticalReference, angle, coord);
}
if (showLevelValue && label != null) {
label.paint(g);
}
if (color != null) {
g.setColor(c);
}
}
}
/**
* Constructor.
* @param z the data matrix to create contour plot.
*/
public Contour(double[][] z) {
this.z = z;
init();
}
/**
* Constructor.
* @param z the data matrix to create contour plot.
* @param numLevels the number of contour levels.
*/
public Contour(double[][] z, int numLevels) {
this.z = z;
this.numLevels = numLevels;
init();
}
/**
* Constructor.
* @param z the data matrix to create contour plot.
* @param numLevels the number of contour levels.
* @param logScale true to interpolate contour levels in logarithmic scale.
*/
public Contour(double[][] z, int numLevels, boolean logScale) {
this.z = z;
this.numLevels = numLevels;
this.logScale = logScale;
init();
}
/**
* Constructor.
* @param z the data matrix to create contour plot.
* @param levels the level values of contours.
*/
public Contour(double[][] z, double[] levels) {
this.z = z;
this.levels = levels;
init();
}
/**
* Constructor.
* @param z the data matrix to create contour plot.
* @param levels the level values of contours.
* @param colors the color for each contour level.
*/
public Contour(double[][] z, double[] levels, Color[] colors) {
this.z = z;
this.levels = levels;
this.colors = colors;
showLevelValue = false;
init();
}
/**
* Constructor.
* @param x the x coordinates of the data grid of z. Must be in ascending order.
* @param y the y coordinates of the data grid of z. Must be in ascending order.
* @param z the data matrix to create contour plot.
*/
public Contour(double[] x, double[] y, double[][] z) {
if (x.length != z[0].length) {
throw new IllegalArgumentException(DIMENSIONS_XZ_DONT_MATCH);
}
if (y.length != z.length) {
throw new IllegalArgumentException(DIMENSIONS_YZ_DONT_MATCH);
}
this.x = x;
this.y = y;
this.z = z;
init();
}
/**
* Constructor.
* @param x the x coordinates of the data grid of z. Must be in ascending order.
* @param y the y coordinates of the data grid of z. Must be in ascending order.
* @param z the data matrix to create contour plot.
* @param numLevels the number of contour levels.
*/
public Contour(double[] x, double[] y, double[][] z, int numLevels) {
if (x.length != z[0].length) {
throw new IllegalArgumentException(DIMENSIONS_XZ_DONT_MATCH);
}
if (y.length != z.length) {
throw new IllegalArgumentException(DIMENSIONS_YZ_DONT_MATCH);
}
this.x = x;
this.y = y;
this.z = z;
this.numLevels = numLevels;
init();
}
/**
* Constructor.
* @param x the x coordinates of the data grid of z. Must be in ascending order.
* @param y the y coordinates of the data grid of z. Must be in ascending order.
* @param z the data matrix to create contour plot.
* @param numLevels the number of contour levels.
* @param logScale true to interpolate contour levels in logarithmical scale.
*/
public Contour(double[] x, double[] y, double[][] z, int numLevels, boolean logScale) {
if (x.length != z[0].length) {
throw new IllegalArgumentException(DIMENSIONS_XZ_DONT_MATCH);
}
if (y.length != z.length) {
throw new IllegalArgumentException(DIMENSIONS_YZ_DONT_MATCH);
}
this.x = x;
this.y = y;
this.z = z;
this.numLevels = numLevels;
this.logScale = logScale;
init();
}
/**
* Constructor.
* @param x the x coordinates of the data grid of z. Must be in ascending order.
* @param y the y coordinates of the data grid of z. Must be in ascending order.
* @param z the data matrix to create contour plot.
* @param levels the level values of contours. Must be strictly increasing and finite.
*/
public Contour(double[] x, double[] y, double[][] z, double[] levels) {
if (x.length != z[0].length) {
throw new IllegalArgumentException(DIMENSIONS_XZ_DONT_MATCH);
}
if (y.length != z.length) {
throw new IllegalArgumentException(DIMENSIONS_YZ_DONT_MATCH);
}
this.x = x;
this.y = y;
this.z = z;
this.levels = levels;
init();
}
/**
* Constructor.
* @param x the x coordinates of the data grid of z. Must be in ascending order.
* @param y the y coordinates of the data grid of z. Must be in ascending order.
* @param z the data matrix to create contour plot.
* @param levels the level values of contours. Must be strictly increasing and finite.
* @param colors the color for each contour level.
*/
public Contour(double[] x, double[] y, double[][] z, double[] levels, Color[] colors) {
if (x.length != z[0].length) {
throw new IllegalArgumentException(DIMENSIONS_XZ_DONT_MATCH);
}
if (y.length != z.length) {
throw new IllegalArgumentException(DIMENSIONS_YZ_DONT_MATCH);
}
if (levels.length != colors.length) {
throw new IllegalArgumentException("The number of levels and colors don't match.");
}
this.x = x;
this.y = y;
this.z = z;
this.levels = levels;
this.colors = colors;
showLevelValue = false;
init();
}
/**
* A line segment in contour line.
*/
class Segment {
double x0;
double y0;
double x1;
double y1;
Segment next;
Segment(double x0, double y0, double x1, double y1, Segment next) {
this.x0 = x0;
this.y0 = y0;
this.x1 = x1;
this.y1 = y1;
this.next = next;
}
void swap() {
double x, y;
x = x0;
y = y0;
x0 = x1;
y0 = y1;
x1 = x;
y1 = y;
}
}
/**
* Initialize the contour lines.
*/
private void init() {
if (x == null) {
x = new double[z[0].length];
for (int i = 0; i < x.length; i++) {
x[i] = i + 0.5;
}
}
if (y == null) {
y = new double[z.length];
for (int i = 0; i < y.length; i++) {
y[i] = i + 0.5;
}
double[][] zz = new double[z.length][];
for (int i = 0; i < z.length; i++) {
zz[i] = z[z.length - i - 1];
}
z = zz;
}
zMin = Math.min(z);
zMax = Math.max(z);
contours = new ArrayList<>(numLevels);
if (logScale && zMin <= 0.0) {
throw new IllegalArgumentException("Log scale is not support for non-positive data");
}
if (levels == null) {
if (logScale) {
double lowerBound = Math.ceil(Math.log10(zMin));
double upperBound = Math.floor(Math.log10(zMax));
int n = (int) Math.round(upperBound - lowerBound);
if (n == 0) {
logScale = false;
}
numLevels = n + 1;
levels = new double[numLevels];
for (int i = 0; i < numLevels; i++) {
levels[i] = Math.pow(10, lowerBound + i);
}
}
if (levels == null) {
double digits = Math.log10(Math.abs(zMax - zMin));
double residual = digits - Math.floor(digits);
if (residual < 0.4) {
// If the range is less than 20 units, we reduce one level.
digits -= 1.0;
}
double precisionDigits = (int) Math.floor(digits);
double precisionUnit = Math.pow(10, precisionDigits);
if (residual >= 0.4 && residual <= 0.7) {
// In case of too few grids, we use a half of precision unit.
precisionUnit /= 2;
precisionDigits -= 1;
}
double lowerBound = precisionUnit * (Math.ceil(zMin / precisionUnit));
double upperBound = precisionUnit * (Math.floor(zMax / precisionUnit));
numLevels = (int) Math.round((upperBound - lowerBound) / precisionUnit) + 1;
levels = new double[numLevels];
for (int i = 0; i < numLevels; i++) {
levels[i] = lowerBound + i * precisionUnit;
}
}
}
double[] xx = new double[4];
double[] yy = new double[4];
int[] ij = new int[2];
int nx = x.length;
int ny = y.length;
Segment[][] segments = new Segment[ny][nx];
double atom = 1E-7 * (zMax - zMin);
for (int c = 0; c < levels.length; c++) {
double zc = levels[c];
for (int i = 0; i < nx; i++) {
for (int j = 0; j < ny; j++) {
segments[j][i] = null;
}
}
for (int i = 0; i < nx - 1; i++) {
double xl = x[i];
double xh = x[i + 1];
for (int j = 0; j < ny - 1; j++) {
double yl = y[j];
double yh = y[j + 1];
double zll = z[j][i];
double zhl = z[j][i + 1];
double zlh = z[j + 1][i];
double zhh = z[j + 1][i + 1];
// If the value at a corner is exactly equal to a contour level,
// change that value by a tiny amount
if (zll == zc) {
zll += atom;
}
if (zhl == zc) {
zhl += atom;
}
if (zlh == zc) {
zlh += atom;
}
if (zhh == zc) {
zhh += atom;
}
// Check for intersections with sides
int nacode = 0;
if (!Double.isInfinite(zll)) {
nacode += 1;
}
if (!Double.isInfinite(zhl)) {
nacode += 2;
}
if (!Double.isInfinite(zlh)) {
nacode += 4;
}
if (!Double.isInfinite(zhh)) {
nacode += 8;
}
int k = 0;
switch (nacode) {
case 15:
if (isIntersect(zll, zhl, zc)) {
double f = getIntersectRatio(zll, zhl, zc);
xx[k] = xl + f * (xh - xl);
yy[k] = yl;
k++;
}
if (isIntersect(zll, zlh, zc)) {
double f = getIntersectRatio(zll, zlh, zc);
yy[k] = yl + f * (yh - yl);
xx[k] = xl;
k++;
}
if (isIntersect(zhl, zhh, zc)) {
double f = getIntersectRatio(zhl, zhh, zc);
yy[k] = yl + f * (yh - yl);
xx[k] = xh;
k++;
}
if (isIntersect(zlh, zhh, zc)) {
double f = getIntersectRatio(zlh, zhh, zc);
xx[k] = xl + f * (xh - xl);
yy[k] = yh;
k++;
}
break;
case 14:
if (isIntersect(zhl, zhh, zc)) {
double f = getIntersectRatio(zhl, zhh, zc);
yy[k] = yl + f * (yh - yl);
xx[k] = xh;
k++;
}
if (isIntersect(zlh, zhh, zc)) {
double f = getIntersectRatio(zlh, zhh, zc);
xx[k] = xl + f * (xh - xl);
yy[k] = yh;
k++;
}
if (isIntersect(zlh, zhl, zc)) {
double f = getIntersectRatio(zlh, zhl, zc);
xx[k] = xl + f * (xh - xl);
yy[k] = yh + f * (yl - yh);
k++;
}
break;
case 13:
if (isIntersect(zll, zlh, zc)) {
double f = getIntersectRatio(zll, zlh, zc);
yy[k] = yl + f * (yh - yl);
xx[k] = xl;
k++;
}
if (isIntersect(zlh, zhh, zc)) {
double f = getIntersectRatio(zlh, zhh, zc);
xx[k] = xl + f * (xh - xl);
yy[k] = yh;
k++;
}
if (isIntersect(zll, zhh, zc)) {
double f = getIntersectRatio(zll, zhh, zc);
xx[k] = xl + f * (xh - xl);
yy[k] = yl + f * (yh - yl);
k++;
}
break;
case 11:
if (isIntersect(zhl, zhh, zc)) {
double f = getIntersectRatio(zhl, zhh, zc);
yy[k] = yl + f * (yh - yl);
xx[k] = xh;
k++;
}
if (isIntersect(zll, zhl, zc)) {
double f = getIntersectRatio(zll, zhl, zc);
xx[k] = xl + f * (xh - xl);
yy[k] = yl;
k++;
}
if (isIntersect(zll, zhh, zc)) {
double f = getIntersectRatio(zll, zhh, zc);
xx[k] = xl + f * (xh - xl);
yy[k] = yl + f * (yh - yl);
k++;
}
break;
case 7:
if (isIntersect(zll, zlh, zc)) {
double f = getIntersectRatio(zll, zlh, zc);
yy[k] = yl + f * (yh - yl);
xx[k] = xl;
k++;
}
if (isIntersect(zll, zhl, zc)) {
double f = getIntersectRatio(zll, zhl, zc);
xx[k] = xl + f * (xh - xl);
yy[k] = yl;
k++;
}
if (isIntersect(zlh, zhl, zc)) {
double f = getIntersectRatio(zlh, zhl, zc);
xx[k] = xl + f * (xh - xl);
yy[k] = yh + f * (yl - yh);
k++;
}
break;
}
// We now have k(=2,4) endpoints. Decide which to join.
Segment seglist = null;
if (k > 0) {
if (k == 2) {
seglist = new Segment(xx[0], yy[0], xx[1], yy[1], seglist);
} else if (k == 4) {
for (k = 3; k >= 1; k--) {
int m = k;
xl = xx[k];
for (int l = 0; l < k; l++) {
if (xx[l] > xl) {
xl = xx[l];
m = l;
}
}
if (m != k) {
xl = xx[k];
yl = yy[k];
xx[k] = xx[m];
yy[k] = yy[m];
xx[m] = xl;
yy[m] = yl;
}
}
seglist = new Segment(xx[0], yy[0], xx[1], yy[1], seglist);
seglist = new Segment(xx[2], yy[2], xx[3], yy[3], seglist);
} else {
throw new IllegalStateException("k != 2 or 4");
}
}
segments[j][i] = seglist;
}
}
// Begin following contours.
// 1. Grab a segment
// 2. Follow its tail
// 3. Follow its head
// 4. Save the contour
for (int i = 0; i < nx - 1; i++) {
for (int j = 0; j < ny - 1; j++) {
Segment seglist = null;
while ((seglist = segments[j][i]) != null) {
ij[0] = i;
ij[1] = j;
Segment start = seglist;
Segment end = seglist;
segments[j][i] = seglist.next;
double xend = seglist.x1;
double yend = seglist.y1;
int dir = 0;
while ((dir = segdir(xend, yend, ij)) != 0) {
// tail
int ii = ij[0];
int jj = ij[1];
Segment[] seg = {segments[jj][ii], null};
segments[jj][ii] = segupdate(xend, yend, dir, true, seg);
if (seg[1] == null) {
break;
}
end.next = seg[1];
end = seg[1];
xend = end.x1;
yend = end.y1;
}
end.next = null;
ij[0] = i;
ij[1] = j;
xend = seglist.x0;
yend = seglist.y0;
dir = 0;
while ((dir = segdir(xend, yend, ij)) != 0) {
// head
int ii = ij[0];
int jj = ij[1];
Segment[] seg = {segments[jj][ii], null};
segments[jj][ii] = segupdate(xend, yend, dir, false, seg);
if (seg[1] == null) {
break;
}
seg[1].next = start;
start = seg[1];
xend = start.x0;
yend = start.y0;
}
// Save the contour locations into the list of contours
Isoline contour = new Isoline(zc);
if (colors != null) {
contour.color = colors[c];
}
Segment s = start;
contour.add(s.x0, s.y0);
while (s.next != null) {
s = s.next;
contour.add(s.x0, s.y0);
}
contour.add(s.x1, s.y1);
if (!contour.points.isEmpty()) {
contours.add(contour);
}
}
}
}
}
}
/**
* Returns true if zc is between z0 and z1.
*/
private boolean isIntersect(double z0, double z1, double zc) {
if ((z0 - zc) * (z1 - zc) < 0.0) {
return true;
}
return false;
}
/**
* Returns the ratio of (zc - z0) / (z1 - z0)
*/
private double getIntersectRatio(double z0, double z1, double zc) {
return (zc - z0) / (z1 - z0);
}
private boolean XMATCH(double x0, double x1) {
return Math.abs(x0 - x1) == 0;
}
private boolean YMATCH(double y0, double y1) {
return Math.abs(y0 - y1) == 0;
}
/**
* Determine the entry direction to the next cell and update the cell
* indices.
*/
private int segdir(double xend, double yend, int[] ij) {
if (YMATCH(yend, y[ij[1]])) {
if (ij[1] == 0) {
return 0;
}
ij[1] -= 1;
return 3;
}
if (XMATCH(xend, x[ij[0]])) {
if (ij[0] == 0) {
return 0;
}
ij[0] -= 1;
return 4;
}
if (YMATCH(yend, y[ij[1] + 1])) {
if (ij[1] >= y.length - 1) {
return 0;
}
ij[1] += 1;
return 1;
}
if (XMATCH(xend, x[ij[0] + 1])) {
if (ij[0] >= x.length - 1) {
return 0;
}
ij[0] += 1;
return 2;
}
return 0;
}
/**
* Search seglist for a segment with endpoint (xend, yend).
* The cell entry direction is dir, and if tail=1/0 we are
* building the tail/head of a contour. The matching segment
* is pointed to by seg and the updated segment list (with
* the matched segment stripped) is returned by the function.
*/
private Segment segupdate(double xend, double yend, int dir, boolean tail, Segment[] seg) {
Segment seglist = seg[0];
if (seglist == null) {
seg[1] = null;
return null;
}
switch (dir) {
case 1:
case 3:
if (YMATCH(yend, seglist.y0)) {
if (!tail) {
seglist.swap();
}
seg[1] = seglist;
return seglist.next;
}
if (YMATCH(yend, seglist.y1)) {
if (tail) {
seglist.swap();
}
seg[1] = seglist;
return seglist.next;
}
if (YMATCH(yend, seglist.y1)) {
if (tail) {
seglist.swap();
}
seg[1] = seglist;
return seglist.next;
}
break;
case 2:
case 4:
if (XMATCH(xend, seglist.x0)) {
if (!tail) {
seglist.swap();
}
seg[1] = seglist;
return seglist.next;
}
if (XMATCH(xend, seglist.x1)) {
if (tail) {
seglist.swap();
}
seg[1] = seglist;
return seglist.next;
}
break;
}
Segment[] seg2 = {seglist.next, seg[1]};
seglist.next = segupdate(xend, yend, dir, tail, seg2);
seg[1] = seg2[1];
return seglist;
}
/**
* Set if show the level value with each contour line.
*/
public void showLevelValue(boolean b) {
showLevelValue = b;
}
@Override
public void paint(Graphics g) {
for (Isoline contour : contours) {
contour.paint(g);
}
if (colors != null) {
g.clearClip();
Color c = g.getColor();
double height = 0.7 / colors.length;
double[] start = {1.1, 0.15};
double[] end = {1.13, start[1] - height};
for (int i = 0; i < colors.length; i++) {
g.setColor(colors[i]);
g.fillRectBaseRatio(start, end);
start[1] += height;
end[1] += height;
}
g.setColor(Color.BLACK);
start[1] -= height;
end[1] = 0.15 - height;
g.drawRectBaseRatio(start, end);
start[0] = 1.14;
double log = Math.log10(Math.abs(levels[levels.length-1]));
int decimal = 1;
if (log < 0) {
decimal = (int) -log + 1;
}
g.drawTextBaseRatio(String.valueOf(Math.round(levels[levels.length-1], decimal)), 0.0, 1.0, start);
start[1] = 0.15 - height;
log = Math.log10(Math.abs(levels[0]));
decimal = 1;
if (log < 0) {
decimal = (int) -log + 1;
}
g.drawTextBaseRatio(String.valueOf(Math.round(levels[0], decimal)), 0.0, 0.0, start);
g.setColor(c);
}
}
/**
* Create a plot canvas with the contour plot of given data.
* @param z a matrix.
*/
public static PlotCanvas plot(double[][] z) {
double[] lowerBound = {0, 0};
double[] upperBound = {z[0].length, z.length};
PlotCanvas canvas = new PlotCanvas(lowerBound, upperBound, false);
canvas.add(new Contour(z));
canvas.getAxis(0).setLabelVisible(false);
canvas.getAxis(1).setLabelVisible(false);
return canvas;
}
/**
* Create a plot canvas with the contour plot of given data.
* @param z a matrix.
*/
public static PlotCanvas plot(double[][] z, double[] levels, Color[] palette) {
double[] lowerBound = {0, 0};
double[] upperBound = {z[0].length, z.length};
PlotCanvas canvas = new PlotCanvas(lowerBound, upperBound, false);
canvas.add(new Contour(z, levels, palette));
canvas.getAxis(0).setLabelVisible(false);
canvas.getAxis(1).setLabelVisible(false);
return canvas;
}
/**
* Create a plot canvas with the contour plot of given data.
* @param z a matrix.
*/
public static PlotCanvas plot(double[] x, double[] y, double[][] z) {
double[] lowerBound = {Math.min(x), Math.min(y)};
double[] upperBound = {Math.max(x), Math.max(y)};
PlotCanvas canvas = new PlotCanvas(lowerBound, upperBound, false);
canvas.add(new Contour(x, y, z));
return canvas;
}
/**
* Create a plot canvas with the contour plot of given data.
* @param z a matrix.
*/
public static PlotCanvas plot(double[] x, double[] y, double[][] z, double[] levels, Color[] palette) {
double[] lowerBound = {Math.min(x), Math.min(y)};
double[] upperBound = {Math.max(x), Math.max(y)};
PlotCanvas canvas = new PlotCanvas(lowerBound, upperBound, false);
canvas.add(new Contour(x, y, z, levels, palette));
return canvas;
}
}
Ads help maintain this website.