function phaseChart(data, opts_param) {
const minX = d3.min(data.map(a => d3.min(a.series.map(s => s.x).filter(a => a > 0))));
const maxX = d3.max(data.map(a => d3.max(a.series.map(s => s.x).filter(a => a >= 0))));
const minY = d3.min(data.map(a => d3.min(a.series.map(s => s.y).filter(a => a>=-1))));
const maxY = d3.max(data.map(a => d3.max(a.series.map(s => s.y).filter(a => a<=100))));
const maxR = d3.max(data.map(a => a.r));
const options = {
bndsX: [Math.max(0.01, minX * 0.95), Math.min(1000, maxX * 1.2)],
bndsY: [Math.max(0, (minY+1)*0.95)-1, Math.min(6, (maxY+1)*1.1)-1],
w: width,
h: width*9/16,
scaleR: 1,
clampX: true,
clampY: true,
...opts_param
};
const margin = ({top: 10 + (options.title ? 10 : 0), right: 5, bottom: 35, left: 45});
const nTicksX = options.w / 40;
const smoothW = Math.round(7 + Math.sqrt(options.smooth));
const scaleX = d3.scaleLog(options.bndsX, [margin.left, options.w - margin.right]);
scaleX.clamp(options.clampX);
const scaleXAxis = d3.axisBottom(scaleX).ticks(nTicksX, ",").tickFormat(d3.format('.2s'));
const xAxis = g => g
.attr("font-family", "'IBM Plex Sans',sans-serif")
.attr("transform", `translate(0,${options.h - margin.bottom})`)
.call(scaleXAxis)
.call(g => g.select(".domain").remove())
.call(g => g.append("text")
.attr("x", options.w)
.attr("y", margin.bottom - 4)
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.text("Primeri na " + (pop_norm/1000) + "k preb. (" + smoothW + "d glajenje)→"));
const yTicks = indexTicks.filter(a => inRange(a, options.bndsY));
const scaleY = d3.scaleLinear(options.bndsY, [options.h - margin.bottom, margin.top]).domain(options.bndsY);
scaleY.clamp(options.clampY);
const yAxis = g => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(scaleY).tickValues(yTicks).tickFormat(idx => d3.format('.00%')( ratio_from_index(idx) - 1 )))
.call(g => g.select(".domain").remove())
.call(g => g.append("text")
.attr("x", -margin.left)
.attr("y", 7)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text("↑ Tedenska rast (" + smoothW + "d glajenje)"));
const radius = d3.scaleSqrt([0, 314], [0, options.w / 20 * options.scaleR]);
const grid = g => g
.attr("stroke", "black")
.attr("stroke-width", 0.05)
.call(g => g.append("g")
.selectAll("line")
.data(scaleX.ticks(nTicksX))
.join("line")
.attr("x1", d => 0.5 + scaleX(d))
.attr("x2", d => 0.5 + scaleX(d))
.attr("y1", margin.top)
.attr("y2", options.h - margin.bottom))
.call(g => g.append("g")
.selectAll("line")
.data(yTicks)
.join("line")
.attr("y1", d => 0.5 + scaleY(d))
.attr("y2", d => 0.5 + scaleY(d))
.attr("x1", margin.left)
.attr("x2", options.w - margin.right));
const svg = d3.create("svg")
.attr("viewBox", [0, 0, options.w, options.h])
.attr("font-family", "'IBM Plex Sans',sans-serif");
svg.append("g").append("line") // attach a line
.style("stroke", "black") // colour the line
.style("stroke-width", 0.2)
.attr("x1", scaleX(0)) // x position of the first end of the line
.attr("y1", scaleY(0)) // y position of the first end of the line
.attr("x2", scaleX(100000)) // x position of the second end of the line
.attr("y2", scaleY(0));
svg.append("g")
.selectAll("rect")
.data([
{x1: 0.01, x2: 14.5, c: 'green'},
{x1: 14.5, x2: 29, c: 'yellow'},
{x1: 29, x2: 48, c: 'orange'},
{x1: 48, x2: 65, c: 'red'},
{x1: 65, x2: 1000, c: 'darkgray'}])
.join("rect")
.attr("opacity",0.2)
.attr("x", d => scaleX(d.x1))
.attr("y", options.h - margin.bottom)
.attr("width", d => scaleX(d.x2) - scaleX(d.x1))
.attr("height", 20)
.attr("fill", d => d.c);
svg.append("g")
.call(xAxis);
svg.append("g")
.call(yAxis);
svg.append("g")
.call(grid);
const line = d3.line()
.defined(d => d && (d.x || isFinite(d.x)) && (d.y || isFinite(d.y)))
.x((d,i) => scaleX(d.x || 0) || 0)
.y((d,i) => scaleY(d.y || 1) || 0);
svg.append("g")
.selectAll("path")
.data(data)
.join("path")
.attr("fill", "none")
.attr("stroke", d => reg_colors[d.region])
.attr("stroke-width", d => radius(d.r)/8)
.attr("stroke-linecap", "round")
.attr("stroke-linejoin", "round")
.attr("opacity", '0.4')
//.style("mix-blend-mode", "multiply")
.attr("d", d => line(d.series));
svg.append("g")
.attr("stroke", "none")
.selectAll("circle")
.data(data)
.join("circle")
.attr("fill", d => reg_colors[d.region])
.attr("opacity", "0.65")
.attr("cx", d => scaleX(d.x))
.attr("cy", d => scaleY(d.y))
.attr("r", d => radius(d.r))
.append("title")
.text(d => d.name);
if (options.labels) {
svg.append("g")
.attr("font-size", 10)
.selectAll("text")
.data(data)
.join("text")
.attr("fill", "black")
.attr("dy", "0.35em")
.attr("x", d => scaleX(d.x) + 7)
.attr("y", d => scaleY(d.y) - radius(d.r))
.text(d => d.name);
}
svg.append("g")
.append("text")
.attr("font-size", 10)
.attr("text-anchor", "end")
.attr("x", options.w)
.attr("y", margin.top)
.text(strFromDate(d3.timeDay.offset(last_data_update, -options.hist_lookback)));
const title_text = (typeof options.title === 'string' || options.title instanceof String)
? options.title
: options.title(data);
if (title_text) {
svg.append("g")
.attr("font-size", 20)
.attr("font-weight", "bold")
.attr("text-anchor", "middle")
.append("text")
.attr("x", 0.5 * (options.w + margin.left - margin.right))
.attr("y", margin.top-2)
.text(title_text);
}
return svg.node();
}