In this recent post we looked at a new 2D heatmap capability that we’ve added to Project Dasher. In this post we look at what was needed to make this heatmap resizable – but with a fixed aspect ratio – as well as being movable and displayed with a transparent background.
Luckily, this previous post shows how to create a transparent, movable window – which we already use to show our surface shading legend – so we were able to use that as a basis for today’s implementation.
Firstly, let’s take a look at it working. Note the fact the window is basically invisible unless hovered over, at which point we see a shadow effect at its border.

So there are two main features we wanted to add: the ability to maintain the panel’s aspect ratio (which in this case is 1, as we want it to be square) and the ability for the user to resize it (while keeping it square :-).
Let’s start with the second one… in this case I went ahead and modified the TransparentDockingPanel class to have an option that specifies that we want the panel to be resizable. Here’s the modified class (in TypeScript, as is usual for this project), which – when initialized – checks the options.resize flag, and if it’s set we create the footer that’s needed to resize the panel:
const initialWidth = 300;
const initialHeight = 400;
const minWidth = 100;
const minHeight = 100;
export class TransparentDockingPanel extends Autodesk.Viewing.UI.DockingPanel {
constructor(
parentContainer: Element,
id: string,
title?: string,
options?: any
) {
super(parentContainer, id, title, options);
const cont = this.container;
cont.dockRight = !!options.dockRight;
cont.dockBottom = !!options.dockBottom;
const style = cont.style;
style.width = (options.width || initialWidth) + 'px';
style.height = (options.height || initialHeight) + 'px';
style.minWidth = (options.minWidth || options.width || minWidth) + 'px';
style.minHeight = (options.minHeight || options.height || minHeight) + 'px';
const classList = cont.classList;
classList.add('modelStructurePanel', 'docking-panel', 'transparentPanel');
if (options) {
for (const additionalClass of options.classes) {
classList.add(additionalClass);
}
}
}
initialize(): void {
// Only create movement handlers (for the dialog body), by default,
// with options for title/close/resize.
// super.initialize();
const cont = this.container;
this.initializeMoveHandlers(this.container);
const self = (<any>this);
const options = self.options;
if (options.title) {
this.title = this.createTitleBar(this.titleLabel || this.container.id);
this.container.appendChild(this.title);
this.setTitle(this.titleLabel || this.container.id, options);
}
if (options.close) {
this.closer = self.createCloseButton();
this.container.appendChild(this.closer);
}
if (options.resize) {
self.footer = self.createFooter();
this.container.appendChild(self.footer);
}
}
}
I’ve gone ahead and added additional options for creating a title and a close button: while we’re not going to use these now, at some point it’s likely we’ll want to add multiple heatmaps – and make it easy for the user to dismiss them – so having a close button and a descriptive label for each could well prove useful.
In case you’re wondering, the footer (and title/close button) creation code is simply taken from the initialization code for the Autodesk.Viewing.UI.DockingPanel class. The main difference in our class is that these become options rather than standard behaviour.
The other piece – to keep the panel square – we do from the child ResizableDockingPanel class. This contains a MutationObserver to keep an eye on the panel’s style, and checks for when the width and height are modified. During the onResize() event we take the minimum of the width and height – and use that as the value for both, assuming it isn’t below a minimum threshold – and then set that back on the panel but also use it to resize the panel contents (in this case our heatmap).
import { TransparentDockingPanel } from './TransparentDockingPanel';
const initialSize = 400;
const minSize = 100;
const initialPadding = 30;
export class ResizableSquarePanel extends TransparentDockingPanel {
private _size: number;
private _padding: number;
private _contents: HTMLElement;
private _resizeContents: (elem: HTMLElement, size: number) => void;
constructor(
parentContainer: Element,
id: string,
contents?: HTMLElement,
title?: string,
options?: any
) {
super(
parentContainer,
id,
title,
{...options, ...{
classes: [...(options.classes || []), 'resizableSquarePanel'],
minWidth: options.minSize || minSize,
minHeight: options.minSize || minSize,
width: options.size || initialSize,
height: options.size || initialSize,
padding: options.padding || initialPadding,
resize: true
}}
);
this._size = options.size || initialSize;
this._padding = options.padding || initialPadding;
// Set the options for top/left/bottom/right
// (we cannot set both left and right or both top and bottom)
const cont = this.container;
const style = cont.style;
const top = options.top;
const left = options.left;
const bottom = options.bottom;
const right = options.right;
if (top) {
style.top = top + 'px';
}
if (left) {
style.left = left + 'px';
}
if (bottom) {
const h = parentContainer.clientHeight;
style.top = (h - this._size - bottom) + 'px';
}
if (options.right) {
const w = parentContainer.clientWidth;
style.left = (w - this._size - right) + 'px';
}
this.contents = contents;
const resizeObserver = new MutationObserver(this.onResize.bind(this));
resizeObserver.observe(cont, {
attributes: true,
attributeFilter: ['style']
});
}
set contents(value: HTMLElement) {
this._contents = value;
if (value) {
this.container.appendChild(value);
this.centerContents();
this.onResize();
}
}
set contentsResizer(value: (elem: HTMLElement, size: number) => void) {
this._resizeContents = value;
}
private centerContents(): void {
if (this._contents && this._padding !== undefined) {
// Hopefully this will center the heatmap by using the padding offset
const hs = this._contents.style;
hs.top = this._padding + 'px';
hs.left = this._padding + 'px';
}
}
private onResize(): void {
// Keep our panel square
const style = this.container.style;
const width = parseInt(style.width, 10);
const height = parseInt(style.height, 10);
const dim = Math.min(width, height); // Choose the smallest side
const size = Math.max(dim, minSize); // Clamp it to the minimum size
if (this._size !== size) {
this._size = size;
}
style.height = size + 'px';
style.width = size + 'px';
const side = (size - (2 * this._padding));
if (this._resizeContents) {
this._resizeContents(this._contents, side);
}
}
}
Note that we can’t set the _resizeContents member during the constructor of any base class – super() calls can’t use the ‘this’ keyword – so we have a separate property set to properly create one of these panels.
Here’s how we’re using this class inside Dasher:
import { ResizableSquarePanel } from '../../core/ResizableSquarePanel';
import { Heatmap2d } from '../../core/Heatmap2d';
const initialSize = 400;
const minSize = 100;
const initialPadding = 30;
export class SurfaceShadingHeatmapPanel extends ResizableSquarePanel {
private _heatmap: Heatmap2d;
constructor(
parentContainer: Element,
id: string,
heatmap: Heatmap2d,
title?: string,
options?: any
) {
super(
parentContainer,
id,
heatmap.canvas,
title,
{...options, ...{
classes: [...(options.classes || []), 'surfaceShadingHeatmap2d'],
minSize: options.minSize || minSize,
size: options.size || initialSize,
padding: options.padding || initialPadding
}},
);
this.contentsResizer =
(elem, size) => {
const hm = this._heatmap;
// The heatmap is scaled by half in CSS, as this gives a much better
// quality result (we double the desired size before passing it on)
const side = size * 2;
if (hm.width !== side || hm.height !== side) {
hm.setSize(side, side);
hm.render();
}
};
this._heatmap = heatmap;
}
}
Here’s the CSS that manages the shadow effect shown on-hover as well as changing the resizer and close button icons from light to dark:
.dasher-page .adsk-viewing-viewer .transparentPanel .docking-panel-title {
background-color: transparent;
border-bottom: unset;
}
.dasher-page .adsk-viewing-viewer .transparentPanel .docking-panel-footer-resizer {
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSI3IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSI+PHBhdGggZD0iTS41IDYuNWw2LTZNNC41IDYuNWwxLjUzNi0xLjUzNiIgc3Ryb2tlPSIjQkVDOEQyIi8+PHBhdGggZD0iTTEuNSA2LjVsNi02TTUuNSA2LjVsMS41MzYtMS41MzYiIHN0cm9rZT0iIzkzOUNBNSIvPjwvZz48L3N2Zz4=");
}
.dasher-page .adsk-viewing-viewer .transparentPanel:hover .docking-panel-footer-resizer {
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSI3IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSI+PHBhdGggZD0iTS41IDYuNWw2LTZNNC41IDYuNWwxLjUzNi0xLjUzNiIgc3Ryb2tlPSIjQkVDOEQyIi8+PHBhdGggZD0iTTEuNSA2LjVsNi02TTUuNSA2LjVsMS41MzYtMS41MzYiIHN0cm9rZT0iIzRBNTU1QiIvPjwvZz48L3N2Zz4=");
}
.dasher-page .adsk-viewing-viewer .transparentPanel .docking-panel-close {
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTEiIGhlaWdodD0iMTEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJNLjQ1NSAxMC45OTdhLjUuNSAwIDAxLS4zMS0uODVsMTAtMTBhLjUwMi41MDIgMCAwMS43MS43MWwtMTAgMTBhLjUuNSAwIDAxLS40LjE0eiIgZmlsbD0iIzk0OTQ5RiIvPjxwYXRoIGQ9Ik0xMC40NTcgMTEuMDA3YS41LjUgMCAwMS0uMzEtLjE1bC0xMC0xMGEuNTAyLjUwMiAwIDAxLjcxLS43MWwxMCAxMGEuNS41IDAgMDEtLjQuODZ6IiBmaWxsPSIjOTM5Q0E1Ii8+PC9nPjwvc3ZnPg==");
}
.dasher-page .adsk-viewing-viewer .transparentPanel:hover .docking-panel-close {
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTEiIGhlaWdodD0iMTEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0iIzRBNTU1QiIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJNLjQ1NSAxMC45OTdhLjUuNSAwIDAxLS4zMS0uODVsMTAtMTBhLjUwMi41MDIgMCAwMS43MS43MWwtMTAgMTBhLjUuNSAwIDAxLS40LjE0eiIvPjxwYXRoIGQ9Ik0xMC40NTcgMTEuMDA3YS41LjUgMCAwMS0uMzEtLjE1bC0xMC0xMGEuNTAyLjUwMiAwIDAxLjcxLS43MWwxMCAxMGEuNS41IDAgMDEtLjQuODZ6Ii8+PC9nPjwvc3ZnPg==")
}
.dasher-page .adsk-viewing-viewer .transparentPanel .docking-panel-footer {
background-color: unset;
border-top: unset;
}
.dasher-page .adsk-viewing-viewer .surfaceShadingHeatmap2d.docking-panel {
box-shadow: unset;
&:hover {
box-shadow: 1px 1px 10px 0 rgba(0, 0, 0, .2);
}
& .docking-panel-title {
display: flex;
justify-content: center;
font-weight: 400;
font-size: 14pt;
}
}
The feature works quite well. Here’s a longer animation showing the new panel but also cycling through different types of sensor data:

If you want to play around with this yourself, here’s a link.
Thinking about where this feature could go in the future: the same mechanism could easily be used to display multiple heatmaps concurrently, which would allow us to compare and contrast different sensor feeds and even simulation results. This would take a bit more UX work, to establish how best to choose the sensor type per heatmap (etc.), but it should provide some interesting analysis capabilities.