/*************************************************************************
* *
* This file is part of the 20n/act project. *
* 20n/act enables DNA prediction for synthetic biology/bioengineering. *
* Copyright (C) 2017 20n Labs, Inc. *
* *
* Please direct all queries to act@20n.com. *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*************************************************************************/
package com.act.lcms;
import java.io.File;
import java.io.FileWriter;
import java.util.Scanner;
import java.io.IOException;
import java.util.List;
import java.util.ArrayList;
import java.lang.StringBuffer;
public class Gnuplotter {
public static final String DRAW_SEPARATOR = "(separator)";
private Double fontScale = null;
public static class PlotConfiguration {
public static final String DRAW_SEPARATOR = "(separator)";
public enum KIND {
HEADER,
SEPARATOR,
GRAPH,
}
private KIND kind;
private String label;
private Integer dataSetIndex;
private Double yRange;
public PlotConfiguration(KIND kind, String label, Integer dataSetIndex, Double yRange) {
this.kind = kind;
this.label = label;
this.dataSetIndex = dataSetIndex;
this.yRange = yRange;
}
public KIND getKind() {
return kind;
}
public void setKind(KIND kind) {
this.kind = kind;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public Integer getDataSetIndex() {
return dataSetIndex;
}
public void setDataSetIndex(Integer dataSetIndex) {
this.dataSetIndex = dataSetIndex;
}
public Double getYRange() {
return yRange;
}
public void setyRange(Double yRange) {
this.yRange = yRange;
}
protected static PlotConfiguration fromOldParameters(String name, Integer dataSetIndex, Double yMax) {
if (DRAW_SEPARATOR.equals(name)) {
return new PlotConfiguration(KIND.SEPARATOR, "", null, null);
}
// Headers aren't available via this method.
return new PlotConfiguration(KIND.GRAPH, name, dataSetIndex, yMax);
}
}
private List<PlotConfiguration> oldParamsToPlotConfigs(String[] setNames, Double yrange, Double[] yMaxes) {
if (yMaxes != null && setNames.length != yMaxes.length) {
throw new RuntimeException(String.format("Number of data sets (%d) must match number of yRanges (%d)",
setNames.length, yMaxes.length));
}
List<PlotConfiguration> configurations = new ArrayList<>(setNames.length);
int dataSetIndex = 0;
for (int i = 0; i < setNames.length; i++) {
PlotConfiguration config =
PlotConfiguration.fromOldParameters(setNames[i], dataSetIndex, yMaxes == null ? yrange : yMaxes[i]);
if (config.getKind() == PlotConfiguration.KIND.GRAPH) {
dataSetIndex++;
}
configurations.add(config);
}
return configurations;
}
private String sanitize(String fname) {
// Gnuplot assumes LaTeX style for text, so when we put
// the file name in the label it get mathified. Escape _
// so that they dont get interpretted as subscript ops
return fname.replace("_", "\\\\_");
}
private enum Plot2DType { IMPULSES, LINES, OVERLAYED_LINES, HEATMAP };
public void plotHeatmap(String dataFile, String outFile, String[] setNames, Double yrange, String fmt) {
List<PlotConfiguration> configurations = oldParamsToPlotConfigs(setNames, yrange, null);
plot2DHelper(Plot2DType.HEATMAP, dataFile, outFile, null, null, null, true, configurations, fmt);
}
public void plotHeatmap(String dataFile, String outFile, String[] setNames, Double yrange, String fmt,
Double sizeX, Double sizeY, Double[] yMaxes, String outputFile) {
List<PlotConfiguration> configurations = oldParamsToPlotConfigs(setNames, yrange, yMaxes);
plot2DHelper(Plot2DType.HEATMAP, dataFile, outFile, null, null, null, true, fmt, sizeX, sizeY,
configurations, outputFile);
}
public void plotHeatmap(String dataFile, String outFile, String fmt, Double sizeX, Double sizeY,
List<PlotConfiguration> configurations, String outputFile) {
plot2DHelper(Plot2DType.HEATMAP, dataFile, outFile, null, null, null, true, fmt, sizeX, sizeY,
configurations, outputFile);
}
public void plotOverlayed2D(String dataFile, String outFile, String[] setNames, String xlabel, Double yrange,
String ylabel, String fmt, String gnuplotFile) {
List<PlotConfiguration> configurations = oldParamsToPlotConfigs(setNames, yrange, null);
// plotOverlayed2D produces the same graph as plot2D, except it collapses all datasets into a single plot
plot2DHelper(Plot2DType.OVERLAYED_LINES, dataFile, outFile, null, xlabel, ylabel, true, fmt, null, null,
configurations, gnuplotFile);
}
public void plot2D(String dataFile, String outFile, String[] setNames, String xlabel, Double yrange,
String ylabel, String fmt) {
List<PlotConfiguration> configurations = oldParamsToPlotConfigs(setNames, yrange, null);
plot2DHelper(Plot2DType.LINES, dataFile, outFile, null, xlabel, ylabel, true, configurations, fmt);
}
public void plot2DImpulsesWithLabels(String dataFile, String outFile, String[] setNames, Double xrange, String xlabel,
Double yrange, String ylabel, String fmt) {
List<PlotConfiguration> configurations = oldParamsToPlotConfigs(setNames, yrange, null);
plot2DHelper(Plot2DType.IMPULSES, dataFile, outFile, xrange, xlabel, ylabel, true, configurations, fmt);
}
public void plot2D(String dataFile, String outFile, String[] setNames, String xlabel, Double yrange, String ylabel,
String fmt, Double sizeX, Double sizeY, Double[] yMaxes, String outputFile) {
List<PlotConfiguration> configurations = oldParamsToPlotConfigs(setNames, yrange, yMaxes);
plot2DHelper(Plot2DType.LINES, dataFile, outFile, sizeX, xlabel, ylabel, true, fmt, sizeX, sizeY,
configurations, outputFile);
}
public void plot2D(String dataFile, String outFile, String xlabel, String ylabel, String fmt,
Double sizeX, Double sizeY, List<PlotConfiguration> configurations, String gnuplotOutputFile) {
plot2DHelper(Plot2DType.LINES, dataFile, outFile, sizeX, xlabel, ylabel, true, fmt, sizeX, sizeY,
configurations, gnuplotOutputFile);
}
private void plot2DHelper(Plot2DType plotTyp, String dataFile, String outFile, Double xrange, String xlabel,
String ylabel, boolean showKey, List<PlotConfiguration> configurations, String fmt) {
plot2DHelper(plotTyp, dataFile, outFile, xrange, xlabel, ylabel, showKey, fmt, null, null, configurations, null);
}
/*
== null -> all graphs in the set have their own autoadjusted y ranges. can see maximum detail in each chart, but makes it difficult to compare across the set.
!= null -> all graphs are uniformly scaled to a yrange value. makes it easy to compare.
*/
/**
* Helps plot 2D data in a grid
*
* @param plotTyp IMPULSES plots vertical lines from xaxis to data -- for sparse plots
* LINES plots a curve connecting points -- for dense plots
* HEATMAP plots a grayscale heatmap plot -- for quick comparisons across many
* @param dataFile file with 2D (x,y) pair data, 2 NL separation between data sets
* @param outFile filename to write the output pdf or png image to
* @param xlabel x-axis label
* @param ylabel y-axis label
* @param showKey does the graph show a legend for the plotted points
* @param fmt "png" or "pdf" (default)
* @param explicitSizeX An X dimension size to use (rather than one computed on the plot configurations)
* @param explicitSizeY An Y dimension size to use (rather than one computed on the plot configurations)
* @param plotConfigurations A list of configurations for the plots to produce
* @param cmdFile An output file to which to write the gnuplot command produced by this function
*/
private void plot2DHelper(Plot2DType plotTyp, String dataFile, String outFile, Double xrange, String xlabel,
String ylabel, boolean showKey, String fmt, Double explicitSizeX, Double explicitSizeY,
List<PlotConfiguration> plotConfigurations, String cmdFile) {
int numDataSets = plotConfigurations.size();
// layout 1 column
int gridX = 1;
// layout n columns, unless graphs merged together using overlayed plots
int gridY = plotTyp.equals(Plot2DType.OVERLAYED_LINES) ? 1 : numDataSets;
// by default gnuplot plots pdfs to a XxY = 5x3 canvas (in inches)
// we need about 1.5 inch for each plot on the y-axis, so if there are
// more than 2 plots beings compared they tend to be squished.
// So we better adjust the size to 1.5 x 5 inches x #grid cells reqd
double sizeY = explicitSizeY != null ? explicitSizeY : (plotTyp == Plot2DType.HEATMAP ? 0.5 : 1.5) * gridY;
double sizeX = explicitSizeX != null ? explicitSizeX : 5 * gridX;
// fmt "pdf" or "png"
if ("png".equals(fmt)) {
// png format takes size in pixels, pdf takes it in inches
sizeY *= 144; // 144 dpi
sizeX *= 144; // 144 dpi
}
StringBuffer cmd = new StringBuffer();
if (!showKey)
cmd.append(" unset key;");
String fontscale = this.fontScale == null ? "" : String.format(" fontscale %.2f", this.fontScale);
if (plotTyp.equals(Plot2DType.HEATMAP)) {
cmd.append(" set view map;");
cmd.append(" set dgrid3d 2,1000;");
// cmd.append(" set palette defined ( 0 0 0 0, 1 1 1 1 );"); // white peaks on black bg
// cmd.append(" set palette defined ( 0 0 0 0, 1 1 0 0 );"); // red peaks on black bg
// cmd.append(" set palette defined ( 1 1 1 1, 1 1 0 0 );"); // red peaks on white bg
// cmd.append(" set palette defined ( 1 1 1 1, 1 0 0 0 );"); // black peaks on white bg
// cmd.append(" set palette defined ( 1 1 1 1, 1 0 0 1 );"); // blue peaks on white bg
// peak -> background = white, yellow, red, black
cmd.append(" set palette defined ( 0 0 0 0, 10 1 0 0, 20 1 1 0, 30 1 1 1, 40 1 1 1 );");
cmd.append(" unset ytics;"); // do not show the [1,2] proxy labels
}
if (xlabel != null)
cmd.append(" set xlabel \"" + xlabel + "\";");
if (ylabel != null)
cmd.append(" set ylabel \"" + ylabel + "\";");
cmd.append(" set terminal " + fmt + " size " + sizeX + "," + sizeY + fontscale + ";");
cmd.append(" set output \"" + outFile + "\";");
String scale = plotTyp.equals(Plot2DType.HEATMAP) ? " scale 1,1.4" : "";
cmd.append(" set multiplot layout " + gridY + ", 1" + scale + "; ");
if (!plotTyp.equals(Plot2DType.HEATMAP))
cmd.append("set lmargin at screen 0.15; ");
if (xrange != null)
cmd.append("set xrange [0:" + xrange + "]; ");
// Overlayed lines need to all share a single y range, so we'll take the max.
if (plotTyp.equals(Plot2DType.OVERLAYED_LINES)) {
Double maxY = 0.0;
for (PlotConfiguration config : plotConfigurations) {
maxY = Math.max(maxY, config.getYRange());
}
cmd.append("set yrange [0:" + maxY + "]; ");
}
if (plotTyp.equals(Plot2DType.HEATMAP)) {
// Crazy heatmap config parameters, found via experimentation.
cmd.append(" set pm3d at b;");
cmd.append(" unset colorbox;");
// With help from http://objectmix.com/graphics/778659-setting-textcolor-legend.html.
cmd.append(" set key tc rgb \"green\";");
cmd.append(" set key right;");
cmd.append(" unset tics;");
cmd.append(" set tmargin 0;");
cmd.append(" set bmargin 0;");
}
for (PlotConfiguration config : plotConfigurations) {
if (config.getKind() == PlotConfiguration.KIND.SEPARATOR || config.getKind() == PlotConfiguration.KIND.HEADER) {
cmd.append(" unset tics; unset border; plot (y = 0) ");
if (config.getKind() == PlotConfiguration.KIND.HEADER) {
cmd.append(" title \"" + config.getLabel() + "\";");
} else {
cmd.append(" notitle;");
}
cmd.append(" set border; ");
if (!plotTyp.equals(Plot2DType.HEATMAP)) {
cmd.append(" set tics;");
}
continue;
}
// If per-graph maxes are defined, set the y or z range before calling `plot`.
if (!plotTyp.equals(Plot2DType.OVERLAYED_LINES)) {
if (plotTyp.equals(Plot2DType.HEATMAP)) {
cmd.append(" set cbrange [0:" + config.getYRange() + "]; ");
} else {
// when we are drawing heatmaps, we are drawing them as flattened versions
// of 3D plots. The yrange there is a {0,1}. The z is the one with the real data
cmd.append(" set yrange [0:" + config.getYRange() + "]; ");
}
}
switch (plotTyp) {
case IMPULSES:
// Plot cmd(s): "plot dataset; plot dataset; plot dataset;"
cmd.append(" plot \"" + dataFile + "\" index " + config.getDataSetIndex());
cmd.append(" title \"" + sanitize(config.getLabel()) + "\" with impulses, ");
// to add labels we have to pretend to plot a different dataset
// but instead specify labels; this is because "with" cannot
// take both impulses and labels in the same plot
cmd.append("'' index " + config.getDataSetIndex());
cmd.append(" using 1:2:1 notitle with labels right offset -0.5,0 font ',3'; ");
break;
case LINES:
// Plot cmd(s): "plot dataset; plot dataset; plot dataset;"
cmd.append(" plot \"" + dataFile + "\" index " + config.getDataSetIndex());
cmd.append(" title \"" + sanitize(config.getLabel()) + "\" with lines;");
break;
case OVERLAYED_LINES:
// Plot cmd: "plot dataset, dataset, dataset;"
// The substantial difference between this case is that it plots a
// single plot (therefore the single "plot" compared to an additional
// "plot" for all iterations of the loop in other cases).
if (config.getDataSetIndex() == 0) cmd.append(" plot");
cmd.append(" \"" + dataFile + "\" index " + config.getDataSetIndex());
cmd.append(" title \"" + sanitize(config.getLabel()) + "\" with lines");
cmd.append(config.getDataSetIndex() == numDataSets - 1 ? ";" : ",");
break;
case HEATMAP:
// Plot cmd(s): "splot dataset; splot dataset; splot dataset;"
cmd.append(" splot \"" + dataFile + "\" index " + config.getDataSetIndex());
cmd.append(" title \"" + sanitize(config.getLabel()) + "\" with pm3d;");
}
}
cmd.append(" unset multiplot; set output;");
if (cmdFile != null) {
try (FileWriter fw = new FileWriter(new File(cmdFile))) {
fw.append(cmd.toString());
fw.append("\n");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
String[] plotCompare2D = new String[] { "gnuplot", "-e", cmd.toString() };
exec(plotCompare2D);
}
public void plot3D(String dataFile, String outFile, String srcNcFile, Double mz) {
// Gnuplot assumes LaTeX style for text, so when we put
// the file name in the label it get mathified. Escape _
// so that they dont get interpretted as subscript ops
String srcNcEsc = sanitize(srcNcFile);
StringBuffer cmd = new StringBuffer();
cmd.append(" set terminal pdf; set output \"" + outFile + "\";");
cmd.append(" set hidden3d; set dgrid 200,200; set xlabel \"m/z\";");
cmd.append(" set ylabel \"time in seconds\" offset -4,-1;");
cmd.append(" set zlabel \"intensity\" offset 2,7;");
cmd.append(" splot \"" + dataFile + "\" u 2:1:3 with lines");
cmd.append(" title \"" + srcNcEsc + " around mass " + mz + "\";");
String[] plot3DSurface = new String[] { "gnuplot", "-e", cmd.toString() };
exec(plot3DSurface);
}
public void plotMulti3D(String dataFile, String outFile, String fmt, String[] dataset_names, double maxz) {
int numDataSets = dataset_names.length;
int gridY = 1, gridX = numDataSets; // landscape layout n columns, 1 row
// So we better adjust the size to 5 inches x #grid cells reqd
double sizeY = 5 * gridY;
double sizeX = 5 * gridX;
if ("png".equals(fmt)) { // can be pdf
// png format takes size in pixels, pdf takes it in inches
sizeY *= 144; // 144 dpi
sizeX *= 144; // 144 dpi
}
StringBuffer cmd = new StringBuffer();
cmd.append(" set terminal " + fmt + " size " + sizeX + "," + sizeY + ";");
cmd.append(" set output \"" + outFile + "\";");
cmd.append(" set multiplot layout " + gridY + ", " + gridX + "; ");
for (int i = 0; i < numDataSets; i++) {
cmd.append(" set hidden3d; set dgrid 50,50; ");
cmd.append(" set xlabel \"m/z\";");
cmd.append(" unset xtics;"); // remove the xaxis labelling
cmd.append(" set ylabel \"time in seconds\";");
cmd.append(" set zlabel \"intensity\" offset 0,-12;");
if (maxz != -1) cmd.append(" set zrange [0:" + maxz + "]; ");
cmd.append(" splot \"" + dataFile + "\" index " + i + " u 2:1:3 with lines title \"" + sanitize(dataset_names[i]) + "\"; ");
}
cmd.append(" unset multiplot; set output;");
String[] plot3DMulti = new String[] { "gnuplot", "-e", cmd.toString() };
exec(plot3DMulti);
}
public void makeAnimatedGIF(String frames, String gifFile) {
// run the imagemagick convert utility to convert this into a animated GIF
// delay is specified in /100 of a second, so 20 is 0.2 seconds
String[] animatedGif = new String[] { "convert",
"-delay", "80",
"-loop", "1",
"-dispose", "previous",
frames,
gifFile
};
exec(animatedGif);
}
public Gnuplotter() { }
public Gnuplotter(Double fontScale) {
this.fontScale = fontScale;
}
private void exec(String[] cmd) {
Process proc = null;
try {
proc = Runtime.getRuntime().exec(cmd);
// read its input stream in case the process reports something
Scanner procSays = new Scanner(proc.getInputStream());
while (procSays.hasNextLine()) {
System.out.println(procSays.nextLine());
}
procSays.close();
// read the error stream in case the plotting failed
procSays = new Scanner(proc.getErrorStream());
while (procSays.hasNextLine()) {
System.err.println("E: " + procSays.nextLine());
}
procSays.close();
// wait for process to finish
proc.waitFor();
} catch (IOException e) {
System.err.println("ERROR: Cannot locate executable for " + cmd[0]);
System.err.println("ERROR: Rerun after installing: ");
System.err.println("If gnuplot you need: brew install gnuplot --with-qt --with-pdflib-lite");
System.err.println("If convert you need: brew install ghostscript; brew install imagemagick");
System.err.println("ERROR: ABORT!\n");
throw new RuntimeException("Required " + cmd[0] + " not in path");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (proc != null) {
proc.destroy();
}
}
}
}