Skip to content

Instantly share code, notes, and snippets.

@steveyerigandev
Last active September 2, 2021 15:39
Show Gist options
  • Save steveyerigandev/da7190a12b6357c904828b170ae11129 to your computer and use it in GitHub Desktop.
Save steveyerigandev/da7190a12b6357c904828b170ae11129 to your computer and use it in GitHub Desktop.
Data vis - Chart (D3.js and React)
export default function define(runtime, observer) {
const main = runtime.module();
main.variable(observer()).define(["md"], function(md){return(
md`# Stock Chart`
)});
main.variable(observer("chart")).define("chart", ["d3","width","height","margin","rectColor","rectHeight","chartData","x","y","lineColor","yLine","line","hoverScale","dateFormat","priceFormat","apyFormat"], function(d3,width,height,margin,rectColor,rectHeight,chartData,x,y,lineColor,yLine,line,hoverScale,dateFormat,priceFormat,apyFormat)
{
let svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("background", "#1B1A40");
svg.append("style").text(`
.tranche--active {
text-decoration: underline
}
`);
var apyToggle = true;
var t = d3.transition().duration(1000);
const dataProps = [
{ line: "netAApy", bar: "trancheAValueUSD", sliceLine: "sliceAApy" },
{ line: "netBApy", bar: "trancheBValueUSD", sliceLine: "sliceBApy" }
];
let selectedData = dataProps[0];
let trancheToggle = true;
let hoveredIndex = -1;
let hoverGroup = svg
.append("g")
.attr("transform", `translate(0,${margin.bottom})`);
let hoverLine = hoverGroup
.append("line")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", 0)
.attr("y2", 0)
.attr("stroke-dasharray", "4,4")
.attr("stroke", "white")
.attr("stroke-width", 2)
.attr("class", "hover");
let dateText = svg
.append("text")
.attr("opacity", 0)
.attr("fill", "white")
.attr("font-size", 14)
.attr("text-anchor", "middle")
.text("gigi");
let rects = svg
.append("g")
.attr("fill", rectColor)
.attr("transform", `translate(0,${height - rectHeight - margin.bottom})`);
function updateRects() {
rects
.selectAll("rect")
.data(chartData[selectedData.bar])
.join("rect")
.attr("x", (d, i) => x(i))
.attr("class", (d, i) => `rect-${i} bar`)
.attr("width", x.bandwidth())
.transition()
.duration(2000)
.attr("y", (d) => y(d))
.attr("height", (d) => y(0) - y(d));
}
let hoverText = rects
.append("text")
.attr("opacity", 0)
.attr("class", "hover")
.attr("fill", "white")
.attr("text-anchor", "middle")
.text("");
let subtitleHoverText = rects
.append("text")
.attr("opacity", 0)
.attr("class", "hover")
.attr("fill", "white")
.attr("font-size", 10)
.attr("text-anchor", "middle")
.text("TRANCHE VALUE");
let lineGroup = svg
.append("g")
.attr("class", "line-group")
.attr("transform", `translate(0,${rectHeight - 50})`);
var netALine = lineGroup
.append("path")
.attr("stroke", lineColor)
.attr("fill", "none")
.attr("stroke-width", 3)
.attr("opacity", 0.8);
var netBLine = lineGroup
.append("path")
.attr("stroke", lineColor)
.attr("fill", "none")
.attr("stroke-width", 3)
.attr("opacity", 0.2);
function updateLines() {
var aData = apyToggle
? chartData[dataProps[0]["line"]]
: chartData[dataProps[0]["sliceLine"]];
var bData = apyToggle
? chartData[dataProps[1]["line"]]
: chartData[dataProps[1]["sliceLine"]];
yLine.domain([0, d3.max([d3.max(aData), d3.max(bData)])]);
netALine.attr("d", line(aData));
netBLine.attr("d", line(bData));
var aLabel = "Tranche A";
var bLabel = "Tranche B";
netAApyLabel.text(aLabel);
netBApyLabel.text(bLabel);
}
var title = svg
.append("text")
.attr("fill", "white")
.attr("text-anchor", "start")
.attr("x", margin.left)
.attr("y", 40)
.attr("font-size", 22)
.attr("font-weight", 700)
.text("Tranche Performance");
const toggleWidth = 200;
var toggleHeight = 30;
var toggleGroup = svg
.append("g")
.attr("transform", `translate(250,${40 - toggleHeight + 5})`);
let toggleRect = toggleGroup
.append("rect")
.attr("x", margin.left)
.attr("y", 0)
.attr("width", toggleWidth)
.attr("height", toggleHeight)
.attr("fill", "#26254A")
.attr("rx", toggleHeight / 2)
.attr("ry", toggleHeight / 2);
let activeRect = toggleGroup
.append("rect")
.attr("x", margin.left)
.attr("y", 0)
.attr("width", toggleWidth / 2)
.attr("height", toggleHeight)
.attr("fill", "#3B3765")
.attr("rx", toggleHeight / 2)
.attr("ry", toggleHeight / 2);
let netApyRectLabel = toggleGroup
.append("text")
.attr("x", margin.left + toggleWidth / 4)
.attr("y", toggleHeight / 2 + 5)
.attr("fill", "white")
.attr("text-anchor", "middle")
.attr("font-size", 12)
.attr("font-weight", 700)
.text("NET APY");
let sliceApyRectLabel = toggleGroup
.append("text")
.attr("x", margin.left + toggleWidth / 4 + toggleWidth / 2)
.attr("y", toggleHeight / 2 + 5)
.attr("fill", "white")
.attr("opacity", 0.5)
.attr("text-anchor", "middle")
.attr("font-size", 12)
.attr("font-weight", 700)
.text("SLICE APY");
let hoverCircle = lineGroup
.append("circle")
.attr("r", 10)
.attr("opacity", 0)
.attr("stroke", "white")
.attr("stroke-width", 4)
.attr("fill", lineColor);
let netAApyLabel = lineGroup
.append("text")
.attr("fill", "white")
.attr("font-size", 12)
.attr("font-weight", 700)
.attr("x", width - margin.right + 10)
.attr("y", yLine(chartData.netAApy[chartData.netAApy.length - 1]) + 4)
.style("cursor", "pointer")
.style("opacity", trancheToggle ? 0.8 : 0.2)
.text("Tranche A")
.on("mousedown", function (e) {
trancheToggle = true;
selectedData = dataProps[0];
netALine.transition().duration(1000).style("opacity", 0.8);
netBLine.transition().duration(1000).style("opacity", 0.2);
netAApyLabel.transition().duration(1000).style("opacity", 1);
netBApyLabel.transition().duration(1000).style("opacity", 0.2);
updateRects();
});
let netBApyLabel = lineGroup
.append("text")
.attr("fill", "white")
.attr("font-size", 12)
.attr("font-weight", 700)
.attr("x", width - margin.right + 10)
.attr("y", yLine(chartData.netBApy[chartData.netBApy.length - 1]) + 4)
.style("cursor", "pointer")
.style("opacity", trancheToggle ? 0.2 : 0.8)
.text("Tranche B")
.on("mousedown", function (e) {
trancheToggle = false;
selectedData = dataProps[1];
netBLine.transition().duration(1000).style("opacity", 0.8);
netALine.transition().duration(1000).style("opacity", 0.2);
netAApyLabel.transition().duration(1000).style("opacity", 0.2);
netBApyLabel.transition().duration(1000).style("opacity", 1);
updateRects();
});
let toggleOverlay = toggleGroup
.append("rect")
.attr("x", margin.left)
.attr("y", 0)
.attr("width", toggleWidth)
.attr("height", toggleHeight)
.attr("opacity", 0)
.attr("rx", toggleHeight / 2)
.attr("ry", toggleHeight / 2)
.style("cursor", "pointer")
.on("mousedown", function (e) {
var offset = margin.left + toggleWidth / 2;
if (!apyToggle) offset = margin.left;
activeRect.transition().attr("x", offset);
var netOpacity = apyToggle ? 0.5 : 1;
var sliceOpacity = apyToggle ? 1 : 0.5;
netApyRectLabel.transition().attr("opacity", netOpacity);
sliceApyRectLabel.transition().attr("opacity", sliceOpacity);
apyToggle = !apyToggle;
updateLines();
});
let lineValueText = lineGroup
.append("text")
.attr("fill", "white")
.attr("font-size", 12)
.attr("text-anchor", "middle")
.attr("opacity", 0);
let lineValueTextSubtitle = lineGroup
.append("text")
.attr("fill", "white")
.attr("font-size", 10)
.attr("text-anchor", "middle")
.attr("opacity", 0);
let hoverRect = svg
.append("rect")
.attr("opacity", 0)
.attr("fill", "white")
.attr("x", margin.left)
.attr("y", margin.top)
.attr("width", width - margin.right - margin.left)
.attr("height", height - margin.bottom)
.on("mousemove", function (event, d) {
var index = Math.floor(hoverScale.invert(event.clientX));
updateHover(index);
})
.on("mouseout", function (event, d) {
clearHover();
});
updateRects();
updateLines();
function updateHover(index) {
if (index == hoveredIndex) return;
else hoveredIndex = index;
d3.selectAll(".bar").attr("fill", rectColor);
var activeProp = apyToggle ? "line" : "sliceLine";
d3.select(`.rect-${index}`).attr("fill", lineColor);
let xOffset = x(index) + x.bandwidth() / 2;
var date = new Date(chartData.date[index]);
var dateString = date ? dateFormat(date) : "";
hoverLine
.attr("opacity", 0.65)
.attr("x1", xOffset)
.attr("x2", xOffset)
.attr("y1", yLine(chartData[selectedData[activeProp]][index]) + 40)
.attr(
"y2",
y(chartData[selectedData.bar][index]) +
height -
rectHeight -
margin.bottom -
115
);
hoverText
.attr("opacity", 1)
.attr("x", xOffset)
.attr("y", y(chartData[selectedData.bar][index]) - 40)
.text(priceFormat(chartData[selectedData.bar][index]))
.raise();
subtitleHoverText
.attr("opacity", 0.65)
.attr("x", xOffset)
.attr("y", y(chartData[selectedData.bar][index]) - 20)
.raise();
dateText
.attr("opacity", 1)
.attr("x", xOffset)
.attr("y", height - margin.bottom + 20)
.text(dateString);
hoverCircle
.attr("opacity", 1)
.attr("cx", xOffset)
.attr("cy", yLine(chartData[selectedData[activeProp]][index]));
lineValueText
.attr("opacity", 1)
.attr("x", xOffset)
.attr("y", yLine(chartData[selectedData[activeProp]][index]) - 40)
.text(apyFormat(chartData[selectedData[activeProp]][index] / 100));
var apyTitle = apyToggle ? "NET APY" : "SLICE APY";
lineValueTextSubtitle
.attr("opacity", 0.65)
.attr("x", xOffset)
.attr("y", yLine(chartData[selectedData[activeProp]][index]) - 20)
.text(apyTitle);
}
function clearHover() {
hoverLine.attr("opacity", 0);
subtitleHoverText.attr("opacity", 0);
hoverText.attr("opacity", 0).text();
dateText.attr("opacity", 0).text();
hoverCircle.attr("opacity", 0);
lineValueText.attr("opacity", 0).text();
lineValueTextSubtitle.attr("opacity", 0);
d3.selectAll(".bar").attr("fill", rectColor);
}
return svg.node();
}
);
main.variable(observer("hoverScale")).define("hoverScale", ["d3","chartData","margin","width"], function(d3,chartData,margin,width){return(
d3
.scaleLinear()
.domain([0, chartData.trancheAApy.length - 1])
.range([margin.left, width - margin.right])
.clamp(true)
)});
main.variable(observer("apyFormat")).define("apyFormat", ["d3"], function(d3){return(
d3.format(".2%")
)});
main.variable(observer("dateFormat")).define("dateFormat", ["d3"], function(d3){return(
d3.timeFormat("%b %d")
)});
main.variable(observer("chartData")).define("chartData", ["apiData"], function(apiData){return(
apiData.result.chartData
)});
main.variable(observer()).define(["htl"], function(htl){return(
htl.html`<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap');
div, input, button {
font-family: 'Inter', sans-serif;
}
</style>`
)});
main.variable(observer("apiData")).define("apiData", ["d3"], function(d3){return(
d3.json("https://chart-tranche.herokuapp.com/chart")
)});
main.variable(observer("priceFormat")).define("priceFormat", ["d3"], function(d3){return(
d3.format("($,.2f")
)});
main.variable(observer("line")).define("line", ["d3","x","yLine"], function(d3,x,yLine){return(
d3
.line()
.x((d, i) => x(i) + x.bandwidth() / 2)
.y((d, i) => yLine(d))
.curve(d3.curveCatmullRom)
)});
main.variable(observer("x")).define("x", ["d3","chartData","margin","width"], function(d3,chartData,margin,width){return(
d3
.scaleBand()
.domain(d3.range(chartData.date.length))
.range([margin.left, width - margin.right])
.padding(0.1)
)});
main.variable(observer("yLine")).define("yLine", ["d3","chartData","rectHeight"], function(d3,chartData,rectHeight){return(
d3
.scaleLinear()
.domain([0, d3.max([d3.max(chartData.netAApy), d3.max(chartData.netBApy)])])
.range([rectHeight, 0])
)});
main.variable(observer("y")).define("y", ["d3","chartData","rectHeight"], function(d3,chartData,rectHeight){return(
d3
.scaleLinear()
.domain([
0,
d3.max([
d3.max(chartData.trancheAValueUSD),
d3.max(chartData.trancheBValueUSD)
])
])
.range([rectHeight, 0])
)});
main.variable(observer("lineColor")).define("lineColor", function(){return(
"#7277FF"
)});
main.variable(observer("lineColorInactive")).define("lineColorInactive", function(){return(
"#342964"
)});
main.variable(observer("rectColor")).define("rectColor", function(){return(
"#33315F"
)});
main.variable(observer("rectHeight")).define("rectHeight", function(){return(
150
)});
main.variable(observer("height")).define("height", function(){return(
600
)});
main.variable(observer("margin")).define("margin", function(){return(
{ top: 60, right: 100, bottom: 50, left: 30 }
)});
return main;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment