Published
Edited
Oct 30, 2021
2 forks
5 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
class GridHeatmap {
constructor() {
const attrs = {
id: "ID" + Math.floor(Math.random() * 1000000),
svgWidth: 400,
svgHeight: 400,
marginTop: 100,
marginBottom: 5,
marginRight: 15,
marginLeft: 15,
container: "body",
defaultTextFill: "#2C3E50",
defaultFont: "Helvetica",
data: null,
chartWidth: null,
chartHeight: null,
rowsCount: null,
columnsCount: null,
firstDraw: true
};
this.getState = () => attrs;
this.setState = (d) => Object.assign(attrs, d);
Object.keys(attrs).forEach((key) => {
//@ts-ignore
this[key] = function (_) {
var string = `attrs['${key}'] = _`;
if (!arguments.length) {
return eval(`attrs['${key}'];`);
}
eval(string);
return this;
};
});
this.initializeEnterExitUpdatePattern();
}

initializeEnterExitUpdatePattern() {
d3.selection.prototype.patternify = function (params) {
var container = this;
var selector = params.selector;
var elementTag = params.tag;
var data = params.data || [selector];
var exitTransition = params.exitTransition || null;
var enterTransition = params.enterTransition || null;
// Pattern in action
var selection = container.selectAll("." + selector).data(data, (d, i) => {
if (typeof d === "object") {
if (d.id) {
return d.id;
}
}
return i;
});
if (exitTransition) {
exitTransition(selection);
} else {
selection.exit().remove();
}

const enterSelection = selection.enter().append(elementTag);
if (enterTransition) {
enterTransition(enterSelection);
}
selection = enterSelection.merge(selection);
selection.attr("class", selector);
return selection;
};
}

// ================== RENDERING ===================
render() {
this.setDynamicContainer();
this.calculateProperties();
this.drawSvgAndWrappers();
this.drawGrids();
this.drawGradient();

this.setState({ firstDraw: false });
return this;
}

drawGradient() {
const {
customColorInterpolator,
svg,
chartWidth,
marginLeft
} = this.getState();

const gradient = svg
.patternify({ tag: "g", selector: "gradient-wrapper" })
.attr("transform", `translate(${marginLeft},40)`);

gradient
.patternify({ tag: "text", selector: "title" })
.text("Similarity")
.attr("fill", "gray")
.attr("y", -10)
.attr("font-size", 13);

const gradientScale = d3
.scaleLinear()
.domain([0, 100])
.range([0, chartWidth]);

const gradientAxis = d3.axisBottom(gradientScale);
const axis = gradient
.patternify({ tag: "g", selector: "gradient-axis-wrapper" })
.attr("transform", `translate(${0},20)`)
.call(gradientAxis);

axis.selectAll("text").attr("fill", "gray");
axis.selectAll("line").attr("stroke", "gray");

gradient
.patternify({
tag: "rect",
selector: "gradient-color",
data: d3.range(100)
})
.attr("x", (d, i, arr) => (chartWidth / arr.length) * i)
.attr("height", 20)
.attr("width", (d, i, arr) => chartWidth / arr.length)
.attr("fill", (d, i, arr) => customColorInterpolator(i / arr.length))
.attr("stroke", (d, i, arr) => customColorInterpolator(i / arr.length));
}

drawGrids() {
const {
data,
chart,
rowsCount,
columnsCount,
chartWidth,
chartHeight
} = this.getState();
const eachCellWidth = chartWidth / columnsCount;
const eachCellHeight = chartHeight / rowsCount;

const xLabels = data.xLabels.map((d, i) => {
return {
type: "label",
row: i,
label: d,
column: 0
};
});

const yLabels = data.yLabels.map((d, j) => {
return {
type: "label",
label: d,
row: xLabels.length,
column: j + 1
};
});

const values = data.values
.map((arr, row) => {
return arr.map((item, column) => ({
column: column + 1,
row: row,
type: "value",
value: item,
colorValue: data.colorValues[row][column]
}));
})
.flat();

const scaleX = d3
.scaleLinear()
.domain([0, 1])
.range([0, eachCellWidth - 10]);
const scaleY = d3
.scaleLinear()
.domain([0, 1])
.range([0, eachCellHeight - 10]);

const colors = ["white", "teal"];
const customColorInterpolator = d3
.scaleLinear()
.domain(colors.map((d, i, arr) => i / (arr.length - 1)))
.range(colors);

let cellsData = values.concat(xLabels).concat(yLabels);

const gridWrapper = chart.patternify({
tag: "g",
selector: "grid-wrapper"
});

const cells = gridWrapper
.patternify({
tag: "g",
selector: "cell",
data: cellsData
})
.attr("transform", (d, i) => {
return `translate(${(d.column - 0) * eachCellWidth}, ${
(d.row - 0) * eachCellHeight
})`;
});

cells
.patternify({ tag: "rect", selector: "borders", data: (d) => [d] })
.attr("x", 0)
.attr("y", 0)
.attr("fill", "none")
.attr("stroke", "#D1D4D8")
.attr("stroke-width", 0.5)
.attr("width", eachCellWidth)
.attr("height", eachCellHeight);

cells
.filter((d) => d.type === "value")
.patternify({
tag: "rect",
selector: "value-rect",
data: (d) => [d]
})
.each((d) => {
const width = scaleX(d.value);
const height = scaleY(d.value);

d.width = width;
d.height = height;
d.x = (eachCellWidth - width) / 2;
d.y = (eachCellHeight - height) / 2;
})
.transition()
.attr("x", (d) => d.x)
.attr("y", (d) => d.y)
.attr("fill", (d) => customColorInterpolator(d.colorValue))
.attr("width", (d) => d.width)
.attr("height", (d) => d.height);

cells
.filter((d) => d.type === "label")
.patternify({
tag: "foreignObject",
selector: "label-fo",
data: (d) => [d]
})
.attr("width", eachCellWidth + "px")
.attr("height", eachCellHeight + "px")
.patternify({
tag: "xhtml:div",
selector: "label-text",
data: (d) => [d]
})
.style("width", eachCellWidth + "px")
.style("height", eachCellHeight + "px")
.style("color", "gray")
.style("font-size", "13px")
.style("display", "flex")
.style("justify-content", "center")
.style("align-items", "center")
.html((d) => `${d.label}`);

this.setState({ customColorInterpolator });
}

setDynamicContainer() {
const attrs = this.getState();

if (!attrs.firstDraw) return;

//Drawing containers
var container = d3.select(attrs.container);

var containerRect = container.node().getBoundingClientRect();
if (containerRect.width > 0) attrs.svgWidth = containerRect.width;
this.setState({ container });
}

drawSvgAndWrappers() {
const {
container,
svgWidth,
svgHeight,
defaultFont,
calc
} = this.getState();

// Draw SVG
const svg = container
.patternify({
tag: "svg",
selector: "svg-chart-container"
})
.attr("width", svgWidth)
.attr("height", svgHeight)
.attr("font-family", defaultFont);

//Add container g element
var chart = svg
.patternify({
tag: "g",
selector: "chart"
})
.attr(
"transform",
"translate(" + calc.chartLeftMargin + "," + calc.chartTopMargin + ")"
);

this.setState({ svg, chart });
}

calculateProperties() {
const {
data,
marginLeft,
marginTop,
marginRight,
marginBottom,
svgWidth,
svgHeight
} = this.getState();

//Calculated properties
var calc = {
id: null,
chartTopMargin: null,
chartLeftMargin: null,
chartWidth: null,
chartHeight: null
};
calc.id = "ID" + Math.floor(Math.random() * 1000000); // id for event handlings
calc.chartLeftMargin = marginLeft;
calc.chartTopMargin = marginTop;
const chartWidth = svgWidth - marginRight - calc.chartLeftMargin;
const chartHeight = svgHeight - marginBottom - calc.chartTopMargin;
const rowsCount = data.values.length + 1;
const columnsCount = data.values[0].length + 1;

this.setState({
calc,
chartWidth,
chartHeight,
rowsCount,
columnsCount
});
}

updateData(data) {
const attrs = this.getChartState();
console.log("smoothly updating data");
return this;
}
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more