Public
Edited
Jun 15, 2023
1 fork
Importers
8 stars
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
class Composers_chart {
constructor(item) {
this.item = item;
this.data = item.data;
this.composers = item.composers;
this.id = item.id;
this.colors = item.colors;
this.sorting = item.sorting;
this.date_extent = d3.extent(this.data, (d) => d.year);
this.type = item.type;
this.language = item.language;
this.select = d3.select(`#${this.id}`);
this.tooltip = item.tooltip;
this.align = item.align_toggle;
this.radios = item.view;
this.view = this.radios;
this.tickFormat = d3.format(".0f");
this.x_ticks_values = [1961, 1970, 1980, 1990, 2000, 2010, 2021];
this.transition_l = d3.transition().duration(1000);
this.transition_s = d3.transition().duration(100);
this.lg = item.language;
this.ref_w = 500;
this.r = 3;
this.scale_delay = d3
.scaleLinear()
.domain(this.date_extent)
.range([0, 2000]);

this.star = {
h: 12,
path:
"M6.077.417l-1.53,3.1-3.424.5A.75.75,0,0,0,.708,5.3L3.185,7.713,2.6,11.123a.75.75,0,0,0,1.088.79L6.75,10.3l3.063,1.61a.75.75,0,0,0,1.087-.79l-.586-3.41L12.792,5.3a.75.75,0,0,0-.415-1.28l-3.424-.5L7.423.417a.751.751,0,0,0-1.345,0Z"
};

this.init();
this.control_radios();
this.check_type();
this.createSVG();
this.build_singers();
this.build_composers();
}

control_radios() {
if (this.language === "es") {
this.view = this.radios === "Compositores" ? "Composers" : "Lyricists";
}
}

init() {
this.margin = { l: 60, l_y: 50, t: 75, r: 30, b: 65 };

this.width =
document.getElementById(this.id).clientWidth -
this.margin.r -
this.margin.l;

this.orientation = this.width >= this.ref_w ? "landscape" : "portrait";

const height =
document.getElementById(this.id).clientHeight -
this.margin.t -
this.margin.b;

this.height =
this.align && this.orientation === "landscape" ? height / 2 : height;
}

check_type() {
// if toggle_align true ---
// dots should align to bottom and orientation to sort
this.y_extent = [1, d3.max(this.data, (d) => d.total)];
this.y_extent_comp = [d3.max(this.composers, (d) => d.y_order), -0.5];
this.vars =
this.orientation === "landscape"
? { x: "year", x0: "year", y: "place_final", y0: "total" }
: { x: "place_final", x0: "total", y: "year", y0: "year" };

if (this.language === "en") {
this.sorting = this.align ? "N Composers / Lyricists" : "Ranking";
} else {
this.sorting = this.align ? "N Compositores / Letristas" : "Ranking";
}
}

createSVG() {
if (this.select.selectAll("svg").empty()) {
this.plot = this.select.append("svg");
this.axis_y = this.plot.append("g").attr("class", "axis axis-y");
this.axis_x = this.plot.append("g").attr("class", "axis axis-x");
this.plot_chart = this.plot.append("g").attr("class", "chart");
this.participation = this.plot_chart
.append("g")
.attr("class", "participation");
this.performer = this.plot_chart.append("g").attr("class", "performer");
this.comp = this.plot_chart.append("g").attr("class", "composers");
this.tooltip = this.plot_chart.append("g").attr("class", "tooltip");
} else {
this.plot = this.select.select("svg");
this.axis_y = this.plot.select(".axis-y");
this.axis_x = this.plot.select(".axis-x");
this.plot_chart = this.plot.select(".chart");
this.participation = this.plot_chart.select(".participation");
this.performer = this.plot_chart.select(".performer");
this.comp = this.plot_chart.select(".composers");
}

this.plot = this.select
.select("svg")
.attr("width", this.width + this.margin.r + this.margin.l)
.attr("height", this.height + this.margin.b + this.margin.t);

this.axis_y.attr(
"transform",
`translate(${this.margin.l_y}, ${this.margin.t})`
);

this.plot_chart.attr(
"transform",
`translate(${this.margin.l}, ${this.margin.t})`
);

this.axis_x.attr(
"transform",
`translate(${this.margin.l}, ${this.height + this.margin.t})`
);
}

build_scales() {
this.scale_comp =
this.orientation === "landscape"
? d3.scaleLinear().domain(this.y_extent_comp).range([0, this.height])
: d3.scaleLinear().domain(this.y_extent_comp).range([this.width, 0]);

if (this.align) {
if (this.orientation === "landscape") {
this.scale_x = d3
.scaleLinear()
.domain(this.date_extent)
.range([0, this.width]);

this.scale_y = this.scale_comp;
} else {
this.scale_x = this.scale_comp;

this.scale_y = d3
.scaleLinear()
.domain(this.date_extent)
.range([0, this.height]);
}
} else {
if (this.orientation === "landscape") {
this.scale_x = d3
.scaleLinear()
.domain(this.date_extent)
.range([0, this.width]);

this.scale_y = d3
.scaleLinear()
.domain(this.y_extent)
.range([0, this.height]);
} else {
this.scale_x = d3
.scaleLinear()
.domain(this.y_extent)
.range([this.width, 0]);

this.scale_y = d3
.scaleLinear()
.domain(this.date_extent)
.range([0, this.height]);
}
}

this.scale_colors = d3
.scaleOrdinal()
.domain(["Mixed", "Male", "Female"])
.range([this.colors.mixed, this.colors.male, this.colors.female]);
}

add_axis() {
this.build_scales();
if (this.orientation === "landscape") {
this.add_axis_landscape();
} else {
this.add_axis_portrait();
}

this.plot.attr("class", this.orientation);
}

// builds landscape axis
add_axis_landscape() {
// axis
const axis_x = d3
.axisBottom(this.scale_x)
.tickPadding(8)
.tickFormat(this.tickFormat)
.tickValues(this.x_ticks_values);

const tickValues = this.align
? [0, 5, 10, this.y_extent_comp[0]]
: [1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, this.y_extent[1]];

const axis_y = d3
.axisLeft()
.scale(this.scale_y)
.tickPadding(5)
.tickSize(-this.width - (this.margin.l - this.margin.l_y) * 2)
.tickValues(tickValues);

this.axis_x.call(axis_x);

this.axis_x
.selectAll(".title-axis")
.data([{ en: "Year", es: "Año" }])
.join("text")
.text((d) => (this.language === "en" ? d.en : d.es))
.attr("class", "title-axis")
.attr("transform", `translate(-15, 50)`);

this.axis_y.call(axis_y);

this.axis_y.selectAll(".tick").attr("class", (d, i) => `tick line_${d}`);

if (this.align) {
this.axis_y.selectAll(".line_1").selectAll(".star").remove();
this.axis_y.selectAll(".line_1").attr("class", "line-1");
} else {
this.axis_y
.selectAll(".line_1")
.selectAll(".star")
.data([this.star])
.join("path")
.attr("d", this.star.path)
.attr("class", "star")
.attr(
"transform",
`translate(${-this.star.h * 2.5}, ${-this.star.h / 2})`
);
}

this.axis_y
.selectAll(".title-axis")
.data([this.sorting])
.join("text")
.text((d) => d)
.attr("class", "title-axis")
.attr("transform", `translate(-30, ${-this.star.h - 5})`);
}

add_axis_portrait() {
const tickValues = this.align
? [0, 5, 10]
: [1, 5, 10, 15, 20, 25, this.y_extent[1]];

this.axis_x.attr(
"transform",
`translate(${this.margin.l}, ${this.margin.t - 10})`
);

// axis vertical
const axis_x = d3
.axisTop(this.scale_x)
.tickPadding(5)
.tickSize(-this.height)
.tickValues([1, 5, 10, 15, 20, 25, this.y_extent[1]]);

const axis_y = d3
.axisLeft()
.scale(this.scale_y)
.tickPadding(8)
.tickFormat(this.tickFormat)
.tickValues(this.x_ticks_values);

this.axis_x.call(axis_x);
this.axis_y.call(axis_y);

this.axis_y
.selectAll(".title-axis")
.data(["Year"])
.join("text")
.text((d) => d)
.attr("class", "title-axis")
.attr("text-anchor", "end")
.attr("transform", `translate(-10, -15)`);

this.axis_x.selectAll(".tick").attr("class", (d, i) => `tick line_${d}`);

if (this.align) {
this.axis_x.selectAll(".line_1").selectAll(".star").remove();
this.axis_x.selectAll(".line_1").remove();
} else {
this.axis_x
.selectAll(".line_1")
.selectAll(".star")
.data([this.star])
.join("path")
.attr("d", this.star.path)
.attr("class", "star")
.attr(
"transform",
`translate(${-this.star.h / 2}, ${-this.star.h * 2 - 10})`
);
}

this.axis_x
.selectAll(".title-axis")
.data([this.sorting])
.join("text")
.text((d) => d)
.attr("class", "title-axis")
.attr(
"transform",
this.align
? `translate(${this.scale_x(10.2)}, ${-this.star.h * 2})`
: `translate(${this.scale_x(2)}, ${-this.star.h * 2})`
);
}

check_xy_star(d) {
let xy;
if (this.align) {
xy =
this.orientation === "landscape"
? `translate(${
this.scale_x(d.year) - this.star.h / 2
}, ${this.scale_y(0.5)})`
: `translate(${this.scale_x(0)}, ${
this.scale_y(d.year) - this.star.h / 2
})`;
} else {
xy =
this.orientation === "landscape"
? (xy = `translate(${this.scale_x(d.year) - this.star.h / 2}, ${
this.scale_y(d.place_final) - this.star.h / 2
})`)
: `translate(${this.scale_x(d.place_final) - this.star.h / 2}, ${
this.scale_y(d.year) - this.star.h / 2
})`;
}

return xy;
}

// builds chart
build_singers() {
this.add_axis();
if (this.align) {
this.participation.selectAll(".part-h").remove();
this.participation.selectAll(".part").remove();
} else {
this.build_lines();
}

// star
this.participation
.selectAll(".stars")
.data(this.data)
.join(
(enter) =>
enter
.append("path")
.attr("id", (d) => `singer_${d.year}`)
.attr("class", (d) => `stars ${d.year}`)
.attr("transform", (d) => this.check_xy_star(d))
.style("opacity", 0)
.call((enter) => {
enter
.transition(this.transition_l)
.delay((d, i) => this.scale_delay(d.year))
.attr("transform", (d) => this.check_xy_star(d))
.style("opacity", 1);
}),
(update) =>
update.call((update) => {
update
.transition(this.transition_s)
.delay((d) => this.scale_delay(d.year))
.attr("transform", (d) => this.check_xy_star(d))
.style("opacity", 1);
})
)
.attr("d", this.star.path)
.style("stroke", (d) => this.scale_colors(d.performer_gender))
.style("stroke-weight", 2)
.style("stroke-opacity", (d) => {
const comp = d.composers_list.filter((e) => d.performer === e.name);
return comp.length !== 0 ? 1 : 0.5;
})
.style("fill", (d) => {
const comp = d.composers_list.filter((e) => d.performer === e.name);
return comp.length !== 0
? this.scale_colors(d.performer_gender)
: this.colors.bkg;
});
}

translate_comp(d) {
let translate = "translate(0, 0)";

if (this.align === false && this.orientation === "landscape") {
translate = `translate(${this.star.h / 1.2}, ${
this.scale_y(d[1][0].place_contest) -
((this.r * 2 + 1) * d[1].length) / 2
})`;
} else if (this.align === false && this.orientation === "portrait") {
translate = `translate(${
this.scale_x(d[1][0].place_contest) -
((this.r * 2 + 1) * d[1].length) / 2
}, ${this.star.h / 1.2})`;
}

return translate;
}

cy_comp(d) {
let cy = this.scale_comp(d.y_order);

if (this.align === false && this.orientation === "landscape") {
cy = (this.r * 2 + 1) * d.y_order;
} else if (this.align && this.orientation === "portrait") {
cy = this.scale_y(d.year);
} else if (this.align === false && this.orientation === "portrait") {
cy = this.scale_y(d[this.vars.y0]);
}

return cy;
}

cx_comp(d) {
let cx = this.scale_x(d[this.vars.x0]);

if (this.align === false && this.orientation === "portrait") {
cx = (this.r * 2 + 1) * d.y_order;
} else if (this.align && this.orientation === "portrait") {
cx = this.scale_comp(d.y_order);
}

return cx;
}

build_composers() {
const years = d3.groups(
this.composers.filter((d) => d.type === this.view),
(d) => d.year
);

const this_g = this.comp
.selectAll(".composers_year")
.data(years, (d) => d[0])
.join("g")
.attr("class", "composers_year")
.attr("id", (d) => `comp_${d[0]}`)
.attr("transform", (d) => this.translate_comp(d));

this_g
.selectAll(".composer")
.data(
(d) => d[1],
(e) => e.name
)
.join(
(enter) =>
enter
.append("circle")
.attr("cx", (d) => this.cx_comp(d))
.attr("cy", (d) => this.cy_comp(d))
.style("opacity", 0)
.transition(this.transition_l)
.delay((d, i) => this.scale_delay(d.year))
.style("opacity", 1)
.attr("cx", (d) => this.cx_comp(d))
.attr("cy", (d) => this.cy_comp(d)),
(update) =>
update.call((update) => {
update
.transition(this.transition_s)
.delay((d, i) => this.scale_delay(d.year))
.style("opacity", 1)
.attr("cx", (d) => this.cx_comp(d))
.attr("cy", (d) => this.cy_comp(d));
})
)
.attr("class", (d) => `composer c_${d.year}`)
.attr("r", this.r)
.style("stroke", this.colors.bkg)
.style("stroke-weight", 2)
.style("fill", (d) => this.scale_colors(d.gender));
}

// participation --- lines
lines_x(d, p) {
let x =
p === "x1" ? this.scale_x(d[this.vars.x0]) : this.scale_x(d[this.vars.x]);

if (this.orientation === "portrait" && p === "x2") {
x = x - this.star.h / 2;
}

return x;
}

lines_y(d, p) {
let y =
p === "y1" ? this.scale_y(d[this.vars.y0]) : this.scale_y(d[this.vars.y]);

if (this.orientation === "landscape" && p === "y2") {
y = y + this.star.h / 2;
}

return y;
}

build_lines() {
this.participation
.selectAll(".part")
.data(this.data)
.join(
(enter) =>
enter
.append("line")
.attr("class", "part")
.attr("x1", (d) => this.lines_x(d, "x1"))
.attr("x2", (d) => this.lines_x(d, "x1"))
.attr("y1", (d) => this.lines_y(d, "y1"))
.attr("y2", (d) => this.lines_y(d, "y1"))
.call((enter) => {
enter
.transition(this.transition_l)
.delay((d, i) => this.scale_delay(d.year))
.attr("x2", (d) => this.lines_x(d, "x2"))
.attr("y2", (d) => this.lines_y(d, "y2"));
}),
(update) =>
update.call((update) => {
update
.transition(this.transition_s)
.attr("x1", (d) => this.lines_x(d, "x1"))
.attr("x2", (d) => this.lines_x(d, "x2"))
.attr("y1", (d) => this.lines_y(d, "y1"))
.attr("y2", (d) => this.lines_y(d, "y2"));
})
)
.style("opacity", 0.5)
.style("stroke", "var(--text-axis-y)");

// horizontal lines
this.participation
.selectAll(".part-h")
.data(this.data)
.join(
(enter) =>
enter
.append("line")
.attr("class", "part-h")
.attr("x1", (d) => this.scale_x(d[this.vars.x0]))
.attr("x2", (d) => this.scale_x(d[this.vars.x0]))
.attr("y1", (d) => this.scale_y(d[this.vars.y0]))
.attr("y2", (d) => this.scale_y(d[this.vars.y0]))
.call((enter) => {
enter
.attr("x1", (d) =>
this.orientation === "landscape"
? this.scale_x(d[this.vars.x0]) - 5
: this.scale_x(d[this.vars.x0])
)
.attr("x2", (d) =>
this.orientation === "landscape"
? this.scale_x(d[this.vars.x0]) + 5
: this.scale_x(d[this.vars.x0])
)
.attr("y1", (d) =>
this.orientation === "landscape"
? this.scale_y(d[this.vars.y0])
: this.scale_y(d[this.vars.y0]) - 5
)
.attr("y2", (d) =>
this.orientation === "landscape"
? this.scale_y(d[this.vars.y0])
: this.scale_y(d[this.vars.y0]) + 5
);
}),
(update) =>
update
.attr("x1", (d) =>
this.orientation === "landscape"
? this.scale_x(d[this.vars.x0]) - 5
: this.scale_x(d[this.vars.x0])
)
.attr("x2", (d) =>
this.orientation === "landscape"
? this.scale_x(d[this.vars.x0]) + 5
: this.scale_x(d[this.vars.x0])
)
.attr("y1", (d) =>
this.orientation === "landscape"
? this.scale_y(d[this.vars.y0])
: this.scale_y(d[this.vars.y0]) - 5
)
.attr("y2", (d) =>
this.orientation === "landscape"
? this.scale_y(d[this.vars.y0])
: this.scale_y(d[this.vars.y0]) + 5
)
)
.style("opacity", 0.5)
.style("stroke", "var(--text-axis-y)");
}

mouseover_performers(item, type) {
this.participation
.selectAll(`#${item.target.id}`)
.classed("highlighted", true);

const year = +item.target.id.split("_")[1];
this.comp.selectAll(`.c_${year}`).classed("highlighted", true);

const entry = this.data.filter((d) => d.year === year);
const entry_comp = this.composers.filter((d) => d.year === year);
this.tooltip.update_text_c(entry[0], this, entry_comp);
}

mouseleave(item, type) {
if (type === "comp") {
const year = +item.target.classList[1].split("_")[1];

this.participation
.selectAll(`#singer_${year}`)
.classed("highlighted", false);
} else {
this.participation
.selectAll(`#${item.target.id}`)
.classed("highlighted", false);
}

this.comp.selectAll(".composer").classed("highlighted", false);
this.tooltip.hide();
}

mouseover_composers(item) {
const year = +item.target.classList[1].split("_")[1];

this.participation
.selectAll(`#singer_${year}`)
.classed("highlighted", true);

this.comp
.selectAll(`.${item.target.classList[1]}`)
.classed("highlighted", true);

const entry = this.data.filter((d) => d.year === year);
const entry_comp = this.composers.filter((d) => d.year === year);
this.tooltip.update_text_c(entry[0], this, entry_comp);
}

resize() {
// svg
this.init();
this.check_type();

this.plot
.attr("width", this.width + this.margin.r + this.margin.l)
.attr("height", this.height + this.margin.t + this.margin.b);

this.axis_x.attr(
"transform",
`translate(${this.margin.l}, ${this.height + this.margin.t})`
);

// singers
this.add_axis();
if (this.align === false) {
this.participation
.selectAll(".part")
.attr("x1", (d) => this.lines_x(d, "x1"))
.attr("x2", (d) => this.lines_x(d, "x2"))
.attr("y1", (d) => this.lines_y(d, "y1"))
.attr("y2", (d) => this.lines_y(d, "y2"));

// horizontal lines
this.participation
.selectAll(".part-h")
.attr("x1", (d) =>
this.orientation === "landscape"
? this.scale_x(d[this.vars.x0]) - 5
: this.scale_x(d[this.vars.x0])
)
.attr("x2", (d) =>
this.orientation === "landscape"
? this.scale_x(d[this.vars.x0]) + 5
: this.scale_x(d[this.vars.x0])
)
.attr("y1", (d) =>
this.orientation === "landscape"
? this.scale_y(d[this.vars.y0])
: this.scale_y(d[this.vars.y0]) - 5
)
.attr("y2", (d) =>
this.orientation === "landscape"
? this.scale_y(d[this.vars.y0])
: this.scale_y(d[this.vars.y0]) + 5
);
}

// star
this.participation
.selectAll(".stars")
.attr("transform", (d) => this.check_xy_star(d));

// composers
this.comp
.selectAll(".composers_year")
.attr("transform", (d) => this.translate_comp(d));

this.comp
.selectAll(".composers_year")
.selectAll(".composer")
.attr("cx", (d) => this.cx_comp(d))
.attr("cy", (d) => this.cy_comp(d));
}
}
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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more