Public
Edited
Oct 21, 2024
1 fork
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
numerical_attr_names = ["Age", "Credit amount", "Duration"]
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/multi-line-chart
function LineChart(
data,
{
x = ([x]) => x, // given d in data, returns the (quantitative) x-value
y = ([, y]) => y, // given d in data, returns the (categorical) y-value
z = () => 1, // given d in data, returns the (categorical) z-value
title = "", // given d in data, returns the title text
defined, // for gaps in data
curve = d3.curveLinear, // method of interpolation between points
marginTop = 20 + 20 + 10, // top margin, in pixels
marginRight = 30, // right margin, in pixels
marginBottom = 30 + 50, // bottom margin, in pixels
marginLeft = 40 + 40 + 5, // left margin, in pixels
width = 640, // outer width, in pixels
height = 400, // outer height, in pixels
xType = d3.scaleLinear, // type of x-scale
xDomain, // [xmin, xmax]
xRange = [marginLeft, width - marginRight], // [left, right]
yType = d3.scaleLinear, // type of y-scale
yDomain, // [ymin, ymax]
xxDomain, // reversed xDomain to show ranks from top to bottom
yRange = [height - marginBottom, marginTop], // [bottom, top]
yFormat, // a format specifier string for the y-axis
yLabel, // a label for the y-axis
zDomain, // array of z-values
color,
// colors = d3.interpolateBlues, // stroke color of line, as a constant or a function of *z*
strokeLinecap, // stroke line cap of line
strokeLinejoin, // stroke line join of line
strokeWidth = 1.0, // stroke width of line
strokeOpacity = 0.4, // stroke opacity of line
mixBlendMode = "multiply", // blend mode of lines
voronoi, // show a Voronoi overlay? (for debugging)
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 });
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
import {interval} from "@vis-interfaces/range_slider_label_editted"
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more