@@ -5,6 +5,14 @@ import { ChartRenderer } from './ChartRenderer';
55import { ComponentRegistry , extractRecords } from '@object-ui/core' ;
66import { AlertCircle } from 'lucide-react' ;
77
8+ /**
9+ * Humanize a snake_case or kebab-case string into Title Case.
10+ * Local implementation to avoid a dependency on @object-ui/fields.
11+ */
12+ export function humanizeLabel ( value : string ) : string {
13+ return value . replace ( / [ _ - ] / g, ' ' ) . replace ( / \b \w / g, c => c . toUpperCase ( ) ) ;
14+ }
15+
816/**
917 * Client-side aggregation for fetched records.
1018 * Groups records by `groupBy` field and applies the aggregation function
@@ -50,6 +58,114 @@ export function aggregateRecords(
5058 } ) ;
5159}
5260
61+ /**
62+ * Resolve groupBy field values to human-readable labels using field metadata.
63+ *
64+ * - **select/picklist** fields: maps value→label via `field.options`.
65+ * - **lookup/master_detail** fields: batch-fetches referenced records
66+ * via `dataSource.find()` and maps id→name.
67+ * - **fallback**: applies `humanizeLabel()` to convert snake_case/kebab-case
68+ * values into Title Case.
69+ *
70+ * The resolved data is a new array with the groupBy key replaced by its label.
71+ * This function is pure data-layer logic — the rendering layer does not need
72+ * to perform any value→label conversion.
73+ */
74+ export async function resolveGroupByLabels (
75+ data : any [ ] ,
76+ groupByField : string ,
77+ objectSchema : any ,
78+ dataSource ?: any ,
79+ ) : Promise < any [ ] > {
80+ if ( ! data . length || ! groupByField ) return data ;
81+
82+ const fieldDef = objectSchema ?. fields ?. [ groupByField ] ;
83+ if ( ! fieldDef ) {
84+ // No metadata available — apply humanizeLabel as fallback
85+ return data . map ( row => ( {
86+ ...row ,
87+ [ groupByField ] : humanizeLabel ( String ( row [ groupByField ] ?? '' ) ) ,
88+ } ) ) ;
89+ }
90+
91+ const fieldType = fieldDef . type ;
92+
93+ // --- select / picklist / dropdown fields ---
94+ if ( fieldType === 'select' || fieldType === 'picklist' || fieldType === 'dropdown' ) {
95+ const options : Array < { value : string ; label : string } | string > = fieldDef . options || [ ] ;
96+ if ( options . length === 0 ) {
97+ return data . map ( row => ( {
98+ ...row ,
99+ [ groupByField ] : humanizeLabel ( String ( row [ groupByField ] ?? '' ) ) ,
100+ } ) ) ;
101+ }
102+
103+ // Build value→label map (options can be {value,label} objects or plain strings)
104+ const labelMap : Record < string , string > = { } ;
105+ for ( const opt of options ) {
106+ if ( typeof opt === 'string' ) {
107+ labelMap [ opt ] = opt ;
108+ } else if ( opt && typeof opt === 'object' ) {
109+ labelMap [ String ( opt . value ) ] = opt . label || String ( opt . value ) ;
110+ }
111+ }
112+
113+ return data . map ( row => {
114+ const rawValue = String ( row [ groupByField ] ?? '' ) ;
115+ return {
116+ ...row ,
117+ [ groupByField ] : labelMap [ rawValue ] || humanizeLabel ( rawValue ) ,
118+ } ;
119+ } ) ;
120+ }
121+
122+ // --- lookup / master_detail fields ---
123+ if ( fieldType === 'lookup' || fieldType === 'master_detail' ) {
124+ const referenceTo = fieldDef . reference_to || fieldDef . reference ;
125+ if ( ! referenceTo || ! dataSource || typeof dataSource . find !== 'function' ) {
126+ // Cannot resolve — return as-is
127+ return data ;
128+ }
129+
130+ // Collect unique IDs to fetch
131+ const ids = [ ...new Set ( data . map ( row => row [ groupByField ] ) . filter ( v => v != null ) ) ] ;
132+ if ( ids . length === 0 ) return data ;
133+
134+ try {
135+ const results = await dataSource . find ( referenceTo , {
136+ $filter : { id : { $in : ids } } ,
137+ $top : ids . length ,
138+ } ) ;
139+ const records = extractRecords ( results ) ;
140+
141+ // Build id→name map using common display fields
142+ const idToName : Record < string , string > = { } ;
143+ for ( const rec of records ) {
144+ const id = String ( rec . id ?? rec . _id ?? '' ) ;
145+ const name = rec . name || rec . label || rec . title || id ;
146+ if ( id ) idToName [ id ] = String ( name ) ;
147+ }
148+
149+ return data . map ( row => {
150+ const rawValue = String ( row [ groupByField ] ?? '' ) ;
151+ return {
152+ ...row ,
153+ [ groupByField ] : idToName [ rawValue ] || rawValue ,
154+ } ;
155+ } ) ;
156+ } catch ( e ) {
157+ console . warn ( '[ObjectChart] Failed to resolve lookup labels:' , e ) ;
158+ return data ;
159+ }
160+ }
161+
162+ // --- fallback for other field types ---
163+ return data . map ( row => ( {
164+ ...row ,
165+ [ groupByField ] : humanizeLabel ( String ( row [ groupByField ] ?? '' ) ) ,
166+ } ) ) ;
167+ }
168+
53169// Re-export extractRecords from @object-ui/core for backward compatibility
54170export { extractRecords } from '@object-ui/core' ;
55171
@@ -98,6 +214,18 @@ export const ObjectChart = (props: any) => {
98214 return ;
99215 }
100216
217+ // Resolve groupBy value→label using field metadata.
218+ // The groupBy field is determined from aggregate config or xAxisKey.
219+ const groupByField = schema . aggregate ?. groupBy || schema . xAxisKey ;
220+ if ( groupByField && typeof ds . getObjectSchema === 'function' ) {
221+ try {
222+ const objectSchema = await ds . getObjectSchema ( schema . objectName ) ;
223+ data = await resolveGroupByLabels ( data , groupByField , objectSchema , ds ) ;
224+ } catch {
225+ // Schema fetch failed — continue with raw values
226+ }
227+ }
228+
101229 if ( mounted . current ) {
102230 setFetchedData ( data ) ;
103231 }
@@ -109,7 +237,7 @@ export const ObjectChart = (props: any) => {
109237 } finally {
110238 if ( mounted . current ) setLoading ( false ) ;
111239 }
112- } , [ schema . objectName , schema . aggregate , schema . filter ] ) ;
240+ } , [ schema . objectName , schema . aggregate , schema . filter , schema . xAxisKey ] ) ;
113241
114242 useEffect ( ( ) => {
115243 const mounted = { current : true } ;
0 commit comments