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