Skip to content

Commit e4bd46f

Browse files
committed
Some label positioning improvements.
There are two new options for positioning labels, `anchor` and `padding`. The `padding` option makes it possible for the author to easily set the padding for the label. This is css padding in pixels for the JSXGraph format, and the value of the node `inner sep` in points for the TikZ format. The `anchor` option is an alternate position approach to the current `h_align` and `v_align` approach. See the POD documentation for a (hopefully) good explanation of this options. Basically, in the TikZ format this is the value for the node `anchor` option in degrees. Since JSXGraph doesn't provide such an option, this had to be implemented using a transformation. This is useful for positioning labels for angles.
1 parent 7881319 commit e4bd46f

File tree

4 files changed

+150
-81
lines changed

4 files changed

+150
-81
lines changed

htdocs/js/Plots/plots.js

Lines changed: 118 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -53,83 +53,129 @@ const PGplots = {
5353
board.containerObj.after(descriptionSpan);
5454
board.containerObj.setAttribute('aria-describedby', descriptionSpan.id);
5555

56-
// Convert a decimal number into a fraction or mixed number. This is basically the JXG.toFraction method
57-
// except that the "mixed" parameter is added, and it returns an improper fraction if mixed is false.
58-
const toFraction = (x, useTeX, mixed, order) => {
59-
const arr = JXG.Math.decToFraction(x, order);
60-
61-
if (arr[1] === 0 && arr[2] === 0) {
62-
return '0';
63-
} else {
64-
let str = '';
65-
// Sign
66-
if (arr[0] < 0) str += '-';
67-
if (arr[2] === 0) {
68-
// Integer
69-
str += arr[1];
70-
} else if (!(arr[2] === 1 && arr[3] === 1)) {
71-
// Proper fraction
72-
if (mixed) {
73-
if (arr[1] !== 0) str += arr[1] + ' ';
74-
if (useTeX) str += `\\frac{${arr[2]}}{${arr[3]}}`;
75-
else str += `${arr[2]}/${arr[3]}`;
56+
// This object is passed to the plotContents method as its second argument and exposes these methods (and
57+
// potentially other things in the future) to the code that is called in that method. So the JavaScript code
58+
// generated in JSXGraph.pm can use these methods.
59+
const plot = {
60+
// Convert a decimal number into a fraction or mixed number. This is basically the JXG.toFraction method
61+
// except that the "mixed" parameter is added, and it returns an improper fraction if mixed is false.
62+
toFraction(x, useTeX, mixed, order) {
63+
const arr = JXG.Math.decToFraction(x, order);
64+
65+
if (arr[1] === 0 && arr[2] === 0) {
66+
return '0';
67+
} else {
68+
let str = '';
69+
// Sign
70+
if (arr[0] < 0) str += '-';
71+
if (arr[2] === 0) {
72+
// Integer
73+
str += arr[1];
74+
} else if (!(arr[2] === 1 && arr[3] === 1)) {
75+
// Proper fraction
76+
if (mixed) {
77+
if (arr[1] !== 0) str += arr[1] + ' ';
78+
if (useTeX) str += `\\frac{${arr[2]}}{${arr[3]}}`;
79+
else str += `${arr[2]}/${arr[3]}`;
80+
} else {
81+
if (useTeX) str += `\\frac{${arr[3] * arr[1] + arr[2]}}{${arr[3]}}`;
82+
else str += `${arr[3] * arr[1] + arr[2]}/${arr[3]}`;
83+
}
84+
}
85+
return str;
86+
}
87+
},
88+
89+
// Override the default axis generateLabelText method so that 0 is displayed
90+
// using MathJax if the axis is configured to show tick labels using MathJax.
91+
generateLabelText(tick, zero, value) {
92+
if (JXG.exists(value)) return this.formatLabelText(value);
93+
const distance = this.getDistanceFromZero(zero, tick);
94+
return this.formatLabelText(Math.abs(distance) < JXG.Math.eps ? 0 : distance / this.visProp.scale);
95+
},
96+
97+
trimTrailingZeros(value) {
98+
if (value.indexOf('.') > -1 && value.endsWith('0')) {
99+
value = value.replace(/0+$/, '');
100+
// Remove the decimal if it is now at the end.
101+
value = value.replace(/\.$/, '');
102+
}
103+
return value;
104+
},
105+
106+
// Override the formatLabelText method for the axes ticks so that
107+
// better number formats can be used for tick labels.
108+
formatLabelText(value) {
109+
let labelText;
110+
111+
if (JXG.isNumber(value)) {
112+
if (this.visProp.label.format === 'fraction' || this.visProp.label.format === 'mixed') {
113+
labelText = plot.toFraction(
114+
value,
115+
this.visProp.label.usemathjax,
116+
this.visProp.label.format === 'mixed'
117+
);
118+
} else if (this.visProp.label.format === 'scinot') {
119+
const [mantissa, exponent] = value.toExponential(this.visProp.digits).toString().split('e');
120+
labelText = this.visProp.label.usemathjax
121+
? `${plot.trimTrailingZeros(mantissa)}\\cdot 10^{${exponent}}`
122+
: `${plot.trimTrailingZeros(mantissa)} x 10^${exponent}`;
76123
} else {
77-
if (useTeX) str += `\\frac{${arr[3] * arr[1] + arr[2]}}{${arr[3]}}`;
78-
else str += `${arr[3] * arr[1] + arr[2]}/${arr[3]}`;
124+
labelText = plot.trimTrailingZeros(value.toFixed(this.visProp.digits).toString());
79125
}
126+
} else {
127+
labelText = value.toString();
80128
}
81-
return str;
82-
}
83-
};
84129

85-
// Override the default axis generateLabelText method so that 0 is displayed
86-
// using MathJax if the axis is configured to show tick labels using MathJax.
87-
const generateLabelText = function (tick, zero, value) {
88-
if (JXG.exists(value)) return this.formatLabelText(value);
89-
const distance = this.getDistanceFromZero(zero, tick);
90-
return this.formatLabelText(Math.abs(distance) < JXG.Math.eps ? 0 : distance / this.visProp.scale);
91-
};
130+
if (this.visProp.scalesymbol.length > 0) {
131+
if (labelText === '1') labelText = this.visProp.scalesymbol;
132+
else if (labelText === '-1') labelText = `-${this.visProp.scalesymbol}`;
133+
else if (labelText !== '0') labelText = labelText + this.visProp.scalesymbol;
134+
}
92135

93-
const trimTrailingZeros = (value) => {
94-
if (value.indexOf('.') > -1 && value.endsWith('0')) {
95-
value = value.replace(/0+$/, '');
96-
// Remove the decimal if it is now at the end.
97-
value = value.replace(/\.$/, '');
98-
}
99-
return value;
100-
};
136+
return this.visProp.label.usemathjax ? `\\(${labelText}\\)` : labelText;
137+
},
138+
139+
createLabel(x, y, text, options) {
140+
const anchor = options.angleAnchor;
141+
delete options.angleAnchor;
142+
const rotate = options.rotate;
143+
delete options.rotate;
101144

102-
// Override the formatLabelText method for the axes ticks so that
103-
// better number formats can be used for tick labels.
104-
const formatLabelText = function (value) {
105-
let labelText;
106-
107-
if (JXG.isNumber(value)) {
108-
if (this.visProp.label.format === 'fraction' || this.visProp.label.format === 'mixed') {
109-
labelText = toFraction(
110-
value,
111-
this.visProp.label.usemathjax,
112-
this.visProp.label.format === 'mixed'
145+
const textElement = board.create('text', [x, y, text], options);
146+
147+
if (typeof anchor !== 'undefined') {
148+
const cosA = Math.cos((anchor * Math.PI) / 180);
149+
const sinA = Math.sin((anchor * Math.PI) / 180);
150+
151+
const transform = board.create(
152+
'transform',
153+
[
154+
() => {
155+
const [w, h] = textElement.getSize();
156+
return (
157+
(w * Math.abs(sinA) > h * Math.abs(cosA)
158+
? (-h / 2 / Math.abs(sinA)) * cosA
159+
: ((cosA < 0 ? 1 : -1) * w) / 2) / board.unitX
160+
);
161+
},
162+
() => {
163+
const [w, h] = textElement.getSize();
164+
return (
165+
(w * Math.abs(sinA) > h * Math.abs(cosA)
166+
? ((sinA < 0 ? 1 : -1) * h) / 2
167+
: (-w / 2 / Math.abs(cosA)) * sinA) / board.unitY
168+
);
169+
}
170+
],
171+
{ type: 'translate' }
113172
);
114-
} else if (this.visProp.label.format === 'scinot') {
115-
const [mantissa, exponent] = value.toExponential(this.visProp.digits).toString().split('e');
116-
labelText = this.visProp.label.usemathjax
117-
? `${trimTrailingZeros(mantissa)}\\cdot 10^{${exponent}}`
118-
: `${trimTrailingZeros(mantissa)} x 10^${exponent}`;
119-
} else {
120-
labelText = trimTrailingZeros(value.toFixed(this.visProp.digits).toString());
173+
transform.bindTo(textElement);
121174
}
122-
} else {
123-
labelText = value.toString();
124-
}
175+
if (rotate) textElement.addRotation(rotate);
125176

126-
if (this.visProp.scalesymbol.length > 0) {
127-
if (labelText === '1') labelText = this.visProp.scalesymbol;
128-
else if (labelText === '-1') labelText = `-${this.visProp.scalesymbol}`;
129-
else if (labelText !== '0') labelText = labelText + this.visProp.scalesymbol;
177+
return textElement;
130178
}
131-
132-
return this.visProp.label.usemathjax ? `\\(${labelText}\\)` : labelText;
133179
};
134180

135181
board.suspendUpdate();
@@ -329,8 +375,8 @@ const PGplots = {
329375
options.xAxis.overrideOptions ?? {}
330376
)
331377
);
332-
xAxis.defaultTicks.generateLabelText = generateLabelText;
333-
xAxis.defaultTicks.formatLabelText = formatLabelText;
378+
xAxis.defaultTicks.generateLabelText = plot.generateLabelText;
379+
xAxis.defaultTicks.formatLabelText = plot.formatLabelText;
334380

335381
if (options.xAxis.location !== 'middle') {
336382
board.create(
@@ -424,8 +470,8 @@ const PGplots = {
424470
options.yAxis.overrideOptions ?? {}
425471
)
426472
);
427-
yAxis.defaultTicks.generateLabelText = generateLabelText;
428-
yAxis.defaultTicks.formatLabelText = formatLabelText;
473+
yAxis.defaultTicks.generateLabelText = plot.generateLabelText;
474+
yAxis.defaultTicks.formatLabelText = plot.formatLabelText;
429475

430476
if (options.yAxis.location !== 'center') {
431477
board.create(
@@ -448,7 +494,7 @@ const PGplots = {
448494
}
449495
}
450496

451-
plotContents(board);
497+
plotContents(board, plot);
452498

453499
board.unsuspendUpdate();
454500

lib/Plots/JSXGraph.pm

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ sub HTML {
149149
(async () => {
150150
const id = 'jsxgraph-plot-$self->{name}';
151151
const options = ${\(Mojo::JSON::encode_json($options))};
152-
const plotContents = (board) => { $self->{JS}$plots->{extra_js_code} };
152+
const plotContents = (board, plot) => { $self->{JS}$plots->{extra_js_code} };
153153
if (document.readyState === 'loading')
154154
window.addEventListener('DOMContentLoaded',
155155
async () => { await PGplots.plot(id, plotContents, options); });
@@ -633,6 +633,9 @@ sub draw {
633633
my $fontsize = $label->style('fontsize') || 'normalsize';
634634
my $h_align = $label->style('h_align') || 'center';
635635
my $v_align = $label->style('v_align') || 'middle';
636+
my $anchor = $label->style('anchor');
637+
my $rotate = $label->style('rotate');
638+
my $padding = $label->style('padding') || 5;
636639
my $textOptions = Mojo::JSON::encode_json({
637640
fontSize => {
638641
tiny => 8,
@@ -646,17 +649,18 @@ sub draw {
646649
huge => 20,
647650
Huge => 23
648651
}->{$fontsize},
649-
$label->style('rotate') ? (rotate => $label->style('rotate')) : (),
650652
strokeColor => $self->get_color($label->style('color')),
651-
anchorX => $h_align eq 'center' ? 'middle' : $h_align,
652-
anchorY => $v_align,
653-
cssStyle => 'padding: 3px 5px;',
654-
useMathJax => 1,
653+
$anchor ne ''
654+
? (angleAnchor => $anchor, anchorX => 'middle', anchorY => 'middle')
655+
: (anchorX => $h_align eq 'center' ? 'middle' : $h_align, anchorY => $v_align),
656+
$rotate ? (rotate => $rotate) : (),
657+
cssStyle => "line-height: 1; padding: ${padding}px;",
658+
useMathJax => 1,
655659
});
656660
$textOptions = "JXG.merge($textOptions, " . Mojo::JSON::encode_json($label->style('jsx_options')) . ')'
657661
if $label->style('jsx_options');
658662

659-
$self->{JS} .= "board.create('text', [$x, $y, '$str'], $textOptions);";
663+
$self->{JS} .= "plot.createLabel($x, $y, '$str', $textOptions);";
660664
}
661665

662666
# JSXGraph only produces HTML graphs and uses TikZ for hadrcopy.

lib/Plots/Tikz.pm

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -728,9 +728,12 @@ sub draw {
728728
my $tikz_options = $label->style('tikz_options');
729729
my $h_align = $label->style('h_align') || 'center';
730730
my $v_align = $label->style('v_align') || 'middle';
731-
my $anchor = join(' ',
731+
my $anchor = $label->style('anchor');
732+
$anchor = join(' ',
732733
$v_align eq 'top' ? 'north' : $v_align eq 'bottom' ? 'south' : (),
733-
$h_align eq 'left' ? 'west' : $h_align eq 'right' ? 'east' : ());
734+
$h_align eq 'left' ? 'west' : $h_align eq 'right' ? 'east' : ())
735+
if $anchor eq '';
736+
my $padding = $label->style('padding') || 5;
734737
$str = {
735738
tiny => '\tiny ',
736739
small => '\small ',
@@ -746,6 +749,7 @@ sub draw {
746749
$tikz_options = $tikz_options ? "$color, $tikz_options" : $color;
747750
$tikz_options = "anchor=$anchor, $tikz_options" if $anchor;
748751
$tikz_options = "rotate=$rotate, $tikz_options" if $rotate;
752+
$tikz_options = "inner sep=${padding}pt, $tikz_options";
749753
$tikzCode .= $self->get_color($color) . "\\node[$tikz_options] at (axis cs: $x,$y) {$str};\n";
750754
}
751755

macros/graph/plots.pl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,21 @@ =head2 LABELS
588588
that states which end of the label is placed at the label's position.
589589
Can be one of 'top', 'middle', or 'bottom'. Default: 'middle'
590590
591+
=item anchor
592+
593+
The angle in degrees of the label anchor relative to the center of the text. In
594+
other words, the text will be positioned relative to the point on the rectangle
595+
encompassing the label text (including C<padding>) where a ray shot from the
596+
text center with the given angle hits the rectangle. This is an alternate method
597+
for positioning the text relative to the label position. If this is set, then
598+
C<h_align> and C<v_align> are not used. This is particularly useful for
599+
positioning text when labeling angles. Default: ''
600+
601+
=item padding
602+
603+
This is the horizontal and vertical padding applied to the text of the label (in
604+
pixels for the JSXGraph format, and in points for the TikZ format). Default: 5
605+
591606
=item jsx_options
592607
593608
An hash reference of options to pass to JSXGraph text object.

0 commit comments

Comments
 (0)