visual-heatmap
Version:
"Advanced Visual Heatmap - High Scale webGL based rendering."
6 lines • 19.1 kB
JavaScript
/*!
* Heatmap
* (c) 2025 Narayana Swamy (narayanaswamy14@gmail.com)
* @license BSD-3-Clause
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).visualHeatmap=e()}(this,(function(){"use strict";const t=.1,e="highp",i="\nvec2 rotation(vec2 v, float a, float aspect) {\n float s = sin(a);\n float c = cos(a);\n mat2 rotationMat = mat2(c, -s, s, c);\n mat2 scaleMat = mat2(aspect, 0.0, 0.0, 1.0);\n mat2 scaleMatInv = mat2(1.0/aspect, 0.0, 0.0, 1.0);\n return scaleMatInv * rotationMat * scaleMat * v;\n}",r={vertex:`#version 300 es\n precision ${e} float;\n in vec2 a_position;\n in float a_intensity;\n uniform float u_size;\n uniform vec2 u_resolution;\n uniform vec2 u_translate; \n uniform float u_zoom; \n uniform float u_angle; \n uniform float u_density;\n out float v_i;\n\n ${i}\n\n void main() {\n vec2 zeroToOne = (a_position * u_density + u_translate * u_density) / (u_resolution);\n vec2 zeroToTwo = zeroToOne * 2.0 - 1.0;\n float zoomFactor = max(u_zoom, ${t});\n zeroToTwo = zeroToTwo / zoomFactor;\n if (u_angle != 0.0) {\n zeroToTwo = rotation(zeroToTwo, u_angle, u_resolution.x / u_resolution.y);\n }\n gl_Position = vec4(zeroToTwo, 0, 1);\n gl_PointSize = u_size * u_density;\n v_i = a_intensity;\n }`,fragment:`#version 300 es\n precision ${e} float;\n uniform float u_max;\n uniform float u_min;\n uniform float u_intensity;\n in float v_i;\n out vec4 fragColor;\n \n void main() {\n float r = 0.0;\n vec2 cxy = 2.0 * gl_PointCoord - 1.0;\n r = dot(cxy, cxy);\n float deno = max(u_max - u_min, 1e-6); // Prevent division by zero\n \n if(r <= 1.0) {\n float alpha = ((v_i - u_min) / deno) * u_intensity * (1.0 - sqrt(r));\n alpha = clamp(alpha, 0.0, 1.0); // Clamp alpha to valid range\n fragColor = vec4(0, 0, 0, alpha);\n } else {\n discard; // Don't process pixels outside the point\n }\n }`},o={vertex:"#version 300 es\n precision highp float;\n in vec2 a_texCoord;\n out vec2 v_texCoord;\n void main() {\n vec2 clipSpace = a_texCoord * 2.0 - 1.0;\n gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);\n v_texCoord = a_texCoord;\n }\n ",fragment:`#version 300 es\n #define MAX_COLORS ${20}\n precision ${e} float;\n \n in vec2 v_texCoord;\n out vec4 fragColor;\n uniform sampler2D u_framebuffer;\n uniform vec4 u_colorArr[MAX_COLORS];\n uniform float u_colorCount;\n uniform float u_opacity;\n uniform float u_offset[MAX_COLORS];\n\n void main() {\n float alpha = texture(u_framebuffer, v_texCoord.xy).a;\n \n if (alpha <= 0.0 || alpha > 1.0) {\n discard;\n return;\n }\n\n vec4 color_;\n if (alpha <= u_offset[0]) {\n color_ = u_colorArr[0];\n } else {\n for (int i = 1; i < MAX_COLORS && i < int(u_colorCount); ++i) {\n if (alpha <= u_offset[i]) {\n float t = (alpha - u_offset[i - 1]) / (u_offset[i] - u_offset[i - 1]);\n color_ = mix(u_colorArr[i - 1], u_colorArr[i], t);\n break;\n }\n }\n }\n\n color_ *= u_opacity;\n color_.a = max(0.0, color_.a);\n fragColor = color_;\n }\n `},n={vertex:`#version 300 es\n precision ${e} float;\n in vec2 a_position;\n in vec2 a_texCoord;\n uniform vec2 u_resolution;\n uniform vec2 u_translate; \n uniform float u_zoom; \n uniform float u_angle; \n uniform float u_density;\n out vec2 v_texCoord;\n\n ${i}\n\n void main() {\n vec2 zeroToOne = (a_position * u_density + u_translate * u_density) / (u_resolution);\n zeroToOne.y = 1.0 - zeroToOne.y;\n vec2 zeroToTwo = zeroToOne * 2.0 - 1.0;\n float zoomFactor = max(u_zoom, ${t});\n zeroToTwo = zeroToTwo / zoomFactor;\n \n if (u_angle != 0.0) {\n zeroToTwo = rotation(zeroToTwo, u_angle * -1.0, u_resolution.x / u_resolution.y);\n }\n\n gl_Position = vec4(zeroToTwo, 0, 1);\n v_texCoord = a_texCoord;\n }\n `,fragment:`#version 300 es\n precision ${e} float;\n uniform sampler2D u_image;\n in vec2 v_texCoord;\n out vec4 fragColor;\n \n void main() {\n vec4 texColor = texture(u_image, v_texCoord);\n if (texColor.a < 0.01) {\n discard; // Don't process nearly transparent pixels\n }\n fragColor = texColor;\n }\n `};function a(t,e,i){const r=t.createShader(t[e]);if(!r)throw new Error("Failed to create shader.");t.shaderSource(r,i),t.compileShader(r);if(!t.getShaderParameter(r,t.COMPILE_STATUS)){const e=t.getShaderInfoLog(r);throw t.deleteShader(r),new Error("*** Error compiling shader '"+r+"':"+e)}return r}function s(t,e){const i=a(t,"VERTEX_SHADER",e.vertex),r=a(t,"FRAGMENT_SHADER",e.fragment),o=t.createProgram();if(!o)throw new Error("Failed to create program.");t.attachShader(o,i),t.attachShader(o,r),t.linkProgram(o);if(t.getProgramParameter(o,t.LINK_STATUS))return o;{const e=t.getProgramInfoLog(o);throw t.deleteProgram(o),new Error("Error in program linking:"+e)}}function h(t){return null==t}function u(t){return"number"!=typeof t}function l(t){if(!Array.isArray(t)||t.length<2)throw new Error("Invalid gradient: Expected an array with at least 2 elements.");if(!function(t){for(let e=0;e<t.length-1;e++)if(t[e+1].offset-t[e].offset<0)return!1;return!0}(t))throw new Error("Invalid gradient: Gradient is not sorted");const e=t.length,i=new Float32Array(4*e),r=new Array(e);return t.forEach((function(t,e){const o=4*e;i[o]=t.color[0]/255,i[o+1]=t.color[1]/255,i[o+2]=t.color[2]/255,i[o+3]=void 0!==t.color[3]?t.color[3]:1,r[e]=t.offset})),{value:i,length:e,offset:r}}function f(t){const e=this,i=t.length;let{posVec:r=new Float32Array,rVec:o=new Float32Array}=e.hearmapExData||{};e._pDataLength!==i&&(r=new Float32Array(new ArrayBuffer(8*i)),o=new Float32Array(new ArrayBuffer(4*i)),e._pDataLength=i);const n={min:1/0,max:-1/0};for(let e=0;e<i;e++)r[2*e]=t[e].x,r[2*e+1]=t[e].y,o[e]=t[e].value,n.min>t[e].value&&(n.min=t[e].value),n.max<t[e].value&&(n.max=t[e].value);return{posVec:r,rVec:o,minMax:n}}function c(t){const e=this.zoom||.1,i=this.width/2,r=this.height/2,{angle:o,translate:n}=this;let a=(t.x-i)/i*e,s=(t.y-r)/r*e;if(0!==o){const t=Math.cos(o),e=Math.sin(o);s=e*a+t*s,a=t*a-e*s}return a=a*i+i-n[0],s=s*r+r-n[1],t.x=a,t.y=s,{x:a,y:s}}function m(){const t=this.ctx;t&&(t.clear(t.COLOR_BUFFER_BIT|t.DEPTH_BUFFER_BIT),t.bindTexture(t.TEXTURE_2D,this._fbTexObj),t.texImage2D(t.TEXTURE_2D,0,t.RGBA,this.width*this.ratio,this.height*this.ratio,0,t.RGBA,t.UNSIGNED_BYTE,null),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_S,t.CLAMP_TO_EDGE),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_T,t.CLAMP_TO_EDGE),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MIN_FILTER,t.LINEAR),t.bindFramebuffer(t.FRAMEBUFFER,this._fbo),t.framebufferTexture2D(t.FRAMEBUFFER,t.COLOR_ATTACHMENT0,t.TEXTURE_2D,this._fbTexObj,0),this.hearmapExData&&_.call(this,t,this.hearmapExData),t.bindFramebuffer(t.FRAMEBUFFER,null),this.imageConfig&&g.call(this,t,this.imageConfig),d.call(this,t))}function _(t,e){var i,r,o,n;t.useProgram(this._gradShadOP.program);const{u_resolution:a,u_translate:s,u_zoom:h,u_angle:u,u_density:l,u_max:f,u_min:c,u_size:m,u_intensity:_}=this._gradShadOP.uniform;this.min=null!==this.configMin?this.configMin:null!==(r=null===(i=null==e?void 0:e.minMax)||void 0===i?void 0:i.min)&&void 0!==r?r:0,this.max=null!==this.configMax?this.configMax:null!==(n=null===(o=null==e?void 0:e.minMax)||void 0===o?void 0:o.max)&&void 0!==n?n:0,this._gradShadOP.attr[0].data=e.posVec||[],this._gradShadOP.attr[1].data=e.rVec||[],t.uniform2fv(a,new Float32Array([this.width*this.ratio,this.height*this.ratio])),t.uniform2fv(s,new Float32Array([this.translate[0],this.translate[1]])),t.uniform1f(h,this.zoom?this.zoom:.01),t.uniform1f(u,this.angle),t.uniform1f(l,this.ratio),t.uniform1f(f,this.max),t.uniform1f(c,this.min),t.uniform1f(m,this.size),t.uniform1f(_,this.intensity),this._gradShadOP.attr.forEach((function(e){t.bindBuffer(e.bufferType,e.buffer),t.bufferData(e.bufferType,e.data,e.drawType),t.enableVertexAttribArray(e.attribute),t.vertexAttribPointer(e.attribute,e.size,e.valueType,!0,0,0)})),t.drawArrays(t.POINTS,0,(e.posVec||[]).length/2)}function g(t,e){const{x:i=0,y:r=0,width:o=0,height:n=0}=e,{u_resolution:a,u_translate:s,u_zoom:h,u_angle:u,u_density:l,u_image:f}=this._imageShaOP.uniform;t.useProgram(this._imageShaOP.program),t.uniform2fv(a,new Float32Array([this.width*this.ratio,this.height*this.ratio])),t.uniform2fv(s,new Float32Array([this.translate[0],this.translate[1]])),t.uniform1f(h,this.zoom?this.zoom:.01),t.uniform1f(u,this.angle),t.uniform1f(l,this.ratio),this._imageShaOP.attr[0].data=new Float32Array([i,r,i+o,r,i,r+n,i,r+n,i+o,r,i+o,r+n]),this._imageShaOP.attr.forEach((function(e){t.bindBuffer(e.bufferType,e.buffer),t.bufferData(e.bufferType,e.data,e.drawType),t.enableVertexAttribArray(e.attribute),t.vertexAttribPointer(e.attribute,e.size,e.valueType,!0,0,0)})),t.uniform1i(f,0),t.activeTexture(this.ctx.TEXTURE0),t.bindTexture(this.ctx.TEXTURE_2D,this._imageTexture),t.drawArrays(t.TRIANGLES,0,6)}function d(t){const{u_colorArr:e,u_colorCount:i,u_offset:r,u_opacity:o,u_framebuffer:n}=this._colorShadOP.uniform;t.useProgram(this._colorShadOP.program),t.uniform4fv(e,this.gradient.value),t.uniform1f(i,this.gradient.length),t.uniform1fv(r,new Float32Array(this.gradient.offset)),t.uniform1f(o,this.opacity),this._colorShadOP.attr.forEach((function(e){t.bindBuffer(e.bufferType,e.buffer),t.bufferData(e.bufferType,e.data,e.drawType),t.enableVertexAttribArray(e.attribute),t.vertexAttribPointer(e.attribute,e.size,e.valueType,!0,0,0)})),t.uniform1i(n,0),t.activeTexture(t.TEXTURE0),t.bindTexture(t.TEXTURE_2D,this._fbTexObj),t.drawArrays(t.TRIANGLES,0,6)}class x{constructor(t,e){this.ctx=null,this.ratio=1,this.width=0,this.height=0,this.imageConfig=null,this.configMin=null,this.configMax=null,this.min=0,this.max=0,this.size=0,this.zoom=0,this.angle=0,this.intensity=0,this.translate=[0,0],this.opacity=0,this.hearmapExData={},this.gradient=null,this._imageTexture=null,this._pDataLength=void 0,this.imgWidth=0,this.imgHeight=0,this.heatmapData=[],this.type="";try{const i="string"==typeof t?document.querySelector(t):t instanceof HTMLElement?t:null;if(!i)throw new Error("Context must be either a string or an Element");const{clientHeight:a,clientWidth:u}=i,f=document.createElement("canvas"),c=f.getContext("webgl2",{premultipliedAlpha:!1,depth:!1,antialias:!0,alpha:!0,preserveDrawingBuffer:!1});this.ratio=function(t){return(window.devicePixelRatio||1)/(t.webkitBackingStorePixelRatio||t.mozBackingStorePixelRatio||t.msBackingStorePixelRatio||t.oBackingStorePixelRatio||t.backingStorePixelRatio||1)}(c),c.clearColor(0,0,0,0),c.enable(c.BLEND),c.blendEquation(c.FUNC_ADD),c.blendFunc(c.ONE,c.ONE_MINUS_SRC_ALPHA),c.depthMask(!0),f.setAttribute("height",(a*this.ratio).toString()),f.setAttribute("width",(u*this.ratio).toString()),f.style.height=`${a}px`,f.style.width=`${u}px`,f.style.position="absolute",i.appendChild(f),this.ctx=c,this.width=u,this.height=a,this.imageConfig=null,this.configMin=null,this.configMax=null,this.hearmapExData={},this.layer=f,this.dom=i,this._gradShadOP=function(t,e){const i=s(t,e),r=t.createBuffer();if(!r)throw new Error("Failed to create position buffer.");const o=t.createBuffer();if(!o)throw new Error("Failed to create intensity buffer.");return{program:i,attr:[{bufferType:t.ARRAY_BUFFER,buffer:r,drawType:t.STATIC_DRAW,valueType:t.FLOAT,size:2,attribute:t.getAttribLocation(i,"a_position"),data:new Float32Array([])},{bufferType:t.ARRAY_BUFFER,buffer:o,drawType:t.STATIC_DRAW,valueType:t.FLOAT,size:1,attribute:t.getAttribLocation(i,"a_intensity"),data:new Float32Array([])}],uniform:{u_resolution:t.getUniformLocation(i,"u_resolution"),u_max:t.getUniformLocation(i,"u_max"),u_min:t.getUniformLocation(i,"u_min"),u_size:t.getUniformLocation(i,"u_size"),u_intensity:t.getUniformLocation(i,"u_intensity"),u_translate:t.getUniformLocation(i,"u_translate"),u_zoom:t.getUniformLocation(i,"u_zoom"),u_angle:t.getUniformLocation(i,"u_angle"),u_density:t.getUniformLocation(i,"u_density")}}}(this.ctx,r),this._colorShadOP=function(t,e){const i=s(t,e),r=t.createBuffer();if(!r)throw new Error("Failed to create texture coordinate buffer.");return{program:i,attr:[{bufferType:t.ARRAY_BUFFER,buffer:r,drawType:t.STATIC_DRAW,valueType:t.FLOAT,size:2,attribute:t.getAttribLocation(i,"a_texCoord"),data:new Float32Array([0,0,1,0,0,1,0,1,1,0,1,1])}],uniform:{u_framebuffer:t.getUniformLocation(i,"u_framebuffer"),u_colorArr:t.getUniformLocation(i,"u_colorArr"),u_colorCount:t.getUniformLocation(i,"u_colorCount"),u_opacity:t.getUniformLocation(i,"u_opacity"),u_offset:t.getUniformLocation(i,"u_offset")}}}(this.ctx,o),this._imageShaOP=function(t,e){const i=s(t,e),r=t.createBuffer();if(!r)throw new Error("Failed to create position buffer.");const o=t.createBuffer();if(!o)throw new Error("Failed to create texture coordinate buffer.");return{program:i,attr:[{bufferType:t.ARRAY_BUFFER,buffer:r,drawType:t.STATIC_DRAW,valueType:t.FLOAT,size:2,attribute:t.getAttribLocation(i,"a_position"),data:new Float32Array([])},{bufferType:t.ARRAY_BUFFER,buffer:o,drawType:t.STATIC_DRAW,valueType:t.FLOAT,size:2,attribute:t.getAttribLocation(i,"a_texCoord"),data:new Float32Array([0,0,1,0,0,1,0,1,1,0,1,1])}],uniform:{u_resolution:t.getUniformLocation(i,"u_resolution"),u_image:t.getUniformLocation(i,"u_image"),u_translate:t.getUniformLocation(i,"u_translate"),u_zoom:t.getUniformLocation(i,"u_zoom"),u_angle:t.getUniformLocation(i,"u_angle"),u_density:t.getUniformLocation(i,"u_density")}}}(this.ctx,n),this._fbTexObj=c.createTexture(),this._fbo=c.createFramebuffer(),h(e.size)?this.size=20:this.setSize(e.size),h(e.max)?this.configMax=null:this.setMax(e.max),h(e.min)?this.configMin=null:this.setMin(e.min),h(e.intensity)?this.intensity=1:this.setIntensity(e.intensity),h(e.translate)?this.translate=[0,0]:this.setTranslate(e.translate),h(e.zoom)?this.zoom=1:this.setZoom(e.zoom),h(e.angle)?this.angle=0:this.setRotationAngle(e.angle),h(e.opacity)?this.opacity=1:this.setOpacity(e.opacity),this.gradient=l(e.gradient),e.backgroundImage&&e.backgroundImage.url&&this.setBackgroundImage(e.backgroundImage),this.heatmapData=[],this.ctx.viewport(0,0,this.ctx.canvas.width,this.ctx.canvas.height)}catch(t){console.error(t)}}resize(){const t=this.dom.clientHeight,e=this.dom.clientWidth;this.layer.setAttribute("height",(t*this.ratio).toString()),this.layer.setAttribute("width",(e*this.ratio).toString()),this.layer.style.height=`${t}px`,this.layer.style.width=`${e}px`,this.width=e,this.height=t,this.ctx.viewport(0,0,this.ctx.canvas.width,this.ctx.canvas.height),this.render()}clear(){this.ctx.clear(this.ctx.COLOR_BUFFER_BIT|this.ctx.DEPTH_BUFFER_BIT)}setMax(t){if(h(t)||u(t))throw new Error("Invalid max: Expected Number");return this.configMax=t,this}setMin(t){if(h(t)||u(t))throw new Error("Invalid min: Expected Number");return this.configMin=t,this}setGradient(t){return this.gradient=l(t),this}setTranslate(t){if(t.constructor!==Array)throw new Error("Invalid Translate: Translate has to be of Array type");if(2!==t.length)throw new Error("Translate has to be of length 2");return this.translate=t,this}setZoom(t){if(h(t)||u(t))throw new Error("Invalid zoom: Expected Number");return this.zoom=t,this}setRotationAngle(t){if(h(t)||u(t))throw new Error("Invalid Angle: Expected Number");return this.angle=t,this}setSize(t){if(h(t)||u(t))throw new Error("Invalid Size: Expected Number");return this.size=t,this}setIntensity(t){if(h(t)||u(t))throw this.intensity=1,new Error("Invalid Intensity: Expected Number");if(t>1||t<0)throw this.intensity=t>1?1:0,new Error("Invalid Intensity value "+t);return this.intensity=t,this}setOpacity(t){if(h(t)||u(t))throw new Error("Invalid Opacity: Expected Number");if(t>1||t<0)throw new Error("Invalid Opacity value "+t);return this.opacity=t,this}setBackgroundImage(t){const e=this;if(!t.url)return;const i=this.ctx.getParameter(this.ctx.MAX_TEXTURE_SIZE);return this._imageTexture=this.ctx.createTexture(),this.type="TEXTURE_2D",this.imageConfig=null,this.imgWidth=t.width||this.width,this.imgHeight=t.height||this.height,this.imgWidth=this.imgWidth>i?i:this.imgWidth,this.imgHeight=this.imgHeight>i?i:this.imgHeight,function(t,e,i){const r=new Image;r.crossOrigin="anonymous",r.onload=e,r.onerror=i,r.src=t}(t.url,(function(){e.ctx.activeTexture(e.ctx.TEXTURE0),e.ctx.bindTexture(e.ctx.TEXTURE_2D,e._imageTexture),e.ctx.texParameteri(e.ctx.TEXTURE_2D,e.ctx.TEXTURE_WRAP_S,e.ctx.CLAMP_TO_EDGE),e.ctx.texParameteri(e.ctx.TEXTURE_2D,e.ctx.TEXTURE_WRAP_T,e.ctx.CLAMP_TO_EDGE),e.ctx.texParameteri(e.ctx.TEXTURE_2D,e.ctx.TEXTURE_MIN_FILTER,e.ctx.LINEAR),e.ctx.texParameteri(e.ctx.TEXTURE_2D,e.ctx.TEXTURE_MAG_FILTER,e.ctx.LINEAR),e.ctx.texImage2D(e.ctx.TEXTURE_2D,0,e.ctx.RGBA,this.naturalWidth,this.naturalHeight,0,e.ctx.RGBA,e.ctx.UNSIGNED_BYTE,this),e.imageConfig={x:t.x||0,y:t.y||0,height:e.imgHeight,width:e.imgWidth,image:this},e.render()}),(function(t){throw new Error(`Image Load Error, ${t}`)})),this}clearData(){this.heatmapData=[],this.hearmapExData={},this.render()}addData(t,e){const i=this;for(let r=0;r<t.length;r++)e&&c.call(i,t[r]),this.heatmapData.push(t[r]);return this.renderData(this.heatmapData),this}renderData(t){if(t.constructor!==Array)throw new Error("Expected Array type");return this.hearmapExData=f.call(this,t),this.heatmapData=t,this.render(),this}render(){m.call(this)}projection(t){const e=this.zoom||.1,i=this.width/2,r=this.height/2,o=this.translate[0],n=this.translate[1],a=this.angle,s=this.width/this.height;let h=(t.x+o-i)/(i*e),u=(t.y+n-r)/(r*e);if(h*=s,0!==a){const t=Math.cos(-a),e=Math.sin(-a),i=t*h-e*u;u=e*h+t*u,h=i}return h*=1/s,h=h*i+i,u=u*r+r,{x:h,y:u}}toBlob(t="image/png",e=.92){return new Promise(((i,r)=>{try{const o=this.ctx,n=this.layer.getContext("webgl2",{premultipliedAlpha:!1,depth:!1,antialias:!0,alpha:!0,preserveDrawingBuffer:!0});if(!n)throw new Error("Failed to create WebGL2 context");this.ctx=n,this.render(),this.layer.toBlob((t=>{t?i(t):r(new Error("Failed to create blob from canvas")),this.ctx=o,this.render()}),t,e)}catch(t){r(t)}}))}}return function(t,e){return new x(t,e)}}));