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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more