From 2d66d2a52209933c07aa4532ef119dd24fe81e4a Mon Sep 17 00:00:00 2001 From: OssamaRafique Date: Tue, 1 Aug 2023 11:34:40 -0700 Subject: [PATCH 1/8] Added ability to customize default margins --- src/BaseGL.js | 29 ++++++++++++++++++++++++++++- src/DotplotGL.js | 7 +------ src/RectplotGL.js | 7 +------ src/TickplotGL.js | 7 +------ src/constants.js | 7 +++++++ 5 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/BaseGL.js b/src/BaseGL.js index ea10db1..3e3aac8 100644 --- a/src/BaseGL.js +++ b/src/BaseGL.js @@ -23,6 +23,7 @@ import { INTENSITY_LEGEND_GRADIENT_SIZE_IN_PX, INTENSITY_LEGEND_SIZE_IN_PX, GROUPING_LEGEND_SIZE_IN_PX, + DEFAULT_MARGINS, } from "./constants"; /** @@ -75,6 +76,8 @@ class BaseGL { ygap: 0.3, }; + this.margins = DEFAULT_MARGINS; + //Default Data for labelOptions this.labelOptions = { rowLabelMaxCharacters: DEFAULT_ROW_MAX_LABEL_LENGTH_ALLOWED, @@ -269,7 +272,7 @@ class BaseGL { ...spec["margins"], top: `${topMarginToAccountForLabels}px`, left: `${leftMarginToAccountForLabels}px`, - right: "20px", + right: `${GROUPING_LEGEND_SIZE_IN_PX}px`, }; } @@ -521,6 +524,30 @@ class BaseGL { }; } + /** + * Set the margins for the visualization. + * all properties are optional, if not provided, the default values will be used. + * @param {object} margins, an object containing the margins + * @param {number} margins.top, top margin + * @param {number} margins.bottom, bottom margin + * @param {number} margins.left, left margin + * @param {number} margins.right, right margin + * @memberof BaseGL + * @example + * this.setMargins({ + * top: '10px', + * bottom: '10px', + * left: '10px', + * right: '10px', + * }) + **/ + setMargins(margins) { + this.margins = { + ...this.margins, + ...margins, + }; + } + /** * resize the plot, without having to send the data to the GPU. * diff --git a/src/DotplotGL.js b/src/DotplotGL.js index 99a5fdd..2bb33aa 100644 --- a/src/DotplotGL.js +++ b/src/DotplotGL.js @@ -50,12 +50,7 @@ class DotplotGL extends BaseGL { spec_inputs.y = this.input.y.map((e, i) => -1 + (2 * e + 1) / ylen); let spec = { - margins: { - top: "25px", - bottom: "50px", - left: "50px", - right: "10px", - }, + margins: this.margins, defaultData: { x: spec_inputs.x, y: spec_inputs.y, diff --git a/src/RectplotGL.js b/src/RectplotGL.js index b28145b..c3cbd3f 100644 --- a/src/RectplotGL.js +++ b/src/RectplotGL.js @@ -63,12 +63,7 @@ class RectplotGL extends BaseGL { spec_inputs.height = this.input.y.map((e, i) => default_height - yGaps(i)); let spec = { - margins: { - top: "25px", - bottom: "50px", - left: "50px", - right: "10px", - }, + margins: this.margins, defaultData: { x: spec_inputs.x, y: spec_inputs.y, diff --git a/src/TickplotGL.js b/src/TickplotGL.js index 41818ca..0692dfe 100644 --- a/src/TickplotGL.js +++ b/src/TickplotGL.js @@ -77,12 +77,7 @@ class TickplotGL extends BaseGL { } let spec = { - margins: { - top: "25px", - bottom: "50px", - left: "50px", - right: "10px", - }, + margins: this.margins, defaultData: { x: this.input.x, y: this.input.y, diff --git a/src/constants.js b/src/constants.js index 11cedd3..8f4f12e 100644 --- a/src/constants.js +++ b/src/constants.js @@ -13,3 +13,10 @@ export const INTENSITY_LEGEND_SIZE_IN_PX = INTENSITY_LEGEND_GRADIENT_SIZE_IN_PX + INTENSITY_LEGEND_LABEL_SIZE_IN_PX; export const GROUPING_LEGEND_SIZE_IN_PX = 20; export const TOOLTIP_IDENTIFIER = "ehgl-tooltip"; + +export const DEFAULT_MARGINS = { + top: "25px", + bottom: "50px", + left: "50px", + right: "10px", +}; From 7926c62314cce3af22d64c4133116e368705ce99 Mon Sep 17 00:00:00 2001 From: OssamaRafique Date: Tue, 1 Aug 2023 11:39:28 -0700 Subject: [PATCH 2/8] Filter out labels --- src/BaseGL.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/BaseGL.js b/src/BaseGL.js index 3e3aac8..3f3b433 100644 --- a/src/BaseGL.js +++ b/src/BaseGL.js @@ -1050,7 +1050,19 @@ class BaseGL { * @param {string} orientation - The orientation of the grouping labels * @returns {void} **/ - renderGroupingLabels(parentElement, groupingRowData, orientation) { + renderGroupingLabels(parentElement, groupingData, orientation) { + // Filter out duplicate labels in the grouping data + groupingData = groupingData.reduce( + (acc, obj) => { + if (!acc.seen[obj.label]) { + acc.seen[obj.label] = true; + acc.result.push(obj); + } + return acc; + }, + { seen: {}, result: [] } + ).result; + const parent = select(parentElement); const svg = parent.append("svg"); @@ -1058,14 +1070,14 @@ class BaseGL { if (orientation === "horizontal") { svg.attr("height", 25); } else { - svg.attr("height", groupingRowData.length * 25); + svg.attr("height", groupingData.length * 25); } const labelHeight = 25; let xOffset = 0; let yOffset = 0; - groupingRowData.forEach((data) => { + groupingData.forEach((data) => { const group = svg.append("g"); group From b8aadce929a8b5a94b9de7c8ffac49cbd16853dc Mon Sep 17 00:00:00 2001 From: OssamaRafique Date: Tue, 1 Aug 2023 11:49:35 -0700 Subject: [PATCH 3/8] Added Ability to set the offset for svg --- src/BaseGL.js | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/BaseGL.js b/src/BaseGL.js index 3f3b433..178092c 100644 --- a/src/BaseGL.js +++ b/src/BaseGL.js @@ -80,6 +80,10 @@ class BaseGL { //Default Data for labelOptions this.labelOptions = { + rowLabelsSvgXOffset: -1.05, + rowLabelsSvgYOffset: 1, + columnLabelsSvgXOffset: 1, + columnLabelsSvgYOffset: -1.05, rowLabelMaxCharacters: DEFAULT_ROW_MAX_LABEL_LENGTH_ALLOWED, columnLabelMaxCharacters: DEFAULT_COLUMN_MAX_LABEL_LENGTH_ALLOWED, rowLabelSlintAngle: DEFAULT_ROW_LABEL_SLINT_ANGLE, @@ -186,6 +190,10 @@ class BaseGL { _generateSpecForLabels(spec) { const { + rowLabelsSvgXOffset, + rowLabelsSvgYOffset, + columnLabelsSvgXOffset, + columnLabelsSvgYOffset, rowLabelMaxCharacters, columnLabelMaxCharacters, rowLabelSlintAngle, @@ -215,8 +223,8 @@ class BaseGL { maxWidth = Math.max(maxWidth, truncatedLabelWidth); labels.push({ - x: -1.02 + (2 * ilx + 1) / xlabels_len, - y: 1.05, + x: columnLabelsSvgXOffset + (2 * ilx + 1) / xlabels_len, + y: columnLabelsSvgYOffset, type: "row", index: ilx, text: truncatedLabel, @@ -249,8 +257,8 @@ class BaseGL { ); maxWidth = Math.max(maxWidth, truncatedLabelWidth); labels.push({ - x: -1.05, - y: -1.02 + (2 * ily + 1) / ylabels_len, + x: rowLabelsSvgXOffset, + y: rowLabelsSvgYOffset + (2 * ily + 1) / ylabels_len, type: "column", index: ily, text: truncatedLabel, @@ -500,6 +508,10 @@ class BaseGL { * @memberof BaseGL * @example * this.labelOptions = { + * rowLabelsSvgXOffset: 0, + * rowLabelsSvgYOffset: 0, + * columnLabelsSvgXOffset: 0, + * columnLabelsSvgYOffset: 0, * rowLabelMaxCharacters: 10, * columnLabelMaxCharacters: 10, * rowLabelSlintAngle: 0, @@ -509,8 +521,12 @@ class BaseGL { * } * @example * this.setLabelOptions({ + * rowLabelsSvgXOffset: 0, + * rowLabelsSvgYOffset: 0, + * columnLabelsSvgXOffset: 0, + * columnLabelsSvgYOffset: 0, * rowLabelMaxCharacters: 10, - * columnLabelMaxCharacters: 10, + * columnLabelMaxCharacters: 10, * rowLabelSlintAngle: 0, * columnLabelSlintAngle: 0, * rowLabelFontSize: "7px", From 172ccd6e8614276e6f61041e6918376ac81a290d Mon Sep 17 00:00:00 2001 From: OssamaRafique Date: Tue, 1 Aug 2023 12:05:39 -0700 Subject: [PATCH 4/8] Added support for typed arrays --- src/DotplotGL.js | 12 +++++-- src/RectplotGL.js | 16 ++++++--- src/utils.js | 92 +++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 106 insertions(+), 14 deletions(-) diff --git a/src/DotplotGL.js b/src/DotplotGL.js index 2bb33aa..2471f07 100644 --- a/src/DotplotGL.js +++ b/src/DotplotGL.js @@ -1,5 +1,5 @@ import BaseGL from "./BaseGL"; -import { getMinMax } from "./utils"; +import { getMinMax, mapArrayOrTypedArray } from "./utils"; /** * Make a DotPlot like plot @@ -46,8 +46,14 @@ class DotplotGL extends BaseGL { let spec_inputs = {}; let xlen = getMinMax(this.input.x)[1] + 1, ylen = getMinMax(this.input.y)[1] + 1; - spec_inputs.x = this.input.x.map((e, i) => -1 + (2 * e + 1) / xlen); - spec_inputs.y = this.input.y.map((e, i) => -1 + (2 * e + 1) / ylen); + spec_inputs.x = mapArrayOrTypedArray( + this.input.x, + (e, i) => -1 + (2 * e + 1) / xlen + ); + spec_inputs.y = mapArrayOrTypedArray( + this.input.y, + (e, i) => -1 + (2 * e + 1) / ylen + ); let spec = { margins: this.margins, diff --git a/src/RectplotGL.js b/src/RectplotGL.js index c3cbd3f..530cd22 100644 --- a/src/RectplotGL.js +++ b/src/RectplotGL.js @@ -1,5 +1,5 @@ import BaseGL from "./BaseGL"; -import { getMinMax } from "./utils"; +import { getMinMax, mapArrayOrTypedArray } from "./utils"; /** * Class to create traditional heatmap plots @@ -53,14 +53,20 @@ class RectplotGL extends BaseGL { }; let spec_inputs = {}; - spec_inputs.x = this.input.x.map((e, i) => String(e)); - spec_inputs.y = this.input.y.map((e, i) => String(e)); + spec_inputs.x = mapArrayOrTypedArray(this.input.x, (e, i) => String(e)); + spec_inputs.y = mapArrayOrTypedArray(this.input.y, (e, i) => String(e)); let default_width = 198 / (getMinMax(this.input.x)[1] + 1); let default_height = 198 / (getMinMax(this.input.y)[1] + 1); - spec_inputs.width = this.input.x.map((e, i) => default_width - xGaps(i)); - spec_inputs.height = this.input.y.map((e, i) => default_height - yGaps(i)); + spec_inputs.width = mapArrayOrTypedArray( + this.input.x, + (e, i) => default_width - xGaps(i) + ); + spec_inputs.height = mapArrayOrTypedArray( + this.input.y, + (e, i) => default_height - yGaps(i) + ); let spec = { margins: this.margins, diff --git a/src/utils.js b/src/utils.js index ec9fdf2..b1ea7d3 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,24 +1,38 @@ import { select } from "d3-selection"; import { TOOLTIP_IDENTIFIER } from "./constants"; +/** + * Check if a given variable is an object and not an array. + * + * @param {any} object - The variable to check. + * @returns {boolean} - Returns true if the variable is an object, and not an array. + */ export function isObject(object) { return typeof object === "object" && Array.isArray(object) === false; } +/** + * Get the minimum and maximum values from an array. + * + * @param {Array} arr - An array of numbers. + * @returns {Array} - An array containing the minimum and maximum values, in that order. + */ export const getMinMax = (arr) => { var max = -Number.MAX_VALUE, min = Number.MAX_VALUE; arr.forEach(function (x) { - if (max < x) { - max = x; - } - if (min > x) { - min = x; - } + if (max < x) max = x; + if (min > x) min = x; }); return [min, max]; }; +/** + * Parses an object of margins and returns an object with top, bottom, left, and right margins as integers. + * + * @param {Object} margins - An object with potential margin properties. + * @returns {Object} - An object with top, bottom, left, and right margins as integers. + */ export const parseMargins = (margins) => { const parsedMargins = { top: 0, @@ -41,6 +55,13 @@ export const parseMargins = (margins) => { return parsedMargins; }; +/** + * Measure the width of a text string for a given font size using SVG. + * + * @param {string} text - The text to measure. + * @param {string} fontSize - The font size to use for the measurement, e.g., '16px'. + * @returns {number} - The width of the text in pixels. + */ export const getTextWidth = (text, fontSize = "16px") => { // Create a temporary SVG to measure the text width const svg = select("body").append("svg"); @@ -50,6 +71,14 @@ export const getTextWidth = (text, fontSize = "16px") => { return width; }; +/** + * Create a tooltip on a specified container at the given position. + * + * @param {HTMLElement} container - The container element. + * @param {string} text - The text for the tooltip. + * @param {number} posX - The x-coordinate for the tooltip. + * @param {number} posY - The y-coordinate for the tooltip. + */ export const createTooltip = (container, text, posX, posY) => { let tooltip = select(container) .append("div") @@ -69,6 +98,11 @@ export const createTooltip = (container, text, posX, posY) => { .style("top", posY - 10 + "px"); }; +/** + * Remove a tooltip from the specified container. + * + * @param {HTMLElement} container - The container from which to remove the tooltip. + */ export const removeTooltip = (container) => { const tooltip = select(container).select(`#${TOOLTIP_IDENTIFIER}`); @@ -76,3 +110,49 @@ export const removeTooltip = (container) => { tooltip.remove(); } }; + +/** + * A function to map over both regular JavaScript arrays and typed arrays. + * + * @param {Array|TypedArray} array - The input array or typed array. + * @param {Function} callback - A function that produces an element of the new array, + * taking three arguments: + * currentValue - The current element being processed in the array. + * index - The index of the current element being processed in the array. + * array - The array map was called upon. + * @returns {Array|TypedArray} - A new array or typed array with each element being the result + * of the callback function. + * @throws {Error} - Throws an error if the input is neither a regular array nor a typed array. + */ +export const mapArrayOrTypedArray = (array, callback) => { + // Check if the input is a regular JavaScript array. + if (Array.isArray(array)) { + return array.map(callback); + } + // Check if the input is a typed array. + else if ( + array instanceof Int8Array || + array instanceof Uint8Array || + array instanceof Uint8ClampedArray || + array instanceof Int16Array || + array instanceof Uint16Array || + array instanceof Int32Array || + array instanceof Uint32Array || + array instanceof Float32Array || + array instanceof Float64Array + ) { + // Create a new typed array of the same type and size as the input. + let result = new array.constructor(array.length); + + // Use forEach to emulate the map functionality for typed arrays. + array.forEach((value, index) => { + result[index] = callback(value, index); + }); + + return result; + } + // Handle the case where the input is neither a regular array nor a typed array. + else { + throw new Error("Input is neither a normal array nor a typed array."); + } +}; From 032bf72c8c2d7ebfed572614c9cf8f18254c0489 Mon Sep 17 00:00:00 2001 From: OssamaRafique Date: Thu, 10 Aug 2023 12:29:55 -0700 Subject: [PATCH 5/8] Bumped up version and fixed a bug --- package.json | 4 ++-- src/BaseGL.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 1097412..1f851c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "epiviz.heatmap.gl", - "version": "0.0.15", + "version": "0.0.17", "repository": "https://github.com/jkanche/epiviz.heatmap.gl", "homepage": "https://github.com/jkanche/epiviz.heatmap.gl", "author": { @@ -20,7 +20,7 @@ "main": "dist/ehgl.js", "module": "dist/index.js", "dependencies": { - "epiviz.gl": "^1.0.11", + "epiviz.gl": "^1.0.15", "d3-axis": "^3.0.0", "d3-scale": "^4.0.2", "d3-selection": "^3.0.0" diff --git a/src/BaseGL.js b/src/BaseGL.js index 178092c..61549b9 100644 --- a/src/BaseGL.js +++ b/src/BaseGL.js @@ -81,9 +81,9 @@ class BaseGL { //Default Data for labelOptions this.labelOptions = { rowLabelsSvgXOffset: -1.05, - rowLabelsSvgYOffset: 1, - columnLabelsSvgXOffset: 1, - columnLabelsSvgYOffset: -1.05, + rowLabelsSvgYOffset: -1.02, + columnLabelsSvgXOffset: -1.02, + columnLabelsSvgYOffset: 1.05, rowLabelMaxCharacters: DEFAULT_ROW_MAX_LABEL_LENGTH_ALLOWED, columnLabelMaxCharacters: DEFAULT_COLUMN_MAX_LABEL_LENGTH_ALLOWED, rowLabelSlintAngle: DEFAULT_ROW_LABEL_SLINT_ANGLE, From 543e067c06e50c758af7ef35dcadf49a88129ec4 Mon Sep 17 00:00:00 2001 From: OssamaRafique Date: Thu, 10 Aug 2023 12:41:27 -0700 Subject: [PATCH 6/8] Updated readme.md --- README.md | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ed4c5b3..09d0928 100644 --- a/README.md +++ b/README.md @@ -79,8 +79,11 @@ This method allows you to customize the label options for your visualization. Al - `labelOptions.columnLabelSlintAngle` (`number`, optional): slant angle for column labels (default: 0) - `labelOptions.rowLabelFontSize` (`string | number`, optional): font size for row labels (default: 7px) - `labelOptions.columnLabelFontSize` (`string | number`, optional): font size for column labels (default: 7px) - -**Example**: + - `labelOptions.rowLabelsSvgXOffset` (`number`, optional): x offset for row labels (default: -1.05) + - `labelOptions.rowLabelsSvgYOffset` (`number`, optional): y offset for row labels (default: -1.02) + - `labelOptions.columnLabelsSvgXOffset` (`number`, optional): x offset for column labels (default: -1.02) + - `labelOptions.columnLabelsSvgYOffset` (`number`, optional): y offset for column labels (default: 1.05) + **Example**: ```javascript plot.setLabelOptions({ @@ -93,6 +96,29 @@ plot.setLabelOptions({ }); ``` +#### Customizing Margins with `setMargins` + +This method allows you to customize the margins for your visualization. All parameters are optional, providing you the flexibility to specify the options that best suit your needs. + +**Parameters**: + +- `margins` (`object`): an object containing the margin options + - `margins.top` (`number`, optional): top margin (default: 25px) + - `margins.bottom` (`number`, optional): bottom margin (default: 50px) + - `margins.left` (`number`, optional): left margin (default: 50px) + - `margins.right` (`number`, optional): right margin (default: 10px) + +**Example**: + +```javascript +plot.setMargins({ + top: 10, + bottom: 10, + left: 10, + right: 10, +}); +``` + #### Grouping Bars and Labels for Rows and Columns This feature allows users to add grouping bars and labels to both rows and columns of the heatmap. It provides a convenient way to visually represent different groups in your heatmap data. From d7e81ca3dbe4763cc050ead64592dabe45b2b251 Mon Sep 17 00:00:00 2001 From: OssamaRafique Date: Mon, 28 Aug 2023 13:28:33 -0400 Subject: [PATCH 7/8] Bump up version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1f851c6..cf3e50d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "epiviz.heatmap.gl", - "version": "0.0.17", + "version": "0.0.18", "repository": "https://github.com/jkanche/epiviz.heatmap.gl", "homepage": "https://github.com/jkanche/epiviz.heatmap.gl", "author": { From 5961cd5152a4264d17854f3ffa8387d701e655ab Mon Sep 17 00:00:00 2001 From: OssamaRafique Date: Wed, 15 Nov 2023 16:32:22 -0500 Subject: [PATCH 8/8] Bump up lib version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cf3e50d..f1b25c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "epiviz.heatmap.gl", - "version": "0.0.18", + "version": "0.0.19", "repository": "https://github.com/jkanche/epiviz.heatmap.gl", "homepage": "https://github.com/jkanche/epiviz.heatmap.gl", "author": {