@@ -98,7 +98,7 @@ export function runCodeOnInputListsInVM(
9898 let target = undefined ;
9999 try {
100100 // copy args to ensure correctness of mapping
101- target = func ( ...JSON . parse ( JSON . stringify ( args ) ) )
101+ target = func ( ...structuredClone ( args ) )
102102 } catch ( err ) {
103103 console . warn ( `execution err ${ err } ` )
104104 }
@@ -179,7 +179,7 @@ export function baseTableToExtTable(table: any[], derivedFields: FieldItem[], al
179179 let args = inputTuples ;
180180 if ( func . length == baseCols . length * 2 + 1 ) {
181181 // avoid side effect, use the copy of the column when calling the function
182- args = [ ...inputTuples , rowIdx , ...JSON . parse ( JSON . stringify ( baseCols ) ) ]
182+ args = [ ...inputTuples , rowIdx , ...structuredClone ( baseCols ) ]
183183 }
184184
185185 target = func ( ...args ) ;
@@ -242,7 +242,13 @@ export function baseTableToExtTable(table: any[], derivedFields: FieldItem[], al
242242}
243243
244244
245- export const instantiateVegaTemplate = ( chartType : string , encodingMap : { [ key in Channel ] : EncodingItem ; } , allFields : FieldItem [ ] , workingTable : any [ ] ) => {
245+ export const assembleVegaChart = (
246+ chartType : string ,
247+ encodingMap : { [ key in Channel ] : EncodingItem ; } ,
248+ conceptShelfItems : FieldItem [ ] ,
249+ workingTable : any [ ] ,
250+ maxNominalValues : number = 68
251+ ) => {
246252
247253 if ( chartType == "Table" ) {
248254 return [ "Table" , undefined ] ;
@@ -251,8 +257,7 @@ export const instantiateVegaTemplate = (chartType: string, encodingMap: { [key i
251257 let chartTemplate = getChartTemplate ( chartType ) as ChartTemplate ;
252258 //console.log(chartTemplate);
253259
254- let vgObj = JSON . parse ( JSON . stringify ( chartTemplate . template ) ) ;
255- const baseTableSchemaObj : any = { } ;
260+ let vgObj = structuredClone ( chartTemplate . template ) ;
256261
257262 for ( const [ channel , encoding ] of Object . entries ( encodingMap ) ) {
258263
@@ -262,30 +267,8 @@ export const instantiateVegaTemplate = (chartType: string, encodingMap: { [key i
262267 encodingObj [ "scale" ] = { "type" : "sqrt" , "zero" : true } ;
263268 }
264269
265- const field = encoding . fieldID ? _ . find ( allFields , ( f ) => f . id === encoding . fieldID ) : undefined ;
270+ const field = encoding . fieldID ? _ . find ( conceptShelfItems , ( f ) => f . id === encoding . fieldID ) : undefined ;
266271 if ( field ) {
267- //console.log(field)
268- // the synthesizer only need to see base table schema
269- let baseFields = ( field . source == "derived" ?
270- ( field . transform as ConceptTransformation ) . parentIDs . map ( ( parentID ) => allFields . find ( ( f ) => f . id == parentID ) as FieldItem )
271- : [ field ] ) ;
272-
273- for ( let baseField of baseFields ) {
274- if ( Object . keys ( baseTableSchemaObj ) . includes ( baseField . name ) ) {
275- continue ;
276- }
277- baseTableSchemaObj [ baseField . name ] = {
278- channel,
279- dtype : getDType ( baseField . type , workingTable . map ( r => r [ baseField . name ] ) ) ,
280- name : baseField . name ,
281- original : baseField . source == "original" ,
282- // domain: {
283- // values: [...new Set(baseField.domain.values)],
284- // is_complete: baseField.domain.isComplete
285- // },
286- } ;
287- }
288-
289272 // create the encoding
290273 encodingObj [ "field" ] = field . name ;
291274 encodingObj [ "type" ] = getDType ( field . type , workingTable . map ( r => r [ field . name ] ) ) ;
@@ -428,16 +411,8 @@ export const instantiateVegaTemplate = (chartType: string, encodingMap: { [key i
428411 vgObj = chartTemplate . postProcessor ( vgObj , workingTable ) ;
429412 }
430413
431- // console.log(JSON.stringify(vgObj))
432-
433- return [ vgObj , baseTableSchemaObj ] ;
434- }
435-
436- export const assembleChart = ( chart : Chart , conceptShelfItems : FieldItem [ ] , dataValues : any [ ] ) => {
437-
438- let vgSpec : any = instantiateVegaTemplate ( chart . chartType , chart . encodingMap , conceptShelfItems , dataValues ) [ 0 ] ;
439-
440- let values = JSON . parse ( JSON . stringify ( dataValues ) ) ;
414+ // this is the data that will be assembled into the vega chart
415+ let values = structuredClone ( workingTable ) ;
441416 values = values . map ( ( r : any ) => {
442417 let keys = Object . keys ( r ) ;
443418 let temporalKeys = keys . filter ( ( k : string ) => conceptShelfItems . some ( concept => concept . name == k && ( concept . type == "date" || concept . semanticType == "Year" ) ) ) ;
@@ -446,9 +421,82 @@ export const assembleChart = (chart: Chart, conceptShelfItems: FieldItem[], data
446421 }
447422 return r ;
448423 } )
449- return { ...vgSpec , data : { values : values } }
424+
425+ // Handle nominal axes with many entries
426+ for ( const channel of [ 'x' , 'y' , 'column' , 'row' , 'xOffset' ] ) {
427+ const encoding = vgObj . encoding ?. [ channel ] ;
428+ if ( encoding ?. type === 'nominal' ) {
429+ const fieldName = encoding . field ;
430+ const uniqueValues = [ ...new Set ( values . map ( ( r : any ) => r [ fieldName ] ) ) ] ;
431+
432+ let valuesToKeep : any [ ] ;
433+ if ( uniqueValues . length > maxNominalValues ) {
434+
435+ if ( channel == 'x' || channel == 'y' ) {
436+ const oppositeChannel = channel === 'x' ? 'y' : 'x' ;
437+ const oppositeEncoding = vgObj . encoding ?. [ oppositeChannel ] ;
438+
439+ if ( oppositeEncoding ?. type === 'quantitative' ) {
440+ // Sort by the quantitative field and take top maxNominalValues
441+ const quantField = oppositeEncoding . field ;
442+ valuesToKeep = uniqueValues
443+ . map ( val => ( {
444+ value : val ,
445+ sum : workingTable
446+ . filter ( r => r [ fieldName ] === val )
447+ . reduce ( ( sum , r ) => sum + ( r [ quantField ] || 0 ) , 0 )
448+ } ) )
449+ . sort ( ( a , b ) => b . sum - a . sum )
450+ . slice ( 0 , maxNominalValues )
451+ . map ( v => v . value ) ;
452+ } else {
453+ // If no quantitative axis, just take first maxNominalValues
454+ valuesToKeep = uniqueValues . slice ( 0 , maxNominalValues ) ;
455+ }
456+ } else if ( channel == 'row' ) {
457+ valuesToKeep = uniqueValues . slice ( 0 , 20 ) ;
458+ } else {
459+ valuesToKeep = uniqueValues . slice ( 0 , maxNominalValues ) ;
460+ }
461+
462+ // Filter the working table
463+ const omittedCount = uniqueValues . length - maxNominalValues ;
464+ const placeholder = `...${ omittedCount } items omitted` ;
465+ values = values . filter ( ( row : any ) => valuesToKeep . includes ( row [ fieldName ] ) ) ;
466+
467+ // Add text formatting configuration
468+ if ( ! encoding . axis ) {
469+ encoding . axis = { } ;
470+ }
471+ encoding . axis . labelColor = {
472+ condition : {
473+ test : `datum.label == '${ placeholder } '` ,
474+ value : "#999999"
475+ } ,
476+ value : "#000000" // default color for other labels
477+ } ;
478+ encoding . axis . labelFont = {
479+ condition : {
480+ test : `datum.label == '${ placeholder } '` ,
481+ value : "italic"
482+ } ,
483+ value : "normal" // default font style for other labels
484+ } ;
485+
486+ // Add placeholder to domain
487+ if ( ! encoding . scale ) {
488+ encoding . scale = { } ;
489+ }
490+ encoding . scale . domain = [ ...valuesToKeep , placeholder ]
491+ }
492+ }
493+ }
494+
495+ return { ...vgObj , data : { values : values } }
450496}
451497
498+
499+
452500export const adaptChart = ( chart : Chart , targetTemplate : ChartTemplate ) => {
453501
454502 let discardedChannels = Object . entries ( chart . encodingMap ) . filter ( ( [ ch , enc ] ) => {
0 commit comments