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