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