Published
Edited
Aug 31, 2020
2 stars
Insert cell
Insert cell
Insert cell
Insert cell
svg`
<svg id="canvas"></svg>
`
Insert cell
width = 900
Insert cell
height = 400
Insert cell
padding = 15
Insert cell
dimensionX = "Horsepower"
Insert cell
dimensionY = "Miles_per_Gallon";
Insert cell
{
// GLOBAL CONFIGURATION ////////////////////////////////////////////////////////////////////////////

const SVG_NAMESPACE = "http://www.w3.org/2000/svg";

const svg = document.querySelector("svg");

let dataset = [];

const domainY = [0, 1];
const domainX = [0, 1];
const rangeX = [padding, width - padding];
const rangeY = [padding, height - padding];

const fontSize = 11;
const fontFamily = "sans-serif";
const tickSize = 5;


// SCALES //////////////////////////////////////////////////////////////////////////////////////////

function scaleX(value) {
const scaledValue = (value - domainX[0]) / domainX[1];
const pixelValue = scaledValue * (rangeX[1] - rangeX[0]) + rangeX[0];
return pixelValue;
}
function invertX(pixelValue) {
const scaledPixelValue = (pixelValue - rangeX[0]) / rangeX[1];
const value = scaledPixelValue * (domainX[1] - domainX[0]) + domainX[0];
return value;
}

function scaleY(value) {
const scaledValue = (value - domainY[0]) / domainY[1];
const pixelValue = scaledValue * (rangeY[1] - rangeY[0]) + rangeY[0];
return pixelValue;
}
function invertY(pixelValue) {
const scaledPixelValue = (pixelValue - rangeY[0]) / rangeY[1];
const value = scaledPixelValue * (domainY[1] - domainY[0]) + domainY[0];
return value;
}


// AXES ////////////////////////////////////////////////////////////////////////////////////////////

function drawAxisGuide(orientation, axisContainer) {
const guide = document.createElementNS(SVG_NAMESPACE, "line");
guide.setAttribute("x1", rangeX[0]);
guide.setAttribute("y1", rangeY[0]);
guide.setAttribute("stroke", "black");

if (orientation === "bottom") {
guide.setAttribute("x2", rangeX[1]);
guide.setAttribute("y2", rangeY[0]);
} else if (orientation === "right") {
guide.setAttribute("x2", rangeX[0]);
guide.setAttribute("y2", rangeY[1]);
}

axisContainer.appendChild(guide);
}

function drawAxisTicks(orientation, axisContainer) {
const ticks = document.createElementNS(SVG_NAMESPACE, "g");
ticks.classList.add("ticks");

for (let i = 0; i < 10; i++) {
const tick = document.createElementNS(SVG_NAMESPACE, "line");
let tickPosition = 0;

if (orientation === "bottom") {
tickPosition = (rangeX[1] - rangeX[0]) / 10 * (i + 1) + rangeX[0];
tick.setAttribute("x1", tickPosition);
tick.setAttribute("x2", tickPosition);
tick.setAttribute("y1", rangeY[0]);
tick.setAttribute("y2", rangeY[0] + tickSize);
} else if (orientation === "right") {
tickPosition = (rangeY[1] - rangeY[0]) / 10 * (i + 1) + rangeY[0];
tick.setAttribute("x1", rangeX[0]);
tick.setAttribute("x2", rangeX[0] + tickSize);
tick.setAttribute("y1", tickPosition);
tick.setAttribute("y2", tickPosition);
}

tick.setAttribute("stroke", "black");
ticks.appendChild(tick);
}

axisContainer.appendChild(ticks);
}

function drawAxisLabels(orientation, axisContainer) {
const lables = document.createElementNS(SVG_NAMESPACE, "g");
lables.classList.add("labels");

for (let i = 0; i < 10; i++) {
const label = document.createElementNS(SVG_NAMESPACE, "text");
let labelPosition = 0;

if (orientation === "bottom") {
labelPosition = (rangeX[1] - rangeX[0]) / 10 * (i + 1) + rangeX[0];
label.setAttribute("x", labelPosition);
label.setAttribute("y", rangeY[0]);
label.setAttribute("text-anchor", "middle");
label.setAttribute("font-size", fontSize);
label.setAttribute("font-family", fontFamily);
label.setAttribute("dy", tickSize + 10);
label.innerHTML = "<" + parseInt(invertX(labelPosition));
} else if (orientation === "right") {
labelPosition = (rangeY[1] - rangeY[0]) / 10 * (i + 1) + rangeY[0];
label.setAttribute("x", rangeX[0]);
label.setAttribute("y", labelPosition);
label.setAttribute("font-size", fontSize);
label.setAttribute("font-family", fontFamily);
label.setAttribute("dominant-baseline", "middle");
label.setAttribute("dx", tickSize + 5);
label.innerHTML = "<" + parseInt(invertY(labelPosition));
}

lables.appendChild(label);
}

axisContainer.appendChild(lables);
}

function drawAxis(orientation, axisContainer) {
axisContainer.classList.add("axis", orientation);

drawAxisGuide(orientation, axisContainer);
drawAxisTicks(orientation, axisContainer);
drawAxisLabels(orientation, axisContainer);
}

function drawAxes() {
const xAxis = document.createElementNS(SVG_NAMESPACE, "g");
const yAxis = document.createElementNS(SVG_NAMESPACE, "g");

drawAxis("bottom", xAxis);
drawAxis("right", yAxis);

svg.appendChild(xAxis);
svg.appendChild(yAxis);
}


// MARKS //////////////////////////////////////////////////////////////////////////////////////////

function drawCircleMark(data, markContainer) {
const radius = 5;

data.forEach(datum => {
const circleMark = document.createElementNS(SVG_NAMESPACE, "circle");
circleMark.classList.add("mark");
circleMark.setAttribute("cx", scaleX(datum[dimensionX]));
circleMark.setAttribute("cy", scaleY(datum[dimensionY]));
circleMark.setAttribute("r", radius);
circleMark.setAttribute("fill", "steelblue");
circleMark.setAttribute("fill-opacity", 0.73)
markContainer.appendChild(circleMark);
});
}

function drawTextMark(data, markContainer) {
const spacing = 10;

data.forEach(datum => {
const textMark = document.createElementNS(SVG_NAMESPACE, "text");
textMark.classList.add("mark");
textMark.setAttribute("x", scaleX(datum[dimensionX]));
textMark.setAttribute("y", scaleY(datum[dimensionY]));
textMark.setAttribute("font-size", fontSize);
textMark.setAttribute("font-family", fontFamily);
textMark.setAttribute("dominant-baseline", "middle");
textMark.setAttribute("dx", spacing)
textMark.innerHTML = `(${datum[dimensionX]}, ${datum[dimensionY]})`;
markContainer.appendChild(textMark);
});
}

function drawMark(data, markType) {
const marks = document.createElementNS(SVG_NAMESPACE, "g");
marks.classList.add("marks");

if (markType === "circle") {
drawCircleMark(data, marks);
} else if (markType === "text") {
drawTextMark(data, marks);
}

svg.appendChild(marks);
}


// DRAW ////////////////////////////////////////////////////////////////////////////////////////////

function resizeCanvas() {
rangeX[0] = padding;
rangeX[1] = width - padding;
rangeY[0] = padding;
rangeY[1] = height - padding;

svg.setAttribute("width", width);
svg.setAttribute("height", height);
}

function draw(withLabels = false) {
while (svg.hasChildNodes()) {
svg.removeChild(svg.firstChild);
}

resizeCanvas();

drawAxes();
drawMark(dataset, "circle");

if (withLabels) {
drawMark(dataset, "text");
}
}

// DATA ////////////////////////////////////////////////////////////////////////////////////////////

fetch("https://vega.github.io/editor/data/cars.json")
.then(res => res.json())
.then(data => {
dataset = data;

domainX[1] = dataset.reduce((previous, next) => Math.max(previous, next[dimensionX]), 0);
domainY[1] = dataset.reduce((previous, next) => Math.max(previous, next[dimensionY]), 0);

draw();
});
}
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