@opendata-ai/openchart-vanilla 6.24.2 → 6.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +74 -50
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/renderers/axes.ts +81 -60
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opendata-ai/openchart-vanilla",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.25.1",
|
|
4
4
|
"description": "Vanilla JS renderer for openchart: SVG charts, HTML tables, force-directed graphs",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Riley Hilliard",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@floating-ui/dom": "^1.7.6",
|
|
53
|
-
"@opendata-ai/openchart-core": "6.
|
|
54
|
-
"@opendata-ai/openchart-engine": "6.
|
|
53
|
+
"@opendata-ai/openchart-core": "6.25.1",
|
|
54
|
+
"@opendata-ai/openchart-engine": "6.25.1",
|
|
55
55
|
"d3-force": "^3.0.0",
|
|
56
56
|
"d3-quadtree": "^3.0.1"
|
|
57
57
|
},
|
package/src/renderers/axes.ts
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { AxisLayout, ChartLayout } from '@opendata-ai/openchart-core';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
estimateTextWidth,
|
|
8
|
+
getAxisTitleOffset,
|
|
9
|
+
TICK_LABEL_OFFSET,
|
|
10
|
+
} from '@opendata-ai/openchart-core';
|
|
7
11
|
import { applyTextStyle, createSVGElement, setAttrs } from './svg-dom';
|
|
8
12
|
|
|
9
13
|
function renderAxis(
|
|
@@ -13,7 +17,8 @@ function renderAxis(
|
|
|
13
17
|
layout: ChartLayout,
|
|
14
18
|
): void {
|
|
15
19
|
const g = createSVGElement('g');
|
|
16
|
-
|
|
20
|
+
const isRight = orientation === 'y' && axis.orient === 'right';
|
|
21
|
+
g.setAttribute('class', `oc-axis oc-axis-${isRight ? 'y2' : orientation}`);
|
|
17
22
|
|
|
18
23
|
const { area } = layout;
|
|
19
24
|
|
|
@@ -66,46 +71,45 @@ function renderAxis(
|
|
|
66
71
|
label.textContent = tick.label;
|
|
67
72
|
g.appendChild(label);
|
|
68
73
|
} else {
|
|
69
|
-
// Label (no tick marks -- gridlines provide sufficient reference)
|
|
70
74
|
const label = createSVGElement('text');
|
|
71
75
|
label.setAttribute('class', 'oc-axis-tick');
|
|
72
76
|
setAttrs(label, {
|
|
73
|
-
x: area.x -
|
|
77
|
+
x: isRight ? area.x + area.width + TICK_LABEL_OFFSET : area.x - TICK_LABEL_OFFSET,
|
|
74
78
|
y: tick.position,
|
|
75
|
-
'text-anchor': 'end',
|
|
79
|
+
'text-anchor': isRight ? 'start' : 'end',
|
|
76
80
|
'dominant-baseline': 'central',
|
|
77
81
|
});
|
|
78
82
|
applyTextStyle(label, axis.tickLabelStyle);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
hi = mid - 1;
|
|
83
|
+
if (!isRight) {
|
|
84
|
+
// Truncate categorical left y-axis labels that exceed available space
|
|
85
|
+
const availableWidth = area.x - TICK_LABEL_OFFSET;
|
|
86
|
+
const fontSize = axis.tickLabelStyle.fontSize;
|
|
87
|
+
const fontWeight = axis.tickLabelStyle.fontWeight;
|
|
88
|
+
const fullWidth = estimateTextWidth(tick.label, fontSize, fontWeight);
|
|
89
|
+
if (fullWidth > availableWidth && availableWidth > 20) {
|
|
90
|
+
const ellipsis = '…';
|
|
91
|
+
const ellipsisWidth = estimateTextWidth(ellipsis, fontSize, fontWeight);
|
|
92
|
+
let lo = 0;
|
|
93
|
+
let hi = tick.label.length;
|
|
94
|
+
while (lo < hi) {
|
|
95
|
+
const mid = (lo + hi + 1) >>> 1;
|
|
96
|
+
const candidate = tick.label.slice(0, mid);
|
|
97
|
+
if (
|
|
98
|
+
estimateTextWidth(candidate, fontSize, fontWeight) + ellipsisWidth <=
|
|
99
|
+
availableWidth
|
|
100
|
+
) {
|
|
101
|
+
lo = mid;
|
|
102
|
+
} else {
|
|
103
|
+
hi = mid - 1;
|
|
104
|
+
}
|
|
102
105
|
}
|
|
106
|
+
label.textContent = lo > 0 ? tick.label.slice(0, lo).trimEnd() + ellipsis : ellipsis;
|
|
107
|
+
const titleEl = createSVGElement('title');
|
|
108
|
+
titleEl.textContent = tick.label;
|
|
109
|
+
label.appendChild(titleEl);
|
|
110
|
+
} else {
|
|
111
|
+
label.textContent = tick.label;
|
|
103
112
|
}
|
|
104
|
-
label.textContent = lo > 0 ? tick.label.slice(0, lo).trimEnd() + ellipsis : ellipsis;
|
|
105
|
-
// Preserve the full label for accessibility / tooltips
|
|
106
|
-
const titleEl = createSVGElement('title');
|
|
107
|
-
titleEl.textContent = tick.label;
|
|
108
|
-
label.appendChild(titleEl);
|
|
109
113
|
} else {
|
|
110
114
|
label.textContent = tick.label;
|
|
111
115
|
}
|
|
@@ -114,31 +118,34 @@ function renderAxis(
|
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
// Gridlines (positions are also absolute from the scales)
|
|
117
|
-
for
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
121
|
+
// Skip gridlines for right-side y-axis (left y-axis gridlines are sufficient)
|
|
122
|
+
if (!isRight) {
|
|
123
|
+
for (const gridline of axis.gridlines) {
|
|
124
|
+
const gl = createSVGElement('line');
|
|
125
|
+
gl.setAttribute('class', 'oc-gridline');
|
|
126
|
+
if (orientation === 'y') {
|
|
127
|
+
setAttrs(gl, {
|
|
128
|
+
x1: area.x,
|
|
129
|
+
y1: gridline.position,
|
|
130
|
+
x2: area.x + area.width,
|
|
131
|
+
y2: gridline.position,
|
|
132
|
+
stroke: layout.theme.colors.gridline,
|
|
133
|
+
'stroke-width': 1,
|
|
134
|
+
'stroke-opacity': 0.6,
|
|
135
|
+
});
|
|
136
|
+
} else {
|
|
137
|
+
setAttrs(gl, {
|
|
138
|
+
x1: gridline.position,
|
|
139
|
+
y1: area.y,
|
|
140
|
+
x2: gridline.position,
|
|
141
|
+
y2: area.y + area.height,
|
|
142
|
+
stroke: layout.theme.colors.gridline,
|
|
143
|
+
'stroke-width': 1,
|
|
144
|
+
'stroke-opacity': 0.6,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
g.appendChild(gl);
|
|
140
148
|
}
|
|
141
|
-
g.appendChild(gl);
|
|
142
149
|
}
|
|
143
150
|
|
|
144
151
|
// Axis label
|
|
@@ -171,13 +178,24 @@ function renderAxis(
|
|
|
171
178
|
y: titleY,
|
|
172
179
|
'text-anchor': 'middle',
|
|
173
180
|
});
|
|
181
|
+
} else if (isRight) {
|
|
182
|
+
// Rotated right y-axis label (tighter offset on compact viewports)
|
|
183
|
+
const titleOffset = getAxisTitleOffset(layout.dimensions.width);
|
|
184
|
+
const titleX = area.x + area.width + titleOffset;
|
|
185
|
+
setAttrs(axisLabel, {
|
|
186
|
+
x: titleX,
|
|
187
|
+
y: area.y + area.height / 2,
|
|
188
|
+
'text-anchor': 'middle',
|
|
189
|
+
transform: `rotate(90, ${titleX}, ${area.y + area.height / 2})`,
|
|
190
|
+
});
|
|
174
191
|
} else {
|
|
175
|
-
// Rotated y-axis label
|
|
192
|
+
// Rotated left y-axis label (tighter offset on compact viewports)
|
|
193
|
+
const titleOffset = getAxisTitleOffset(layout.dimensions.width);
|
|
176
194
|
setAttrs(axisLabel, {
|
|
177
|
-
x: area.x -
|
|
195
|
+
x: area.x - titleOffset,
|
|
178
196
|
y: area.y + area.height / 2,
|
|
179
197
|
'text-anchor': 'middle',
|
|
180
|
-
transform: `rotate(-90, ${area.x -
|
|
198
|
+
transform: `rotate(-90, ${area.x - titleOffset}, ${area.y + area.height / 2})`,
|
|
181
199
|
});
|
|
182
200
|
}
|
|
183
201
|
g.appendChild(axisLabel);
|
|
@@ -193,4 +211,7 @@ export function renderAxes(parent: SVGElement, layout: ChartLayout): void {
|
|
|
193
211
|
if (layout.axes.y) {
|
|
194
212
|
renderAxis(parent, layout.axes.y, 'y', layout);
|
|
195
213
|
}
|
|
214
|
+
if (layout.axes.y2) {
|
|
215
|
+
renderAxis(parent, layout.axes.y2, 'y', layout);
|
|
216
|
+
}
|
|
196
217
|
}
|