1 /* 2 Copyright 2008-2023 3 Matthias Ehmann, 4 Michael Gerhaeuser, 5 Carsten Miller, 6 Bianca Valentin, 7 Alfred Wassermann, 8 Peter Wilfahrt 9 10 This file is part of JSXGraph. 11 12 JSXGraph is free software dual licensed under the GNU LGPL or MIT License. 13 14 You can redistribute it and/or modify it under the terms of the 15 16 * GNU Lesser General Public License as published by 17 the Free Software Foundation, either version 3 of the License, or 18 (at your option) any later version 19 OR 20 * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT 21 22 JSXGraph is distributed in the hope that it will be useful, 23 but WITHOUT ANY WARRANTY; without even the implied warranty of 24 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 25 GNU Lesser General Public License for more details. 26 27 You should have received a copy of the GNU Lesser General Public License and 28 the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/> 29 and <https://opensource.org/licenses/MIT/>. 30 */ 31 32 /*global JXG: true, define: true*/ 33 /*jslint nomen: true, plusplus: true*/ 34 35 /** 36 * @fileoverview In this file the geometry object Ticks is defined. Ticks provides 37 * methods for creation and management of ticks on an axis. 38 * @author graphjs 39 * @version 0.1 40 */ 41 42 import JXG from "../jxg"; 43 import Mat from "../math/math"; 44 import Geometry from "../math/geometry"; 45 import Numerics from "../math/numerics"; 46 import Const from "./constants"; 47 import GeometryElement from "./element"; 48 import Coords from "./coords"; 49 import Type from "../utils/type"; 50 51 /** 52 * Creates ticks for an axis. 53 * @class Ticks provides methods for creation and management 54 * of ticks on an axis. 55 * @param {JXG.Line} line Reference to the axis the ticks are drawn on. 56 * @param {Number|Array} ticks Number defining the distance between two major ticks or an array defining static ticks. 57 * @param {Object} attributes Properties 58 * @see JXG.Line#addTicks 59 * @constructor 60 * @augments JXG.GeometryElement 61 */ 62 JXG.Ticks = function (line, ticks, attributes) { 63 this.constructor(line.board, attributes, Const.OBJECT_TYPE_TICKS, Const.OBJECT_CLASS_OTHER); 64 65 /** 66 * The line the ticks belong to. 67 * @type JXG.Line 68 * @private 69 */ 70 this.line = line; 71 72 /** 73 * The board the ticks line is drawn on. 74 * @type JXG.Board 75 * @private 76 */ 77 this.board = this.line.board; 78 79 // /** 80 // * A function calculating ticks delta depending on the ticks number. 81 // * @type Function 82 // */ 83 // // this.ticksFunction = null; 84 85 /** 86 * Array of fixed ticks. 87 * @type Array 88 * @private 89 */ 90 this.fixedTicks = null; 91 92 /** 93 * Flag if the ticks are equidistant. If true, their distance is defined by ticksFunction. 94 * @type Boolean 95 * @private 96 */ 97 this.equidistant = false; 98 99 /** 100 * A list of labels which have to be displayed in updateRenderer. 101 * @type Array 102 * @private 103 */ 104 this.labelsData = []; 105 106 if (Type.isFunction(ticks)) { 107 this.ticksFunction = ticks; 108 throw new Error("Function arguments are no longer supported."); 109 } 110 111 if (Type.isArray(ticks)) { 112 this.fixedTicks = ticks; 113 } else { 114 // Obsolete: 115 // if (Math.abs(ticks) < Mat.eps || ticks < 0) { 116 // ticks = attributes.defaultdistance; 117 // } 118 this.equidistant = true; 119 } 120 121 // /** 122 // * Least distance between two ticks, measured in pixels. 123 // * @type int 124 // */ 125 // // this.minTicksDistance = attributes.minticksdistance; 126 127 /** 128 * Stores the ticks coordinates 129 * @type Array 130 * @private 131 */ 132 this.ticks = []; 133 134 // /** 135 // * Distance between two major ticks in user coordinates 136 // * @type Number 137 // */ 138 // this.ticksDelta = 1; 139 140 /** 141 * Array where the labels are saved. There is an array element for every tick, 142 * even for minor ticks which don't have labels. In this case the array element 143 * contains just <tt>null</tt>. 144 * @type Array 145 * @private 146 */ 147 this.labels = []; 148 149 /** 150 * Used to ensure the uniqueness of label ids this counter is used. 151 * @type number 152 * @private 153 */ 154 this.labelCounter = 0; 155 156 this.id = this.line.addTicks(this); 157 this.elType = "ticks"; 158 this.inherits.push(this.labels); 159 this.board.setId(this, "Ti"); 160 }; 161 162 JXG.Ticks.prototype = new GeometryElement(); 163 164 JXG.extend( 165 JXG.Ticks.prototype, 166 /** @lends JXG.Ticks.prototype */ { 167 // /** 168 // * Ticks function: 169 // * determines the distance (in user units) of two major ticks. 170 // * See above in constructor and in @see JXG.GeometryElement#setAttribute 171 // * 172 // * @private 173 // * @param {Number} ticks Distance between two major ticks 174 // * @returns {Function} returns method ticksFunction 175 // */ 176 // // makeTicksFunction: function (ticks) { 177 // // return function () { 178 // ticksFunction: function () { 179 // var delta, b, dist, 180 // number_major_tick_intervals = 5; 181 182 // if (Type.evaluate(this.visProp.insertticks)) { 183 // b = this.getLowerAndUpperBounds(this.getZeroCoordinates(), 'ticksdistance'); 184 // dist = b.upper - b.lower; 185 186 // // delta: Proposed distance in user units between two major ticks 187 // delta = Math.pow(10, Math.floor(Math.log(dist / number_major_tick_intervals) / Math.LN10)); 188 // console.log("delta", delta, b.upper, b.lower, dist, dist / number_major_tick_intervals * 1.1) 189 // if (5 * delta < dist / number_major_tick_intervals * 1.1) { 190 // return 5 * delta; 191 // } 192 // if (2 * delta < dist / number_major_tick_intervals * 1.1) { 193 // return 2 * delta; 194 // } 195 196 // // < v1.6.0: 197 // // delta = Math.pow(10, Math.floor(Math.log(0.6 * dist) / Math.LN10)); 198 // if (false && dist <= 6 * delta) { 199 // delta *= 0.5; 200 // } 201 // return delta; 202 // } 203 204 // // In case of insertTicks==false 205 // return Type.evaluate(this.visProp.ticksdistance); 206 // // return ticks; 207 // // }; 208 // }, 209 210 /** 211 * Checks whether (x,y) is near the line. 212 * Only available for line elements, not for ticks on curves. 213 * @param {Number} x Coordinate in x direction, screen coordinates. 214 * @param {Number} y Coordinate in y direction, screen coordinates. 215 * @returns {Boolean} True if (x,y) is near the line, False otherwise. 216 */ 217 hasPoint: function (x, y) { 218 var i, 219 t, 220 len = (this.ticks && this.ticks.length) || 0, 221 r, 222 type; 223 224 if (Type.isObject(Type.evaluate(this.visProp.precision))) { 225 type = this.board._inputDevice; 226 r = Type.evaluate(this.visProp.precision[type]); 227 } else { 228 // 'inherit' 229 r = this.board.options.precision.hasPoint; 230 } 231 r += Type.evaluate(this.visProp.strokewidth) * 0.5; 232 if ( 233 !Type.evaluate(this.line.visProp.scalable) || 234 this.line.elementClass === Const.OBJECT_CLASS_CURVE 235 ) { 236 return false; 237 } 238 239 // Ignore non-axes and axes that are not horizontal or vertical 240 if ( 241 this.line.stdform[1] !== 0 && 242 this.line.stdform[2] !== 0 && 243 this.line.type !== Const.OBJECT_TYPE_AXIS 244 ) { 245 return false; 246 } 247 248 for (i = 0; i < len; i++) { 249 t = this.ticks[i]; 250 251 // Skip minor ticks 252 if (t[2]) { 253 // Ignore ticks at zero 254 if ( 255 !( 256 (this.line.stdform[1] === 0 && 257 Math.abs(t[0][0] - this.line.point1.coords.scrCoords[1]) < 258 Mat.eps) || 259 (this.line.stdform[2] === 0 && 260 Math.abs(t[1][0] - this.line.point1.coords.scrCoords[2]) < 261 Mat.eps) 262 ) 263 ) { 264 // tick length is not zero, ie. at least one pixel 265 if ( 266 Math.abs(t[0][0] - t[0][1]) >= 1 || 267 Math.abs(t[1][0] - t[1][1]) >= 1 268 ) { 269 if (this.line.stdform[1] === 0) { 270 // Allow dragging near axes only. 271 if ( 272 Math.abs(y - (t[1][0] + t[1][1]) * 0.5) < 2 * r && 273 t[0][0] - r < x && 274 x < t[0][1] + r 275 ) { 276 return true; 277 } 278 } else if (this.line.stdform[2] === 0) { 279 if ( 280 Math.abs(x - (t[0][0] + t[0][1]) * 0.5) < 2 * r && 281 t[1][0] - r < y && 282 y < t[1][1] + r 283 ) { 284 return true; 285 } 286 } 287 } 288 } 289 } 290 } 291 292 return false; 293 }, 294 295 /** 296 * Sets x and y coordinate of the tick. 297 * @param {number} method The type of coordinates used here. Possible values are {@link JXG.COORDS_BY_USER} and {@link JXG.COORDS_BY_SCREEN}. 298 * @param {Array} coords coordinates in screen/user units 299 * @param {Array} oldcoords previous coordinates in screen/user units 300 * @returns {JXG.Ticks} this element 301 */ 302 setPositionDirectly: function (method, coords, oldcoords) { 303 var dx, 304 dy, 305 c = new Coords(method, coords, this.board), 306 oldc = new Coords(method, oldcoords, this.board), 307 bb = this.board.getBoundingBox(); 308 309 if ( 310 this.line.type !== Const.OBJECT_TYPE_AXIS || 311 !Type.evaluate(this.line.visProp.scalable) 312 ) { 313 return this; 314 } 315 316 if ( 317 Math.abs(this.line.stdform[1]) < Mat.eps && 318 Math.abs(c.usrCoords[1] * oldc.usrCoords[1]) > Mat.eps 319 ) { 320 // Horizontal line 321 dx = oldc.usrCoords[1] / c.usrCoords[1]; 322 bb[0] *= dx; 323 bb[2] *= dx; 324 this.board.setBoundingBox(bb, this.board.keepaspectratio, "update"); 325 } else if ( 326 Math.abs(this.line.stdform[2]) < Mat.eps && 327 Math.abs(c.usrCoords[2] * oldc.usrCoords[2]) > Mat.eps 328 ) { 329 // Vertical line 330 dy = oldc.usrCoords[2] / c.usrCoords[2]; 331 bb[3] *= dy; 332 bb[1] *= dy; 333 this.board.setBoundingBox(bb, this.board.keepaspectratio, "update"); 334 } 335 336 return this; 337 }, 338 339 /** 340 * (Re-)calculates the ticks coordinates. 341 * @private 342 */ 343 calculateTicksCoordinates: function () { 344 var coordsZero, b, r_max, bb; 345 346 if (this.line.elementClass === Const.OBJECT_CLASS_LINE) { 347 // Calculate Ticks width and height in Screen and User Coordinates 348 this.setTicksSizeVariables(); 349 350 // If the parent line is not finite, we can stop here. 351 if (Math.abs(this.dx) < Mat.eps && Math.abs(this.dy) < Mat.eps) { 352 return; 353 } 354 } 355 356 // Get Zero (coords element for lines, number for curves) 357 coordsZero = this.getZeroCoordinates(); 358 359 // Calculate lower bound and upper bound limits based on distance 360 // between p1 and center and p2 and center 361 if (this.line.elementClass === Const.OBJECT_CLASS_LINE) { 362 b = this.getLowerAndUpperBounds(coordsZero, 'ticksdistance'); 363 } else { 364 b = { 365 lower: this.line.minX(), 366 upper: this.line.maxX(), 367 a1: 0, 368 a2: 0, 369 m1: 0, 370 m2: 0 371 }; 372 } 373 374 if (Type.evaluate(this.visProp.type) === "polar") { 375 bb = this.board.getBoundingBox(); 376 r_max = Math.max( 377 Math.sqrt(bb[0] * bb[0] + bb[1] * bb[1]), 378 Math.sqrt(bb[2] * bb[2] + bb[3] * bb[3]) 379 ); 380 b.upper = r_max; 381 } 382 383 // Clean up 384 this.ticks = []; 385 this.labelsData = []; 386 // Create Ticks Coordinates and Labels 387 if (this.equidistant) { 388 this.generateEquidistantTicks(coordsZero, b); 389 } else { 390 this.generateFixedTicks(coordsZero, b); 391 } 392 393 return this; 394 }, 395 396 /** 397 * Sets the variables used to set the height and slope of each tick. 398 * 399 * @private 400 */ 401 setTicksSizeVariables: function (pos) { 402 var d, 403 mi, 404 ma, 405 len, 406 distMaj = Type.evaluate(this.visProp.majorheight) * 0.5, 407 distMin = Type.evaluate(this.visProp.minorheight) * 0.5; 408 409 // For curves: 410 if (Type.exists(pos)) { 411 mi = this.line.minX(); 412 ma = this.line.maxX(); 413 len = this.line.points.length; 414 if (len < 2) { 415 this.dxMaj = 0; 416 this.dyMaj = 0; 417 } else if (Mat.relDif(pos, mi) < Mat.eps) { 418 this.dxMaj = 419 this.line.points[0].usrCoords[2] - this.line.points[1].usrCoords[2]; 420 this.dyMaj = 421 this.line.points[1].usrCoords[1] - this.line.points[0].usrCoords[1]; 422 } else if (Mat.relDif(pos, ma) < Mat.eps) { 423 this.dxMaj = 424 this.line.points[len - 2].usrCoords[2] - 425 this.line.points[len - 1].usrCoords[2]; 426 this.dyMaj = 427 this.line.points[len - 1].usrCoords[1] - 428 this.line.points[len - 2].usrCoords[1]; 429 } else { 430 this.dxMaj = -Numerics.D(this.line.Y)(pos); 431 this.dyMaj = Numerics.D(this.line.X)(pos); 432 } 433 } else { 434 // ticks width and height in screen units 435 this.dxMaj = this.line.stdform[1]; 436 this.dyMaj = this.line.stdform[2]; 437 } 438 this.dxMin = this.dxMaj; 439 this.dyMin = this.dyMaj; 440 441 // ticks width and height in user units 442 this.dx = this.dxMaj; 443 this.dy = this.dyMaj; 444 445 // After this, the length of the vector (dxMaj, dyMaj) in screen coordinates is equal to distMaj pixel. 446 d = Math.sqrt( 447 this.dxMaj * this.dxMaj * this.board.unitX * this.board.unitX + 448 this.dyMaj * this.dyMaj * this.board.unitY * this.board.unitY 449 ); 450 this.dxMaj *= (distMaj / d) * this.board.unitX; 451 this.dyMaj *= (distMaj / d) * this.board.unitY; 452 this.dxMin *= (distMin / d) * this.board.unitX; 453 this.dyMin *= (distMin / d) * this.board.unitY; 454 455 // Grid-like ticks? 456 this.minStyle = Type.evaluate(this.visProp.minorheight) < 0 ? "infinite" : "finite"; 457 this.majStyle = Type.evaluate(this.visProp.majorheight) < 0 ? "infinite" : "finite"; 458 }, 459 460 /** 461 * Returns the coordinates of the point zero of the line. 462 * 463 * If the line is an {@link Axis}, the coordinates of the projection of the board's zero point is returned 464 * 465 * Otherwise, the coordinates of the point that acts as zero are 466 * established depending on the value of {@link JXG.Ticks#anchor} 467 * 468 * @returns {JXG.Coords} Coords object for the zero point on the line 469 * @private 470 */ 471 getZeroCoordinates: function () { 472 var c1x, c1y, c1z, c2x, c2y, c2z, 473 t, mi, ma, 474 ev_a = Type.evaluate(this.visProp.anchor); 475 476 if (this.line.elementClass === Const.OBJECT_CLASS_LINE) { 477 if (this.line.type === Const.OBJECT_TYPE_AXIS) { 478 return Geometry.projectPointToLine( 479 { 480 coords: { 481 usrCoords: [1, 0, 0] 482 } 483 }, 484 this.line, 485 this.board 486 ); 487 } 488 c1z = this.line.point1.coords.usrCoords[0]; 489 c1x = this.line.point1.coords.usrCoords[1]; 490 c1y = this.line.point1.coords.usrCoords[2]; 491 c2z = this.line.point2.coords.usrCoords[0]; 492 c2x = this.line.point2.coords.usrCoords[1]; 493 c2y = this.line.point2.coords.usrCoords[2]; 494 495 if (ev_a === "right") { 496 return this.line.point2.coords; 497 } 498 if (ev_a === "middle") { 499 return new Coords( 500 Const.COORDS_BY_USER, 501 [(c1z + c2z) * 0.5, (c1x + c2x) * 0.5, (c1y + c2y) * 0.5], 502 this.board 503 ); 504 } 505 if (Type.isNumber(ev_a)) { 506 return new Coords( 507 Const.COORDS_BY_USER, 508 [ 509 c1z + (c2z - c1z) * ev_a, 510 c1x + (c2x - c1x) * ev_a, 511 c1y + (c2y - c1y) * ev_a 512 ], 513 this.board 514 ); 515 } 516 return this.line.point1.coords; 517 } 518 mi = this.line.minX(); 519 ma = this.line.maxX(); 520 if (ev_a === "right") { 521 t = ma; 522 } else if (ev_a === "middle") { 523 t = (mi + ma) * 0.5; 524 } else if (Type.isNumber(ev_a)) { 525 t = mi * (1 - ev_a) + ma * ev_a; 526 // t = ev_a; 527 } else { 528 t = mi; 529 } 530 return t; 531 }, 532 533 /** 534 * Calculate the lower and upper bounds for tick rendering. 535 * If {@link JXG.Ticks#includeBoundaries} is false, the boundaries will exclude point1 and point2. 536 * 537 * @param {JXG.Coords} coordsZero 538 * @returns {String} [type] If type=='ticksdistance', the bounds are 539 * the intersection of the line with the bounding box of the board, respecting 540 * the value of the line attribute 'margin' and the width of arrow heads. 541 * Otherwise, it is the projection of the corners of the bounding box 542 * to the line - without the attribute 'margin' and width of arrow heads. 543 * <br> 544 * The first case is needed to determine which ticks are displayed, i.e. where to stop. 545 * The second case is to determine the distance between ticks in case of 'insertTicks:true'. 546 * @returns {Object} {lower: Number, upper: Number } containing the lower and upper bounds in user units. 547 * 548 * @private 549 */ 550 getLowerAndUpperBounds: function (coordsZero, type) { 551 var lowerBound, upperBound, 552 fA, lA, 553 point1, point2, 554 isPoint1inBoard, isPoint2inBoard, 555 // We use the distance from zero to P1 and P2 to establish lower and higher points 556 dZeroPoint1, dZeroPoint2, 557 arrowData, 558 a1, a2, m1, m2, 559 eps = Mat.eps * 10, 560 ev_sf = Type.evaluate(this.line.visProp.straightfirst), 561 ev_sl = Type.evaluate(this.line.visProp.straightlast), 562 ev_i = Type.evaluate(this.visProp.includeboundaries); 563 564 // The line's defining points that will be adjusted to be within the board limits 565 if (this.line.elementClass === Const.OBJECT_CLASS_CURVE) { 566 return { 567 lower: this.line.minX(), 568 upper: this.line.maxX() 569 }; 570 } 571 572 point1 = new Coords(Const.COORDS_BY_USER, this.line.point1.coords.usrCoords, this.board); 573 point2 = new Coords(Const.COORDS_BY_USER, this.line.point2.coords.usrCoords, this.board); 574 575 // Are the original defining points within the board? 576 isPoint1inBoard = 577 Math.abs(point1.usrCoords[0]) >= Mat.eps && 578 point1.scrCoords[1] >= 0.0 && 579 point1.scrCoords[1] <= this.board.canvasWidth && 580 point1.scrCoords[2] >= 0.0 && 581 point1.scrCoords[2] <= this.board.canvasHeight; 582 isPoint2inBoard = 583 Math.abs(point2.usrCoords[0]) >= Mat.eps && 584 point2.scrCoords[1] >= 0.0 && 585 point2.scrCoords[1] <= this.board.canvasWidth && 586 point2.scrCoords[2] >= 0.0 && 587 point2.scrCoords[2] <= this.board.canvasHeight; 588 589 // Adjust line limit points to be within the board 590 if (Type.exists(type) && type === 'ticksdistance') { 591 // The good old calcStraight is needed for determining the distance between major ticks. 592 // Here, only the visual area is of importance 593 Geometry.calcStraight(this.line, point1, point2, 0); 594 m1 = this.getDistanceFromZero(coordsZero, point1); 595 m2 = this.getDistanceFromZero(coordsZero, point2); 596 Geometry.calcStraight(this.line, point1, point2, Type.evaluate(this.line.visProp.margin)); 597 m1 = this.getDistanceFromZero(coordsZero, point1) - m1; 598 m2 = this.getDistanceFromZero(coordsZero, point2) . m2; 599 } else { 600 // This function projects the corners of the board to the line. 601 // This is important for diagonal lines with infinite tick lines. 602 Geometry.calcLineDelimitingPoints(this.line, point1, point2); 603 } 604 605 // Shorten ticks bounds such that ticks are not through arrow heads 606 fA = Type.evaluate(this.line.visProp.firstarrow); 607 lA = Type.evaluate(this.line.visProp.lastarrow); 608 609 a1 = this.getDistanceFromZero(coordsZero, point1); 610 a2 = this.getDistanceFromZero(coordsZero, point2); 611 if (fA || lA) { 612 // Do not display ticks at through arrow heads. 613 // In arrowData we ignore the highlighting status. 614 // Ticks would appear to be too nervous. 615 arrowData = this.board.renderer.getArrowHeadData( 616 this.line, 617 Type.evaluate(this.line.visProp.strokewidth), 618 '' 619 ); 620 621 this.board.renderer.getPositionArrowHead( 622 this.line, 623 point1, 624 point2, 625 arrowData 626 ); 627 } 628 // Calculate (signed) distance from Zero to P1 and to P2 629 dZeroPoint1 = this.getDistanceFromZero(coordsZero, point1); 630 dZeroPoint2 = this.getDistanceFromZero(coordsZero, point2); 631 632 // Recompute lengths of arrow heads 633 a1 = dZeroPoint1 - a1; 634 a2 = dZeroPoint1 - a2; 635 636 // We have to establish if the direction is P1->P2 or P2->P1 to set the lower and upper 637 // bounds appropriately. As the distances contain also a sign to indicate direction, 638 // we can compare dZeroPoint1 and dZeroPoint2 to establish the line direction 639 if (dZeroPoint1 < dZeroPoint2) { 640 // Line goes P1->P2 641 lowerBound = dZeroPoint1; 642 upperBound = dZeroPoint2; 643 644 if (!ev_sf && isPoint1inBoard && !ev_i) { 645 lowerBound += eps; 646 } 647 if (!ev_sl && isPoint2inBoard && !ev_i) { 648 upperBound -= eps; 649 } 650 } else if (dZeroPoint2 < dZeroPoint1) { 651 // Line goes P2->P1 652 lowerBound = dZeroPoint2; 653 upperBound = dZeroPoint1; 654 655 if (!ev_sl && isPoint2inBoard && !ev_i) { 656 lowerBound += eps; 657 } 658 if (!ev_sf && isPoint1inBoard && !ev_i) { 659 upperBound -= eps; 660 } 661 } else { 662 // P1 = P2 = Zero, we can't do a thing 663 lowerBound = 0; 664 upperBound = 0; 665 } 666 667 return { 668 lower: lowerBound, 669 upper: upperBound, 670 a1: a1, 671 a2: a2, 672 m1: m1, 673 m2: m2 674 }; 675 }, 676 677 /** 678 * Calculates the distance in user coordinates from zero to a given point including its sign. 679 * Sign is positive, if the direction from zero to point is the same as the direction 680 * zero to point2 of the line. 681 * 682 * @param {JXG.Coords} zero coordinates of the point considered zero 683 * @param {JXG.Coords} point coordinates of the point to find out the distance 684 * @returns {Number} distance between zero and point, including its sign 685 * @private 686 */ 687 getDistanceFromZero: function (zero, point) { 688 var p1, p2, dirLine, dirPoint, distance; 689 690 p1 = this.line.point1.coords; 691 p2 = this.line.point2.coords; 692 distance = zero.distance(Const.COORDS_BY_USER, point); 693 694 // Establish sign 695 dirLine = [ 696 p2.usrCoords[0] - p1.usrCoords[0], 697 p2.usrCoords[1] - p1.usrCoords[1], 698 p2.usrCoords[2] - p1.usrCoords[2] 699 ]; 700 dirPoint = [ 701 point.usrCoords[0] - zero.usrCoords[0], 702 point.usrCoords[1] - zero.usrCoords[1], 703 point.usrCoords[2] - zero.usrCoords[2] 704 ]; 705 if (Mat.innerProduct(dirLine, dirPoint, 3) < 0) { 706 distance *= -1; 707 } 708 709 return distance; 710 }, 711 712 /** 713 * Creates ticks coordinates and labels automatically. 714 * The frequency of ticks is affected by the values of {@link JXG.Ticks#insertTicks}, {@link JXG.Ticks#minTicksDistance}, 715 * and {@link JXG.Ticks#ticksDistance} 716 * 717 * @param {JXG.Coords} coordsZero coordinates of the point considered zero 718 * @param {Object} bounds contains the lower and upper bounds for ticks placement 719 * @private 720 */ 721 generateEquidistantTicks: function (coordsZero, bounds) { 722 var tickPosition, 723 eps = Mat.eps, 724 deltas, ticksDelta, 725 ev_mia = Type.evaluate(this.visProp.minorticksinarrow), 726 ev_maa = Type.evaluate(this.visProp.minorticksinarrow), 727 ev_mla = Type.evaluate(this.visProp.minorticksinarrow), 728 ev_mt = Type.evaluate(this.visProp.minorticks); 729 730 // Determine a proposed distance between major ticks in user units 731 ticksDelta = this.getDistanceMajorTicks(); 732 733 // Obsolete, since this.equidistant is always true at this point 734 // ticksDelta = this.equidistant ? this.ticksFunction(1) : this.ticksDelta; 735 736 if (this.line.elementClass === Const.OBJECT_CLASS_LINE) { 737 // Calculate x and y distances between two points on the line which are 1 unit apart 738 // In essence, these are cosine and sine. 739 deltas = this.getXandYdeltas(); 740 } 741 742 ticksDelta *= Type.evaluate(this.visProp.scale); 743 744 // In case of insertTicks, adjust ticks distance to satisfy the minTicksDistance restriction. 745 // if (ev_it) { // } && this.minTicksDistance > Mat.eps) { 746 // ticksDelta = this.adjustTickDistance(ticksDelta, coordsZero, deltas); 747 // } 748 749 // Convert ticksdelta to the distance between two minor ticks 750 ticksDelta /= (ev_mt + 1); 751 this.ticksDelta = ticksDelta; 752 753 if (ticksDelta < Mat.eps) { 754 return; 755 } 756 757 // Position ticks from zero to the positive side while not reaching the upper boundary 758 tickPosition = 0; 759 if (!Type.evaluate(this.visProp.drawzero)) { 760 tickPosition = ticksDelta; 761 } 762 while (tickPosition <= bounds.upper + eps) { 763 // Only draw ticks when we are within bounds, ignore case where tickPosition < lower < upper 764 if (tickPosition >= bounds.lower - eps) { 765 this.processTickPosition(coordsZero, tickPosition, ticksDelta, deltas); 766 } 767 tickPosition += ticksDelta; 768 769 // Emergency out 770 if (bounds.upper - tickPosition > ticksDelta * 10000) { 771 break; 772 } 773 } 774 775 // Position ticks from zero (not inclusive) to the negative side while not reaching the lower boundary 776 tickPosition = -ticksDelta; 777 while (tickPosition >= bounds.lower - eps) { 778 // Only draw ticks when we are within bounds, ignore case where lower < upper < tickPosition 779 if (tickPosition <= bounds.upper + eps) { 780 this.processTickPosition(coordsZero, tickPosition, ticksDelta, deltas); 781 } 782 tickPosition -= ticksDelta; 783 784 // Emergency out 785 if (tickPosition - bounds.lower > ticksDelta * 10000) { 786 break; 787 } 788 } 789 }, 790 791 /** 792 * Calculates the distance between two major ticks in user units. 793 * <ul> 794 * <li> If the attribute "insertTicks" is false, the value of the attribute 795 * "ticksDistance" is returned. The attribute "minTicksDistance" is ignored in this case. 796 * <li> If the attribute "insertTicks" is true, the attribute "ticksDistance" is ignored. 797 * The distance between two major ticks is computed 798 * as <i>a 10<sup>i</sup></i>, where <i>a</i> is one of <i>{1, 2, 5}</i> and 799 * the number <i>a 10<sup>i</sup></i> is maximized such that there are approximately 800 * 6 major ticks and there are at least "minTicksDistance" pixel between minor ticks. 801 * The latter restriction has priority over the number of major ticks. 802 * </ul> 803 * @returns Number 804 * @private 805 */ 806 getDistanceMajorTicks: function () { 807 var delta, delta2, 808 b, d, dist, 809 scale, 810 numberMajorTicks = 5, 811 maxDist, minDist, ev_minti; 812 813 if (Type.evaluate(this.visProp.insertticks)) { 814 // Case of insertTicks==true: 815 // Here, we ignore the attribute 'margin' 816 b = this.getLowerAndUpperBounds(this.getZeroCoordinates(), ''); 817 818 dist = (b.upper - b.lower); 819 scale = Type.evaluate(this.visProp.scale); 820 821 maxDist = dist / (numberMajorTicks + 1) / scale; 822 minDist = Type.evaluate(this.visProp.minticksdistance) / scale; 823 ev_minti = Type.evaluate(this.visProp.minorticks); 824 825 d = this.getXandYdeltas(); 826 d.x *= this.board.unitX; 827 d.y *= this.board.unitY; 828 minDist /= Math.sqrt(d.x * d.x + d.y * d.y); 829 minDist *= (ev_minti + 1); 830 831 // Determine minimal delta to fulfill the minTicksDistance constraint 832 delta = Math.pow(10, Math.floor(Math.log(minDist) / Math.LN10)); 833 if (2 * delta >= minDist) { 834 delta *= 2; 835 } else if (5 * delta >= minDist) { 836 delta *= 5; 837 } 838 839 // Determine maximal delta to fulfill the constraint to have approx. "numberMajorTicks" majorTicks 840 delta2 = Math.pow(10, Math.floor(Math.log(maxDist) / Math.LN10)); 841 if (5 * delta2 < maxDist) { 842 delta2 *= 5; 843 } else if (2 * delta2 < maxDist) { 844 delta2 *= 2; 845 } 846 // Take the larger value of the two delta's, that is 847 // minTicksDistance has priority over numberMajorTicks 848 delta = Math.max(delta, delta2); 849 850 // < v1.6.0: 851 // delta = Math.pow(10, Math.floor(Math.log(0.6 * dist) / Math.LN10)); 852 // if (false && dist <= 6 * delta) { 853 // delta *= 0.5; 854 // } 855 return delta; 856 } 857 858 // Case of insertTicks==false 859 return Type.evaluate(this.visProp.ticksdistance); 860 }, 861 862 // /** 863 // * Auxiliary method used by {@link JXG.Ticks#generateEquidistantTicks} to adjust the 864 // * distance between two ticks depending on {@link JXG.Ticks#minTicksDistance} value 865 // * 866 // * @param {Number} ticksDelta distance between two major ticks in user coordinates 867 // * @param {JXG.Coords} coordsZero coordinates of the point considered zero 868 // * @param {Object} deltas x and y distance in pixel between two user units 869 // * @param {Object} bounds upper and lower bound of the tick positions in user units. 870 // * @private 871 // */ 872 // adjustTickDistance: function (ticksDelta, coordsZero, deltas) { 873 // var nx, 874 // ny, 875 // // bounds, 876 // distScr, 877 // sgn = 1, 878 // ev_mintd = Type.evaluate(this.visProp.minticksdistance), 879 // ev_minti = Type.evaluate(this.visProp.minorticks); 880 881 // if (this.line.elementClass === Const.OBJECT_CLASS_CURVE) { 882 // return ticksDelta; 883 // } 884 // // Seems to be ignored: 885 // // bounds = this.getLowerAndUpperBounds(coordsZero, "ticksdistance"); 886 887 // // distScr is the distance between two major Ticks in pixel 888 // nx = coordsZero.usrCoords[1] + deltas.x * ticksDelta; 889 // ny = coordsZero.usrCoords[2] + deltas.y * ticksDelta; 890 // distScr = coordsZero.distance( 891 // Const.COORDS_BY_SCREEN, 892 // new Coords(Const.COORDS_BY_USER, [nx, ny], this.board) 893 // ); 894 // // console.log(deltas, distScr, this.board.unitX, this.board.unitY, "ticksDelta:", ticksDelta); 895 896 // if (ticksDelta === 0.0) { 897 // return 0.0; 898 // } 899 900 // // console.log(":", distScr, ev_minti + 1, distScr / (ev_minti + 1), ev_mintd) 901 // while (false && distScr / (ev_minti + 1) < ev_mintd) { 902 // if (sgn === 1) { 903 // ticksDelta *= 2; 904 // } else { 905 // ticksDelta *= 5; 906 // } 907 // sgn *= -1; 908 909 // nx = coordsZero.usrCoords[1] + deltas.x * ticksDelta; 910 // ny = coordsZero.usrCoords[2] + deltas.y * ticksDelta; 911 // distScr = coordsZero.distance( 912 // Const.COORDS_BY_SCREEN, 913 // new Coords(Const.COORDS_BY_USER, [nx, ny], this.board) 914 // ); 915 // } 916 917 // return ticksDelta; 918 // }, 919 920 /** 921 * Auxiliary method used by {@link JXG.Ticks#generateEquidistantTicks} to create a tick 922 * in the line at the given tickPosition. 923 * 924 * @param {JXG.Coords} coordsZero coordinates of the point considered zero 925 * @param {Number} tickPosition current tick position relative to zero 926 * @param {Number} ticksDelta distance between two major ticks in user coordinates 927 * @param {Object} deltas x and y distance between two major ticks 928 * @private 929 */ 930 processTickPosition: function (coordsZero, tickPosition, ticksDelta, deltas) { 931 var x, 932 y, 933 tickCoords, 934 ti, 935 isLabelPosition, 936 ticksPerLabel = Type.evaluate(this.visProp.ticksperlabel), 937 labelVal = null; 938 939 // Calculates tick coordinates 940 if (this.line.elementClass === Const.OBJECT_CLASS_LINE) { 941 x = coordsZero.usrCoords[1] + tickPosition * deltas.x; 942 y = coordsZero.usrCoords[2] + tickPosition * deltas.y; 943 } else { 944 x = this.line.X(coordsZero + tickPosition); 945 y = this.line.Y(coordsZero + tickPosition); 946 } 947 tickCoords = new Coords(Const.COORDS_BY_USER, [x, y], this.board); 948 if (this.line.elementClass === Const.OBJECT_CLASS_CURVE) { 949 labelVal = coordsZero + tickPosition; 950 this.setTicksSizeVariables(labelVal); 951 } 952 953 // Test if tick is a major tick. 954 // This is the case if tickPosition/ticksDelta is 955 // a multiple of the number of minorticks+1 956 tickCoords.major = 957 Math.round(tickPosition / ticksDelta) % 958 (Type.evaluate(this.visProp.minorticks) + 1) === 959 0; 960 961 if (!ticksPerLabel) { 962 // In case of null, 0 or false, majorTicks are labelled 963 ticksPerLabel = Type.evaluate(this.visProp.minorticks) + 1; 964 } 965 isLabelPosition = Math.round(tickPosition / ticksDelta) % ticksPerLabel === 0; 966 967 // Compute the start position and the end position of a tick. 968 // If both positions are out of the canvas, ti is empty. 969 ti = this.createTickPath(tickCoords, tickCoords.major); 970 if (ti.length === 3) { 971 this.ticks.push(ti); 972 if (isLabelPosition && Type.evaluate(this.visProp.drawlabels)) { 973 // Create a label at this position 974 this.labelsData.push( 975 this.generateLabelData( 976 this.generateLabelText(tickCoords, coordsZero, labelVal), 977 tickCoords, 978 this.ticks.length 979 ) 980 ); 981 } else { 982 // minor ticks have no labels 983 this.labelsData.push(null); 984 } 985 } 986 }, 987 988 /** 989 * Creates ticks coordinates and labels based on {@link JXG.Ticks#fixedTicks} and {@link JXG.Ticks#labels}. 990 * 991 * @param {JXG.Coords} coordsZero Coordinates of the point considered zero 992 * @param {Object} bounds contains the lower and upper bounds for ticks placement 993 * @private 994 */ 995 generateFixedTicks: function (coordsZero, bounds) { 996 var tickCoords, 997 labelText, 998 i, 999 ti, 1000 x, 1001 y, 1002 eps2 = Mat.eps, 1003 fixedTick, 1004 hasLabelOverrides = Type.isArray(this.visProp.labels), 1005 deltas, 1006 ev_dl = Type.evaluate(this.visProp.drawlabels); 1007 1008 if (this.line.elementClass === Const.OBJECT_CLASS_LINE) { 1009 // Calculate x and y distances between two points on the line which are 1 unit apart 1010 // In essence, these are cosine and sine. 1011 deltas = this.getXandYdeltas(); 1012 } 1013 for (i = 0; i < this.fixedTicks.length; i++) { 1014 if (this.line.elementClass === Const.OBJECT_CLASS_LINE) { 1015 fixedTick = this.fixedTicks[i]; 1016 x = coordsZero.usrCoords[1] + fixedTick * deltas.x; 1017 y = coordsZero.usrCoords[2] + fixedTick * deltas.y; 1018 } else { 1019 fixedTick = coordsZero + this.fixedTicks[i]; 1020 x = this.line.X(fixedTick); 1021 y = this.line.Y(fixedTick); 1022 } 1023 tickCoords = new Coords(Const.COORDS_BY_USER, [x, y], this.board); 1024 1025 if (this.line.elementClass === Const.OBJECT_CLASS_CURVE) { 1026 this.setTicksSizeVariables(fixedTick); 1027 } 1028 1029 // Compute the start position and the end position of a tick. 1030 // If tick is out of the canvas, ti is empty. 1031 ti = this.createTickPath(tickCoords, true); 1032 if ( 1033 ti.length === 3 && 1034 fixedTick >= bounds.lower - eps2 && 1035 fixedTick <= bounds.upper + eps2 1036 ) { 1037 this.ticks.push(ti); 1038 1039 if (ev_dl && (hasLabelOverrides || Type.exists(this.visProp.labels[i]))) { 1040 labelText = hasLabelOverrides 1041 ? Type.evaluate(this.visProp.labels[i]) 1042 : fixedTick; 1043 this.labelsData.push( 1044 this.generateLabelData( 1045 this.generateLabelText(tickCoords, coordsZero, labelText), 1046 tickCoords, 1047 i 1048 ) 1049 ); 1050 } else { 1051 this.labelsData.push(null); 1052 } 1053 } 1054 } 1055 }, 1056 1057 /** 1058 * Calculates the x and y distances in user coordinates between two units in user space. 1059 * In essence, these are cosine and sine. The only work to be done is to determine 1060 * the direction of the line. 1061 * 1062 * @returns {Object} 1063 * @private 1064 */ 1065 getXandYdeltas: function () { 1066 var // Auxiliary points to store the start and end of the line according to its direction 1067 point1UsrCoords, 1068 point2UsrCoords, 1069 distP1P2 = this.line.point1.Dist(this.line.point2); 1070 1071 if (this.line.type === Const.OBJECT_TYPE_AXIS) { 1072 // When line is an Axis, direction depends on board coordinates system 1073 // Assume line.point1 and line.point2 are in correct order 1074 point1UsrCoords = this.line.point1.coords.usrCoords; 1075 point2UsrCoords = this.line.point2.coords.usrCoords; 1076 // Check if direction is incorrect, then swap 1077 if ( 1078 point1UsrCoords[1] > point2UsrCoords[1] || 1079 (Math.abs(point1UsrCoords[1] - point2UsrCoords[1]) < Mat.eps && 1080 point1UsrCoords[2] > point2UsrCoords[2]) 1081 ) { 1082 point1UsrCoords = this.line.point2.coords.usrCoords; 1083 point2UsrCoords = this.line.point1.coords.usrCoords; 1084 } 1085 } /* if (this.line.elementClass === Const.OBJECT_CLASS_LINE)*/ else { 1086 // Line direction is always from P1 to P2 for non axis types 1087 point1UsrCoords = this.line.point1.coords.usrCoords; 1088 point2UsrCoords = this.line.point2.coords.usrCoords; 1089 } 1090 return { 1091 x: (point2UsrCoords[1] - point1UsrCoords[1]) / distP1P2, 1092 y: (point2UsrCoords[2] - point1UsrCoords[2]) / distP1P2 1093 }; 1094 }, 1095 1096 /** 1097 * Check if (parts of) the tick is inside the canvas. The tick intersects the boundary 1098 * at two positions: [x[0], y[0]] and [x[1], y[1]] in screen coordinates. 1099 * @param {Array} x Array of length two 1100 * @param {Array} y Array of length two 1101 * @return {Boolean} true if parts of the tick are inside of the canvas or on the boundary. 1102 */ 1103 _isInsideCanvas: function (x, y, m) { 1104 var cw = this.board.canvasWidth, 1105 ch = this.board.canvasHeight; 1106 1107 if (m === undefined) { 1108 m = 0; 1109 } 1110 return ( 1111 (x[0] >= m && x[0] <= cw - m && y[0] >= m && y[0] <= ch - m) || 1112 (x[1] >= m && x[1] <= cw - m && y[1] >= m && y[1] <= ch - m) 1113 ); 1114 }, 1115 1116 /** 1117 * @param {JXG.Coords} coords Coordinates of the tick on the line. 1118 * @param {Boolean} major True if tick is major tick. 1119 * @returns {Array} Array of length 3 containing path coordinates in screen coordinates 1120 * of the tick (arrays of length 2). 3rd entry is true if major tick otherwise false. 1121 * If the tick is outside of the canvas, the return array is empty. 1122 * @private 1123 */ 1124 createTickPath: function (coords, major) { 1125 var c, 1126 lineStdForm, 1127 intersection, 1128 dxs, 1129 dys, 1130 dxr, 1131 dyr, 1132 alpha, 1133 style, 1134 x = [-2000000, -2000000], 1135 y = [-2000000, -2000000], 1136 i, r, r_max, bb, full, delta, 1137 // Used for infinite ticks 1138 te0, te1, // Tick ending visProps 1139 dists; // 'signed' distances of intersections to the parent line 1140 1141 c = coords.scrCoords; 1142 if (major) { 1143 dxs = this.dxMaj; 1144 dys = this.dyMaj; 1145 style = this.majStyle; 1146 te0 = Type.evaluate(this.visProp.majortickendings[0]) > 0; 1147 te1 = Type.evaluate(this.visProp.majortickendings[1]) > 0; 1148 } else { 1149 dxs = this.dxMin; 1150 dys = this.dyMin; 1151 style = this.minStyle; 1152 te0 = Type.evaluate(this.visProp.tickendings[0]) > 0; 1153 te1 = Type.evaluate(this.visProp.tickendings[1]) > 0; 1154 } 1155 lineStdForm = [-dys * c[1] - dxs * c[2], dys, dxs]; 1156 1157 // For all ticks regardless if of finite or infinite 1158 // tick length the intersection with the canvas border is 1159 // computed. 1160 if (major && Type.evaluate(this.visProp.type) === "polar") { 1161 // polar style 1162 bb = this.board.getBoundingBox(); 1163 full = 2.0 * Math.PI; 1164 delta = full / 180; 1165 //ratio = this.board.unitY / this.board.X; 1166 1167 // usrCoords: Test if 'circle' is inside of the canvas 1168 c = coords.usrCoords; 1169 r = Math.sqrt(c[1] * c[1] + c[2] * c[2]); 1170 r_max = Math.max( 1171 Math.sqrt(bb[0] * bb[0] + bb[1] * bb[1]), 1172 Math.sqrt(bb[2] * bb[2] + bb[3] * bb[3]) 1173 ); 1174 1175 if (r < r_max) { 1176 // Now, switch to screen coords 1177 x = []; 1178 y = []; 1179 for (i = 0; i <= full; i += delta) { 1180 x.push( 1181 this.board.origin.scrCoords[1] + r * Math.cos(i) * this.board.unitX 1182 ); 1183 y.push( 1184 this.board.origin.scrCoords[2] + r * Math.sin(i) * this.board.unitY 1185 ); 1186 } 1187 return [x, y, major]; 1188 } 1189 } else { 1190 // line style 1191 if (style === 'infinite') { 1192 // Problematic are infinite ticks which have set tickendings:[0,1]. 1193 // For example, this is the default setting for minor ticks 1194 if (Type.evaluate(this.visProp.ignoreinfinitetickendings)) { 1195 te0 = te1 = true; 1196 } 1197 intersection = Geometry.meetLineBoard(lineStdForm, this.board); 1198 1199 if (te0 && te1) { 1200 x[0] = intersection[0].scrCoords[1]; 1201 x[1] = intersection[1].scrCoords[1]; 1202 y[0] = intersection[0].scrCoords[2]; 1203 y[1] = intersection[1].scrCoords[2]; 1204 } else { 1205 // Assuming the usrCoords of both intersections are normalized, a 'signed distance' 1206 // with respect to the parent line is computed for the intersections. The sign is 1207 // used to conclude whether the point is either at the left or right side of the 1208 // line. The magnitude can be used to compare the points and determine which point 1209 // is closest to the line. 1210 dists = [ 1211 Mat.innerProduct( 1212 intersection[0].usrCoords.slice(1, 3), 1213 this.line.stdform.slice(1, 3) 1214 ) + this.line.stdform[0], 1215 Mat.innerProduct( 1216 intersection[1].usrCoords.slice(1, 3), 1217 this.line.stdform.slice(1, 3) 1218 ) + this.line.stdform[0], 1219 ]; 1220 1221 // Reverse intersection array order if first intersection is not the leftmost one. 1222 if (dists[0] < dists[1]) { 1223 intersection.reverse(); 1224 dists.reverse(); 1225 } 1226 1227 if (te0) { // Left-infinite tick 1228 if (dists[0] < 0) { // intersections at the wrong side of line 1229 return []; 1230 } else if (dists[1] < 0) { // 'default' case, tick drawn from line to board bounds 1231 x[0] = intersection[0].scrCoords[1]; 1232 y[0] = intersection[0].scrCoords[2]; 1233 x[1] = c[1]; 1234 y[1] = c[2]; 1235 } else { // tick visible, but coords of tick on line are outside the visible area 1236 x[0] = intersection[0].scrCoords[1]; 1237 y[0] = intersection[0].scrCoords[2]; 1238 x[1] = intersection[1].scrCoords[1]; 1239 y[1] = intersection[1].scrCoords[2]; 1240 } 1241 } else if (te1) { // Right-infinite tick 1242 if (dists[1] > 0) { // intersections at the wrong side of line 1243 return []; 1244 } else if (dists[0] > 0) { // 'default' case, tick drawn from line to board bounds 1245 x[0] = c[1]; 1246 y[0] = c[2]; 1247 x[1] = intersection[1].scrCoords[1]; 1248 y[1] = intersection[1].scrCoords[2]; 1249 } else { // tick visible, but coords of tick on line are outside the visible area 1250 x[0] = intersection[0].scrCoords[1]; 1251 y[0] = intersection[0].scrCoords[2]; 1252 x[1] = intersection[1].scrCoords[1]; 1253 y[1] = intersection[1].scrCoords[2]; 1254 } 1255 } 1256 } 1257 } else { 1258 if (Type.evaluate(this.visProp.face) === ">") { 1259 alpha = Math.PI / 4; 1260 } else if (Type.evaluate(this.visProp.face) === "<") { 1261 alpha = -Math.PI / 4; 1262 } else { 1263 alpha = 0; 1264 } 1265 dxr = Math.cos(alpha) * dxs - Math.sin(alpha) * dys; 1266 dyr = Math.sin(alpha) * dxs + Math.cos(alpha) * dys; 1267 1268 x[0] = c[1] + dxr * te0; // Type.evaluate(this.visProp.tickendings[0]); 1269 y[0] = c[2] - dyr * te0; // Type.evaluate(this.visProp.tickendings[0]); 1270 x[1] = c[1]; 1271 y[1] = c[2]; 1272 1273 alpha = -alpha; 1274 dxr = Math.cos(alpha) * dxs - Math.sin(alpha) * dys; 1275 dyr = Math.sin(alpha) * dxs + Math.cos(alpha) * dys; 1276 1277 x[2] = c[1] - dxr * te1; // Type.evaluate(this.visProp.tickendings[1]); 1278 y[2] = c[2] + dyr * te1; // Type.evaluate(this.visProp.tickendings[1]); 1279 } 1280 1281 // Check if (parts of) the tick is inside the canvas. 1282 if (this._isInsideCanvas(x, y)) { 1283 return [x, y, major]; 1284 } 1285 } 1286 1287 return []; 1288 }, 1289 1290 /** 1291 * Format label texts. Show the desired number of digits 1292 * and use utf-8 minus sign. 1293 * @param {Number} value Number to be displayed 1294 * @return {String} The value converted into a string. 1295 * @private 1296 */ 1297 formatLabelText: function (value) { 1298 var labelText, 1299 digits, 1300 ev_s = Type.evaluate(this.visProp.scalesymbol); 1301 1302 if (Type.isNumber(value)) { 1303 digits = Type.evaluate(this.visProp.digits); 1304 1305 if (this.useLocale()) { 1306 labelText = this.formatNumberLocale(value, digits); 1307 } else { 1308 labelText = (Math.round(value * 1e11) / 1e11).toString(); 1309 1310 if ( 1311 labelText.length > Type.evaluate(this.visProp.maxlabellength) || 1312 labelText.indexOf("e") !== -1 1313 ) { 1314 if (Type.evaluate(this.visProp.precision) !== 3 && digits === 3) { 1315 // Use the deprecated attribute "precision" 1316 digits = Type.evaluate(this.visProp.precision); 1317 } 1318 1319 //labelText = value.toPrecision(digits).toString(); 1320 labelText = value.toExponential(digits).toString(); 1321 } 1322 } 1323 1324 if (Type.evaluate(this.visProp.beautifulscientificticklabels)) { 1325 labelText = this.beautifyScientificNotationLabel(labelText); 1326 } 1327 1328 if (labelText.indexOf(".") > -1 && labelText.indexOf("e") === -1) { 1329 // trim trailing zeros 1330 labelText = labelText.replace(/0+$/, ""); 1331 // trim trailing . 1332 labelText = labelText.replace(/\.$/, ""); 1333 } 1334 } else { 1335 labelText = value.toString(); 1336 } 1337 1338 if (ev_s.length > 0) { 1339 if (labelText === "1") { 1340 labelText = ev_s; 1341 } else if (labelText === "-1") { 1342 labelText = "-" + ev_s; 1343 } else if (labelText !== "0") { 1344 labelText = labelText + ev_s; 1345 } 1346 } 1347 1348 if (Type.evaluate(this.visProp.useunicodeminus)) { 1349 labelText = labelText.replace(/-/g, "\u2212"); 1350 } 1351 return labelText; 1352 }, 1353 1354 /** 1355 * Formats label texts to make labels displayed in scientific notation look beautiful. 1356 * For example, label 5.00e+6 will become 5•10⁶, label -1.00e-7 will become into -1•10⁻⁷ 1357 * @param {String} labelText - The label that we want to convert 1358 * @returns {String} If labelText was not in scientific notation, return labelText without modifications. 1359 * Otherwise returns beautified labelText with proper superscript notation. 1360 */ 1361 beautifyScientificNotationLabel: function (labelText) { 1362 var returnString; 1363 1364 if (labelText.indexOf("e") === -1) { 1365 return labelText; 1366 } 1367 1368 // Clean up trailing 0's, so numbers like 5.00e+6.0 for example become into 5e+6 1369 returnString = 1370 parseFloat(labelText.substring(0, labelText.indexOf("e"))) + 1371 labelText.substring(labelText.indexOf("e")); 1372 1373 // Replace symbols like -,0,1,2,3,4,5,6,7,8,9 with their superscript version. 1374 // Gets rid of + symbol since there is no need for it anymore. 1375 returnString = returnString.replace(/e(.*)$/g, function (match, $1) { 1376 var temp = "\u2022" + "10"; 1377 // Note: Since board ticks do not support HTTP elements like <sub>, we need to replace 1378 // all the numbers with superscript Unicode characters. 1379 temp += $1 1380 .replace(/-/g, "\u207B") 1381 .replace(/\+/g, "") 1382 .replace(/0/g, "\u2070") 1383 .replace(/1/g, "\u00B9") 1384 .replace(/2/g, "\u00B2") 1385 .replace(/3/g, "\u00B3") 1386 .replace(/4/g, "\u2074") 1387 .replace(/5/g, "\u2075") 1388 .replace(/6/g, "\u2076") 1389 .replace(/7/g, "\u2077") 1390 .replace(/8/g, "\u2078") 1391 .replace(/9/g, "\u2079"); 1392 1393 return temp; 1394 }); 1395 1396 return returnString; 1397 }, 1398 1399 /** 1400 * Creates the label text for a given tick. A value for the text can be provided as a number or string 1401 * 1402 * @param {JXG.Coords} tick The Coords-object of the tick to create a label for 1403 * @param {JXG.Coords} zero The Coords-object of line's zero 1404 * @param {Number|String} value A predefined value for this tick 1405 * @returns {String} 1406 * @private 1407 */ 1408 generateLabelText: function (tick, zero, value) { 1409 var labelText, distance; 1410 1411 // No value provided, equidistant, so assign distance as value 1412 if (!Type.exists(value)) { 1413 // could be null or undefined 1414 distance = this.getDistanceFromZero(zero, tick); 1415 if (Math.abs(distance) < Mat.eps) { 1416 // Point is zero 1417 return "0"; 1418 } 1419 value = distance / Type.evaluate(this.visProp.scale); 1420 } 1421 labelText = this.formatLabelText(value); 1422 1423 return labelText; 1424 }, 1425 1426 /** 1427 * Create a tick label data, i.e. text and coordinates 1428 * @param {String} labelText 1429 * @param {JXG.Coords} tick 1430 * @param {Number} tickNumber 1431 * @returns {Object} with properties 'x', 'y', 't' (text), 'i' (tick number) or null in case of o label 1432 * @private 1433 */ 1434 generateLabelData: function (labelText, tick, tickNumber) { 1435 var xa, ya, m, fs; 1436 1437 // Test if large portions of the label are inside of the canvas 1438 // This is the last chance to abandon the creation of the label if it is mostly 1439 // outside of the canvas. 1440 fs = Type.evaluate(this.visProp.label.fontsize); 1441 xa = [tick.scrCoords[1], tick.scrCoords[1]]; 1442 ya = [tick.scrCoords[2], tick.scrCoords[2]]; 1443 m = fs === undefined ? 12 : fs; 1444 m *= 0.5; 1445 if (!this._isInsideCanvas(xa, ya, m)) { 1446 return null; 1447 } 1448 1449 xa = Type.evaluate(this.visProp.label.offset[0]); 1450 ya = Type.evaluate(this.visProp.label.offset[1]); 1451 1452 return { 1453 x: tick.usrCoords[1] + xa / this.board.unitX, 1454 y: tick.usrCoords[2] + ya / this.board.unitY, 1455 t: labelText, 1456 i: tickNumber 1457 }; 1458 }, 1459 1460 /** 1461 * Recalculate the tick positions and the labels. 1462 * @returns {JXG.Ticks} 1463 */ 1464 update: function () { 1465 if (this.needsUpdate) { 1466 //this.visPropCalc.visible = Type.evaluate(this.visProp.visible); 1467 // A canvas with no width or height will create an endless loop, so ignore it 1468 if (this.board.canvasWidth !== 0 && this.board.canvasHeight !== 0) { 1469 this.calculateTicksCoordinates(); 1470 } 1471 // this.updateVisibility(this.line.visPropCalc.visible); 1472 // 1473 // for (var i = 0; i < this.labels.length; i++) { 1474 // if (this.labels[i] !== null) { 1475 // this.labels[i].prepareUpdate() 1476 // .updateVisibility(this.line.visPropCalc.visible) 1477 // .updateRenderer(); 1478 // } 1479 // } 1480 } 1481 1482 return this; 1483 }, 1484 1485 /** 1486 * Uses the boards renderer to update the arc. 1487 * @returns {JXG.Ticks} Reference to the object. 1488 */ 1489 updateRenderer: function () { 1490 if (!this.needsUpdate) { 1491 return this; 1492 } 1493 1494 if (this.visPropCalc.visible) { 1495 this.board.renderer.updateTicks(this); 1496 } 1497 this.updateRendererLabels(); 1498 1499 this.setDisplayRendNode(); 1500 // if (this.visPropCalc.visible != this.visPropOld.visible) { 1501 // this.board.renderer.display(this, this.visPropCalc.visible); 1502 // this.visPropOld.visible = this.visPropCalc.visible; 1503 // } 1504 1505 this.needsUpdate = false; 1506 return this; 1507 }, 1508 1509 /** 1510 * Updates the label elements of the major ticks. 1511 * 1512 * @private 1513 * @returns {JXG.Ticks} Reference to the object. 1514 */ 1515 updateRendererLabels: function () { 1516 var i, j, lenData, lenLabels, attr, label, ld, visible; 1517 1518 // The number of labels needed 1519 lenData = this.labelsData.length; 1520 // The number of labels which already exist 1521 // The existing labels are stored in this.labels[] 1522 // The new label positions and label values are stored in this.labelsData[] 1523 lenLabels = this.labels.length; 1524 1525 for (i = 0, j = 0; i < lenData; i++) { 1526 if (this.labelsData[i] === null) { 1527 // This is a tick without label 1528 continue; 1529 } 1530 1531 ld = this.labelsData[i]; 1532 if (j < lenLabels) { 1533 // Take an already existing text element 1534 label = this.labels[j]; 1535 label.setText(ld.t); 1536 label.setCoords(ld.x, ld.y); 1537 j++; 1538 } else { 1539 // A new text element is needed 1540 this.labelCounter += 1; 1541 1542 attr = { 1543 isLabel: true, 1544 layer: this.board.options.layer.line, 1545 highlightStrokeColor: this.board.options.text.strokeColor, 1546 highlightStrokeWidth: this.board.options.text.strokeWidth, 1547 highlightStrokeOpacity: this.board.options.text.strokeOpacity, 1548 priv: this.visProp.priv 1549 }; 1550 attr = Type.deepCopy(attr, this.visProp.label); 1551 attr.id = this.id + ld.i + "Label" + this.labelCounter; 1552 1553 label = JXG.createText(this.board, [ld.x, ld.y, ld.t], attr); 1554 this.addChild(label); 1555 label.setParents(this); 1556 label.isDraggable = false; 1557 label.dump = false; 1558 this.labels.push(label); 1559 } 1560 1561 // Look-ahead if the label inherits visibility. 1562 // If yes, update label. 1563 visible = Type.evaluate(this.visProp.label.visible); 1564 if (visible === 'inherit') { 1565 visible = this.visPropCalc.visible; 1566 } 1567 1568 label.prepareUpdate().updateVisibility(visible).updateRenderer(); 1569 1570 label.distanceX = Type.evaluate(this.visProp.label.offset[0]); 1571 label.distanceY = Type.evaluate(this.visProp.label.offset[1]); 1572 } 1573 1574 // Hide unused labels 1575 lenData = j; 1576 for (j = lenData; j < lenLabels; j++) { 1577 this.board.renderer.display(this.labels[j], false); 1578 // Tick labels have the attribute "visible: 'inherit'" 1579 // This must explicitely set to false, otherwise 1580 // this labels would be set to visible in the upcoming 1581 // update of the labels. 1582 this.labels[j].visProp.visible = this.labels[j].visPropCalc.visible = false; 1583 } 1584 1585 return this; 1586 }, 1587 1588 hideElement: function () { 1589 var i; 1590 1591 JXG.deprecated("Element.hideElement()", "Element.setDisplayRendNode()"); 1592 1593 this.visPropCalc.visible = false; 1594 this.board.renderer.display(this, false); 1595 for (i = 0; i < this.labels.length; i++) { 1596 if (Type.exists(this.labels[i])) { 1597 this.labels[i].hideElement(); 1598 } 1599 } 1600 1601 return this; 1602 }, 1603 1604 showElement: function () { 1605 var i; 1606 1607 JXG.deprecated("Element.showElement()", "Element.setDisplayRendNode()"); 1608 1609 this.visPropCalc.visible = true; 1610 this.board.renderer.display(this, false); 1611 1612 for (i = 0; i < this.labels.length; i++) { 1613 if (Type.exists(this.labels[i])) { 1614 this.labels[i].showElement(); 1615 } 1616 } 1617 1618 return this; 1619 } 1620 } 1621 ); 1622 1623 /** 1624 * @class Ticks are used as distance markers on a line or curve. 1625 * They are mainly used for axis elements and slider elements. Ticks may stretch infinitely 1626 * or finitely, which can be set with {@link Ticks#majorHeight} and {@link Ticks#minorHeight}. 1627 * <p> 1628 * There are the following ways to position the tick lines: 1629 * <ol> 1630 * <li> If an array is given as optional second parameter for the constructor 1631 * like e.g. <tt>board.create('ticks', [line, [1, 4, 5]])</tt>, then there will be (fixed) ticks at position 1632 * 1, 4 and 5 of the line. 1633 * <li> If there is only one parameter given, like e.g. <tt>board.create('ticks', [line])</tt>, the ticks will be set 1634 * equidistant across the line element. There are two variants: 1635 * <ol type="i"> 1636 * <li> Setting the attribute <tt>insertTicks:false</tt>: in this case the distance between two major ticks 1637 * is determined by the attribute <tt>ticksDistance</tt>. This distance is given in user units. 1638 * <li> Setting the attribute <tt>insertTicks:true</tt>: in this case the distance between two major ticks 1639 * is set automatically, depending on 1640 * <ul> 1641 * <li> the size of the board, 1642 * <li> the attribute <tt>minTicksDistance</tt>, which is the minimum distance between two consecutive minor ticks (in pixel). 1643 * </ul> 1644 * The distance between two major ticks is a value of the form 1645 * <i>a 10<sup>i</sup></i>, where <i>a</i> is one of <i>{1, 2, 5}</i> and 1646 * the number <i>a 10<sup>i</sup></i> is maximized such that there are approximately 1647 * 6 major ticks and there are at least "minTicksDistance" pixel between minor ticks. 1648 * </ol> 1649 * <p> 1650 * For arbitrary lines (and not axes) a "zero coordinate" is determined 1651 * which defines where the first tick is positioned. This zero coordinate 1652 * can be altered with the attribute <tt>anchor</tt>. Possible values are "left", "middle", "right" or a number. 1653 * The default value is "left". 1654 * 1655 * @pseudo 1656 * @description 1657 * @name Ticks 1658 * @augments JXG.Ticks 1659 * @constructor 1660 * @type JXG.Ticks 1661 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 1662 * @param {JXG.Line|JXG.Curve} line The parents consist of the line or curve the ticks are going to be attached to. 1663 * @param {Array} [ticks] Optional array of numbers. If given, a fixed number of static ticks is created 1664 * at these user-supplied positions. 1665 * <p> 1666 * Deprecated: Alternatively, a number defining the distance between two major ticks 1667 * can be specified. However, this is meanwhile ignored. Use attribute <tt>ticksDistance</tt> instead. 1668 * 1669 * @example 1670 * // Add ticks to line 'l1' through 'p1' and 'p2'. The major ticks are 1671 * // two units apart and 40 px long. 1672 * var p1 = board.create('point', [0, 3]); 1673 * var p2 = board.create('point', [1, 3]); 1674 * var l1 = board.create('line', [p1, p2]); 1675 * var t = board.create('ticks', [l1], { 1676 * ticksDistance: 2, 1677 * majorHeight: 40 1678 * }); 1679 * </pre><div class="jxgbox" id="JXGee7f2d68-75fc-4ec0-9931-c76918427e63" style="width: 300px; height: 300px;"></div> 1680 * <script type="text/javascript"> 1681 * (function () { 1682 * var board = JXG.JSXGraph.initBoard('JXGee7f2d68-75fc-4ec0-9931-c76918427e63', { 1683 * boundingbox: [-1, 7, 7, -1], axis: true, showcopyright: false, shownavigation: true}); 1684 * var p1 = board.create('point', [0, 3]); 1685 * var p2 = board.create('point', [1, 3]); 1686 * var l1 = board.create('line', [p1, p2]); 1687 * var t = board.create('ticks', [l1, 2], {ticksDistance: 2, majorHeight: 40}); 1688 * })(); 1689 * </script><pre> 1690 */ 1691 JXG.createTicks = function (board, parents, attributes) { 1692 var el, 1693 dist, 1694 attr = Type.copyAttributes(attributes, board.options, "ticks"); 1695 1696 if (parents.length < 2) { 1697 dist = attr.ticksdistance; // Will be ignored anyhow and attr.ticksDistance will be used instead 1698 } else { 1699 dist = parents[1]; 1700 } 1701 1702 if ( 1703 parents[0].elementClass === Const.OBJECT_CLASS_LINE || 1704 parents[0].elementClass === Const.OBJECT_CLASS_CURVE 1705 ) { 1706 el = new JXG.Ticks(parents[0], dist, attr); 1707 } else { 1708 throw new Error( 1709 "JSXGraph: Can't create Ticks with parent types '" + typeof parents[0] + "'." 1710 ); 1711 } 1712 1713 // deprecated 1714 if (Type.isFunction(attr.generatelabelvalue)) { 1715 el.generateLabelText = attr.generatelabelvalue; 1716 } 1717 if (Type.isFunction(attr.generatelabeltext)) { 1718 el.generateLabelText = attr.generatelabeltext; 1719 } 1720 1721 el.setParents(parents[0]); 1722 el.isDraggable = true; 1723 el.fullUpdate(parents[0].visPropCalc.visible); 1724 1725 return el; 1726 }; 1727 1728 /** 1729 * @class Hatches can be used to mark congruent lines or curves. 1730 * @pseudo 1731 * @description 1732 * @name Hatch 1733 * @augments JXG.Ticks 1734 * @constructor 1735 * @type JXG.Ticks 1736 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 1737 * @param {JXG.Line|JXG.curve} line The line or curve the hatch marks are going to be attached to. 1738 * @param {Number} numberofhashes Number of dashes. The distance of the hashes can be controlled with the attribute ticksDistance. 1739 * @example 1740 * // Create an axis providing two coords pairs. 1741 * var p1 = board.create('point', [0, 3]); 1742 * var p2 = board.create('point', [1, 3]); 1743 * var l1 = board.create('line', [p1, p2]); 1744 * var t = board.create('hatch', [l1, 3]); 1745 * </pre><div class="jxgbox" id="JXG4a20af06-4395-451c-b7d1-002757cf01be" style="width: 300px; height: 300px;"></div> 1746 * <script type="text/javascript"> 1747 * (function () { 1748 * var board = JXG.JSXGraph.initBoard('JXG4a20af06-4395-451c-b7d1-002757cf01be', {boundingbox: [-1, 7, 7, -1], showcopyright: false, shownavigation: false}); 1749 * var p1 = board.create('point', [0, 3]); 1750 * var p2 = board.create('point', [1, 3]); 1751 * var l1 = board.create('line', [p1, p2]); 1752 * var t = board.create('hatch', [l1, 3]); 1753 * })(); 1754 * </script><pre> 1755 * 1756 * @example 1757 * // Alter the position of the hatch 1758 * 1759 * var p = board.create('point', [-5, 0]); 1760 * var q = board.create('point', [5, 0]); 1761 * var li = board.create('line', [p, q]); 1762 * var h = board.create('hatch', [li, 2], {anchor: 0.2, ticksDistance:0.4}); 1763 * 1764 * </pre><div id="JXG05d720ee-99c9-11e6-a9c7-901b0e1b8723" class="jxgbox" style="width: 300px; height: 300px;"></div> 1765 * <script type="text/javascript"> 1766 * (function() { 1767 * var board = JXG.JSXGraph.initBoard('JXG05d720ee-99c9-11e6-a9c7-901b0e1b8723', 1768 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 1769 * 1770 * var p = board.create('point', [-5, 0]); 1771 * var q = board.create('point', [5, 0]); 1772 * var li = board.create('line', [p, q]); 1773 * var h = board.create('hatch', [li, 2], {anchor: 0.2, ticksDistance:0.4}); 1774 * 1775 * })(); 1776 * 1777 * </script><pre> 1778 * 1779 * @example 1780 * // Alternative hatch faces 1781 * 1782 * var li = board.create('line', [[-6,0], [6,3]]); 1783 * var h1 = board.create('hatch', [li, 2], {tickEndings: [1,1], face:'|'}); 1784 * var h2 = board.create('hatch', [li, 2], {tickEndings: [1,1], face:'>', anchor: 0.3}); 1785 * var h3 = board.create('hatch', [li, 2], {tickEndings: [1,1], face:'<', anchor: 0.7}); 1786 * 1787 * </pre><div id="JXG974f7e89-eac8-4187-9aa3-fb8068e8384b" class="jxgbox" style="width: 300px; height: 300px;"></div> 1788 * <script type="text/javascript"> 1789 * (function() { 1790 * var board = JXG.JSXGraph.initBoard('JXG974f7e89-eac8-4187-9aa3-fb8068e8384b', 1791 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 1792 * // Alternative hatch faces 1793 * 1794 * var li = board.create('line', [[-6,0], [6,3]]); 1795 * var h1 = board.create('hatch', [li, 2], {tickEndings: [1,1], face:'|'}); 1796 * var h2 = board.create('hatch', [li, 2], {tickEndings: [1,1], face:'>', anchor: 0.3}); 1797 * var h3 = board.create('hatch', [li, 2], {tickEndings: [1,1], face:'<', anchor: 0.7}); 1798 * 1799 * })(); 1800 * 1801 * </script><pre> 1802 * 1803 */ 1804 JXG.createHatchmark = function (board, parents, attributes) { 1805 var num, i, base, width, totalwidth, el, 1806 pos = [], 1807 attr = Type.copyAttributes(attributes, board.options, 'hatch'); 1808 1809 if ( 1810 (parents[0].elementClass !== Const.OBJECT_CLASS_LINE && 1811 parents[0].elementClass !== Const.OBJECT_CLASS_CURVE) || 1812 typeof parents[1] !== "number" 1813 ) { 1814 throw new Error( 1815 "JSXGraph: Can't create Hatch mark with parent types '" + 1816 typeof parents[0] + 1817 "' and '" + 1818 typeof parents[1] + 1819 " and ''" + 1820 typeof parents[2] + 1821 "'." 1822 ); 1823 } 1824 1825 num = parents[1]; 1826 width = attr.ticksdistance; 1827 totalwidth = (num - 1) * width; 1828 base = -totalwidth * 0.5; 1829 1830 for (i = 0; i < num; i++) { 1831 pos[i] = base + i * width; 1832 } 1833 1834 el = board.create('ticks', [parents[0], pos], attr); 1835 el.elType = 'hatch'; 1836 parents[0].inherits.push(el); 1837 1838 return el; 1839 }; 1840 1841 JXG.registerElement("ticks", JXG.createTicks); 1842 JXG.registerElement("hash", JXG.createHatchmark); 1843 JXG.registerElement("hatch", JXG.createHatchmark); 1844 1845 export default JXG.Ticks; 1846 // export default { 1847 // Ticks: JXG.Ticks, 1848 // createTicks: JXG.createTicks, 1849 // createHashmark: JXG.createHatchmark, 1850 // createHatchmark: JXG.createHatchmark 1851 // }; 1852