From b79aa7a3057cded2048f993eb74a59f907ebde95 Mon Sep 17 00:00:00 2001 From: Anjal Date: Tue, 11 Jun 2024 09:56:07 +0545 Subject: [PATCH 1/4] multi order table sort added, data grouping with separators added, data numbering for multi order sort added --- docs/examples/table-sort.json | 458 ++++++++++++++++++ .../table-with-multi-column-sort.html | 78 +++ docs/index.html | 1 + docs/maptable.css | 2 +- docs/maptable.js | 123 ++++- src/components/Table.js | 101 +++- src/maptable.css | 22 + src/maptable.js | 31 +- src/utils.js | 32 +- 9 files changed, 787 insertions(+), 61 deletions(-) create mode 100644 docs/examples/table-sort.json create mode 100644 docs/examples/table-with-multi-column-sort.html diff --git a/docs/examples/table-sort.json b/docs/examples/table-sort.json new file mode 100644 index 0000000..ce1337f --- /dev/null +++ b/docs/examples/table-sort.json @@ -0,0 +1,458 @@ +[ + { + "name": "Tiger Nixon", + "position": "System Architect", + "office": "Edinburgh", + "extn": "5421", + "day":"Sunday", + "month":"January" + }, + { + "name": "Garrett Winters", + "position": "Accountant", + "office": "Tokyo", + "extn": "8422", + "day":"Friday", + "month":"November" + }, + { + "name": "Ashton Cox", + "position": "Junior Technical Author", + "office": "San Francisco", + "extn": "1562", + "day":"Wednesday", + "month":"April" + }, + { + "name": "Cedric Kelly", + "position": "Senior Javascript Developer", + "office": "Edinburgh", + "extn": "6224", + "day":"Thursday", + "month":"October" + }, + { + "name": "Airi Satou", + "position": "Accountant", + "office": "Tokyo", + "extn": "5407", + "day":"Sunday", + "month":"January" + }, + { + "name": "Brielle Williamson", + "position": "Integration Specialist", + "office": "New York", + "extn": "4804", + "day":"Tuesday", + "month":"May" + }, + { + "name": "Herrod Chandler", + "position": "Sales Assistant", + "office": "San Francisco", + "extn": "9608", + "day":"Friday", + "month":"July" + }, + { + "name": "Rhona Davidson", + "position": "Integration Specialist", + "office": "Tokyo", + "extn": "6200", + "day":"Sunday", + "month":"December" + }, + { + "name": "Colleen Hurst", + "position": "Javascript Developer", + "office": "San Francisco", + "extn": "2360", + "day":"Saturday", + "month":"October" + }, + { + "name": "Sonya Frost", + "position": "software Engineer", + "office": "Edinburgh", + "extn": "1667", + "day":"Monday", + "month":"September" + }, + { + "name": "Jena Gaines", + "position": "Office Manager", + "office": "London", + "extn": "3814", + "day":"Tuesday", + "month":"January" + }, + { + "name": "Quinn Flynn", + "position": "Support Lead", + "office": "Edinburgh", + "extn": "9497", + "day":"Wednesday", + "month":"October" + }, + { + "name": "Charde Marshall", + "position": "Regional Director", + "office": "San Francisco", + "extn": "6741", + "day":"Tuesday", + "month":"May" + }, + { + "name": "Haley Kennedy", + "position": "Senior Marketing Designer", + "office": "London", + "extn": "3597", + "day":"Wednesday", + "month":"October" + }, + { + "name": "Tatyana Fitzpatrick", + "position": "Regional Director", + "office": "London", + "extn": "1965", + "day":"Friday", + "month":"July" + }, + { + "name": "Michael Silva", + "position": "Marketing Designer", + "office": "London", + "extn": "1581", + "day":"Saturday", + "month":"April" + }, + { + "name": "Paul Byrd", + "position": "Chief Financial Officer (CFO)", + "office": "New York", + "extn": "3059", + "day":"Thursday", + "month":"October" + }, + { + "name": "Gloria Little", + "position": "Systems Administrator", + "office": "New York", + "extn": "1721", + "day":"Sunday", + "month":"January" + }, + { + "name": "Bradley Greer", + "position": "software Engineer", + "office": "London", + "extn": "2558", + "day":"Friday", + "month":"July" + }, + { + "name": "Dai Rios", + "position": "Personnel Lead", + "office": "Edinburgh", + "extn": "2290", + "day":"Thursday", + "month":"April" + }, + { + "name": "Jenette Caldwell", + "position": "Development Lead", + "office": "New York", + "extn": "1937", + "day":"Thursday", + "month":"June" + }, + { + "name": "Yuri Berry", + "position": "Chief Marketing Officer (CMO)", + "office": "New York", + "extn": "6154", + "day":"Wednesday", + "month":"April" + }, + { + "name": "Caesar Vance", + "position": "Pre-Sales Support", + "office": "New York", + "extn": "8330", + "day":"Thursday", + "month":"March" + }, + { + "name": "Doris Wilder", + "position": "Sales Assistant", + "office": "Sidney", + "extn": "3023", + "day":"Sunday", + "month":"January" + }, + { + "name": "Angelica Ramos", + "position": "Chief Executive Officer (CEO)", + "office": "London", + "extn": "5797", + "day":"Tuesday", + "month":"May" + }, + { + "name": "Gavin Joyce", + "position": "Developer", + "office": "Edinburgh", + "extn": "8822", + "day":"Saturday", + "month":"April" + }, + { + "name": "Jennifer Chang", + "position": "Regional Director", + "office": "Singapore", + "extn": "9239", + "day":"Friday", + "month":"July" + }, + { + "name": "Brenden Wagner", + "position": "Software Engineer", + "office": "San Francisco", + "extn": "1314", + "day":"Thursday", + "month":"June" + }, + { + "name": "Fiona Green", + "position": "Chief Operating Officer (COO)", + "office": "San Francisco", + "extn": "2947", + "day":"Thursday", + "month":"March" + }, + { + "name": "Shou Itou", + "position": "Regional Marketing", + "office": "Tokyo", + "extn": "8899", + "day":"Wednesday", + "month":"April" + }, + { + "name": "Michelle House", + "position": "Integration Specialist", + "office": "Sidney", + "extn": "2769", + "day":"Sunday", + "month":"January" + }, + { + "name": "Suki Burks", + "position": "Developer", + "office": "London", + "extn": "6832", + "day":"Thursday", + "month":"November" + }, + { + "name": "Prescott Bartlett", + "position": "Technical Author", + "office": "London", + "extn": "3606", + "day":"Friday", + "month":"July" + }, + { + "name": "Gavin Cortez", + "position": "Team Leader", + "office": "San Francisco", + "extn": "2860", + "day":"Saturday", + "month":"April" + }, + { + "name": "Martena Mccray", + "position": "Post-Sales support", + "office": "Edinburgh", + "extn": "8240", + "day":"Sunday", + "month":"January" + }, + { + "name": "Unity Butler", + "position": "Marketing Designer", + "office": "San Francisco", + "extn": "5384", + "day":"Thursday", + "month":"November" + }, + { + "name": "Howard Hatfield", + "position": "Office Manager", + "office": "San Francisco", + "extn": "7031", + "day":"Friday", + "month":"May" + }, + { + "name": "Hope Fuentes", + "position": "Secretary", + "office": "San Francisco", + "extn": "6318", + "day":"Tuesday", + "month":"January" + }, + { + "name": "Vivian Harrell", + "position": "Financial Controller", + "office": "San Francisco", + "extn": "9422", + "day":"Wednesday", + "month":"April" + }, + { + "name": "Timothy Mooney", + "position": "Office Manager", + "office": "London", + "extn": "7580", + "day":"Friday", + "month":"July" + }, + { + "name": "Jackson Bradshaw", + "position": "Director", + "office": "New York", + "extn": "1042", + "day":"Monday", + "month":"January" + }, + { + "name": "Olivia Liang", + "position": "Support Engineer", + "office": "Singapore", + "extn": "2120", + "day":"Thursday", + "month":"August" + }, + { + "name": "Bruno Nash", + "position": "Software Engineer", + "office": "London", + "extn": "6222", + "day":"Thursday", + "month":"August" + }, + { + "name": "Sakura Yamamoto", + "position": "Support Engineer", + "office": "Tokyo", + "extn": "9383", + "day":"Wednesday", + "month":"April" + }, + { + "name": "Thor Walton", + "position": "Developer", + "office": "New York", + "extn": "8327", + "day":"Friday", + "month":"July" + }, + { + "name": "Finn Camacho", + "position": "Support Engineer", + "office": "San Francisco", + "extn": "2927", + "day":"Thursday", + "month":"February" + }, + { + "name": "Serge Baldwin", + "position": "Data Coordinator", + "office": "Singapore", + "extn": "8352", + "day":"Saturday", + "month":"January" + }, + { + "name": "Zenaida Frank", + "position": "Software Engineer", + "office": "New York", + "extn": "7439", + "day":"Wednesday", + "month":"April" + }, + { + "name": "Zorita Serrano", + "position": "Software Engineer", + "office": "San Francisco", + "extn": "4389", + "day":"Tuesday", + "month":"June" + }, + { + "name": "Jennifer Acosta", + "position": "Junior Javascript Developer", + "office": "Edinburgh", + "extn": "3431", + "day":"Thursday", + "month":"February" + }, + { + "name": "Cara Stevens", + "position": "Sales Assistant", + "office": "New York", + "extn": "3990", + "day":"Wednesday", + "month":"April" + }, + { + "name": "Hermione Butler", + "position": "Regional Director", + "office": "London", + "extn": "1016", + "day":"Friday", + "month":"July" + }, + { + "name": "Lael Greer", + "position": "Systems Administrator", + "office": "London", + "extn": "6733", + "day":"Thursday", + "month":"February" + }, + { + "name": "Jonas Alexander", + "position": "Developer", + "office": "San Francisco", + "extn": "8196", + "day":"Thursday", + "month":"February" + }, + { + "name": "Shad Decker", + "position": "Regional Director", + "office": "Edinburgh", + "extn": "6373", + "day":"Wednesday", + "month":"April" + }, + { + "name": "Michael Bruce", + "position": "Javascript Developer", + "office": "Singapore", + "extn": "5384", + "day":"Sunday", + "month":"January" + }, + { + "name": "Donna Snider", + "position": "Customer Support", + "office": "New York", + "extn": "4226", + "day":"Monday", + "month":"July" + } + ] \ No newline at end of file diff --git a/docs/examples/table-with-multi-column-sort.html b/docs/examples/table-with-multi-column-sort.html new file mode 100644 index 0000000..c4767c4 --- /dev/null +++ b/docs/examples/table-with-multi-column-sort.html @@ -0,0 +1,78 @@ + + + + + + + MapTable example + + + + + + + + + +
+
+
[Example] Table with multi-column sort
+
+ Demonstating: rendering a table and filters with multi-column sort
+
+ +
+
+
+
+ + + + + + \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index ddc10db..7f8c3b7 100644 --- a/docs/index.html +++ b/docs/index.html @@ -78,6 +78,7 @@

MapTable negative values table only fixed table header + multi-order table sort tooltip country w/ legend heatmap aggregate diff --git a/docs/maptable.css b/docs/maptable.css index 83d6821..5016bf8 100755 --- a/docs/maptable.css +++ b/docs/maptable.css @@ -1 +1 @@ -.form-control-inline{min-width:0;width:auto;display:inline}.panel{margin-top:15px}.mt-map-container{width:100%;height:0;position:relative;padding-bottom:52%}.mt-map-container #mt-map{width:100%;height:100%;display:block;position:absolute}.mt-map-country:hover{opacity:.9}.mt-map-tooltip{color:#222;background-color:hsla(0,0%,100%,.9);min-width:200px;max-width:300px;position:absolute}.mt-map-tooltip .badge{margin:5px 10px}.mt-map-marker{opacity:.9}.mt-map-marker:hover{opacity:1}.mt-table-sortable{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mt-table-sortable.sort_asc:after{content:"▲";font-size:7px;padding-left:5px}.mt-table-sortable.sort_desc:after{content:"▼";font-size:7px;padding-left:5px}.mt-table-sortable{cursor:pointer}#mt-filters-new{margin:10px}.mt-filter-row{margin:10px;padding-bottom:10px;border-bottom:1px solid #ddd;color:#aaa}.mt-filter-row span{font-size:14px;font-weight:500;color:#333;letter-spacing:1px;line-height:34px;padding-left:4px;padding-right:4px}#mt-map:hover #mt-map-export{opacity:1;transition:opacity .5s linear}#mt-map-export{position:absolute;top:10px;right:10px;opacity:0;transition:opacity .5s linear}.mt-loading{position:absolute;top:50%;color:#999;width:100%;text-align:center;font-size:24px;margin-top:-18px;display:none}#mt-map path{will-change:fill}.mt-blur{filter:blur(18px);transform:translateZ(0)}.mt-sun-gradient{background-image:radial-gradient(circle,rgba(255,192,0,.5) 0,rgba(255,192,0,.15) 45%,rgba(255,192,0,0) 100%)}.mt-header-fixed{position:sticky} \ No newline at end of file +.form-control-inline{min-width:0;width:auto;display:inline}.panel{margin-top:15px}.mt-map-container{width:100%;height:0;position:relative;padding-bottom:52%}.mt-map-container #mt-map{width:100%;height:100%;display:block;position:absolute}.mt-map-country:hover{opacity:.9}.mt-map-tooltip{color:#222;background-color:hsla(0,0%,100%,.9);min-width:200px;max-width:300px;position:absolute}.mt-map-tooltip .badge{margin:5px 10px}.mt-map-marker{opacity:.9}.mt-map-marker:hover{opacity:1}.mt-table-sortable{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mt-table-sortable.sort_asc:after{content:"▲";font-size:7px;padding-left:5px}.mt-table-sortable.sort_desc:after{content:"▼";font-size:7px;padding-left:5px}.mt-table-sortable{cursor:pointer}#mt-filters-new{margin:10px}.mt-filter-row{margin:10px;padding-bottom:10px;border-bottom:1px solid #ddd;color:#aaa}.mt-filter-row span{font-size:14px;font-weight:500;color:#333;letter-spacing:1px;line-height:34px;padding-left:4px;padding-right:4px}#mt-map:hover #mt-map-export{opacity:1;transition:opacity .5s linear}#mt-map-export{position:absolute;top:10px;right:10px;opacity:0;transition:opacity .5s linear}.mt-loading{position:absolute;top:50%;color:#999;width:100%;text-align:center;font-size:24px;margin-top:-18px;display:none}#mt-map path{will-change:fill}.mt-blur{filter:blur(18px);transform:translateZ(0)}.mt-sun-gradient{background-image:radial-gradient(circle,rgba(255,192,0,.5) 0,rgba(255,192,0,.15) 45%,rgba(255,192,0,0) 100%)}.mt-header-fixed{position:sticky}.bold{border-bottom:1.5px solid #999898}.table-sort-sn{position:absolute;left:0;width:38px;height:38px;display:flex;align-items:center;justify-content:flex-end;padding-right:7px;margin-left:-38px;opacity:.2;color:#000}.display-none{display:none} \ No newline at end of file diff --git a/docs/maptable.js b/docs/maptable.js index 5412939..cab99ee 100644 --- a/docs/maptable.js +++ b/docs/maptable.js @@ -111,7 +111,7 @@ this.d3.maptable = (function () { function toNumber(str) { if (!str || str === '') return null; - var resStr = str.toString().replace(/[^0-9.]+|\s+/gmi, ''); + var resStr = str.toString().replace(/[^0-9.]+|\s+/gim, ''); if (resStr !== '') return Number(resStr); return null; } @@ -152,6 +152,25 @@ this.d3.maptable = (function () { return str === null || str === '' || str === undefined; }; + var customSortOrders = { + days: ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'], + months: ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'] + }; + + var customSortAsc = function customSortAsc(type, d1, d2) { + var elem1 = d1.toLowerCase(); + var elem2 = d2.toLowerCase(); + var customSortOrder = customSortOrders[type] || []; + return customSortOrder.indexOf(elem1) - customSortOrder.indexOf(elem2); + }; + + var customSortDesc = function customSortDesc(type, d1, d2) { + var elem1 = d1.toLowerCase(); + var elem2 = d2.toLowerCase(); + var customSortOrder = customSortOrders[type] || []; + return customSortOrder.indexOf(elem2) - customSortOrder.indexOf(elem1); + }; + var utils = { rangeToBool: rangeToBool, appendOptions: appendOptions, @@ -162,7 +181,9 @@ this.d3.maptable = (function () { quantile: quantile, uniqueValues: uniqueValues, formatDate: formatDate, - isBlank: isBlank + isBlank: isBlank, + customSortAsc: customSortAsc, + customSortDesc: customSortDesc }; var defaultOptions = { @@ -2713,10 +2734,17 @@ this.d3.maptable = (function () { this.maptable = maptable; this.options = options; + this.sorting = []; // initialize sort array + if (this.options.defaultSorting) { - if (Array.isArray(this.options.defaultSorting) && this.options.defaultSorting.length === 2) { + if (Array.isArray(this.options.defaultSorting) && (this.options.defaultSorting.length === 2 || this.options.defaultSorting.length === 3)) { + // handle case for second-order and third-order sort options this.sorting = this.options.defaultSorting; + } else if (Array.isArray(this.options.defaultSorting) && this.options.defaultSorting.length >= 4) { + // handle case for more than three default sort options + this.sorting = this.options.defaultSorting.slice(0, 3); } else { + // handle case for single default sort option this.sorting = [this.options.defaultSorting]; } this.sorting.forEach(function (s) { @@ -2846,7 +2874,6 @@ this.d3.maptable = (function () { // Apply Sort this.applySort(); - var tableData = this.maptable.data; if (this.options.distinctBy) { tableData = d3.nest().key(function (d) { @@ -2864,11 +2891,13 @@ this.d3.maptable = (function () { // Update var uniqueCollapsedRows = []; - this.body.selectAll('tr').data(tableData).attr('class', function (row) { + this.sortIndex = 0; + this.body.selectAll('tr').data(tableData).attr('class', function (row, index) { + var enableSeparator = _this2.isEndOfPrimarySort(tableData, index); if (_this2.options.rowClassName) { - return 'line ' + _this2.options.rowClassName(row); + return 'line ' + _this2.options.rowClassName(row) + ' ' + (enableSeparator ? 'bold' : ''); } - return 'line'; + return 'line ' + (enableSeparator ? 'bold' : ''); }).html(function (row) { var tds = ''; _this2.activeColumns.forEach(function (columnKey) { @@ -2892,6 +2921,14 @@ this.d3.maptable = (function () { tds += ''; }); return tds; + }).append('span').attr('class', 'table-sort-sn ' + (!this.options.dataSN || this.options.dataSN && !this.options.dataSN.enabled || this.sorting && this.sorting.length <= 1 ? 'display-none' : '') + ' ').html(function (row, index) { + var isSortGroupEnd = _this2.isEndOfPrimarySort(tableData, index - 1); + if (isSortGroupEnd) { + _this2.sortIndex = 1; + } else { + _this2.sortIndex += 1; + } + return '' + _this2.sortIndex; }); // On render @@ -2908,13 +2945,14 @@ this.d3.maptable = (function () { for (var i = 0; i < sortableColums.length; i += 1) { sortableColums[i].setAttribute('class', 'mt-table-sortable'); } - this.sorting.forEach(function (column) { - _this3.container.querySelector('#column_header_' + utils.sanitizeKey(column.key)).setAttribute('class', 'mt-table-sortable sort_' + column.mode); + this.sorting.forEach(function (column, index) { + _this3.container.querySelector('#column_header_' + utils.sanitizeKey(column.key)).setAttribute('class', 'mt-table-sortable sort_' + column.mode + ' sort_order_' + (index + 1)); }); this.maptable.data = this.maptable.data.sort(function (a, b) { var compareBool = false; _this3.sorting.forEach(function (column) { var d3SortMode = column.mode === 'asc' ? d3.ascending : d3.descending; + var d3CustomSortMode = column.mode === 'asc' ? utils.customSortAsc : utils.customSortDesc; var columnDetails = _this3.maptable.columnDetails[column.key]; var el1 = a[column.key]; var el2 = b[column.key]; @@ -2933,7 +2971,12 @@ this.d3.maptable = (function () { el1 = el1.toLowerCase(); el2 = el2.toLowerCase(); } - compareBool = compareBool || d3SortMode(el1, el2); + + if (columnDetails.filterInputType === 'months' || columnDetails.filterInputType === 'days') { + compareBool = compareBool || d3CustomSortMode(columnDetails.filterInputType, el1, el2); + } else { + compareBool = compareBool || d3SortMode(el1, el2); + } }); return compareBool; }); @@ -2954,7 +2997,12 @@ this.d3.maptable = (function () { if (sortIndex === -1) { sortValue.mode = 'desc'; if (d3.event && d3.event.shiftKey) { - this.sorting[1] = sortValue; + // FIFO - pop last sort element from array after third-order-sorting + if (this.sorting.length === 3) { + this.sorting.pop(); + } + // FIFO - push new sort element to array + this.sorting.unshift(sortValue); } else { this.sorting = [sortValue]; } @@ -2967,12 +3015,57 @@ this.d3.maptable = (function () { } if (!d3.event.shiftKey) { this.sorting = [this.sorting[sortIndex]]; + } else { + // FIFO - set latest clicked column key as first-order-sorting + this.reOrderSorting(sortIndex, 0); } } - this.saveState(); this.render(); } + + /** + * Util for changing position of sorting array elements + * @param from: from position index + * @param to: to position index + */ + + }, { + key: 'reOrderSorting', + value: function reOrderSorting(from, to) { + if (to === from) return; + + var target = this.sorting[from]; + var increment = to < from ? -1 : 1; + + for (var k = from; k != to; k += increment) { + this.sorting[k] = this.sorting[k + increment]; + } + this.sorting[to] = target; + } + + /** + * Util for finding end of primary sort data + * @param data: table data + * @param index: current row index + */ + + }, { + key: 'isEndOfPrimarySort', + value: function isEndOfPrimarySort(data, index) { + // check if data group separator is enabled from passed options + if (!this.options.dataGroupSeparator || this.options.dataGroupSeparator && !this.options.dataGroupSeparator.enabled) return false; + // check if multi-order-sort + if (this.sorting && this.sorting.length <= 1) return false; + + var primarySort = this.sorting[0].key || ''; + if (data[index] && data[index + 1]) { + if (data[index][primarySort] && data[index + 1][primarySort]) { + return data[index][primarySort].toLowerCase() !== data[index + 1][primarySort].toLowerCase(); + } + } + return false; + } }]); return Table; }(); @@ -3253,6 +3346,8 @@ this.d3.maptable = (function () { }, { key: 'setColumnDetails', value: function setColumnDetails() { + var _this5 = this; + var that = this; if (that.rawData.length === 0) { return; @@ -3265,9 +3360,10 @@ this.d3.maptable = (function () { defaultColumns[k] = { title: utils.keyToTile(k), filterMethod: isNumber ? 'compare' : 'field', - filterInputType: isNumber ? 'number' : 'text', + filterInputType: isNumber ? 'number' : _this5.options.columns[k] && _this5.options.columns[k].filterInputType ? _this5.options.columns[k].filterInputType : 'text', sorting: true }; + if (isNumber) { defaultColumns[k].dataParse = function (val) { return parseFloat(val); @@ -3275,7 +3371,6 @@ this.d3.maptable = (function () { } }); that.columnDetails = utils.extendRecursive(defaultColumns, this.options.columns); - // add isVirtual to columns details Object.keys(that.columnDetails).forEach(function (k) { that.columnDetails[k].isVirtual = typeof that.columnDetails[k].virtual === 'function'; diff --git a/src/components/Table.js b/src/components/Table.js index 6306770..397571c 100644 --- a/src/components/Table.js +++ b/src/components/Table.js @@ -10,10 +10,17 @@ export default class Table { constructor(maptable, options) { this.maptable = maptable; this.options = options; + this.sorting = []; // initialize sort array + if (this.options.defaultSorting) { - if (Array.isArray(this.options.defaultSorting) && this.options.defaultSorting.length === 2) { + if (Array.isArray(this.options.defaultSorting) && (this.options.defaultSorting.length === 2 || this.options.defaultSorting.length === 3)) { + // handle case for second-order and third-order sort options this.sorting = this.options.defaultSorting; + } else if (Array.isArray(this.options.defaultSorting) && this.options.defaultSorting.length >= 4) { + // handle case for more than three default sort options + this.sorting = this.options.defaultSorting.slice(0, 3); } else { + // handle case for single default sort option this.sorting = [this.options.defaultSorting]; } this.sorting.forEach((s) => { @@ -49,8 +56,7 @@ export default class Table { this.body = this.node.append('tbody'); if (this.options.show) { - const arrayDiff = this.options.show - .filter((i) => Object.keys(this.maptable.columnDetails).indexOf(i) < 0); + const arrayDiff = this.options.show.filter((i) => Object.keys(this.maptable.columnDetails).indexOf(i) < 0); if (arrayDiff.length > 0) { throw new Error(`MapTable: invalid columns "${arrayDiff.join(', ')}"`); } @@ -79,8 +85,7 @@ export default class Table { .enter() .append('tr') .selectAll('th') - .data(this.activeColumns.map((k) => ( - utils.extendRecursive({ key: k }, this.maptable.columnDetails[k])))) + .data(this.activeColumns.map((k) => utils.extendRecursive({ key: k }, this.maptable.columnDetails[k]))) .enter() .append('th') .attr('class', (d) => { @@ -137,7 +142,6 @@ export default class Table { render() { // Apply Sort this.applySort(); - let tableData = this.maptable.data; if (this.options.distinctBy) { tableData = d3 @@ -155,14 +159,16 @@ export default class Table { // Update const uniqueCollapsedRows = []; + this.sortIndex = 0; this.body .selectAll('tr') .data(tableData) - .attr('class', (row) => { + .attr('class', (row, index) => { + const enableSeparator = this.isEndOfPrimarySort(tableData, index); if (this.options.rowClassName) { - return `line ${this.options.rowClassName(row)}`; + return `line ${this.options.rowClassName(row)} ${enableSeparator ? 'bold' : ''}`; } - return 'line'; + return `line ${enableSeparator ? 'bold' : ''}`; }) .html((row) => { let tds = ''; @@ -174,11 +180,7 @@ export default class Table { } tds += '>'; - if (!( - this.options.collapseRowsBy.indexOf(columnKey) !== -1 - && uniqueCollapsedRows[columnKey] - && uniqueCollapsedRows[columnKey] === row[columnKey]) - ) { + if (!(this.options.collapseRowsBy.indexOf(columnKey) !== -1 && uniqueCollapsedRows[columnKey] && uniqueCollapsedRows[columnKey] === row[columnKey])) { if (column.cellContent) { tds += column.cellContent(row); } else if (column.virtual) { @@ -191,6 +193,17 @@ export default class Table { tds += ''; }); return tds; + }) + .append('span') + .attr('class', `table-sort-sn ${!this.options.dataSN || (this.options.dataSN && !this.options.dataSN.enabled) || (this.sorting && this.sorting.length <= 1) ? 'display-none' : ''} `) + .html((row, index) => { + const isSortGroupEnd = this.isEndOfPrimarySort(tableData, index - 1); + if (isSortGroupEnd) { + this.sortIndex = 1; + } else { + this.sortIndex += 1; + } + return `${this.sortIndex}`; }); // On render @@ -204,13 +217,14 @@ export default class Table { for (let i = 0; i < sortableColums.length; i += 1) { sortableColums[i].setAttribute('class', 'mt-table-sortable'); } - this.sorting.forEach((column) => { - this.container.querySelector(`#column_header_${utils.sanitizeKey(column.key)}`).setAttribute('class', `mt-table-sortable sort_${column.mode}`); + this.sorting.forEach((column, index) => { + this.container.querySelector(`#column_header_${utils.sanitizeKey(column.key)}`).setAttribute('class', `mt-table-sortable sort_${column.mode} sort_order_${index + 1}`); }); this.maptable.data = this.maptable.data.sort((a, b) => { let compareBool = false; this.sorting.forEach((column) => { const d3SortMode = column.mode === 'asc' ? d3.ascending : d3.descending; + const d3CustomSortMode = column.mode === 'asc' ? utils.customSortAsc : utils.customSortDesc; const columnDetails = this.maptable.columnDetails[column.key]; let el1 = a[column.key]; let el2 = b[column.key]; @@ -229,7 +243,12 @@ export default class Table { el1 = el1.toLowerCase(); el2 = el2.toLowerCase(); } - compareBool = compareBool || d3SortMode(el1, el2); + + if (columnDetails.filterInputType === 'months' || columnDetails.filterInputType === 'days') { + compareBool = compareBool || d3CustomSortMode(columnDetails.filterInputType, el1, el2); + } else { + compareBool = compareBool || d3SortMode(el1, el2); + } }); return compareBool; }); @@ -245,7 +264,12 @@ export default class Table { if (sortIndex === -1) { sortValue.mode = 'desc'; if (d3.event && d3.event.shiftKey) { - this.sorting[1] = sortValue; + // FIFO - pop last sort element from array after third-order-sorting + if (this.sorting.length === 3) { + this.sorting.pop(); + } + // FIFO - push new sort element to array + this.sorting.unshift(sortValue); } else { this.sorting = [sortValue]; } @@ -258,10 +282,49 @@ export default class Table { } if (!d3.event.shiftKey) { this.sorting = [this.sorting[sortIndex]]; + } else { + // FIFO - set latest clicked column key as first-order-sorting + this.reOrderSorting(sortIndex, 0); } } - this.saveState(); this.render(); } + + /** + * Util for changing position of sorting array elements + * @param from: from position index + * @param to: to position index + */ + reOrderSorting(from, to) { + if (to === from) return; + + var target = this.sorting[from]; + var increment = to < from ? -1 : 1; + + for (var k = from; k != to; k += increment) { + this.sorting[k] = this.sorting[k + increment]; + } + this.sorting[to] = target; + } + + /** + * Util for finding end of primary sort data + * @param data: table data + * @param index: current row index + */ + isEndOfPrimarySort(data, index) { + // check if data group separator is enabled from passed options + if (!this.options.dataGroupSeparator || (this.options.dataGroupSeparator && !this.options.dataGroupSeparator.enabled)) return false; + // check if multi-order-sort + if (this.sorting && this.sorting.length <= 1) return false; + + const primarySort = this.sorting[0].key || ''; + if (data[index] && data[index + 1]) { + if (data[index][primarySort] && data[index + 1][primarySort]) { + return data[index][primarySort].toLowerCase() !== data[index + 1][primarySort].toLowerCase(); + } + } + return false; + } } diff --git a/src/maptable.css b/src/maptable.css index 8ccda09..c068658 100755 --- a/src/maptable.css +++ b/src/maptable.css @@ -131,4 +131,26 @@ .mt-header-fixed { position: sticky; +} + +.bold { + border-bottom: 1.5px solid rgb(153, 152, 152); +} + +.table-sort-sn { + position: absolute; + left: 0; + width:38px; + height: 38px; + display: flex; + align-items: center; + justify-content: flex-end; + padding-right: 7px; + margin-left: -38px; + opacity: 0.2; + color: #000; +} + +.display-none { + display: none; } \ No newline at end of file diff --git a/src/maptable.js b/src/maptable.js index 68752fc..6887718 100644 --- a/src/maptable.js +++ b/src/maptable.js @@ -58,7 +58,7 @@ export default class MapTable { // Map wrapper const mapWrapper = document.createElement('div'); mapWrapper.setAttribute('class', 'mt-map-container'); - const isIE = (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0); + const isIE = navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0; if (this.options.map.heatmap && isIE) { mapWrapper.innerHTML = '
The heatmap feature is not supported with Internet Explorer.
Please use another modern browser to see this map.
'; this.node.insertBefore(mapWrapper, this.node.firstChild); @@ -167,7 +167,7 @@ export default class MapTable { */ parseState(stateName) { const params = document.location.href.replace(/%21mt/g, '!mt').split(`!mt-${stateName}=`); - return (params[1]) ? decodeURIComponent(params[1].split('!mt')[0]) : null; + return params[1] ? decodeURIComponent(params[1].split('!mt')[0]) : null; } /** @@ -202,7 +202,7 @@ export default class MapTable { Object.keys(this.state).forEach((k) => { if (!this.state[k]) return; let stateValue = this.state[k]; - if (typeof (this.state[k]) === 'object') { + if (typeof this.state[k] === 'object') { if (!Object.keys(this.state[k]).length) return; stateValue = JSON.stringify(this.state[k]); } @@ -223,10 +223,7 @@ export default class MapTable { if (this.map) { this.map.render(); // On complete - if (!this.firstExecution - && this.options.map.onComplete - && this.options.map.onComplete.constructor === Function - ) { + if (!this.firstExecution && this.options.map.onComplete && this.options.map.onComplete.constructor === Function) { this.options.map.onComplete.bind(this)(); } } @@ -234,19 +231,13 @@ export default class MapTable { if (this.table) { this.table.render(); // On complete - if (!this.firstExecution - && this.options.table.onComplete - && this.options.table.onComplete.constructor === Function - ) { + if (!this.firstExecution && this.options.table.onComplete && this.options.table.onComplete.constructor === Function) { this.options.table.onComplete.bind(this)(); } } // On complete - if (!this.firstExecution - && this.options.onComplete - && this.options.onComplete.constructor === Function - ) { + if (!this.firstExecution && this.options.onComplete && this.options.onComplete.constructor === Function) { this.options.onComplete.bind(this)(); } this.firstExecution = true; @@ -261,22 +252,22 @@ export default class MapTable { Object.keys(that.rawData[0]).forEach((k) => { const patternNumber = /^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$/; - const isNumber = (patternNumber.test(that.rawData[0][k])); + const isNumber = patternNumber.test(that.rawData[0][k]); defaultColumns[k] = { title: utils.keyToTile(k), - filterMethod: (isNumber) ? 'compare' : 'field', - filterInputType: (isNumber) ? 'number' : 'text', + filterMethod: isNumber ? 'compare' : 'field', + filterInputType: isNumber ? 'number' : this.options.columns[k] && this.options.columns[k].filterInputType ? this.options.columns[k].filterInputType : 'text', sorting: true, }; + if (isNumber) { defaultColumns[k].dataParse = (val) => parseFloat(val); } }); that.columnDetails = utils.extendRecursive(defaultColumns, this.options.columns); - // add isVirtual to columns details Object.keys(that.columnDetails).forEach((k) => { - that.columnDetails[k].isVirtual = (typeof (that.columnDetails[k].virtual) === 'function'); + that.columnDetails[k].isVirtual = typeof that.columnDetails[k].virtual === 'function'; }); } } diff --git a/src/utils.js b/src/utils.js index af994d5..a0353e4 100644 --- a/src/utils.js +++ b/src/utils.js @@ -36,7 +36,7 @@ function extendRecursive() { const dst = {}; let src; const args = [].splice.call(arguments, 0); - const toString = ({}).toString; + const toString = {}.toString; while (args.length > 0) { src = args.splice(0, 1)[0]; @@ -59,15 +59,12 @@ function keyToTile(k) { } function sanitizeKey(k) { - return k.toLowerCase() - .replace(/ /g, '_') - .replace(/"/g, '') - .replace(/'/g, ''); + return k.toLowerCase().replace(/ /g, '_').replace(/"/g, '').replace(/'/g, ''); } function toNumber(str) { if (!str || str === '') return null; - const resStr = str.toString().replace(/[^0-9.]+|\s+/gmi, ''); + const resStr = str.toString().replace(/[^0-9.]+|\s+/gim, ''); if (resStr !== '') return Number(resStr); return null; } @@ -102,7 +99,26 @@ const formatDate = (d, zone) => { return newDate.toISOString().split('T')[1].substr(0, 5); }; -const isBlank = (str) => (str === null || str === '' || str === undefined); +const isBlank = (str) => str === null || str === '' || str === undefined; + +const customSortOrders = { + days: ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'], + months: ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'], +}; + +const customSortAsc = (type, d1, d2) => { + const elem1 = d1.toLowerCase(); + const elem2 = d2.toLowerCase(); + const customSortOrder = customSortOrders[type] || []; + return customSortOrder.indexOf(elem1) - customSortOrder.indexOf(elem2); +}; + +const customSortDesc = (type, d1, d2) => { + const elem1 = d1.toLowerCase(); + const elem2 = d2.toLowerCase(); + const customSortOrder = customSortOrders[type] || []; + return customSortOrder.indexOf(elem2) - customSortOrder.indexOf(elem1); +}; export default { rangeToBool, @@ -115,4 +131,6 @@ export default { uniqueValues, formatDate, isBlank, + customSortAsc, + customSortDesc, }; From 1f096b00903b18930912f55c6220c48c0e32469d Mon Sep 17 00:00:00 2001 From: Anjal Date: Wed, 12 Jun 2024 13:28:28 +0545 Subject: [PATCH 2/4] customSortOrder now definable from outside --- docs/examples/table-with-multi-column-sort.html | 5 +++-- docs/maptable.js | 16 ++++++++++------ src/components/Table.js | 6 ++++-- src/utils.js | 8 ++++---- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/docs/examples/table-with-multi-column-sort.html b/docs/examples/table-with-multi-column-sort.html index c4767c4..f0ed561 100644 --- a/docs/examples/table-with-multi-column-sort.html +++ b/docs/examples/table-with-multi-column-sort.html @@ -65,8 +65,9 @@ .table({ show: ['name', 'position', 'office', 'month', 'day', 'links'], dataGroupSeparator: { enabled: true }, - dataSN: { enabled: true }, - defaultSorting: [{key:'month',mode:'desc'},{key:'name',mode:'asc'},{key:'position',mode:'asc'}], + dataCountIndicator: { enabled: true }, + defaultSorting: [{key:'month',mode:'desc'},{key:'day',mode:'asc'}], + customSortOrder: [{key:'day',order:['monday', 'tuesday', 'wednesday', 'thursday', 'friday','saturday','sunday',]},{key:'month',order:['february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december','january']}] }) .render(); diff --git a/docs/maptable.js b/docs/maptable.js index cab99ee..ad58ed0 100644 --- a/docs/maptable.js +++ b/docs/maptable.js @@ -157,17 +157,17 @@ this.d3.maptable = (function () { months: ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'] }; - var customSortAsc = function customSortAsc(type, d1, d2) { + var customSortAsc = function customSortAsc(type, d1, d2, sortOrder) { var elem1 = d1.toLowerCase(); var elem2 = d2.toLowerCase(); - var customSortOrder = customSortOrders[type] || []; + var customSortOrder = sortOrder.length === 0 ? customSortOrders[type] || [] : sortOrder; return customSortOrder.indexOf(elem1) - customSortOrder.indexOf(elem2); }; - var customSortDesc = function customSortDesc(type, d1, d2) { + var customSortDesc = function customSortDesc(type, d1, d2, sortOrder) { var elem1 = d1.toLowerCase(); var elem2 = d2.toLowerCase(); - var customSortOrder = customSortOrders[type] || []; + var customSortOrder = sortOrder.length === 0 ? customSortOrders[type] || [] : sortOrder; return customSortOrder.indexOf(elem2) - customSortOrder.indexOf(elem1); }; @@ -2921,7 +2921,7 @@ this.d3.maptable = (function () { tds += ''; }); return tds; - }).append('span').attr('class', 'table-sort-sn ' + (!this.options.dataSN || this.options.dataSN && !this.options.dataSN.enabled || this.sorting && this.sorting.length <= 1 ? 'display-none' : '') + ' ').html(function (row, index) { + }).append('span').attr('class', 'table-sort-sn ' + (!this.options.dataCountIndicator || this.options.dataCountIndicator && !this.options.dataCountIndicator.enabled || this.sorting && this.sorting.length <= 1 ? 'display-none' : '') + ' ').html(function (row, index) { var isSortGroupEnd = _this2.isEndOfPrimarySort(tableData, index - 1); if (isSortGroupEnd) { _this2.sortIndex = 1; @@ -2973,7 +2973,11 @@ this.d3.maptable = (function () { } if (columnDetails.filterInputType === 'months' || columnDetails.filterInputType === 'days') { - compareBool = compareBool || d3CustomSortMode(columnDetails.filterInputType, el1, el2); + var currentCustomSortValues = _this3.options.customSortOrder && _this3.options.customSortOrder.filter(function (cs) { + return cs.key === column.key; + }); + var currentCustomSortOrder = currentCustomSortValues && currentCustomSortValues.length !== 0 ? currentCustomSortValues[0].order || [] : []; + compareBool = compareBool || d3CustomSortMode(columnDetails.filterInputType, el1, el2, currentCustomSortOrder); } else { compareBool = compareBool || d3SortMode(el1, el2); } diff --git a/src/components/Table.js b/src/components/Table.js index 397571c..067f99a 100644 --- a/src/components/Table.js +++ b/src/components/Table.js @@ -195,7 +195,7 @@ export default class Table { return tds; }) .append('span') - .attr('class', `table-sort-sn ${!this.options.dataSN || (this.options.dataSN && !this.options.dataSN.enabled) || (this.sorting && this.sorting.length <= 1) ? 'display-none' : ''} `) + .attr('class', `table-sort-sn ${!this.options.dataCountIndicator || (this.options.dataCountIndicator && !this.options.dataCountIndicator.enabled) || (this.sorting && this.sorting.length <= 1) ? 'display-none' : ''} `) .html((row, index) => { const isSortGroupEnd = this.isEndOfPrimarySort(tableData, index - 1); if (isSortGroupEnd) { @@ -245,7 +245,9 @@ export default class Table { } if (columnDetails.filterInputType === 'months' || columnDetails.filterInputType === 'days') { - compareBool = compareBool || d3CustomSortMode(columnDetails.filterInputType, el1, el2); + const currentCustomSortValues = this.options.customSortOrder && this.options.customSortOrder.filter((cs) => cs.key === column.key); + const currentCustomSortOrder = currentCustomSortValues && currentCustomSortValues.length !== 0 ? currentCustomSortValues[0].order || [] : []; + compareBool = compareBool || d3CustomSortMode(columnDetails.filterInputType, el1, el2, currentCustomSortOrder); } else { compareBool = compareBool || d3SortMode(el1, el2); } diff --git a/src/utils.js b/src/utils.js index a0353e4..9c1a55a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -106,17 +106,17 @@ const customSortOrders = { months: ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'], }; -const customSortAsc = (type, d1, d2) => { +const customSortAsc = (type, d1, d2, sortOrder) => { const elem1 = d1.toLowerCase(); const elem2 = d2.toLowerCase(); - const customSortOrder = customSortOrders[type] || []; + const customSortOrder = sortOrder.length === 0 ? customSortOrders[type] || [] : sortOrder; return customSortOrder.indexOf(elem1) - customSortOrder.indexOf(elem2); }; -const customSortDesc = (type, d1, d2) => { +const customSortDesc = (type, d1, d2, sortOrder) => { const elem1 = d1.toLowerCase(); const elem2 = d2.toLowerCase(); - const customSortOrder = customSortOrders[type] || []; + const customSortOrder = sortOrder.length === 0 ? customSortOrders[type] || [] : sortOrder; return customSortOrder.indexOf(elem2) - customSortOrder.indexOf(elem1); }; From 8e0d35e8f8907ba9ddb942428e9b499ba4b38b88 Mon Sep 17 00:00:00 2001 From: Anjal Date: Wed, 12 Jun 2024 13:32:32 +0545 Subject: [PATCH 3/4] filterRangeSelect value not found issue fix --- docs/maptable.js | 25 ++++++------ src/components/Filters.js | 80 +++++++++++++++++---------------------- 2 files changed, 49 insertions(+), 56 deletions(-) diff --git a/docs/maptable.js b/docs/maptable.js index ad58ed0..d1039ef 100644 --- a/docs/maptable.js +++ b/docs/maptable.js @@ -2349,18 +2349,21 @@ this.d3.maptable = (function () { var filterOutput = [columnDetails.filterMethod]; if (columnDetails.filterMethod === 'compare') { var filterRangeSelect = element.querySelector('.mt-filter-range'); - filterOutput[1] = filterRangeSelect.value; - if (filterRangeSelect.value !== 'any') { - if (filterRangeSelect.value === 'BETWEEN') { - var filterValueMin = element.querySelector('.mt-filter-value-min').value; - var filterValueMax = element.querySelector('.mt-filter-value-max').value; - if (filterValueMin !== '' && filterValueMax === '') { - filterOutput[2] = filterValueMin; - filterOutput[3] = filterValueMax; + if (filterRangeSelect) { + filterOutput[1] = filterRangeSelect.value; + + if (filterRangeSelect.value !== 'any') { + if (filterRangeSelect.value === 'BETWEEN') { + var filterValueMin = element.querySelector('.mt-filter-value-min').value; + var filterValueMax = element.querySelector('.mt-filter-value-max').value; + if (filterValueMin !== '' && filterValueMax === '') { + filterOutput[2] = filterValueMin; + filterOutput[3] = filterValueMax; + } + } else { + var filterValue = element.querySelector('.mt-filter-value-min').value; + filterOutput[2] = filterValue; } - } else { - var filterValue = element.querySelector('.mt-filter-value-min').value; - filterOutput[2] = filterValue; } } } else if (columnDetails.filterMethod === 'field' || columnDetails.filterMethod === 'dropdown') { diff --git a/src/components/Filters.js b/src/components/Filters.js index 9edb19b..ff18da4 100644 --- a/src/components/Filters.js +++ b/src/components/Filters.js @@ -7,8 +7,7 @@ export default class Filters { this.criteria = []; if (this.options.show) { - const arrayDiff = this.options.show - .filter((i) => Object.keys(this.maptable.columnDetails).indexOf(i) < 0); + const arrayDiff = this.options.show.filter((i) => Object.keys(this.maptable.columnDetails).indexOf(i) < 0); if (arrayDiff.length > 0) { throw new Error(`MapTable: invalid columns "${arrayDiff.join(', ')}"`); } @@ -135,22 +134,24 @@ export default class Filters { const filterOutput = [columnDetails.filterMethod]; if (columnDetails.filterMethod === 'compare') { const filterRangeSelect = element.querySelector('.mt-filter-range'); - filterOutput[1] = filterRangeSelect.value; - if (filterRangeSelect.value !== 'any') { - if (filterRangeSelect.value === 'BETWEEN') { - const filterValueMin = element.querySelector('.mt-filter-value-min').value; - const filterValueMax = element.querySelector('.mt-filter-value-max').value; - if (filterValueMin !== '' && filterValueMax === '') { - filterOutput[2] = filterValueMin; - filterOutput[3] = filterValueMax; + if (filterRangeSelect) { + filterOutput[1] = filterRangeSelect.value; + + if (filterRangeSelect.value !== 'any') { + if (filterRangeSelect.value === 'BETWEEN') { + const filterValueMin = element.querySelector('.mt-filter-value-min').value; + const filterValueMax = element.querySelector('.mt-filter-value-max').value; + if (filterValueMin !== '' && filterValueMax === '') { + filterOutput[2] = filterValueMin; + filterOutput[3] = filterValueMax; + } + } else { + const filterValue = element.querySelector('.mt-filter-value-min').value; + filterOutput[2] = filterValue; } - } else { - const filterValue = element.querySelector('.mt-filter-value-min').value; - filterOutput[2] = filterValue; } } - } else if (columnDetails.filterMethod === 'field' - || columnDetails.filterMethod === 'dropdown') { + } else if (columnDetails.filterMethod === 'field' || columnDetails.filterMethod === 'dropdown') { filterOutput[1] = ''; const filterValue = element.querySelector('.mt-filter-value').value; filterOutput[2] = filterValue; @@ -171,8 +172,7 @@ export default class Filters { Object.keys(criteria).forEach((filterName) => { this.create(filterName); const criterion = criteria[filterName]; - const row = document - .querySelector(`#mt-filters-elements [data-mt-filter-name="${filterName}"]`); + const row = document.querySelector(`#mt-filters-elements [data-mt-filter-name="${filterName}"]`); if (row) { if (criterion[0] === 'compare') { row.querySelector('.mt-filter-range').value = criterion[1]; @@ -235,11 +235,10 @@ export default class Filters { line += `${filterValue}`; } } - } else if (columnDetails.filterMethod === 'field' - || columnDetails.filterMethod === 'dropdown') { + } else if (columnDetails.filterMethod === 'field' || columnDetails.filterMethod === 'dropdown') { const filterValue = element.querySelector('.mt-filter-value').value; if (filterValue === '') continue; - const separatorWord = (columnDetails.filterMethod === 'field') ? 'contains' : 'is'; + const separatorWord = columnDetails.filterMethod === 'field' ? 'contains' : 'is'; line += `${columnDetails.title} ${separatorWord} ${filterValue}`; } @@ -281,7 +280,7 @@ export default class Filters { filterNameSelect.setAttribute('class', 'mt-filter-name form-control form-control-inline'); utils.appendOptions( filterNameSelect, - possibleFilters.map((f) => ({ text: f.title, value: f.key })), + possibleFilters.map((f) => ({ text: f.title, value: f.key })) ); filterNameSelect.value = filterName; @@ -296,7 +295,7 @@ export default class Filters { // Filter verb const filterVerb = document.createElement('span'); - filterVerb.innerText = (columnDetails.filterMethod === 'field') ? ' contains ' : ' is '; + filterVerb.innerText = columnDetails.filterMethod === 'field' ? ' contains ' : ' is '; rowNode.appendChild(filterVerb); // Filter range @@ -304,7 +303,10 @@ export default class Filters { if (columnDetails.filterMethod !== 'field' && columnDetails.filterMethod !== 'dropdown') { filterRange = document.createElement('select'); filterRange.setAttribute('class', 'mt-filter-range form-control form-control-inline'); - utils.appendOptions(filterRange, ['any', '=', '≠', '<', '>', '≤', '≥', 'BETWEEN'].map((v) => ({ text: v, value: v }))); + utils.appendOptions( + filterRange, + ['any', '=', '≠', '<', '>', '≤', '≥', 'BETWEEN'].map((v) => ({ text: v, value: v })) + ); filterRange.addEventListener('change', function () { that.handleRangeChange(this); }); @@ -322,10 +324,7 @@ export default class Filters { if (columnDetails.filterMethod === 'compare') { ['min', 'max'].forEach((val, i) => { const filterInput = document.createElement('input'); - filterInput.setAttribute( - 'class', - `form-control form-control-inline mt-filter-value-${val}`, - ); + filterInput.setAttribute('class', `form-control form-control-inline mt-filter-value-${val}`); filterInput.setAttribute('type', columnDetails.filterInputType); filterInput.addEventListener('keyup', this.maptable.render.bind(this.maptable)); filterInput.addEventListener('change', this.maptable.render.bind(this.maptable)); @@ -349,7 +348,9 @@ export default class Filters { const filterSelect = document.createElement('select'); filterSelect.setAttribute('class', 'form-control form-control-inline mt-filter-value'); - const uniqueValues = d3.nest().key((d) => d[filterName]) + const uniqueValues = d3 + .nest() + .key((d) => d[filterName]) .sortKeys(d3.ascending) .entries(this.maptable.rawData); @@ -391,11 +392,7 @@ export default class Filters { getPossibleFilters(except) { return Object.keys(this.maptable.columnDetails) .map((k) => utils.extendRecursive({ key: k }, this.maptable.columnDetails[k])) - .filter((v) => (this.activeColumns.indexOf(v.key) !== -1) - && ( - (except && except === v.key) - || (this.criteria.indexOf(v.key) === -1 && v.filterMethod && !v.isVirtual) - )); + .filter((v) => this.activeColumns.indexOf(v.key) !== -1 && ((except && except === v.key) || (this.criteria.indexOf(v.key) === -1 && v.filterMethod && !v.isVirtual))); } filterData() { @@ -425,15 +422,9 @@ export default class Filters { const filterValueMin = rowNode.querySelector('.mt-filter-value-min').value; const filterValueMax = rowNode.querySelector('.mt-filter-value-max').value; if (filterValueMin === '' || filterValueMax === '') continue; - if (fmt - && (fmt(d[filterName]) < fmt(filterValueMin) - || fmt(d[filterName]) > fmt(filterValueMax)) - ) { + if (fmt && (fmt(d[filterName]) < fmt(filterValueMin) || fmt(d[filterName]) > fmt(filterValueMax))) { matched = false; - } else if ( - parseInt(d[filterName], 10) < parseInt(filterValueMin, 10) - || parseInt(d[filterName], 10) > parseInt(filterValueMax, 10) - ) { + } else if (parseInt(d[filterName], 10) < parseInt(filterValueMin, 10) || parseInt(d[filterName], 10) > parseInt(filterValueMax, 10)) { matched = false; } } else { @@ -468,7 +459,7 @@ export default class Filters { filterNameSelect.innerHTML = ''; utils.appendOptions( filterNameSelect, - possibleFilters.map((f) => ({ text: f.title, value: f.key })), + possibleFilters.map((f) => ({ text: f.title, value: f.key })) ); filterNameSelect.value = filterName; } @@ -479,9 +470,8 @@ export default class Filters { } // Check if we reached the maximum of allowed filters - const disableNewFilter = (!this.getPossibleFilters().length); - this.node.querySelector('#mt-filters-new').style.visibility = disableNewFilter - ? 'hidden' : 'visible'; + const disableNewFilter = !this.getPossibleFilters().length; + this.node.querySelector('#mt-filters-new').style.visibility = disableNewFilter ? 'hidden' : 'visible'; } toggle() { From 553fc66a9d8bc3eeb72574de0da918cafb494442 Mon Sep 17 00:00:00 2001 From: Anjal Date: Wed, 12 Jun 2024 14:06:42 +0545 Subject: [PATCH 4/4] readme file updated --- README.md | 572 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 295 insertions(+), 277 deletions(-) diff --git a/README.md b/README.md index ec0e51e..1ea027f 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -MapTable -======== +# MapTable [![GitHub stars](https://img.shields.io/github/stars/Packet-Clearing-House/maptable.svg?style=social&label=Star)]() [![GitHub release](https://img.shields.io/github/release/Packet-Clearing-House/maptable.svg)]() [![license](https://img.shields.io/github/license/Packet-Clearing-House/maptable.svg)]() @@ -49,8 +48,8 @@ You can also browse [other code samples and **examples**](https://packet-clearin ## Dependencies - [D3.js](https://d3js.org/) -- TopoJSON*: [homepage](https://github.com/mbostock/topojson) or [download (cdnjs)](https://cdnjs.com/libraries/topojson) -- FileSaver.js*: [homepage](https://github.com/eligrey/FileSaver.js) or [download (cdnjs)](https://cdnjs.com/libraries/FileSaver.js) - only used if you want to do client side SVG export +- TopoJSON\*: [homepage](https://github.com/mbostock/topojson) or [download (cdnjs)](https://cdnjs.com/libraries/topojson) +- FileSaver.js\*: [homepage](https://github.com/eligrey/FileSaver.js) or [download (cdnjs)](https://cdnjs.com/libraries/FileSaver.js) - only used if you want to do client side SVG export \* Only used if you need a map @@ -59,18 +58,23 @@ You can also browse [other code samples and **examples**](https://packet-clearin Here is minimum amount of HTML to render a MapTable with Map, Filter and Table. ```html -
- - - - +
+ + + + + + + + ``` @@ -79,8 +83,7 @@ MapTable is available on cdnjs.com. Remember though, cool kids concatenate their If you want to style the MapTable elements with some existing styles, you can prepend the above HTML with: ```html - - + ``` ## Declaring MapTable elements @@ -88,16 +91,16 @@ If you want to style the MapTable elements with some existing styles, you can pr To create a visualization (Map or/and Table or/and Filters) of your dataset, you need to provide first the container of your visualization ```html -
+
``` The default order of the 3 components is `Map`, `Filters` and `Table`. If you want to place the components in a different order, you can put them on the main container: ```html -
-
-
-
+
+
+
+
``` @@ -123,13 +126,14 @@ The MapTable `viz` declaration in the above example is a chain of functions. The - [viz.table(tableOptions)](#table) with `tableOptions` as a JS dictionary. You can add/remove it of you want a table on your visualization. - [viz.render([onComplete])](#render) that closes the chain and renders the visualization. Don't forget this! It can take an optional callback function onComplete, that's executed when MapTable finishes rendering its components. For example if you have `function alertTest(){ alert('test!'); }` you would call it with `viz.render(alertTest)`. -*Example with preFilter* +_Example with preFilter_ ```js -var viz = d3.maptable('#vizContainer') - .json('dir_data.json', (d) => parseInt(d.traffic) > 0) - .map({ path: 'countries.json' }) - .render(); +var viz = d3 + .maptable('#vizContainer') + .json('dir_data.json', (d) => parseInt(d.traffic) > 0) + .map({ path: 'countries.json' }) + .render(); ``` ### Import datasets @@ -160,16 +164,16 @@ If you're planning to add markers on your map, you would need to provide `latitu ```json [ - {"longitude": "13.23000", "latitude": "-8.85000"}, - {"longitude": "168.32000", "latitude": "-17.75000"}, + { "longitude": "13.23000", "latitude": "-8.85000" }, + { "longitude": "168.32000", "latitude": "-17.75000" } ] ``` If you're planing to add country related information, you should provide consistent country information on your dataset from the TopJSON file. You should provide at least one of these types on your mapOptions: -- `countryIdentifierKey:` *(string, default: 'country_code')* Column name of country identifier (from the dataset). It goes as pair with the option `countryIdentifierType`. -- `countryIdentifierType:` *(string, default: 'iso_a2')* Country identifier type that we're using to attach data to countries on the map. The available types are: +- `countryIdentifierKey:` _(string, default: 'country_code')_ Column name of country identifier (from the dataset). It goes as pair with the option `countryIdentifierType`. +- `countryIdentifierType:` _(string, default: 'iso_a2')_ Country identifier type that we're using to attach data to countries on the map. The available types are: - `iso_a2` (default): [ISO_3166-1_alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country code format - `iso_a3`: [ISO_3166-1_alpha-3](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3) country code format - `name`: Country name that came from the GeoJSON map file. @@ -179,8 +183,8 @@ For example for this dataset: ```json [ - {"country_code": "MAR", "Country Name": "Morocco", "bar": "foo"}, - {"country_code": "FRA", "Country Name": "France", "bar": "foo"}, + { "country_code": "MAR", "Country Name": "Morocco", "bar": "foo" }, + { "country_code": "FRA", "Country Name": "France", "bar": "foo" } ] ``` @@ -210,19 +214,19 @@ By default, MapTable imports all the columns and detects their format automatica #### columnsDetails format -- `:` *(Object)* Provide the columns key here to apply the options below to it. If this key is not defined in yoru dataset, then it will be dynamically added as virtual column: - - `nowrap:` *(bool, default: false)* When present, it specifies that the content inside a that column should not wrap. - - `title:` *(string, default: columnKey)* What we show as column name in filters and table. - - `filterMethod:` *(string, default: 'field')* Column format type, used for filtering. Available options: +- `:` _(Object)_ Provide the columns key here to apply the options below to it. If this key is not defined in yoru dataset, then it will be dynamically added as virtual column: + - `nowrap:` _(bool, default: false)_ When present, it specifies that the content inside a that column should not wrap. + - `title:` _(string, default: columnKey)_ What we show as column name in filters and table. + - `filterMethod:` _(string, default: 'field')_ Column format type, used for filtering. Available options: - `field`: filter by keyword - `dropdown`: exact match using a dropdown - `compare`: filter using comparison (≥, ≤, between ....) - - `virtual`: *(function(d), default: null)* To create a new column that doesn't exists in the dataset, and we'd like to show it on the table or filters. You can also use it if you want to transform an existing column. - - `cellContent`: *(function(d), default: null)* Function that transforms an existing content using other rows. (for example to change the color depending on the data). - - `dataParse:` *(function(d), default: null)* Function that return the formatted data used to sort and compare cells. - - `filterInputType:` *(string, default: 'text')* HTML input type that we're using for the filters for that specific column (e.g. date, number, tel ...) + - `virtual`: _(function(d), default: null)_ To create a new column that doesn't exists in the dataset, and we'd like to show it on the table or filters. You can also use it if you want to transform an existing column. + - `cellContent`: _(function(d), default: null)_ Function that transforms an existing content using other rows. (for example to change the color depending on the data). + - `dataParse:` _(function(d), default: null)_ Function that return the formatted data used to sort and compare cells. + - `filterInputType:` _(string, default: 'text')_ HTML input type that we're using for the filters for that specific column (e.g. date, number, tel ...) -*Example (adding `nowrap` and `type` to the `region` column key):* +_Example (adding `nowrap` and `type` to the `region` column key):_ ```js .columns({ @@ -249,26 +253,27 @@ Functions that have `groupedData` as parameter, means that `groupedData` is a JS - `path:` _(string, **required** if pathData not set)_ URL of the TOPOJSON map, you can get them from Mike Bostock's repo: [world atlas](https://github.com/mbostock/world-atlas) and [us atlas](https://github.com/mbostock/us-atlas). Or use [this tool](https://github.com/melalj/topojson-map-generator) to generate these files as we did on the examples. - `pathData:` _(string, **required** if path not set)_ string containing the TOPOJSON map -- `onComplete:` *(function, default: null)* Callback function when the map first loaded. -- `onRender:` *(function, default: null)* Callback function when the map finished rendering. -- `width:` *(integer, default:'window.innerWidth')* Map Width. -- `height:` *(integer, default:'window.innerHeight')* Map Height. -- `saveState:` *(bool, default: true)* Save zoom state into the URL -- `zoom:` *(bool, default: true)* Enable zoom on the map (when scrolling up/down on the map). -- `filterCountries:` *(function(country))* Filter countries follow a specific condition. - *Example:* +- `onComplete:` _(function, default: null)_ Callback function when the map first loaded. +- `onRender:` _(function, default: null)_ Callback function when the map finished rendering. +- `width:` _(integer, default:'window.innerWidth')_ Map Width. +- `height:` _(integer, default:'window.innerHeight')_ Map Height. +- `saveState:` _(bool, default: true)_ Save zoom state into the URL +- `zoom:` _(bool, default: true)_ Enable zoom on the map (when scrolling up/down on the map). +- `filterCountries:` _(function(country))_ Filter countries follow a specific condition. + _Example:_ ```js filterCountries: (country) => country.id !== 'AQ', // to remove Antarctica from the map ``` - `title:` _(object, default: *see below*)_ Add a title within the map. - - `title.bgColor:` *(string, default: '#000000')* Title font size. - - `title.fontSize:` *(integer, default: 12)* Title font size. - - `title.fontFamily:` *(string, default: 'Helevetica, Arial, Sans-Serif')* Title font family. - - `title.content:` *(function(countShown, countTotal, filtersDescription)* Function to define how the title is rendered - - `title.source:` *(function())*_ Function to define how the HTML in the title. - *Example:* + + - `title.bgColor:` _(string, default: '#000000')_ Title font size. + - `title.fontSize:` _(integer, default: 12)_ Title font size. + - `title.fontFamily:` _(string, default: 'Helevetica, Arial, Sans-Serif')_ Title font family. + - `title.content:` _(function(countShown, countTotal, filtersDescription)_ Function to define how the title is rendered + - `title.source:` _(function())_\_ Function to define how the HTML in the title. + _Example:_ ```js title: { @@ -288,215 +293,225 @@ Functions that have `groupedData` as parameter, means that `groupedData` is a JS }, ``` -- `scaleZoom:` *([integer, integer], default: [1, 10])* The map zoom scale. -- `scaleHeight:` *(float, default: 1.0)* Ratio to scale the map height. -- `autoFitContent:` *(bool, default: true)* Enable auto zoom to focus on the active markers. -- `fitContentMargin:` *(integer, default: 10)* Padding in pixels to leave when we filter on a specific area. -- `ratioFromWidth:` *(float, default: 0.5)* Ratio between the height and the width: height/width, used to deduce the height from the the width. -- `countryIdentifierKey:` *(string, default: 'country_code')* Column name of country identifier (from the dataset). It goes as pair with the option `countryIdentifierType`. -- `countryIdentifierType:` *(string, default: 'iso_a2')* Country identifier type that we're using to attach data to countries on the map. The available types are: +- `scaleZoom:` _([integer, integer], default: [1, 10])_ The map zoom scale. +- `scaleHeight:` _(float, default: 1.0)_ Ratio to scale the map height. +- `autoFitContent:` _(bool, default: true)_ Enable auto zoom to focus on the active markers. +- `fitContentMargin:` _(integer, default: 10)_ Padding in pixels to leave when we filter on a specific area. +- `ratioFromWidth:` _(float, default: 0.5)_ Ratio between the height and the width: height/width, used to deduce the height from the the width. +- `countryIdentifierKey:` _(string, default: 'country_code')_ Column name of country identifier (from the dataset). It goes as pair with the option `countryIdentifierType`. +- `countryIdentifierType:` _(string, default: 'iso_a2')_ Country identifier type that we're using to attach data to countries on the map. The available types are: - `iso_a2` (default): [ISO_3166-1_alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country code format - `iso_a3`: [ISO_3166-1_alpha-3](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3) country code format - `name`: Country name that came from the GeoJSON map file. - `continent`: Continent name that came from the GeoJSON map file. -- `longitudeKey:` *(string, default: 'longitude')* Column name of the longitude (from the dataset). -- `latitudeKey:` *(string, default: 'latitude')* Column name of the latitude (from the dataset). -- `exportSvg:` *(string, default: null)* URL endpoint to download the current visualization as SVG. Read more on the section export SVG. (more details on a the section "Export as SVG") -- `exportSvgClient:` *(bool, default: false)* Show button to download the current visualization as SVG using only the client browser instead of querying the backend (in the opposite of `exportSvg`). You'll need to download [FileSaver.js](https://github.com/eligrey/FileSaver.js) and add a `