function Drawer({
marginTop = 20,
marginRight = 30,
marginBottom = 30,
marginLeft = 40,
width = 640,
height = 400,
xSamples = 30,
xType = d3.scaleUtc,
xDomain = [new Date("2022-01-01"), new Date("2023-01-01")],
yType = d3.scaleLinear,
yDomain = [0, 1],
yRange = [height - marginBottom, marginTop],
yFormat,
yLabel,
curve = d3.curveLinear,
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;
const yScale = yType(yDomain, yRange);
const yAxis = d3.axisLeft(yScale).ticks(height / 40, yFormat);
data.forEach((d,i) => {
data[i][3] = data[i][1]/data.reduce((a,v) => a + v[1], 0)
})
const xScale = d3.scaleBand().domain(d3.range(data.length)).range([marginLeft,width-marginRight]).padding(0.2)
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)) }
const line = d3.line()
.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")
.property("value", data);
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]))
.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)
.style("font-size", "12px")
.attr("fill", "gray")
.text(totalText)
}
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.style('font-size','14px')
.call( xAxis)
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.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);
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)
function clicked(event, d) {
if (event.defaultPrevented) return;
const mousePos = d3.pointer(event, this);
const xBar = clamp(0, data.length, Math.floor((event.x - marginLeft-xScale.bandwidth()/2)/xScale.step()));
const dx = d3.scaleOrdinal(xScale.range(), xScale.domain())
const dy = yScale.invert(mousePos[1]);
let i = data[xBar][0]
data[i][1] = dy;
data.forEach((d,i) => {
data[i][3] = data[i][1]/data.reduce((a,v) => a + v[1], 0)
})
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]))
.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)
.style("font-size", "12px")
.attr("fill", "gray")
.text(totalText)
}
path.attr("d", line);
svg.dispatch("input");
}
path.attr("d", line);
svg.dispatch("input");
function dragstarted() {
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((event.x - marginLeft )/xScale.step()));
const dx = d3.scaleOrdinal(xScale.range(), xScale.domain())
const dy = yScale.invert(event.y);
let i = data[xBar][0]
data[i][1] = dy;
data.forEach((d,i) => {
data[i][3] = data[i][1]/data.reduce((a,v) => a + v[1], 0)
})
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]))
.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)
.style("font-size", "12px")
.attr("fill", "gray")
.text(totalText)
}
path.attr("d", line);
}
function dragend(event) {
svg.dispatch("input");
}
return svg.node();
}