Published
Edited
Jan 7, 2020
Importers
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
html`<a target="_blank" href="https://observablehq.com/@loredanacirstea/comicstrip#${btoa(jsontoyaml.stringify(JSON.stringify(cstripobj)))}">https://observablehq.com/@loredanacirstea/comicstrip#${btoa(jsontoyaml.stringify(JSON.stringify(cstripobj)))}</a>`
Insert cell
Insert cell
html`<a target="_blank" href="${svgToDataUri(document.querySelector('svg'))}">${svgToDataUri(document.querySelector('svg'))}</a>`
Insert cell
Insert cell
run()
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function run() {
let loadedObj = window.location.hash.substring(1);
if (loadedObj) {
try {
const obj = JSON.parse(yamltojson.load(atob(loadedObj)));
cstripobj.title = obj.title;
cstripobj.description = obj.description;
cstripobj.images = obj.images;
cstripobj.comments = obj.comments;
cstripobj.options = obj.options || cstripobj.options || {};
} catch (e) {}
}
cstrip('#ex', cstripobj)
}
Insert cell
function addImge(url) {
console.log('url', url)
const newimage = {
url,
center: {x: 300, y: 150},
circle: {x: 300, y: 100},
}
cstripobj.images.push(newimage);
run();
}
Insert cell
function cstrip(domid, cstripObj) {
const DEFAULT_OPTIONS = {
box: {opacity: 0.6, radius: 10},
svg: {width: 960, height: 500, viewBox: {width: 960, height: 500}},
text: {fontSize: 30, fontFamily: "sans-serif"},
};
const options = {
box: Object.assign({}, DEFAULT_OPTIONS.box, cstripObj.options.box || {}),
text: Object.assign({}, DEFAULT_OPTIONS.text, cstripObj.options.text || {}),
svg: Object.assign({}, DEFAULT_OPTIONS.svg, cstripObj.options.svg || {}),
}
d3.select(domid).html('')
var svg = d3.select(domid).append("svg")

svg.attr("width", "100%").attr("height", "100%")
.attr("viewBox","0 0 " + options.svg.viewBox.width +" " + options.svg.viewBox.height)
.attr("preserveAspectRatio", "xMinYMin meet")

options.id = domid.substring(1);
cstripImages(svg, cstripObj.images, options);
cstripComments(svg, cstripObj.comments, options);
toggleControls(svg, false);
svg.on('dblclick', function() {
cstripObj.comments.push(newComment(d3.mouse(this)));
cstripComments(svg, cstripObj.comments, options);
})
svg.on('mouseenter', () => {toggleControls(svg, true)});
svg.on('mouseleave', () => {toggleControls(svg, false)});
}
Insert cell
function toggleControls(svg, visible) {
const visibility = visible ? "visible" : "hidden";
svg.selectAll(".imagemove").attr("visibility", visibility);
svg.selectAll(".imageresrot").attr("visibility", visibility);
svg.selectAll(".boxmove").attr("visibility", visibility);
svg.selectAll(".boxsource").attr("visibility", visibility);
svg.selectAll(".boxresize").attr("visibility", visibility);
}
Insert cell
function newComment(coords) {
return {
topleft: {x: coords[0], y: coords[1]},
bottomright: {x: coords[0] + 150, y: coords[1] + 100},
source: {x: coords[0] + 50, y: coords[1] + 150},
text: 'Placeholder',
}
}
Insert cell
function cstripImages(svg, images, options) {
const getimageid = i => `panel_${options.id}_csimage_${i}`;
svg.selectAll(".csimage")
.data(images)
.join("image")
.attr("xlink:href", cobj => cobj.url)
.attr("id", (cobj, i) => getimageid(i))
.attr("class", "csimage")
.attr("aratio", function(cobj) {
const inibbox = this.getBoundingClientRect();
const w = inibbox.width || 200;
const h = inibbox.height || 200;
return String(w / h);
})
.attr("width", function(cobj) {
// setImageBbox(this, cobj, i)
const bbox = getImageBbox(this, cobj);
d3.select(this)
.attr("x", bbox.x)
.attr("y", bbox.y)
.attr("width", bbox.width)
.attr("height", bbox.height)
.attr('transform', `rotate(${bbox.angle}, ${cobj.center.x}, ${cobj.center.y})`);
return bbox.width;
})
svg.selectAll(".imagemove")
.data(images)
.join("rect")
.attr("class", "imagemove")
.attr("id", (cobj, i) => `${getimageid(i)}_imagemove`)
.attr("imageid", (cobj, i) => getimageid(i))
.attr("x", d => d.center.x - 5)
.attr("y", d => d.center.y - 5)
.attr("width", 10)
.attr("height", 10)
.attr("fill", "yellow")
.attr("stroke", "black")
.attr("stroke-width", 2)
.call(dragImageMove())
svg.selectAll(".imageresrot")
.data(images)
.join("circle")
.attr("class", "imageresrot")
.attr("id", (cobj, i) => `${getimageid(i)}_imageresrot`)
.attr("imageid", (cobj, i) => getimageid(i))
.attr("cx", d => d.circle.x)
.attr("cy", d => d.circle.y)
.attr("r", 10)
.attr("fill", "blue")
.attr("stroke", "black")
.attr("stroke-width", 2)
.call(dragImageResRot())
}
Insert cell
function dist(a, b) {
const pozitive = (a) => a * Math.sign(a);
const s1 = pozitive(b.x - a.x);
const s2 = pozitive(b.y - a.y);
return Math.sqrt(s1**2 + s2**2);
}
Insert cell
function getImageBbox(elem, cobj) {
const aratio = parseFloat(d3.select(elem).attr('aratio'));
const height = dist(cobj.center, cobj.circle) * 5;
const width = height * aratio;
const angle = getAngle(cobj.center, cobj.circle) * 180 / Math.PI;
return {x: cobj.center.x - width/2, y: cobj.center.y - height/2, width, height, angle};
}
Insert cell
function setImageBbox(elem, cobj) {
const bbox = getImageBbox(elem, cobj);
d3.select(elem)
.attr("x", bbox.x)
.attr("y", bbox.y)
.attr("width", bbox.width)
.attr("height", bbox.height)
.attr('transform', `rotate(${bbox.angle}, ${cobj.center.x}, ${cobj.center.y})`);
}
Insert cell
function cstripComments(svg, comments, options) {
const getwidth = p => p.bottomright.x - p.topleft.x;
const getheight = p => p.bottomright.y - p.topleft.y;
const getboxid = i => `panel_${options.id}_box_${i}`;
function removeComment(cobj, i) {
d3.event.stopPropagation();
comments.splice(i, 1);
cstripComments(svg, comments, options);
}

svg.selectAll("path")
.data(comments)
.join("path")
.attr("stroke", "black")
.attr("stroke-width", 2)
.attr("fill", "white")
.attr("opacity", String(options.box.opacity))
.attr("id", (cobj, i) => getboxid(i))
.attr("d", (cobj, i) => {
return commentPath(cobj, options.box)
})
svg.selectAll(".boxmove")
.data(comments)
.join("circle")
.attr("class", "boxmove")
.attr("id", (cobj, i) => `${getboxid(i)}_boxmove`)
.attr("boxid", (cobj, i) => getboxid(i))
.attr("cx", d => d.topleft.x)
.attr("cy", d => d.topleft.y)
.attr("r", options.box.radius)
.attr("fill", "black")
.call(dragBox())
.on('dblclick', (cobj, i) => removeComment(cobj, i));

svg.selectAll(".boxsource")
.data(comments)
.join("circle")
.attr("class", "boxsource")
.attr("id", (cobj, i) => `${getboxid(i)}_boxsource`)
.attr("boxid", (cobj, i) => getboxid(i))
.attr("cx", d => d.source.x)
.attr("cy", d => d.source.y)
.attr("r", options.box.radius)
.attr("fill", "black")
.call(dragBoxSource());
svg.selectAll(".boxresize")
.data(comments)
.join("circle")
.attr("class", "boxresize")
.attr("id", (cobj, i) => `${getboxid(i)}_boxresize`)
.attr("boxid", (cobj, i) => getboxid(i))
.attr("cx", d => d.bottomright.x)
.attr("cy", d => d.bottomright.y)
.attr("r", options.box.radius)
.attr("fill", "black")
.call(dragBoxResize());
svg.selectAll(".boxtext")
.data(comments)
.join("foreignObject")
.attr('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility')
.attr("id", (cobj, i) => `${getboxid(i)}_boxtext`)
.attr("class", "boxtext")
.attr("boxid", (cobj, i) => getboxid(i))
.attr('width', d => getwidth(d))
.attr('height', d => getheight(d))
.attr("x", d => d.topleft.x)
.attr("y", d => d.topleft.y)
.attr("font-size", options.text.fontSize)
.attr("font-family", options.text.fontFamily)
.attr("text-anchor", "middle")
.attr("font-weight", "normal")
.html('')
.append('xhtml:div')
.attr("class", "boxtextdiv")
.style("padding-left", `${options.box.radius}px`)
.style('width', d => getwidth(d))
.style('height', d => getheight(d))
.style('max-width', d => getwidth(d))
.style('max-height', d => getheight(d))
.style('min-width', d => getwidth(d))
.style('min-height', d => getheight(d))
.attr("id", (cobj, i) => `${getboxid(i)}_boxtextdiv`)
.attr("boxid", (cobj, i) => getboxid(i))
.attr("contenteditable", true)
.html(d => d.text)
.on('input', function (cobj) {
const newtext = d3.select(this).node().innerHTML;
cobj.text = newtext;
});
}
Insert cell
function getAngle(center, p1) {
var p0 = {x: center.x, y: center.y - Math.sqrt(Math.abs(p1.x - center.x) * Math.abs(p1.x - center.x) + Math.abs(p1.y - center.y) * Math.abs(p1.y - center.y))};
return (2 * Math.atan2(p1.y - p0.y, p1.x - p0.x));// * 180 / Math.PI;
}
Insert cell
function dragImageResRot() {

function dragstarted(d) {
d3.select(this).raise().attr("stroke", "black");
}

function dragged(d) {
const coords = d3.mouse(this);
const imageid = d3.select(this).attr("imageid");
const scimage = d3.select(`#${imageid}`);
d3.select(this).attr("cx", d.circle.x = coords[0]).attr("cy", d.circle.y = coords[1]);
setImageBbox(scimage.node(), d);
}

function dragended(d) {
d3.select(this).attr("stroke", null);
}

return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
Insert cell
function dragImageMove() {

function dragstarted(d) {
d3.select(this).raise().attr("stroke", "black");
}

function dragged(d) {
let coords= d3.mouse(this);
const diff = {x: coords[0] - d.center.x, y: coords[1] - d.center.y}
d3.select(this).attr("x", coords[0] - 5).attr("y", coords[1] - 5);
d.center.x = coords[0];
d.center.y = coords[1];
d.circle.x += diff.x;
d.circle.y += diff.y;
const imageid = d3.select(this).attr("imageid");
const csimage = d3.select(`#${imageid}`)
setImageBbox(csimage.node(), d);
d3.select(`#${imageid}_imageresrot`).attr("cx", d.circle.x).attr("cy", d.circle.y)
}

function dragended(d) {
d3.select(this).attr("stroke", null);
}

return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
Insert cell
function dragBox() {

function dragstarted(d) {
d3.select(this).raise().attr("stroke", "black");
}

function dragged(d) {
let coords= d3.mouse(this);
const diff = {x: coords[0] - d.topleft.x, y: coords[1] - d.topleft.y}
d3.select(this).attr("cx", d.topleft.x = coords[0]).attr("cy", d.topleft.y = coords[1]);
const boxid = d3.select(this).attr("boxid");
const box = d3.select(`#${boxid}`)
.attr("d", commentPath(d, {radius: 10}));
// const boxsource = d3.select(`#${boxid}_boxsource`)
// .attr("cx", d.source.x = d.source.x + diff.x)
// .attr("cy", d.source.y = d.source.y + diff.y);
const boxresize = d3.select(`#${boxid}_boxresize`)
.attr("cx", d.bottomright.x = d.bottomright.x + diff.x)
.attr("cy", d.bottomright.y = d.bottomright.y + diff.y);
const boxtext = d3.select(`#${boxid}_boxtext`)
.attr("x", d.topleft.x)
.attr("y", d.topleft.y)
}

function dragended(d) {
d3.select(this).attr("stroke", null);
}

return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
Insert cell
function dragBoxSource() {

function dragstarted(d) {
d3.select(this).raise().attr("stroke", "black");
}

function dragged(d) {
let coords= d3.mouse(this);
d3.select(this).attr("cx", d.source.x = coords[0]).attr("cy", d.source.y = coords[1]);
const boxid = d3.select(this).attr("boxid");
const box = d3.select(`#${boxid}`)
.attr("d", commentPath(d, {radius: 10}))
}

function dragended(d) {
d3.select(this).attr("stroke", null);
}

return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
Insert cell
function dragBoxResize() {

function dragstarted(d) {
d3.select(this).raise().attr("stroke", "black");
}

function dragged(d) {
let coords= d3.mouse(this);
d3.select(this).attr("cx", d.bottomright.x = coords[0]).attr("cy", d.bottomright.y = coords[1]);
d.width = coords[0] - d.topleft.x;
d.height = coords[1] - d.topleft.y;
const boxid = d3.select(this).attr("boxid");
const box = d3.select(`#${boxid}`)
.attr("d", commentPath(d, {radius: 10}))
const boxtext = d3.select(`#${boxid}_boxtext`)
.attr('width', d.width)
.attr('height', d.height)
}

function dragended(d) {
d3.select(this).attr("stroke", null);
}

return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
Insert cell
// function getTextBounds(svg, obj, options) {
// let height = 50;
// const width = textWidth(svg, obj, {ratio: options.text.ratio});

// const faketext = commentText(svg, obj, width);
// height = faketext.node().getBoundingClientRect().height;
// faketext.remove();
// obj.width = width;
// obj.height = height;
// return obj;
// }
Insert cell
// function textWidth(svg, obj, options) {
// var text = svg.append("text")
// .text(obj.text)
// .attr("x", obj.topleft.x)
// .attr("y", obj.topleft.y)
// .attr("font-size", 12)
// .attr("font-family", "Gloria Hallelujah")
// .attr("text-anchor", "middle")
// .attr("font-weight", "normal")
// const textlen = text.node().getComputedTextLength()
// text.remove()
// return Math.ceil(options.ratio * Math.sqrt(textlen / options.ratio));
// }
Insert cell
function commentPath(obj, options) {
const {topleft, bottomright, source, text} = obj;
const {radius} = options;
const width = bottomright.x - topleft.x;
const height = bottomright.y - topleft.y;
const hw = width / 2;
const hh = height / 2;
const sw = width - (2 * radius);
const sh = height - (2 * radius);
const hsw = sw / 2;
const hsh = sh / 2;
const tl = {x: topleft.x + radius, y: topleft.y};
const center = {x: tl.x + hsw, y: tl.y + hh};
let linkup = "", linkdown = "";
if (center.y < source.y) {
linkdown = "h" + - sw / 3
+ "L "+ source.x + "," + source.y
+ "L "+ (tl.x + sw / 3) + "," + (tl.y + height)
+ "h" + - sw / 3
} else {
linkup = "h" + sw / 3
+ "L "+ source.x + "," + source.y
+ "L "+ (tl.x + 2 * sw / 3) + "," + tl.y
+ "h" + sw / 3
}
const start = "M" + tl.x + "," + tl.y
const rside = 'L' + (tl.x + sw) + "," + tl.y
+ "a" + radius + "," + radius + " 0 0 1 " + radius + "," + radius
+ "v" + sh
+ "a" + radius + "," + radius + " 0 0 1 " + -radius + "," + radius
const lside = 'L' + tl.x + "," + (tl.y + height)
+ "a" + - radius + "," + - radius + " 0 0 1 " +- radius + "," + - radius
+ "v" + - sh
+ "a" + radius + "," + radius + " 0 0 1 " + radius + "," + - radius

return start
+ linkup
+ rside
+ linkdown
+ lside
+ "z";
}
Insert cell
function svgToDataUri(svg){
const img = new Image(),
serializer = new XMLSerializer();
try {
const svgStr = serializer.serializeToString(svg);
return 'data:image/svg+xml;base64,' + window.btoa(svgStr);
} catch(e) {
return 'data:image/svg+xml;base64,';
}

// You could also use the actual string without base64 encoding it:
//return "data:image/svg+xml;utf8," + svgStr;
};
Insert cell
// function svgToCanvas(svg){
// const img = new Image(),
// serializer = new XMLSerializer(),
// svgStr = serializer.serializeToString(svg);

// img.src = 'data:image/svg+xml;base64,' + window.btoa(svgStr);

// // You could also use the actual string without base64 encoding it:
// //img.src = "data:image/svg+xml;utf8," + svgStr;

// var canvas = document.createElement("canvas");
// document.body.appendChild(canvas);

// canvas.width = w;
// canvas.height = h;
// canvas.getContext("2d").drawImage(img,0,0,w,h);
// // Now save as png or whatever
// };
Insert cell
Insert cell
Insert cell
yamltojson = require('js-yaml@3.13.1/dist/js-yaml.js')
Insert cell
jsontoyaml = require('https://bundle.run/json-to-pretty-yaml@1.2.2')
Insert cell
Insert cell
html `<link href="https://fonts.googleapis.com/css?family=Gloria+Hallelujah&display=swap" rel="stylesheet"> <style>
h1, h2, h3, h4 {
font-family: 'Gloria Hallelujah', cursive;
}
</style>`
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