Axis constructor

Axis(int npoints, double start, double end, double physStart, double physWidth, bool center, double calib, double _coordShift, int axisLength, int axisWidth, int gridLength, PhysicalToScreen physToScreen, Map<AxA, String> axesAttributes, List<TouchCallback> touchCallbacks)

Constructs an axis. See Axis.coord and Axis.intens for a description of the arguments. Use these named constructors rather than this one.

Implementation

Axis(
    int npoints,
    final double start,
    final double end,
    double physStart,
    double physWidth,
    bool center,
    double calib,
    this._coordShift,
    final int axisLength,
    final int axisWidth,
    final int gridLength,
    this.physToScreen,
    Map<AxA, String> axesAttributes,
    List<TouchCallback> touchCallbacks) {
  String legendText = axesAttributes[AxA.LEGENDTEXT];
  String position = axesAttributes[AxA.POSITION];
  if (position.contains('-')) _isReversed = true;

  bool isXaxis = true;
  bool isXaxisF1 = false;
  if (position.contains("y")) {
    isXaxis = false; // vertical intensity axis
  }

  if (position.contains("x2")) {
    isXaxis = false;
    isXaxisF1 = true; // vertical x2 coordinate axis
  }

  // make sure not null to avoid crash if, for y axis, [_isReversed] is
  // true, but the user left [_coordShift] with null.
  if (_coordShift == null) _coordShift = 0.0;

  // Just a helper
  double physFromIndex(double index) {
    return PhysUnits.physFromIndex(
        index, physStart, physWidth, npoints, _isReversed, center, calib);
  }

  // Just a helper
  double physToIndex(double phys) {
    return PhysUnits.physToIndex(
        phys, physStart, physWidth, npoints, _isReversed, center, calib);
  }

  _attributes = Map.from(AXIS_DEFAULT_ATTRIBUTES); // init. attributes
  if (axesAttributes != null) {
    // possibly changed attributes as wished by caller
    _attributes.addAll(axesAttributes);
  }

  // find number of desired x labels from attribute NUMBER_AXIS_LABELS
  // depending on axis width
  Map<String, String> numLabelsX =
      JsonUtils.decodeMSS(_attributes[AxA.NLABELS_X]);
  Map<String, String> numLabelsY =
      JsonUtils.decodeMSS(_attributes[AxA.NLABELS_Y]);

  int numberLabels = 6;
  Map<String, String> numLabels = numLabelsX;
  if (!isXaxis) numLabels = numLabelsY;

  numLabels.forEach((length, nlabs) {
    if (axisLength > int.parse(length)) {
      numberLabels = int.parse(nlabs);
      return;
    }
  });

  double first = start, last = end;

  // This applies when coordShift is specified, and reverse is specified externally
  if (_coordShift != null && _isReversed) // reverse axis
  {
    first = _coordShift - start;
    last = _coordShift - end;
//      _isReversed = true;
  }

  List<String> displayedXValues;

  // prepare layout: legend - labels - ticks or ticks - labels - legend, depending on position
  // relative to labelsContainer
  int fontsize = int.parse(_attributes[AxA.FONT_SIZE]);

  int ticksFrom, ticksTo, labelPos, legendPos;
  String axis_text_color;
  if (isXaxis) {
    if (_isReversed) {
      _coordShift = start + end;
      first = _coordShift - start;
      last = _coordShift - end;
    }

    displayedXValues = genLabels(physFromIndex(first), physFromIndex(last),
        tightStyle: false, nticks: numberLabels, scale: null);

//      displayedXValues = genLabels(DSPhys.indexToPhysX(ydata, first),
//          DSPhys.indexToPhysX(ydata, last),
//          tightStyle: false, nticks: numberLabels, scale: null);

    if (displayedXValues.length > 2) {
      // if genLabels() delivered values which are outside "first" and "last"
      // try again with an increased "numberLabels" input.
      // This mimic could be repeated, if such cases would occur in practice.
      double firstx = double.parse(displayedXValues.first);
      double lastx = double.parse(displayedXValues.last);
      if (firstx < first || lastx > last) {
        numberLabels++;
        displayedXValues = genLabels(
            physFromIndex(first), physFromIndex(last),
            tightStyle: false, nticks: numberLabels, scale: null);
      }
    }

    // check again whether the sum of all labels would exceed the axis length, and reduce
    // the number of labels significantly. This occurs especially when the coordinate numbers are large.
    double itemp = displayedXValues.length *
        displayedXValues[displayedXValues.length ~/ 2].length *
        fontsize *
        0.6;
    if (itemp.round() > axisLength) {
      numberLabels = (0.8 * numberLabels).round();
      displayedXValues = genLabels(physFromIndex(first), physFromIndex(last),
          tightStyle: false, nticks: numberLabels, scale: null);
    }

    // bottom axis
    ticksFrom = 0;
    ticksTo = int.parse(_attributes[AxA.TICK_LENGTH]);
    labelPos = ticksTo + fontsize + 4;
    legendPos = labelPos + int.parse(_attributes[AxA.LEGENDTEXT_OFFSET_X]);
    if (position.contains("t")) // top axis
    {
      ticksFrom = axisWidth;
      ticksTo = axisWidth - int.parse(_attributes[AxA.TICK_LENGTH]);
      labelPos = ticksTo - 4; // distance between ticks and labels
      legendPos = labelPos -
          int.parse(_attributes[AxA
              .LEGENDTEXT_OFFSET_X]); //  Note that text y is at the text baseline,
    }
    axis_text_color = _attributes[AxA.TEXT_COLOR_X];
  } else {
    // treat y axis or F1 axis
    if (isXaxisF1) {
      if (_isReversed) {
        _coordShift = start + end;
        first = _coordShift - start;
        last = _coordShift - end;
      }
      displayedXValues = genLabels(physFromIndex(first), physFromIndex(last),
          tightStyle: false, nticks: numberLabels, scale: null);
    } else {
      try {
        double maxval = physStart;
        double normalizedMaxval = physWidth;
        displayedXValues = genLabels(
            PhysUnits.normalize(first, maxval, normalizedMaxval),
            PhysUnits.normalize(last, maxval, normalizedMaxval),
            tightStyle: false,
            nticks: numberLabels,
            scale: null);
      } catch (e) {
        // this occurs e.g. if the entire spectrum is 0.0.
        displayedXValues = ["0.0"];
      }
    }

    // left axis
    ticksFrom = axisWidth;
    ticksTo = axisWidth - int.parse(_attributes[AxA.TICK_LENGTH]);
    labelPos =
        ticksTo - int.parse(_attributes[AxA.LABELS_OFFSET_Y]); //fontsize - 4;
    legendPos =
        labelPos - int.parse(_attributes[AxA.LEGENDTEXT_LEFT_OFFSET_Y]);
    if (position.contains("t")) {
      // right axis
      ticksFrom = 0;
      ticksTo = int.parse(_attributes[AxA.TICK_LENGTH]);
      // distance between ticks and labels:
      labelPos = ticksTo + int.parse(_attributes[AxA.LABELS_OFFSET_Y]);
      //  Note that text y is at the text baseline:
      legendPos =
          labelPos + int.parse(_attributes[AxA.LEGENDTEXT_RIGHT_OFFSET_Y]);
    }
    axis_text_color = _attributes[AxA.TEXT_COLOR_Y];
  } // end yaxis

  // draw the labels at the right position in the labelsContainer and draw tick marks
  List<int> labelPositions = [];
  double physVal;
  labelsContainer = SvgSvgElement(); // now position the labels here
  TextElement te;
  LineElement line;
  int posScreen, linePos, x, y;
  String textAnchor = "middle", baselineShift = "0";
  extra_space_for_edge_labels_x =
      int.parse(_attributes[AxA.EXTRA_SPACE_FOR_EDGE_LABELS_X]);
  extra_space_for_edge_labels_y =
      int.parse(_attributes[AxA.EXTRA_SPACE_FOR_EDGE_LABELS_Y]);

  for (int i = 0; i < displayedXValues.length; i++) {
    physVal = double.parse(
        displayedXValues[i]); // physical position of an axis label

    if (isXaxis || isXaxisF1) {
      posScreen = getScreenPos(physToIndex(physVal));
    } else {
      double maxval = physStart;
      double normalizedMaxval = physWidth;
      // because polyline has unnormalized values:
      posScreen = getScreenPos(
          PhysUnits.undoNormalize(physVal, maxval, normalizedMaxval));
    }

    // must come BEFORE range check to keep the list length of
    // labelPositions consistent:
//      labelPositions.add(posScreen);

    // don't display labels lying outside dataArea
    if (posScreen < 0) continue;
    if (posScreen > axisLength) continue;
    labelPositions.add(posScreen);

    te = TextElement();

    if (displayedXValues[i].length > 4 && physVal.abs() > 99999) {
      // display too large numbers in exponential form: 4.00e+8
      // with this may digits after point:
      displayedXValues[i] = physVal.toStringAsExponential(2);
      // remove "e" to save space: 4.00+8
      displayedXValues[i] = displayedXValues[i].replaceAll("e", "");
    }

    te.text = displayedXValues[i];

    if (isXaxis) {
      x = posScreen + extra_space_for_edge_labels_x;
      y = labelPos;
    } else {
      x = labelPos;
      y = posScreen + extra_space_for_edge_labels_y;
      textAnchor = "end";
      if (position.contains("t")) {
        textAnchor = "start"; // right axis
      }
      // this will center the label text vertically with respect to labelPos
      baselineShift = "-33%";
    }

    SVG.setAttr(te, {
      SVG.X: "$x", // start position of text w/r xaxis area
      SVG.Y: "$y", // relative to bottom of data area
      SVG.FILL: axis_text_color,
      SVG.STROKE: "none",
      SVG.FONT_SIZE: _attributes[AxA.FONT_SIZE],
      SVG.TEXT_ANCHOR: "$textAnchor",
      SVG.BASELINE_SHIFT: "$baselineShift",
      SVG.CURSOR: "default" // don't auto-change to text entry cursor shape
    });
    labelsContainer.append(te);

    // draw tick marks if wanted, tick length 0 means no ticks
    if (_attributes.containsKey(AxA.TICK_LENGTH) &&
        int.parse(_attributes[AxA.TICK_LENGTH]) > 0) {
      line = LineElement(); // line parallel to y axis
      if (isXaxis) {
        linePos = posScreen + extra_space_for_edge_labels_x;
      } else {
        linePos = posScreen + extra_space_for_edge_labels_y;
      }

      if (isXaxis) {
        SVG.setAttr(line, {
          SVG.X1: "$linePos",
          SVG.Y1: "$ticksFrom",
          SVG.X2: "$linePos",
          SVG.Y2: "$ticksTo",
        });
      } else {
        SVG.setAttr(line, {
          SVG.X1: "$ticksFrom",
          SVG.Y1: "$linePos",
          SVG.X2: "$ticksTo",
          SVG.Y2: "${linePos}",
        });
      }

      // set common attributes
      SVG.setAttr(line, {
        SVG.STROKE: _attributes[AxA.STROKE],
        SVG.STROKE_WIDTH: _attributes[AxA.STROKE_WIDTH]
      });
      labelsContainer.append(line);
    }

//      // draw x grid if wanted
//      if (position.contains("g") && gridLength != null && gridLength > 0) {
//        // axesAttributes may contain the grid attributes. If not, XYGrid will use its defaults.
//        if (isXaxis) {
//          xyGrid =
//              XYGrid(labelPositions, null, null, gridLength, axesAttributes);
//        } else {
//          xyGrid =
//              XYGrid(null, labelPositions, gridLength, null, axesAttributes);
//        }
//      }
  } // for(int i= 0; i<displayedXValues.length; i++)

  // draw x grid if wanted
  if (position.contains("g") && gridLength != null && gridLength > 0) {
    // axesAttributes may contain the grid attributes. If not, XYGrid will use its defaults.
    if (isXaxis) {
      xyGrid =
          XYGrid(labelPositions, null, null, gridLength, axesAttributes);
    } else {
      xyGrid =
          XYGrid(null, labelPositions, gridLength, null, axesAttributes);
    }
  }

  // add axis legend text (e.g. the axis unit) at the bottom of the axis labels and center the text
  if (legendText != null && legendText.trim().isNotEmpty) {
    te = TextElement();
    te.text = legendText;

    if (isXaxis) {
      SVG.setAttr(te, {
        SVG.X: "${axisLength / 2 + extra_space_for_edge_labels_x}",
        // center text in xaxis area
        SVG.Y: "$legendPos",
        // offset from xaxis labels
        SVG.FILL: axis_text_color,
        SVG.STROKE: "none",
        SVG.FONT_SIZE: _attributes[AxA.FONT_SIZE],
        SVG.TEXT_ANCHOR: "middle",
        SVG.CURSOR: "default"
        // don't auto-change to text entry cursor shape
      });
    } else {
      // Set up the coordinate tranformation to achieve a text rotation around 90 or -90 where the
      // text is centered relative to the y axis, and offset from the y axis labels.
      // It consists of the actual rotation followed by a translation considering that the
      // rotation origin is at (0,0) of the container, and the text anchor being set to "middle".
      // default legend text direction is from bottom to top
      int legendTextOffset = int.parse(_attributes[
          AxA.LEGENDTEXT_LEFT_OFFSET_Y]); // relative to yaxis labels
      String rotate =
          "rotate(-90) translate(${-axisLength / 2}, ${legendTextOffset})";

      if (_attributes[AxA.YLEGENDTEXT_DIRECTION] == "tb") {
        // top to bottom
        rotate =
            "rotate(90) translate(${axisLength / 2}, ${-legendTextOffset})";
        // right y axis
        if (position.contains("t")) {
//            rotate = "rotate(90) translate(${axisLength/2}, ${-axisWidth + legendTextOffset})";
          rotate =
              "rotate(90) translate(${axisLength / 2}, ${-axisWidth + (axisWidth * 0.22).round()})";
        }
      }

      SVG.setAttr(te, {
        SVG.X: "0",
        SVG.Y: "0",
        // (x,y) = (0,0) is the 90 degree rotation origin
        SVG.FILL: axis_text_color,
        SVG.STROKE: "none",
        SVG.FONT_SIZE: _attributes[AxA.FONT_SIZE],
        SVG.TEXT_ANCHOR: "middle",
        // changing this would change the transformation formula above
        SVG.TRANSFORM: rotate,
        // rotates around text origin, more complicated than "writing-mode: tb"
        SVG.CURSOR: "default"
        // don't auto-change to text entry cursor shape
      });
    }

    // on touch devices make sure that touches on the text don't get lost
    te.onTouchStart.listen((UIEvent e) {
      e.preventDefault();
      if (touchCallbacks != null && touchCallbacks.isNotEmpty) {
        touchCallbacks[0](e);
      }
    });

    te.onTouchMove.listen((UIEvent e) {
      e.preventDefault();
      if (touchCallbacks != null && touchCallbacks.length >= 2) {
        touchCallbacks[1](e);
      }
    });

    labelsContainer.append(te);
  }
}