chart_levels = {
const svg = d3
.create("svg")
.attr("width", isMobile ? myWidth_mobile_levels : myWidth)
.attr("height", height_levels)
.style("overflow", isMobile ? "visible" : "hidden");
const defs = svg.append("defs");
createGradient(defs, "left");
const axisGroup = svg.append("g").attr("id", "axis");
axisGroup.append("g").call(xAxis_levels);
axisGroup.append("g").call(yAxis_levels);
const auxGroup = svg.append("g").attr("id", "aux");
const auxRects = auxGroup
.selectAll("a")
.data(dataset)
.join("a")
.attr("href", (d) => (isMobile ? null : d.source || null))
.attr("target", "_blank");
if (!isMobile) {
auxRects
.append("title")
.text((d) => (d.source ? locale.titleHoverNote : null));
}
const rectsEl = auxRects
.append("rect")
.attr("data-date", (d) => formatTime(d.date))
.style("opacity", 0)
.on("mouseover", onMouseOver)
.on("mouseout", onMouseOut);
if (!isMobile) {
rectsEl
.attr("x", (d) => xScale_levels(d.date))
.attr("width", xScale_levels.step())
.attr("y", (d) => margin_levels.top)
.attr(
"height",
(d) => height_levels - margin_levels.top - margin_levels.bottom
);
} else {
rectsEl
.attr("x", (d) => margin_levels.left)
.attr("width", myWidth - margin_levels.left - margin_levels.right)
.attr("y", (d) => yScale_levels(d.date) + yScale_levels.bandwidth() / 2)
.attr("height", yScale_levels.step());
}
// CHART ELEMENTS
const chartGroup = svg.append("g").attr("id", "chart-elements");
// Lines
const linesGroup = chartGroup.append("g").attr("id", "volumen-lines");
const lines = linesGroup
.selectAll("line")
.data(dataset)
.join("line")
.attr("class", "volume interactive")
.attr("data-date", (d) => formatTime(d.date))
.style("stroke", colorNeutral(80))
.style("pointer-events", "none");
if (!isMobile) {
lines
.attr("x1", (d) => xScale_levels(d.date) + xScale_levels.bandwidth() / 2)
.attr("x2", (d) => xScale_levels(d.date) + xScale_levels.bandwidth() / 2)
.attr("y1", (d) => yScale_levels(0))
.attr("y2", (d) => yScale_levels(0))
.style("stroke-width", xScale_levels.bandwidth() * 0.6)
.call(highlightCurrentDate)
.transition()
.duration(transitionDuration)
.delay((d, i) => i * transtionDelay)
.attr("y2", (d) => yScale_levels(d.volume));
} else {
lines
.attr("x1", (d) => xScale_levels(0))
.attr("x2", (d) => xScale_levels(0))
.attr("y1", (d) => yScale_levels(d.date) + yScale_levels.bandwidth() / 2)
.attr("y2", (d) => yScale_levels(d.date) + yScale_levels.bandwidth() / 2)
.style("stroke-width", yScale_levels.bandwidth() * 0.6)
.call(highlightCurrentDate)
.transition()
.duration(transitionDuration)
.delay((d, i) => i * transtionDelay)
.attr("x2", (d) => xScale_levels(d.volume));
}
// Rects
const rectsGroup = chartGroup.append("g").attr("id", "trasvases-rects");
const rects = rectsGroup
.selectAll("rect")
.data(dataset)
.join("rect")
.attr("data-date", (d) => formatTime(d.date))
.attr("class", "trasvase interactive")
.style("fill", (d) => `url(#linear-gradient-${d.level})`)
.style("opacity", 0.7)
.style("pointer-events", "none");
if (!isMobile) {
rects
.attr("x", (d) => xScale_levels(d.date) + 1)
.attr("width", xScale_levels.bandwidth() - 2)
.attr("y", (d) => yScale_levels(d.volume))
.attr("height", 0)
.transition()
.duration(transitionDuration)
.delay((d, i) => 1500 + i * transtionDelay)
.attr("height", (d) => yScale_levels(0) - yScale_levels(d.transfer));
} else {
rects
.attr("x", (d) => xScale_levels(d.volume))
.attr("width", 0)
.attr("y", (d) => yScale_levels(d.date) + 1)
.attr("height", yScale_levels.bandwidth() - 2)
.transition()
.duration(transitionDuration)
.delay((d, i) => 1500 + i * transtionDelay)
.attr(
"x",
(d) =>
xScale_levels(d.volume) -
(xScale_levels(d.transfer) - xScale_levels(0))
)
.attr("width", (d) => xScale_levels(d.transfer) - xScale_levels(0)); // Growing with time
}
// Lines - level
const linesGroup2 = chartGroup.append("g").attr("id", "levels-lines2");
const lines2 = linesGroup2
.selectAll("line")
.data(dataset)
.join("line")
.attr("class", "level interactive")
.attr("data-date", (d) => formatTime(d.date))
//.style("stroke", colorNeutral(1000))
.style("stroke", (d) => d3.color(scaleColor(d.level)).darker(0.9))
.style("stroke-width", 2)
.style("pointer-events", "none");
if (!isMobile) {
lines2
.attr("x1", (d) => xScale_levels(d.date))
.attr("x2", (d) => xScale_levels(d.date) + xScale_levels.bandwidth())
.attr("y1", yScale_levels(0))
.attr("y2", yScale_levels(0))
.transition()
.duration(transitionDuration)
.delay((d, i) => i * transtionDelay)
.attr("y1", (d) => yScale_levels(d.volume))
.attr("y2", (d) => yScale_levels(d.volume));
} else {
lines2
.attr("x1", xScale_levels(0))
.attr("x2", xScale_levels(0))
.attr("y1", (d) => yScale_levels(d.date))
.attr("y2", (d) => yScale_levels(d.date) + yScale_levels.bandwidth())
.transition()
.duration(transitionDuration)
.delay((d, i) => i * transtionDelay)
.attr("x1", (d) => xScale_levels(d.volume))
.attr("x2", (d) => xScale_levels(d.volume));
}
// TOOLTIP
const tooltip = svg.append("g").attr("id", "tooltip");
const initialHeight = 140;
const lineHeight = 20;
const horizOffset = 10;
if (!isMobile) {
// Lines - level
const linesGroup3 = tooltip.append("g").attr("id", "tooltip-lines3");
const lines3 = linesGroup3
.selectAll("line")
.data(dataset)
.join("line")
.attr("class", "tooltip-line interactive")
.attr("data-date", (d) => formatTime(d.date))
.attr("x1", (d) => xScale_levels(d.date) + xScale_levels.bandwidth() / 2)
.attr("x2", (d) => xScale_levels(d.date) + xScale_levels.bandwidth() / 2)
.attr("y1", (d) => yScale_levels(d.volume) - 10)
.attr("y2", (d) => yScale_levels(d.volume) - initialHeight - 10)
.style("stroke", colorNeutral(900))
.style("stroke-width", 1.5)
.call(highlightCurrentDate)
.style("pointer-events", "none")
.style("opacity", 0);
}
// Texts
const textGroup = tooltip.append("g").attr("id", "texts");
const date = textGroup.append("g");
date
.selectAll("text")
.data(dataset)
.join("text")
.attr("class", "interactive")
.attr("x", (d) =>
isMobile
? xScale_levels(d["volume"])
: xScale_levels(d.date) + xScale_levels.bandwidth() / 2
)
.attr("y", (d) =>
isMobile
? yScale_levels(d.date) + yScale_levels.bandwidth() / 2 + 4
: yScale_levels(d["volume"])
)
.style(
"text-anchor",
isMobile
? "start"
: (d) => (xScale_levels(d.date) <= midPoint ? "start" : "end")
)
.attr("dy", isMobile ? -15 : -initialHeight)
.attr("dx", function (d) {
if (isMobile) return 20;
else
return xScale_levels(d.date) <= midPoint ? horizOffset : -horizOffset;
})
.text((d) =>
isMobile ? formatDateTooltip_mobile(d.date) : formatDateTooltip(d.date)
)
.style("font-size", isMobile ? "0.7rem" : "0.8rem")
.style("fill", colorNeutral(600))
.style("opacity", 0)
.call(highlightCurrentDate)
.style("pointer-events", "none")
// To improve readability
.call((text) => text.clone(true))
.attr("fill", "none")
.attr("stroke", colorNeutral(0))
.attr("stroke-width", 6);
const volume = textGroup.append("g");
volume
.selectAll("text")
.data(dataset)
.join("text")
.attr("class", "interactive")
.attr("x", (d) =>
isMobile
? xScale_levels(d["volume"])
: xScale_levels(d.date) + xScale_levels.bandwidth() / 2
)
.attr("y", (d) =>
isMobile
? yScale_levels(d.date) + yScale_levels.bandwidth() / 2
: yScale_levels(d["volume"])
)
.style("text-anchor", (d) =>
isMobile ? "start" : xScale_levels(d.date) <= midPoint ? "start" : "end"
)
.attr("dy", isMobile ? 5 : -initialHeight + lineHeight)
.attr("dx", function (d) {
if (isMobile) return 20;
else
return xScale_levels(d.date) <= midPoint ? horizOffset : -horizOffset;
})
.text((d) =>
isMobile
? formatVolume(d.volume)
: `Volumen embalse: ${formatVolume(d.volume)}`
)
.style("fill", colorNeutral(1000))
.style("font-weight", 600)
.style("font-size", isMobile ? "0.8rem" : "0.9rem")
.style("opacity", 0)
.call(highlightCurrentDate)
.style("pointer-events", "none")
// To improve readability
.call((text) => text.clone(true))
.attr("fill", "none")
.attr("stroke", colorNeutral(0))
.attr("stroke-width", 6);
if (!isMobile) {
const quantity = textGroup.append("g");
quantity
.selectAll("text")
.data(dataset)
.join("text")
.attr("class", "interactive")
.attr("x", (d) => xScale_levels(d.date) + xScale_levels.bandwidth() / 2)
.attr("y", (d) => yScale_levels(d["volume"]))
.style("text-anchor", (d) =>
xScale_levels(d.date) <= midPoint ? "start" : "end"
)
.attr("dy", -initialHeight + lineHeight * 2)
.attr("dx", (d) =>
xScale_levels(d.date) <= midPoint ? horizOffset : -horizOffset
)
.text((d) => `Trasvase mensual: ${formatVolume(d.transfer)}`)
.style("fill", (d) => scaleColor(d.level))
.style("font-weight", 800)
.style("opacity", 0)
.call(highlightCurrentDate)
.style("pointer-events", "none");
}
const level = textGroup.append("g");
level
.selectAll("text")
.data(dataset)
.join("text")
.attr("class", "interactive")
.attr("x", (d) =>
isMobile
? xScale_levels(d["volume"])
: xScale_levels(d.date) + xScale_levels.bandwidth() / 2
)
.attr("y", (d) =>
isMobile
? yScale_levels(d.date) + yScale_levels.bandwidth() / 2
: yScale_levels(d["volume"])
)
.style("text-anchor", (d) =>
isMobile ? "start" : xScale_levels(d.date) <= midPoint ? "start" : "end"
)
.attr("dy", isMobile ? 20 : -initialHeight + lineHeight * 3)
.attr("dx", function (d) {
if (isMobile) return 20;
else
return xScale_levels(d.date) <= midPoint ? horizOffset : -horizOffset;
})
.text((d) => `Nivel ${d.level}`)
.style("fill", (d) => scaleColor(d.level))
.style("font-size", isMobile ? "0.8rem" : "0.9rem")
.style("font-weight", isMobile ? 800 : 400)
.style("opacity", 0)
.call(highlightCurrentDate)
.style("pointer-events", "none")
// To improve readability
.call((text) => text.clone(true))
.attr("fill", "none")
.attr("stroke", colorNeutral(0))
.attr("stroke-width", 6);
if (!isMobile) {
/// LEVELS RULES
// Levels lines
const levelsGroup = svg.append("g").attr("id", "levels");
const levels_constant = levelsGroup
.selectAll("line")
.data([5, ...levelsArray_reduced])
.join("line")
.attr("class", "limit-level")
.attr("data-level-up", (d, i) => d)
.attr("data-level-down", (d, i) => d - 1)
.attr(
"x1",
xScale_levels(dataset[0].date) + xScale_levels.bandwidth() / 2
)
.attr(
"x2",
xScale_levels(dataset[dataset.length - 1].date) +
xScale_levels.bandwidth()
)
.attr("y1", (d, i) => yScale_levels([0, ...levelsLimitsArray_reduced][i]))
.attr("y2", (d, i) => yScale_levels([0, ...levelsLimitsArray_reduced][i]))
.style("stroke", baseColorLevels)
.style("opacity", baseOpacityLevels);
// Step curve
const levels_step = levelsGroup.append("g");
levels_step
.append("path")
.datum(dataset)
.attr("d", line)
.attr("class", "limit-level")
.attr("data-level-up", 3)
.attr("data-level-down", 2)
.style("stroke", baseColorLevels)
.style("fill", "none")
.style("opacity", baseOpacityLevels);
// LEGEND LEFT SIDE
const legendGroup = svg.append("g").attr("id", "legend");
const legend = legendGroup
.selectAll("line")
.data([5, ...levelsArray])
.join("line")
.attr("class", "limit-level")
.attr("data-level-up", (d, i) => d)
.attr("data-level-down", (d, i) => d - 1)
.attr("x1", 0)
.attr("x2", margin_levels.left - 35)
.attr("y1", (d, i) => yScale_levels([0, ...levelsLimitsArray][i]))
.attr("y2", (d, i) => yScale_levels([0, ...levelsLimitsArray][i]))
.style("stroke", baseColorLevels)
.style("opacity", baseOpacityLevels);
//.style("stroke-dasharray", baseDashArray);
const r = 10;
const circles = legendGroup
.selectAll("circle")
.data(levelsArray)
.join("circle")
.attr("cx", margin_levels.left - 50)
.attr("cy", (d, i) => yScale_levels(levelsMiddlePointArray[i]) - r / 2)
.attr("r", r)
.style("fill", (d) => scaleColor(d));
const texts = legendGroup
.append("g")
.selectAll("text")
.data(levelsArray)
.join("text")
.attr("x", margin_levels.left - 50)
.attr("y", (d, i) => yScale_levels(levelsMiddlePointArray[i]))
.text((d) => d)
.style("text-anchor", "middle")
.style("font-size", "0.8rem")
.style("fill", "white");
const texts2 = legendGroup
.append("g")
.selectAll("text")
.data(levelsArray)
.join("text")
.attr("x", margin_levels.left - 70)
.attr("y", (d, i) => yScale_levels(levelsMiddlePointArray[i]) + 1)
.text("Nivel")
.style("text-anchor", "end")
.style("fill", colorNeutral(1000));
const texts3 = legendGroup
.append("g")
.selectAll("text")
.data(levelsArray)
.join("text")
.attr("x", margin_levels.left - 40)
.attr("y", (d, i) => yScale_levels(levelsMiddlePointArray[i]) + 20)
.text((d, i) => levelsCategoryArray[i])
.style("text-anchor", "end")
.style("fill", (d) => scaleColor(d))
.style("font-size", "0.85rem")
.style("font-weight", 700);
const offsetArea = 2;
const area = legendGroup
.selectAll("rect")
.data(levelsArray)
.join("rect")
.attr("class", "interactive area-legend")
.attr("data-level", (d) => d)
.attr("x", 0)
.attr("width", margin_levels.left - 35)
.attr("y", (d, i) => yScale_levels(levelsLimitsArray[i]) + offsetArea)
.attr("height", (d, i) => levelsHeightArray[i] - offsetArea * 2)
.style("fill", colorNeutral(0))
.style("opacity", baseOpacityAreas)
.style("cursor", "pointer")
.call(highlightCurrentDate)
.on("mouseover", onMouseOverArea)
.on("mouseout", onMouseOutArea);
// Foreign object method
// https://observablehq.com/@bumbeishvili/foreignobject-typical-usage
const fo = svg
.append("foreignObject") // Append for d3.v4
.classed("fo-object", true)
.attr("width", 320)
.attr("height", height_transfers)
.style("pointer-events", "none");
fo.append("xhtml:div") // Append for d3.v4
.classed("trasvase-tooltip", true);
} else {
//
}
// TITLES
const titles = svg.append("g").attr("id", "titles");
const textElY = titles
.append("text")
.attr("y", isMobile ? 20 : 20)
//.attr("y", height_levels)
//.style("text-anchor", isMobile ? "start" : "start")
.style("text-anchor", isMobile ? "start" : "start")
.style("font-weight", 600)
.style("font-size", "0.9rem");
if (!isMobile) {
textElY
.append("tspan")
.text(locale.title_levels1)
.attr("dx", 0)
.attr("dy", 0)
.attr("x", 10);
//.attr("x", margin_levels.left);
textElY
.append("tspan")
.text(locale.title_levels2)
.attr("dx", 0)
.attr("dy", 20)
.attr("x", 10);
//.attr("x", margin_levels.left);
} else {
textElY
.append("tspan")
.text(locale.title_levels1_mobile)
.attr("dx", 0)
.attr("dy", 0)
.attr("x", margin_levels.left);
textElY
.append("tspan")
.text(locale.title_levels2_mobile)
.attr("dx", 0)
.attr("dy", 20)
.attr("x", margin_levels.left);
textElY
.append("tspan")
.text(locale.title_levels3_mobile)
.attr("dx", 0)
.attr("dy", 20)
.attr("x", margin_levels.left);
textElY
.append("tspan")
.text(locale.title_levels4_mobile)
.attr("dx", 0)
.attr("dy", 20)
.attr("x", margin_levels.left);
}
return svg.node();
}