/************************************************************************* * * * 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(); } } } }