From 2ebab4f57da440039fe903fb5bd60146bf445576 Mon Sep 17 00:00:00 2001 From: helloklyn Date: Tue, 24 Aug 2021 16:05:49 -0400 Subject: [PATCH] [update]: big number aggregation --- preview/big-number-aggregation/main.js | 254 +++++++++++++++++++++++++ preview/big-number-aggregation/util.js | 112 +++++++++++ 2 files changed, 366 insertions(+) create mode 100644 preview/big-number-aggregation/main.js create mode 100644 preview/big-number-aggregation/util.js diff --git a/preview/big-number-aggregation/main.js b/preview/big-number-aggregation/main.js new file mode 100644 index 0000000..a1e38cf --- /dev/null +++ b/preview/big-number-aggregation/main.js @@ -0,0 +1,254 @@ +const inputDimensionBreakdownNameLookML = "_breakdown"; +const { tidy, select, distinct, arrange, desc } = Tidy; + +const visObject = { + options: { + bar_color: { + type: "string", + display: "color", + label: "1. Choose Primary Color", + default: "#3259F9", + }, + is_human_readable: { + type: "boolean", + label: "2. Toggle for Readable Number", + default: false, + }, + is_percentage_number: { + type: "boolean", + label: "3. Toggle for Percentage Number", + default: false, + }, + aggregation_type: { + type: "string", + display: "select", + label: "4. Select Aggregation Type", + default: "sum", + values: [ + { sum: "sum" }, + { average: "average" }, + { min: "min" }, + { max: "max" }, + { median: "median" }, + ], + }, + }, + create: function (element, config) { + element.innerHTML = ` + +
+
+
+ `; + }, + updateAsync: function (data, element, config, queryResponse, details, done) { + this.clearErrors(); + var errorMessage = ` + Instructions🧭 + This viz package requires + 1 dimension in named _breakdown in .lkml + 1 measure + Please contact Hong Wu(@hongkuiw) if you still facing errors + `; + + if ( + queryResponse.fields.dimensions.length == 0 || + (queryResponse.fields.dimensions.length == 1 && + queryResponse.fields.measures.length > 1) + ) { + console.error(errorMessage); + return; + } + + dataInput = queryResponse.data; + var dataRecords = generateDataRecords(dataInput); + var highchartsFigureHeight = document.getElementById("container").offsetHeight; + var pointHeightResponsive = parseInt((highchartsFigureHeight / dataRecords.length) * 0.45); + + var viewName = queryResponse.fields.dimensions.length > 0 ? queryResponse.fields.dimensions[0].view : queryResponse.fields.measures[0].view; + var dimensionName = viewName + "." + inputDimensionBreakdownNameLookML; + dimensionMetaInfoValue = getFieldMetaInfoValue(queryResponse, dimensionName); + breakdownName = dimensionMetaInfoValue[0].label_short; + breakdownDescription = dimensionMetaInfoValue[0].description; + var measureName = queryResponse.fields.measures[0].name; + measureMetaInfoValue = getFieldMetaInfoValue(queryResponse, measureName); + metricsTitle = measureMetaInfoValue[0].label_short; + chartTitle = metricsTitle; + dataRecordsSortDescending = tidy(dataRecords, arrange((a, b) => b.measureName - a.measureName)); + var numberBreakdowns = dataRecordsSortDescending.length; + var dataHighCharts = generateHighChartsDataSeries(dataRecordsSortDescending); + var dataBreakdowns = []; + var dataSeries = []; + dataHighCharts.forEach((d) => { + dataBreakdowns.push(d[0]); + dataSeries.push(d[1]); + }); + + metricsValueAggregated = calculateAggregatedValue(dataSeries, config.aggregation_type); + + Highcharts.chart("container", { + chart: { + zoomType: "x", + panning: "true", + panKey: "shift", + type: "bar", + events: { + load: function () { + this.title.on("mouseover", (e) => { + myLabel = this.renderer + .label( + measureMetaInfoValue[0]["description"], + e.x, + e.y, + "rectangle" + ) + .css({ color: "#FFFFFF" }) + .attr({ + fill: "#181818", + "font-family": "Circular Spotify Text, Helvetica, Arial, sans-serif", + }) + .add() + .toFront(); + }); + this.title.on("mouseout", (e) => { + if (myLabel) { + myLabel.destroy(); + } + }); + }, + }, + }, + title: { + text: + chartTitle + + '

by ' + + breakdownName + + "

" + + "
" + + "
" + + "
" + + "

" + translateAggregationType(config.aggregation_type) + ":

" + + '

' + humanReadableNumber(percentageNumber(metricsValueAggregated, config.is_percentage_number),config.is_human_readable) + + "

", + align: "left", + }, + subtitle: { + }, + xAxis: { + categories: dataBreakdowns, + title: { + text: undefined, + }, + }, + yAxis: { + title: { + text: null, + }, + labels: { + overflow: "justify", + enable: false, + }, + }, + tooltip: { + valuePrefix: metricsTitle + ": ", + }, + plotOptions: { + bar: { + dataLabels: { + enabled: true, + formatter: function() { + return humanReadableNumber(percentageNumber(parseFloat(this.y), config.is_percentage_number),config.is_human_readable) + } + }, + color: config.bar_color + }, + }, + legend: { + enabled: false, + }, + credits: { + enabled: false, + }, + exporting: { + enabled: false + }, + series: [ + { + pointWidth: pointHeightResponsive, + name: breakdownName, + data: dataSeries, + }, + ], + }); + done(); + }, +}; + +looker.plugins.visualizations.add(visObject); diff --git a/preview/big-number-aggregation/util.js b/preview/big-number-aggregation/util.js new file mode 100644 index 0000000..e3ee8d8 --- /dev/null +++ b/preview/big-number-aggregation/util.js @@ -0,0 +1,112 @@ +var generateDataRecords = (dataIndexFormat) => { + //HELP: convert looker response data into common records format + //NOTE: used by metrics-widget__big-number + var dataRecords = [] + dataIndexFormat.forEach(d=>{ + obj = {} + var headerName = Object.keys(d); + headerName.forEach(h=>{ + obj[h] = d[h].value + }); + dataRecords.push(obj) + }) + return dataRecords +} + +function generateHighChartsDataSeries(dataRecordsInput) { + //HELP: convert DataRecords into HighChart DataSeries without Header/Column Name + //NOTE: used by metrics-widget__big-number + dataHighCharts = [] + dataRecordsInput.forEach(function(d) { + var rowValueOnly = [] + var columnNames = Object.keys(d); + // console.log(columnNames); + columnNames.forEach(function(c) { + rowValueOnly.push(d[c]) + }) + dataHighCharts.push(rowValueOnly) + }); + return dataHighCharts; +} + +function getFieldMetaInfoValue(queryResponse, fieldName) { + //HELP: look up meta info of looker fields + // @queryResponse: looker Response + // @fieldName: viewName.fieldTechicalName + // @return: array of metainfo of this field + const queryResponseFieldsDimensions = queryResponse.fields.dimensions + const queryResponseFieldsMeasures = queryResponse.fields.measures + + f_dimension = queryResponseFieldsDimensions.filter(d=>{ + return d.name == fieldName + }) + + f_measure = queryResponseFieldsMeasures.filter(d=>{ + return d.name == fieldName + }) + + var f = f_dimension.length == 0 ? f_measure : f_dimension + + return f +} + +function calculateAggregatedValue(inputArray1D, aggregationType) { + // HELP + // @aggregationType - String: sum, average, min, max, median + switch (aggregationType) { + case "sum": + return d3.sum(inputArray1D); + break; + case "average": + return d3.mean(inputArray1D); + break; + case "min": + return d3.min(inputArray1D); + break; + case "max": + return d3.max(inputArray1D); + break; + case "median": + return d3.median(inputArray1D); + break; + case "major-between": + return ( d3.quantile(inputArray1D, 0.25) + " ~ " + d3.quantile(inputArray1D, 0.75)); + break; + } +} + +function humanReadableNumber(value, is_human_readable) { + if (is_human_readable == true) { + return numeral(value).format("0.00a") + } else { + return value + } +} + +function percentageNumber(value, is_percentage_number) { + if (is_percentage_number == true) { + return numeral(value).format("0.00%") + } else { + return value + } +} + +function translateAggregationType(aggregationType) { + switch (aggregationType) { + case "sum": + return "Total"; + break; + case "average": + return "Average"; + break; + case "max": + return "Max"; + break; + case "min": + return "Min"; + break; + case "median": + return "Median"; + break; + } +} \ No newline at end of file