import { Component } from 'vue-property-decorator' ; import _ from 'lodash' ; import template from './visual-editor.html' ; import { injectNodeTemplate } from '@/components/node' ; import SubsetNode from '@/components/subset-node/subset-node' ; import { VisualProperties , VisualPropertyType , isNumericalVisual } from '@/data/visuals' ; import { SubsetPackage } from '@/data/package' ; import FormSelect from '@/components/form-select/form-select' ; import FormInput from '@/components/form-input/form-input' ; import ColorInput from '@/components/color-input/color-input' ; import ColumnSelect from '@/components/column-select/column-select' ; import ColorScaleSelect from '@/components/color-scale-select/color-scale-select' ; import ColorScaleDisplay from '@/components/color-scale-display/color-scale-display' ; import { getScale , GetScaleOptions , OrdinalScaleType } from '@/components/visualization' ; import { getColorScale } from '@/common/color-scale' ; import * as history from './history' ; import { NODE_CONTENT_PADDING_PX } from '@/common/constants' ; import { ValueType } from '@/data/parser' ; export enum VisualEditorMode { ASSIGNMENT = 'assignment' , ENCODING = 'encoding' } interface NullableVisualProperties { color : string | null ; border : string | null ; size : number | null ; width : number | null ; opacity : number | null ; [ prop ] : number | string | null ; } interface VisualEditorSave { mode : VisualEditorMode ; visuals : NullableVisualProperties ; encoding : EncodingParams ; } interface EncodingParams { column : number | null ; type : VisualPropertyType ; colorScaleId : string ; numericalScale : { min : number ; max : number ; } ; } @ Component ( { template : injectNodeTemplate ( template ) , components : { FormSelect , ColumnSelect , ColorScaleSelect , FormInput , ColorInput , ColorScaleDisplay } } ) export default class VisualEditor extends SubsetNode { protected NODE_TYPE = 'visual-editor' ; protected RESIZABLE = true ; protected DEFAULT_WIDTH = 60 ; protected DEFAULT_HEIGHT = 60 ; private mode : VisualEditorMode = VisualEditorMode . ASSIGNMENT ; private visuals : NullableVisualProperties = { color : null , border : null , size : null , width : null , opacity : null } ; private encoding : EncodingParams = { column : null , type : VisualPropertyType . COLOR , colorScaleId : 'red-green' , numericalScale : { min : 1 , max : 1 } } ; get modeOptions ( ) : SelectOption [ ] { return [ { label : 'Assignment' , value : VisualEditorMode . ASSIGNMENT } , { label : 'Encoding' , value : VisualEditorMode . ENCODING } , ] ; } get encodingTypeOptions ( ) : SelectOption [ ] { return [ { label : 'Color' , value : VisualPropertyType . COLOR } , { label : 'Border' , value : VisualPropertyType . BORDER } , { label : 'Size' , value : VisualPropertyType . SIZE } , { label : 'Width' , value : VisualPropertyType . WIDTH } , { label : 'Opacity' , value : VisualPropertyType . OPACITY } , ] ; } get isNumericalEncoding ( ) : boolean { return this . encoding . column !== null && isNumericalVisual ( this . encoding . type ) ; } get displayStyle ( ) { let width = this . visuals . width ? Math . min ( this . visuals . width , this . width / 2 ) : 0 ; if ( this . visuals . border && width === 0 ) { width = 1 ; } const baseSize = Math . min ( this . width , this . height ) - NODE_CONTENT_PADDING_PX * 2 ; const size = this . visuals . size ? Math . min ( baseSize , this . visuals . size ) : baseSize * .75 ; return { 'background-color' : this . visuals . color || '' , 'border-color' : this . visuals . border || '' , 'border-width' : width + 'px' , 'left' : ( this . width - size ) / 2 + 'px' , 'top' : ( this . height - size ) / 2 + 'px' , 'width' : size + 'px' , 'height' : size + 'px' , 'opacity' : this . visuals . opacity || 1 } ; } public setMode ( mode ) { this . mode = mode ; this . updateAndPropagate ( ) ; } public setVisualsColor ( color ) { this . visuals . color = color ; this . updateAndPropagate ( ) ; } public setVisualsBorder ( border ) { this . visuals . border = border ; this . updateAndPropagate ( ) ; } public setVisualsSize ( size ) { this . visuals . size = size ; this . updateAndPropagate ( ) ; } public setVisualsWidth ( width ) { this . visuals . width = width ; this . updateAndPropagate ( ) ; } public setVisualsOpacity ( opacity ) { if ( opacity === null ) { delete this . visuals . opacity ; } else { this . visuals . opacity = opacity ; } this . updateAndPropagate ( ) ; } public setEncodingColumn ( column ) { this . encoding . column = column ; this . updateAndPropagate ( ) ; } public setEncodingType ( type ) { this . encoding . type = type ; this . updateAndPropagate ( ) ; } public setEncodingColorScale ( colorScaleId ) { this . encoding . colorScaleId = colorScaleId ; this . updateAndPropagate ( ) ; } public setEncodingScaleMin ( value ) { this . encoding . numericalScale . min = value || 0 ; this . updateAndPropagate ( ) ; } public setEncodingScaleMax ( value ) { this . encoding . numericalScale . max = value || 0 ; this . updateAndPropagate ( ) ; } public getMode ( ) { return this . mode ; } public getVisualsAssignment ( ) { return _ . clone ( this . visuals ) ; } public getVisualsEncoding ( ) { return _ . clone ( this . encoding ) ; } protected onDatasetChange ( ) { this . encoding . column = this . updateColumnOnDatasetChange ( this . encoding . column ) ; } protected update ( ) { if ( ! this . checkDataset ( ) ) { return ; } this . dye ( ) ; } protected created ( ) { this . serializationChain . push ( ( ) : VisualEditorSave => ( { mode : this . mode , visuals : this . visuals , encoding : this . encoding } ) ) ; } protected updateAndPropagate ( ) { this . update ( ) ; this . propagate ( ) ; } private dye ( ) { let pkg ; if ( this . mode === VisualEditorMode . ASSIGNMENT ) { pkg = this . dyeAssignment ( ) ; } else { pkg = this . dyeEncoding ( ) ; } this . updateOutput ( pkg ) ; } private dyeAssignment ( ) { const pkg = this . inputPortMap . in . getSubsetPackage ( ) . clone ( ) ; const visuals = this . removeNullVisuals ( this . visuals ) ; _ . each ( visuals , ( value , prop ) => { if ( visuals [ prop ] === undefined ) { delete visuals [ prop ] ; } } ) ; pkg . getItems ( ) . forEach ( item => { _ . extend ( item . visuals , visuals ) ; } ) ; return pkg ; } private dyeEncoding ( ) { const pkg = this . inputPortMap . in . getSubsetPackage ( ) . clone ( ) ; if ( this . encoding . column === null || ( ! this . isNumericalEncoding && ! this . encoding . colorScaleId ) ) { return pkg ; } const dataset = this . getDataset ( ) ; const itemIndices = pkg . getItemIndices ( ) ; if ( isNumericalVisual ( this . encoding . type ) ) { const scale = getScale ( dataset . getColumnType ( this . encoding . column ) , dataset . getDomain ( this . encoding . column , itemIndices ) , [ this . encoding . numericalScale . min , this . encoding . numericalScale . max ] ) ; pkg . getItems ( ) . forEach ( item => { const value = dataset . getCell ( item , this . encoding . column as number ) ; _ . extend ( item . visuals , { [ this . encoding . type ] : scale ( value ) } ) ; } ) ; } else { const colorScale = getColorScale ( this . encoding . colorScaleId ) ; let domain ; let columnType ; let range ; const options = { domainMargin : 0 } ; if ( this . encoding . colorScaleId === 'categorical' ) { columnType = ValueType . STRING ; domain = dataset . getDomainValues ( this . encoding . column , itemIndices , true ) ; range = _ . range ( domain . length ) ; options . ordinal = { type : OrdinalScaleType . ORDINAL } ; } else { columnType = dataset . getColumnType ( this . encoding . column ) ; domain = dataset . getDomain ( this . encoding . column , itemIndices ) ; range = [ 0 , 1 ] ; } const scale = getScale ( columnType , domain , range , options ) ; pkg . getItems ( ) . forEach ( item => { const value = dataset . getCell ( item , this . encoding . column as number ) ; const colorScaleValue = scale ( value ) ; _ . extend ( item . visuals , { [ this . encoding . type ] : colorScale ( colorScaleValue ) } ) ; } ) ; } return pkg ; } private removeNullVisuals ( visuals ) { const cleanVisuals = { } ; _ . each ( visuals , ( value , prop ) => { if ( value !== null ) { cleanVisuals [ prop ] = value ; } } ) ; return cleanVisuals ; } private onSelectMode ( mode , prevMode ) { this . commitHistory ( history . selectModeEvent ( this , mode , prevMode ) ) ; this . setMode ( mode ) ; } private onInputVisualsColor ( color , prevColor ) { this . commitHistory ( history . inputVisualsColorEvent ( this , color , prevColor ) ) ; this . setVisualsColor ( color ) ; } private onInputVisualsBorder ( border , prevBorder ) { this . commitHistory ( history . inputVisualsBorderEvent ( this , border , prevBorder ) ) ; this . setVisualsBorder ( border ) ; } private onInputVisualsSize ( size , prevSize ) { this . commitHistory ( history . inputVisualsSizeEvent ( this , size , prevSize ) ) ; this . setVisualsSize ( size ) ; } private onInputVisualsWidth ( width , prevWidth ) { this . commitHistory ( history . inputVisualsWidthEvent ( this , width , prevWidth ) ) ; this . setVisualsWidth ( width ) ; } private onInputVisualsOpacity ( opacity , prevOpacity ) { this . commitHistory ( history . inputVisualsOpacityEvent ( this , opacity , prevOpacity ) ) ; this . setVisualsOpacity ( opacity ) ; } private onSelectEncodingColumn ( column , prevColumn ) { this . commitHistory ( history . selectEncodingColumnEvent ( this , column , prevColumn ) ) ; this . setEncodingColumn ( column ) ; } private onSelectEncodingType ( type , prevType ) { this . commitHistory ( history . selectEncodingTypeEvent ( this , type , prevType ) ) ; this . setEncodingType ( type ) ; } private onSelectEncodingColorScale ( colorScaleId , prevColorScaleId ) { this . commitHistory ( history . selectEncodingColorScaleEvent ( this , colorScaleId , prevColorScaleId ) ) ; this . setEncodingColorScale ( colorScaleId ) ; } private onInputEncodingScaleMin ( value , prevValue ) { this . commitHistory ( history . inputEncodingScaleMinEvent ( this , value , prevValue ) ) ; this . setEncodingScaleMin ( value ) ; } private onInputEncodingScaleMax ( value , prevValue ) { this . commitHistory ( history . inputEncodingScaleMaxEvent ( this , value , prevValue ) ) ; this . setEncodingScaleMax ( value ) ; } }