Published
Edited
Dec 25, 2021
2 stars
Insert cell
Insert cell
Insert cell
class Chart {
constructor() {
// Defining state attributes
const attrs = {
id: this.createId(),
svgWidth: 400,
svgHeight: 400,
marginTop: 25,
marginBottom: 25,
marginRight: 25,
marginLeft: 25,
container: "body",
defaultTextFill: "#2C3E50",
defaultFont: "Helvetica",
ctx: document.createElement("canvas").getContext("2d"),
zeroBased: true,
data: null,
calc: null,
svg: null,
chart: null,
d3Container: null,
yTickMax: 100
};

// Defining accessors
this.getState = () => attrs;
this.setState = (d) => Object.assign(attrs, d);

// Automatically generate getter and setters for chart object based on the state properties;
Object.keys(attrs).forEach((key) => {
//@ts-ignore
this[key] = function (_) {
if (!arguments.length) {
return attrs[key];
}
attrs[key] = _;
return this;
};
});

// Custom enter exit update pattern initialization (prototype method)
this.initializeEnterExitUpdatePattern();
}
}
Insert cell
initializeEnterExitUpdatePattern = (Chart.prototype.initializeEnterExitUpdatePattern = function () {
d3.selection.prototype._add = function (params) {
var container = this;
var className = params.className;
var elementTag = params.tag;
var data = params.data || [className];
var exitTransition = params.exitTransition || null;
var enterTransition = params.enterTransition || null;
// Pattern in action
var selection = container.selectAll("." + className).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", className);
return selection;
};
})
Insert cell
render = (Chart.prototype.render = function () {
// Define containers and set SVG width based on container size
this.setDynamicContainer();

// Calculate some properties
this.calculateProperties();

// Create scales
this.createScales(this.getState());

// Draw SVG and its wrappers
this.drawSvgAndWrappers();

// Drawing x and y axises
this.drawAxises(this.getState());

// Draw Lines
this.drawLines(this.getState());

// Draw voronoi tips
this.attachVoronoiTip(this.getState());

// listen for resize event and reRender accordingly
this.reRenderOnResize();

// Allow chaining
return this;
})
Insert cell
setDynamicContainer = (Chart.prototype.setDynamicContainer = function () {
const { container, svgWidth } = this.getState();

// Drawing containers
const d3Container = d3.select(container);
var containerRect = d3Container.node().getBoundingClientRect();
let newSvgWidth = containerRect.width > 0 ? containerRect.width : svgWidth;
this.setState({ d3Container, svgWidth: newSvgWidth });
})
Insert cell
calculateProperties = (Chart.prototype.calculateProperties = function () {
const {
marginTop,
marginLeft,
marginRight,
marginBottom,
svgWidth,
svgHeight
} = this.getState();

// Calculated properties
var calc = {
id: this.createId(), // id for event handlings,
chartTopMargin: marginTop,
chartLeftMargin: marginLeft,
chartWidth: svgWidth - marginRight - marginLeft,
chartHeight: svgHeight - marginBottom - marginTop
};

this.setState({ calc });
})
Insert cell
createScales = (Chart.prototype.createScales = function ({
data,
yTickMax,
zeroBased,
calc: { chartWidth, chartHeight }
}) {
const minX = d3.min(data, (lineData) => d3.min(lineData, (d) => d.x));
const maxX = d3.max(data, (lineData) => d3.max(lineData, (d) => d.x));
let minY = d3.min(data, (lineData) => d3.min(lineData, (d) => d.y));
if (zeroBased) minY = 0;
let maxY = d3.max(data, (lineData) => d3.max(lineData, (d) => d.x));
if (yTickMax) maxY = yTickMax;
const scaleX = d3.scaleLinear().domain([minX, maxX]).range([0, chartWidth]);
const scaleY = d3.scaleLinear().domain([minY, maxY]).range([chartHeight, 0]);
this.setState({ scaleX, scaleY });
})
Insert cell
drawSvgAndWrappers = (Chart.prototype.drawSvgAndWrappers = function () {
const {
d3Container,
svgWidth,
svgHeight,
defaultFont,
calc
} = this.getState();
const { chartLeftMargin, chartTopMargin, chartWidth, chartHeight } = calc;

// Create tip instance
const tip = d3Tip()
.offset([-10, 0])
.attr("class", "d3-tip")
.html(
(EVENT, d) => `
<table>
<tr><td>Name</td><td>Value<td></tr>
</table>
`
);

// Draw SVG
const svg = d3Container
._add({ tag: "svg", className: "svg-chart-container" })
.attr("width", svgWidth)
.attr("height", svgHeight)
.style("background-color", "#fafafa")
.attr("font-family", defaultFont);

svg.call(tip);

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

this.setState({ chart, svg, tip });
})
Insert cell
drawAxises = (Chart.prototype.drawAxises = function ({
scaleX,
scaleY,
chart,
calc: { chartWidth, chartHeight }
}) {
const axisWrapper = chart._add({
tag: "g",
className: "axis-wrapper"
});
const xAxisWrapper = axisWrapper
._add({
tag: "g",
className: "x-axis-wrapper"
})
.attr("transform", `translate(0,${chartHeight})`);
const xAxis = d3.axisBottom(scaleX);
xAxisWrapper.call(xAxis);

const yAxisWrapper = axisWrapper._add({
tag: "g",
className: "y-axis-wrapper"
});

const yAxis = d3.axisLeft(scaleY);
yAxisWrapper.call(yAxis);
})
Insert cell
drawLines = (Chart.prototype.drawLines = function ({
data,
chart,
scaleX,
scaleY
}) {
const line = d3
.line()
.x((d) => scaleX(d.x))
.y((d) => scaleY(d.y));
const linesWrapper = chart._add({
tag: "g",
className: "lines-wrapper"
});

linesWrapper
._add({ tag: "path", className: "line-paths", data: data })
.attr("d", (d) => line(d))
.attr("fill", "none")
.attr("stroke-width", 2)
.attr("stroke", "black");
})
Insert cell
attachVoronoiTip = (Chart.prototype.attachVoronoiTip = function ({
data,
chart,
scaleX,
scaleY,
tip,
calc: { chartWidth, chartHeight }
}) {
const voronoi = d3.Delaunay.from(
data.flat(),
(d) => scaleX(d.x),
(d) => scaleY(d.y)
).voronoi([0, 0, chartWidth, chartHeight]);

chart
._add({ tag: "g", className: "voronoi-wrapper" })
._add({ tag: "path", className: "voronoi-path", data: data.flat() })
.attr("opacity", 0.5)
.attr("stroke", "#ff1493")
.attr("fill", "none")
.style("pointer-events", "all")
.attr("d", (d, i) => {
const pathD = voronoi.renderCell(i);
return pathD;
})
.on("mouseenter", (event, d, i) => {
const c = chart
._add({
tag: "circle",
className: "mouse-enter-circle"
})
.attr("r", 3)
.attr("cx", scaleX(d.x))
.attr("cy", scaleY(d.y));
tip.show(event, d, c.node());
})
.on("mouseleave", () => {
chart.selectAll(".mouse-enter-circle").remove();
tip.hide();
});
})
Insert cell
reRenderOnResize = (Chart.prototype.reRenderOnResize = function () {
const { id, d3Container, svgWidth } = this.getState();
d3.select(window).on("resize." + id, () => {
const containerRect = d3Container.node().getBoundingClientRect();
const newSvgWidth =
containerRect.width > 0 ? containerRect.width : svgWidth;
this.setState({ svgWidth: newSvgWidth });
this.render();
});
})
Insert cell
Insert cell
createId = (Chart.prototype.createId = function () {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
})
Insert cell
chartContainer = html`<div style="width:700px; height:350px"></div>`
Insert cell
chart = {
// wait for 1 sec, to allow all external methods to be linked with our class, then invoke it (we won't need it in the real application)
return new Promise((res) => {
setTimeout((d) => {
let ch = new Chart()
.container(chartContainer)
.svgHeight(300)
.marginLeft(100)
.data([
[
{ x: 10, y: 100 },
{ x: 20, y: 90 },
{ x: 50, y: 10 },
{ x: 70, y: 80 }
],
[
{ x: 10, y: 90 },
{ x: 20, y: 70 },
{ x: 50, y: 20 },
{ x: 60, y: 50 },
{ x: 90, y: 5 }
]
])
.render();

res(ch);
}, 1000);
});
}
Insert cell
Insert cell
d3 = require("d3@v6")
Insert cell
d3V6Tip = require("d3-v6-tip", "d3-delaunay@4")
Insert cell
d3Tip = d3V6Tip.tip
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