1 /* 2 Copyright 2008-2025 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_mt; 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_mt = 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_mt + 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 853 // Take the larger value of the two delta's, that is 854 // minTicksDistance has priority over numberMajorTicks 855 delta = Math.max(delta, delta2); 856 857 // < v1.6.0: 858 // delta = Math.pow(10, Math.floor(Math.log(0.6 * dist) / Math.LN10)); 859 // if (false && dist <= 6 * delta) { 860 // delta *= 0.5; 861 // } 862 return delta; 863 } 864 865 // Case of insertTicks==false 866 return this.evalVisProp('ticksdistance'); 867 }, 868 869 // /** 870 // * Auxiliary method used by {@link JXG.Ticks#generateEquidistantTicks} to adjust the 871 // * distance between two ticks depending on {@link JXG.Ticks#minTicksDistance} value 872 // * 873 // * @param {Number} ticksDelta distance between two major ticks in user coordinates 874 // * @param {JXG.Coords} coordsZero coordinates of the point considered zero 875 // * @param {Object} deltas x and y distance in pixel between two user units 876 // * @param {Object} bounds upper and lower bound of the tick positions in user units. 877 // * @private 878 // */ 879 // adjustTickDistance: function (ticksDelta, coordsZero, deltas) { 880 // var nx, 881 // ny, 882 // // bounds, 883 // distScr, 884 // sgn = 1, 885 // ev_mintd = this.evalVisProp('minticksdistance'), 886 // ev_minti = this.evalVisProp('minorticks'); 887 888 // if (this.line.elementClass === Const.OBJECT_CLASS_CURVE) { 889 // return ticksDelta; 890 // } 891 // // Seems to be ignored: 892 // // bounds = this.getLowerAndUpperBounds(coordsZero, "ticksdistance"); 893 894 // // distScr is the distance between two major Ticks in pixel 895 // nx = coordsZero.usrCoords[1] + deltas.x * ticksDelta; 896 // ny = coordsZero.usrCoords[2] + deltas.y * ticksDelta; 897 // distScr = coordsZero.distance( 898 // Const.COORDS_BY_SCREEN, 899 // new Coords(Const.COORDS_BY_USER, [nx, ny], this.board) 900 // ); 901 // // console.log(deltas, distScr, this.board.unitX, this.board.unitY, "ticksDelta:", ticksDelta); 902 903 // if (ticksDelta === 0.0) { 904 // return 0.0; 905 // } 906 907 // // console.log(":", distScr, ev_minti + 1, distScr / (ev_minti + 1), ev_mintd) 908 // while (false && distScr / (ev_minti + 1) < ev_mintd) { 909 // if (sgn === 1) { 910 // ticksDelta *= 2; 911 // } else { 912 // ticksDelta *= 5; 913 // } 914 // sgn *= -1; 915 916 // nx = coordsZero.usrCoords[1] + deltas.x * ticksDelta; 917 // ny = coordsZero.usrCoords[2] + deltas.y * ticksDelta; 918 // distScr = coordsZero.distance( 919 // Const.COORDS_BY_SCREEN, 920 // new Coords(Const.COORDS_BY_USER, [nx, ny], this.board) 921 // ); 922 // } 923 924 // return ticksDelta; 925 // }, 926 927 /** 928 * Auxiliary method used by {@link JXG.Ticks#generateEquidistantTicks} to create a tick 929 * in the line at the given tickPosition. 930 * 931 * @param {JXG.Coords} coordsZero coordinates of the point considered zero 932 * @param {Number} tickPosition current tick position relative to zero 933 * @param {Number} ticksDelta distance between two major ticks in user coordinates 934 * @param {Object} deltas x and y distance between two major ticks 935 * @private 936 */ 937 processTickPosition: function (coordsZero, tickPosition, ticksDelta, deltas) { 938 var x, 939 y, 940 tickCoords, 941 ti, 942 ev_mt, 943 isLabelPosition, 944 ticksPerLabel = this.evalVisProp('ticksperlabel'), 945 labelVal = null; 946 947 // Calculates tick coordinates 948 if (this.line.elementClass === Const.OBJECT_CLASS_LINE) { 949 x = coordsZero.usrCoords[1] + tickPosition * deltas.x; 950 y = coordsZero.usrCoords[2] + tickPosition * deltas.y; 951 } else { 952 x = this.line.X(coordsZero + tickPosition); 953 y = this.line.Y(coordsZero + tickPosition); 954 } 955 tickCoords = new Coords(Const.COORDS_BY_USER, [x, y], this.board); 956 if (this.line.elementClass === Const.OBJECT_CLASS_CURVE) { 957 labelVal = coordsZero + tickPosition; 958 this.setTicksSizeVariables(labelVal); 959 } 960 961 ev_mt = this.evalVisProp('minorticks'); 962 // Test if tick is a major tick. 963 // This is the case if tickPosition/ticksDelta is 964 // a multiple of the number of minorticks+1 965 tickCoords.major = 966 Math.round(tickPosition / ticksDelta) % (ev_mt + 1) === 0; 967 968 if (!ticksPerLabel) { 969 // In case of null, 0 or false, majorTicks are labelled 970 ticksPerLabel = ev_mt + 1; 971 } 972 isLabelPosition = Math.round(tickPosition / ticksDelta) % ticksPerLabel === 0; 973 974 // Compute the start position and the end position of a tick. 975 // If both positions are out of the canvas, ti is empty. 976 ti = this.createTickPath(tickCoords, tickCoords.major); 977 if (ti.length === 3) { 978 this.ticks.push(ti); 979 if (isLabelPosition && this.evalVisProp('drawlabels')) { 980 // Create a label at this position 981 this.labelsData.push( 982 this.generateLabelData( 983 this.generateLabelText(tickCoords, coordsZero, labelVal), 984 tickCoords, 985 this.ticks.length 986 ) 987 ); 988 } else { 989 // minor ticks have no labels 990 this.labelsData.push(null); 991 } 992 } 993 }, 994 995 /** 996 * Creates ticks coordinates and labels based on {@link JXG.Ticks#fixedTicks} and {@link JXG.Ticks#labels}. 997 * 998 * @param {JXG.Coords} coordsZero Coordinates of the point considered zero 999 * @param {Object} bounds contains the lower and upper bounds for ticks placement 1000 * @private 1001 */ 1002 generateFixedTicks: function (coordsZero, bounds) { 1003 var tickCoords, 1004 labelText, 1005 i, 1006 ti, 1007 x, 1008 y, 1009 eps2 = Mat.eps, 1010 fixedTick, 1011 hasLabelOverrides = Type.isArray(this.visProp.labels), 1012 deltas, 1013 ev_dl = this.evalVisProp('drawlabels'); 1014 1015 if (this.line.elementClass === Const.OBJECT_CLASS_LINE) { 1016 // Calculate x and y distances between two points on the line which are 1 unit apart 1017 // In essence, these are cosine and sine. 1018 deltas = this.getXandYdeltas(); 1019 } 1020 for (i = 0; i < this.fixedTicks.length; i++) { 1021 if (this.line.elementClass === Const.OBJECT_CLASS_LINE) { 1022 fixedTick = this.fixedTicks[i]; 1023 x = coordsZero.usrCoords[1] + fixedTick * deltas.x; 1024 y = coordsZero.usrCoords[2] + fixedTick * deltas.y; 1025 } else { 1026 fixedTick = coordsZero + this.fixedTicks[i]; 1027 x = this.line.X(fixedTick); 1028 y = this.line.Y(fixedTick); 1029 } 1030 tickCoords = new Coords(Const.COORDS_BY_USER, [x, y], this.board); 1031 1032 if (this.line.elementClass === Const.OBJECT_CLASS_CURVE) { 1033 this.setTicksSizeVariables(fixedTick); 1034 } 1035 1036 // Compute the start position and the end position of a tick. 1037 // If tick is out of the canvas, ti is empty. 1038 ti = this.createTickPath(tickCoords, true); 1039 if ( 1040 ti.length === 3 && 1041 fixedTick >= bounds.lower - eps2 && 1042 fixedTick <= bounds.upper + eps2 1043 ) { 1044 this.ticks.push(ti); 1045 1046 if (ev_dl && (hasLabelOverrides || Type.exists(this.visProp.labels[i]))) { 1047 labelText = hasLabelOverrides 1048 ? this.evalVisProp('labels.' + i) 1049 : fixedTick; 1050 this.labelsData.push( 1051 this.generateLabelData( 1052 this.generateLabelText(tickCoords, coordsZero, labelText), 1053 tickCoords, 1054 i 1055 ) 1056 ); 1057 } else { 1058 this.labelsData.push(null); 1059 } 1060 } 1061 } 1062 }, 1063 1064 /** 1065 * Calculates the x and y distances in user coordinates between two units in user space. 1066 * In essence, these are cosine and sine. The only work to be done is to determine 1067 * the direction of the line. 1068 * 1069 * @returns {Object} 1070 * @private 1071 */ 1072 getXandYdeltas: function () { 1073 var // Auxiliary points to store the start and end of the line according to its direction 1074 point1UsrCoords, 1075 point2UsrCoords, 1076 distP1P2 = this.line.point1.Dist(this.line.point2); 1077 1078 // if (this.line.type === Const.OBJECT_TYPE_AXIS) { 1079 // // When line is an Axis, direction depends on board coordinates system 1080 // // Assume line.point1 and line.point2 are in correct order 1081 // point1UsrCoords = this.line.point1.coords.usrCoords; 1082 // point2UsrCoords = this.line.point2.coords.usrCoords; 1083 // // Check if direction is incorrect, then swap 1084 // if ( 1085 // point1UsrCoords[1] > point2UsrCoords[1] || 1086 // (Math.abs(point1UsrCoords[1] - point2UsrCoords[1]) < Mat.eps && 1087 // point1UsrCoords[2] > point2UsrCoords[2]) 1088 // ) { 1089 // point1UsrCoords = this.line.point2.coords.usrCoords; 1090 // point2UsrCoords = this.line.point1.coords.usrCoords; 1091 // } 1092 // } /* if (this.line.elementClass === Const.OBJECT_CLASS_LINE)*/ else { 1093 // Line direction is always from P1 to P2 for non axis types 1094 point1UsrCoords = this.line.point1.coords.usrCoords; 1095 point2UsrCoords = this.line.point2.coords.usrCoords; 1096 // } 1097 return { 1098 x: (point2UsrCoords[1] - point1UsrCoords[1]) / distP1P2, 1099 y: (point2UsrCoords[2] - point1UsrCoords[2]) / distP1P2 1100 }; 1101 }, 1102 1103 /** 1104 * Check if (parts of) the tick is inside the canvas. The tick intersects the boundary 1105 * at two positions: [x[0], y[0]] and [x[1], y[1]] in screen coordinates. 1106 * @param {Array} x Array of length two 1107 * @param {Array} y Array of length two 1108 * @return {Boolean} true if parts of the tick are inside of the canvas or on the boundary. 1109 */ 1110 _isInsideCanvas: function (x, y, m) { 1111 var cw = this.board.canvasWidth, 1112 ch = this.board.canvasHeight; 1113 1114 if (m === undefined) { 1115 m = 0; 1116 } 1117 return ( 1118 (x[0] >= m && x[0] <= cw - m && y[0] >= m && y[0] <= ch - m) || 1119 (x[1] >= m && x[1] <= cw - m && y[1] >= m && y[1] <= ch - m) 1120 ); 1121 }, 1122 1123 /** 1124 * @param {JXG.Coords} coords Coordinates of the tick on the line. 1125 * @param {Boolean} major True if tick is major tick. 1126 * @returns {Array} Array of length 3 containing path coordinates in screen coordinates 1127 * of the tick (arrays of length 2). 3rd entry is true if major tick otherwise false. 1128 * If the tick is outside of the canvas, the return array is empty. 1129 * @private 1130 */ 1131 createTickPath: function (coords, major) { 1132 var c, 1133 lineStdForm, 1134 intersection, 1135 dxs, 1136 dys, 1137 dxr, 1138 dyr, 1139 alpha, 1140 style, 1141 x = [-2000000, -2000000], 1142 y = [-2000000, -2000000], 1143 i, r, r_max, bb, full, delta, 1144 // Used for infinite ticks 1145 te0, te1, // Tick ending visProps 1146 dists; // 'signed' distances of intersections to the parent line 1147 1148 c = coords.scrCoords; 1149 if (major) { 1150 dxs = this.dxMaj; 1151 dys = this.dyMaj; 1152 style = this.majStyle; 1153 te0 = this.evalVisProp('majortickendings.0') > 0; 1154 te1 = this.evalVisProp('majortickendings.1') > 0; 1155 } else { 1156 dxs = this.dxMin; 1157 dys = this.dyMin; 1158 style = this.minStyle; 1159 te0 = this.evalVisProp('tickendings.0') > 0; 1160 te1 = this.evalVisProp('tickendings.1') > 0; 1161 } 1162 lineStdForm = [-dys * c[1] - dxs * c[2], dys, dxs]; 1163 1164 // For all ticks regardless if of finite or infinite 1165 // tick length the intersection with the canvas border is 1166 // computed. 1167 if (major && this.evalVisProp('type') === "polar") { 1168 // polar style 1169 bb = this.board.getBoundingBox(); 1170 full = 2.0 * Math.PI; 1171 delta = full / 180; 1172 //ratio = this.board.unitY / this.board.X; 1173 1174 // usrCoords: Test if 'circle' is inside of the canvas 1175 c = coords.usrCoords; 1176 r = Mat.hypot(c[1], c[2]); 1177 r_max = Math.max( 1178 Mat.hypot(bb[0], bb[1]), 1179 Mat.hypot(bb[2], bb[3]) 1180 ); 1181 1182 if (r < r_max) { 1183 // Now, switch to screen coords 1184 x = []; 1185 y = []; 1186 for (i = 0; i <= full; i += delta) { 1187 x.push( 1188 this.board.origin.scrCoords[1] + r * Math.cos(i) * this.board.unitX 1189 ); 1190 y.push( 1191 this.board.origin.scrCoords[2] + r * Math.sin(i) * this.board.unitY 1192 ); 1193 } 1194 return [x, y, major]; 1195 } 1196 } else { 1197 // line style 1198 if (style === 'infinite') { 1199 // Problematic are infinite ticks which have set tickendings:[0,1]. 1200 // For example, this is the default setting for minor ticks 1201 if (this.evalVisProp('ignoreinfinitetickendings')) { 1202 te0 = te1 = true; 1203 } 1204 intersection = Geometry.meetLineBoard(lineStdForm, this.board); 1205 1206 if (te0 && te1) { 1207 x[0] = intersection[0].scrCoords[1]; 1208 x[1] = intersection[1].scrCoords[1]; 1209 y[0] = intersection[0].scrCoords[2]; 1210 y[1] = intersection[1].scrCoords[2]; 1211 } else { 1212 // Assuming the usrCoords of both intersections are normalized, a 'signed distance' 1213 // with respect to the parent line is computed for the intersections. The sign is 1214 // used to conclude whether the point is either at the left or right side of the 1215 // line. The magnitude can be used to compare the points and determine which point 1216 // is closest to the line. 1217 dists = [ 1218 Mat.innerProduct( 1219 intersection[0].usrCoords.slice(1, 3), 1220 this.line.stdform.slice(1, 3) 1221 ) + this.line.stdform[0], 1222 Mat.innerProduct( 1223 intersection[1].usrCoords.slice(1, 3), 1224 this.line.stdform.slice(1, 3) 1225 ) + this.line.stdform[0] 1226 ]; 1227 1228 // Reverse intersection array order if first intersection is not the leftmost one. 1229 if (dists[0] < dists[1]) { 1230 intersection.reverse(); 1231 dists.reverse(); 1232 } 1233 1234 if (te0) { // Left-infinite tick 1235 if (dists[0] < 0) { // intersections at the wrong side of line 1236 return []; 1237 } else if (dists[1] < 0) { // 'default' case, tick drawn from line to board bounds 1238 x[0] = intersection[0].scrCoords[1]; 1239 y[0] = intersection[0].scrCoords[2]; 1240 x[1] = c[1]; 1241 y[1] = c[2]; 1242 } else { // tick visible, but coords of tick on line are outside the visible area 1243 x[0] = intersection[0].scrCoords[1]; 1244 y[0] = intersection[0].scrCoords[2]; 1245 x[1] = intersection[1].scrCoords[1]; 1246 y[1] = intersection[1].scrCoords[2]; 1247 } 1248 } else if (te1) { // Right-infinite tick 1249 if (dists[1] > 0) { // intersections at the wrong side of line 1250 return []; 1251 } else if (dists[0] > 0) { // 'default' case, tick drawn from line to board bounds 1252 x[0] = c[1]; 1253 y[0] = c[2]; 1254 x[1] = intersection[1].scrCoords[1]; 1255 y[1] = intersection[1].scrCoords[2]; 1256 } else { // tick visible, but coords of tick on line are outside the visible area 1257 x[0] = intersection[0].scrCoords[1]; 1258 y[0] = intersection[0].scrCoords[2]; 1259 x[1] = intersection[1].scrCoords[1]; 1260 y[1] = intersection[1].scrCoords[2]; 1261 } 1262 } 1263 } 1264 } else { 1265 if (this.evalVisProp('face') === ">") { 1266 alpha = Math.PI / 4; 1267 } else if (this.evalVisProp('face') === "<") { 1268 alpha = -Math.PI / 4; 1269 } else { 1270 alpha = 0; 1271 } 1272 dxr = Math.cos(alpha) * dxs - Math.sin(alpha) * dys; 1273 dyr = Math.sin(alpha) * dxs + Math.cos(alpha) * dys; 1274 1275 x[0] = c[1] + dxr * te0; 1276 y[0] = c[2] - dyr * te0; 1277 x[1] = c[1]; 1278 y[1] = c[2]; 1279 1280 alpha = -alpha; 1281 dxr = Math.cos(alpha) * dxs - Math.sin(alpha) * dys; 1282 dyr = Math.sin(alpha) * dxs + Math.cos(alpha) * dys; 1283 1284 x[2] = c[1] - dxr * te1; 1285 y[2] = c[2] + dyr * te1; 1286 } 1287 1288 // Check if (parts of) the tick is inside the canvas. 1289 if (this._isInsideCanvas(x, y)) { 1290 return [x, y, major]; 1291 } 1292 } 1293 1294 return []; 1295 }, 1296 1297 /** 1298 * Format label texts. Show the desired number of digits 1299 * and use utf-8 minus sign. 1300 * @param {Number} value Number to be displayed 1301 * @return {String} The value converted into a string. 1302 * @private 1303 */ 1304 formatLabelText: function (value) { 1305 var labelText, 1306 digits, 1307 ev_um = this.evalVisProp('label.usemathjax'), 1308 ev_uk = this.evalVisProp('label.usekatex'), 1309 ev_s = this.evalVisProp('scalesymbol'); 1310 1311 if (Type.isNumber(value)) { 1312 if (this.evalVisProp('label.tofraction')) { 1313 if (ev_um) { 1314 labelText = '\\(' + Type.toFraction(value, true) + '\\)'; 1315 } else { 1316 labelText = Type.toFraction(value, ev_uk); 1317 } 1318 } else { 1319 digits = this.evalVisProp('digits'); 1320 if (this.useLocale()) { 1321 labelText = this.formatNumberLocale(value, digits); 1322 } else { 1323 labelText = (Math.round(value * 1e11) / 1e11).toString(); 1324 1325 if ( 1326 labelText.length > this.evalVisProp('maxlabellength') || 1327 labelText.indexOf("e") !== -1 1328 ) { 1329 if (this.evalVisProp('precision') !== 3 && digits === 3) { 1330 // Use the deprecated attribute "precision" 1331 digits = this.evalVisProp('precision'); 1332 } 1333 1334 //labelText = value.toPrecision(digits).toString(); 1335 labelText = value.toExponential(digits).toString(); 1336 } 1337 } 1338 } 1339 1340 if (this.evalVisProp('beautifulscientificticklabels')) { 1341 labelText = this.beautifyScientificNotationLabel(labelText); 1342 } 1343 1344 if (labelText.indexOf(".") > -1 && labelText.indexOf("e") === -1) { 1345 // trim trailing zeros 1346 labelText = labelText.replace(/0+$/, ""); 1347 // trim trailing . 1348 labelText = labelText.replace(/\.$/, ""); 1349 } 1350 } else { 1351 labelText = value.toString(); 1352 } 1353 1354 if (ev_s.length > 0) { 1355 if (labelText === "1") { 1356 labelText = ev_s; 1357 } else if (labelText === "-1") { 1358 labelText = "-" + ev_s; 1359 } else if (labelText !== "0") { 1360 labelText = labelText + ev_s; 1361 } 1362 } 1363 1364 if (this.evalVisProp('useunicodeminus')) { 1365 labelText = labelText.replace(/-/g, "\u2212"); 1366 } 1367 return labelText; 1368 }, 1369 1370 /** 1371 * Formats label texts to make labels displayed in scientific notation look beautiful. 1372 * For example, label 5.00e+6 will become 5•10⁶, label -1.00e-7 will become into -1•10⁻⁷ 1373 * @param {String} labelText - The label that we want to convert 1374 * @returns {String} If labelText was not in scientific notation, return labelText without modifications. 1375 * Otherwise returns beautified labelText with proper superscript notation. 1376 */ 1377 beautifyScientificNotationLabel: function (labelText) { 1378 var returnString; 1379 1380 if (labelText.indexOf("e") === -1) { 1381 return labelText; 1382 } 1383 1384 // Clean up trailing 0's, so numbers like 5.00e+6.0 for example become into 5e+6 1385 returnString = 1386 parseFloat(labelText.substring(0, labelText.indexOf("e"))) + 1387 labelText.substring(labelText.indexOf("e")); 1388 1389 // Replace symbols like -,0,1,2,3,4,5,6,7,8,9 with their superscript version. 1390 // Gets rid of + symbol since there is no need for it anymore. 1391 returnString = returnString.replace(/e(.*)$/g, function (match, $1) { 1392 var temp = "\u2022" + "10"; 1393 // Note: Since board ticks do not support HTTP elements like <sub>, we need to replace 1394 // all the numbers with superscript Unicode characters. 1395 temp += $1 1396 .replace(/-/g, "\u207B") 1397 .replace(/\+/g, "") 1398 .replace(/0/g, "\u2070") 1399 .replace(/1/g, "\u00B9") 1400 .replace(/2/g, "\u00B2") 1401 .replace(/3/g, "\u00B3") 1402 .replace(/4/g, "\u2074") 1403 .replace(/5/g, "\u2075") 1404 .replace(/6/g, "\u2076") 1405 .replace(/7/g, "\u2077") 1406 .replace(/8/g, "\u2078") 1407 .replace(/9/g, "\u2079"); 1408 1409 return temp; 1410 }); 1411 1412 return returnString; 1413 }, 1414 1415 /** 1416 * Creates the label text for a given tick. A value for the text can be provided as a number or string 1417 * 1418 * @param {JXG.Coords} tick The Coords-object of the tick to create a label for 1419 * @param {JXG.Coords} zero The Coords-object of line's zero 1420 * @param {Number|String} value A predefined value for this tick 1421 * @returns {String} 1422 * @private 1423 */ 1424 generateLabelText: function (tick, zero, value) { 1425 var labelText, distance; 1426 1427 // No value provided, equidistant, so assign distance as value 1428 if (!Type.exists(value)) { 1429 // could be null or undefined 1430 distance = this.getDistanceFromZero(zero, tick); 1431 if (Math.abs(distance) < Mat.eps) { 1432 // Point is zero 1433 return "0"; 1434 } 1435 value = distance / this.evalVisProp('scale'); 1436 } 1437 labelText = this.formatLabelText(value); 1438 1439 return labelText; 1440 }, 1441 1442 /** 1443 * Create a tick label data, i.e. text and coordinates 1444 * @param {String} labelText 1445 * @param {JXG.Coords} tick 1446 * @param {Number} tickNumber 1447 * @returns {Object} with properties 'x', 'y', 't' (text), 'i' (tick number) or null in case of o label 1448 * @private 1449 */ 1450 generateLabelData: function (labelText, tick, tickNumber) { 1451 var xa, ya, m, fs; 1452 1453 // Test if large portions of the label are inside of the canvas 1454 // This is the last chance to abandon the creation of the label if it is mostly 1455 // outside of the canvas. 1456 fs = this.evalVisProp('label.fontsize'); 1457 xa = [tick.scrCoords[1], tick.scrCoords[1]]; 1458 ya = [tick.scrCoords[2], tick.scrCoords[2]]; 1459 m = fs === undefined ? 12 : fs; 1460 m *= 0.5; 1461 if (!this._isInsideCanvas(xa, ya, m)) { 1462 return null; 1463 } 1464 1465 xa = this.evalVisProp('label.offset')[0]; 1466 ya = this.evalVisProp('label.offset')[1]; 1467 1468 return { 1469 x: tick.usrCoords[1] + xa / this.board.unitX, 1470 y: tick.usrCoords[2] + ya / this.board.unitY, 1471 t: labelText, 1472 i: tickNumber 1473 }; 1474 }, 1475 1476 /** 1477 * Recalculate the tick positions and the labels. 1478 * @returns {JXG.Ticks} 1479 */ 1480 update: function () { 1481 if (this.needsUpdate) { 1482 //this.visPropCalc.visible = this.evalVisProp('visible'); 1483 // A canvas with no width or height will create an endless loop, so ignore it 1484 if (this.board.canvasWidth !== 0 && this.board.canvasHeight !== 0) { 1485 this.calculateTicksCoordinates(); 1486 } 1487 // this.updateVisibility(this.line.visPropCalc.visible); 1488 // 1489 // for (var i = 0; i < this.labels.length; i++) { 1490 // if (this.labels[i] !== null) { 1491 // this.labels[i].prepareUpdate() 1492 // .updateVisibility(this.line.visPropCalc.visible) 1493 // .updateRenderer(); 1494 // } 1495 // } 1496 } 1497 1498 return this; 1499 }, 1500 1501 /** 1502 * Uses the boards renderer to update the arc. 1503 * @returns {JXG.Ticks} Reference to the object. 1504 */ 1505 updateRenderer: function () { 1506 if (!this.needsUpdate) { 1507 return this; 1508 } 1509 1510 if (this.visPropCalc.visible) { 1511 this.board.renderer.updateTicks(this); 1512 } 1513 this.updateRendererLabels(); 1514 1515 this.setDisplayRendNode(); 1516 // if (this.visPropCalc.visible != this.visPropOld.visible) { 1517 // this.board.renderer.display(this, this.visPropCalc.visible); 1518 // this.visPropOld.visible = this.visPropCalc.visible; 1519 // } 1520 1521 this.needsUpdate = false; 1522 return this; 1523 }, 1524 1525 /** 1526 * Updates the label elements of the major ticks. 1527 * 1528 * @private 1529 * @returns {JXG.Ticks} Reference to the object. 1530 */ 1531 updateRendererLabels: function () { 1532 var i, j, lenData, lenLabels, attr, label, ld, visible; 1533 1534 // The number of labels needed 1535 lenData = this.labelsData.length; 1536 // The number of labels which already exist 1537 // The existing labels are stored in this.labels[] 1538 // The new label positions and label values are stored in this.labelsData[] 1539 lenLabels = this.labels.length; 1540 1541 for (i = 0, j = 0; i < lenData; i++) { 1542 if (this.labelsData[i] === null) { 1543 // This is a tick without label 1544 continue; 1545 } 1546 1547 ld = this.labelsData[i]; 1548 if (j < lenLabels) { 1549 // Take an already existing text element 1550 label = this.labels[j]; 1551 label.setText(ld.t); 1552 label.setCoords(ld.x, ld.y); 1553 j++; 1554 } else { 1555 // A new text element is needed 1556 this.labelCounter += 1; 1557 1558 attr = Type.deepCopy(this.visProp.label); 1559 attr.isLabel = true; 1560 attr.priv = this.visProp.priv; 1561 attr.id = this.id + ld.i + "Label" + this.labelCounter; 1562 1563 label = JXG.createText(this.board, [ld.x, ld.y, ld.t], attr); 1564 this.addChild(label); 1565 label.setParents(this); 1566 label.isDraggable = false; 1567 label.dump = false; 1568 this.labels.push(label); 1569 } 1570 1571 // Look-ahead if the label inherits visibility. 1572 // If yes, update label. 1573 visible = this.evalVisProp('label.visible'); 1574 if (visible === 'inherit') { 1575 visible = this.visPropCalc.visible; 1576 } 1577 1578 label.prepareUpdate().updateVisibility(visible).updateRenderer(); 1579 1580 label.distanceX = this.evalVisProp('label.offset')[0]; 1581 label.distanceY = this.evalVisProp('label.offset')[1]; 1582 } 1583 1584 // Hide unused labels 1585 lenData = j; 1586 for (j = lenData; j < lenLabels; j++) { 1587 this.board.renderer.display(this.labels[j], false); 1588 // Tick labels have the attribute "visible: 'inherit'" 1589 // This must explicitly set to false, otherwise 1590 // this labels would be set to visible in the upcoming 1591 // update of the labels. 1592 this.labels[j].visProp.visible = this.labels[j].visPropCalc.visible = false; 1593 } 1594 1595 return this; 1596 }, 1597 1598 hideElement: function () { 1599 var i; 1600 1601 JXG.deprecated("Element.hideElement()", "Element.setDisplayRendNode()"); 1602 1603 this.visPropCalc.visible = false; 1604 this.board.renderer.display(this, false); 1605 for (i = 0; i < this.labels.length; i++) { 1606 if (Type.exists(this.labels[i])) { 1607 this.labels[i].hideElement(); 1608 } 1609 } 1610 1611 return this; 1612 }, 1613 1614 showElement: function () { 1615 var i; 1616 1617 JXG.deprecated("Element.showElement()", "Element.setDisplayRendNode()"); 1618 1619 this.visPropCalc.visible = true; 1620 this.board.renderer.display(this, false); 1621 1622 for (i = 0; i < this.labels.length; i++) { 1623 if (Type.exists(this.labels[i])) { 1624 this.labels[i].showElement(); 1625 } 1626 } 1627 1628 return this; 1629 } 1630 } 1631 ); 1632 1633 /** 1634 * @class Ticks are used as distance markers on a line or curve. 1635 * They are mainly used for axis elements and slider elements. Ticks may stretch infinitely 1636 * or finitely, which can be set with {@link Ticks#majorHeight} and {@link Ticks#minorHeight}. 1637 * <p> 1638 * There are the following ways to position the tick lines: 1639 * <ol> 1640 * <li> If an array is given as optional second parameter for the constructor 1641 * like e.g. <tt>board.create('ticks', [line, [1, 4, 5]])</tt>, then there will be (fixed) ticks at position 1642 * 1, 4 and 5 of the line. 1643 * <li> If there is only one parameter given, like e.g. <tt>board.create('ticks', [line])</tt>, the ticks will be set 1644 * equidistant across the line element. There are two variants: 1645 * <ol type="i"> 1646 * <li> Setting the attribute <tt>insertTicks:false</tt>: in this case the distance between two major ticks 1647 * is determined by the attribute <tt>ticksDistance</tt>. This distance is given in user units. 1648 * <li> Setting the attribute <tt>insertTicks:true</tt>: in this case the distance between two major ticks 1649 * is set automatically, depending on 1650 * <ul> 1651 * <li> the size of the board, 1652 * <li> the attribute <tt>minTicksDistance</tt>, which is the minimum distance between two consecutive minor ticks (in pixel). 1653 * </ul> 1654 * The distance between two major ticks is a value of the form 1655 * <i>a 10<sup>i</sup></i>, where <i>a</i> is one of <i>{1, 2, 5}</i> and 1656 * the number <i>a 10<sup>i</sup></i> is maximized such that there are approximately 1657 * 6 major ticks and there are at least "minTicksDistance" pixel between minor ticks. 1658 * </ol> 1659 * <p> 1660 * For arbitrary lines (and not axes) a "zero coordinate" is determined 1661 * which defines where the first tick is positioned. This zero coordinate 1662 * can be altered with the attribute <tt>anchor</tt>. Possible values are "left", "middle", "right" or a number. 1663 * The default value is "left". 1664 * 1665 * @pseudo 1666 * @name Ticks 1667 * @augments JXG.Ticks 1668 * @constructor 1669 * @type JXG.Ticks 1670 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 1671 * @param {JXG.Line|JXG.Curve} line The parents consist of the line or curve the ticks are going to be attached to. 1672 * @param {Array} [ticks] Optional array of numbers. If given, a fixed number of static ticks is created 1673 * at these user-supplied positions. 1674 * <p> 1675 * Deprecated: Alternatively, a number defining the distance between two major ticks 1676 * can be specified. However, this is meanwhile ignored. Use attribute <tt>ticksDistance</tt> instead. 1677 * 1678 * @example 1679 * // Add ticks to line 'l1' through 'p1' and 'p2'. The major ticks are 1680 * // two units apart and 40 px long. 1681 * var p1 = board.create('point', [0, 3]); 1682 * var p2 = board.create('point', [1, 3]); 1683 * var l1 = board.create('line', [p1, p2]); 1684 * var t = board.create('ticks', [l1], { 1685 * ticksDistance: 2, 1686 * majorHeight: 40 1687 * }); 1688 * </pre><div class="jxgbox" id="JXGee7f2d68-75fc-4ec0-9931-c76918427e63" style="width: 300px; height: 300px;"></div> 1689 * <script type="text/javascript"> 1690 * (function () { 1691 * var board = JXG.JSXGraph.initBoard('JXGee7f2d68-75fc-4ec0-9931-c76918427e63', { 1692 * boundingbox: [-1, 7, 7, -1], axis: true, showcopyright: false, shownavigation: true}); 1693 * var p1 = board.create('point', [0, 3]); 1694 * var p2 = board.create('point', [1, 3]); 1695 * var l1 = board.create('line', [p1, p2]); 1696 * var t = board.create('ticks', [l1, 2], {ticksDistance: 2, majorHeight: 40}); 1697 * })(); 1698 * </script><pre> 1699 * @example 1700 * // Create ticks labels as fractions 1701 * board.create('axis', [[0,1], [1,1]], { 1702 * ticks: { 1703 * label: { 1704 * toFraction: true, 1705 * useMathjax: true, 1706 * display: 'html', 1707 * anchorX: 'middle', 1708 * offset: [0, -10] 1709 * } 1710 * } 1711 * }); 1712 * 1713 * </pre><div id="JXG4455acb2-6bf3-4801-8887-d7fcc1e4e1da" class="jxgbox" style="width: 300px; height: 300px;"></div> 1714 * <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js" id="MathJax-script"></script> 1715 * <script type="text/javascript"> 1716 * (function() { 1717 * var board = JXG.JSXGraph.initBoard('JXG4455acb2-6bf3-4801-8887-d7fcc1e4e1da', 1718 * {boundingbox: [-1.2, 2.3, 1.2, -2.3], axis: true, showcopyright: false, shownavigation: false}); 1719 * board.create('axis', [[0,1], [1,1]], { 1720 * ticks: { 1721 * label: { 1722 * toFraction: true, 1723 * useMathjax: true, 1724 * display: 'html', 1725 * anchorX: 'middle', 1726 * offset: [0, -10] 1727 * } 1728 * } 1729 * }); 1730 * 1731 * })(); 1732 * 1733 * </script><pre> 1734 * 1735 * @example 1736 */ 1737 JXG.createTicks = function (board, parents, attributes) { 1738 var el, 1739 dist, 1740 attr = Type.copyAttributes(attributes, board.options, "ticks"); 1741 1742 if (parents.length < 2) { 1743 dist = attr.ticksdistance; // Will be ignored anyhow and attr.ticksDistance will be used instead 1744 } else { 1745 dist = parents[1]; 1746 } 1747 1748 if ( 1749 parents[0].elementClass === Const.OBJECT_CLASS_LINE || 1750 parents[0].elementClass === Const.OBJECT_CLASS_CURVE 1751 ) { 1752 el = new JXG.Ticks(parents[0], dist, attr); 1753 } else { 1754 throw new Error( 1755 "JSXGraph: Can't create Ticks with parent types '" + typeof parents[0] + "'." 1756 ); 1757 } 1758 1759 // deprecated 1760 if (Type.isFunction(attr.generatelabelvalue)) { 1761 el.generateLabelText = attr.generatelabelvalue; 1762 } 1763 if (Type.isFunction(attr.generatelabeltext)) { 1764 el.generateLabelText = attr.generatelabeltext; 1765 } 1766 1767 el.setParents(parents[0]); 1768 el.isDraggable = true; 1769 el.fullUpdate(parents[0].visPropCalc.visible); 1770 1771 return el; 1772 }; 1773 1774 /** 1775 * @class Hatches are collections of short line segments used to mark congruent lines or curves. 1776 * @pseudo 1777 * @name Hatch 1778 * @augments JXG.Ticks 1779 * @constructor 1780 * @type JXG.Ticks 1781 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 1782 * @param {JXG.Line|JXG.curve} line The line or curve the hatch marks are going to be attached to. 1783 * @param {Number} numberofhashes Number of dashes. The distance of the hashes can be controlled with the attribute ticksDistance. 1784 * @example 1785 * // Create an axis providing two coords pairs. 1786 * var p1 = board.create('point', [0, 3]); 1787 * var p2 = board.create('point', [1, 3]); 1788 * var l1 = board.create('line', [p1, p2]); 1789 * var t = board.create('hatch', [l1, 3]); 1790 * </pre><div class="jxgbox" id="JXG4a20af06-4395-451c-b7d1-002757cf01be" style="width: 300px; height: 300px;"></div> 1791 * <script type="text/javascript"> 1792 * (function () { 1793 * var board = JXG.JSXGraph.initBoard('JXG4a20af06-4395-451c-b7d1-002757cf01be', {boundingbox: [-1, 7, 7, -1], showcopyright: false, shownavigation: false}); 1794 * var p1 = board.create('point', [0, 3]); 1795 * var p2 = board.create('point', [1, 3]); 1796 * var l1 = board.create('line', [p1, p2]); 1797 * var t = board.create('hatch', [l1, 3]); 1798 * })(); 1799 * </script><pre> 1800 * 1801 * @example 1802 * // Alter the position of the hatch 1803 * 1804 * var p = board.create('point', [-5, 0]); 1805 * var q = board.create('point', [5, 0]); 1806 * var li = board.create('line', [p, q]); 1807 * var h = board.create('hatch', [li, 2], {anchor: 0.2, ticksDistance:0.4}); 1808 * 1809 * </pre><div id="JXG05d720ee-99c9-11e6-a9c7-901b0e1b8723" class="jxgbox" style="width: 300px; height: 300px;"></div> 1810 * <script type="text/javascript"> 1811 * (function() { 1812 * var board = JXG.JSXGraph.initBoard('JXG05d720ee-99c9-11e6-a9c7-901b0e1b8723', 1813 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 1814 * 1815 * var p = board.create('point', [-5, 0]); 1816 * var q = board.create('point', [5, 0]); 1817 * var li = board.create('line', [p, q]); 1818 * var h = board.create('hatch', [li, 2], {anchor: 0.2, ticksDistance:0.4}); 1819 * 1820 * })(); 1821 * 1822 * </script><pre> 1823 * 1824 * @example 1825 * // Alternative hatch faces 1826 * 1827 * var li = board.create('line', [[-6,0], [6,3]]); 1828 * var h1 = board.create('hatch', [li, 2], {tickEndings: [1,1], face:'|'}); 1829 * var h2 = board.create('hatch', [li, 2], {tickEndings: [1,1], face:'>', anchor: 0.3}); 1830 * var h3 = board.create('hatch', [li, 2], {tickEndings: [1,1], face:'<', anchor: 0.7}); 1831 * 1832 * </pre><div id="JXG974f7e89-eac8-4187-9aa3-fb8068e8384b" class="jxgbox" style="width: 300px; height: 300px;"></div> 1833 * <script type="text/javascript"> 1834 * (function() { 1835 * var board = JXG.JSXGraph.initBoard('JXG974f7e89-eac8-4187-9aa3-fb8068e8384b', 1836 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 1837 * // Alternative hatch faces 1838 * 1839 * var li = board.create('line', [[-6,0], [6,3]]); 1840 * var h1 = board.create('hatch', [li, 2], {tickEndings: [1,1], face:'|'}); 1841 * var h2 = board.create('hatch', [li, 2], {tickEndings: [1,1], face:'>', anchor: 0.3}); 1842 * var h3 = board.create('hatch', [li, 2], {tickEndings: [1,1], face:'<', anchor: 0.7}); 1843 * 1844 * })(); 1845 * 1846 * </script><pre> 1847 * 1848 */ 1849 JXG.createHatchmark = function (board, parents, attributes) { 1850 var num, i, base, width, totalwidth, el, 1851 pos = [], 1852 attr = Type.copyAttributes(attributes, board.options, 'hatch'); 1853 1854 if ( 1855 (parents[0].elementClass !== Const.OBJECT_CLASS_LINE && 1856 parents[0].elementClass !== Const.OBJECT_CLASS_CURVE) || 1857 typeof parents[1] !== "number" 1858 ) { 1859 throw new Error( 1860 "JSXGraph: Can't create Hatch mark with parent types '" + 1861 typeof parents[0] + 1862 "' and '" + 1863 typeof parents[1] + 1864 " and ''" + 1865 typeof parents[2] + 1866 "'." 1867 ); 1868 } 1869 1870 num = parents[1]; 1871 width = attr.ticksdistance; 1872 totalwidth = (num - 1) * width; 1873 base = -totalwidth * 0.5; 1874 1875 for (i = 0; i < num; i++) { 1876 pos[i] = base + i * width; 1877 } 1878 1879 el = board.create('ticks', [parents[0], pos], attr); 1880 el.elType = 'hatch'; 1881 parents[0].inherits.push(el); 1882 1883 return el; 1884 }; 1885 1886 JXG.registerElement("ticks", JXG.createTicks); 1887 JXG.registerElement("hash", JXG.createHatchmark); 1888 JXG.registerElement("hatch", JXG.createHatchmark); 1889 1890 export default JXG.Ticks; 1891 // export default { 1892 // Ticks: JXG.Ticks, 1893 // createTicks: JXG.createTicks, 1894 // createHashmark: JXG.createHatchmark, 1895 // createHatchmark: JXG.createHatchmark 1896 // }; 1897