import { array , extend , isArray , isObject , isString , Mark , Spec } from 'vega' ; import MARK_EXTENTS from '../constants/markExtents' ; import { State , store } from '../store' ; import { GuideType } from '../store/factory/Guide' ; import { applicationIsEnabled , InteractionRecord , MarkApplicationRecord } from '../store/factory/Interaction' ; import { GroupRecord } from '../store/factory/marks/Group' ; import { PipelineRecord } from '../store/factory/Pipeline' ; import { WidgetRecord } from '../store/factory/Widget' ; import { input } from '../util/dataset-utils' ; import duplicate from '../util/duplicate' ; import name from '../util/exportName' ; import exportName from '../util/exportName' ; import { propSg } from '../util/prop-signal' ; import { signalLookup } from '../util/signal-lookup' ; import { addApplicationToScene , addDatasetsToScene , addInputsToScene , addSelectionToScene , addVoronoiMark , addWidgetApplicationToScene , addWidgetSelectionToScene , getScaleInfoForGroup , pushSignalsInScene , getFieldsOfGroup } from './demonstrations' ; import manipulators from './manipulators' ; const json2csv = require ( 'json2csv' ) , imutils = require ( '../util/immutable-utils' ) , getIn = imutils . getIn , getInVis = imutils . getInVis , ORDER = require ( '../constants/sortOrder' ) ; const SPEC_COUNT = { data : { } , scales : { } , _totaled : false } , DATA_COUNT = { marks : { } , scales : { } } , SCALE_COUNT = { marks : { } , guides : { } } ; let counts = duplicate ( SPEC_COUNT ) ; param { boolean } internal returns { Object } export function exporter ( internal = false ) { const state = store . getState ( ) ; const int = internal === true ; counts = duplicate ( SPEC_COUNT ) ; let spec = exporter . scene ( state , int ) ; spec . data = exporter . pipelines ( state , int ) ; spec = exporter . interactions ( state , spec ) ; spec = exporter . widgets ( state , spec ) ; return spec ; } exporter . interactions = function ( state , spec ) { state . getIn ( [ 'vis' , 'present' , 'interactions' ] ) . forEach ( ( interaction ) => { const group = state . getIn ( [ 'vis' , 'present' , 'marks' , String ( interaction . groupId ) ] ) ; const groupName = exportName ( group . name ) ; const scaleInfo = getScaleInfoForGroup ( state , group . _id ) ; const fieldsOfGroup = getFieldsOfGroup ( state , group . _id ) ; const mouseTypes = group . _interactions . map ( interactionId => { const interaction = state . getIn ( [ 'vis' , 'present' , 'interactions' , String ( interactionId ) ] ) ; if ( ! interaction . input ) { return null ; } return interaction . input . mouse ; } ) . filter ( x => x ) ; const exclusive = ( new Set ( mouseTypes ) ) . size !== mouseTypes . length ; spec = addDatasetsToScene ( spec , groupName , interaction . id ) ; spec = addInputsToScene ( spec , groupName , interaction . id , interaction . input , scaleInfo , fieldsOfGroup , exclusive ) ; if ( interaction . selection ) { spec = addSelectionToScene ( spec , groupName , interaction . id , interaction . input , interaction . selection ) ; } if ( interaction . applications . length ) { for ( const application of interaction . applications ) { if ( ! applicationIsEnabled ( application , interaction ) ) continue ; spec = addApplicationToScene ( spec , groupName , interaction . id , interaction . input , application ) ; if ( interaction . input && interaction . input . mouse && interaction . input . mouse === 'mouseover' && interaction . input . nearest ) { const targetMarkName = ( application as any ) . targetMarkName ; if ( targetMarkName ) { const encoding = interaction . selection && interaction . selection . type === 'point' ? interaction . selection . encoding : null ; spec = addVoronoiMark ( spec , groupName , encoding , targetMarkName , interaction . id , application . id ) ; } } } } spec = pushSignalsInScene ( spec , groupName , interaction . signals ) ; } ) ; return spec ; } ; exporter . widgets = function ( state , spec ) { state . getIn ( [ 'vis' , 'present' , 'widgets' ] ) . forEach ( ( widget ) => { const group = state . getIn ( [ 'vis' , 'present' , 'marks' , String ( widget . groupId ) ] ) ; const groupName = exportName ( group . name ) ; if ( widget . selection ) { spec = addWidgetSelectionToScene ( spec , widget , widget . selection ) ; } if ( widget . applications . length ) { for ( const application of widget . applications ) { spec = addWidgetApplicationToScene ( spec , groupName , widget , application ) ; } } } ) ; return spec ; } ; exporter . pipelines = function ( state , internal ) { const pipelines = getInVis ( state , 'pipelines' ) . valueSeq ( ) ; return pipelines . reduce ( function ( spec , pipeline ) { spec . push ( exporter . dataset ( state , internal , pipeline . _source ) ) ; const aggrs = pipeline . _aggregates . toJS ( ) ; for ( const key in aggrs ) { spec . push ( exporter . dataset ( state , internal , aggrs [ key ] ) ) ; } return spec ; } , [ ] ) ; } ; exporter . dataset = function ( state , internal , id ) { const dataset = getInVis ( state , 'datasets.' + id ) . toJS ( ) , spec = clean ( duplicate ( dataset ) , internal ) , values = input ( id ) , format = spec . format && spec . format . type , sort = exporter . sort ( dataset ) ; counts . data [ id ] = counts . data [ id ] || duplicate ( DATA_COUNT ) ; if ( spec . source ) { spec . source = name ( getInVis ( state , 'datasets.' + spec . source + '.name' ) ) ; } else if ( internal ) { spec . values = values ; delete spec . url ; delete spec . format ; } else if ( ! spec . url ) { spec . values = format && format !== 'json' ? json2csv ( { data : values , del : format === 'tsv' ? '\t' : ',' } ) : values ; } const interactions = state . getIn ( [ 'vis' , 'present' , 'interactions' ] ) . valueSeq ( ) . toArray ( ) ; const interactionSignals = [ ] . concat . apply ( [ ] , interactions . filter ( interaction => interaction . signals . length ) . map ( interaction => interaction . signals . map ( signal => signal . signal ) ) ) ; const widgets = state . getIn ( [ 'vis' , 'present' , 'widgets' ] ) . valueSeq ( ) . toArray ( ) ; const widgetSignals = [ ] . concat . apply ( [ ] , widgets . filter ( widget => widget . signals . length ) . map ( widget => widget . signals . map ( signal => signal . signal ) ) ) ; const signals = interactionSignals . concat ( widgetSignals ) ; spec . transform = spec . transform || [ ] ; if ( sort !== undefined ) { spec . transform . push ( sort ) ; } spec . transform . forEach ( s => { if ( s . type === 'lookup' ) { s . from = state . getIn ( [ 'vis' , 'present' , 'datasets' , s . from , 'name' ] ) ; } if ( s . type === 'filter' ) { signals . forEach ( signal => { if ( s . expr . indexOf ( signal ) >= 0 ) { s . expr = `(${ signal } ? ${ s . expr } : true)` ; } } ) ; } } ) ; return spec ; } ; param { object } dataset returns { object } exporter . sort = function ( dataset ) { const sort = dataset . _sort ; if ( ! sort ) { return ; } return { type : 'sort' , by : ( sort . order === ORDER . DESC ? '-' : '' ) + sort . field } ; } ; exporter . scene = function ( state , internal ) : Mark { const sceneId = state . getIn ( [ 'vis' , 'present' , 'scene' , '_id' ] ) ; let spec = exporter . group ( state , internal , sceneId ) ; if ( internal ) { spec = spec [ 0 ] ; } delete spec . from ; delete spec . encode ; return spec ; } ; exporter . mark = function ( state , internal , id ) { const mark = getInVis ( state , 'marks.' + id ) . toJS ( ) ; const spec = clean ( duplicate ( mark ) , internal ) ; const up = mark . encode . update ; const upspec = spec . encode . update ; const facet = mark . _facet ; if ( facet ) { spec . from = { data : facet . name } ; } else if ( spec . from ) { let fromId ; if ( ( fromId = spec . from . data ) ) { spec . from . data = name ( getInVis ( state , 'datasets.' + fromId + '.name' ) ) ; const count = counts . data [ fromId ] || ( counts . data [ fromId ] = duplicate ( DATA_COUNT ) ) ; count . marks [ id ] = true ; } else if ( ( fromId = spec . from . mark ) ) { spec . from . mark = name ( getInVis ( state , 'marks.' + fromId + '.name' ) ) ; } } Object . keys ( upspec ) . forEach ( function ( key ) { let specVal = upspec [ key ] ; const origVal = up [ key ] ; const origScale = origVal . scale ; if ( ! isObject ( specVal ) ) { specVal = upspec [ key ] = { value : specVal } ; } if ( origScale ) { specVal . scale = name ( getInVis ( state , 'scales.' + origScale + '.name' ) ) ; const count = counts . scales [ origScale ] || ( counts . scales [ origScale ] = duplicate ( SCALE_COUNT ) ) ; count . marks [ id ] = true ; } if ( origVal . group ) { specVal . field = { group : origVal . group } ; delete specVal . group ; } } ) ; if ( internal ) { spec . role = `lyra_${ mark . _id }` ; const s = manipulators ( mark , spec ) ; return facet ? pathgroup ( state , s , facet ) : s ; } return facet ? pathgroup ( state , spec , facet ) : spec ; } ; function pathgroup ( state , marks , facet ) { return { name : 'pathgroup' , type : 'group' , from : { facet : { ... facet , data : name ( getInVis ( state , 'datasets.' + facet . data + '.name' ) ) } } , encode : { update : { width : { field : { group : 'width' } } , height : { field : { group : 'height' } } } } , marks : array ( marks ) } ; } exporter . group = function ( state , internal , id ) { const mark = getInVis ( state , `marks.${ id }` ) ; const spec = exporter . mark ( state , internal , id ) ; const group = internal ? spec [ 0 ] : spec ; [ 'scale' , 'mark' , 'axe' , 'legend' ] . forEach ( function ( childType ) { const childTypes = childType + 's' , storePath = childTypes === 'axes' || childTypes === 'legends' ? 'guides' : childTypes ; group [ childTypes ] = mark [ childTypes ] . map ( cid => { const child = getInVis ( state , `${ storePath }.${ cid }` ) ; return ! child ? null : exporter [ child . type ] ? exporter [ child . type ] ( state , internal , cid ) : exporter [ childType ] ? exporter [ childType ] ( state , internal , cid ) : clean ( duplicate ( child . toJS ( ) ) , internal ) ; } ) . reduce ( ( children , child ) => { if ( isArray ( child ) ) { children . push . apply ( children , child ) ; } else if ( child ) { children . push ( child ) ; } return children ; } , [ ] ) ; } ) ; if ( mark . name !== 'Scene' ) { group . signals = group . signals || [ ] ; group . signals . push ( { name : 'width' , value : groupSize ( mark , 'x' ) } , { name : 'height' , value : groupSize ( mark , 'y' ) } ) ; } return spec ; } ; exporter . area = function ( state , internal , id ) { const spec = exporter . mark ( state , internal , id ) ; const area = array ( spec . name === 'pathgroup' ? spec . marks : spec ) [ 0 ] ; const update = area . encode . update ; if ( ! area . from ) { area . from = { data : 'dummy_data' } ; update . x = { signal : `datum.x + ${ propSg ( id , 'area' , 'x' ) }` } ; update . y = { signal : `datum.y + ${ propSg ( id , 'area' , 'y' ) }` } ; } if ( update . orient . value === 'horizontal' ) { delete update . y2 ; } else { delete update . x2 ; } return spec ; } ; exporter . line = function ( state , internal , id ) { const spec = exporter . mark ( state , internal , id ) ; const line = array ( spec . name === 'pathgroup' ? spec . marks : spec ) [ 0 ] ; const update = line . encode . update ; if ( ! line . from ) { line . from = { data : 'dummy_data' } ; update . x = { signal : `datum.x + ${ propSg ( id , 'line' , 'x' ) }` } ; update . y = { signal : `datum.y + ${ propSg ( id , 'line' , 'y' ) }` } ; } return spec ; } ; exporter . scale = function ( state , internal , id ) { const scale = getInVis ( state , 'scales.' + id ) . toJS ( ) , spec = clean ( duplicate ( scale ) , internal ) ; counts . scales [ id ] = counts . scales [ id ] || duplicate ( SCALE_COUNT ) ; if ( ! scale . domain && scale . _domain && scale . _domain . length ) { spec . domain = scale . _manual && scale . _manualDomain . length ? scale . _manualDomain : dataRef ( state , scale , scale . _domain ) ; } if ( ! scale . range && scale . _range && scale . _range . length ) { spec . range = dataRef ( state , scale , scale . _range ) ; } const sortOrder = isObject ( spec . domain ) && spec . domain . _sortOrder ; if ( sortOrder ) { spec . reverse = sortOrder === ORDER . DESC ? ! spec . reverse : ! ! spec . reserve ; } return spec ; } ; exporter . axe = exporter . legend = function ( state , internal , id ) { const guide = getInVis ( state , 'guides.' + id ) . toJS ( ) , spec = clean ( duplicate ( guide ) , internal ) , gtype = guide . _gtype , type = guide . _type ; if ( gtype === GuideType . Axis ) { counts . scales [ spec . scale ] . guides [ id ] = true ; spec . scale = name ( getInVis ( state , 'scales.' + spec . scale + '.name' ) ) ; } else if ( gtype === GuideType . Legend ) { counts . scales [ spec [ type ] ] . guides [ id ] = true ; spec [ type ] = name ( getInVis ( state , 'scales.' + spec [ type ] + '.name' ) ) ; } Object . keys ( spec . encode ) . forEach ( function ( prop ) { const def = spec . encode [ prop ] ; Object . keys ( def ) . forEach ( function ( key ) { if ( ! isObject ( def [ key ] ) ) { def [ key ] = { value : def [ key ] } ; } } ) ; } ) ; return spec ; } ; function groupSize ( group , dimension ) { const update = group . encode . update , extents = MARK_EXTENTS [ dimension ] ; if ( ! update [ extents . SPAN . name ] . _disabled ) { return signalLookup ( update [ extents . SPAN . name ] . signal ) ; } } param { Object } spec param { Boolean } internal returns { Object } function clean ( spec , internal ) { let cleanKey ; for ( const [ key , prop ] of Object . entries ( spec ) ) { cleanKey = key . startsWith ( '_' ) ; cleanKey = cleanKey || prop === null || prop === undefined || ( prop as any ) . _disabled ; if ( cleanKey ) { delete spec [ key ] ; } else if ( key === 'name' && isString ( prop ) ) { spec [ key ] = name ( prop ) ; } else if ( isObject ( prop ) ) { if ( ( prop as any ) . signal && ! internal ) { if ( ( prop as any ) . signal . startsWith ( 'lyra' ) ) { spec [ key ] = signalLookup ( ( prop as any ) . signal ) ; } } else { spec [ key ] = clean ( spec [ key ] , internal ) ; } } } return spec ; } export function getCounts ( recount ) { let key , entry ; if ( recount ) { exporter ( ) ; } else if ( counts . _totaled ) { return counts ; } for ( key in counts . data ) { entry = counts . data [ key ] ; entry . total = Object . keys ( entry . marks ) . length + Object . keys ( entry . scales ) . length ; } for ( key in counts . scales ) { entry = counts . scales [ key ] ; entry . markTotal = Object . keys ( entry . marks ) . length ; entry . guideTotal = Object . keys ( entry . guides ) . length ; entry . total = entry . markTotal + entry . guideTotal ; } return ( counts . _totaled = true ) , counts ; } param { object } state param { Object } scale param { Array } ref returns { Object } function dataRef ( state , scale , ref ) { let sets = { } , data , did , field , i , len , keys ; if ( ref . length === 1 ) { ref = ref [ 0 ] ; data = getInVis ( state , 'datasets.' + ref . data ) ; return sortDataRef ( data , scale , ref . field ) ; } for ( i = 0 , len = ref . length ; i < len ; ++ i ) { data = getInVis ( state , 'datasets.' + ref [ i ] . data ) ; field = ref [ i ] . field ; sets [ ( did = data . get ( '_id' ) ) ] = sets [ did ] || ( sets [ did ] = [ ] ) ; sets [ did ] . push ( field ) ; } keys = Object . keys ( sets ) ; if ( keys . length === 1 ) { return sortDataRef ( data , scale , sets [ did ] ) ; } ref = { fields : [ ] } ; for ( i = 0 , len = keys . length ; i < len ; ++ i ) { data = getInVis ( state , 'datasets.' + keys [ i ] ) ; ref . fields . push ( sortDataRef ( data , scale , sets [ keys [ i ] ] ) ) ; } return ref ; } function sortDataRef ( data , scale , field ) { let ref ; if ( Array . isArray ( field ) ) { ref = { data : name ( data . get ( 'name' ) ) , fields : field } ; } else { ref = { data : name ( data . get ( 'name' ) ) , field : field } ; } if ( scale . type === 'ordinal' && data . get ( '_sort' ) ) { const sortField = getIn ( data , '_sort.field' ) ; return extend ( { } , ref , { sort : sortField === ref . field ? true : { field : sortField , op : 'min' } , _sortOrder : getIn ( data , '_sort.order' ) } ) ; } const dsId = data . get ( '_id' ) , count = counts . data [ dsId ] || ( counts . data [ dsId ] = duplicate ( DATA_COUNT ) ) ; count . scales [ scale . _id ] = true ; return ref ; } module . exports . exportName = name ; module . exports . counts = getCounts ;