1212import type {
1313 ArcMark ,
1414 AreaMark ,
15- AxisLayout ,
1615 ChartLayout ,
1716 LineMark ,
1817 Mark ,
@@ -23,10 +22,10 @@ import type {
2322 TextMarkLayout ,
2423 TickMarkLayout ,
2524} from '@opendata-ai/openchart-core' ;
26- import { estimateTextWidth } from '@opendata-ai/openchart-core' ;
2725import { clampStaggerDelay } from '@opendata-ai/openchart-engine' ;
2826import { buildGradientDefs , resolveMarkFill } from './gradient-utils' ;
2927import { renderAnnotations } from './renderers/annotations' ;
28+ import { renderAxes } from './renderers/axes' ;
3029import { renderBrand } from './renderers/brand' ;
3130import { renderChrome } from './renderers/chrome' ;
3231import { renderLegend } from './renderers/legend' ;
@@ -67,167 +66,6 @@ const EASE_VAR_MAP: Record<string, string> = {
6766 snappy : 'var(--oc-ease-snappy)' ,
6867} ;
6968
70- // ---------------------------------------------------------------------------
71- // Axis rendering
72- // ---------------------------------------------------------------------------
73-
74- function renderAxis (
75- parent : SVGElement ,
76- axis : AxisLayout ,
77- orientation : 'x' | 'y' ,
78- layout : ChartLayout ,
79- ) : void {
80- const g = createSVGElement ( 'g' ) ;
81- g . setAttribute ( 'class' , `oc-axis oc-axis-${ orientation } ` ) ;
82-
83- const { area } = layout ;
84-
85- // Only draw axis line for x-axis (bottom baseline).
86- // Horizontal gridlines already guide y-values, so the vertical y-axis line is redundant.
87- if ( orientation === 'x' ) {
88- const line = createSVGElement ( 'line' ) ;
89- line . setAttribute ( 'class' , 'oc-axis-line' ) ;
90- setAttrs ( line , {
91- x1 : axis . start . x ,
92- y1 : axis . start . y ,
93- x2 : axis . end . x ,
94- y2 : axis . end . y ,
95- stroke : layout . theme . colors . axis ,
96- 'stroke-width' : 1 ,
97- } ) ;
98- g . appendChild ( line ) ;
99- }
100-
101- // Ticks and labels
102- // Tick positions are absolute pixel coordinates from D3 scales whose range
103- // was set to [chartArea.x, chartArea.x + chartArea.width] (and similarly for y).
104- // Don't add area.x/area.y again or you'll double-offset everything.
105- for ( const tick of axis . ticks ) {
106- if ( orientation === 'x' ) {
107- // Label (no tick marks -- gridlines provide sufficient reference)
108- const label = createSVGElement ( 'text' ) ;
109- label . setAttribute ( 'class' , 'oc-axis-tick' ) ;
110-
111- if ( axis . tickAngle && Math . abs ( axis . tickAngle ) > 10 ) {
112- // Rotated labels: anchor at the rotation pivot point
113- const labelX = tick . position ;
114- const labelY = area . y + area . height + 6 ;
115- setAttrs ( label , {
116- x : labelX ,
117- y : labelY ,
118- 'text-anchor' : axis . tickAngle < 0 ? 'end' : 'start' ,
119- 'dominant-baseline' : 'central' ,
120- transform : `rotate(${ axis . tickAngle } , ${ labelX } , ${ labelY } )` ,
121- } ) ;
122- } else {
123- setAttrs ( label , {
124- x : tick . position ,
125- y : area . y + area . height + 14 ,
126- 'text-anchor' : 'middle' ,
127- } ) ;
128- }
129-
130- applyTextStyle ( label , axis . tickLabelStyle ) ;
131- label . textContent = tick . label ;
132- g . appendChild ( label ) ;
133- } else {
134- // Label (no tick marks -- gridlines provide sufficient reference)
135- const label = createSVGElement ( 'text' ) ;
136- label . setAttribute ( 'class' , 'oc-axis-tick' ) ;
137- setAttrs ( label , {
138- x : area . x - 6 ,
139- y : tick . position ,
140- 'text-anchor' : 'end' ,
141- 'dominant-baseline' : 'central' ,
142- } ) ;
143- applyTextStyle ( label , axis . tickLabelStyle ) ;
144- label . textContent = tick . label ;
145- g . appendChild ( label ) ;
146- }
147- }
148-
149- // Gridlines (positions are also absolute from the scales)
150- for ( const gridline of axis . gridlines ) {
151- const gl = createSVGElement ( 'line' ) ;
152- gl . setAttribute ( 'class' , 'oc-gridline' ) ;
153- if ( orientation === 'y' ) {
154- setAttrs ( gl , {
155- x1 : area . x ,
156- y1 : gridline . position ,
157- x2 : area . x + area . width ,
158- y2 : gridline . position ,
159- stroke : layout . theme . colors . gridline ,
160- 'stroke-width' : 1 ,
161- 'stroke-opacity' : 0.6 ,
162- } ) ;
163- } else {
164- setAttrs ( gl , {
165- x1 : gridline . position ,
166- y1 : area . y ,
167- x2 : gridline . position ,
168- y2 : area . y + area . height ,
169- stroke : layout . theme . colors . gridline ,
170- 'stroke-width' : 1 ,
171- 'stroke-opacity' : 0.6 ,
172- } ) ;
173- }
174- g . appendChild ( gl ) ;
175- }
176-
177- // Axis label
178- if ( axis . label && axis . labelStyle ) {
179- const axisLabel = createSVGElement ( 'text' ) ;
180- axisLabel . setAttribute ( 'class' , 'oc-axis-title' ) ;
181- applyTextStyle ( axisLabel , axis . labelStyle ) ;
182- axisLabel . textContent = axis . label ;
183-
184- if ( orientation === 'x' ) {
185- // Position axis title below tick labels. For rotated labels, compute
186- // the vertical extent of the rotated ticks and place the title below.
187- let titleY = area . y + area . height + 35 ;
188- if ( axis . tickAngle && Math . abs ( axis . tickAngle ) > 10 ) {
189- const angleRad = Math . abs ( axis . tickAngle ) * ( Math . PI / 180 ) ;
190- let maxLabelWidth = 40 ;
191- for ( const tick of axis . ticks ) {
192- const w = estimateTextWidth (
193- tick . label ,
194- axis . tickLabelStyle . fontSize ,
195- axis . tickLabelStyle . fontWeight ,
196- ) ;
197- if ( w > maxLabelWidth ) maxLabelWidth = w ;
198- }
199- const rotatedHeight = Math . min ( maxLabelWidth * Math . sin ( angleRad ) + 6 , 120 ) ;
200- titleY = area . y + area . height + rotatedHeight + 14 ;
201- }
202- setAttrs ( axisLabel , {
203- x : area . x + area . width / 2 ,
204- y : titleY ,
205- 'text-anchor' : 'middle' ,
206- } ) ;
207- } else {
208- // Rotated y-axis label
209- setAttrs ( axisLabel , {
210- x : area . x - 45 ,
211- y : area . y + area . height / 2 ,
212- 'text-anchor' : 'middle' ,
213- transform : `rotate(-90, ${ area . x - 45 } , ${ area . y + area . height / 2 } )` ,
214- } ) ;
215- }
216- g . appendChild ( axisLabel ) ;
217- }
218-
219- parent . appendChild ( g ) ;
220- }
221-
222- function renderAxes ( parent : SVGElement , layout : ChartLayout ) : void {
223- if ( layout . axes . x ) {
224- renderAxis ( parent , layout . axes . x , 'x' , layout ) ;
225- }
226- if ( layout . axes . y ) {
227- renderAxis ( parent , layout . axes . y , 'y' , layout ) ;
228- }
229- }
230-
23169// ---------------------------------------------------------------------------
23270// Mark rendering (dispatch per mark type)
23371// ---------------------------------------------------------------------------
0 commit comments