@@ -1338,22 +1338,115 @@ function render({ model, el }) {
13381338 // Clip
13391339 ctx . save ( ) ; ctx . beginPath ( ) ; ctx . rect ( r . x , r . y , r . w , r . h ) ; ctx . clip ( ) ;
13401340
1341- function _drawLine ( yData , lineXArr , color , lw ) {
1342- if ( ! yData || ! yData . length ) return ;
1343- const n = yData . length ;
1344- ctx . beginPath ( ) ; ctx . strokeStyle = color ; ctx . lineWidth = lw ; ctx . lineJoin = 'round' ;
1345- let first = true ;
1346- for ( let i = 0 ; i < n ; i ++ ) {
1347- const xFrac = lineXArr . length >= 2 ?( lineXArr [ i ] - lineXArr [ 0 ] ) / ( ( lineXArr [ lineXArr . length - 1 ] - lineXArr [ 0 ] ) || 1 ) :i / ( ( n - 1 ) || 1 ) ;
1348- const px = _fracToPx1d ( xFrac , x0 , x1 , r ) , py = _valToPy1d ( yData [ i ] , dMin , dMax , r ) ;
1349- if ( first ) { ctx . moveTo ( px , py ) ; first = false ; } else { ctx . lineTo ( px , py ) ; }
1341+ // Linestyle → canvas dash pattern
1342+ const _LINESTYLE_DASH = {
1343+ 'solid' : [ ] ,
1344+ 'dashed' : [ 6 , 3 ] ,
1345+ 'dotted' : [ 2 , 3 ] ,
1346+ 'dashdot' : [ 6 , 3 , 2 , 3 ] ,
1347+ } ;
1348+
1349+ // Draw a single marker symbol centred at (px, py) with half-size ms.
1350+ // The caller is responsible for beginPath() before and fill()/stroke() after.
1351+ function _drawMarkerSymbol ( mctx , marker , px , py , ms ) {
1352+ switch ( marker ) {
1353+ case 'o' :
1354+ mctx . arc ( px , py , ms , 0 , Math . PI * 2 ) ;
1355+ break ;
1356+ case 's' :
1357+ mctx . rect ( px - ms , py - ms , ms * 2 , ms * 2 ) ;
1358+ break ;
1359+ case '^' :
1360+ mctx . moveTo ( px , py - ms ) ;
1361+ mctx . lineTo ( px + ms , py + ms ) ;
1362+ mctx . lineTo ( px - ms , py + ms ) ;
1363+ mctx . closePath ( ) ;
1364+ break ;
1365+ case 'v' :
1366+ mctx . moveTo ( px , py + ms ) ;
1367+ mctx . lineTo ( px + ms , py - ms ) ;
1368+ mctx . lineTo ( px - ms , py - ms ) ;
1369+ mctx . closePath ( ) ;
1370+ break ;
1371+ case 'D' :
1372+ mctx . moveTo ( px , py - ms ) ;
1373+ mctx . lineTo ( px + ms , py ) ;
1374+ mctx . lineTo ( px , py + ms ) ;
1375+ mctx . lineTo ( px - ms , py ) ;
1376+ mctx . closePath ( ) ;
1377+ break ;
1378+ case '+' :
1379+ mctx . moveTo ( px - ms , py ) ; mctx . lineTo ( px + ms , py ) ;
1380+ mctx . moveTo ( px , py - ms ) ; mctx . lineTo ( px , py + ms ) ;
1381+ break ;
1382+ case 'x' :
1383+ mctx . moveTo ( px - ms , py - ms ) ; mctx . lineTo ( px + ms , py + ms ) ;
1384+ mctx . moveTo ( px + ms , py - ms ) ; mctx . lineTo ( px - ms , py + ms ) ;
1385+ break ;
1386+ default :
1387+ mctx . arc ( px , py , ms , 0 , Math . PI * 2 ) ;
1388+ }
1389+ }
1390+
1391+ // Stroke-only markers (no meaningful fill area)
1392+ const _MARKER_STROKE_ONLY = new Set ( [ '+' , 'x' ] ) ;
1393+
1394+ function _drawLine ( yData , lineXArr , color , lw , linestyle , alpha , marker , markersize ) {
1395+ if ( ! yData || ! yData . length ) return ;
1396+ const n = yData . length ;
1397+ const dash = _LINESTYLE_DASH [ linestyle || 'solid' ] || [ ] ;
1398+ const eff_alpha = ( alpha != null && alpha < 1.0 ) ? alpha : 1.0 ;
1399+ const ms = Math . max ( 1 , markersize || 4 ) ;
1400+ const doMarker = marker && marker !== 'none' ;
1401+
1402+ ctx . save ( ) ;
1403+ if ( eff_alpha < 1.0 ) ctx . globalAlpha = eff_alpha ;
1404+ ctx . setLineDash ( dash ) ;
1405+ ctx . beginPath ( ) ;
1406+ ctx . strokeStyle = color ; ctx . lineWidth = lw ; ctx . lineJoin = 'round' ;
1407+
1408+ const pts = doMarker ? [ ] : null ;
1409+ let first = true ;
1410+ for ( let i = 0 ; i < n ; i ++ ) {
1411+ const xFrac = lineXArr . length >= 2
1412+ ? ( lineXArr [ i ] - lineXArr [ 0 ] ) / ( ( lineXArr [ lineXArr . length - 1 ] - lineXArr [ 0 ] ) || 1 )
1413+ : i / ( ( n - 1 ) || 1 ) ;
1414+ const px = _fracToPx1d ( xFrac , x0 , x1 , r ) ;
1415+ const py = _valToPy1d ( yData [ i ] , dMin , dMax , r ) ;
1416+ if ( first ) { ctx . moveTo ( px , py ) ; first = false ; } else { ctx . lineTo ( px , py ) ; }
1417+ if ( pts ) pts . push ( [ px , py ] ) ;
13501418 }
13511419 ctx . stroke ( ) ;
1420+ ctx . setLineDash ( [ ] ) ;
1421+
1422+ // Per-point marker symbols
1423+ if ( doMarker && pts && pts . length ) {
1424+ ctx . strokeStyle = color ;
1425+ ctx . fillStyle = color ;
1426+ ctx . lineWidth = Math . max ( 1 , lw * 0.8 ) ;
1427+ const strokeOnly = _MARKER_STROKE_ONLY . has ( marker ) ;
1428+ for ( const [ px , py ] of pts ) {
1429+ ctx . beginPath ( ) ;
1430+ _drawMarkerSymbol ( ctx , marker , px , py , ms ) ;
1431+ if ( ! strokeOnly ) ctx . fill ( ) ;
1432+ ctx . stroke ( ) ;
1433+ }
1434+ }
1435+
1436+ ctx . restore ( ) ;
13521437 }
13531438
1354- _drawLine ( st . data , xArr , st . line_color || '#4fc3f7' , st . line_linewidth || 1.5 ) ;
1355- for ( const ex of ( st . extra_lines || [ ] ) ) {
1356- _drawLine ( ex . data || [ ] , ex . x_axis || xArr , ex . color || ( theme . dark ?'#fff' :'#333' ) , ex . linewidth || 1.5 ) ;
1439+ _drawLine ( st . data , xArr ,
1440+ st . line_color || '#4fc3f7' , st . line_linewidth || 1.5 ,
1441+ st . line_linestyle || 'solid' ,
1442+ st . line_alpha != null ? st . line_alpha : 1.0 ,
1443+ st . line_marker || 'none' , st . line_markersize || 4 ) ;
1444+ for ( const ex of ( st . extra_lines || [ ] ) ) {
1445+ _drawLine ( ex . data || [ ] , ex . x_axis || xArr ,
1446+ ex . color || ( theme . dark ? '#fff' : '#333' ) , ex . linewidth || 1.5 ,
1447+ ex . linestyle || 'solid' ,
1448+ ex . alpha != null ? ex . alpha : 1.0 ,
1449+ ex . marker || 'none' , ex . markersize || 4 ) ;
13571450 }
13581451 ctx . restore ( ) ;
13591452
@@ -1400,12 +1493,28 @@ function render({ model, el }) {
14001493
14011494 // Legend
14021495 const labels = [ ] ;
1403- if ( st . line_label ) labels . push ( { color :st . line_color || '#4fc3f7' , text :st . line_label } ) ;
1404- for ( const ex of ( st . extra_lines || [ ] ) ) if ( ex . label ) labels . push ( { color :ex . color || ( theme . dark ?'#fff' :'#333' ) , text :ex . label } ) ;
1496+ if ( st . line_label ) labels . push ( {
1497+ color : st . line_color || '#4fc3f7' , text : st . line_label ,
1498+ linestyle : st . line_linestyle || 'solid' ,
1499+ marker : st . line_marker || 'none' , ms : st . line_markersize || 4 ,
1500+ } ) ;
1501+ for ( const ex of ( st . extra_lines || [ ] ) ) if ( ex . label ) labels . push ( {
1502+ color : ex . color || ( theme . dark ?'#fff' :'#333' ) , text : ex . label ,
1503+ linestyle : ex . linestyle || 'solid' ,
1504+ marker : ex . marker || 'none' , ms : ex . markersize || 4 ,
1505+ } ) ;
14051506 if ( labels . length ) {
14061507 ctx . font = '10px monospace' ; let ly = r . y + 6 ;
14071508 for ( const lb of labels ) {
1408- ctx . strokeStyle = lb . color ; ctx . lineWidth = 2 ; ctx . beginPath ( ) ; ctx . moveTo ( r . x + 8 , ly + 5 ) ; ctx . lineTo ( r . x + 24 , ly + 5 ) ; ctx . stroke ( ) ;
1509+ const ldash = _LINESTYLE_DASH [ lb . linestyle || 'solid' ] || [ ] ;
1510+ ctx . strokeStyle = lb . color ; ctx . lineWidth = 2 ;
1511+ ctx . setLineDash ( ldash ) ;
1512+ ctx . beginPath ( ) ; ctx . moveTo ( r . x + 8 , ly + 5 ) ; ctx . lineTo ( r . x + 24 , ly + 5 ) ; ctx . stroke ( ) ;
1513+ ctx . setLineDash ( [ ] ) ;
1514+ if ( lb . marker && lb . marker !== 'none' ) {
1515+ ctx . strokeStyle = lb . color ; ctx . fillStyle = lb . color ; ctx . lineWidth = 1.5 ;
1516+ ctx . beginPath ( ) ; _drawMarkerSymbol ( ctx , lb . marker , r . x + 16 , ly + 5 , Math . min ( lb . ms || 4 , 4 ) ) ; ctx . fill ( ) ; ctx . stroke ( ) ;
1517+ }
14091518 ctx . fillStyle = theme . tickText ; ctx . textAlign = 'left' ; ctx . textBaseline = 'top' ; ctx . fillText ( lb . text , r . x + 28 , ly ) ; ly += 16 ;
14101519 }
14111520 }
0 commit comments