diff --git a/README.md b/README.md
index e24abec..4ae4b76 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,18 @@ package is available through [npm](https://www.npmjs.com/package/epiviz.heatmap.
### Usage
- [app/index.html](./app/index.html) provides an easier example and code on how to use the library
+- Include the css in your application
+
+```html
+
+```
+
+```scss
+@import url("https://epiviz.github.io/epiviz.heatmap.gl/dist/ehgl.css");
+```
#### Intensity Legend
@@ -418,3 +430,28 @@ These attributes either take a fixed value or an array of values for each data p
columnGroupingData: ,
});
```
+
+### Tooltip Functionality
+
+The `Tooltip` class in `epiviz.heatmap.gl` provides dynamic tooltips for enhanced data point interaction. This feature allows users to see detailed information about specific data points on hover.
+
+#### Implementation
+
+- A singleton `Tooltip` instance is created for consistent behavior across the application.
+- Tooltips are triggered on mouse movement over data points, displaying context-sensitive information.
+- Positioning and content of the tooltip are dynamically updated based on the data point under the cursor.
+
+#### Usage Example
+
+```javascript
+const tooltip = new Tooltip();
+svgElement.addEventListener("mousemove", (event) => {
+ const content = `Detailed info`;
+ tooltip.updateTooltip(content, event.clientX, event.clientY);
+});
+svgElement.addEventListener("mouseout", () => {
+ tooltip.hideTooltip();
+});
+```
+
+This feature enhances the interactive experience by providing immediate, in-context data insights to users.
diff --git a/app/data-processor-worker-a4482939-6147ac31.js b/app/data-processor-worker-a4482939-b12f104a.js
similarity index 99%
rename from app/data-processor-worker-a4482939-6147ac31.js
rename to app/data-processor-worker-a4482939-b12f104a.js
index f728b00..5b49e7b 100644
--- a/app/data-processor-worker-a4482939-6147ac31.js
+++ b/app/data-processor-worker-a4482939-b12f104a.js
@@ -1,5 +1,5 @@
-import { b as getViewportForSpecification } from './rgb-b646728b-0756143b.js';
-import { S as SpecificationProcessor, a as SIZE_UNITS, t as transformGenomicRangeArcToStandard, b as transformGenomicRangeToStandard } from './specification-processor-f5ff7c7a-f223adaf.js';
+import { b as getViewportForSpecification } from './rgb-b646728b-2f709b61.js';
+import { S as SpecificationProcessor, a as SIZE_UNITS, t as transformGenomicRangeArcToStandard, b as transformGenomicRangeToStandard } from './specification-processor-f5ff7c7a-2d0faaee.js';
class FlatQueue {
diff --git a/app/ehgl.css b/app/ehgl.css
new file mode 100644
index 0000000..802209e
--- /dev/null
+++ b/app/ehgl.css
@@ -0,0 +1 @@
+.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}.tippy-iOS{-webkit-tap-highlight-color:transparent;cursor:pointer!important}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{background-color:#333;border-radius:4px;color:#fff;font-size:14px;line-height:1.4;outline:0;position:relative;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{border-top-color:#333;border-width:8px 8px 0;bottom:-7px;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{border-bottom-color:#333;border-width:0 8px 8px;top:-7px;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-left-color:#333;border-width:8px 0 8px 8px;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{border-right-color:#333;border-width:8px 8px 8px 0;left:-7px;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{height:16px;width:16px}.tippy-arrow:before{border-color:transparent;border-style:solid;content:"";position:absolute}.tippy-content{padding:5px 9px;position:relative;z-index:1}
\ No newline at end of file
diff --git a/app/ehgl.js b/app/ehgl.js
index 7299c25..6a29a21 100644
--- a/app/ehgl.js
+++ b/app/ehgl.js
@@ -4,11 +4,13 @@ Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
-var d3Selection = require('d3-selection');
var d3Array = require('d3-array');
-var WebGLVis = _interopDefault(require('epiviz.gl'));
var d3Scale = require('d3-scale');
+var d3Selection = require('d3-selection');
+var WebGLVis = _interopDefault(require('epiviz.gl'));
var d3Axis = require('d3-axis');
+var tippy = _interopDefault(require('tippy.js'));
+require('tippy.js/dist/tippy.css');
const DEFAULT_ROW_MAX_LABEL_LENGTH_ALLOWED = 15;
const DEFAULT_COLUMN_MAX_LABEL_LENGTH_ALLOWED = 30;
@@ -16,7 +18,6 @@ const DEFAULT_ROW_LABEL_SLINT_ANGLE = 0;
const DEFAULT_COLUMN_LABEL_SLINT_ANGLE = 0;
const DEFAULT_ROW_LABEL_FONT_SIZE = "7px";
const DEFAULT_COLUMN_LABEL_FONT_SIZE = "7px";
-const DEFAULT_VISIBLE_RANGE = [-1, 1];
const LABELS_MARGIN_BUFFER_IN_PX = 20;
const INTENSITY_LEGEND_LABEL_SIZE_IN_PX = 25;
@@ -24,30 +25,54 @@ const INTENSITY_LEGEND_GRADIENT_SIZE_IN_PX = 20;
const INTENSITY_LEGEND_SIZE_IN_PX =
INTENSITY_LEGEND_GRADIENT_SIZE_IN_PX + INTENSITY_LEGEND_LABEL_SIZE_IN_PX;
const GROUPING_LEGEND_SIZE_IN_PX = 20;
-const TOOLTIP_IDENTIFIER = "ehgl-tooltip";
+const INTENSITY_LEGEND_IDENTIFIER = "ehgl-intensity-legend";
const DEFAULT_SIZE_LEGEND_SVG_PADDING = 10;
const DEFAULT_SIZE_LEGEND_CIRCLE_GAP = 10;
const DEFAULT_SIZE_LEGEND_CIRCLE_TEXT_GAP = 10;
+const DEFAULT_MIN_RADIUS_FOR_DOTPLOT = 3;
+const DEFAULT_MARGIN_BETWEEN_DOTS = 2;
+
+const DEFAULT_MARGINS = {
+ top: "25px",
+ bottom: "50px",
+ left: "50px",
+ right: "10px",
+};
+
+/**
+ * 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.
+ */
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.
+ */
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.
+ */
const parseMargins = (margins) => {
const parsedMargins = {
top: 0,
@@ -70,6 +95,13 @@ 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.
+ */
const getTextWidth = (text, fontSize = "16px") => {
// Create a temporary SVG to measure the text width
const svg = d3Selection.select("body").append("svg");
@@ -79,50 +111,117 @@ const getTextWidth = (text, fontSize = "16px") => {
return width;
};
-const createTooltip = (container, text, posX, posY) => {
- let tooltip = d3Selection.select(container)
- .append("div")
- .attr("id", TOOLTIP_IDENTIFIER)
- .style("position", "absolute")
- .style("background", "#f9f9f9")
- .style("padding", "8px")
- .style("border", "1px solid #ccc")
- .style("border-radius", "6px")
- .style("z-index", "1000")
- .style("visibility", "hidden");
-
- tooltip
- .style("visibility", "visible")
- .text(text)
- .style("left", posX + 10 + "px")
- .style("top", posY - 10 + "px");
-};
-
-const removeTooltip = (container) => {
- const tooltip = d3Selection.select(container).select(`#${TOOLTIP_IDENTIFIER}`);
-
- if (tooltip) {
- tooltip.remove();
- }
-};
-
-const getMaxRadiusForDotplot = (xlen, ylen) => {
- return getMinMax([198 / (xlen + 1), 198 / (ylen + 1)])[1] - 5;
+const getMaxRadiusForDotplot = (xlen, ylen, padding) => {
+ return Math.max(
+ Math.min(198 / (xlen + 1), 198 / (ylen + 1)) - padding,
+ DEFAULT_MIN_RADIUS_FOR_DOTPLOT
+ );
};
const getScaledRadiusForDotplot = (
radius,
maxRadiusScaled,
minRadiusOriginal,
- maxRadiusOriginal
+ maxRadiusOriginal,
+ defaultMinRadius = DEFAULT_MIN_RADIUS_FOR_DOTPLOT
) => {
return (
- (maxRadiusScaled - 5) *
- ((radius - minRadiusOriginal) / (maxRadiusOriginal - minRadiusOriginal)) +
- 5
+ defaultMinRadius +
+ (maxRadiusScaled - defaultMinRadius) *
+ ((radius - minRadiusOriginal) / (maxRadiusOriginal - minRadiusOriginal))
);
};
+/**
+ * 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.
+ */
+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.");
+ }
+};
+
+class Tooltip {
+ constructor() {
+ if (!Tooltip.instance) {
+ Tooltip.instance = this;
+ this.createSingletonTooltip();
+ }
+ return Tooltip.instance;
+ }
+
+ createSingletonTooltip() {
+ // Create an invisible, persistent tooltip
+ this.currentTooltip = tippy(document.body, {
+ content: "",
+ trigger: "manual",
+ arrow: true,
+ placement: "right",
+ });
+ }
+
+ updateTooltip(content, x, y, options = {}) {
+ this.currentTooltip.setContent(content);
+ this.currentTooltip.setProps({
+ getReferenceClientRect: () => ({
+ width: 0,
+ height: 0,
+ top: y,
+ left: x,
+ right: x,
+ bottom: y,
+ }),
+ ...options,
+ });
+ if (!this.currentTooltip.state.isVisible) {
+ this.currentTooltip.show();
+ }
+ }
+
+ hideTooltip() {
+ if (this.currentTooltip) {
+ this.currentTooltip.hide();
+ }
+ }
+}
+
/**
* Base class for all matrix like layout plots.
* This class is not to be used directly.
@@ -164,6 +263,10 @@ class BaseGL {
ylabels: null,
};
+ // Plot domain
+ this.xAxisRange = null;
+ this.yAxisRange = null;
+
// state
this.state = {
size: 20,
@@ -173,8 +276,14 @@ class BaseGL {
ygap: 0.3,
};
+ this.margins = DEFAULT_MARGINS;
+
//Default Data for labelOptions
this.labelOptions = {
+ rowLabelsSvgXOffset: -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,
@@ -234,6 +343,8 @@ class BaseGL {
this.highlightedIndices = [];
this.indexStates = {};
+
+ this.tooltipInstance = new Tooltip();
}
/**
@@ -281,6 +392,10 @@ class BaseGL {
_generateSpecForLabels(spec) {
const {
+ rowLabelsSvgXOffset,
+ rowLabelsSvgYOffset,
+ columnLabelsSvgXOffset,
+ columnLabelsSvgYOffset,
rowLabelMaxCharacters,
columnLabelMaxCharacters,
rowLabelSlintAngle,
@@ -310,8 +425,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,
@@ -344,8 +459,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,
@@ -367,7 +482,7 @@ class BaseGL {
...spec["margins"],
top: `${topMarginToAccountForLabels}px`,
left: `${leftMarginToAccountForLabels}px`,
- right: "20px",
+ right: `${GROUPING_LEGEND_SIZE_IN_PX}px`,
};
}
@@ -596,6 +711,10 @@ class BaseGL {
* @memberof BaseGL
* @example
* this.labelOptions = {
+ * rowLabelsSvgXOffset: 0,
+ * rowLabelsSvgYOffset: 0,
+ * columnLabelsSvgXOffset: 0,
+ * columnLabelsSvgYOffset: 0,
* rowLabelMaxCharacters: 10,
* columnLabelMaxCharacters: 10,
* rowLabelSlintAngle: 0,
@@ -605,8 +724,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",
@@ -620,6 +743,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.
*
@@ -774,20 +921,20 @@ class BaseGL {
const labelType = e.detail.labelObject.type;
e.preventDefault();
- createTooltip(
- document.body,
+ this.tooltipInstance.updateTooltip(
labelType === "row"
? this.input.xlabels[hoveredIndex]
: this.input.ylabels[hoveredIndex],
- e.detail.event.pageX,
- e.detail.event.pageY
+ e.detail.event.clientX,
+ e.detail.event.clientY
);
+
this.labelHoveredCallback(e.detail);
});
this.plot.addEventListener("labelUnhovered", (e) => {
e.preventDefault();
- removeTooltip(document.body);
+ this.tooltipInstance.hideTooltip();
this.labelUnhoveredCallback(e.detail);
});
}
@@ -801,6 +948,11 @@ class BaseGL {
// Only render the legend if we have the legend data and the legend dom element
if (!this.legendDomElement || !this.intensityLegendData) return;
+ //Clear the legend dom element
+ d3Selection.select(this.legendDomElement)
+ .select(`#${INTENSITY_LEGEND_IDENTIFIER}`)
+ .remove();
+
const parsedMargins = parseMargins(this._spec.margins);
const containerWidth =
this.legendWidth ||
@@ -809,7 +961,7 @@ class BaseGL {
this.legendHeight ||
this.elem.clientHeight - parsedMargins.top - parsedMargins.bottom;
- const averageCharWidth = 6; // rough estimation of the width of a single character
+ const averageCharWidth = 8; // rough estimation of the width of a single character
const legendWidth = containerWidth - 2 * averageCharWidth;
const legendHeight = containerHeight - 2 * averageCharWidth;
@@ -825,12 +977,12 @@ class BaseGL {
transformX = averageCharWidth;
}
- const svgContainer = d3
- .select(this.legendDomElement)
+ const svgContainer = d3Selection.select(this.legendDomElement)
.append("svg")
+ .attr("id", INTENSITY_LEGEND_IDENTIFIER)
.attr("width", svgWidth)
.attr("height", svgHeight)
- .attr("overflow", "visible");
+ .style("overflow", "visible");
const defs = svgContainer.append("defs");
@@ -949,7 +1101,7 @@ class BaseGL {
**/
renderRowGroupingLegend() {
const position = this.rowGroupingLegendPosition;
- const visibleRange = this.viewport?.yRange || DEFAULT_VISIBLE_RANGE;
+ const visibleRange = this.viewport?.yRange || this.yAxisRange;
if (
!this.rowGroupingLegendDomElement ||
@@ -983,10 +1135,13 @@ class BaseGL {
const yScale = d3Scale.scaleLinear()
.domain(visibleRange) // Input range is currently visible range
.range([svgHeight, 0]); // Output range is SVG height
+ const maxYRange = this.yAxisRange[1] - this.yAxisRange[0];
+ const minY = this.yAxisRange[0];
this.groupingRowData.forEach((group, idx) => {
- const normalizedStart = (group.startIndex * 2) / totalData - 1;
- const normalizedEnd = ((group.endIndex + 1) * 2) / totalData - 1;
+ const normalizedStart = (group.startIndex / totalData) * maxYRange + minY;
+ const normalizedEnd =
+ ((group.endIndex + 1) / totalData) * maxYRange + minY;
if (
normalizedEnd >= visibleRange[0] &&
@@ -999,7 +1154,6 @@ class BaseGL {
const rectHeight = Math.abs(
yScale(rectEndInView) - yScale(rectStartInView)
);
-
svgContainer
.append("rect")
.attr("x", 0)
@@ -1007,12 +1161,12 @@ class BaseGL {
.attr("width", legendWidth)
.attr("height", rectHeight)
.style("fill", group.color)
- .on("mouseover", (e) => {
+ .on("mousemove", (e) => {
const text = group.label;
- createTooltip(document.body, text, e.pageX, e.pageY);
+ this.tooltipInstance.updateTooltip(text, e.clientX, e.clientY);
})
.on("mouseout", (e) => {
- removeTooltip(document.body);
+ this.tooltipInstance.hideTooltip();
});
}
});
@@ -1029,7 +1183,7 @@ class BaseGL {
* */
renderColumnGroupingLegend() {
const position = this.columnGroupingLegendPosition; // should be 'top' or 'bottom'
- const visibleRange = this.viewport?.xRange || DEFAULT_VISIBLE_RANGE;
+ const visibleRange = this.viewport?.xRange || this.xAxisRange;
// Only render the legend if we have the legend data, the dom element,
// the position is either 'top' or 'bottom' and visibleRange exists
@@ -1069,9 +1223,13 @@ class BaseGL {
.domain(visibleRange) // Input range is currently visible range
.range([0, svgWidth]); // Output range is SVG width
+ const maxXRange = this.xAxisRange[1] - this.xAxisRange[0];
+ const minX = this.xAxisRange[0];
+
this.groupingColumnData.forEach((group, idx) => {
- const normalizedStart = (group.startIndex * 2) / totalData - 1;
- const normalizedEnd = ((group.endIndex + 1) * 2) / totalData - 1;
+ const normalizedStart = (group.startIndex / totalData) * maxXRange + minX;
+ const normalizedEnd =
+ ((group.endIndex + 1) / totalData) * maxXRange + minX;
if (
normalizedEnd >= visibleRange[0] &&
@@ -1092,12 +1250,13 @@ class BaseGL {
.attr("width", rectWidth)
.attr("height", legendHeight)
.style("fill", group.color)
- .on("mouseover", (e) => {
+ .on("mousemove", (e) => {
const text = group.label;
- createTooltip(document.body, text, e.pageX, e.pageY);
+
+ this.tooltipInstance.updateTooltip(text, e.clientX, e.clientY);
})
.on("mouseout", (e) => {
- removeTooltip(document.body);
+ this.tooltipInstance.hideTooltip();
});
}
});
@@ -1122,7 +1281,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 = d3Selection.select(parentElement);
const svg = parent.append("svg");
@@ -1130,14 +1301,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
@@ -1430,7 +1601,7 @@ class DotplotGL extends BaseGL {
* @param {Array|number} encoding.xgap, same as size, but sets the gap along x-axis.
* @param {Array|number} encoding.ygap, same as size, but sets the gap along y-axis.
* @param {Array} encoding.intensityLegendData - an array of objects containing the color, intensity and label for the legend.
- * @param {Array} encoding.sizeLegendData - an object containing minSize, maxSize and steps for the legend.
+ * @param {Array} encoding.sizeLegendData - an object containing minSize, maxSize, steps and maxSizeInPx for the legend.
* @param {Array} encoding.rowGroupingData - an array of objects containing the startIndex, endIndex, color and label for the row grouping.
* @param {Array} encoding.columnGroupingData - an array of objects containing the startIndex, endIndex, color and label for the column grouping.
* @memberof BaseGL
@@ -1457,16 +1628,22 @@ class DotplotGL extends BaseGL {
const [, maxY] = getMinMax(this.input.y);
let xlen = maxX + 1,
ylen = maxY + 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
+ );
+
+ // Setting X and Y Axis Domains
+ this.xAxisRange = [-1, 1];
+ this.yAxisRange = [-1, 1];
let spec = {
- margins: {
- top: "25px",
- bottom: "50px",
- left: "50px",
- right: "10px",
- },
+ margins: this.margins,
defaultData: {
x: spec_inputs.x,
y: spec_inputs.y,
@@ -1479,12 +1656,12 @@ class DotplotGL extends BaseGL {
x: {
attribute: "x",
type: "quantitative",
- domain: [-1, 1],
+ domain: this.xAxisRange,
},
y: {
attribute: "y",
type: "quantitative",
- domain: [-1, 1],
+ domain: this.yAxisRange,
},
opacity: { value: this.state.opacity },
},
@@ -1492,7 +1669,12 @@ class DotplotGL extends BaseGL {
};
// scale size of dots
- const maxRadiusScaled = getMaxRadiusForDotplot(xlen, ylen);
+ const maxRadiusScaled = getMaxRadiusForDotplot(
+ xlen,
+ ylen,
+ DEFAULT_MARGIN_BETWEEN_DOTS
+ );
+
let tsize = this.state["size"];
if (Array.isArray(this.state["size"])) {
let [minRadiusOriginal, maxRadiusOriginal] = getMinMax(
@@ -1506,8 +1688,6 @@ class DotplotGL extends BaseGL {
maxRadiusOriginal
)
);
-
- console.log(getMinMax(tsize), "tize", tsize);
}
this._generateSpecForLabels(spec);
@@ -1587,7 +1767,8 @@ class DotplotGL extends BaseGL {
*/
renderSizeLegend() {
if (!this.sizeLegendData) return;
- let { minSize, maxSize, steps } = this.sizeLegendData;
+ let { minSize, maxSize, steps, maxSizeInPx, minSizeInPx } =
+ this.sizeLegendData;
const [, maxX] = getMinMax(this.input.x);
const [, maxY] = getMinMax(this.input.y);
let xlen = maxX + 1,
@@ -1596,19 +1777,37 @@ class DotplotGL extends BaseGL {
const [minRadiusOriginal, maxRadiusOriginal] = getMinMax(
this.state["size"]
);
- const maxRadiusScaled = getMaxRadiusForDotplot(xlen, ylen);
+ const maxRadiusAsPerPlot = getMaxRadiusForDotplot(
+ xlen,
+ ylen,
+ DEFAULT_MARGIN_BETWEEN_DOTS
+ );
minSize = getScaledRadiusForDotplot(
- minSize,
- maxRadiusScaled,
+ minSize || minRadiusOriginal, // if minSize is not provided, use minRadiusOriginal
+ maxRadiusAsPerPlot,
minRadiusOriginal,
maxRadiusOriginal
);
maxSize = getScaledRadiusForDotplot(
- maxSize,
- maxRadiusScaled,
+ maxSize || maxRadiusOriginal, // if maxSize is not provided, use maxRadiusOriginal
+ maxRadiusAsPerPlot,
minRadiusOriginal,
maxRadiusOriginal
);
+
+ // Desired max size in pixels
+ const maxPx = maxSizeInPx || maxSize;
+
+ // Desired min size in pixels
+ const minPx = minSizeInPx || minSize;
+
+ // Create a linear scale
+ const sizeScale = d3Scale.scaleLinear()
+ .domain([minSize, maxSize])
+ .range([minPx, maxPx]);
+
+ minSize = sizeScale(minSize);
+ maxSize = sizeScale(maxSize);
const orientation = this.sizeLegendOptions.orientation;
// Calculate step size
@@ -1720,7 +1919,6 @@ class DotplotGL extends BaseGL {
.style("right", "0px");
break;
}
- console.log("before", this._spec.margins);
this.updateMarginsToAccountForSizeLegend();
this.plot.setSpecification(this._spec);
@@ -1942,22 +2140,28 @@ 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));
+
+ // Setting X and Y Axis Domains to [-1, 1]
+ this.xAxisRange = [-1, 1];
+ this.yAxisRange = [-1, 1];
+
+ 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: {
- top: "25px",
- bottom: "50px",
- left: "50px",
- right: "10px",
- },
+ margins: this.margins,
defaultData: {
x: spec_inputs.x,
y: spec_inputs.y,
@@ -2072,13 +2276,12 @@ class TickplotGL extends BaseGL {
}
}
+ // Setting X and Y Axis Domains
+ this.xAxisRange = getMinMax(this.input.x);
+ this.yAxisRange = getMinMax(this.input.y);
+
let spec = {
- margins: {
- top: "25px",
- bottom: "50px",
- left: "50px",
- right: "10px",
- },
+ margins: this.margins,
defaultData: {
x: this.input.x,
y: this.input.y,
@@ -2091,12 +2294,12 @@ class TickplotGL extends BaseGL {
x: {
attribute: "x",
type: "quantitative",
- domain: getMinMax(this.input.x),
+ domain: this.xAxisRange,
},
y: {
attribute: "y",
type: "quantitative",
- domain: getMinMax(this.input.y),
+ domain: this.yAxisRange,
},
opacity: { value: this.state.opacity },
width: { value: default_width },
diff --git a/app/index.html b/app/index.html
index 68ddf36..2c87ed3 100644
--- a/app/index.html
+++ b/app/index.html
@@ -11,6 +11,7 @@
+