Skip to content

Commit 897856b

Browse files
committed
split CompositePlotCLI to create script class
For the purpose of building out a GUI for the composite plot image generating tool (#96), the CLI class is broken up in this commit to separate out the input data parsing and chart building from command line parsing and input validation components in the CLI class. The new class organization further added minor adjustments to script behavior: - the default filepath suffix is now "_plot.png" - color defaults are determined by a null input and n>2 condition uses YEP color palette instead of Kelly colors - data parsing issues throw Exceptions rather than print statements - input/output extensions no longer restricted The new ColorSeries class saves color palettes and can be reused and expanded upon for future tools.
1 parent 3da23d5 commit 897856b

3 files changed

Lines changed: 220 additions & 158 deletions

File tree

src/cli/Figure_Generation/CompositePlotCLI.java

Lines changed: 26 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,21 @@
55
import picocli.CommandLine.Parameters;
66

77
import java.awt.Color;
8-
import java.lang.NullPointerException;
98
import java.io.File;
109
import java.io.IOException;
11-
import java.io.OutputStream;
12-
import java.io.FileOutputStream;
13-
import java.io.FileNotFoundException;
1410
import java.util.ArrayList;
15-
import java.util.Scanner;
1611
import java.util.concurrent.Callable;
1712
import java.util.regex.Matcher;
1813
import java.util.regex.Pattern;
1914

20-
import org.jfree.chart.JFreeChart;
21-
import org.jfree.chart.ChartUtils;
22-
import org.jfree.data.xy.XYSeriesCollection;
23-
import org.jfree.data.xy.XYSeries;
24-
25-
import charts.CompositePlot;
2615
import objects.ToolDescriptions;
27-
import util.ExtensionFileFilter;
16+
import scripts.Figure_Generation.PlotComposite;
2817

2918
/**
30-
* Figure_GenerationCLI/CompositePlotCLI
19+
* Command line interface class for scripts.Figure_Generation.PlotComposite to create line plot images based on the output files of scripts.Figure_Generation.TagPileup.
20+
* @author Olivia Lang
21+
* @see scripts.Figure_Generation.PlotComposite
22+
* @see scripts.Figure_Generation.TagPileup
3123
*/
3224
@Command(name = "composite-plot", mixinStandardHelpOptions = true,
3325
description = ToolDescriptions.composite_description,
@@ -38,10 +30,9 @@
3830
public class CompositePlotCLI implements Callable<Integer> {
3931

4032
@Parameters(index = "0", description = "Composite data to plot. (formatted like TagPileup composite output)")
41-
private File compositeData;
33+
private File inputComposite;
4234

43-
@Option(names = { "-o",
44-
"--output" }, description = "specify output filename, please use PNG extension\n(default=Input filename with \"_compositePlot.png\" appended to the name in working directory of ScriptManager")
35+
@Option(names = { "-o", "--output" }, description = "specify output filename, please use PNG extension\n(default=Input filename with \"_compositePlot.png\" appended to the name in working directory of ScriptManager")
4536
private File output = null;
4637
@Option(names = { "-t", "--title" }, description = "set title (default=<composite-file-name>)")
4738
private String title = null;
@@ -55,37 +46,11 @@ public class CompositePlotCLI implements Callable<Integer> {
5546
"--custom-colors" }, description = "indicate colors to use for each series. Must indicate a number of colors that matches number of dataseries\n"
5647
+ "default behavior:\n" + "if one series input, use black\n"
5748
+ "if two series input, use blue(sense) and red(anti)\n"
58-
+ "if greater than two series, cycle through a set of 20 preset colors.", arity = "1..")
49+
+ "if greater than two series, cycle through a set of 10 preset colors based on Rossi et al, 2021 (PMID:33692541).", arity = "1..")
5950
private String[] colors = null;
6051

61-
XYSeriesCollection xydata = null;
6252
private ArrayList<Color> COLORS = new ArrayList<Color>();
6353

64-
// Colors copied from response on StackOverflow:
65-
// https://stackoverflow.com/questions/470690/how-to-automatically-generate-n-distinct-colors
66-
String[] KELLY_COLORS_HEX = { "0xFFB300", // Vivid Yellow
67-
"0x803E75", // Strong Purple
68-
"0xFF6800", // Vivid Orange
69-
"0xA6BDD7", // Very Light Blue
70-
"0xC10020", // Vivid Red
71-
"0xCEA262", // Grayish Yellow
72-
"0x817066", // Medium Gray
73-
// The following don't work well for people with defective color vision
74-
"0x007D34", // Vivid Green
75-
"0xF6768E", // Strong Purplish Pink
76-
"0x00538A", // Strong Blue
77-
"0xFF7A5C", // Strong Yellowish Pink
78-
"0x53377A", // Strong Violet
79-
"0xFF8E00", // Vivid Orange Yellow
80-
"0xB32851", // Strong Purplish Red
81-
"0xF4C800", // Vivid Greenish Yellow
82-
"0x7F180D", // Strong Reddish Brown
83-
"0x93AA00", // Vivid Yellowish Green
84-
"0x593315", // Deep Yellowish Brown
85-
"0xF13A13", // Vivid Reddish Orange
86-
"0x232C16", // Dark Olive Green
87-
};
88-
8954
@Override
9055
public Integer call() throws Exception {
9156
System.err.println(">CompositePlotCLI.call()");
@@ -97,98 +62,52 @@ public Integer call() throws Exception {
9762
}
9863

9964
// Generate Composite Plot
100-
JFreeChart chart = CompositePlot.createChart(xydata, title, COLORS, legend);
101-
// Save Composite Plot
102-
OutputStream OUT = new FileOutputStream(output);
103-
ChartUtils.writeChartAsPNG(OUT, chart, pixelWidth, pixelHeight);
65+
PlotComposite.plotCompositeFile(inputComposite, output, true, title, COLORS, legend, pixelWidth, pixelHeight);
10466

10567
System.err.println("Image Generated.");
10668
return (0);
10769
}
10870

71+
/**
72+
* Validate input values and create user-readable error messages.
73+
* @return
74+
* @throws IOException
75+
*/
10976
private String validateInput() throws IOException {
11077
String r = "";
11178

112-
// check inputs exist
113-
if (!compositeData.exists()) {
114-
r += "(!)Composite Data file does not exist: " + compositeData.getName() + "\n";
115-
return (r);
116-
}
117-
// check input extensions
118-
if (!"out".equals(ExtensionFileFilter.getExtension(compositeData))) {
119-
r += "(!)Is this a \".out\" file? Check extension: " + compositeData.getName() + "\n";
79+
// Input file check
80+
if (!inputComposite.exists()) {
81+
r += "(!)Composite Data file does not exist: " + inputComposite.getName() + "\n";
82+
} else if (inputComposite.isDirectory()) {
83+
r += "(!)Composite Data file is a directory: " + inputComposite.getName() + "\n";
12084
}
121-
// set default output filename
122-
if (output == null) {
123-
output = new File(ExtensionFileFilter.stripExtension(compositeData) + "_compositePlot.png");
124-
// check output filename is valid
125-
} else {
126-
// check ext
127-
try {
128-
if (!"png".equals(ExtensionFileFilter.getExtension(output))) {
129-
r += "(!)Use PNG extension for output filename. Try: " + ExtensionFileFilter.stripExtension(output)
130-
+ ".png\n";
131-
}
132-
} catch (NullPointerException e) {
133-
r += "(!)Output filename must have extension: use PNG extension for output filename. Try: "
134-
+ ExtensionFileFilter.stripExtension(output) + ".png\n";
135-
}
136-
// check directory
85+
86+
// Output file check
87+
if (output != null) {
13788
if (output.getParent() == null) {
13889
// System.err.println("default to current directory");
13990
} else if (!new File(output.getParent()).exists()) {
14091
r += "(!)Check output directory exists: " + output.getParent() + "\n";
14192
}
14293
}
14394

144-
// check pixel ranges are valid
95+
// Pixel ranges must be valid
14596
if (pixelHeight <= 0) {
14697
r += "(!)Cell height must be a positive integer value! check \"-y\" flag.\"";
14798
}
14899
if (pixelWidth <= 0) {
149100
r += "(!)Cell width must be a positive integer value! check \"-x\" flag.\"";
150101
}
151102

152-
// Parse Datafile
153-
try {
154-
xydata = parseData();
155-
if (xydata == null) {
156-
r += "(!)The number of y-values don't match the number of x-values";
157-
}
158-
} catch (FileNotFoundException e) {
159-
e.printStackTrace();
160-
}
161-
162-
// Set Color
163-
if (colors == null) { // set defaults based on number of dataseries (n=1 -> black, n=2 -> blue,red,
164-
// n=3 -> cycle through kelly colors)
165-
if (xydata.getSeriesCount() == 1) {
166-
// set color to black default unless otherwise indicated
167-
COLORS.add(Color.BLACK);
168-
} else if (xydata.getSeriesCount() == 2) {
169-
// set colors to blue and red default unless otherwise indicated
170-
COLORS.add(Color.BLUE);
171-
COLORS.add(Color.RED);
172-
} else if (xydata.getSeriesCount() > 2) {
173-
// assign a diverse set of colors (as many as there are series)
174-
for (int i = 0; i < xydata.getSeriesCount(); i++) {
175-
int index = i % KELLY_COLORS_HEX.length;
176-
COLORS.add(Color.decode(KELLY_COLORS_HEX[index]));
177-
}
178-
}
179-
} else { // set user specified values and check they match number of data series
180-
// assert custom colors have been assigned and length matches number of series
181-
if (colors.length != xydata.getSeriesCount()) {
182-
r += "(!)Number of colors specified(" + colors.length + ") must match number of dataseries("
183-
+ xydata.getSeriesCount() + ")\n";
184-
}
103+
// Set Colors (customized)
104+
if (colors != null) {
185105
// check color input format and decode
186106
Pattern hexColorPat = Pattern.compile("[0-9A-Fa-f]{6}");
187107
for (int i = 0; i < colors.length; i++) {
188108
Matcher m = hexColorPat.matcher(colors[i]);
189109
if (!m.matches()) {
190-
r += "(!)Color(" + colors[i]
191-
+ ") must be formatted as a hexidecimal String!\n\tExpected input string format: \"[0-9A-Fa-f]{6}\"\n";
110+
r += "(!)Color(" + colors[i] + ") must be formatted as a hexidecimal String!\n\tExpected input string format: \"[0-9A-Fa-f]{6}\"\n";
192111
} else {
193112
System.err.println("Decoding color: 0x" + colors[i]);
194113
COLORS.add(Color.decode("0x" + colors[i]));
@@ -197,55 +116,4 @@ private String validateInput() throws IOException {
197116
}
198117
return (r);
199118
}
200-
201-
public XYSeriesCollection parseData() throws FileNotFoundException {
202-
// parse input into XYDataset obj
203-
XYSeriesCollection dataset = new XYSeriesCollection();
204-
Scanner scan = new Scanner(compositeData);
205-
// parse x values
206-
String[] tokens = scan.nextLine().split("\t");
207-
if (!tokens[0].equals("")) {
208-
scan.close();
209-
System.err.println("(!) First row of input file must have an empty first column (as x-values)");
210-
return null;
211-
}
212-
double[] x = new double[tokens.length - 1];
213-
for (int i = 1; i < tokens.length; i++) {
214-
x[i - 1] = Double.parseDouble(tokens[i]);
215-
}
216-
217-
XYSeries s;
218-
// line-by-line through file
219-
while (scan.hasNextLine()) {
220-
tokens = scan.nextLine().split("\t");
221-
// check for format consistency: number of x-values matches y-values
222-
if (tokens.length - 1 != x.length) {
223-
System.err.println("(!) Check number of x-values matches number of y-values");
224-
scan.close();
225-
return null;
226-
}
227-
// skip any rows with blank labels
228-
if (tokens[0].equals("")) {
229-
for (int i = 1; i < tokens.length; i++) {
230-
if (x[i - 1] != Double.parseDouble(tokens[i])) {
231-
System.err.println(x[i - 1]);
232-
System.err.println(tokens[i]);
233-
System.err.println("(!) Check dataseries based on same x-scale file");
234-
scan.close();
235-
return null;
236-
}
237-
}
238-
continue;
239-
}
240-
// initialize series based on rowname column of input file
241-
s = new XYSeries(tokens[0]);
242-
// parse y-values of current row
243-
for (int i = 1; i < tokens.length; i++) {
244-
s.add(x[i - 1], Double.parseDouble(tokens[i]));
245-
}
246-
dataset.addSeries(s);
247-
}
248-
scan.close();
249-
return (dataset);
250-
}
251119
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package scripts.Figure_Generation;
2+
3+
import java.awt.Color;
4+
import java.io.File;
5+
import java.io.FileNotFoundException;
6+
import java.io.FileOutputStream;
7+
import java.io.IOException;
8+
import java.util.ArrayList;
9+
import java.util.Scanner;
10+
11+
import org.jfree.chart.ChartUtils;
12+
import org.jfree.chart.JFreeChart;
13+
import org.jfree.data.xy.XYSeries;
14+
import org.jfree.data.xy.XYSeriesCollection;
15+
16+
import charts.CompositePlot;
17+
import util.ColorSeries;
18+
import util.ExtensionFileFilter;
19+
20+
/**
21+
* The script class to create/display line plot images based on the output files of scripts.Figure_Generation.TagPileup.
22+
* @author Olivia Lang
23+
* @see util.ColorSeries
24+
* @see charts.CompositePlot
25+
* @see cli.Figure_Generation.CompositePlotCLI
26+
*/
27+
public class PlotComposite {
28+
29+
/**
30+
* Static method that parses the input composite data (formatted like output of TagPileup), determines the default color palette if none specified, and plots the line chart, saving the image file to the appropriate output if indicated.
31+
* @param input a tab-delimited file containing the composite information in the format of scripts.Figure_Generation.TagPileup's output
32+
* @param OUT_PATH filepath to save composite image to. if null, defaults to &lt;InputWithoutExtension&gt;_plot.png.
33+
* @param outputImage to save image (true) or not (false)
34+
* @param title the string to include at the top of the line chart
35+
* @param COLORS the color palette list of colors to plot. If null, then a different color palette is chosen based on the number of lines parsed from the composite input file: if n=1, then black is used, if n=2, then the first plot is blue and the second is red, if n>2, then the YEP color pallete is used.
36+
* @param legend to include the legend in the chart (true) or not (false)
37+
* @param pxHeight height of image to save
38+
* @param pxWidth width of image to save
39+
* @return
40+
* @throws IOException
41+
* @throws IllegalArgumentException
42+
* @throws FileNotFoundException
43+
*/
44+
public static JFreeChart plotCompositeFile(File input, File OUT_PATH, boolean outputImage, String title, ArrayList<Color> COLORS, boolean legend, int pxHeight, int pxWidth) throws IOException, IllegalArgumentException, FileNotFoundException {
45+
Scanner scan = new Scanner(input);
46+
// parse x values
47+
String[] tokens = scan.nextLine().split("\t");
48+
if (!tokens[0].equals("")) {
49+
scan.close();
50+
throw new IllegalArgumentException("(!) First row of input file must have an empty first column (as x-values)");
51+
}
52+
double[] x = new double[tokens.length - 1];
53+
for (int i = 1; i < tokens.length; i++) {
54+
x[i - 1] = Double.parseDouble(tokens[i]);
55+
}
56+
57+
// parse input into XYDataset obj
58+
XYSeriesCollection dataset = new XYSeriesCollection();
59+
60+
XYSeries s;
61+
// line-by-line through file
62+
while (scan.hasNextLine()) {
63+
tokens = scan.nextLine().split("\t");
64+
// check for format consistency: number of x-values matches y-values
65+
if (tokens.length - 1 != x.length) {
66+
scan.close();
67+
throw new IllegalArgumentException("(!) Check number of x-values matches number of y-values");
68+
}
69+
// skip any rows with blank labels
70+
if (tokens[0].equals("")) {
71+
for (int i = 1; i < tokens.length; i++) {
72+
if (x[i - 1] != Double.parseDouble(tokens[i])) {
73+
System.err.println(x[i - 1]);
74+
System.err.println(tokens[i]);
75+
scan.close();
76+
throw new IllegalArgumentException("(!) Check dataseries based on same x-scale file");
77+
}
78+
}
79+
continue;
80+
}
81+
// initialize series based on rowname column of input file
82+
s = new XYSeries(tokens[0]);
83+
// parse y-values of current row
84+
for (int i = 1; i < tokens.length; i++) {
85+
s.add(x[i - 1], Double.parseDouble(tokens[i]));
86+
}
87+
dataset.addSeries(s);
88+
}
89+
scan.close();
90+
91+
// Set-up colors
92+
if (COLORS==null) {
93+
COLORS = new ArrayList<Color>(2);
94+
if (dataset.getSeriesCount() == 1) { // n=1, default to black
95+
COLORS.add(Color.BLACK);
96+
} else if (dataset.getSeriesCount() == 2) { // n=2, default to red & blue XO colors
97+
COLORS = ColorSeries.InitializeXOColors();
98+
} else if (dataset.getSeriesCount() > 2) { // n>2, default to yep colors
99+
COLORS = ColorSeries.InitializeYEPColors();
100+
}
101+
// assert custom colors have been assigned and length matches number of series
102+
} else if (COLORS.size() != dataset.getSeriesCount()) {
103+
System.err.println("(Caution!) Number of colors specified(" + COLORS.size() + ") doesnt match number of dataseries(" + dataset.getSeriesCount() + ")\n");
104+
}
105+
106+
// Make chart
107+
JFreeChart chart = CompositePlot.createChart(dataset, title, COLORS, legend);
108+
109+
// Save Composite Plot
110+
if (outputImage) {
111+
// set default output filename
112+
if (OUT_PATH == null) {
113+
OUT_PATH = new File(ExtensionFileFilter.stripExtension(input) + "_plot.png");
114+
}
115+
ChartUtils.writeChartAsPNG(new FileOutputStream(OUT_PATH), chart, pxWidth, pxHeight);
116+
}
117+
118+
return (chart);
119+
}
120+
}

0 commit comments

Comments
 (0)