Skip to content

Commit

Permalink
Preview for the HCS images (#2489) - Add vertical and horisontal Lege…
Browse files Browse the repository at this point in the history
…nds to main canvas. Sync scroll positions between canvas and legends
  • Loading branch information
AleksandrGorodetskii committed Feb 7, 2022
1 parent 18f435e commit ab0a68d
Showing 1 changed file with 215 additions and 52 deletions.
267 changes: 215 additions & 52 deletions client/src/components/special/hcs-image/hcs-control-grid/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,35 @@ const CELL_DIAMETER_LIMITS = {
min: 5,
max: 50
};
const CTX_STYLE = {
const DEFAULTS = {
strokeStyle: '#595959',
lineWidth: 1
lineWidth: 1,
background: '#ececec'
};

class HcsControlGrid extends React.Component {
canvasContainer;
canvas;
verticalLegendContainer;
verticalLegend;
horisontalLegendContainer;
horisontalLegend;
scrolling = false;

componentWillUnmount () {
if (this.canvas) {
this.canvas.removeEventListener('wheel', this.handleZoom);
this.canvas.removeEventListener('click', this.handleClick);
this.canvasContainer.removeEventListener('scroll', this.handleScroll);
}
}

get bounds () {
if (this.canvas) {
return {
xFrom: CANVAS_PADDING + CTX_STYLE.lineWidth,
xFrom: CANVAS_PADDING + DEFAULTS.lineWidth,
xTo: this.canvas.width - CANVAS_PADDING,
yFrom: CANVAS_PADDING + CTX_STYLE.lineWidth,
yFrom: CANVAS_PADDING + DEFAULTS.lineWidth,
yTo: this.canvas.height - CANVAS_PADDING
};
}
Expand All @@ -65,6 +73,20 @@ class HcsControlGrid extends React.Component {
this.draw();
};

initializeCanvasContainer = (canvas) => {
this.canvasContainer = canvas;
this.canvasContainer.addEventListener('scroll', this.handleScroll);
};

initializeLegends = (canvas, type) => {
if (type === 'vertical') {
this.verticalLegend = canvas;
} else if (type === 'horisontal') {
this.horisontalLegend = canvas;
}
this.drawLegends();
};

getElementAtEvent = (event) => {
if (event && this.canvas) {
const rect = this.canvas.getBoundingClientRect();
Expand Down Expand Up @@ -93,6 +115,32 @@ class HcsControlGrid extends React.Component {
}
};

handleScroll = () => {
if (!this.scrolling) {
window.requestAnimationFrame(() => {
this.synchronizeScrolls();
this.scrolling = false;
});
this.scrolling = true;
}
};

synchronizeScrolls = () => {
if (this.canvasContainer) {
const scrollWidth = this.canvasContainer.offsetWidth - this.canvasContainer.clientWidth;
if (this.horisontalLegendContainer) {
this.horisontalLegendContainer.scrollLeft = this.canvasContainer.scrollLeft;
this.horisontalLegendContainer.style.width = `${
this.canvasContainer.offsetWidth - scrollWidth}px`;
}
if (this.verticalLegendContainer) {
this.verticalLegendContainer.scrollTop = this.canvasContainer.scrollTop;
this.verticalLegendContainer.style.height = `${
this.canvasContainer.offsetHeight - scrollWidth}px`;
}
}
};

cleanUpCanvas = () => {
if (this.canvas) {
const ctx = this.canvas.getContext('2d');
Expand All @@ -102,13 +150,115 @@ class HcsControlGrid extends React.Component {
}
};

zoomIn = () => {
if (this.canvas) {
const {offsetWidth, offsetHeight} = this.canvas;
this.canvas.style.width = `${
offsetWidth + (offsetWidth * ZOOM_TICK_PERCENT)}px`;
this.canvas.style.height = `${
offsetHeight + (offsetHeight * ZOOM_TICK_PERCENT)}px`;
this.horisontalLegend.style.width = `${
offsetWidth + (offsetWidth * ZOOM_TICK_PERCENT)}px`;
this.verticalLegend.style.height = `${
offsetHeight + (offsetHeight * ZOOM_TICK_PERCENT)}px`;
this.draw();
}
};

zoomOut = () => {
if (this.canvas) {
const {offsetWidth, offsetHeight} = this.canvas;
this.canvas.style.width = `${
offsetWidth - (offsetWidth * ZOOM_TICK_PERCENT)}px`;
this.canvas.style.height = `${
offsetHeight - (offsetHeight * ZOOM_TICK_PERCENT)}px`;
this.horisontalLegend.style.width = `${
offsetWidth + (offsetWidth * ZOOM_TICK_PERCENT)}px`;
this.verticalLegend.style.height = `${
offsetHeight + (offsetHeight * ZOOM_TICK_PERCENT)}px`;
this.draw();
}
};

handleZoom = (e) => {
if (e && e.shiftKey && this.cellDiameter) {
const zoomIn = e.deltaY < 0;
const nextCellDiameter = zoomIn
? this.cellDiameter + (this.cellDiameter * ZOOM_TICK_PERCENT)
: this.cellDiameter - (this.cellDiameter * ZOOM_TICK_PERCENT);
const furtherZoomPossible = zoomIn
? nextCellDiameter < CELL_DIAMETER_LIMITS.max
: nextCellDiameter > CELL_DIAMETER_LIMITS.min;
if (!furtherZoomPossible) {
return;
}
return zoomIn ? this.zoomIn() : this.zoomOut();
}
};

drawLegends = () => {
const {columns, rows} = this.props;
if (this.horisontalLegend && this.verticalLegend) {
const verticalCtx = this.verticalLegend.getContext('2d');
const horisontalCtx = this.horisontalLegend.getContext('2d');
this.verticalLegend.width = this.verticalLegend.offsetWidth;
this.verticalLegend.height = this.verticalLegend.offsetHeight;
this.horisontalLegend.height = this.horisontalLegend.offsetHeight;
this.horisontalLegend.width = this.horisontalLegend.offsetWidth;
this.drawVerticalLegend(verticalCtx, rows);
this.drawHorisontalLegend(horisontalCtx, columns);
}
};

drawHorisontalLegend = (ctx, columns) => {
ctx.clearRect(0, 0, this.horisontalLegend.width, this.horisontalLegend.height);
ctx.fillStyle = DEFAULTS.background;
ctx.fillRect(0, 0, this.horisontalLegend.width, this.horisontalLegend.height);
ctx.fillStyle = DEFAULTS.strokeStyle;
ctx.beginPath();
ctx.moveTo(CANVAS_PADDING, CANVAS_PADDING);
ctx.lineTo(this.horisontalLegend.width, CANVAS_PADDING);
ctx.stroke();
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
for (let column = 0; column < columns; column++) {
ctx.fillText(
column + 1,
(this.bounds.xFrom + (this.cellDiameter / 2)) + column * this.cellDiameter,
CANVAS_PADDING - 2
);
}
};

drawVerticalLegend = (ctx, rows) => {
ctx.clearRect(0, 0, this.verticalLegend.width, this.verticalLegend.height);
ctx.fillStyle = DEFAULTS.background;
ctx.fillRect(0, 0, this.verticalLegend.width, this.verticalLegend.height);
ctx.fillStyle = DEFAULTS.strokeStyle;
ctx.beginPath();
ctx.moveTo(CANVAS_PADDING, CANVAS_PADDING);
ctx.lineTo(CANVAS_PADDING, this.verticalLegend.height);
ctx.stroke();
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
for (let row = 0; row < rows; row++) {
ctx.fillText(
row + 1,
CANVAS_PADDING / 2,
(this.bounds.yFrom + (this.cellDiameter / 2)) + row * this.cellDiameter
);
}
};

draw = () => {
if (this.canvas && this.canvas.getContext) {
const {rows, columns} = this.props;
if (rows && columns) {
const ctx = this.canvas.getContext('2d');
this.cleanUpCanvas();
this.drawCells(ctx, rows, columns);
this.drawLegends();
this.synchronizeScrolls();
}
}
};
Expand Down Expand Up @@ -138,59 +288,72 @@ class HcsControlGrid extends React.Component {
}
};

zoomIn = () => {
if (this.canvas) {
const {style, offsetWidth, offsetHeight} = this.canvas;
style.width = `${offsetWidth + (offsetWidth * ZOOM_TICK_PERCENT)}px`;
style.height = `${offsetHeight + (offsetHeight * ZOOM_TICK_PERCENT)}px`;
this.draw();
}
};

zoomOut = () => {
if (this.canvas) {
const {style, offsetWidth, offsetHeight} = this.canvas;
style.width = `${offsetWidth - (offsetWidth * ZOOM_TICK_PERCENT)}px`;
style.height = `${offsetHeight - (offsetHeight * ZOOM_TICK_PERCENT)}px`;
this.draw();
}
};

handleZoom = (e) => {
if (e && e.shiftKey && this.cellDiameter) {
const zoomIn = e.deltaY < 0;
const nextCellDiameter = zoomIn
? this.cellDiameter + (this.cellDiameter * ZOOM_TICK_PERCENT)
: this.cellDiameter - (this.cellDiameter * ZOOM_TICK_PERCENT);
const furtherZoomPossible = zoomIn
? nextCellDiameter < CELL_DIAMETER_LIMITS.max
: nextCellDiameter > CELL_DIAMETER_LIMITS.min;
if (!furtherZoomPossible) {
return;
}
return zoomIn ? this.zoomIn() : this.zoomOut();
}
};

// todo: add canvas legend components, synced up with main plot scrolling
// todo: dynamic canvas height, based on (rows * cellDiameter) + paddings
// todo: scroll positions synced with event(x, y)?

render () {
const {
style
} = this.props;
return (
<div style={Object.assign({overflow: 'auto'}, style)}>
<canvas
style={{
position: 'relative',
width: '100%',
height: '100%',
border: '1px solid #dfdfdf'
}}
ref={this.initializeCanvas}
/>
<div style={{position: 'relative', overflow: 'hidden'}}>
<div
ref={this.initializeCanvasContainer}
style={Object.assign({overflow: 'auto'}, style)}
>
<canvas
style={{
width: '100%',
height: '100%'
}}
ref={this.initializeCanvas}
/>
<div
style={{
width: '100%',
height: CANVAS_PADDING,
overflow: 'hidden',
position: 'absolute',
top: 0,
left: 0
}}
ref={(canvas) => { this.horisontalLegendContainer = canvas; }}
>
<canvas
style={{
width: '100%',
height: CANVAS_PADDING
}}
ref={(canvas) => this.initializeLegends(canvas, 'horisontal')}
/>
</div>
<div
style={{
width: CANVAS_PADDING,
height: '100%',
overflow: 'hidden',
position: 'absolute',
top: 0,
left: 0
}}
ref={(canvas) => { this.verticalLegendContainer = canvas; }}
>
<canvas
style={{
width: CANVAS_PADDING,
height: '100%'
}}
ref={(canvas) => this.initializeLegends(canvas, 'vertical')}
/>
</div>
<span
style={{
position: 'absolute',
top: 0,
left: 0,
width: CANVAS_PADDING,
height: CANVAS_PADDING,
background: DEFAULTS.background
}}
/>
</div>
</div>
);
}
Expand Down

0 comments on commit ab0a68d

Please sign in to comment.