@@ -18,6 +18,7 @@ import Option from 'components/Dropdown/Option.react';
1818import Toggle from 'components/Toggle/Toggle.react' ;
1919import TextInput from 'components/TextInput/TextInput.react' ;
2020import styles from 'components/Modal/Modal.scss' ;
21+ import stringCompare from 'lib/stringCompare' ;
2122import { validateFormula } from 'lib/FormulaEvaluator' ;
2223
2324const CHART_TYPES = [
@@ -154,6 +155,7 @@ export default class GraphDialog extends React.Component {
154155 maxDataPoints : initialConfig . maxDataPoints || 1000 ,
155156 maxDataPointsInput : null ,
156157 showDeleteConfirmation : false ,
158+ objectPathInput : null ,
157159 } ;
158160 }
159161
@@ -240,29 +242,55 @@ export default class GraphDialog extends React.Component {
240242 return Object . entries ( this . props . columns )
241243 . filter ( ( [ key , col ] ) => key !== 'objectId' && ( ! types || types . includes ( col . type ) ) )
242244 . map ( ( [ key ] ) => key )
243- . sort ( ( a , b ) => a . localeCompare ( b ) ) ;
245+ . sort ( stringCompare ) ;
244246 }
245247
246248 getAllColumns ( ) {
247249 return this . getColumnsByType ( ) ;
248250 }
249251
250252 getNumericColumns ( ) {
251- return this . getColumnsByType ( [ 'Number' ] ) ;
253+ return this . getColumnsByType ( [ 'Number' , 'Object' ] ) ;
252254 }
253255
254256 getNumericAndPointerColumns ( ) {
255- return this . getColumnsByType ( [ 'Number' , 'Pointer' ] ) ;
257+ return this . getColumnsByType ( [ 'Number' , 'Pointer' , 'Object' ] ) ;
256258 }
257259
258260 getStringColumns ( ) {
259261 return this . getColumnsByType ( [ 'String' ] ) ;
260262 }
261263
262264 getStringAndPointerColumns ( ) {
263- return this . getColumnsByType ( [ 'String' , 'Pointer' ] ) ;
265+ return this . getColumnsByType ( [ 'String' , 'Pointer' , 'Object' ] ) ;
264266 }
265267
268+ isObjectColumn ( col ) {
269+ return this . props . columns && this . props . columns [ col ] && this . props . columns [ col ] . type === 'Object' ;
270+ }
271+
272+ isObjectDotPath ( fieldValue ) {
273+ if ( ! fieldValue || ! this . props . columns ) {
274+ return false ;
275+ }
276+ const dotIndex = fieldValue . indexOf ( '.' ) ;
277+ if ( dotIndex <= 0 ) {
278+ return false ;
279+ }
280+ const topLevel = fieldValue . substring ( 0 , dotIndex ) ;
281+ return this . props . columns [ topLevel ] && this . props . columns [ topLevel ] . type === 'Object' ;
282+ }
283+
284+ getObjectDotPathOptions ( selectedFields , columnList ) {
285+ return ( selectedFields || [ ] ) . filter ( f => this . isObjectDotPath ( f ) && ! columnList . includes ( f ) ) ;
286+ }
287+
288+ confirmObjectPath = ( ) => {
289+ const { field, path, onConfirm } = this . state . objectPathInput ;
290+ onConfirm ( `${ field } .${ path } ` ) ;
291+ this . setState ( { objectPathInput : null } ) ;
292+ } ;
293+
266294 getNumericAndCalculatedFields ( currentIndex = - 1 ) {
267295 const numericColumns = this . getNumericColumns ( ) ;
268296 // Include series fields and titles
@@ -496,13 +524,25 @@ export default class GraphDialog extends React.Component {
496524 < div >
497525 < MultiSelect
498526 value = { s . fields || [ ] }
499- onChange = { fields => this . updateSeries ( index , 'fields' , fields ) }
527+ onChange = { fields => {
528+ const added = fields . find ( f => ! ( s . fields || [ ] ) . includes ( f ) ) ;
529+ if ( added && this . isObjectColumn ( added ) ) {
530+ this . setState ( { objectPathInput : { field : added , onConfirm : fullPath => this . updateSeries ( index , 'fields' , [ ...( s . fields || [ ] ) , fullPath ] . sort ( stringCompare ) ) } } ) ;
531+ return ;
532+ }
533+ this . updateSeries ( index , 'fields' , fields ) ;
534+ } }
500535 placeHolder = "Select field(s)"
501536 formatSelection = { selection => selection . length === 1 ? selection [ 0 ] : `${ selection . length } fields` }
502537 >
503538 { numericAndPointerColumns . map ( col => (
504539 < MultiSelectOption key = { col } value = { col } >
505- { col }
540+ { this . isObjectColumn ( col ) ? `${ col } [object]` : col }
541+ </ MultiSelectOption >
542+ ) ) }
543+ { this . getObjectDotPathOptions ( s . fields , numericAndPointerColumns ) . map ( f => (
544+ < MultiSelectOption key = { f } value = { f } >
545+ { f }
506546 </ MultiSelectOption >
507547 ) ) }
508548 </ MultiSelect >
@@ -762,16 +802,25 @@ export default class GraphDialog extends React.Component {
762802 < Dropdown
763803 value = { calc . fields && calc . fields [ 0 ] ? calc . fields [ 0 ] : '' }
764804 onChange = { numerator => {
805+ if ( this . isObjectColumn ( numerator ) ) {
806+ this . setState ( { objectPathInput : { field : numerator , onConfirm : fullPath => this . updateCalculatedValue ( index , 'fields' , [ fullPath , calc . fields && calc . fields [ 1 ] ? calc . fields [ 1 ] : '' ] ) } } ) ;
807+ return ;
808+ }
765809 const newFields = [ numerator , calc . fields && calc . fields [ 1 ] ? calc . fields [ 1 ] : '' ] ;
766810 this . updateCalculatedValue ( index , 'fields' , newFields ) ;
767811 } }
768812 placeHolder = "Select field"
769813 >
770814 { numericAndCalculatedFields . map ( col => (
771815 < Option key = { col } value = { col } >
772- { col }
816+ { this . isObjectColumn ( col ) ? ` ${ col } [object]` : col }
773817 </ Option >
774818 ) ) }
819+ { calc . fields && calc . fields [ 0 ] && this . isObjectDotPath ( calc . fields [ 0 ] ) && ! numericAndCalculatedFields . includes ( calc . fields [ 0 ] ) && (
820+ < Option key = { calc . fields [ 0 ] } value = { calc . fields [ 0 ] } >
821+ { calc . fields [ 0 ] }
822+ </ Option >
823+ ) }
775824 </ Dropdown >
776825 </ div >
777826 </ div >
@@ -783,16 +832,25 @@ export default class GraphDialog extends React.Component {
783832 < Dropdown
784833 value = { calc . fields && calc . fields [ 1 ] ? calc . fields [ 1 ] : '' }
785834 onChange = { denominator => {
835+ if ( this . isObjectColumn ( denominator ) ) {
836+ this . setState ( { objectPathInput : { field : denominator , onConfirm : fullPath => this . updateCalculatedValue ( index , 'fields' , [ calc . fields && calc . fields [ 0 ] ? calc . fields [ 0 ] : '' , fullPath ] ) } } ) ;
837+ return ;
838+ }
786839 const newFields = [ calc . fields && calc . fields [ 0 ] ? calc . fields [ 0 ] : '' , denominator ] ;
787840 this . updateCalculatedValue ( index , 'fields' , newFields ) ;
788841 } }
789842 placeHolder = "Select field"
790843 >
791844 { numericAndCalculatedFields . map ( col => (
792845 < Option key = { col } value = { col } >
793- { col }
846+ { this . isObjectColumn ( col ) ? ` ${ col } [object]` : col }
794847 </ Option >
795848 ) ) }
849+ { calc . fields && calc . fields [ 1 ] && this . isObjectDotPath ( calc . fields [ 1 ] ) && ! numericAndCalculatedFields . includes ( calc . fields [ 1 ] ) && (
850+ < Option key = { calc . fields [ 1 ] } value = { calc . fields [ 1 ] } >
851+ { calc . fields [ 1 ] }
852+ </ Option >
853+ ) }
796854 </ Dropdown >
797855 </ div >
798856 </ div >
@@ -805,13 +863,25 @@ export default class GraphDialog extends React.Component {
805863 < div >
806864 < MultiSelect
807865 value = { calc . fields }
808- onChange = { fields => this . updateCalculatedValue ( index , 'fields' , fields ) }
866+ onChange = { fields => {
867+ const added = fields . find ( f => ! calc . fields . includes ( f ) ) ;
868+ if ( added && this . isObjectColumn ( added ) ) {
869+ this . setState ( { objectPathInput : { field : added , onConfirm : fullPath => this . updateCalculatedValue ( index , 'fields' , [ ...calc . fields , fullPath ] . sort ( stringCompare ) ) } } ) ;
870+ return ;
871+ }
872+ this . updateCalculatedValue ( index , 'fields' , fields ) ;
873+ } }
809874 placeHolder = "Select field(s)"
810875 formatSelection = { selection => selection . length === 1 ? selection [ 0 ] : `${ selection . length } fields` }
811876 >
812877 { numericAndCalculatedFields . map ( col => (
813878 < MultiSelectOption key = { col } value = { col } >
814- { col }
879+ { this . isObjectColumn ( col ) ? `${ col } [object]` : col }
880+ </ MultiSelectOption >
881+ ) ) }
882+ { this . getObjectDotPathOptions ( calc . fields , numericAndCalculatedFields ) . map ( f => (
883+ < MultiSelectOption key = { f } value = { f } >
884+ { f }
815885 </ MultiSelectOption >
816886 ) ) }
817887 </ MultiSelect >
@@ -990,29 +1060,50 @@ export default class GraphDialog extends React.Component {
9901060 < Field label = { < Label text = "X-Axis" /> } input = {
9911061 < Dropdown
9921062 value = { this . state . xColumn }
993- onChange = { xColumn => this . setState ( { xColumn } ) }
1063+ onChange = { xColumn => {
1064+ if ( this . isObjectColumn ( xColumn ) ) {
1065+ this . setState ( { objectPathInput : { field : xColumn , onConfirm : fullPath => this . setState ( { xColumn : fullPath } ) } } ) ;
1066+ return ;
1067+ }
1068+ this . setState ( { xColumn } ) ;
1069+ } }
9941070 placeHolder = "Select field"
9951071 >
9961072 { ( chartType === 'scatter' ? numericColumns : allColumns ) . map ( col => (
9971073 < Option key = { col } value = { col } >
998- { col }
1074+ { this . isObjectColumn ( col ) ? ` ${ col } [object]` : col }
9991075 </ Option >
10001076 ) ) }
1077+ { this . state . xColumn && this . isObjectDotPath ( this . state . xColumn ) && ! ( chartType === 'scatter' ? numericColumns : allColumns ) . includes ( this . state . xColumn ) && (
1078+ < Option key = { this . state . xColumn } value = { this . state . xColumn } >
1079+ { this . state . xColumn }
1080+ </ Option >
1081+ ) }
10011082 </ Dropdown >
10021083 } />
1003-
10041084 { chartType === 'scatter' && (
10051085 < Field label = { < Label text = "Y-Axis" /> } input = {
10061086 < Dropdown
10071087 value = { this . state . yColumn }
1008- onChange = { yColumn => this . setState ( { yColumn } ) }
1088+ onChange = { yColumn => {
1089+ if ( this . isObjectColumn ( yColumn ) ) {
1090+ this . setState ( { objectPathInput : { field : yColumn , onConfirm : fullPath => this . setState ( { yColumn : fullPath } ) } } ) ;
1091+ return ;
1092+ }
1093+ this . setState ( { yColumn } ) ;
1094+ } }
10091095 placeHolder = "Select field"
10101096 >
10111097 { numericColumns . map ( col => (
10121098 < Option key = { col } value = { col } >
1013- { col }
1099+ { this . isObjectColumn ( col ) ? ` ${ col } [object]` : col }
10141100 </ Option >
10151101 ) ) }
1102+ { this . state . yColumn && this . isObjectDotPath ( this . state . yColumn ) && ! numericColumns . includes ( this . state . yColumn ) && (
1103+ < Option key = { this . state . yColumn } value = { this . state . yColumn } >
1104+ { this . state . yColumn }
1105+ </ Option >
1106+ ) }
10161107 </ Dropdown >
10171108 } />
10181109 ) }
@@ -1037,13 +1128,25 @@ export default class GraphDialog extends React.Component {
10371128 < Field label = { < Label text = "Group By" description = "Optional" /> } input = {
10381129 < MultiSelect
10391130 value = { this . state . groupByColumn }
1040- onChange = { groupByColumn => this . setState ( { groupByColumn } ) }
1131+ onChange = { groupByColumn => {
1132+ const added = groupByColumn . find ( f => ! this . state . groupByColumn . includes ( f ) ) ;
1133+ if ( added && this . isObjectColumn ( added ) ) {
1134+ this . setState ( { objectPathInput : { field : added , onConfirm : fullPath => this . setState ( { groupByColumn : [ ...this . state . groupByColumn , fullPath ] . sort ( stringCompare ) } ) } } ) ;
1135+ return ;
1136+ }
1137+ this . setState ( { groupByColumn } ) ;
1138+ } }
10411139 placeHolder = "Select field(s)"
10421140 formatSelection = { selection => selection . length === 1 ? selection [ 0 ] : `${ selection . length } fields` }
10431141 >
10441142 { stringAndPointerColumns . map ( col => (
10451143 < MultiSelectOption key = { col } value = { col } >
1046- { col }
1144+ { this . isObjectColumn ( col ) ? `${ col } [object]` : col }
1145+ </ MultiSelectOption >
1146+ ) ) }
1147+ { this . getObjectDotPathOptions ( this . state . groupByColumn , stringAndPointerColumns ) . map ( f => (
1148+ < MultiSelectOption key = { f } value = { f } >
1149+ { f }
10471150 </ MultiSelectOption >
10481151 ) ) }
10491152 </ MultiSelect >
@@ -1207,26 +1310,51 @@ export default class GraphDialog extends React.Component {
12071310 ) ;
12081311
12091312 return (
1210- < Modal
1211- type = { Modal . Types . INFO }
1212- icon = "analytics-outline"
1213- iconSize = { 40 }
1214- title = { isEditing ? 'Edit Graph' : 'Create Graph' }
1215- subtitle = { isEditing ? 'Modify your data visualization settings' : 'Configure your data visualization' }
1216- customFooter = { customFooter }
1217- >
1218- < div style = { {
1219- maxHeight : 'calc(100vh - 260px)' ,
1220- overflowY : 'auto' ,
1221- overflowX : 'hidden' ,
1222- border : 'none'
1223- } } >
1224- { this . renderTitleSection ( ) }
1225- { this . renderChartTypeSection ( ) }
1226- { this . renderColumnSelectionSection ( ) }
1227- { this . renderOptionsSection ( ) }
1228- </ div >
1229- </ Modal >
1313+ < >
1314+ < Modal
1315+ type = { Modal . Types . INFO }
1316+ icon = "analytics-outline"
1317+ iconSize = { 40 }
1318+ title = { isEditing ? 'Edit Graph' : 'Create Graph' }
1319+ subtitle = { isEditing ? 'Modify your data visualization settings' : 'Configure your data visualization' }
1320+ customFooter = { customFooter }
1321+ >
1322+ < div style = { {
1323+ maxHeight : 'calc(100vh - 260px)' ,
1324+ overflowY : 'auto' ,
1325+ overflowX : 'hidden' ,
1326+ border : 'none'
1327+ } } >
1328+ { this . renderTitleSection ( ) }
1329+ { this . renderChartTypeSection ( ) }
1330+ { this . renderColumnSelectionSection ( ) }
1331+ { this . renderOptionsSection ( ) }
1332+ </ div >
1333+ </ Modal >
1334+ { this . state . objectPathInput && (
1335+ < Modal
1336+ type = { Modal . Types . INFO }
1337+ title = "Enter Object Path"
1338+ subtitle = { `Specify the nested key path for "${ this . state . objectPathInput . field } "` }
1339+ confirmText = "Add"
1340+ cancelText = "Cancel"
1341+ onConfirm = { this . confirmObjectPath }
1342+ onCancel = { ( ) => this . setState ( { objectPathInput : null } ) }
1343+ disabled = { ! this . state . objectPathInput . path || ! this . state . objectPathInput . path . trim ( ) }
1344+ >
1345+ < Field label = { < Label text = "Path" /> } input = {
1346+ < div style = { { display : 'flex' , alignItems : 'center' } } >
1347+ < span style = { { paddingLeft : '10px' , color : '#666' , whiteSpace : 'nowrap' } } > { this . state . objectPathInput . field } .</ span >
1348+ < TextInput
1349+ value = { this . state . objectPathInput . path || '' }
1350+ onChange = { path => this . setState ( { objectPathInput : { ...this . state . objectPathInput , path } } ) }
1351+ placeholder = "e.g. views.count"
1352+ />
1353+ </ div >
1354+ } />
1355+ </ Modal >
1356+ ) }
1357+ </ >
12301358 ) ;
12311359 }
12321360}
0 commit comments