Published
Edited
Dec 13, 2020
Importers
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
render_data_table(table_info)
Insert cell
Insert cell
Insert cell
chart = {
const width = margins.width + 40;
const div = d3
.create("div")
.attr("id", "theMainShebang")
.attr("width", margins.width + "px");

const svg_div = div
.append("div")
.attr("width", "100%")
.attr("class", "svg-flex");

const svg = svg_div
.append("svg")
.attr("viewBox", [0, 0, width, margins.svg_main_height])
.classed("top", true);

const svgBottom = svg_div
.append("svg")
.attr("viewBox", [0, 0, width, margins.svg_diff_height])
.classed("bottom", true);

const songBox = div
.append("div")
.attr("id", "songBox")
.attr("width", "30%")
.text("Songs");
const text = songBox.append("div").attr("class", "titles");

const title = svg
.append("text")
.attr("transform", `translate(${width / 2}, 20)`)
.attr("text-anchor", "middle");
const xAxisLabel = svg
.append("text")
.attr(
"transform",
`translate(${width / 2}, ${margins.svg_main_height - 20})`
)
.attr("text-anchor", "middle")
.text(`Index of word (token)`);
const yAxisLabel = svg
.append("text")
.attr(
"transform",
`translate(${20}, ${margins.svg_main_height / 2}), rotate(270)`
)
.attr("text-anchor", "middle")
.text(`Cumulative Sentiment (AFINN)`);

const hell = svg // lol i can add the focus on point thing later bc rn i am SUFFERING
.append('rect')
.style("fill", "none")
.style("pointer-events", "all")
.attr('width', width)
.attr('height', margins.height);

// create empty axis for main
const xAxis = svg
.append("g")
.attr("class", "xAxis")
.attr(
"transform",
`translate(0, ${margins.svg_main_height - margins.bottom})`
);
const yAxis = svg
.append("g")
.attr("class", "yAxis")
.attr("transform", `translate(${margins.left}, 0)`);

// create empty axis for other
const xAxisSub = svgBottom
.append("g")
.attr("class", "xAxis")
.attr(
"transform",
`translate(0, ${margins.svg_diff_height - margins.bottom})`
);
const yAxisSub = svgBottom
.append("g")
.attr("class", "yAxis")
.attr("transform", `translate(${margins.left}, 0)`);

const line = svgBottom.append("g").attr("clip-path", "url(#clip)");

div.node().update = album => {
title.text(`Sentiment chart for the album: ${album}`);
// filter down data
let filtered_data = data.filter(
d => d.album == album || selected_songs.has(d.track)
);
console.log(filtered_data);
cumulate(filtered_data, "track", "afinn");
let grouped = d3.group(filtered_data, d => d.track);

// add y axis
let y = d3
.scaleLinear()
.domain(d3.extent(filtered_data, d => d.value))
.range([margins.svg_main_height - margins.bottom, margins.top]);
yAxis
.transition()
.duration(500)
.call(d3.axisLeft(y));
// add x axis
let x = d3
.scaleLinear()
.domain([0, d3.max([...grouped.values()], d => d.length)])
.range([margins.left, width - margins.right]);
xAxis
.transition()
.duration(500)
.call(d3.axisBottom(x))
.attr("transform", `translate(0, ${y(0)})`); // here to re-align x axis to y(0)

// ADD SUB AXIS
// add y axis
let ySub = d3
.scaleLinear()
.domain(d3.extent(filtered_data, d => d.value))
.range([margins.svg_diff_height - margins.bottom, margins.top]);
ySub.ticks(3);
yAxisSub
.transition()
.duration(500)
.call(d3.axisLeft(ySub));
// add x axis
let xSub = d3
.scaleLinear()
.domain([0, d3.max([...grouped.values()], d => d.length)])
.range([margins.left, width - margins.right]);
xAxisSub
.transition()
.duration(500)
.call(d3.axisBottom(x))
.attr("transform", `translate(0, ${ySub(0)})`); // here to re-align x axis to y(0)

// add color
let color = d3
.scaleOrdinal()
.domain([...grouped.keys()])
.range(d3.schemeCategory10);

// add legend
const grouped_keys = enumerate([...grouped.keys()]); // trying to remember why I did this
text
.selectAll(".song")
.data(grouped_keys, d => d.data)
.join(
enter => {
let text = enter
.append("p")
.text(d => d.data)
.attr(
"class",
d =>
`song ${d.data
.replace(/\s+|\W+/g, "_")
.replaceAll(/\d/g, "digit")}`
)
.classed("selected", d => selected_songs.has(d.data))
.classed("locked", d => selected_songs.has(d.data));

text.on("mouseover", (d, i) => {
mouseOver(
d,
i,
line_styles.width_selected,
svg,
svgBottom,
songBox
);
});
text.on("mouseout", (d, i) => {
mouseOut(d, i, line_styles.width, svg, svgBottom, songBox);
});
text.on("click", (d, i) => {
click(d, i, line_styles.width, svg, svgBottom, songBox);
});
return text;
},
update =>
update
.text(d => d.data)
.classed("selected", d => selected_songs.has(d.data)),
exit => exit.remove()
);

let clip = svgBottom
.append("defs")
.append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("width", width - margins.right)
.attr("height", margins.svg_diff_height)
.attr("x", 0)
.attr("y", 0);

let brush = d3
.brushX()
.extent([
[margins.left, margins.top],
[width - margins.right, margins.svg_diff_height - margins.bottom]
])
.on("end", updateChart);

// draw svg
let grouped2 = [...grouped.keys()].map(d => ({
data: d,
arr: grouped.get(d)
}));
console.log("grp");
console.log(grouped2);
svg
.selectAll(".line")
.data(grouped2, d => d.data)
.join(
enter =>
enter
.append("path")
.attr(
"class",
d =>
`line ${d.data
.replace(/\s+|\W+/g, "_")
.replace(/\d/g, "digit")}`
)
.classed("selected", d => selected_songs.has(d.data))
.classed("locked", d => selected_songs.has(d.data))
.attr("opacity", .3)
.attr("stroke", d => color(d.data))
.attr("fill", "none")
.attr("stroke-width", line_styles.width)
.attr("d", d =>
d3
.line()
.x(d => x(d.x))
.y(d => y(d.value))(d.arr)
)
.attr("cursor", "pointer")
.on("click", (d, i) =>
click(d, i, line_styles.width_selected, svg, svgBottom, songBox)
),
update =>
update.call(update =>
update
.transition()
.duration(500)
.attr("d", d =>
d3
.line()
.x(d => x(d.x))
.y(d => y(d.value))(d.arr)
)
),
exit => exit.remove()
);

line
.selectAll(".line")
.data(grouped2, d => d.data)
.join(
enter =>
enter
.append("path")
.attr(
"class",
d =>
`line ${d.data
.replace(/\s+|\W+/g, "_")
.replace(/\d/g, "digit")}`
)
.classed("selected", d => selected_songs.has(d.data))
.attr("opacity", .3)
.attr("stroke", d => color(d.data))
.attr("fill", "none")
.attr("stroke-width", line_styles.width - 1)
.attr("d", d =>
d3
.line()
.x(d => xSub(d.x))
.y(d => ySub(d.value))(d.arr)
),
update =>
update.call(update =>
update
.transition()
.duration(500)
.attr("d", d =>
d3
.line()
.x(d => xSub(d.x))
.y(d => ySub(d.value))(d.arr)
)
),
exit => exit.remove()
);
line
.append("g")
.attr("class", "brush")
.call(brush);

let idleTimeout;
function idled() {
idleTimeout = null;
}

function updateChart() {
let extent = d3.brushSelection(this);
if (!extent) {
if (!idleTimeout) return (idleTimeout = setTimeout(idled, 350));
xSub.domain([4, 8]); // but y tho?
x.domain([4, 8]);
} else {
xSub.domain([x.invert(extent[0]), x.invert(extent[1])]);
x.domain([x.invert(extent[0]), x.invert(extent[1])]);
line.select("brush").call(brush.move, null);
}
// update axis and position for bottom
let domain = xSub.domain();
xAxisSub
.transition()
.duration(1000)
.call(d3.axisBottom(xSub));
line
.selectAll(".line")
.transition()
.duration(1000)
.attr("d", d =>
d3
.line()
.x(d => xSub(d.x))
.y(d => ySub(d.value))(
d.arr.filter(datum => datum.x >= domain[0] || datum.x <= domain[1])
)
);

// update axis and position for TOP
domain = x.domain();
xAxis
.transition()
.duration(1000)
.call(d3.axisBottom(x));
svg
.selectAll(".line")
.transition()
.duration(1000)
.attr("d", d =>
d3
.line()
.x(d => x(d.x))
.y(d => y(d.value))(
d.arr.filter(datum => datum.x >= domain[0] || datum.x <= domain[1])
)
);
}

svgBottom.on("dblclick", function() {
let max = d3.max([...grouped.values()], d => d.length);
xSub.domain([0, max]);
x.domain([0, max]);

xAxisSub
.transition()
.duration(1000)
.call(d3.axisBottom(xSub));
line
.selectAll('.line')
.transition()
.duration(1000)
.attr("d", d =>
d3
.line()
.x(d => xSub(d.x))
.y(d => ySub(d.value))(d.arr)
);

xAxis
.transition()
.duration(1000)
.call(d3.axisBottom(x));
svg
.selectAll('.line')
.transition()
.duration(1000)
.attr("d", d =>
d3
.line()
.x(d => x(d.x))
.y(d => y(d.value))(d.arr)
);
});
};

return div.node();
}
Insert cell
Insert cell
md`
The first technique I employed was selection. This allows users to highlight and keep track of a sentiment line as they are exploring the data. By clicking on a song title or line, you make the color deeper and then "lock" it for comparison with other songs. This allows users to ask, how does this song compare to songs from other albums. It is mainly used as an action to enhance the other two techniques.

The second technique I used was explore. This one lets users dial in on interesting parts of my plot by using the mini plot on the bottom. you can highlight a selection that is interesting and the plots will zoom to it. Double clicking the bottom plot resets the zoom. This lets users ask how sentiment changed over specific segments of time.

The final technique I used was filter. This lets users conditionally select which album to use, or even to simply use their bank of selected songs. This technique lets users ask how sentiments changed for SPECIFIC SONGS from SPECIFIC ALBUMS.
`
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
data.filter(d => d.track == "goodbye")
Insert cell
Insert cell
Insert cell
Insert cell
albums = album_data.map(d => d.album)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart.update(dd2)
Insert cell
Insert cell
Insert cell
import {select} from "@jashkenas/inputs"
Insert cell
line_styles = ({
width: 3,
width_selected: 4,
opacity: .3,
opacity_selected: 1
})
Insert cell
html`<style>
#theMainShebang {
display: flex;
width: 100%;
justify-content: space-between;
}

#songBox {
height: 100%;
text-align: center;
}
.titles {
margin-bottom: 12px;
height: ${margins.height - margins.bottom}px;
text-align: left;
overflow-y: scroll;
width: 190px;
}

.svg-flex {
flex-grow:3;
}
.song {
opacity: .5;
cursor: pointer;
margin-left: 6px;
margin-right: 6px;
transition: box-shadow .3s
}

.song:hover {
box-shadow: 0 0 6px rgba(33,33,33,.2);
}

.selected {
opacity: 1;
}
.top .selected {
stroke-width: ${line_styles.width_selected};
}

.bottom .selected {
stroke-width: ${line_styles.width_selected - 1};
}

</style>`
Insert cell
Insert cell
margins = ({
height: 600,
svg_main_height: 600,
svg_diff_height: 150,
top: 30,
bottom: 40,
left: 50,
right: 20,
line_length: 150,
width: width
})
Insert cell
Insert cell
Insert cell
_ = require("lodash")
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