@@ -14,7 +14,6 @@ import type {
1414 AreaMark ,
1515 AxisLayout ,
1616 ChartLayout ,
17- LegendLayout ,
1817 LineMark ,
1918 Mark ,
2019 Point ,
@@ -31,6 +30,7 @@ import { clampStaggerDelay } from '@opendata-ai/openchart-engine';
3130import { buildGradientDefs , resolveMarkFill } from './gradient-utils' ;
3231import { renderBrand } from './renderers/brand' ;
3332import { renderChrome } from './renderers/chrome' ;
33+ import { renderLegend } from './renderers/legend' ;
3434import { applyTextStyle , createSVGElement , SVG_NS , setAttrs } from './renderers/svg-dom' ;
3535import { nextSvgId } from './svg-ids' ;
3636
@@ -802,134 +802,6 @@ function renderAnnotation(
802802 parent . appendChild ( g ) ;
803803}
804804
805- // ---------------------------------------------------------------------------
806- // Legend rendering
807- // ---------------------------------------------------------------------------
808-
809- function renderLegend ( parent : SVGElement , legend : LegendLayout ) : void {
810- if ( legend . entries . length === 0 ) return ;
811-
812- const g = createSVGElement ( 'g' ) ;
813- g . setAttribute ( 'class' , 'oc-legend' ) ;
814- g . setAttribute ( 'role' , 'list' ) ;
815- g . setAttribute ( 'aria-label' , 'Chart legend' ) ;
816-
817- const isHorizontal = legend . position === 'top' || legend . position === 'bottom' ;
818- let offsetX = legend . bounds . x ;
819- let offsetY = legend . bounds . y ;
820-
821- for ( let i = 0 ; i < legend . entries . length ; i ++ ) {
822- const entry = legend . entries [ i ] ;
823-
824- // Pre-check: wrap to next line if this entry would overflow bounds
825- if ( isHorizontal && i > 0 ) {
826- const labelWidth = estimateTextWidth (
827- entry . label ,
828- legend . labelStyle . fontSize ,
829- legend . labelStyle . fontWeight ,
830- ) ;
831- const entryWidth = legend . swatchSize + legend . swatchGap + labelWidth + legend . entryGap ;
832- if ( offsetX + entryWidth > legend . bounds . x + legend . bounds . width ) {
833- offsetX = legend . bounds . x ;
834- offsetY += legend . swatchSize + 6 ;
835- }
836- }
837- const entryG = createSVGElement ( 'g' ) ;
838- entryG . setAttribute ( 'class' , 'oc-legend-entry' ) ;
839- entryG . setAttribute ( 'role' , 'listitem' ) ;
840- entryG . setAttribute ( 'data-legend-index' , String ( i ) ) ;
841- entryG . setAttribute ( 'data-legend-label' , entry . label ) ;
842- if ( entry . overflow ) {
843- entryG . setAttribute ( 'data-legend-overflow' , 'true' ) ;
844- entryG . setAttribute ( 'aria-label' , entry . label ) ;
845- entryG . setAttribute ( 'opacity' , '0.5' ) ;
846- } else {
847- entryG . setAttribute (
848- 'aria-label' ,
849- `${ entry . label } : ${ entry . active !== false ? 'visible' : 'hidden' } ` ,
850- ) ;
851- entryG . setAttribute ( 'style' , 'cursor: pointer' ) ;
852-
853- // Apply dimming for inactive entries
854- if ( entry . active === false ) {
855- entryG . setAttribute ( 'opacity' , '0.3' ) ;
856- }
857- }
858-
859- // Swatch
860- if ( entry . shape === 'circle' ) {
861- const circle = createSVGElement ( 'circle' ) ;
862- setAttrs ( circle , {
863- cx : offsetX + legend . swatchSize / 2 ,
864- cy : offsetY + legend . swatchSize / 2 ,
865- r : legend . swatchSize / 2 ,
866- fill : entry . color ,
867- } ) ;
868- entryG . appendChild ( circle ) ;
869- } else if ( entry . shape === 'line' ) {
870- // Line swatch: a short line segment with a dot in the middle
871- const line = createSVGElement ( 'line' ) ;
872- setAttrs ( line , {
873- x1 : offsetX ,
874- y1 : offsetY + legend . swatchSize / 2 ,
875- x2 : offsetX + legend . swatchSize ,
876- y2 : offsetY + legend . swatchSize / 2 ,
877- stroke : entry . color ,
878- 'stroke-width' : 2 ,
879- } ) ;
880- entryG . appendChild ( line ) ;
881- // Small dot at center
882- const dot = createSVGElement ( 'circle' ) ;
883- setAttrs ( dot , {
884- cx : offsetX + legend . swatchSize / 2 ,
885- cy : offsetY + legend . swatchSize / 2 ,
886- r : 2.5 ,
887- fill : entry . color ,
888- } ) ;
889- entryG . appendChild ( dot ) ;
890- } else {
891- const rect = createSVGElement ( 'rect' ) ;
892- setAttrs ( rect , {
893- x : offsetX ,
894- y : offsetY ,
895- width : legend . swatchSize ,
896- height : legend . swatchSize ,
897- fill : entry . color ,
898- rx : 2 ,
899- } ) ;
900- entryG . appendChild ( rect ) ;
901- }
902-
903- // Label
904- const label = createSVGElement ( 'text' ) ;
905- setAttrs ( label , {
906- x : offsetX + legend . swatchSize + legend . swatchGap ,
907- y : offsetY + legend . swatchSize / 2 ,
908- 'dominant-baseline' : 'central' ,
909- } ) ;
910- applyTextStyle ( label , legend . labelStyle ) ;
911- label . textContent = entry . label ;
912- entryG . appendChild ( label ) ;
913-
914- g . appendChild ( entryG ) ;
915-
916- // Advance position for next entry
917- if ( isHorizontal ) {
918- const labelWidth = estimateTextWidth (
919- entry . label ,
920- legend . labelStyle . fontSize ,
921- legend . labelStyle . fontWeight ,
922- ) ;
923- const entryWidth = legend . swatchSize + legend . swatchGap + labelWidth + legend . entryGap ;
924- offsetX += entryWidth ;
925- } else {
926- offsetY += legend . swatchSize + legend . entryGap ;
927- }
928- }
929-
930- parent . appendChild ( g ) ;
931- }
932-
933805// ---------------------------------------------------------------------------
934806// Main render function
935807// ---------------------------------------------------------------------------
0 commit comments