@@ -28,15 +28,17 @@ import type {
2828 TextMarkLayout ,
2929 TickMarkLayout ,
3030} from '@opendata-ai/openchart-core' ;
31- import {
32- BRAND_FONT_SIZE ,
33- BRAND_MIN_WIDTH ,
34- estimateTextWidth ,
35- wrapText ,
36- } from '@opendata-ai/openchart-core' ;
31+ import { estimateTextWidth , wrapText } from '@opendata-ai/openchart-core' ;
3732import { clampStaggerDelay } from '@opendata-ai/openchart-engine' ;
3833import { buildGradientDefs , resolveMarkFill } from './gradient-utils' ;
39- import { applyTextStyle , createSVGElement , SVG_NS , setAttrs , XLINK_NS } from './renderers/svg-dom' ;
34+ import { renderBrand } from './renderers/brand' ;
35+ import {
36+ applyTextStyle ,
37+ computeXAxisExtent ,
38+ createSVGElement ,
39+ SVG_NS ,
40+ setAttrs ,
41+ } from './renderers/svg-dom' ;
4042import { nextSvgId } from './svg-ids' ;
4143
4244/**
@@ -73,31 +75,6 @@ const EASE_VAR_MAP: Record<string, string> = {
7375 snappy : 'var(--oc-ease-snappy)' ,
7476} ;
7577
76- /**
77- * Compute the vertical extent of x-axis labels below the chart area.
78- * Accounts for rotated tick labels which need more vertical space.
79- */
80- function computeXAxisExtent ( layout : ChartLayout ) : number {
81- const xAxis = layout . axes . x ;
82- if ( ! xAxis ) return 0 ;
83-
84- if ( xAxis . tickAngle && Math . abs ( xAxis . tickAngle ) > 10 ) {
85- // Rotated labels: estimate height from the longest tick label.
86- const fontSize = xAxis . tickLabelStyle . fontSize ;
87- const fontWeight = xAxis . tickLabelStyle . fontWeight ;
88- const angleRad = Math . abs ( xAxis . tickAngle ) * ( Math . PI / 180 ) ;
89- let maxLabelWidth = 40 ;
90- for ( const tick of xAxis . ticks ) {
91- const w = estimateTextWidth ( tick . label , fontSize , fontWeight ) ;
92- if ( w > maxLabelWidth ) maxLabelWidth = w ;
93- }
94- const rotatedHeight = Math . min ( maxLabelWidth * Math . sin ( angleRad ) + 6 , 120 ) ;
95- return xAxis . label ? rotatedHeight + 20 : rotatedHeight ;
96- }
97-
98- return xAxis . label ? 48 : 26 ;
99- }
100-
10178// ---------------------------------------------------------------------------
10279// Chrome rendering
10380// ---------------------------------------------------------------------------
@@ -1049,78 +1026,6 @@ function renderLegend(parent: SVGElement, legend: LegendLayout): void {
10491026 parent . appendChild ( g ) ;
10501027}
10511028
1052- // ---------------------------------------------------------------------------
1053- // Brand rendering
1054- // ---------------------------------------------------------------------------
1055-
1056- const BRAND_URL = 'https://tryopendata.ai' ;
1057-
1058- /**
1059- * Render the "OpenData" brand as a footer-row element, right-aligned on the
1060- * same baseline as the first bottom chrome text (source/byline/footer).
1061- * Uses the same font size as chrome source text so it blends in as a subtle
1062- * footer item rather than occupying independent visual space.
1063- */
1064- function renderBrand ( parent : SVGElement , layout : ChartLayout ) : void {
1065- if ( layout . dimensions . width < BRAND_MIN_WIDTH ) return ;
1066-
1067- const { width } = layout . dimensions ;
1068- const padding = layout . theme . spacing . padding ;
1069- const rightEdge = width - padding ;
1070- const fill = layout . theme . colors . axis ;
1071-
1072- // Vertically align with the first bottom chrome element.
1073- const { chrome } = layout ;
1074- const xAxisExtent = computeXAxisExtent ( layout ) ;
1075- const bottomOffset = layout . area . y + layout . area . height + xAxisExtent ;
1076- const firstBottom = chrome . source ?? chrome . byline ?? chrome . footer ;
1077- const chromeY = firstBottom
1078- ? bottomOffset + firstBottom . y
1079- : bottomOffset + layout . theme . spacing . chartToFooter ;
1080-
1081- const a = createSVGElement ( 'a' ) ;
1082- a . setAttribute ( 'href' , BRAND_URL ) ;
1083- a . setAttributeNS ( XLINK_NS , 'xlink:href' , BRAND_URL ) ;
1084- a . setAttribute ( 'target' , '_blank' ) ;
1085- a . setAttribute ( 'rel' , 'noopener' ) ;
1086- a . setAttribute ( 'class' , 'oc-chrome-ref' ) ;
1087-
1088- // "try" in normal weight, "OpenData" in semibold, ".ai" in normal weight,
1089- // rendered as a single right-aligned text element with three tspans.
1090- // Use alphabetic baseline so mixed-size tspans share a common bottom line.
1091- const BRAND_LARGE = 16 ;
1092- const text = createSVGElement ( 'text' ) ;
1093- setAttrs ( text , {
1094- x : rightEdge ,
1095- y : chromeY + BRAND_LARGE ,
1096- 'dominant-baseline' : 'alphabetic' ,
1097- 'font-family' : layout . theme . fonts . family ,
1098- 'font-size' : BRAND_FONT_SIZE ,
1099- 'text-anchor' : 'end' ,
1100- 'fill-opacity' : 0.55 ,
1101- } ) ;
1102- ( text as SVGElement & ElementCSSInlineStyle ) . style . setProperty ( 'fill' , fill ) ;
1103-
1104- const trySpan = createSVGElement ( 'tspan' ) ;
1105- trySpan . setAttribute ( 'font-weight' , '500' ) ;
1106- trySpan . textContent = 'try' ;
1107- text . appendChild ( trySpan ) ;
1108-
1109- const openDataSpan = createSVGElement ( 'tspan' ) ;
1110- openDataSpan . setAttribute ( 'font-weight' , '600' ) ;
1111- openDataSpan . setAttribute ( 'font-size' , String ( BRAND_LARGE ) ) ;
1112- openDataSpan . textContent = 'OpenData' ;
1113- text . appendChild ( openDataSpan ) ;
1114-
1115- const aiSpan = createSVGElement ( 'tspan' ) ;
1116- aiSpan . setAttribute ( 'font-weight' , '500' ) ;
1117- aiSpan . textContent = '.ai' ;
1118- text . appendChild ( aiSpan ) ;
1119-
1120- a . appendChild ( text ) ;
1121- parent . appendChild ( a ) ;
1122- }
1123-
11241029// ---------------------------------------------------------------------------
11251030// Main render function
11261031// ---------------------------------------------------------------------------
0 commit comments