Axis constructor
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,
bool is2D,
double this._coordShift,
final int axisLength,
final int axisWidth,
final int gridLength,
final String legendText,
final String position,
PhysicalToScreen this.physToScreen,
Map<AxA, String> axesAttributes,
List<TouchCallback> touchCallbacks) {
if (position.contains('-')) _isReversed = true;
bool isXaxis = true;
bool isXaxisF1 = false;
if (position.contains("y")) {
isXaxis = false;
if (is2D) isXaxisF1 = true;
}
// 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 = new 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 = new 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;
te = new 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")) // right axis
textAnchor = "start";
// 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);
// The following is not straightened out yet
// if(yaxisLabelsWidthPx == null && !isXaxis)
// {
// DivElement plotDiv = (querySelector("#plot_div") as DivElement);
// plotDiv.append(labelsContainer);
// yaxisLabelsWidthPx = te.getComputedTextLength().round();
// labelsContainer.remove();
// }
// 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 = new 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++)
// 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);
}
}