function LineChart(
data,
{
x = ([x]) => x,
y = ([, y]) => y,
z = () => 1,
title = "",
defined,
curve = d3.curveLinear,
marginTop = 20 + 20 + 10,
marginRight = 30,
marginBottom = 30 + 50,
marginLeft = 40 + 40 + 5,
width = 640,
height = 400,
xType = d3.scaleLinear,
xDomain,
xRange = [marginLeft, width - marginRight],
yType = d3.scaleLinear,
yDomain,
xxDomain,
yRange = [height - marginBottom, marginTop],
yFormat,
yLabel,
zDomain,
color,
strokeLinecap,
strokeLinejoin,
strokeWidth = 1.0,
strokeOpacity = 0.4,
mixBlendMode = "multiply",
voronoi,
selected_color_attr = "Sex",
selected_categorical_value,
scores_distribution = [],
rank_boundaries = []
} = {}
) {
// Compute values.
const X = d3.map(data, x);
const Y = d3.map(data, y);
const Z = d3.map(data, z);
const O = d3.map(data, (d) => d);
// if (defined === undefined) defined = (d, i) => !isNaN(X[i]) && !isNaN(Y[i]);
if (defined === undefined) defined = (d, i) => !isNaN(X[i]);
const D = d3.map(data, defined);
// Compute default domains, and unique the z-domain.
if (xDomain === undefined) xDomain = d3.extent(X);
if (yDomain === undefined) yDomain = Y;
if (xxDomain === undefined) xxDomain = [1, d3.max(X)];
if (zDomain === undefined) zDomain = Z;
zDomain = new d3.InternSet(zDomain);
// Omit any data not present in the z-domain.
const I = d3.range(X.length).filter((i) => zDomain.has(Z[i]));
// console.log(I);
// Construct scales and axes.
const xScale = xType(xDomain, xRange);
const yScale = d3.scalePoint(yDomain, yRange);
const xxScale = xType(xxDomain, xRange);
const xAxis = d3
.axisBottom(xxScale)
.ticks(width / 30)
.tickPadding(5)
.tickSize(15);
const yAxis = d3.axisLeft(yScale).ticks(height / 60); //.tickSizeOuter(0);
const score_histogram_axis = d3
.axisTop(xxScale)
.ticks(width / 30)
.tickSize(0)
.tickFormat((d) => "");
// Construct a line generator.
const line = d3
.line()
.defined((i) => D[i])
.curve(curve)
.x((i) => xScale(X[i]))
.y((i) => yScale(Y[i]));
const brush = d3
.brushX()
.extent([
[marginLeft, height - (marginBottom + 30)],
[width - (marginRight - 20), height - (marginBottom - 80)]
])
.on("brush", brushed)
.on("end", ended);
const score_brush = d3
.brushX()
.extent([
[marginLeft - 10, 0],
[width - (marginRight - 20), marginTop + 20]
])
.on("brush", score_brushed)
.on("end", ended);
const svg = d3
.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;")
.style("-webkit-tap-highlight-color", "transparent")
.on("dblclick", dblclicked);
const text_container = svg
.selectAll(".text_container")
.data(["0"])
.join("g")
.classed("text_container", true);
text_container
.selectAll(".title")
.data([title])
.join("text")
.classed("title", true)
.attr("x", width / 2)
.attr("y", 10)
.attr("text-anchor", "middle")
.style("font-size", "12px")
.text(title);
text_container
.selectAll(".score_high")
.data(["0"])
.join("text")
.classed("score_high", true)
.attr("x", marginLeft)
.attr("y", 7)
.attr("text-anchor", "middle")
.style("font-size", "10px")
.style("font-weight", "bold")
.style("fill", "grey")
.text("high");
text_container
.selectAll(".score_low")
.data(["0"])
.join("text")
.classed("score_low", true)
.attr("x", width - marginRight)
.attr("y", 7)
.attr("text-anchor", "middle")
.style("font-size", "10px")
.style("font-weight", "bold")
.style("fill", "grey")
.text("low");
text_container
.selectAll(".rank_high")
.data(["0"])
.join("text")
.classed("rank_high", true)
.attr("x", marginLeft)
.attr("y", height - 40)
.attr("text-anchor", "middle")
.style("font-size", "10px")
.style("font-weight", "bold")
.style("fill", "grey")
.text("high");
text_container
.selectAll(".rank_low")
.data(["0"])
.join("text")
.classed("rank_low", true)
.attr("x", width - marginRight)
.attr("y", height - 40)
.attr("text-anchor", "middle")
.style("font-size", "10px")
.style("font-weight", "bold")
.style("fill", "grey")
.text("low");
const container = svg
.selectAll(".label-container")
.data(["0"])
.join("g")
.classed("label-container", true)
.attr("display", "none");
svg
.selectAll(".brush")
.data(["0"])
.join("g")
.classed("brush", true)
.call(brush);
svg
.selectAll(".score_brush")
.data(["0"])
.join("g")
.classed("score_brush", true)
.call(score_brush);
// An optional Voronoi display (for fun).
if (voronoi)
svg
.append("path")
.attr("fill", "none")
.attr("stroke", "#ccc")
.attr(
"d",
d3.Delaunay.from(
I,
(i) => xScale(X[i]),
(i) => yScale(Y[i])
)
.voronoi([0, 0, width, height])
.render()
);
svg
.selectAll(".x_axis_g")
.data(["0"])
.join("g")
.classed("x_axis_g", true)
.attr("transform", `translate(0,${height - marginBottom})`)
.call(xAxis);
svg
.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.style("font", "20px times")
.call(yAxis)
.call((g) => g.select(".domain").remove())
.call(
voronoi
? () => {}
: (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)
);
let temp = d3.group(I, (i) => Z[i]);
let temp_values = [...temp.keys()];
let indexes = [...Array(temp_values.length).keys()];
const path_container = svg
.selectAll(".path_container")
.data(["0"])
.join("g")
.classed("path_container", true);
const path = path_container
.selectAll("path")
.data(d3.group(I, (i) => Z[i]))
.join("path")
.attr("fill", "none")
.attr("stroke-linecap", strokeLinecap)
.attr("stroke-linejoin", strokeLinejoin)
.attr("stroke-width", strokeWidth)
.attr("stroke-opacity", strokeOpacity)
.style("mix-blend-mode", mixBlendMode)
.attr("stroke", function (d, i) {
if (
filtered_data[indexes[i]][selected_color_attr] ===
selected_categorical_value
) {
return color_scale(filtered_data[indexes[i]][selected_color_attr]);
} else {
return "#c0c0c0";
}
})
.attr("d", ([, I]) => line(I))
.on("click", function () {
d3.select(this).classed("selected", true);
mutable on_click = true;
let temp = JSON.stringify(this);
let id = temp.substring(temp.indexOf("[") + 1, temp.indexOf(","));
let subset = temp.substring(temp.indexOf(",") + 1, temp.indexOf("]") + 1);
let rank_index = parseInt(subset.substring(1, subset.indexOf(",")));
let score_index = parseInt(
subset.substring(subset.indexOf(",") + 1, subset.length - 1)
);
highlighted_ids.push(parseInt(id));
clicked_ranks.push(data[rank_index]["value"]);
on_click_ids.push(id);
on_click_rank_indexes.push(rank_index);
on_click_data.push({
id: id,
rank: data[rank_index]["value"],
rank_coord: xScale(data[rank_index]["value"])
});
get_single_label(on_click_data);
});
svg
.selectAll(".score_histogram_axis")
.data(["0"])
.join("g")
.classed("score_histogram_axis", true)
.attr("transform", `translate(0,${marginTop})`)
.call(score_histogram_axis);
function get_score_x_coordinates() {
let res = {};
let starting_x_coords_lst = [];
let html_string_1 = path["_groups"][0][0]["outerHTML"].substring(
path["_groups"][0][0]["outerHTML"].indexOf("d")
);
let rank_y1 = html_string_1.substring(
html_string_1.indexOf(",") + 1,
html_string_1.indexOf("L")
);
let score_y1 = html_string_1.substring(
html_string_1.indexOf(",", html_string_1.indexOf("L")) + 1,
html_string_1.indexOf("s", html_string_1.indexOf("L")) - 2
);
for (let i = 0; i < path["_groups"][0].length; i++) {
let html_string = path["_groups"][0][i]["outerHTML"];
starting_x_coords_lst[i] = html_string.substring(
html_string.indexOf("L") + 1,
html_string.indexOf(",", html_string.indexOf("L"))
);
}
return {
coords: starting_x_coords_lst,
rank_y1: parseInt(rank_y1),
score_y1: parseInt(score_y1)
};
}
const scores_dist_values = scores_distribution.map((d) => d.dist);
const scores_dist_scale = d3
.scaleLinear()
.domain([0, d3.max(scores_dist_values)])
.range([50, 10]);
const axis_coords = get_score_x_coordinates();
const coords = axis_coords.coords;
const score_lines_data = scores_distribution;
const score_lines = svg
.selectAll(".score_lines_g")
.data(["0"])
.join("g")
.classed("score_lines_g", true)
.selectAll("line")
.data(score_lines_data)
.classed("score_lines", true)
.join("line")
.attr("x1", (d) => xScale(d.key))
.attr("x2", (d) => xScale(d.key))
.attr("y2", scores_dist_scale(0))
.attr("y1", (d) => scores_dist_scale(d.dist))
.attr("stroke", "grey");
// const rank_boundary_lines = svg
// .append("g")
// .selectAll("line")
// .data(rank_boundaries)
// .join("line")
// .attr("x1", (d) => xScale(d))
// .attr("x2", (d) => xScale(d))
// .attr("y1", axis_coords.rank_y1)
// .attr("y2", axis_coords.rank_y1 + 10)
// .attr("stroke", "grey");
var on_click_data = [];
var on_click_ids = [];
var on_click_rank_indexes = [];
function get_single_label(lst) {
let coords = lst.map((d) => d.rank_coord);
let ids = lst.map((d) => d.id);
let ranks = lst.map((d) => d.rank);
let coord = height - (marginBottom - 45);
container.attr("display", null);
console.log(ids);
const containers = container
.selectAll("g")
.data(lst)
.join("g")
.classed("single-label-container", true)
.attr(
"transform",
({ rank_coord }) => `translate(${[rank_coord, coord].join(",")})`
);
containers.selectAll("path").remove();
const boxes = containers
.append("path")
.attr("class", "tooltip")
.attr("fill", "#d58a3c")
.attr("opacity", "0.5");
containers.selectAll("text").remove();
const labels = containers
.append("text")
.data(ranks)
.text((d) => "Rank: " + d)
.attr("class", "tooltip")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("text-anchor", "middle")
.attr("y", -10)
.attr("x", 0);
const vals = { x: -23, y: -17, height: 19, width: 47 };
boxes.attr(
"d",
`M${-vals.width / 2 - 10}, 5H-5l5,-5l5, 5H${vals.width / 2 + 10}v${
vals.height + 25
}h-${vals.width + 20}z`
);
labels.attr("transform", `translate(0, 33)`);
}
var selected_ranks = [];
var s_ranks = [];
function get_labels(selected_ranks, selection, ranges) {
let x_max = xScale(d3.max(selected_ranks));
let x_min = xScale(d3.min(selected_ranks));
container.attr(
"transform",
`translate(${(x_max + x_min) / 2},${height - (marginBottom - 35)})`
);
container.attr("display", null);
const box = container
.selectAll("path")
.data([,])
.join("path")
.attr("class", "tooltip")
.attr("fill", "#d58a3c")
.attr("opacity", "0.5");
const label = container
.selectAll("text")
.data([,])
.join("text")
.attr("class", "tooltip")
.call((text) =>
text
.selectAll("tspan")
.data(`${ranges}`.split(/\n/))
.join("tspan")
.attr("x", 0)
.attr("y", (_, i) => `${i * 1.1}em`)
.text("Rank ranges: " + ranges[0] + " - " + ranges[1])
)
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("text-anchor", "middle")
.attr("y", -8);
const vals = { x: -55, y: -9, height: 11, width: 110 };
box.attr(
"d",
`M${-vals.width / 2 - 10}, 5H-5l5,-5l5, 5H${vals.width / 2 + 10}v${
vals.height + 25
}h-${vals.width + 20}z`
);
const limitW = xScale(X[data.length - 1]);
const hbw = vals.width / 2 + 10;
const hbh = vals.height / 2 + 10;
const switchHW = limitW - hbw;
const tooltipOver = `M${-hbw},-5H-5l5,5l5,-5H${hbw}v${-hbh * 2}h-${
2 * hbw
}z`;
const tooltipUnder = `M${-hbw},5H-5l5,-5l5,5H${hbw}v${hbh * 2}h-${
2 * hbw
}z`;
const tooltipLeft = `M-5,${-hbh}V-5l5,5l-5,5V${hbh}H${-hbw * 2}V-${hbh}z`;
const tooltipRight = `M5,${-hbh}V-5l-5,5l5,5V${hbh}H${hbw * 2}V-${hbh}z`;
box.attr(
"d",
selection[0] > switchHW
? tooltipLeft
: selection[1] < hbw
? tooltipRight
: tooltipUnder
);
const shiftLabelX =
selection[0] > switchHW ? -hbw : selection[1] < hbw ? hbw : 0;
const shiftLabelY =
selection[0] > switchHW || selection[1] < hbw
? vals.y - vals.height / 2 + 15
: 15 - vals.y;
label.attr("transform", `translate(${shiftLabelX},${shiftLabelY})`);
}
if (unique_highlighted_ids.length != 0) {
path.classed("selected", function (d) {
if (unique_highlighted_ids.includes(d[0])) {
s_ranks.push(X[d[1][0]]);
}
let val = get_selected_categorical_attr_value(d[0], selected_color_attr);
return (
unique_highlighted_ids.includes(d[0]) &&
val === selected_categorical_value
);
});
if (
clicked_ranks.length != 0 &&
clicked_ranks.length === highlighted_ids.length
) {
let c_data = [];
for (let i = 0; i < highlighted_ids.length; i++) {
let rank = get_rank(highlighted_ids[i], data);
c_data.push({
id: highlighted_ids[i],
rank: rank,
rank_coord: xScale(rank)
});
}
setTimeout(get_single_label(c_data), 3000);
} else {
if (s_ranks.length != 0) {
let s_rank_range = d3.extent(s_ranks);
let [init_x, init_y] = [
xScale(s_rank_range[0]),
xScale(s_rank_range[1])
];
setTimeout(get_labels(s_ranks, [init_x, init_y], s_rank_range), 3000);
}
}
}
function score_brushed({ selection }) {
if (selection) {
updateChart_score_brush(selection);
}
}
function updateChart_score_brush(sel, d) {
path.classed("selected", function (d) {
return isBrushed_score_brush(sel, d);
});
}
function isBrushed_score_brush(brush_coords, d) {
let cx = xScale(data[d[1][1]]["value"]);
var x0 = brush_coords[0],
x1 = brush_coords[1];
let val = get_selected_categorical_attr_value(d[0], selected_color_attr);
if (x0 <= cx && cx <= x1) {
highlighted_ids.push(d[0]);
selected_ranks.push(X[d[1][0]]);
}
return x0 <= cx && cx <= x1 && val === selected_categorical_value;
}
function brushed({ selection }) {
if (selection) {
updateChart(selection);
}
}
function updateChart(sel, d) {
path.classed("selected", function (d) {
return isBrushed(sel, d);
});
//.attr("stroke-opacity", 1.0);
}
function isBrushed(brush_coords, d) {
let cx = xScale(X[d[1][0]]);
var x0 = brush_coords[0],
x1 = brush_coords[1];
let val = get_selected_categorical_attr_value(d[0], selected_color_attr);
if (x0 <= cx && cx <= x1) {
highlighted_ids.push(d[0]);
selected_ranks.push(X[d[1][0]]);
}
return x0 <= cx && cx <= x1 && val === selected_categorical_value;
}
function ended({ selection }) {
if (selection) {
let ranges = d3.extent(selected_ranks);
get_labels(selected_ranks, selection, ranges);
path.attr("stroke-opacity", strokeOpacity);
}
}
function dblclicked(event, d) {
highlighted_ids.length = 0;
selected_ranks.length = 0;
on_click_ids.length = 0;
on_click_rank_indexes.length = 0;
on_click_data.length = 0;
clicked_ranks.length = 0;
mutable on_click = false;
path.classed("selected", false).attr("stroke-opacity", strokeOpacity);
container.attr("display", "none");
}
// const dot = svg.append("g").attr("display", "none");
// dot.append("circle").attr("r", 2.5);
return Object.assign(svg.node(), { value: null });
}