@opendata-ai/openchart-vanilla 6.24.2 → 6.25.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opendata-ai/openchart-vanilla",
3
- "version": "6.24.2",
3
+ "version": "6.25.0",
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.24.2",
54
- "@opendata-ai/openchart-engine": "6.24.2",
53
+ "@opendata-ai/openchart-core": "6.25.0",
54
+ "@opendata-ai/openchart-engine": "6.25.0",
55
55
  "d3-force": "^3.0.0",
56
56
  "d3-quadtree": "^3.0.1"
57
57
  },
@@ -13,7 +13,8 @@ function renderAxis(
13
13
  layout: ChartLayout,
14
14
  ): void {
15
15
  const g = createSVGElement('g');
16
- g.setAttribute('class', `oc-axis oc-axis-${orientation}`);
16
+ const isRight = orientation === 'y' && axis.orient === 'right';
17
+ g.setAttribute('class', `oc-axis oc-axis-${isRight ? 'y2' : orientation}`);
17
18
 
18
19
  const { area } = layout;
19
20
 
@@ -66,46 +67,45 @@ function renderAxis(
66
67
  label.textContent = tick.label;
67
68
  g.appendChild(label);
68
69
  } else {
69
- // Label (no tick marks -- gridlines provide sufficient reference)
70
70
  const label = createSVGElement('text');
71
71
  label.setAttribute('class', 'oc-axis-tick');
72
72
  setAttrs(label, {
73
- x: area.x - 6,
73
+ x: isRight ? area.x + area.width + 6 : area.x - 6,
74
74
  y: tick.position,
75
- 'text-anchor': 'end',
75
+ 'text-anchor': isRight ? 'start' : 'end',
76
76
  'dominant-baseline': 'central',
77
77
  });
78
78
  applyTextStyle(label, axis.tickLabelStyle);
79
- // Truncate categorical y-axis labels that exceed available space so
80
- // they don't overflow into the chart area. The engine may clamp
81
- // margin.left on narrow containers; render what fits with an ellipsis.
82
- const availableWidth = area.x - 6;
83
- const fontSize = axis.tickLabelStyle.fontSize;
84
- const fontWeight = axis.tickLabelStyle.fontWeight;
85
- const fullWidth = estimateTextWidth(tick.label, fontSize, fontWeight);
86
- if (fullWidth > availableWidth && availableWidth > 20) {
87
- // Binary-search the longest prefix that fits with a trailing ellipsis
88
- const ellipsis = '\u2026';
89
- const ellipsisWidth = estimateTextWidth(ellipsis, fontSize, fontWeight);
90
- let lo = 0;
91
- let hi = tick.label.length;
92
- while (lo < hi) {
93
- const mid = (lo + hi + 1) >>> 1;
94
- const candidate = tick.label.slice(0, mid);
95
- if (
96
- estimateTextWidth(candidate, fontSize, fontWeight) + ellipsisWidth <=
97
- availableWidth
98
- ) {
99
- lo = mid;
100
- } else {
101
- hi = mid - 1;
79
+ if (!isRight) {
80
+ // Truncate categorical left y-axis labels that exceed available space
81
+ const availableWidth = area.x - 6;
82
+ const fontSize = axis.tickLabelStyle.fontSize;
83
+ const fontWeight = axis.tickLabelStyle.fontWeight;
84
+ const fullWidth = estimateTextWidth(tick.label, fontSize, fontWeight);
85
+ if (fullWidth > availableWidth && availableWidth > 20) {
86
+ const ellipsis = '…';
87
+ const ellipsisWidth = estimateTextWidth(ellipsis, fontSize, fontWeight);
88
+ let lo = 0;
89
+ let hi = tick.label.length;
90
+ while (lo < hi) {
91
+ const mid = (lo + hi + 1) >>> 1;
92
+ const candidate = tick.label.slice(0, mid);
93
+ if (
94
+ estimateTextWidth(candidate, fontSize, fontWeight) + ellipsisWidth <=
95
+ availableWidth
96
+ ) {
97
+ lo = mid;
98
+ } else {
99
+ hi = mid - 1;
100
+ }
102
101
  }
102
+ label.textContent = lo > 0 ? tick.label.slice(0, lo).trimEnd() + ellipsis : ellipsis;
103
+ const titleEl = createSVGElement('title');
104
+ titleEl.textContent = tick.label;
105
+ label.appendChild(titleEl);
106
+ } else {
107
+ label.textContent = tick.label;
103
108
  }
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
109
  } else {
110
110
  label.textContent = tick.label;
111
111
  }
@@ -114,31 +114,34 @@ function renderAxis(
114
114
  }
115
115
 
116
116
  // Gridlines (positions are also absolute from the scales)
117
- for (const gridline of axis.gridlines) {
118
- const gl = createSVGElement('line');
119
- gl.setAttribute('class', 'oc-gridline');
120
- if (orientation === 'y') {
121
- setAttrs(gl, {
122
- x1: area.x,
123
- y1: gridline.position,
124
- x2: area.x + area.width,
125
- y2: gridline.position,
126
- stroke: layout.theme.colors.gridline,
127
- 'stroke-width': 1,
128
- 'stroke-opacity': 0.6,
129
- });
130
- } else {
131
- setAttrs(gl, {
132
- x1: gridline.position,
133
- y1: area.y,
134
- x2: gridline.position,
135
- y2: area.y + area.height,
136
- stroke: layout.theme.colors.gridline,
137
- 'stroke-width': 1,
138
- 'stroke-opacity': 0.6,
139
- });
117
+ // Skip gridlines for right-side y-axis (left y-axis gridlines are sufficient)
118
+ if (!isRight) {
119
+ for (const gridline of axis.gridlines) {
120
+ const gl = createSVGElement('line');
121
+ gl.setAttribute('class', 'oc-gridline');
122
+ if (orientation === 'y') {
123
+ setAttrs(gl, {
124
+ x1: area.x,
125
+ y1: gridline.position,
126
+ x2: area.x + area.width,
127
+ y2: gridline.position,
128
+ stroke: layout.theme.colors.gridline,
129
+ 'stroke-width': 1,
130
+ 'stroke-opacity': 0.6,
131
+ });
132
+ } else {
133
+ setAttrs(gl, {
134
+ x1: gridline.position,
135
+ y1: area.y,
136
+ x2: gridline.position,
137
+ y2: area.y + area.height,
138
+ stroke: layout.theme.colors.gridline,
139
+ 'stroke-width': 1,
140
+ 'stroke-opacity': 0.6,
141
+ });
142
+ }
143
+ g.appendChild(gl);
140
144
  }
141
- g.appendChild(gl);
142
145
  }
143
146
 
144
147
  // Axis label
@@ -171,8 +174,17 @@ function renderAxis(
171
174
  y: titleY,
172
175
  'text-anchor': 'middle',
173
176
  });
177
+ } else if (isRight) {
178
+ // Rotated right y-axis label
179
+ const titleX = area.x + area.width + 45;
180
+ setAttrs(axisLabel, {
181
+ x: titleX,
182
+ y: area.y + area.height / 2,
183
+ 'text-anchor': 'middle',
184
+ transform: `rotate(90, ${titleX}, ${area.y + area.height / 2})`,
185
+ });
174
186
  } else {
175
- // Rotated y-axis label
187
+ // Rotated left y-axis label
176
188
  setAttrs(axisLabel, {
177
189
  x: area.x - 45,
178
190
  y: area.y + area.height / 2,
@@ -193,4 +205,7 @@ export function renderAxes(parent: SVGElement, layout: ChartLayout): void {
193
205
  if (layout.axes.y) {
194
206
  renderAxis(parent, layout.axes.y, 'y', layout);
195
207
  }
208
+ if (layout.axes.y2) {
209
+ renderAxis(parent, layout.axes.y2, 'y', layout);
210
+ }
196
211
  }