1 module ggplotd.bounds; 2 3 /// Point with x and y value 4 struct Point 5 { 6 /// x value 7 double x; 8 /// y value 9 double y; 10 11 /// Constructor taking x and y value 12 this(double my_x, double my_y) 13 { 14 x = my_x; 15 y = my_y; 16 } 17 18 /// Constructor taking a string holding the x and y value separated by a comma 19 this(string value) 20 { 21 import std.conv : to; 22 import std.range : split; 23 24 auto coords = value.split(","); 25 assert(coords.length == 2); 26 x = to!double(coords[0]); 27 y = to!double(coords[1]); 28 } 29 30 unittest 31 { 32 assert(Point("1.0,0.1") == Point(1.0, 0.1)); 33 } 34 35 /// Test whether two points are equal to each other 36 bool opEquals(in Point point) const 37 { 38 return point.x == x && point.y == y; 39 } 40 41 } 42 43 /// Bounds struct holding the bounds (min_x, max_x, min_y, max_y) 44 struct Bounds 45 { 46 /// Lower x limit 47 double min_x; 48 /// Upper x limit 49 double max_x; 50 /// Lower y limit 51 double min_y; 52 /// Upper y limit 53 double max_y; 54 55 /// Constructor taking the x and y limits 56 this(double my_min_x, double my_max_x, double my_min_y, double my_max_y) 57 { 58 min_x = my_min_x; 59 max_x = my_max_x; 60 min_y = my_min_y; 61 max_y = my_max_y; 62 } 63 64 /// Constructor taking the x and y limits separated by a comma 65 this(string value) 66 { 67 import std.conv : to; 68 import std.range : split; 69 import std.string : strip; 70 71 auto bnds = value.strip.split(","); 72 assert(bnds.length == 4); 73 min_x = to!double(bnds[0]); 74 max_x = to!double(bnds[1]); 75 min_y = to!double(bnds[2]); 76 max_y = to!double(bnds[3]); 77 } 78 79 unittest 80 { 81 assert(Bounds("0.1,0.2,0.3,0.4") == Bounds(0.1, 0.2, 0.3, 0.4)); 82 assert(Bounds("0.1,0.2,0.3,0.4\n") == Bounds(0.1, 0.2, 0.3, 0.4)); 83 } 84 85 } 86 87 /// Return the height of the given bounds 88 double height(Bounds bounds) 89 { 90 return bounds.max_y - bounds.min_y; 91 } 92 93 unittest 94 { 95 assert(Bounds(0, 1.5, 1, 5).height == 4); 96 } 97 98 /// Return the width of the given bounds 99 double width(Bounds bounds) 100 { 101 return bounds.max_x - bounds.min_x; 102 } 103 104 unittest 105 { 106 assert(Bounds(0, 1.5, 1, 5).width == 1.5); 107 } 108 109 /// Is the point within the Bounds 110 bool withinBounds(Bounds bounds, Point point) 111 { 112 return (point.x <= bounds.max_x && point.x >= bounds.min_x 113 && point.y <= bounds.max_y && point.y >= bounds.min_y); 114 } 115 116 unittest 117 { 118 assert(Bounds(0, 1, 0, 1).withinBounds(Point(1, 0))); 119 assert(Bounds(0, 1, 0, 1).withinBounds(Point(0, 1))); 120 assert(!Bounds(0, 1, 0, 1).withinBounds(Point(0, 1.1))); 121 assert(!Bounds(0, 1, 0, 1).withinBounds(Point(-0.1, 1))); 122 assert(!Bounds(0, 1, 0, 1).withinBounds(Point(1.1, 0.5))); 123 assert(!Bounds(0, 1, 0, 1).withinBounds(Point(0.1, -0.1))); 124 } 125 126 /// Can we construct valid bounds given these points 127 bool validBounds(Point[] points) 128 { 129 if (points.length < 2) 130 return false; 131 bool validx = false; 132 bool validy = false; 133 immutable x = points[0].x; 134 immutable y = points[0].y; 135 foreach (point; points[1 .. $]) 136 { 137 if (point.x != x) 138 validx = true; 139 if (point.y != y) 140 validy = true; 141 if (validx && validy) 142 return true; 143 } 144 return false; 145 } 146 147 unittest 148 { 149 assert(validBounds([Point(0, 1), Point(1, 0)])); 150 assert(!validBounds([Point(0, 1)])); 151 assert(!validBounds([Point(0, 1), Point(0, 0)])); 152 assert(!validBounds([Point(0, 1), Point(1, 1)])); 153 } 154 155 /// Return minimal bounds size containing those points 156 Bounds minimalBounds(Point[] points) 157 { 158 if (points.length == 0) 159 return Bounds(-1, 1, -1, 1); 160 double min_x = points[0].x; 161 double max_x = points[0].x; 162 double min_y = points[0].y; 163 double max_y = points[0].y; 164 if (points.length > 1) 165 { 166 foreach (point; points[1 .. $]) 167 { 168 if (point.x < min_x) 169 min_x = point.x; 170 else if (point.x > max_x) 171 max_x = point.x; 172 if (point.y < min_y) 173 min_y = point.y; 174 else if (point.y > max_y) 175 max_y = point.y; 176 } 177 } 178 if (min_x == max_x) 179 { 180 min_x = min_x - 0.5; 181 max_x = max_x + 0.5; 182 } 183 if (min_y == max_y) 184 { 185 min_y = min_y - 0.5; 186 max_y = max_y + 0.5; 187 } 188 return Bounds(min_x, max_x, min_y, max_y); 189 } 190 191 unittest 192 { 193 assert(minimalBounds([]) == Bounds(-1, 1, -1, 1)); 194 assert(minimalBounds([Point(0, 0)]) == Bounds(-0.5, 0.5, -0.5, 0.5)); 195 assert(minimalBounds([Point(0, 0), Point(0, 0)]) == Bounds(-0.5, 0.5, -0.5, 0.5)); 196 assert(minimalBounds([Point(0.1, 0), Point(0, 0.2)]) == Bounds(0, 0.1, 0, 0.2)); 197 } 198 199 /// Returns adjust bounds based on given bounds to include point 200 Bounds adjustedBounds(Bounds bounds, Point point) 201 { 202 import std.algorithm : min, max; 203 204 if (bounds.min_x > point.x) 205 { 206 bounds.min_x = min(bounds.min_x - 0.1 * bounds.width, point.x); 207 } 208 else if (bounds.max_x < point.x) 209 { 210 bounds.max_x = max(bounds.max_x + 0.1 * bounds.width, point.x); 211 } 212 if (bounds.min_y > point.y) 213 { 214 bounds.min_y = min(bounds.min_y - 0.1 * bounds.height, point.y); 215 } 216 else if (bounds.max_y < point.y) 217 { 218 bounds.max_y = max(bounds.max_y + 0.1 * bounds.height, point.y); 219 } 220 return bounds; 221 } 222 223 unittest 224 { 225 assert(adjustedBounds(Bounds(0, 1, 0, 1), Point(0, 1.01)) == Bounds(0, 1, 0, 1.1)); 226 assert(adjustedBounds(Bounds(0, 1, 0, 1), Point(0, 1.5)) == Bounds(0, 1, 0, 1.5)); 227 assert(adjustedBounds(Bounds(0, 1, 0, 1), Point(-1, 1.01)) == Bounds(-1, 1, 0, 228 1.1)); 229 assert(adjustedBounds(Bounds(0, 1, 0, 1), Point(1.2, -0.01)) == Bounds(0, 1.2, 230 -0.1, 1)); 231 } 232 233 /// Bounds that can adapt to new points being passed 234 struct AdaptiveBounds 235 { 236 /* 237 Notes: the main problem with adaptive bounds is the beginning, where we need to 238 make sure we have enough points to form valid bounds (i.e. with width and height 239 > 0). For example if all points fall on a vertical lines, we have no information 240 for the width of the plot 241 242 Here we take care to always return a valid set of bounds 243 */ 244 245 /// Actual bounds being used 246 Bounds bounds = Bounds(0, 1, 0, 1); 247 alias bounds this; 248 249 /// Constructor taking comma separated x and y limits 250 this(string str) 251 { 252 bounds = Bounds(str); 253 } 254 255 /// Constructor taking x and y limits 256 this(double my_min_x, double my_max_x, double my_min_y, double my_max_y) 257 { 258 bounds = Bounds(my_min_x, my_max_x, my_min_y, my_max_y); 259 } 260 261 /// Contructor taking an existing Bounds struct 262 this(Bounds bnds) 263 { 264 bounds = bnds; 265 } 266 267 /// Adapt bounds to include the new point 268 bool adapt(T : Point)(in T point) 269 { 270 import std.math : isFinite; 271 bool adapted = false; 272 if (!isFinite(point.x) || !isFinite(point.y)) 273 return adapted; 274 275 if (!valid) 276 { 277 adapted = true; 278 pointCache ~= point; 279 valid = validBounds(pointCache); 280 bounds = minimalBounds(pointCache); 281 if (valid) 282 pointCache.length = 0; 283 } 284 else 285 { 286 if (!bounds.withinBounds(point)) 287 { 288 bounds = bounds.adjustedBounds(point); 289 adapted = true; 290 } 291 } 292 return adapted; 293 } 294 295 /// Adapt by passing x and y variable 296 bool adapt(double x, double y) 297 { 298 return adapt(Point(x, y)); 299 } 300 301 /// Adapt bounds to include the given bounds 302 bool adapt(T : AdaptiveBounds)(in T bounds) 303 { 304 bool adapted = false; 305 if (bounds.valid) 306 { 307 immutable bool adaptMin = adapt(Point(bounds.min_x, bounds.min_y)); 308 immutable bool adaptMax = adapt(Point(bounds.max_x, bounds.max_y)); 309 adapted = (adaptMin || adaptMax); 310 } 311 else 312 { 313 adapted = adapt(bounds.pointCache); 314 } 315 return adapted; 316 } 317 318 import std.range : isInputRange; 319 320 /// Adapt bounds to include the new points 321 bool adapt(T)(in T points) 322 { 323 import std.range : save; 324 bool adapted = false; 325 foreach (point; points.save) 326 { 327 immutable a = adapt(point); 328 if (a) 329 adapted = true; 330 } 331 return adapted; 332 } 333 334 private: 335 Point[] pointCache; 336 bool valid = false; 337 } 338 339 unittest 340 { 341 assert(AdaptiveBounds("0.1,0.2,0.3,0.4") == Bounds(0.1, 0.2, 0.3, 0.4)); 342 // Test adapt 343 AdaptiveBounds bounds; 344 assert(bounds.width > 0); 345 assert(bounds.height > 0); 346 auto pnt = Point(5, 2); 347 assert(bounds.adapt(pnt)); 348 assert(bounds.width > 0); 349 assert(bounds.height > 0); 350 assert(bounds.withinBounds(pnt)); 351 assert(!bounds.valid); 352 pnt = Point(3, 2); 353 assert(bounds.adapt(pnt)); 354 assert(bounds.width >= 2); 355 assert(bounds.height > 0); 356 assert(bounds.withinBounds(pnt)); 357 assert(!bounds.valid); 358 pnt = Point(3, 5); 359 assert(bounds.adapt(pnt)); 360 assert(bounds.width >= 2); 361 assert(bounds.height >= 3); 362 assert(bounds.withinBounds(pnt)); 363 assert(bounds.valid); 364 pnt = Point(4, 4); 365 assert(!bounds.adapt(pnt)); 366 367 368 assert(!bounds.adapt(Point(double.init, 1.0))); 369 assert(!bounds.adapt(Point(-1.0,double.init))); 370 assert(!bounds.adapt(Point(double.init, double.init))); 371 372 import std.math : log; 373 assert(!bounds.adapt(Point(log(0.0), 1.0))); 374 assert(!bounds.adapt(Point(-1.0,log(0.0)))); 375 assert(!bounds.adapt(Point(log(0.0), log(0.0)))); 376 } 377 378 unittest 379 { 380 AdaptiveBounds bounds; 381 assert(!bounds.valid); 382 AdaptiveBounds bounds2; 383 assert(!bounds.adapt(bounds2)); 384 385 bounds2.adapt(Point(1.1, 1.2)); 386 bounds.adapt(bounds2); 387 assert(!bounds.valid); 388 AdaptiveBounds bounds3; 389 bounds3.adapt(Point(1.2, 1.3)); 390 bounds.adapt(bounds3); 391 assert(bounds.valid); 392 393 AdaptiveBounds bounds4; 394 assert(!bounds4.valid); 395 AdaptiveBounds bounds5; 396 bounds5.adapt(Point(1.1, 1.2)); 397 bounds5.adapt(Point(1.3, 1.3)); 398 assert(bounds5.valid); 399 bounds4.adapt(bounds5); 400 assert(bounds4.valid); 401 }