@@ -17,6 +17,9 @@ import {
1717import { _alignStartEnd , _textX , _toLeftRightCenter } from '../helpers/helpers.extras.js' ;
1818import { toTRBLCorners } from '../helpers/helpers.options.js' ;
1919
20+
21+ // @ts -ignore
22+
2023/**
2124 * @typedef { import('../types/index.js').ChartEvent } ChartEvent
2225 */
@@ -119,40 +122,74 @@ export class Legend extends Element {
119122
120123 this . legendItems = legendItems ;
121124 }
125+ fit ( ) {
126+ const { options, ctx } = this ;
127+ if ( ! options . display ) {
128+ this . width = this . height = 0 ;
129+ return ;
130+ }
122131
123- fit ( ) {
124- const { options, ctx} = this ;
125-
126- // The legend may not be displayed for a variety of reasons including
127- // the fact that the defaults got set to `false`.
128- // When the legend is not displayed, there are no guarantees that the options
129- // are correctly formatted so we need to bail out as early as possible.
130- if ( ! options . display ) {
131- this . width = this . height = 0 ;
132- return ;
133- }
132+ const labelOpts = options . labels ;
133+ const labelFont = toFont ( labelOpts . font ) ;
134+ const fontSize = labelFont . size ;
135+ const titleHeight = this . _computeTitleHeight ( ) ;
136+ const { boxWidth, itemHeight } = getBoxSize ( labelOpts , fontSize ) ;
137+
138+ let width , height ;
139+ ctx . font = labelFont . string ;
140+
141+ if ( this . isHorizontal ( ) ) {
142+ width = this . maxWidth ;
143+ height = this . _fitRows ( titleHeight , fontSize , boxWidth , itemHeight ) + 10 ;
144+ } else {
145+ height = this . maxHeight ;
146+ width = this . _fitCols ( titleHeight , labelFont , boxWidth , itemHeight ) + 10 ;
147+ }
134148
135- const labelOpts = options . labels ;
136- const labelFont = toFont ( labelOpts . font ) ;
137- const fontSize = labelFont . size ;
138- const titleHeight = this . _computeTitleHeight ( ) ;
139- const { boxWidth, itemHeight} = getBoxSize ( labelOpts , fontSize ) ;
149+ // --- handle scroll height limit ---
150+ const scrollOpts = options . scroll || { } ;
151+ if ( scrollOpts . enabled && scrollOpts . maxItems ) {
152+ const singleItemHeight = itemHeight + labelOpts . padding ;
153+ const visibleHeight = singleItemHeight * scrollOpts . maxItems + titleHeight + labelOpts . padding * 2 ;
154+ this . height = Math . min ( this . height , visibleHeight ) ;
140155
141- let width , height ;
156+ // wrap legend in scroll container
157+ this . _wrapLegendScroll ( visibleHeight ) ;
158+ }
142159
143- ctx . font = labelFont . string ;
160+ this . width = Math . min ( width , options . maxWidth || this . maxWidth ) ;
161+ this . height = Math . min ( height , options . maxHeight || this . maxHeight ) ;
162+ }
144163
145- if ( this . isHorizontal ( ) ) {
146- width = this . maxWidth ; // fill all the width
147- height = this . _fitRows ( titleHeight , fontSize , boxWidth , itemHeight ) + 10 ;
148- } else {
149- height = this . maxHeight ; // fill all the height
150- width = this . _fitCols ( titleHeight , labelFont , boxWidth , itemHeight ) + 10 ;
151- }
164+ /**
165+ * Private helper to wrap the <ul> legend in a scrollable container
166+ */
167+ _wrapLegendScroll ( visibleHeight ) {
168+ const canvasContainer = this . chart . canvas . parentNode ;
169+ if ( ! canvasContainer ) return ;
170+
171+ const legendUL = canvasContainer . querySelector ( 'ul.chartjs-legend' ) ;
172+ if ( ! legendUL ) return ;
173+
174+ // Skip if already wrapped
175+ if ( canvasContainer . querySelector ( '.legend-scroll-wrapper' ) ) return ;
176+
177+ const legendWrapper = document . createElement ( 'div' ) ;
178+ legendWrapper . className = 'legend-scroll-wrapper' ;
179+ legendWrapper . style . overflowY = 'auto' ;
180+ legendWrapper . style . maxHeight = `${ visibleHeight } px` ;
181+ legendWrapper . style . display = 'inline-block' ;
182+
183+ // Force single-column layout
184+ legendUL . style . display = 'flex' ;
185+ legendUL . style . flexDirection = 'column' ;
186+ legendUL . style . margin = '0' ;
187+ legendUL . style . padding = '0' ;
188+
189+ legendUL . parentNode . insertBefore ( legendWrapper , legendUL ) ;
190+ legendWrapper . appendChild ( legendUL ) ;
191+ }
152192
153- this . width = Math . min ( width , options . maxWidth || this . maxWidth ) ;
154- this . height = Math . min ( height , options . maxHeight || this . maxHeight ) ;
155- }
156193
157194 /**
158195 * @private
@@ -188,45 +225,62 @@ export class Legend extends Element {
188225 return totalHeight ;
189226 }
190227
191- _fitCols ( titleHeight , labelFont , boxWidth , _itemHeight ) {
192- const { ctx, maxHeight, options : { labels : { padding} } } = this ;
193- const hitboxes = this . legendHitBoxes = [ ] ;
194- const columnSizes = this . columnSizes = [ ] ;
195- const heightLimit = maxHeight - titleHeight ;
228+ _fitCols ( titleHeight , labelFont , boxWidth , _itemHeight ) {
229+ const { ctx, maxHeight, options : { labels : { padding} } } = this ;
230+ const scrollOpts = this . options . scroll || { } ;
231+ const hitboxes = this . legendHitBoxes = [ ] ;
232+ const columnSizes = this . columnSizes = [ ] ;
196233
197- let totalWidth = padding ;
198- let currentColWidth = 0 ;
199- let currentColHeight = 0 ;
234+ const heightLimit = maxHeight - titleHeight ;
200235
201- let left = 0 ;
202- let col = 0 ;
236+ // --- SCROLL ENABLED: force single column ---
237+ if ( scrollOpts . enabled && scrollOpts . maxItems ) {
238+ let totalWidth = 0 ;
239+ let top = 0 ;
240+ let maxItemWidth = 0 ;
203241
204242 this . legendItems . forEach ( ( legendItem , i ) => {
205243 const { itemWidth, itemHeight} = calculateItemSize ( boxWidth , labelFont , ctx , legendItem , _itemHeight ) ;
206-
207- // If too tall, go to new column
208- if ( i > 0 && currentColHeight + itemHeight + 2 * padding > heightLimit ) {
209- totalWidth += currentColWidth + padding ;
210- columnSizes . push ( { width : currentColWidth , height : currentColHeight } ) ; // previous column size
211- left += currentColWidth + padding ;
212- col ++ ;
213- currentColWidth = currentColHeight = 0 ;
214- }
215-
216- // Store the hitbox width and height here. Final position will be updated in `draw`
217- hitboxes [ i ] = { left, top : currentColHeight , col, width : itemWidth , height : itemHeight } ;
218-
219- // Get max width
220- currentColWidth = Math . max ( currentColWidth , itemWidth ) ;
221- currentColHeight += itemHeight + padding ;
244+ hitboxes [ i ] = { left : 0 , top, col : 0 , width : itemWidth , height : itemHeight } ;
245+ top += itemHeight + padding ;
246+ maxItemWidth = Math . max ( maxItemWidth , itemWidth ) ;
222247 } ) ;
223248
224- totalWidth += currentColWidth ;
225- columnSizes . push ( { width : currentColWidth , height : currentColHeight } ) ; // previous column size
249+ columnSizes . push ( { width : maxItemWidth , height : top } ) ;
250+ totalWidth = maxItemWidth + 2 * padding ;
226251
227252 return totalWidth ;
228253 }
229254
255+ // --- SCROLL DISABLED: original multi-column logic ---
256+ let totalWidth = padding ;
257+ let currentColWidth = 0 ;
258+ let currentColHeight = 0 ;
259+ let left = 0 ;
260+ let col = 0 ;
261+
262+ this . legendItems . forEach ( ( legendItem , i ) => {
263+ const { itemWidth, itemHeight} = calculateItemSize ( boxWidth , labelFont , ctx , legendItem , _itemHeight ) ;
264+
265+ if ( i > 0 && currentColHeight + itemHeight + 2 * padding > heightLimit ) {
266+ totalWidth += currentColWidth + padding ;
267+ columnSizes . push ( { width : currentColWidth , height : currentColHeight } ) ;
268+ left += currentColWidth + padding ;
269+ col ++ ;
270+ currentColWidth = currentColHeight = 0 ;
271+ }
272+
273+ hitboxes [ i ] = { left, top : currentColHeight , col, width : itemWidth , height : itemHeight } ;
274+
275+ currentColWidth = Math . max ( currentColWidth , itemWidth ) ;
276+ currentColHeight += itemHeight + padding ;
277+ } ) ;
278+
279+ totalWidth += currentColWidth ;
280+ columnSizes . push ( { width : currentColWidth , height : currentColHeight } ) ;
281+
282+ return totalWidth ;
283+ }
230284 adjustHitBoxes ( ) {
231285 if ( ! this . options . display ) {
232286 return ;
@@ -391,7 +445,12 @@ export class Legend extends Element {
391445 overrideTextDirection ( this . ctx , opts . textDirection ) ;
392446
393447 const lineHeight = itemHeight + padding ;
394- this . legendItems . forEach ( ( legendItem , i ) => {
448+ const scrollOpts = this . options . scroll || { } ;
449+ let legendItems = this . legendItems ;
450+ if ( scrollOpts . enabled && scrollOpts . maxItems ) {
451+ legendItems = legendItems . slice ( 0 , scrollOpts . maxItems ) ;
452+ }
453+ legendItems . forEach ( ( legendItem , i ) => {
395454 ctx . strokeStyle = legendItem . fontColor ; // for strikethrough effect
396455 ctx . fillStyle = legendItem . fontColor ; // render in correct colour
397456
@@ -552,6 +611,15 @@ export class Legend extends Element {
552611 }
553612}
554613
614+
615+
616+
617+
618+
619+
620+
621+
622+
555623function calculateItemSize ( boxWidth , labelFont , ctx , legendItem , _itemHeight ) {
556624 const itemWidth = calculateItemWidth ( legendItem , boxWidth , labelFont , ctx ) ;
557625 const itemHeight = calculateItemHeight ( _itemHeight , legendItem , labelFont . lineHeight ) ;
@@ -624,6 +692,20 @@ export default {
624692 const legend = chart . legend ;
625693 legend . buildLabels ( ) ;
626694 legend . adjustHitBoxes ( ) ;
695+ const scrollOpts = legend . options . scroll || { } ;
696+ if ( scrollOpts . enabled && scrollOpts . maxItems ) {
697+ const canvasContainer = chart . canvas . parentNode ;
698+ if ( canvasContainer ) {
699+ const legendUL = canvasContainer . querySelector ( 'ul.chartjs-legend' ) ;
700+ if ( legendUL && ! canvasContainer . querySelector ( '.legend-scroll-wrapper' ) ) {
701+ const singleItemHeight = legend . legendHitBoxes [ 0 ] . height + legend . options . labels . padding ;
702+ const titleHeight = legend . _computeTitleHeight ( ) ;
703+ const visibleHeight = singleItemHeight * scrollOpts . maxItems + titleHeight + legend . options . labels . padding * 2 ;
704+ legend . _wrapLegendScroll ( visibleHeight ) ;
705+ console . log ( 'Legend wrapper created inside afterUpdate!' ) ;
706+ }
707+ }
708+ }
627709 } ,
628710
629711
0 commit comments