@@ -166,6 +166,17 @@ export interface LineChartAttrs {
166166 * Note: When stacked, all series must be aligned to the same X values.
167167 */
168168 readonly stacked ?: boolean ;
169+
170+ /**
171+ * Position of the legend. Defaults to 'bottom' when multiple series,
172+ * 'top' otherwise.
173+ */
174+ readonly legendPosition ?: 'top' | 'right' | 'bottom' ;
175+
176+ /**
177+ * Callback when a series is clicked. Called with the series name.
178+ */
179+ readonly onSeriesClick ?: ( seriesName : string ) => void ;
169180}
170181
171182export class LineChart implements m . ClassComponent < LineChartAttrs > {
@@ -208,12 +219,17 @@ function buildLineOption(
208219 lineWidth = 2 ,
209220 gridLines,
210221 stacked = false ,
222+ legendPosition,
211223 } = attrs ;
212224 const fmtX = formatXValue ?? formatNumber ;
213225 const fmtY = formatYValue ?? formatNumber ;
214226
215227 const displayLegend = showLegend ?? data . series . length > 1 ;
228+ const legendPos =
229+ legendPosition ?? ( data . series . length > 1 ? 'bottom' : 'top' ) ;
216230
231+ // When stacking, reverse the series order so that the first series is drawn
232+ // on top. This aligns the series order with the legend and tooltip.
217233 const series = data . series . map ( ( s , i ) => {
218234 const base : Record < string , unknown > = {
219235 type : 'line' ,
@@ -227,9 +243,13 @@ function buildLineOption(
227243 showSymbol : showPoints ,
228244 symbolSize : 6 ,
229245 triggerLineEvent : true ,
230- emphasis : { focus : 'series' , itemStyle : { borderWidth : 2 } } ,
246+ emphasis : stacked
247+ ? { focus : 'series' , blurScope : 'global' as const }
248+ : { focus : 'series' , itemStyle : { borderWidth : 2 } } ,
231249 stack : stacked ? 'total' : undefined ,
232- areaStyle : stacked ? { } : undefined ,
250+ areaStyle : stacked ? { opacity : 0.8 } : undefined ,
251+ // invisible wider hitbox
252+ silent : false ,
233253 } ;
234254
235255 // Render selection highlight on the first series only.
@@ -243,8 +263,16 @@ function buildLineOption(
243263
244264 const option = buildChartOption ( {
245265 grid : {
246- top : displayLegend ? 30 : 10 ,
247- bottom : xAxisLabel ? 40 : 25 ,
266+ top : legendPos === 'top' && displayLegend ? 30 : 10 ,
267+ right : legendPos === 'right' && displayLegend ? 150 : undefined ,
268+ bottom :
269+ legendPos === 'bottom' && displayLegend
270+ ? xAxisLabel
271+ ? 70
272+ : 55
273+ : xAxisLabel
274+ ? 40
275+ : 25 ,
248276 } ,
249277 xAxis : {
250278 // Nasty ECharts quirk: when stacking, the xAxis must be type 'category'
@@ -282,16 +310,31 @@ function buildLineOption(
282310 ) => {
283311 if ( ! Array . isArray ( params ) || params . length === 0 ) return '' ;
284312 const xVal = params [ 0 ] . data ?. [ 0 ] ;
285- const header = xVal !== undefined ? `X: ${ fmtX ( xVal ) } ` : '' ;
286- const lines = params . map (
313+ const header = xVal !== undefined ? fmtX ( xVal ) : '' ;
314+ const ordered = stacked ? [ ...params ] . reverse ( ) : params ;
315+ const lines = ordered . map (
287316 ( p ) =>
288317 `${ p . marker ?? '' } ${ p . seriesName ?? '' } : ${ fmtY ( p . data ?. [ 1 ] ?? 0 ) } ` ,
289318 ) ;
290319 return [ header , ...lines ] . join ( '<br>' ) ;
291320 } ,
292321 } ,
293322 brush : attrs . onBrush ? { xAxisIndex : 0 , brushType : 'lineX' } : undefined ,
294- legend : displayLegend ? buildLegendOption ( ) : { show : false } ,
323+ legend : displayLegend
324+ ? {
325+ ...buildLegendOption ( legendPos ) ,
326+ ...( stacked
327+ ? { data : [ ...data . series ] . reverse ( ) . map ( ( s ) => s . name ) }
328+ : { } ) ,
329+ formatter : ( name : string ) => {
330+ const s = data . series . find ( ( sr ) => sr . name === name ) ;
331+ if ( s !== undefined && s . points . length > 0 ) {
332+ return `${ name } ${ fmtY ( s . points [ s . points . length - 1 ] . y ) } ` ;
333+ }
334+ return name ;
335+ } ,
336+ }
337+ : { show : false } ,
295338 } ) ;
296339
297340 ( option as Record < string , unknown > ) . series = series ;
@@ -302,18 +345,16 @@ function buildLineEventHandlers(
302345 attrs : LineChartAttrs ,
303346 data : LineChartData | undefined ,
304347) : ReadonlyArray < EChartEventHandler > {
348+ const handlers : EChartEventHandler [ ] = [ ] ;
349+
305350 if (
306- ! attrs . onBrush ||
307- data === undefined ||
308- data . series . length === 0 ||
309- data . series . every ( ( s ) => s . points . length === 0 )
351+ attrs . onBrush &&
352+ data !== undefined &&
353+ data . series . length > 0 &&
354+ data . series . some ( ( s ) => s . points . length > 0 )
310355 ) {
311- return [ ] ;
312- }
313- const onBrush = attrs . onBrush ;
314-
315- return [
316- {
356+ const onBrush = attrs . onBrush ;
357+ handlers . push ( {
317358 eventName : 'brushEnd' ,
318359 handler : ( params ) => {
319360 const range = extractBrushRange ( params ) ;
@@ -322,6 +363,21 @@ function buildLineEventHandlers(
322363 onBrush ( { start, end} ) ;
323364 }
324365 } ,
325- } ,
326- ] ;
366+ } ) ;
367+ }
368+
369+ if ( attrs . onSeriesClick ) {
370+ const onSeriesClick = attrs . onSeriesClick ;
371+ handlers . push ( {
372+ eventName : 'click' ,
373+ handler : ( params ) => {
374+ const p = params as { seriesName ?: string } ;
375+ if ( p . seriesName !== undefined ) {
376+ onSeriesClick ( p . seriesName ) ;
377+ }
378+ } ,
379+ } ) ;
380+ }
381+
382+ return handlers ;
327383}
0 commit comments