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