@@ -3,13 +3,13 @@ import { Axis } from 'd3'
33export function getAxes (
44 width : number ,
55 height : number ,
6- margin : { top : number ; right : number ; bottom : number ; left : number } ,
6+ rawMargin : { top : number ; right : number ; bottom : number ; left : number } ,
77 size : { x : number ; y : number }
88) {
9- // +/- 1 is to avoid overlapping axis lines
10- const xScale = ( value : number ) => ( value * ( width - margin . left - margin . right ) ) / size . x + margin . left + 1
11- const yScale = ( value : number ) =>
12- height - margin . bottom - ( value / size . y ) * ( height - margin . top - margin . bottom ) - 1
9+ // +1 is to avoid overlapping axis lines
10+ const margin = { ... rawMargin , left : rawMargin . left + 1 , bottom : rawMargin . bottom + 1 }
11+ const xScale = ( value : number ) => ( value * ( width - margin . left - margin . right ) ) / size . x + margin . left
12+ const yScale = ( value : number ) => height - margin . bottom - ( value / size . y ) * ( height - margin . top - margin . bottom )
1313 const innerWidth = width - margin . left - margin . right
1414 const innerHeight = height - margin . top - margin . bottom
1515 return { xScale, yScale, innerWidth, innerHeight }
@@ -62,6 +62,57 @@ export function drawXAxis(
6262 const textWidth = context . measureText ( label ) . width
6363 context . fillText ( label , xPos - textWidth / 2 , height )
6464 }
65+ } else {
66+ // auto mode
67+ const MIN_TICKS = 4
68+ const MAX_TICKS = 10
69+ const { min, max } = range
70+ const rangeSize = max - min
71+ const roughInterval = rangeSize / ( ( MAX_TICKS + MIN_TICKS ) / 2 )
72+ let interval = Math . pow ( 10 , Math . ceil ( Math . log10 ( roughInterval ) ) ) * 10
73+ let refinedInterval = 0
74+ while ( ! refinedInterval && interval >= 1 ) {
75+ interval = interval / 10
76+ refinedInterval =
77+ interval *
78+ ( [ 1 , 2 , 5 ] . find (
79+ ( multiplier ) =>
80+ rangeSize / ( interval * multiplier ) <= MAX_TICKS &&
81+ rangeSize / ( interval * multiplier ) >= MIN_TICKS
82+ ) || 0 )
83+ }
84+
85+ if ( ! refinedInterval ) {
86+ refinedInterval = Math . pow ( 10 , Math . ceil ( Math . log10 ( roughInterval ) ) )
87+ }
88+
89+ // Calculate tick positions
90+ const start = Math . ceil ( min / refinedInterval ) * refinedInterval
91+ const end = Math . floor ( max / refinedInterval ) * refinedInterval
92+ const ticks = [ ]
93+ for ( let tick = start ; tick <= end ; tick += refinedInterval ) {
94+ ticks . push ( tick )
95+ }
96+
97+ // Ensure a tick at 0 if within range
98+ if ( min <= 0 && max >= 0 && ! ticks . includes ( 0 ) ) {
99+ ticks . push ( 0 )
100+ ticks . sort ( ( a , b ) => a - b )
101+ }
102+
103+ // Render ticks and labels
104+ ticks . forEach ( ( tick ) => {
105+ const xPos = margin . left + ( tick - min ) / valueRangeScale
106+ context . beginPath ( )
107+ context . moveTo ( xPos , height - margin . bottom )
108+ context . lineTo ( xPos , height - margin . bottom + 6 )
109+ context . stroke ( )
110+
111+ context . fillStyle = options . textColor ?? 'black'
112+ const label = ( tick != Math . round ( tick ) ? tick . toFixed ( 1 ) : tick ) . toString ( )
113+ const textWidth = context . measureText ( label ) . width
114+ context . fillText ( label , xPos - textWidth / 2 , height )
115+ } )
65116 }
66117}
67118export function drawYAxis (
@@ -104,6 +155,58 @@ export function drawYAxis(
104155 const textWidth = context . measureText ( label ) . width
105156 context . fillText ( label , margin . left - textWidth - 10 , yPos + 5 )
106157 }
158+ } else {
159+ // auto mode for y-axis
160+ const MIN_TICKS = 3
161+ const MAX_TICKS = 10
162+ const { min, max } = range
163+ const rangeSize = max - min
164+ const roughInterval = rangeSize / ( ( MAX_TICKS + MIN_TICKS ) / 2 )
165+ let interval = Math . pow ( 10 , Math . ceil ( Math . log10 ( roughInterval ) ) ) * 10
166+ let refinedInterval = 0
167+
168+ while ( ! refinedInterval && interval >= 1 ) {
169+ interval = interval / 10
170+ refinedInterval =
171+ interval *
172+ ( [ 1 , 2 , 5 ] . find (
173+ ( multiplier ) =>
174+ rangeSize / ( interval * multiplier ) <= MAX_TICKS &&
175+ rangeSize / ( interval * multiplier ) >= MIN_TICKS
176+ ) || 0 )
177+ }
178+
179+ if ( ! refinedInterval ) {
180+ refinedInterval = Math . pow ( 10 , Math . ceil ( Math . log10 ( roughInterval ) ) )
181+ }
182+
183+ // Calculate tick positions
184+ const start = Math . ceil ( min / refinedInterval ) * refinedInterval
185+ const end = Math . floor ( max / refinedInterval ) * refinedInterval
186+ const ticks = [ ]
187+ for ( let tick = start ; tick <= end ; tick += refinedInterval ) {
188+ ticks . push ( tick )
189+ }
190+
191+ // Ensure a tick at 0 if within range
192+ if ( min <= 0 && max >= 0 && ! ticks . includes ( 0 ) ) {
193+ ticks . push ( 0 )
194+ ticks . sort ( ( a , b ) => a - b )
195+ }
196+
197+ // Render ticks and labels
198+ ticks . forEach ( ( tick ) => {
199+ const yPos = height - margin . bottom - ( tick - min ) / valueRangeScale
200+ context . beginPath ( )
201+ context . moveTo ( margin . left , yPos )
202+ context . lineTo ( margin . left - 6 , yPos )
203+ context . stroke ( )
204+
205+ context . fillStyle = options . textColor ?? 'black'
206+ const label = ( tick != Math . round ( tick ) ? tick . toFixed ( 1 ) : tick ) . toString ( )
207+ const textWidth = context . measureText ( label ) . width
208+ context . fillText ( label , margin . left - textWidth - 10 , yPos + 5 )
209+ } )
107210 }
108211}
109212
0 commit comments