PageSourceSearch

https://terrancelaw.github.io/cow_event/js/settingFilterPane.js

js terrancelaw.github.io collected 2026-10-03 10:04:36 UTC 21,706 bytes, 564 lines download raw bytes

1const SettingFilterPane = {
2	filterList: {},
3
4	show: function() {
5		const self = this;
6		$('#setting-pane > .settings').removeClass('visualize');
7		$('#setting-pane > .settings').removeClass('group');
8		$('#setting-pane > .settings').removeClass('filter');
9		$('#setting-pane > .settings').removeClass('encode');
10		$('#setting-pane > .settings').removeClass('highlight');
11		$('#setting-pane > .settings').removeClass('other');
12		$('#setting-pane > .settings').addClass('filter');
13		self.filterList = {};
14		self.showSearchBox();
15		self.addCountryFiltersToList();
16		self.addEventFiltersToList();
17		self.drawFilters();
18		self.initClickListFilterHeaderBehaviour();
19		self.initClickRangeFilterHeaderBehaviour();
20		self.initHoverTitleBehaviour();
21		self.initHoverFilterHeaderBehaviour();
22		self.initSearchBoxBehaviour();
23		$('#setting-pane > .settings').scrollTop(0);
24	},
25	showSearchBox: function() {
26		$('#setting-pane > .search-box')
27			.css('display', '');
28	},
29	addCountryFiltersToList: function() {
30		const self = this;
31		var countryAttributeList = Database.entityList.country;
32		var filterList = self.filterList;
33
34		filterList.country = [];
35
36		for (var i = 0; i < countryAttributeList.length; i++) {
37			var attributeData = countryAttributeList[i];
38			var attributeName = attributeData.attributeName;
39			var attributeType = attributeData.attributeType;
40			var filterObject = null;
41			var previousFilterObjectList = State.getPreviousFilterObjectList({
42				databaseQueryType: 'entity',
43				databaseQueryName: 'country',
44				attributeName: attributeName,
45				attributeType: attributeType
46			});
47
48			if (previousFilterObjectList.length == 0) { // not previously stored
49				filterObject = {};
50				filterObject.databaseQueryType = 'entity';
51				filterObject.databaseQueryName = 'country';
52				filterObject.attributeName = attributeName;
53				filterObject.attributeType = attributeType;
54				filterObject.displayName = attributeName;
55				filterObject.isFiltered = false;
56				self.addDataToFilterObject(filterObject, attributeType, attributeData);
57				filterList.country.push(filterObject);
58			}
59			else if (previousFilterObjectList.length > 0) {
60				filterObject = previousFilterObjectList[0]; // only one match
61				filterList.country.push(filterObject);
62			}
63		}
64	},
65	addEventFiltersToList: function() {
66		const self = this;
67		var filterList = self.filterList;
68		var visualizeState = State.visualize;
69		var selectedEvents = visualizeState.filter(function(d) {
70			return (d.type == 'pointEvent' || d.type == 'intervalEvent');
71		});
72
73		for (var i = 0; i < selectedEvents.length; i++) {
74			var eventData = selectedEvents[i].data;
75			var eventName = eventData.eventName;
76			var eventAttributeList = eventData.eventAttributeList;
77
78			filterList[eventName] = [];
79
80			for (var j = 0; j < eventAttributeList.length; j++) {
81				var eventAttributeData = eventAttributeList[j];
82				var eventAttributeName = eventAttributeData.eventAttributeName;
83				var eventAttributeType = eventAttributeData.eventAttributeType;
84				var eventAttributeAttributeList = eventAttributeData.eventAttributeAttributeList;
85				var filterObject = null;
86				var previousFilterObjectList = State.getPreviousFilterObjectList({
87					databaseQueryType: 'event',
88					databaseQueryName: eventName,
89					eventAttributeName: eventAttributeName,
90					eventAttributeType: eventAttributeType,
91					displayName: eventAttributeName // to differentiate from event attribute attribute
92				});
93
94				// handle event attribute
95				if (previousFilterObjectList.length == 0) { // not previously stored
96					filterObject = {};
97					filterObject.databaseQueryType = 'event';
98					filterObject.databaseQueryName = eventName;
99					filterObject.eventAttributeName = eventAttributeName;
100					filterObject.eventAttributeType = eventAttributeType;
101					filterObject.displayName = eventAttributeName;
102					filterObject.isFiltered = false;
103					self.addDataToFilterObject(filterObject, eventAttributeType, eventAttributeData);
104					filterList[eventName].push(filterObject);
105				}
106				else if (previousFilterObjectList.length > 0) {
107					filterObject = previousFilterObjectList[0]; // only one match
108					filterList[eventName].push(filterObject);
109				}
110
111				// handle event attribute attribute
112				for (var k = 0; k < eventAttributeAttributeList.length; k++) {
113					var eventAttributeAttributeData = eventAttributeAttributeList[k];
114					var eventAttributeAttributeName = eventAttributeAttributeData.attributeName;
115					var eventAttributeAttributeType = eventAttributeAttributeData.attributeType;
116					var eventAttributEntityKey = eventAttributeData.entityKey;
117					var shortEventAttributeName = eventAttributeName.length > 10 ? eventAttributeName.substring(0, 10) + '...' : eventAttributeName;
118					var filterObject = null;
119					var previousFilterObjectList = State.getPreviousFilterObjectList({
120						databaseQueryType: 'event',
121						databaseQueryName: eventName,
122						eventAttributeName: eventAttributeName,
123						eventAttributeType: eventAttributeType,
124						eventAttributeAttributeName: eventAttributeAttributeName,
125						eventAttributeAttributeType: eventAttributeAttributeType
126					});
127
128					if (previousFilterObjectList.length == 0) { // not previously stored
129						filterObject = {};
130						filterObject.databaseQueryType = 'event';
131						filterObject.databaseQueryName = eventName;
132						filterObject.eventAttributeName = eventAttributeName;
133						filterObject.eventAttributeType = eventAttributeType;
134						filterObject.eventAttributeAttributeName = eventAttributeAttributeName;
135						filterObject.eventAttributeAttributeType = eventAttributeAttributeType;
136						filterObject.entityKey = eventAttributEntityKey;
137						filterObject.displayName = shortEventAttributeName + ' : ' + eventAttributeAttributeName;
138						filterObject.isFiltered = false;
139						self.addDataToFilterObject(filterObject, eventAttributeAttributeType, eventAttributeAttributeData);
140						filterList[eventName].push(filterObject);
141					}
142					else if (previousFilterObjectList.length > 0) {
143						filterObject = previousFilterObjectList[0]; // only one match
144						filterList[eventName].push(filterObject);
145					}
146				}
147			}
148		}
149	},
150	drawFilters: function() {
151		const self = this;
152		var filterList = self.filterList;
153
154		for (var filterItem in filterList) {
155			var title = 'Filter ' + filterItem;
156
157			$('#setting-pane > .settings')
158				.append('<div class="title">' + title + '</div>'); // add name span for showing tooltip
159
160			for (var i = 0; i < filterList[filterItem].length; i++) {
161				var filterObject = filterList[filterItem][i];
162				var attributeName = filterObject.displayName;
163				var isFiltered = filterObject.isFiltered;
164				var attributeType = null;
165				var filterCapsuleHTML = null;
166				var addedFilterCapsuleEl = null;
167
168				if ('eventAttributeAttributeType' in filterObject) attributeType = filterObject.eventAttributeAttributeType;
169				else if ('eventAttributeType' in filterObject) attributeType = filterObject.eventAttributeType;
170				else if ('attributeType' in filterObject) attributeType = filterObject.attributeType;
171				filterCapsuleHTML = self.generateFilterCapsuleHTML(attributeName, attributeType, isFiltered);
172				$('#setting-pane > .settings').append(filterCapsuleHTML);
173				addedFilterCapsuleEl = $('#setting-pane > .settings > .filter-capsule').last()[0];
174				d3.select(addedFilterCapsuleEl).datum(filterObject);
175			}
176		}
177	},
178	initClickListFilterHeaderBehaviour: function() {
179		const self = this;
180
181		$('#setting-pane > .settings.filter > .filter-capsule.list > .header')
182			.on('click', onClickFilterPaneListFilterHeader);
183
184		function onClickFilterPaneListFilterHeader(event) {
185			var filterCapsuleEl = this.parentNode;
186			var needToExpand = !$(filterCapsuleEl).hasClass('expanded');
187			var filterContentEl = $(filterCapsuleEl).find('.content')[0];
188			var filterObject = d3.select(filterCapsuleEl).datum();
189			var clickedSelectAllButton = $(event.target).hasClass('select-all-button');
190			var clickedSelectNoneButton = $(event.target).hasClass('select-none-button');
191
192			// expand
193			if (needToExpand && !clickedSelectAllButton && !clickedSelectNoneButton) {
194				$(filterCapsuleEl).addClass('expanded');
195				$(filterContentEl).empty();
196				$(this).find('.expand-button').removeClass('fa-caret-square-down');
197				$(this).find('.expand-button').addClass('fa-caret-square-up');
198				for (var i = 0; i < filterObject.categories.length; i++) {
199					var categoryName = filterObject.categories[i];
200					var categoryIsChecked = filterObject.filteredCategories.indexOf(categoryName) != -1;
201					var categoryHTML = self.generateCategoryHTML(categoryName, categoryIsChecked);
202					var addedCategoryEl = null;
203					$(filterContentEl).append(categoryHTML);
204					addedCategoryEl = $(filterContentEl).find('.category').last()[0];
205					d3.select(addedCategoryEl).datum(categoryName);
206				}
207				self.initClickListFilterCategoryBehaviour(filterContentEl);
208				self.initHoverListFilterCategoryBehaviour(filterContentEl);
209			}
210
211			// collapse
212			else if (!needToExpand && !clickedSelectAllButton && !clickedSelectNoneButton) {
213				$(filterCapsuleEl).removeClass('expanded');
214				$(filterContentEl).empty();
215				$(this).find('.expand-button').removeClass('fa-caret-square-up');
216				$(this).find('.expand-button').addClass('fa-caret-square-down');
217			}
218
219			// select all
220			else if (clickedSelectAllButton) {
221				ListFilter.clickSelectAll(filterCapsuleEl); // update states
222				RightColumn.update();
223			}
224
225			// select none
226			else if (clickedSelectNoneButton) {
227				ListFilter.clickSelectNone(filterCapsuleEl); // update states
228				RightColumn.update();
229			}
230		}
231	},
232	initClickRangeFilterHeaderBehaviour: function() {
233		const self = this;
234
235		$('#setting-pane > .settings.filter > .filter-capsule.range > .header')
236			.on('click', onClickFilterPaneRangeFilter);
237
238		function onClickFilterPaneRangeFilter() {
239			var filterCapsuleEl = this.parentNode;
240			var needToExpand = !$(filterCapsuleEl).hasClass('expanded');
241			var filterContentEl = $(filterCapsuleEl).find('.content')[0];
242			var filterObject = d3.select(filterCapsuleEl).datum();
243			var clickedSelectAllButton = $(event.target).hasClass('select-all-button');
244			var clickedSelectNoneButton = $(event.target).hasClass('select-none-button');
245
246			// expand
247			if (needToExpand && !clickedSelectAllButton && !clickedSelectNoneButton) {
248				var sliderHTML = self.generateSliderHTML();
249				var sliderObject = new FilterSlider(filterContentEl);
250
251				var filterMin = filterObject.range[0];
252				var filterMax = filterObject.range[1];
253				var filterHandleMin = filterObject.filteredRange[0];
254				var filterHandleMax = filterObject.filteredRange[1];
255				let realMaxNumberOfDecimal = Math.max(self.countDecimals(filterMin), self.countDecimals(filterMax));
256				let maxNumberOfDecimal = (realMaxNumberOfDecimal > 2) ? 2 : realMaxNumberOfDecimal;
257				let step = 1 / Math.pow(10, maxNumberOfDecimal);
258
259				$(filterCapsuleEl).addClass('expanded');
260				$(filterContentEl).empty();
261				$(this).find('.expand-button').removeClass('fa-caret-square-down');
262				$(this).find('.expand-button').addClass('fa-caret-square-up');
263				$(filterContentEl).append(sliderHTML);
264
265				sliderObject.init();
266				sliderObject.initHandleValues();
267				sliderObject.initDragBehaviour();
268				sliderObject.updateMinMax(filterMin, filterMax);
269				sliderObject.updateValues([ filterHandleMin, filterHandleMax ]);
270				sliderObject.updateStep(step);
271				sliderObject.updateHandles();
272				sliderObject.updateMinMaxText();
273			}
274
275			// collapse
276			else if (!needToExpand && !clickedSelectAllButton && !clickedSelectNoneButton) {
277				$(filterCapsuleEl).removeClass('expanded');
278				$(filterContentEl).empty();
279				$(this).find('.expand-button').removeClass('fa-caret-square-up');
280				$(this).find('.expand-button').addClass('fa-caret-square-down');
281			}
282		}
283	},
284	initHoverTitleBehaviour: function() {
285		$('#setting-pane > .settings.filter > .title')
286			.unbind('mouseover').unbind('mouseout')
287			.on('mouseover', onMouseoverTitle)
288			.on('mouseout', onMouseoutTitle);
289
290		function onMouseoverTitle() {
291			var name = $(this).text();
292			SettingPaneAttributeTooltip.show(name, this);
293		}
294
295		function onMouseoutTitle() {
296			SettingPaneAttributeTooltip.hide();
297		}
298	},
299	initHoverFilterHeaderBehaviour: function() {
300		$('#setting-pane > .settings.filter > .filter-capsule > .header')
301			.unbind('mouseover').unbind('mouseout')
302			.on('mouseover', onMouseoverFilterHeader)
303			.on('mouseout', onMouseoutFilterHeader);
304
305		function onMouseoverFilterHeader(event) {
306			var data = d3.select(this.parentNode).datum();
307			var name = null;
308			var hoveredSelectNoneButton = $(event.target).hasClass('select-none-button');
309			var hoveredSelectAllButton = $(event.target).hasClass('select-all-button');
310
311			if (data.databaseQueryType == 'entity') name = data.attributeName;
312			else if ('eventAttributeAttributeName' in data) name = data.eventAttributeName + ' : ' + data.eventAttributeAttributeName;
313			else name = data.eventAttributeName;
314
315			if (!hoveredSelectNoneButton && !hoveredSelectAllButton) {
316				$(this).addClass('hover');
317				SettingPaneAttributeTooltip.show(name, this);
318			}
319			else if (hoveredSelectNoneButton) {
320				var offset = $(this).offset();
321				var width = $(this).width();
322
323				$(this).removeClass('hover');
324				SettingPaneAttributeTooltip.simplyShow(
325					left = offset.left + width + 8 + 5,
326					top = offset.top + 8,
327					text = 'Click to deselect all categories below'
328				);
329			}
330			else if (hoveredSelectAllButton) {
331				var offset = $(this).offset();
332				var width = $(this).width();
333
334				$(this).removeClass('hover');
335				SettingPaneAttributeTooltip.simplyShow(
336					left = offset.left + width + 8 + 5,
337					top = offset.top + 8,
338					text = 'Click to select all categories below'
339				);
340			}
341		}
342
343		function onMouseoutFilterHeader() {
344			$(this).removeClass('hover');
345			SettingPaneAttributeTooltip.hide();
346		}
347	},
348	initSearchBoxBehaviour: function() {
349		const self = this;
350
351		$('#setting-pane > .search-box > input')
352			.unbind('input')
353			.on('input', onInputSearchQuery);
354
355		function onInputSearchQuery() {
356			var searchQueryInLowerCase = $(this).val().toLowerCase();
357
358			if (searchQueryInLowerCase == '') {
359				SettingPane.empty();
360				self.filterList = {};
361				self.addCountryFiltersToList();
362				self.addEventFiltersToList();
363				self.drawFilters();
364				self.initClickListFilterHeaderBehaviour();
365				self.initClickRangeFilterHeaderBehaviour();
366				self.initHoverTitleBehaviour();
367				self.initHoverFilterHeaderBehaviour();
368			}
369
370			else if (searchQueryInLowerCase != '') {
371				self.expandListFilters(searchQueryInLowerCase);
372				self.markVariables(searchQueryInLowerCase);
373				self.filterFilters(searchQueryInLowerCase);
374				self.filterTitles();
375			}
376		}
377	},
378
379	// initSearchBoxBehaviour
380
381	expandListFilters: function(searchQueryInLowerCase) {
382		const self = this;
383
384		$('#setting-pane > .settings.filter > .filter-capsule.list').each(function() {
385			var filterCapsuleEl = this;
386			var filterContentEl = $(filterCapsuleEl).find('.content')[0];
387			var filterObject = d3.select(filterCapsuleEl).datum();
388			var allCategoryList = filterObject.categories;
389			var filteredCategoryList = filterObject.filteredCategories;
390			var matchedCategoryList = [];
391
392			// get matchedCategoryList
393			for (var i = 0; i < allCategoryList.length; i++) {
394				var categoryName = allCategoryList[i];
395				var categoryNameInLowerCase = categoryName.toLowerCase();
396				var categoryNameMatchedQuery = categoryNameInLowerCase.indexOf(searchQueryInLowerCase) != -1;
397				if (categoryNameMatchedQuery) matchedCategoryList.push(categoryName);
398			}
399
400			// expand if matchedCategoryList is not empty
401			if (matchedCategoryList.length > 0) {
402				$(filterCapsuleEl).addClass('expanded');
403				$(filterContentEl).empty();
404				$(this).find('.expand-button').removeClass('fa-caret-square-down');
405				$(this).find('.expand-button').addClass('fa-caret-square-up');
406			}
407
408			// collapse if matchedCategoryList is empty
409			else if (matchedCategoryList.length == 0) {
410				$(filterCapsuleEl).removeClass('expanded');
411				$(filterContentEl).empty();
412				$(this).find('.expand-button').removeClass('fa-caret-square-up');
413				$(this).find('.expand-button').addClass('fa-caret-square-down');
414			}
415
416			// draw matchedCategoryList
417			for (var i = 0; i < matchedCategoryList.length; i++) {
418				var categoryName = matchedCategoryList[i];
419				var categoryIsChecked = filteredCategoryList.indexOf(categoryName) != -1;
420				var categoryHTML = self.generateCategoryHTML(categoryName, categoryIsChecked);
421				var addedCategoryEl = null;
422				$(filterContentEl).append(categoryHTML);
423				addedCategoryEl = $(filterContentEl).find('.category').last()[0];
424				d3.select(addedCategoryEl).datum(categoryName);
425			}
426			self.initClickListFilterCategoryBehaviour(filterContentEl);
427			self.initHoverListFilterCategoryBehaviour(filterContentEl);
428		});
429	},
430	markVariables: function(searchQueryInLowerCase) {
431		var markInstance = new Mark(document.querySelectorAll('#setting-pane > .settings.filter'));
432
433		markInstance.unmark({
434	  		done: function(){
435	    		markInstance.mark(searchQueryInLowerCase, {
436    				'separateWordSearch': false,
437    				'diacritics': false
438	    		});
439	    	}
440	  	});
441	},
442	filterFilters: function(searchQueryInLowerCase) {
443		$('#setting-pane > .settings.filter > .filter-capsule').each(function() {
444			var filterCapsuleEl = this;
445			var filterObject = d3.select(filterCapsuleEl).datum();
446			var attributeName = filterObject.displayName;
447			var attributeNameInLowerCase = attributeName.toLowerCase();
448			var attributeNameMatchedQuery = attributeNameInLowerCase.indexOf(searchQueryInLowerCase) != -1;
449			var categoriesMatchedQuery = $(filterCapsuleEl).hasClass('expanded');
450
451			if (attributeNameMatchedQuery || categoriesMatchedQuery)
452				$(filterCapsuleEl).css('display', '');
453			else if (!attributeNameMatchedQuery && !categoriesMatchedQuery)
454				$(filterCapsuleEl).css('display', 'none');
455		});
456	},
457	filterTitles: function() {
458		$('#setting-pane > .settings.filter > .title').each(function() {
459			var currentTitleEl = this;
460			var currentEl = $(this).next()[0];
461			var currentTitleHasContent = false;
462			var isCurrentFilterCapsule = $(currentEl).hasClass('filter-capsule');
463
464			while (isCurrentFilterCapsule) {
465				if ($(currentEl).is(':visible')) {
466					currentTitleHasContent = true; break;
467				}
468				currentEl = $(currentEl).next()[0];
469				isCurrentFilterCapsule = $(currentEl).hasClass('filter-capsule');
470			}
471
472			if (currentTitleHasContent) $(currentTitleEl).css('display', '');
473			else if (!currentTitleHasContent) $(currentTitleEl).css('display', 'none');
474		});
475	},
476
477	// helpers
478	addDataToFilterObject: function(filterObject, attributeType, data) {
479		const self = this;
480
481		if (attributeType == 'categorical') {
482			filterObject.categories = self.shallowCopyList(data.categories);
483			filterObject.filteredCategories = self.shallowCopyList(data.categories);
484		}
485		else if (attributeType == 'nominal') {
486			filterObject.range = d3.extent(data.levels);
487			filterObject.filteredRange = d3.extent(data.levels);
488		}
489		else if (attributeType == 'numerical') {
490			filterObject.range = self.shallowCopyList(data.range);
491			filterObject.filteredRange = self.shallowCopyList(data.range);
492		}
493	},
494	shallowCopyList: function(list) {
495		var newList = [];
496
497		for (var i = 0; i < list.length; i++)
498			newList.push(list[i]);
499
500		return newList;
501	},
502	initClickListFilterCategoryBehaviour: function(filterContentEl) {
503		$(filterContentEl).find('.category')
504			.on('click', onClickListFilterCategory);
505
506		function onClickListFilterCategory() {
507			var filterCapsuleEl = filterContentEl.parentNode;
508			ListFilter.clickCategory(filterCapsuleEl, this); // update states
509			RightColumn.update();
510		}
511	},
512	initHoverListFilterCategoryBehaviour: function(filterContentEl) {
513		$(filterContentEl).find('.category')
514			.unbind('mouseover').unbind('mouseout')
515			.on('mouseover', onMouseoverListFilterCategory)
516			.on('mouseout', onMouseoutListFilterCategory);
517
518		function onMouseoverListFilterCategory() {
519			var name = d3.select(this).datum();
520			SettingPaneAttributeTooltip.show(name, this);
521		}
522
523		function onMouseoutListFilterCategory() {
524			SettingPaneAttributeTooltip.hide();
525		}
526	},
527	generateFilterCapsuleHTML: function(attributeName, attributeType, isFiltered) {
528		var typeClass = attributeType == 'categorical' ? 'fa-font' 
529					  : (attributeType == 'temporal' ? 'fa-chart-line' : 'fa-hashtag');
530		var filterClass = attributeType == 'categorical' ? 'list' : 'range';
531		var isFilteredClass = isFiltered ? ' filtered' : '';
532
533		return '<div class="filter-capsule ' + filterClass + isFilteredClass +  '">' +
534					'<div class="header">' +
535						'<span class="fa fa-filter filter-icon"></span>' +
536						'<span class="fa ' + typeClass + ' type"></span>' +
537						'<span class="name">' + attributeName + '</span>' +
538						'<span class="far fa-square select-none-button"></span>' +
539						'<span class="far fa-check-square select-all-button"></span>' +
540						'<span class="far fa-caret-square-down expand-button"></span>' +
541					'</div>' +
542					'<div class="content"></div>' +
543				'</div>';
544	},
545	generateCategoryHTML: function(categoryName, categoryIsChecked) {
546		return '<div class="category">' +
547					'<span class="round-check-box">' +
548				    	'<input type="checkbox"' + (categoryIsChecked ? ' checked' : '') + '/>' +
549				    	'<label></label>' +
550				  	'</span>' +
551					'<span class="name">' + categoryName + '</span>' +
552				'</div>';
553	},
554	generateSliderHTML: function() {
555		return '<span class="min-text">min</span>' +
556			   '<input type="text"/>' +
557			   '<span class="max-text">max</span>';
558	},
559	countDecimals: function(value) {
560		if (Math.floor(value) !== value)
561	        return value.toString().split(".")[1].length || 0;
562	    return 0;
563	}
564}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.