Commit 546b3b47e049601250c8f6b0822c1998f4609465
Merge pull request #395 from biometrics/plotting
Plotting customization
Showing
2 changed files
with
189 additions
and
177 deletions
docs/docs/api_docs/c_api/functions.md
| ... | ... | @@ -595,14 +595,15 @@ In order of their output, the figures are: |
| 595 | 595 | 1. Metadata table |
| 596 | 596 | 2. Receiver Operating Characteristic (ROC) |
| 597 | 597 | 3. Detection Error Tradeoff (DET) |
| 598 | -4. Score Distribution (SD) histogram | |
| 599 | -5. True Accept Rate Bar Chart (BC) | |
| 600 | -6. Cumulative Match Characteristic (CMC) | |
| 601 | -7. Error Rate (ERR) curve | |
| 598 | +4. Identification Error Tradeoff (IET) | |
| 599 | +5. Cumulative Match Characteristic (CMC) | |
| 600 | +6. Score Distribution (SD) histogram | |
| 601 | +7. True Accept Rate Bar Chart (BC) | |
| 602 | +8. Error Rate (ERR) curve | |
| 602 | 603 | |
| 603 | 604 | Two files will be created: |
| 604 | 605 | * **destination.R** which is the auto-generated R script used to render the figures. |
| 605 | - * **destination.pdf** which has all of the figures in one file multi-page file. | |
| 606 | + * **destination.pdf** which has all of the figures in one file multi-page file (note that **destination%1d.png** will output each figure to a separate file). | |
| 606 | 607 | |
| 607 | 608 | OpenBR uses file and folder names to automatically determine the plot legend. |
| 608 | 609 | For example, let's consider the case where three algorithms (<tt>A</tt>, <tt>B</tt>, & <tt>C</tt>) were each evaluated on two datasets (<tt>Y</tt> & <tt>Z</tt>). |
| ... | ... | @@ -611,6 +612,37 @@ The '<tt>_</tt>' character plays a special role in determining the legend title( |
| 611 | 612 | In this case, <tt>A</tt>, <tt>B</tt>, & <tt>C</tt> will be identified as different values of type <tt>Algorithm</tt>, and each will be assigned its own color; <tt>Y</tt> & <tt>Z</tt> will be identified as different values of type Dataset, and each will be assigned its own line style. |
| 612 | 613 | Matches around the EER will be displayed if the matches parameter is set in [br_eval](#br_eval). |
| 613 | 614 | |
| 615 | +It is possible to customize some aspects of your plots using the [File](../cpp_api/file/file.md) key/value metadata convention; possible keys are described below. | |
| 616 | + | |
| 617 | + | |
| 618 | +Key | Value | Description | |
| 619 | +--- | ---- | ----------- | |
| 620 | +smooth | [QString] | The file pivot to average across evaluation splits. Typically "Dataset" if using the folder name from above. | |
| 621 | +ncol | int | Number of columns in plot legends. | |
| 622 | +confidence | float | Confidence interval calculated for smooth, defaults to `0.95` | |
| 623 | +metadata | bool | Optional plot metadata, defaults to `true` | |
| 624 | +csv | bool | Optional output metadata tables to csv, defaults to `false` | |
| 625 | +\*Options | [QStringList] | Key/value list of options for a specific plot. Plots include "roc", "det", "iet", "cmc" | |
| 626 | + | |
| 627 | +Specific plot options are described below: | |
| 628 | + | |
| 629 | +Key | Value | Description | |
| 630 | +--- | ---- | ----------- | |
| 631 | +title | [QString] | Plot title | |
| 632 | +size | float | Line width | |
| 633 | +legendPosition | [QPointF] | Legend coordinates on plot, ex. legendPosition=(X,Y) | |
| 634 | +textSize | float | Size of text for title, legend and axes | |
| 635 | +xTitle/yTitle | [QString] | Title for x/y axis | |
| 636 | +xLog/yLog | bool | Plot log scale for x/y axis | |
| 637 | +xLimits/yLimits | [QPointF] | Set x/y axis limits, ex. xLimits=(lower,upper) | |
| 638 | +xLabels/yLabels | [QString] | Labels for ticks on x/y axis, ex. xLabeles=percent or xLabels=c(1,5,10) | |
| 639 | +xBreaks/yBreaks | [QString] | Specify breaks/ticks on x/y axis, ex. xBreaks=pretty_breaks(n=10) or xBreaks=c(1,5,10) | |
| 640 | + | |
| 641 | +If specifying plot options it is a good idea to wrap the destination file in single quotes to avoid parsing errors. | |
| 642 | +The example below plots plots the six br_eval results in the Algorithm_Dataset folder described above, sets the number of legend columns and specifies some options for the CMC plot. | |
| 643 | + | |
| 644 | +`br -plot Algorithm_Dataset/* 'destination.pdf[ncol=3,cmcOptions=[xLog=false,xLimits=(1,20),xBreaks=pretty_breaks(n=10),xTitle=Ranks 1 through 20]]'` | |
| 645 | + | |
| 614 | 646 | This function requires a current [R][R] installation with the following packages: |
| 615 | 647 | |
| 616 | 648 | install.packages(c("ggplot2", "gplots", "reshape", "scales", "jpg", "png")) |
| ... | ... | @@ -1395,3 +1427,6 @@ Close a provided [Gallery](../cpp_api/gallery/gallery.md). |
| 1395 | 1427 | <!-- Links --> |
| 1396 | 1428 | [R]: http://www.r-project.org/ "R" |
| 1397 | 1429 | [QRegExp]: http://doc.qt.io/qt-5/QRegExp.html "QRegExp" |
| 1430 | +[QString]: http://doc.qt.io/qt-5/QString.html "QString" | |
| 1431 | +[QStringList]: http://doc.qt.io/qt-5/QStringList.html "QStringList" | |
| 1432 | +[QPointF]: http://doc.qt.io/qt-5/QPointF.html "QPointF" | ... | ... |
openbr/core/plot.cpp
| ... | ... | @@ -42,20 +42,6 @@ static QString getScale(const QString &mode, const QString &title, int vals) |
| 42 | 42 | // Custom sorting method to ensure datasets are ordered nicely |
| 43 | 43 | static bool sortFiles(const QString &fileA, const QString &fileB) |
| 44 | 44 | { |
| 45 | - QFileInfo fileInfoA(fileA); | |
| 46 | - QFileInfo fileInfoB(fileB); | |
| 47 | - | |
| 48 | - if (fileInfoA.fileName().contains("Good")) return true; | |
| 49 | - if (fileInfoB.fileName().contains("Good")) return false; | |
| 50 | - if (fileInfoA.fileName().contains("Bad")) return true; | |
| 51 | - if (fileInfoB.fileName().contains("Bad")) return false; | |
| 52 | - if (fileInfoA.fileName().contains("Ugly")) return true; | |
| 53 | - if (fileInfoB.fileName().contains("Ugly")) return false; | |
| 54 | - if (fileInfoA.fileName().contains("MEDS")) return true; | |
| 55 | - if (fileInfoB.fileName().contains("MEDS")) return false; | |
| 56 | - if (fileInfoA.fileName().contains("PCSO")) return true; | |
| 57 | - if (fileInfoB.fileName().contains("PCSO")) return false; | |
| 58 | - | |
| 59 | 45 | return fileA < fileB; |
| 60 | 46 | } |
| 61 | 47 | |
| ... | ... | @@ -65,7 +51,8 @@ struct RPlot |
| 65 | 51 | QFile file; |
| 66 | 52 | QStringList pivotHeaders; |
| 67 | 53 | QVector< QSet<QString> > pivotItems; |
| 68 | - float confidence; | |
| 54 | + float confidence; // confidence interval for plotting across splits | |
| 55 | + int ncol; // Number of columns for plot legends | |
| 69 | 56 | |
| 70 | 57 | bool flip; |
| 71 | 58 | |
| ... | ... | @@ -124,6 +111,7 @@ struct RPlot |
| 124 | 111 | } |
| 125 | 112 | file.write("data <- rbind(data, tmp)\n"); |
| 126 | 113 | } |
| 114 | + | |
| 127 | 115 | for (int i=0; i<pivotItems.size(); i++) { |
| 128 | 116 | const int size = pivotItems[i].size(); |
| 129 | 117 | if (size > major.size) { |
| ... | ... | @@ -133,13 +121,10 @@ struct RPlot |
| 133 | 121 | minor = Pivot(i, size, pivotHeaders[i]); |
| 134 | 122 | } |
| 135 | 123 | } |
| 124 | + | |
| 136 | 125 | const QString &smooth = destination.get<QString>("smooth", ""); |
| 137 | - if (destination.contains(QString("confidence"))) { | |
| 138 | - const QString &CI = destination.get<QString>("confidence"); | |
| 139 | - confidence = !CI.isEmpty() ? CI.toFloat()/100.0 : 0.95; | |
| 140 | - } else { | |
| 141 | - confidence = 0.95; | |
| 142 | - } | |
| 126 | + confidence = destination.get<float>("confidence", 95) / 100.0; | |
| 127 | + | |
| 143 | 128 | major.smooth = !smooth.isEmpty() && (major.header == smooth) && (major.size > 1); |
| 144 | 129 | minor.smooth = !smooth.isEmpty() && (minor.header == smooth) && (minor.size > 1); |
| 145 | 130 | if (major.smooth) major.size = 1; |
| ... | ... | @@ -147,6 +132,7 @@ struct RPlot |
| 147 | 132 | if (major.size < minor.size) |
| 148 | 133 | std::swap(major, minor); |
| 149 | 134 | |
| 135 | + ncol = destination.get<int>("ncol", major.size > 1 ? major.size : (minor.header.isEmpty() ? major.size : minor.size)); | |
| 150 | 136 | flip = minor.header == "Algorithm"; |
| 151 | 137 | // Format data |
| 152 | 138 | if (isEvalFormat) |
| ... | ... | @@ -187,7 +173,7 @@ struct RPlot |
| 187 | 173 | "TS$Y <- as.character(TS$Y)\n" |
| 188 | 174 | "CMC$Y <- as.numeric(as.character(CMC$Y))\n" |
| 189 | 175 | "\n" |
| 190 | - "if (%1) {\n\tsummarySE <- function(data=NULL, measurevar, groupvars=NULL, na.rm=FALSE, conf.interval=%7, .drop=TRUE) {\n\t\t" | |
| 176 | + "if (%1) {\n\tsummarySE <- function(data=NULL, measurevar, groupvars=NULL, na.rm=FALSE, conf.interval=%3, .drop=TRUE) {\n\t\t" | |
| 191 | 177 | "require(plyr)\n\n\t\tlength2 <- function (x, na.rm=FALSE) {\n\t\t\tif (na.rm) sum(!is.na(x))\n\t\t\telse length(x)" |
| 192 | 178 | "\n\t\t}\n\n\t\tdatac <- ddply(data, groupvars, .drop=.drop, .fun = function(xx, col) {\n\t\t\t" |
| 193 | 179 | "c(N=length2(xx[[col]], na.rm=na.rm), mean=mean(xx[[col]], na.rm=na.rm), sd=sd(xx[[col]], na.rm=na.rm))\n\t\t\t}," |
| ... | ... | @@ -197,6 +183,7 @@ struct RPlot |
| 197 | 183 | "datac$lower <- if(datac[, measurevar] - datac$ci > 0) (datac[, measurevar] - datac$ci) else 0\n\n\t\treturn(datac)\n\t}\n\t" |
| 198 | 184 | "DET <- summarySE(DET, measurevar=\"Y\", groupvars=c(\"%2\", \"X\"))\n\t" |
| 199 | 185 | "IET <- summarySE(IET, measurevar=\"Y\", groupvars=c(\"%2\", \"X\"))\n\t" |
| 186 | + "CMC <- summarySE(CMC, measurevar=\"Y\", groupvars=c(\"%2\", \"X\"))\n\t" | |
| 200 | 187 | "ERR <- summarySE(ERR, measurevar=\"X\", groupvars=c(\"Error\", \"%2\", \"Y\"))\n\t" |
| 201 | 188 | "TF <- summarySE(TF, measurevar=\"Y\", groupvars=c(\"%2\", \"X\"))\n\t" |
| 202 | 189 | "FT <- summarySE(FT, measurevar=\"Y\", groupvars=c(\"%2\", \"X\"))\n\t" |
| ... | ... | @@ -204,82 +191,112 @@ struct RPlot |
| 204 | 191 | "# Code to format FAR values\n" |
| 205 | 192 | "far_names <- list('0.001'=\"FAR = 0.1%\", '0.01'=\"FAR = 1%\")\n" |
| 206 | 193 | "far_labeller <- function(variable,value) { return(far_names[as.character(value)]) }\n" |
| 207 | - "\n" | |
| 208 | - "# Code to format TAR@FAR table\n" | |
| 209 | - "algs <- unique(%6)\n" | |
| 210 | - "algs <- algs[!duplicated(algs)]\n" | |
| 211 | - "mat <- matrix(%3,nrow=6,ncol=length(algs),byrow=FALSE)\n" | |
| 212 | - "colnames(mat) <- algs \n" | |
| 213 | - "rownames(mat) <- c(\"FAR = 1e-06\", \"FAR = 1e-05\", \"FAR = 1e-04\", \"FAR = 1e-03\", \"FAR = 1e-02\", \"FAR = 1e-01\")\n" | |
| 214 | - "FTtable <- as.table(mat)\n" | |
| 215 | - "\n" | |
| 216 | - "# Code to format FAR@TAR table\n" | |
| 217 | - "mat <- matrix(%4,nrow=6,ncol=length(algs),byrow=FALSE)\n" | |
| 218 | - "colnames(mat) <- algs \n" | |
| 219 | - "rownames(mat) <- c(\"TAR = 0.40\", \"TAR = 0.50\", \"TAR = 0.65\", \"TAR = 0.75\", \"TAR = 0.85\", \"TAR = 0.95\")\n" | |
| 220 | - "F_at_Ttable <- as.table(mat)\n" | |
| 221 | - "\n" | |
| 222 | - "# Code to format CMC Table\n" | |
| 223 | - "mat <- matrix(%5,nrow=6,ncol=length(algs),byrow=FALSE)\n" | |
| 224 | - "colnames(mat) <- algs \n" | |
| 225 | - "rownames(mat) <- c(\" Rank 1\", \"Rank 5\", \"Rank 10\", \"Rank 20\", \"Rank 50\", \"Rank 100\")\n" | |
| 226 | - "CMCtable <- as.table(mat)\n" | |
| 227 | - "\n" | |
| 228 | - "# Code to format Template Size Table\n" | |
| 229 | - "if (nrow(TS) != 0) {\n\t" | |
| 230 | - "mat <- matrix(TS$Y,nrow=1,ncol=length(algs),byrow=FALSE)\n\t" | |
| 231 | - "colnames(mat) <- algs\n\t" | |
| 232 | - "rownames(mat) <- c(\"Template Size (bytes):\")\n\t" | |
| 233 | - "TStable <- as.table(mat)\n}\n").arg(((major.smooth || minor.smooth) ? "TRUE" : "FALSE"), major.size > 1 ? major.header : (minor.header.isEmpty() ? major.header : minor.header), | |
| 234 | - (major.smooth || minor.smooth) && confidence != 0 ? "paste(as.character(round(TF$Y, 3)), round(TF$ci, 3), sep=\"\\u00b1\")" : "TF$Y", | |
| 235 | - (major.smooth || minor.smooth) && confidence != 0 ? "paste(as.character(round(FT$Y, 3)), round(FT$ci, 3), sep=\"\\u00b1\")" : "FT$Y", | |
| 236 | - (major.smooth || minor.smooth) && confidence != 0 ? "paste(as.character(round(CT$Y, 3)), round(CT$ci, 3), sep=\"\\u00b1\")" : "CT$Y", | |
| 237 | - (major.size > 1 && minor.size > 1) && !(major.smooth || minor.smooth) ? QString("paste(TF$%1, TF$%2, sep=\"_\")").arg(major.header, minor.header) : QString("TF$%1").arg(major.size > 1 ? major.header : (minor.header.isEmpty() ? major.header : minor.header)), | |
| 238 | - QString::number(confidence)))); | |
| 194 | + "\n").arg((major.smooth || minor.smooth) ? "TRUE" : "FALSE", | |
| 195 | + major.size > 1 ? major.header : (minor.header.isEmpty() ? major.header : minor.header), | |
| 196 | + QString::number(confidence)))); | |
| 239 | 197 | |
| 240 | 198 | // Open output device |
| 241 | 199 | file.write(qPrintable(QString("\n" |
| 242 | 200 | "# Open output device\n" |
| 243 | - "%1(\"%2.%1\")\n").arg(suffix, basename))); | |
| 244 | - | |
| 245 | - // Write metadata table | |
| 246 | - if ((suffix == "pdf") && isEvalFormat) { | |
| 247 | - file.write("\n" | |
| 248 | - "# Write metadata table\n"); | |
| 249 | - QString textplot = "MT <- as.data.frame(Metadata[c(1,2,3,4,5),])\n" | |
| 250 | - "par(mfrow=c(4,1))\n" | |
| 251 | - "plot.new()\n" | |
| 252 | - "print(title(paste(\"%1 - %2\",date(),sep=\"\\n\")))\n" | |
| 253 | - "mat <- matrix(MT$X[c(1,2)],ncol=2)\n" | |
| 254 | - "colnames(mat) <- c(\"Gallery\", \"Probe\")\n" | |
| 255 | - "imageTable <- as.table(mat)\n" | |
| 256 | - "print(textplot(imageTable,show.rownames=FALSE))\n" | |
| 257 | - "print(title(\"Images\"))\n" | |
| 258 | - "mat <- matrix(MT$X[c(3,4,5)],ncol=3)\n" | |
| 259 | - "colnames(mat) <- c(\"Genuine\", \"Impostor\", \"Ignored\")\n" | |
| 260 | - "matchTable <- as.table(mat)\n" | |
| 261 | - "print(textplot(matchTable,show.rownames=FALSE))\n" | |
| 262 | - "print(title(\"Matches\"))\n" | |
| 263 | - "plot.new()\n" | |
| 264 | - "print(title(\"Gallery * Probe = Genuine + Impostor + Ignored\"))\n" | |
| 265 | - "plot.new()\n" | |
| 266 | - "print(textplot(FTtable))\n" | |
| 267 | - "print(title(\"Table of True Accept Rates at various False Accept Rates\"))\n" | |
| 268 | - "print(textplot(F_at_Ttable))\n" | |
| 269 | - "print(title(\"Table of False Accept Rates at various True Accept Rates\"))\n" | |
| 270 | - "print(textplot(CMCtable))\n" | |
| 271 | - "print(title(\"Table of retrieval rate at various ranks\"))\n" | |
| 272 | - "if (nrow(TS) != 0) {\n\t" | |
| 273 | - "print(textplot(TStable, cex=1.15))\n\t" | |
| 274 | - "print(title(\"Template Size by Algorithm\"))\n}\n"; | |
| 275 | - file.write(qPrintable(textplot.arg(PRODUCT_NAME, PRODUCT_VERSION))); | |
| 276 | - } | |
| 201 | + "%1(\"%2.%1\"%3)\n").arg(suffix, basename, suffix != "pdf" ? ", width=800, height=800" : ""))); | |
| 277 | 202 | |
| 278 | 203 | // Write figures |
| 279 | 204 | file.write("\n" |
| 280 | 205 | "# Write figures\n"); |
| 281 | 206 | } |
| 282 | 207 | |
| 208 | + void plotMetadata(bool csv) | |
| 209 | + { | |
| 210 | + file.write(qPrintable(QString("# Code to format TAR@FAR table\n" | |
| 211 | + "algs <- unique(%4)\n" | |
| 212 | + "algs <- algs[!duplicated(algs)]\n" | |
| 213 | + "mat <- matrix(%1,nrow=6,ncol=length(algs),byrow=FALSE)\n" | |
| 214 | + "colnames(mat) <- algs \n" | |
| 215 | + "rownames(mat) <- c(\"FAR = 1e-06\", \"FAR = 1e-05\", \"FAR = 1e-04\", \"FAR = 1e-03\", \"FAR = 1e-02\", \"FAR = 1e-01\")\n" | |
| 216 | + "TFtable <- as.table(mat)\n" | |
| 217 | + "\n" | |
| 218 | + "# Code to format FAR@TAR table\n" | |
| 219 | + "mat <- matrix(%2,nrow=6,ncol=length(algs),byrow=FALSE)\n" | |
| 220 | + "colnames(mat) <- algs \n" | |
| 221 | + "rownames(mat) <- c(\"TAR = 0.40\", \"TAR = 0.50\", \"TAR = 0.65\", \"TAR = 0.75\", \"TAR = 0.85\", \"TAR = 0.95\")\n" | |
| 222 | + "FTtable <- as.table(mat)\n" | |
| 223 | + "\n" | |
| 224 | + "# Code to format CMC Table\n" | |
| 225 | + "mat <- matrix(%3,nrow=6,ncol=length(algs),byrow=FALSE)\n" | |
| 226 | + "colnames(mat) <- algs \n" | |
| 227 | + "rownames(mat) <- c(\"Rank 1\", \"Rank 5\", \"Rank 10\", \"Rank 20\", \"Rank 50\", \"Rank 100\")\n" | |
| 228 | + "CMCtable <- as.table(mat)\n" | |
| 229 | + "\n" | |
| 230 | + "# Code to format Template Size Table\n" | |
| 231 | + "if (nrow(TS) != 0) {\n\t" | |
| 232 | + "mat <- matrix(TS$Y,nrow=1,ncol=length(algs),byrow=FALSE)\n\t" | |
| 233 | + "colnames(mat) <- algs\n\t" | |
| 234 | + "rownames(mat) <- c(\"Template Size (bytes):\")\n\t" | |
| 235 | + "TStable <- as.table(mat)\n}" | |
| 236 | + "\n").arg((major.smooth || minor.smooth) && confidence != 0 ? "paste(as.character(round(TF$Y, 3)), round(TF$ci, 3), sep=\"\\u00b1\")" : "TF$Y", | |
| 237 | + (major.smooth || minor.smooth) && confidence != 0 ? "paste(as.character(round(FT$Y, 3)), round(FT$ci, 3), sep=\"\\u00b1\")" : "FT$Y", | |
| 238 | + (major.smooth || minor.smooth) && confidence != 0 ? "paste(as.character(round(CT$Y, 3)), round(CT$ci, 3), sep=\"\\u00b1\")" : "CT$Y", | |
| 239 | + (major.size > 1 && minor.size > 1) && !(major.smooth || minor.smooth) ? QString("paste(TF$%1, TF$%2, sep=\"_\")").arg(major.header, minor.header) | |
| 240 | + : QString("TF$%1").arg(major.size > 1 ? major.header : (minor.header.isEmpty() ? major.header : minor.header))))); | |
| 241 | + | |
| 242 | + file.write("\n# Write metadata table\n"); | |
| 243 | + QString textplot = "MT <- as.data.frame(Metadata[c(1,2,3,4,5),])\n" | |
| 244 | + "par(mfrow=c(4,1))\n" | |
| 245 | + "plot.new()\n" | |
| 246 | + "print(title(paste(\"%1 - %2\",date(),sep=\"\\n\")))\n" | |
| 247 | + "mat <- matrix(MT$X[c(1,2)],ncol=2)\n" | |
| 248 | + "colnames(mat) <- c(\"Gallery\", \"Probe\")\n" | |
| 249 | + "imageTable <- as.table(mat)\n" | |
| 250 | + "print(textplot(imageTable,show.rownames=FALSE))\n" | |
| 251 | + "print(title(\"Images\"))\n" | |
| 252 | + "mat <- matrix(MT$X[c(3,4,5)],ncol=3)\n" | |
| 253 | + "colnames(mat) <- c(\"Genuine\", \"Impostor\", \"Ignored\")\n" | |
| 254 | + "matchTable <- as.table(mat)\n" | |
| 255 | + "print(textplot(matchTable,show.rownames=FALSE))\n" | |
| 256 | + "print(title(\"Matches\"))\n" | |
| 257 | + "plot.new()\n" | |
| 258 | + "print(title(\"Gallery * Probe = Genuine + Impostor + Ignored\"))\n"; | |
| 259 | + file.write(qPrintable(textplot.arg(PRODUCT_NAME, PRODUCT_VERSION))); | |
| 260 | + | |
| 261 | + if (csv) | |
| 262 | + textplot = QString("write.csv(TFtable,file=\"%1_TF.csv\")\n" | |
| 263 | + "write.csv(FTtable,file=\"%1_FT.csv\")\n" | |
| 264 | + "write.csv(CMCtable,file=\"%1_CMC.csv\")\n\n").arg(basename); | |
| 265 | + else | |
| 266 | + textplot = "plot.new()\n" | |
| 267 | + "print(textplot(TFtable))\n" | |
| 268 | + "print(title(\"Table of True Accept Rates at various False Accept Rates\"))\n" | |
| 269 | + "print(textplot(FTtable))\n" | |
| 270 | + "print(title(\"Table of False Accept Rates at various True Accept Rates\"))\n" | |
| 271 | + "print(textplot(CMCtable))\n" | |
| 272 | + "print(title(\"Table of retrieval rate at various ranks\"))\n" | |
| 273 | + "if (nrow(TS) != 0) {\n\t" | |
| 274 | + "print(textplot(TStable, cex=1.15))\n\t" | |
| 275 | + "print(title(\"Template Size by Algorithm\"))\n}\n\n"; | |
| 276 | + file.write(qPrintable(textplot)); | |
| 277 | + } | |
| 278 | + | |
| 279 | + void qplot(QString geom, QString data, bool flipY, File opts) | |
| 280 | + { | |
| 281 | + file.write(qPrintable(QString("qplot(X, %1, data=%2, geom=\"%3\", main=\"%4\"").arg(flipY ? "1-Y" : "Y", data, geom, opts.get<QString>("title", "")) + | |
| 282 | + (opts.contains("size") ? QString(", size=I(%1)").arg(opts.get<QString>("size")) : QString()) + | |
| 283 | + (major.size > 1 ? QString(", colour=factor(%1)").arg(major.header) : QString()) + | |
| 284 | + (minor.size > 1 ? QString(", linetype=factor(%1)").arg(minor.header) : QString()) + | |
| 285 | + (QString(", xlab=\"%1\", ylab=\"%2\") + theme_minimal()").arg(opts.get<QString>("xTitle"), opts.get<QString>("yTitle"))) + | |
| 286 | + ((major.smooth || minor.smooth) && confidence != 0 && data != "CMC" ? QString(" + geom_errorbar(data=%1[seq(1, NROW(%1), by = 29),], aes(x=X, ymin=%2), width=0.1, alpha=I(1/2))").arg(data, flipY ? "(1-lower), ymax=(1-upper)" : "lower, ymax=upper") : QString()) + | |
| 287 | + (major.size > 1 ? getScale("colour", major.header, major.size) : QString()) + | |
| 288 | + (minor.size > 1 ? QString(" + scale_linetype_discrete(\"%1\")").arg(minor.header) : QString()) + | |
| 289 | + (opts.getBool("xLog") ? QString(" + scale_x_log10(labels=%1, breaks=%2) + annotation_logticks(sides=\"b\")").arg(opts.get<QString>("xLabels", "trans_format(\"log10\", math_format())"), opts.get<QString>("xBreaks", "waiver()")) | |
| 290 | + : QString(" + scale_x_continuous(labels=%1, breaks=%2)").arg(opts.get<QString>("xLabels", "percent"), opts.get<QString>("xBreaks", "pretty_breaks(n=10)"))) + | |
| 291 | + (opts.getBool("yLog") ? QString(" + scale_y_log10(labels=%1, breaks=%2) + annotation_logticks(sides=\"l\")").arg(opts.get<QString>("yLabels", "trans_format(\"log10\", math_format())"), opts.get<QString>("yBreaks", "waiver()")) | |
| 292 | + : QString(" + scale_y_continuous(labels=%1, breaks=%2)").arg(opts.get<QString>("yLabels", "percent"), opts.get<QString>("yBreaks", "pretty_breaks(n=10)"))) + | |
| 293 | + (opts.contains("xLimits") ? QString(" + xlim%1").arg(QtUtils::toString(opts.get<QPointF>("xLimits", QPointF()))) : QString()) + | |
| 294 | + (opts.contains("yLimits") ? QString(" + ylim%1").arg(QtUtils::toString(opts.get<QPointF>("yLimits", QPointF()))) : QString()) + | |
| 295 | + QString(" + theme(legend.title = element_text(size = %1), legend.text = element_text(size = %1), plot.title = element_text(size = %1), axis.text = element_text(size = %1), axis.title.x = element_text(size = %1), axis.title.y = element_text(size = %1),").arg(QString::number(opts.get<float>("textSize",12))) + | |
| 296 | + QString(" legend.position=%1, legend.background = element_rect(fill = 'white'), panel.grid.major = element_line(colour = \"gray\"), panel.grid.minor = element_line(colour = \"gray\", linetype = \"dashed\"))").arg(opts.contains("legendPosition") ? "c"+QtUtils::toString(opts.get<QPointF>("legendPosition")) : "'bottom'") + | |
| 297 | + QString(" + guides(col=guide_legend(ncol=%1))\n\n").arg(ncol))); | |
| 298 | + } | |
| 299 | + | |
| 283 | 300 | bool finalize(bool show = false) |
| 284 | 301 | { |
| 285 | 302 | file.write("dev.off()\n"); |
| ... | ... | @@ -297,66 +314,31 @@ bool Plot(const QStringList &files, const File &destination, bool show) |
| 297 | 314 | { |
| 298 | 315 | qDebug("Plotting %d file(s) to %s", files.size(), qPrintable(destination)); |
| 299 | 316 | |
| 300 | - const bool minimalist = destination.getBool("minimalist"); | |
| 301 | - const bool uncertainty = destination.get<bool>("uncertainty"); | |
| 317 | + RPlot p(files, destination); | |
| 302 | 318 | |
| 303 | 319 | // Use a br::file for simple storage of plot options |
| 304 | - File cmcOpts; | |
| 305 | - const QStringList cmcOptions = destination.get<QStringList>("cmcOptions", QStringList()); | |
| 306 | - foreach (const QString& option, cmcOptions) { | |
| 307 | - QStringList words = QtUtils::parse(option, '='); | |
| 308 | - QtUtils::checkArgsSize(words[0],words,1,2); | |
| 309 | - cmcOpts.set(words[0],words[1]); | |
| 310 | - } | |
| 311 | - | |
| 312 | - File rocOpts; | |
| 313 | - const QStringList rocOptions = destination.get<QStringList>("rocOptions", QStringList()); | |
| 314 | - foreach (const QString& option, rocOptions) { | |
| 315 | - QStringList words = QtUtils::parse(option, '='); | |
| 316 | - QtUtils::checkArgsSize(words[0],words,1,2); | |
| 317 | - rocOpts.set(words[0],words[1]); | |
| 320 | + QMap<QString,File> optMap; | |
| 321 | + optMap.insert("rocOptions", File(QString("[xTitle=False Accept Rate,yTitle=True Accept Rate,xLog=true,yLog=false]"))); | |
| 322 | + optMap.insert("detOptions", File(QString("[xTitle=False Accept Rate,yTitle=False Reject Rate,xLog=true,yLog=true]"))); | |
| 323 | + optMap.insert("ietOptions", File(QString("[xTitle=False Positive Identification Rate (FPIR),yTitle=False Negative Identification Rate (FNIR),xLog=true,yLog=true]"))); | |
| 324 | + optMap.insert("cmcOptions", File(QString("[xTitle=Rank,yTitle=Retrieval Rate,xLog=true,yLog=false,size=1,xLabels=c(1,5,10,50,100),xBreaks=c(1,5,10,50,100)]"))); | |
| 325 | + | |
| 326 | + foreach (const QString &key, optMap.keys()) { | |
| 327 | + const QStringList options = destination.get<QStringList>(key, QStringList()); | |
| 328 | + foreach (const QString &option, options) { | |
| 329 | + QStringList words = QtUtils::parse(option, '='); | |
| 330 | + QtUtils::checkArgsSize(words[0], words, 1, 2); | |
| 331 | + optMap[key].set(words[0], words[1]); | |
| 332 | + } | |
| 318 | 333 | } |
| 319 | 334 | |
| 320 | - | |
| 321 | - RPlot p(files, destination); | |
| 322 | - | |
| 323 | - p.file.write(qPrintable(QString("qplot(X, 1-Y, data=DET, geom=\"line\", main=\"%1\"").arg(rocOpts.get<QString>("title",QString())) + | |
| 324 | - (p.major.size > 1 ? QString(", colour=factor(%1)").arg(p.major.header) : QString()) + | |
| 325 | - (p.minor.size > 1 ? QString(", linetype=factor(%1)").arg(p.minor.header) : QString()) + | |
| 326 | - QString(", xlab=\"False Accept Rate\", ylab=\"True Accept Rate\") + theme_minimal()") + | |
| 327 | - ((p.major.smooth || p.minor.smooth) && p.confidence != 0 ? " + geom_errorbar(data=DET[seq(1, NROW(DET), by = 29),], aes(x=X, ymin=(1-lower), ymax=(1-upper)), width=0.1, alpha=I(1/2))" : QString()) + | |
| 328 | - (p.major.size > 1 ? getScale("colour", p.major.header, p.major.size) : QString()) + | |
| 329 | - (p.minor.size > 1 ? QString(" + scale_linetype_discrete(\"%1\")").arg(p.minor.header) : QString()) + | |
| 330 | - QString(" + scale_x_log10(labels=trans_format(\"log10\", math_format()))") + | |
| 331 | - (rocOpts.contains("yLimits") ? QString(" + scale_y_continuous(labels=percent) + coord_cartesian(ylim=%1)").arg("c"+QtUtils::toString(rocOpts.get<QPointF>("yLimits",QPointF()))) : QString(" + scale_y_continuous(labels=percent)")) + | |
| 332 | - QString(" + annotation_logticks(sides=\"b\")") + | |
| 333 | - QString(" + theme(legend.title = element_text(size = %1), plot.title = element_text(size = %1), axis.text = element_text(size = %1), axis.title.x = element_text(size = %1), axis.title.y = element_text(size = %1)," | |
| 334 | - " legend.position=%2, legend.background = element_rect(fill = 'white'), panel.grid.major = element_line(colour = \"gray\"), panel.grid.minor = element_line(colour = \"gray\", linetype = \"dashed\"), legend.text = element_text(size = %1))").arg(QString::number(rocOpts.get<float>("textSize",12)), rocOpts.contains("legendPosition") ? "c"+QtUtils::toString(rocOpts.get<QPointF>("legendPosition")) : "'bottom'") + | |
| 335 | - QString(" + guides(col=guide_legend(ncol=%1))\n\n").arg(destination.get<int>("ncol", p.major.size > 1 ? p.major.size : (p.minor.header.isEmpty() ? p.major.size : p.minor.size))))); | |
| 336 | - | |
| 337 | - p.file.write(qPrintable(QString("qplot(X, Y, data=DET, geom=\"line\"") + | |
| 338 | - (p.major.size > 1 ? QString(", colour=factor(%1)").arg(p.major.header) : QString()) + | |
| 339 | - (p.minor.size > 1 ? QString(", linetype=factor(%1)").arg(p.minor.header) : QString()) + | |
| 340 | - QString(", xlab=\"False Accept Rate\", ylab=\"False Reject Rate\") + geom_abline(alpha=0.5, colour=\"grey\", linetype=\"dashed\") + theme_minimal()") + | |
| 341 | - ((p.major.smooth || p.minor.smooth) && p.confidence != 0 ? " + geom_errorbar(data=DET[seq(1, NROW(DET), by = 29),], aes(x=X, ymin=lower, ymax=upper), width=0.1, alpha=I(1/2))" : QString()) + | |
| 342 | - (p.major.size > 1 ? getScale("colour", p.major.header, p.major.size) : QString()) + | |
| 343 | - (p.minor.size > 1 ? QString(" + scale_linetype_discrete(\"%1\")").arg(p.minor.header) : QString()) + | |
| 344 | - QString(" + theme(legend.title = element_text(size = %1), plot.title = element_text(size = %1), axis.text = element_text(size = %1), axis.title.x = element_text(size = %1), axis.title.y = element_text(size = %1)," | |
| 345 | - " legend.position=%2, legend.background = element_rect(fill = 'white'), panel.grid.major = element_line(colour = \"gray\"), panel.grid.minor = element_line(colour = \"gray\", linetype = \"dashed\"), legend.text = element_text(size = %1))").arg(QString::number(rocOpts.get<float>("textSize",12)), rocOpts.contains("legendPosition") ? "c"+QtUtils::toString(rocOpts.get<QPointF>("legendPosition")) : "'bottom'") + | |
| 346 | - QString(" + scale_x_log10(labels=trans_format(\"log10\", math_format())) + scale_y_log10(labels=trans_format(\"log10\", math_format())) + annotation_logticks()") + | |
| 347 | - QString(" + guides(col=guide_legend(ncol=%1))\n\n").arg(destination.get<int>("ncol", p.major.size > 1 ? p.major.size : (p.minor.header.isEmpty() ? p.major.size : p.minor.size))))); | |
| 348 | - | |
| 349 | - p.file.write(qPrintable(QString("qplot(X, Y, data=IET, geom=\"line\"") + | |
| 350 | - (p.major.size > 1 ? QString(", colour=factor(%1)").arg(p.major.header) : QString()) + | |
| 351 | - (p.minor.size > 1 ? QString(", linetype=factor(%1)").arg(p.minor.header) : QString()) + | |
| 352 | - QString(", xlab=\"False Positive Identification Rate (FPIR)\", ylab=\"False Negative Identification Rate (FNIR)\") + theme_minimal()") + | |
| 353 | - ((p.major.smooth || p.minor.smooth) && p.confidence != 0 ? " + geom_errorbar(data=IET[seq(1, NROW(IET), by = 29),], aes(x=X, ymin=lower, ymax=upper), width=0.1, alpha=I(1/2))" : QString()) + | |
| 354 | - (p.major.size > 1 ? getScale("colour", p.major.header, p.major.size) : QString()) + | |
| 355 | - (p.minor.size > 1 ? QString(" + scale_linetype_discrete(\"%1\")").arg(p.minor.header) : QString()) + | |
| 356 | - QString(" + theme(legend.title = element_text(size = %1), plot.title = element_text(size = %1), axis.text = element_text(size = %1), axis.title.x = element_text(size = %1), axis.title.y = element_text(size = %1)," | |
| 357 | - " legend.position=%2, legend.background = element_rect(fill = 'white'), panel.grid.major = element_line(colour = \"gray\"), panel.grid.minor = element_line(colour = \"gray\", linetype = \"dashed\"), legend.text = element_text(size = %1))").arg(QString::number(rocOpts.get<float>("textSize",12)), rocOpts.contains("legendPosition") ? "c"+QtUtils::toString(rocOpts.get<QPointF>("legendPosition")) : "'bottom'") + | |
| 358 | - QString(" + scale_x_log10(labels=trans_format(\"log10\", math_format())) + scale_y_log10(labels=trans_format(\"log10\", math_format())) + annotation_logticks()") + | |
| 359 | - QString(" + guides(col=guide_legend(ncol=%1))\n\n").arg(destination.get<int>("ncol", p.major.size > 1 ? p.major.size : (p.minor.header.isEmpty() ? p.major.size : p.minor.size))))); | |
| 335 | + // optional plot metadata and accuracy tables | |
| 336 | + if (destination.getBool("metadata", true)) | |
| 337 | + p.plotMetadata(destination.getBool("csv", false)); | |
| 338 | + p.qplot("line", "DET", true, optMap["rocOptions"]); | |
| 339 | + p.qplot("line", "DET", false, optMap["detOptions"]); | |
| 340 | + p.qplot("line", "IET", false, optMap["ietOptions"]); | |
| 341 | + p.qplot("line", "CMC", false, optMap["cmcOptions"]); | |
| 360 | 342 | |
| 361 | 343 | p.file.write(qPrintable(QString("qplot(X, data=SD, geom=\"histogram\", fill=Y, position=\"identity\", alpha=I(1/2)") + |
| 362 | 344 | QString(", xlab=\"Score\", ylab=\"Frequency\"") + |
| ... | ... | @@ -364,17 +346,6 @@ bool Plot(const QStringList &files, const File &destination, bool show) |
| 364 | 346 | (p.major.size > 1 ? (p.minor.size > 1 ? QString(" + facet_grid(%2 ~ %1, scales=\"free\")").arg((p.flip ? p.major.header : p.minor.header), (p.flip ? p.minor.header : p.major.header)) : QString(" + facet_wrap(~ %1, scales = \"free\")").arg(p.major.header)) : QString()) + |
| 365 | 347 | QString(" + theme(aspect.ratio=1)\n\n"))); |
| 366 | 348 | |
| 367 | - p.file.write(qPrintable(QString("ggplot(CMC, aes(x=X, y=Y%1%2)) + ggtitle(\"%3\") + xlab(\"Rank\") + ylab(\"Retrieval Rate\")").arg(p.major.size > 1 ? QString(" ,colour=factor(%1)").arg(p.major.header) : QString(), p.minor.size > 1 ? QString(", linetype=factor(%1)").arg(p.minor.header) : QString(), cmcOpts.get<QString>("title",QString())) + | |
| 368 | - QString(((p.major.smooth || p.minor.smooth) ? (!uncertainty ? " + stat_summary(geom=\"line\", fun.y=mean, size=%1)" : " + stat_summary(geom=\"line\", fun.y=min, aes(linetype=\"Min/Max\"), size=%1) + stat_summary(geom=\"line\", " | |
| 369 | - "fun.y=max, aes(linetype=\"Min/Max\"), size=%1) + stat_summary(geom=\"line\", fun.y=mean, aes(linetype=\"Mean\"), size=%1) + scale_linetype_manual(\"Legend\", values=c(\"Mean\"=1, \"Min/Max\"=2))") : " + geom_line(size=%1)")).arg(QString::number(cmcOpts.get<float>("thickness",1))) + | |
| 370 | - (minimalist ? "" : " + scale_x_log10(labels=c(1,5,10,50,100), breaks=c(1,5,10,50,100)) + annotation_logticks(sides=\"b\")") + | |
| 371 | - (p.major.size > 1 ? getScale("colour", p.major.header, p.major.size) : QString()) + | |
| 372 | - (p.minor.size > 1 ? QString(" + scale_linetype_discrete(\"%1\")").arg(p.minor.header) : QString()) + | |
| 373 | - (cmcOpts.contains("yLimits") ? QString(" + scale_y_continuous(labels=percent) + coord_cartesian(ylim=%1)").arg("c"+QtUtils::toString(cmcOpts.get<QPointF>("yLimits",QPointF()))) : QString(" + scale_y_continuous(labels=percent)")) + | |
| 374 | - QString(" + theme_minimal() + theme(legend.title = element_text(size = %1), plot.title = element_text(size = %1), axis.text = element_text(size = %1), axis.title.x = element_text(size = %1), axis.title.y = element_text(size = %1)," | |
| 375 | - " legend.position=%2, legend.background = element_rect(fill = 'white'), panel.grid.major = element_line(colour = \"gray\"), panel.grid.minor = element_line(colour = \"gray\", linetype = \"dashed\"), legend.text = element_text(size = %1))").arg(QString::number(cmcOpts.get<float>("textSize",12)), cmcOpts.contains("legendPosition") ? "c"+QtUtils::toString(cmcOpts.get<QPointF>("legendPosition")) : "'bottom'") + | |
| 376 | - QString(" + guides(col=guide_legend(ncol=%1))\n\n").arg(destination.get<int>("ncol", p.major.size > 1 ? p.major.size : (p.minor.header.isEmpty() ? p.major.size : p.minor.size))))); | |
| 377 | - | |
| 378 | 349 | p.file.write(qPrintable(QString("qplot(factor(%1)%2, data=BC, %3").arg(p.major.smooth ? (p.minor.header.isEmpty() ? "Algorithm" : p.minor.header) : p.major.header, (p.major.smooth || p.minor.smooth) ? ", Y" : "", (p.major.smooth || p.minor.smooth) ? "geom=\"boxplot\"" : "geom=\"bar\", position=\"dodge\", weight=Y") + |
| 379 | 350 | (p.major.size > 1 ? QString(", fill=factor(%1)").arg(p.major.header) : QString()) + |
| 380 | 351 | QString(", xlab=\"False Accept Rate\", ylab=\"True Accept Rate\") + theme_minimal()") + |
| ... | ... | @@ -451,6 +422,20 @@ bool PlotDetection(const QStringList &files, const File &destination, bool show) |
| 451 | 422 | qDebug("Plotting %d detection file(s) to %s", files.size(), qPrintable(destination)); |
| 452 | 423 | RPlot p(files, destination, false); |
| 453 | 424 | |
| 425 | + // Use a br::file for simple storage of plot options | |
| 426 | + QMap<QString,File> optMap; | |
| 427 | + optMap.insert("rocOptions", File(QString("[xTitle=False Accepts Per Image,yTitle=True Accept Rate,xLog=true,yLog=false]"))); | |
| 428 | + optMap.insert("prOptions", File(QString("[xTitle=False Accept Rate,yTitle=False Reject Rate,xLog=true,yLog=true]"))); | |
| 429 | + | |
| 430 | + foreach (const QString &key, optMap.keys()) { | |
| 431 | + const QStringList options = destination.get<QStringList>(key, QStringList()); | |
| 432 | + foreach (const QString &option, options) { | |
| 433 | + QStringList words = QtUtils::parse(option, '='); | |
| 434 | + QtUtils::checkArgsSize(words[0], words, 1, 2); | |
| 435 | + optMap[key].set(words[0], words[1]); | |
| 436 | + } | |
| 437 | + } | |
| 438 | + | |
| 454 | 439 | p.file.write("# Split data into individual plots\n" |
| 455 | 440 | "plot_index = which(names(data)==\"Plot\")\n" |
| 456 | 441 | "DiscreteROC <- data[grep(\"DiscreteROC\",data$Plot),-c(1)]\n" |
| ... | ... | @@ -466,23 +451,15 @@ bool PlotDetection(const QStringList &files, const File &destination, bool show) |
| 466 | 451 | if (filesHaveSinglePoint(files)) |
| 467 | 452 | plotType = QString("point"); |
| 468 | 453 | |
| 469 | - foreach (const QString &type, QStringList() << "Discrete" << "Continuous") | |
| 470 | - p.file.write(qPrintable(QString("qplot(X, Y, data=%1ROC%2").arg(type, (p.major.smooth || p.minor.smooth) ? ", geom=\"smooth\", method=loess, level=0.99" : QString(", geom=\"%1\"").arg(plotType)) + | |
| 471 | - (p.major.size > 1 ? QString(", colour=factor(%1)").arg(p.major.header) : QString()) + | |
| 472 | - (p.minor.size > 1 ? QString(", linetype=factor(%1)").arg(p.minor.header) : QString()) + | |
| 473 | - QString(", xlab=\"False Accepts Per Image\", ylab=\"True Accept Rate\") + theme_minimal()") + | |
| 474 | - (p.major.size > 1 ? getScale("colour", p.major.header, p.major.size) : QString()) + | |
| 475 | - (p.minor.size > 1 ? QString(" + scale_linetype_discrete(\"%1\")").arg(p.minor.header) : QString()) + | |
| 476 | - QString(" + scale_x_log10() + scale_y_continuous(labels=percent, limits=c(0,1)) + annotation_logticks(sides=\"b\") + ggtitle(\"%1\") + theme(legend.position=\"bottom\")\n\n").arg(type))); | |
| 477 | - | |
| 478 | - foreach (const QString &type, QStringList() << "Discrete" << "Continuous") | |
| 479 | - p.file.write(qPrintable(QString("qplot(X, Y, data=%1PR%2").arg(type, (p.major.smooth || p.minor.smooth) ? ", geom=\"smooth\", method=loess, level=0.99" : QString(", geom=\"%1\"").arg(plotType)) + | |
| 480 | - (p.major.size > 1 ? QString(", colour=factor(%1)").arg(p.major.header) : QString()) + | |
| 481 | - (p.minor.size > 1 ? QString(", linetype=factor(%1)").arg(p.minor.header) : QString()) + | |
| 482 | - QString(", xlab=\"Recall\", ylab=\"Precision\") + theme_minimal()") + | |
| 483 | - (p.major.size > 1 ? getScale("colour", p.major.header, p.major.size) : QString()) + | |
| 484 | - (p.minor.size > 1 ? QString(" + scale_linetype_discrete(\"%1\")").arg(p.minor.header) : QString()) + | |
| 485 | - QString(" + scale_x_continuous(limits=c(0,1)) + scale_y_continuous(labels=percent, limits=c(0,1)) + ggtitle(\"%1\") + theme(legend.position=\"bottom\")\n\n").arg(type))); | |
| 454 | + foreach (const QString &type, QStringList() << "Discrete" << "Continuous") { | |
| 455 | + optMap["rocOptions"].set("title", type); | |
| 456 | + p.qplot(plotType, type + "ROC", false, optMap["rocOptions"]); | |
| 457 | + } | |
| 458 | + | |
| 459 | + foreach (const QString &type, QStringList() << "Discrete" << "Continuous") { | |
| 460 | + optMap["prOptions"].set("title", type); | |
| 461 | + p.qplot(plotType, type + "PR", false, optMap["prOptions"]); | |
| 462 | + } | |
| 486 | 463 | |
| 487 | 464 | p.file.write(qPrintable(QString("qplot(X, data=Overlap, geom=\"histogram\", position=\"identity\", xlab=\"Overlap\", ylab=\"Frequency\")") + |
| 488 | 465 | QString(" + theme_minimal() + scale_x_continuous(minor_breaks=NULL) + scale_y_continuous(minor_breaks=NULL) + theme(axis.text.y=element_blank(), axis.ticks=element_blank(), axis.text.x=element_text(angle=-90, hjust=0))") + | ... | ... |