1 /* 2 ------------------------------------------------------------------- 3 4 Copyright (C) 2014, Edwin van Leeuwen 5 6 This file is part of plotd plotting library. 7 8 Plotd is free software; you can redistribute it and/or modify 9 it under the terms of the GNU General Public License as published by 10 the Free Software Foundation; either version 3 of the License, or 11 (at your option) any later version. 12 13 Plotd is distributed in the hope that it will be useful, 14 but WITHOUT ANY WARRANTY; without even the implied warranty of 15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 GNU General Public License for more details. 17 18 You should have received a copy of the GNU General Public License 19 along with Plotd. If not, see <http://www.gnu.org/licenses/>. 20 21 ------------------------------------------------------------------- 22 */ 23 24 module cli.parsing; 25 26 import std.algorithm; 27 import std.conv : ConvException, to; 28 import std.math : isNaN; 29 import std.range; 30 import std.stdio : write, writeln; 31 import std.string; 32 33 import std.regex : ctRegex, match, split; 34 35 import docopt; 36 37 import plotd.binning; 38 import plotd.drawing; 39 import plotd.plot; 40 import plotd.primitives; 41 42 import cli.algorithm : groupBy; 43 import cli.column; 44 import cli.figure : Figure; 45 import cli.options : helpText, Settings, updateSettings; 46 47 version( unittest ) { 48 import std.stdio; 49 } 50 51 alias void delegate( PlotState plot ) Event; 52 53 private auto csvRegex = ctRegex!(`,\s*|\s`); 54 55 double[] toRange( string line ) { 56 try { 57 return line.split( csvRegex ) 58 .map!( (d) => d.strip( ' ' ).to!double ) 59 .array; 60 } catch (ConvException exp) { 61 return []; 62 } 63 } 64 65 unittest { 66 assert( "1,2".toRange == [1,2] ); 67 assert( "0.5, 2".toRange == [0.5,2] ); 68 assert( "bla, 2".toRange == [] ); 69 assert( "1\t2".toRange == [1,2] ); 70 assert( "1 2".toRange == [1,2] ); 71 } 72 73 Point[] toPoints( double[] coords ) { 74 Point[] points; 75 if (coords.length >= 2) 76 points ~= Point( coords[0], coords[1] ); 77 return points; 78 } 79 80 unittest { 81 assert( [1.0].toPoints.length == 0 ); 82 assert( equal([1.0,2.0].toPoints, [Point( 1, 2 )] ) ); 83 } 84 85 Event[] toEvents( Point[] points ) { 86 Event[] events; 87 88 ColorRange colorRange; 89 90 // Workaround point not properly copied in foreach loop 91 void delegate( PlotState ) createEvent( Point point, Color col ) { 92 return delegate( PlotState plot ) { 93 plot.plotContext = color( plot.plotContext, col ); 94 colorRange.popFront; 95 point.draw( plot ); 96 }; 97 } 98 99 foreach( point; points ) { 100 events ~= createEvent( point, colorRange.front ); 101 colorRange.popFront; 102 } 103 return events; 104 } 105 106 Event[] toLineEvents( Point[] points, Point[] previousPoints ) { 107 Event[] events; 108 109 ColorRange colorRange; 110 111 // Workaround point not properly copied in foreach loop 112 void delegate( PlotState ) createEvent( size_t i, Color col ) { 113 return delegate( PlotState plot ) { 114 plot.plotContext = color( plot.plotContext, col ); 115 plot.plotContext = drawLine( previousPoints[i], points[i], plot.plotContext ); 116 }; 117 } 118 119 foreach( i; 0..points.length ) { 120 events ~= createEvent( i, colorRange.front ); 121 colorRange.popFront; 122 } 123 return events; 124 } 125 126 /// Struct to hold the different points etc 127 struct ParsedRow { 128 Point[] points; 129 Point[] linePoints; 130 double[] histData; 131 } 132 133 // Warning assumes array with either one x or one y value. 134 private Point[] columnDataToPoints( ColumnData[] cMs, double defaultCoord ) 135 { 136 Point[] pnts; 137 if (cMs.length == 0) 138 return pnts; 139 140 auto coords = cMs.groupBy!( (cm) { 141 if (cm.xCoord) 142 return "x"; 143 return "y"; } ); 144 145 if ( "x" !in coords ) { 146 return coords["y"] 147 .map!( (cmy) => Point( defaultCoord, cmy.value ) ).array; 148 } else if ( "y" !in coords ) { 149 return coords["x"] 150 .map!( (cmx) => Point( cmx.value, defaultCoord ) ).array; 151 } 152 else if ( coords["x"].length == 1 ) { 153 return coords["y"] 154 .map!( (cmy) => Point( coords["x"].front.value, cmy.value ) ).array; 155 } 156 else if ( coords["y"].length == 1 ) { 157 return coords["x"] 158 .map!( (cmx) => Point( cmx.value, coords["y"].front.value ) ).array; 159 } 160 assert( 0, "Invalid input for columnModeToPoints " ~ cMs.to!string ); 161 } 162 163 /** Turn columns into drawable results. If no x or y value is present then columnID is used as the x or y value; 164 165 This function tries to be relative human like in parsing and the logic is difficult to follow. See the unittests for its behaviour 166 */ 167 ParsedRow applyColumnData( ColumnData[] cMs, size_t columnID ) { 168 ParsedRow parsed; 169 foreach ( type, groupedCMs; 170 cMs.groupBy!( (cm) { 171 if ( cm.mode.to!string == "" ) 172 return "none"; 173 if ( cm.mode.front.to!string == "l" ) 174 return "line"; 175 if ( cm.mode.front.to!string == "h" ) 176 return "hist"; 177 return "point"; 178 } 179 ) ) 180 { 181 if ( type != "none" ) { 182 ColumnData[] xyGroup; 183 size_t xs = 0; 184 size_t ys = 0; 185 double lastX; // If x is used set lastX to isNaN? 186 double lastY; 187 Point[] addRange; 188 foreach ( cM; groupedCMs ) { 189 if ( cM.xCoord || cM.yCoord ) { 190 if ( xs > 1 && ys > 1 ) { 191 // We never want a group with more than 1 x coord or y coord 192 xs = 0; 193 ys = 0; 194 addRange ~= columnDataToPoints( 195 xyGroup[0..$-1], columnID ); 196 xyGroup = [xyGroup.back]; 197 } else if ( xs >= 1 && ys >= 1 && xyGroup.back.mode != cM.mode ) { 198 xs = 0; 199 ys = 0; 200 addRange ~= columnDataToPoints( xyGroup, columnID ); 201 xyGroup = [cM]; 202 } else 203 xyGroup ~= cM; 204 if (cM.xCoord) { 205 lastX = cM.value; 206 xs++; 207 } else if (cM.yCoord) { 208 lastY = cM.value; 209 ys++; 210 } 211 } else { 212 if (type == "hist") 213 parsed.histData ~= cM.value; 214 } 215 } 216 if (xyGroup.length > 0) { 217 // If we found no x or y coord at all then use columnID 218 if (lastX.isNaN || lastY.isNaN) 219 addRange ~= columnDataToPoints( xyGroup, columnID ); 220 else if ( xyGroup.front.xCoord ) 221 addRange ~= columnDataToPoints( xyGroup, lastY ); 222 else 223 addRange ~= columnDataToPoints( xyGroup, lastX ); 224 } 225 if ( type == "line" ) { 226 parsed.linePoints ~= addRange; 227 } else if ( type == "point" ) 228 parsed.points ~= addRange; 229 } 230 } 231 return parsed; 232 } 233 234 unittest { 235 ColumnData cm( string mode, double value ) { 236 return ColumnData( mode, -1, "", value ); 237 } 238 239 auto pr = applyColumnData( [cm("x",1), cm("y",2)], 0 ); 240 assert( pr.points == [Point( 1, 2 )] ); 241 242 pr = applyColumnData( [cm("x",3), cm("y",2), cm("y",4)], 0 ); 243 assert( pr.points == [Point( 3, 2 ), Point( 3, 4 )] ); 244 245 pr = applyColumnData( [cm("x",1)], 5 ); 246 assert( pr.points == [Point( 1, 5 )] ); 247 248 pr = applyColumnData( [cm("x",1), cm("x",3)], 5 ); 249 assert( pr.points == [Point( 1, 5 ),Point( 3, 5 )] ); 250 251 pr = applyColumnData( [cm("y",2)], 5 ); 252 assert( pr.points == [Point( 5, 2 )] ); 253 254 pr = applyColumnData( [cm("y",2), cm("y",4)], 5 ); 255 assert( pr.points == [Point( 5, 2 ),Point( 5, 4 )] ); 256 257 pr = applyColumnData( [cm("y",2), cm("x",1), cm("y",4), cm("y",6)], 5 ); 258 assert( pr.points == [Point( 1, 2 ),Point( 1, 4 ),Point( 1, 6 )] ); 259 pr = applyColumnData( [cm("x",2), cm("y",1), cm("x",4), cm("x",6)], 5 ); 260 assert( pr.points == [Point( 2, 1 ),Point( 4, 1 ),Point( 6, 1 )] ); 261 262 pr = applyColumnData( [cm("y",2), cm("x",1), cm("y",4), cm("x",3)], 5 ); 263 assert( pr.points == [Point( 1, 2 ),Point( 3, 4 )] ); 264 pr = applyColumnData( [cm("y",2), cm("y",8), cm("x",1), cm("y",4), cm("y",6), cm("x",3)], 5 ); 265 assert( pr.points == [Point( 1, 2 ),Point( 1, 8 ),Point( 3, 4 ),Point( 3, 6 )] ); 266 pr = applyColumnData( [cm("x",2), cm("y",8), cm("y",1), cm("x",4), cm("y",6), cm("y",3)], 5 ); 267 assert( pr.points == [Point( 2, 8 ),Point( 2, 1 ),Point( 4, 6 ),Point( 4, 3 )] ); 268 269 // Lines 270 // Should really be more indepth, but since same code is used, as for 271 // points should be ok 272 pr = applyColumnData( [cm("lx",1), cm("ly",2)], 0 ); 273 assert( pr.linePoints == [Point( 1, 2 )] ); 274 275 pr = applyColumnData( [cm("x",2), cm("y",8), cm("lx",11), cm("y",1), cm("x",4), cm("y",6), cm("y",3)], 5 ); 276 assert( pr.points == [Point( 2, 8 ),Point( 2, 1 ),Point( 4, 6 ),Point( 4, 3 )] ); 277 assert( pr.linePoints == [Point( 11, 5 )] ); 278 279 // Hist 280 pr = applyColumnData( [cm("h",1.1), cm("h",2.1)], 0 ); 281 assert( pr.histData == [1.1,2.1] ); 282 283 pr = applyColumnData( [cm("x",2), cm("h",1.1), cm("y",8), cm("lx",11), cm("y",1), cm("x",4), cm("h",2.1), cm("y",6), cm("y",3)], 5 ); 284 assert( pr.points == [Point( 2, 8 ),Point( 2, 1 ),Point( 4, 6 ),Point( 4, 3 )] ); 285 assert( pr.linePoints == [Point( 11, 5 )] ); 286 assert( pr.histData == [1.1,2.1] ); 287 } 288 289 /// Check whether current RowMode makes sense for new data. 290 Formats updateFormat( double[] floats, Formats formats ) { 291 if ( floats.length == 0 ) 292 return formats; 293 if ( formats.validFormat( floats.length ) ) 294 return formats; 295 else 296 return Formats( floats.length ); 297 } 298 299 300 Figure[string] figures; 301 302 // High level functionality for handlingMessages 303 void handleMessage( string msg, ref Settings settings ) { 304 if ( "" !in figures ) 305 figures[""] = new Figure; 306 307 debug write( "Received message: ", msg ); 308 309 auto m = msg.match( r"^#plotcli (.*)" ); 310 if (m) { 311 settings = settings.updateSettings( 312 docopt.docopt(helpText, 313 std..string.split( m.captures[1], " " ), true, "plotcli") ); 314 //writeln( settings ); 315 } 316 317 auto floats = msg.strip 318 .toRange; 319 320 debug writeln( "Converted to doubles: ", floats ); 321 322 settings.formats = updateFormat( floats, settings.formats ); 323 324 auto columnData = settings.formats.zip(floats).map!( 325 (mv) { auto cD = ColumnData( mv[0] ); cD.value = mv[1]; return cD; } ); 326 327 foreach( plotID, cMs1; columnData.groupBy!( (cm) => cm.plotID ) ) 328 { 329 if ( plotID !in figures ) 330 figures[plotID] = new Figure; 331 foreach( dataID, cMs; cMs1.groupBy!( (cm) => cm.dataID ) ) { 332 debug writeln( "Plotting data: ", cMs ); 333 auto parsedRow = applyColumnData( cMs, figures[plotID].columnCount ); 334 335 bool needAdjusting = false; 336 if ( !figures[plotID].validBound ) { 337 needAdjusting = true; 338 figures[plotID].pointCache ~= parsedRow.points ~ parsedRow.linePoints; 339 figures[plotID].validBound = validBounds( figures[plotID].pointCache ); 340 figures[plotID].plot.plotBounds = minimalBounds( figures[plotID].pointCache ); 341 if (figures[plotID].validBound) 342 figures[plotID].pointCache = []; 343 } else { 344 foreach( point; parsedRow.points ~ parsedRow.linePoints ) { 345 if (!figures[plotID].plot.plotBounds.withinBounds( point )) { 346 figures[plotID].plot.plotBounds = adjustedBounds( figures[plotID].plot.plotBounds, point ); 347 needAdjusting = true; 348 } 349 } 350 } 351 352 if (needAdjusting) { 353 354 figures[plotID].plot = createPlotState( figures[plotID].plot.plotBounds, 355 figures[plotID].plot.marginBounds ); 356 foreach( event; figures[plotID].eventCache ) 357 event( figures[plotID].plot ); 358 } 359 360 auto events = parsedRow.points.toEvents; 361 362 if (dataID !in figures[plotID].previousLines) { 363 Point[] pnts; 364 figures[plotID].previousLines[dataID] = pnts; 365 } 366 367 368 if ( figures[plotID].previousLines[dataID].length 369 == parsedRow.linePoints.length ) 370 events ~= parsedRow.linePoints.toLineEvents( 371 figures[plotID].previousLines[dataID] ); 372 373 if (parsedRow.linePoints.length > 0) 374 figures[plotID].previousLines[dataID] = parsedRow.linePoints; 375 376 377 foreach( event; events ) 378 event( figures[plotID].plot ); 379 380 figures[plotID].eventCache ~= events; 381 382 // Histograms 383 foreach( data; parsedRow.histData ) { 384 if ( data < figures[plotID].histRange[0] || isNaN(figures[plotID].histRange[0]) ) { 385 figures[plotID].histRange[0] = data; 386 } 387 if ( data > figures[plotID].histRange[1] || isNaN(figures[plotID].histRange[1]) ) { 388 figures[plotID].histRange[1] = data; 389 } 390 figures[plotID].histData ~= data; 391 } 392 393 394 if (figures[plotID].histData.length > 0) { 395 // Create bin 396 Bins!size_t bins; 397 bins.min = figures[plotID].histRange[0]; 398 bins.width = 0.5; 399 bins.length = max( 11, min( 31, figures[plotID].histData.length/100 ) ); 400 if( figures[plotID].histRange[0] != figures[plotID].histRange[1] ) 401 bins.width = (figures[plotID].histRange[1]-figures[plotID].histRange[0])/bins.length; 402 // add all data to bin 403 foreach( data; figures[plotID].histData ) 404 bins = bins.addDataToBin( [bins.binId( data )] ); 405 406 auto histBounds = bins.optimalBounds( 0.99 ); 407 408 debug writeln( "Adjusting histogram to bounds: ", histBounds ); 409 // Adjust plotBounds 410 figures[plotID].plot = createPlotState( histBounds, 411 figures[plotID].plot.marginBounds ); 412 // Plot Bins 413 figures[plotID].plot.plotContext = drawBins( figures[plotID].plot.plotContext, bins ); 414 debug writeln( "Drawn bins to histogram: ", bins ); 415 } 416 417 } 418 figures[plotID].columnCount += 1; 419 } 420 } 421 422 void saveFigures( string baseName ) { 423 foreach ( plotID, figure; figures ) { 424 figure.plot.save( baseName ~ plotID ~ ".png" ); 425 } 426 }