Published
Edited
Sep 27, 2022
Fork of You Draw It
Importers
1 star
Insert cell
Insert cell
viewof data = Drawer({
marginBottom:20,
marginLeft:30,marginRight:30,
total: 300000,
width:800,
//height:200,
data: [[0,0.5,"€5k"],[1,0.7,"€10k"],[2,0.8,"€20k"],[3,0.7,"€30k"],[4,0.6,"€40k"],[5,0.3,"€50k"],[6,0.2,"€60k"],[7,0.1,"€70k"],[8,0.1,"€80k"],[9,0.1,"€90k"],[10,0.05,"€100k"],[11,0.2,"€150k"],[12,0.1,"€300k"]]
})
Insert cell
data
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function Drawer({
marginTop = 20, // top margin, in pixels
marginRight = 30, // right margin, in pixels
marginBottom = 30, // bottom margin, in pixels
marginLeft = 40, // left margin, in pixels
width = 640, // outer width, in pixels
height = 400, // outer height, in pixels
xSamples = 30, // approximate number of data points
xType = d3.scaleUtc, // the x-scale type
xDomain = [new Date("2022-01-01"), new Date("2023-01-01")], // [xmin, xmax]
//xRange = [marginLeft, width - marginRight], // [left, right]
yType = d3.scaleLinear, // the y-scale type
yDomain = [0, 1], // [ymin, ymax]
yRange = [height - marginBottom, marginTop], // [bottom, top]
yFormat, // a format specifier string for the y-axis
yLabel, // a label for the y-axis
curve = d3.curveLinear, // method of interpolation between points
data,
total, totalText = d => {
if (d[3]*total > 1000)
return (d[3]*total/1000).toLocaleString("en", {maximumFractionDigits: 0 })+'k'
else
return (d[3]*total).toLocaleString("en", {maximumFractionDigits: 0 })
}
} = {}) {

const bisectX = d3.bisector(([x]) => x).center;


/*d3.scaleBand()
.domain(d3.range(data.length))
.range([margin.left, width - margin.right])
.padding(0.3)*/
const yScale = yType(yDomain, yRange);
const yAxis = d3.axisLeft(yScale).ticks(height / 40, yFormat);
//data = data.map(d => [...d,d.push(d[0])]);
data.forEach((d,i) => {
data[i][3] = data[i][1]/data.reduce((a,v) => a + v[1], 0)
})
//let data = [[0,0.5],[1,0.7],[2,0.7],[3,0.7]]//xScale.ticks(xSamples).map(x => [x, 0.5]);
const xScale = d3.scaleBand().domain(d3.range(data.length)).range([marginLeft,width-marginRight]).padding(0.2)//xType(xDomain, [50,100,180,200]);
const xRange = xScale.range();
const xAxis = d3.axisBottom(xScale).tickSizeOuter(0).tickFormat(i => data[i][2]);

function clamp(a, b, c){ return Math.max(a, Math.min(b, c)) } // https://observablehq.com/@duitel/you-draw-it-bar-chart


const line = d3.line()
//.curve(curve)
//.defined(([, y]) => y != null)
.x(([x]) => xScale(x))
.y(([, y]) => yScale(y));

const svg = d3.create("svg")
.style('cursor', 'grab')
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("pointer-events", "all")
//.attr("style", "max-width: 100%; height: auto; height: intrinsic;")
.property("value", data);

// new
const g = svg.append("g")
.attr("id" , "youData")
.attr("fill", "lightgreen")
.selectAll("rect")
.data(data)
.join("g").append("g");
g.append("rect")
.attr("x", (d, i) => xScale(i))
.attr("y", d => yScale(d[1]))
.attr("height", d => yScale(0)-yScale(d[1]))// y(0) - y(d.youDraw))
.attr("width", xScale.bandwidth())
g.append("text")
.attr("class", "percs")
.attr("x", (d, i) => xScale(i))
.attr("y", d => yScale(d[1]))
.attr("dy", -5)
.style("font-size", "94%")
.text(d => (d[3]*100).toFixed(1)+'%')

if (total) {
g.append("text")
.attr("class", "nums")
.attr("x", (d, i) => xScale(i))
.attr("y", d => height-marginBottom-2)
//.attr("dy", 30)//.style("font-weight", "bold")
.style("font-size", "12px")
.attr("fill", "gray")
.text(totalText) // bad formatting
}

svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
//.style("font-weight", "bold")
.style('font-size','14px')
.call( xAxis)//d3.axisBottom(xScale).tickFormat(i => data[i]).tickSizeOuter(0))// xAxis);

svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
//.attr("font-weight", "bold")
.call(yAxis)
.attr('opacity','0.1')
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone()
.attr("x2", width - marginLeft - marginRight)
.attr("stroke-opacity", 0.1))
.call(g => g.append("text")
.attr("x", -marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(yLabel));

const path = svg.append("path")
.attr("fill", "none")
.attr("stroke", "green")
.attr("opacity", "0.1")
.attr("stroke-width", 3)
.attr("transform", `translate(${xScale.bandwidth()/2},0)`)
.datum(data);

// full screen rect
svg.append("rect")
.attr("width", width)
.attr("height", height)
.attr("fill", "none")
.attr("pointer-events", "all")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragend))
.on('click', clicked)
//.on('mousedown', clicked); // doesnt work



function clicked(event, d) {
if (event.defaultPrevented) return; // dragged
const mousePos = d3.pointer(event, this);
//const xBar = clamp(0, data.length, Math.floor((mousePos[0])/xScale.step()));
const xBar = clamp(0, data.length, Math.floor((event.x - marginLeft-xScale.bandwidth()/2/* Trial and error ?! */)/xScale.step()));

const dx = d3.scaleOrdinal(xScale.range(), xScale.domain())//xScale.invert(x);
const dy = yScale.invert(mousePos[1]);
let i = data[xBar][0]//(data, dx);
//let i = bisectX(data, dx);
data[i][1] = dy;
data.forEach((d,i) => {
data[i][3] = data[i][1]/data.reduce((a,v) => a + v[1], 0)
})
//data[i][3] = 0.5
// Fill preceding gaps, if any.
/*for (let k = i - 1; k >= 0; --k) {
if (data[k][1] != null) {
while (++k < i) data[k][1] = dy;
break;
}
}*/
// Fill following gaps, if any.
/*for (let k = i + 1; k < data.length; ++k) {
if (data[k][1] != null) {
while (--k > i) data[k][1] = dy;
break;
}
}*/
const g = d3.select("#youData")
.selectAll("rect")
.data(data)
.join("rect").attr("x", (d, i) => xScale(i))
.attr("y", d => yScale(d[1]))
.attr("height", d => yScale(0)-yScale(d[1]))// y(0) - y(d.youDraw))
.attr("width", xScale.bandwidth());

d3.select("#youData")
.selectAll(".percs")
.attr("x", (d, i) => xScale(i))
.attr("y", d => yScale(d[1]))
.attr("dy", -5)
.text(d => (d[3]*100).toFixed(1)+'%')

if (total) {
d3.select("#youData")
.selectAll(".nums")
.attr("x", (d, i) => xScale(i))
.attr("y", d => height-marginBottom-2)
//.attr("dy", 30)//.style("font-weight", "bold")
.style("font-size", "12px")
.attr("fill", "gray")
.text(totalText) // bad formatting
}
path.attr("d", line);
svg.dispatch("input");
}

//svg.property("value", data);
//dragged({x:0,y:0.1});
path.attr("d", line);
svg.dispatch("input");

function dragstarted() {
//data = xScale.ticks(xSamples).map(x => [x, null]);
//data = data.map(d =>
svg.property("value", data);
path.datum(data);
dragged.call(this);
}

function dragged(event) {
const mousePos = d3.pointer(event, this);
//const xBar = clamp(0, data.length, Math.floor((mousePos[0])/xScale.step()));
const xBar = clamp(0, data.length, Math.floor((event.x - marginLeft /* Trial and error ?! */)/xScale.step()));

const dx = d3.scaleOrdinal(xScale.range(), xScale.domain())//xScale.invert(x);
const dy = yScale.invert(event.y);
let i = data[xBar][0]//(data, dx);
//let i = bisectX(data, dx);
data[i][1] = dy;
// d3: must mutate, map breaks object constancy, or something ?
data.forEach((d,i) => {
data[i][3] = data[i][1]/data.reduce((a,v) => a + v[1], 0)
})
//data = data.map(d => [...d])//[d[0],d[1],d[2],d[1]/data.reduce((a,v) => a + v[1], 0)])
//data[i][3] = data[i][1]/data.reduce((a,v) => a + v[1], 0)
// Fill preceding gaps, if any.
/*for (let k = i - 1; k >= 0; --k) {
if (data[k][1] != null) {
while (++k < i) data[k][1] = dy;
break;
}
}*/
// Fill following gaps, if any.
/*for (let k = i + 1; k < data.length; ++k) {
if (data[k][1] != null) {
while (--k > i) data[k][1] = dy;
break;
}
}*/
const g = d3.select("#youData")
.selectAll("rect")
.data(data)
.join("rect").attr("x", (d, i) => xScale(i))
.attr("y", d => yScale(d[1]))
.attr("height", d => yScale(0)-yScale(d[1]))// y(0) - y(d.youDraw))
.attr("width", xScale.bandwidth());

d3.select("#youData")
.selectAll(".percs")
.attr("x", (d, i) => xScale(i))
.attr("y", d => yScale(d[1]))
.attr("dy", -5)
.text(d => (d[3]*100).toFixed(1)+'%')

if (total) {
d3.select("#youData")
.selectAll(".nums")
.attr("x", (d, i) => xScale(i))
.attr("y", d => height-marginBottom-2)
//.attr("dy", 30)//.style("font-weight", "bold")
.style("font-size", "12px")
.attr("fill", "gray")
.text(totalText) // bad formatting
}

path.attr("d", line);
//svg.dispatch("input");
}
function dragend(event) {
svg.dispatch("input");
}

return svg.node();
}
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