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