@@ -4,23 +4,24 @@ var d3 = require('d3');
44var LeafValue = require ( 'mongodb-language-model' ) . LeafValue ;
55var ListOperator = require ( 'mongodb-language-model' ) . ListOperator ;
66var Range = require ( 'mongodb-language-model' ) . helpers . Range ;
7+ var debug = require ( 'debug' ) ( 'scout:minicharts:querybuilder' ) ;
78
89var MODIFIERKEY = 'shiftKey' ;
910
1011module . exports = {
1112 /**
12- * Extract a value that can be compared
13+ * Extract a value that can be ordered (e.g. number, date, ...)
1314 * @param {Object } d event data object triggered by the minichart
1415 * @return {Any } value to be returned that can be used for comparisons < and >
1516 */
16- _getComparableValue : function ( d ) {
17+ _getOrderedValue : function ( d ) {
1718 return d . value . _bsontype === 'ObjectID' ? d . value . getTimestamp ( ) : d . value ;
1819 } ,
1920
2021 /**
2122 * build new distinct ($in) query based on current selection and store as this.refineValue.
2223 */
23- buildDistinctQuery : function ( ) {
24+ buildQuery_distinct : function ( ) {
2425 // build new refineValue
2526 if ( this . selectedValues . length === 0 ) {
2627 // no value
@@ -42,27 +43,69 @@ module.exports = {
4243 }
4344 } ,
4445
46+ /**
47+ * build new range ($gte, $lt(e)) query based on current selection and store as this.refineValue.
48+ */
49+ buildQuery_range : function ( ) {
50+ var firstSelected = this . selectedValues [ 0 ] ;
51+ var getOrderedValue = this . _getOrderedValue . bind ( this ) ;
52+
53+ if ( ! firstSelected ) {
54+ this . unset ( 'refineValue' ) ;
55+ return ;
56+ }
57+ var first = _ . min ( this . selectedValues , function ( el ) {
58+ return getOrderedValue ( el ) ;
59+ } ) ;
60+ var last = _ . max ( this . selectedValues , function ( el ) {
61+ return getOrderedValue ( el ) ;
62+ } ) ;
63+ var upperInclusive = last . dx === 0 ;
64+
65+ var lower = first . value ;
66+ var upper = last . value ;
67+ if ( this . model . getType ( ) === 'Number' ) {
68+ upper += last . dx ;
69+ }
70+ if ( lower === upper ) {
71+ this . refineValue = new LeafValue ( {
72+ content : lower
73+ } ) ;
74+ } else {
75+ this . refineValue = new Range ( lower , upper , upperInclusive ) ;
76+ }
77+ } ,
78+
4579 /**
4680 * Handler for query builder events that result in distinct selection, e.g. string and unique
4781 * type. Single click selects individual element, shift-click adds to selection.
48- * @param {Object } data the contains information about the event, @see handleQueryBuilderEvent
82+ *
83+ * For distinct-type minicharts, this.selectedValues contains an entry for each selected value.
84+ *
85+ * @param {Object } data contains information about the event, @see handleQueryBuilderEvent
4986 */
50- handleDistinctEvent : function ( data ) {
87+ handleEvent_distinct : function ( data ) {
5188 // update selectedValues
5289 if ( ! data . evt [ MODIFIERKEY ] ) {
5390 if ( this . selectedValues . length === 1 && this . selectedValues [ 0 ] . value === data . d . value ) {
91+ // case where 1 element is selected and it is clicked again (need to unselect)
5492 this . selectedValues = [ ] ;
5593 } else {
94+ // case where multiple or no elements are selected (need to select that one item)
5695 this . selectedValues = [ data . d ] ;
5796 }
5897 } else if ( _ . contains ( _ . pluck ( this . selectedValues , 'value' ) , data . d . value ) ) {
98+ // case where selected element is shift-clicked (need to remove from selection)
5999 _ . remove ( this . selectedValues , function ( d ) {
60100 return d . value === data . d . value ;
61101 } ) ;
62102 } else {
103+ // case where unselected element is shift-clicked (need to add to selection)
63104 this . selectedValues . push ( data . d ) ;
64105 }
65106
107+ debug ( 'selectedValues' , this . selectedValues ) ;
108+
66109 // visual updates
67110 _ . each ( data . all , function ( el ) {
68111 var elData = data . source === 'unique' ? el . innerText : d3 . select ( el ) . data ( ) [ 0 ] . value ;
@@ -80,51 +123,26 @@ module.exports = {
80123 } . bind ( this ) ) ;
81124 } ,
82125
83- /**
84- * build new range ($gte, $lt(e)) query based on current selection and store as this.refineValue.
85- */
86- buildRangeQuery : function ( ) {
87- var firstSelected = this . selectedValues [ 0 ] ;
88- var getComparableValue = this . _getComparableValue . bind ( this ) ;
89-
90- if ( ! firstSelected ) {
91- this . unset ( 'refineValue' ) ;
92- return ;
93- }
94- var first = _ . min ( this . selectedValues , function ( el ) {
95- return getComparableValue ( el ) ;
96- } ) ;
97- var last = _ . max ( this . selectedValues , function ( el ) {
98- return getComparableValue ( el ) ;
99- } ) ;
100- var upperInclusive = last . dx === 0 ;
101-
102- var lower = first . value ;
103- var upper = last . value ;
104- if ( this . model . getType ( ) === 'Number' ) {
105- upper += last . dx ;
106- }
107- if ( lower === upper ) {
108- this . refineValue = new LeafValue ( {
109- content : lower
110- } ) ;
111- } else {
112- this . refineValue = new Range ( lower , upper , upperInclusive ) ;
113- }
114- } ,
115-
116126 /**
117127 * Handler for query builder events that result in range selection, e.g. number type.
118128 * single click selects individual element, shift-click extends to range (the single click is
119129 * interpreted as one end of the range, shift-click as the other).
130+ *
131+ * For range-type minicharts, this.selectedValues contains two values, the lower and upper bound.
132+ * The 0th value is the one selected via regular click, the 1st value is the shift-clicked one.
133+ * If only a single value is selected ($eq), is stored at the 0th index.
134+ *
120135 * @param {Object } data the contains information about the event, @see handleQueryBuilderEvent
121136 */
122- handleRangeEvent : function ( data ) {
137+ handleEvent_range : function ( data ) {
123138 if ( data . evt [ MODIFIERKEY ] ) {
139+ // shift-click modifies the value at index 1
124140 this . selectedValues [ 1 ] = data . d ;
125141 } else if ( this . selectedValues [ 0 ] && this . selectedValues [ 0 ] . value === data . d . value ) {
142+ // case where single selected item is clicked again (need to unselect)
126143 this . selectedValues = [ ] ;
127144 } else {
145+ // case where multiple or no elements are selected (need to just select one item)
128146 this . selectedValues = [ data . d ] ;
129147 }
130148 var firstSelected = this . selectedValues [ 0 ] ;
@@ -137,21 +155,18 @@ module.exports = {
137155 el . classList . add ( 'unselected' ) ;
138156 }
139157 } ) ;
140- if ( ! firstSelected ) {
141- // no value
142- this . unset ( 'refineValue' ) ;
143- } else {
144- var getComparableValue = this . _getComparableValue . bind ( this ) ;
158+ if ( firstSelected ) {
159+ var getOrderedValue = this . _getOrderedValue . bind ( this ) ;
145160 var first = _ . min ( this . selectedValues , function ( el ) {
146- return getComparableValue ( el ) ;
161+ return getOrderedValue ( el ) ;
147162 } ) ;
148163 var last = _ . max ( this . selectedValues , function ( el ) {
149- return getComparableValue ( el ) ;
164+ return getOrderedValue ( el ) ;
150165 } ) ;
151166
152- // use getComparableValue to determine what elements should be selected
153- var lower = getComparableValue ( first ) ;
154- var upper = getComparableValue ( last ) ;
167+ // use getOrderedValue to determine what elements should be selected
168+ var lower = getOrderedValue ( first ) ;
169+ var upper = getOrderedValue ( last ) ;
155170 if ( this . model . getType ( ) === 'Number' ) {
156171 upper += last . dx ;
157172 }
@@ -165,15 +180,22 @@ module.exports = {
165180 */
166181 var upperInclusive = last . dx === 0 ;
167182 _ . each ( data . all , function ( el ) {
168- var elData = getComparableValue ( d3 . select ( el ) . data ( ) [ 0 ] ) ;
183+ var elData = getOrderedValue ( d3 . select ( el ) . data ( ) [ 0 ] ) ;
169184 if ( elData >= lower && ( upperInclusive ? elData <= upper : elData < upper ) ) {
170185 el . classList . add ( 'selected' ) ;
171186 el . classList . remove ( 'unselected' ) ;
172187 }
173188 } ) ;
174189 }
175190 } ,
176- handleDragEvent : function ( data ) {
191+ /**
192+ * Handler for query builder events created with a click-drag mouse action. The visual updates
193+ * are handled by d3 directly, so all we have to do is update the selected values based on the
194+ * selected elements.
195+ *
196+ * @param {Object } data the contains information about the event, @see handleQueryBuilderEvent
197+ */
198+ handleEvent_drag : function ( data ) {
177199 this . selectedValues = d3 . selectAll ( data . selected ) . data ( ) ;
178200 } ,
179201 /**
@@ -199,37 +221,40 @@ module.exports = {
199221 *
200222 */
201223 handleQueryBuilderEvent : function ( data ) {
202- var distinctEvent = data . type === 'drag' ? this . handleDragEvent : this . handleDistinctEvent ;
203- var rangeEvent = data . type === 'drag' ? this . handleDragEvent : this . handleRangeEvent ;
224+ var queryType ;
204225
205226 if ( data . type === 'click' ) {
206227 data . evt . stopPropagation ( ) ;
207228 data . evt . preventDefault ( ) ;
208229 }
209230
231+ // determine what kind of query this is (distinct or range)
210232 switch ( this . model . getType ( ) ) {
211233 case 'Boolean' : // fall-through to String
212234 case 'String' :
213- distinctEvent . call ( this , data ) ;
214- this . buildDistinctQuery ( ) ;
235+ queryType = 'distinct' ;
215236 break ;
216237 case 'Number' :
217238 if ( data . source === 'unique' ) {
218- distinctEvent . call ( this , data ) ;
219- this . buildDistinctQuery ( ) ;
239+ queryType = 'distinct' ;
220240 } else {
221- rangeEvent . call ( this , data ) ;
222- this . buildRangeQuery ( ) ;
241+ queryType = 'range' ;
223242 }
224243 break ;
225244 case 'ObjectID' : // fall-through to Date
226245 case 'Date' :
227- // @todo : for dates, data.all is not sorted, so this is not yet working
228- rangeEvent . call ( this , data ) ;
229- this . buildRangeQuery ( ) ;
246+ queryType = 'range' ;
230247 break ;
231248 default : // @todo other types not implemented yet
232- break ;
249+ throw new Error ( 'unsupported querybuilder type ' + this . model . getType ( ) ) ;
250+ }
251+
252+ // now call appropriate event handlers and query build methods
253+ if ( data . type === 'drag' ) {
254+ this . handleEvent_drag ( data ) ;
255+ } else {
256+ this [ 'handleEvent_' + queryType ] ( data ) ;
233257 }
258+ this [ 'buildQuery_' + queryType ] ( ) ;
234259 }
235260} ;
0 commit comments