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 break; 183 ColumnData[] xyGroup; 184 size_t xs = 0; 185 size_t ys = 0; 186 double lastX; // If x is used set lastX to isNaN? 187 double lastY; 188 Point[] addRange; 189 foreach ( cM; groupedCMs ) { 190 if ( cM.xCoord || cM.yCoord ) { 191 if ( xs > 1 && ys > 1 ) { 192 // We never want a group with more than 1 x coord or y coord 193 xs = 0; 194 ys = 0; 195 addRange ~= columnDataToPoints( 196 xyGroup[0..$-1], columnID ); 197 xyGroup = [xyGroup.back]; 198 } else if ( xs >= 1 && ys >= 1 && xyGroup.back.mode != cM.mode ) { 199 xs = 0; 200 ys = 0; 201 addRange ~= columnDataToPoints( xyGroup, columnID ); 202 xyGroup = [cM]; 203 } else 204 xyGroup ~= cM; 205 if (cM.xCoord) { 206 lastX = cM.value; 207 xs++; 208 } else if (cM.yCoord) { 209 lastY = cM.value; 210 ys++; 211 } 212 } else { 213 if (type == "hist") 214 parsed.histData ~= cM.value; 215 } 216 } 217 if (xyGroup.length > 0) { 218 // If we found no x or y coord at all then use columnID 219 if (lastX.isNaN || lastY.isNaN) 220 addRange ~= columnDataToPoints( xyGroup, columnID ); 221 else if ( xyGroup.front.xCoord ) 222 addRange ~= columnDataToPoints( xyGroup, lastY ); 223 else 224 addRange ~= columnDataToPoints( xyGroup, lastX ); 225 } 226 if ( type == "line" ) { 227 parsed.linePoints ~= addRange; 228 } else if ( type == "point" ) 229 parsed.points ~= addRange; 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 bool needAdjusting = false; 335 if ( !figures[plotID].validBound ) { 336 needAdjusting = true; 337 figures[plotID].pointCache ~= parsedRow.points ~ parsedRow.linePoints; 338 figures[plotID].validBound = validBounds( figures[plotID].pointCache ); 339 figures[plotID].plot.plotBounds = minimalBounds( figures[plotID].pointCache ); 340 if (figures[plotID].validBound) 341 figures[plotID].pointCache = []; 342 } else { 343 foreach( point; parsedRow.points ~ parsedRow.linePoints ) { 344 if (!figures[plotID].plot.plotBounds.withinBounds( point )) { 345 figures[plotID].plot.plotBounds = adjustedBounds( figures[plotID].plot.plotBounds, point ); 346 needAdjusting = true; 347 } 348 } 349 } 350 351 if (needAdjusting) { 352 353 figures[plotID].plot = createPlotState( figures[plotID].plot.plotBounds, 354 figures[plotID].plot.marginBounds ); 355 foreach( event; figures[plotID].eventCache ) 356 event( figures[plotID].plot ); 357 } 358 359 auto events = parsedRow.points.toEvents; 360 361 if ( figures[plotID].previousLines.length == parsedRow.linePoints.length ) 362 events ~= parsedRow.linePoints.toLineEvents( figures[plotID].previousLines ); 363 364 if (parsedRow.linePoints.length > 0) 365 figures[plotID].previousLines = parsedRow.linePoints; 366 367 368 foreach( event; events ) 369 event( figures[plotID].plot ); 370 371 figures[plotID].eventCache ~= events; 372 373 // Histograms 374 foreach( data; parsedRow.histData ) { 375 if ( data < figures[plotID].histRange[0] || isNaN(figures[plotID].histRange[0]) ) { 376 figures[plotID].histRange[0] = data; 377 } 378 if ( data > figures[plotID].histRange[1] || isNaN(figures[plotID].histRange[1]) ) { 379 figures[plotID].histRange[1] = data; 380 } 381 figures[plotID].histData ~= data; 382 } 383 384 if (figures[plotID].histData.length > 0) { 385 // Create bin 386 Bins!size_t bins; 387 bins.min = figures[plotID].histRange[0]; 388 bins.width = 0.5; 389 bins.length = max( 11, figures[plotID].histData.length/100 ); 390 if( figures[plotID].histRange[0] != figures[plotID].histRange[1] ) 391 bins.width = (figures[plotID].histRange[1]-figures[plotID].histRange[0])/10.0; 392 // add all data to bin 393 foreach( data; figures[plotID].histData ) 394 bins = bins.addDataToBin( [bins.binId( data )] ); 395 396 auto histBounds = Bounds( bins.min, bins.max, 0, 397 figures[plotID].histData.length ); 398 399 debug writeln( "Adjusting histogram to bounds: ", histBounds ); 400 // Adjust plotBounds 401 figures[plotID].plot = createPlotState( histBounds, 402 figures[plotID].plot.marginBounds ); 403 // Plot Bins 404 figures[plotID].plot.plotContext = drawBins( figures[plotID].plot.plotContext, bins ); 405 debug writeln( "Drawn bins to histogram: ", bins ); 406 } 407 408 } 409 figures[plotID].columnCount += 1; 410 } 411 } 412 413 void saveFigures( string baseName ) { 414 foreach ( plotID, figure; figures ) { 415 figure.plot.save( baseName ~ plotID ~ ".png" ); 416 } 417 }