diff --git a/figures/interactive/top_recommendations.html b/figures/interactive/top_recommendations.html
index 04583f4..2431654 100644
--- a/figures/interactive/top_recommendations.html
+++ b/figures/interactive/top_recommendations.html
@@ -11,6 +11,7 @@
overflow: hidden;
display: grid;
font-family: system-ui, sans-serif;
+ background-color: white;
}
h1, h2 { text-align: center; }
h2 {
@@ -56,6 +57,25 @@
fieldset {
border: 0;
}
+fieldset:has(input[name="period"]) {
+ font-size: 14px;
+}
+
+label:has(input:checked) {
+ border-bottom: 3px solid black;
+}
+fieldset:has(input[name="score_sort"]) {
+ text-align: center;
+ font-size: 12px;
+}
+label:has(input[name="score_sort"]) {
+ margin-left: 5px;
+}
+/* hide the actual radio buttons */
+input[type="radio"] {
+ display: none;
+ visibility: hidden;
+}
input#search {
margin: 0 10px;
@@ -68,18 +88,33 @@
height: 15px;
margin: 2px 2px 0;
}
-.swatch.period1921::before {
+.swatch.period1921::before, .swatch.periodboth::before {
background-color:#1f77b4;
}
.swatch.period1924::before {
background-color: #ff7f0e;
}
+.swatch.periodboth {
+ position: relative;
+ margin: 0 2px;
+}
+
+.swatch.periodboth::after {
+ background-color: #ff7f0e;
+ position: absolute;
+ top: 1px;
+ left: 10px;
+ content: " ";
+ display: inline-block;
+ width: 7px;
+ height: 14px;
+}
#imf {
grid-area: plot1;
}
#imf .plot-axis svg {
- margin-top: 50px; /* adjust for shape legend on cf plot, so plots align */
+ margin-top: 10px; /* adjust for shape legend on cf plot, so plots align */
}
#cf {
grid-area: plot2;
@@ -137,23 +172,27 @@
background-color: white;
}
+.visually-hidden {
+ clip: rect(0 0 0 0);
+ clip-path: inset(50%);
+ height: 1px;
+ overflow: hidden;
+ position: absolute;
+ white-space: nowrap;
+ width: 1px;
+}
+
-
Top Recommendations
+ Top Recommendations
Implicit Matrix Factorization Model
+
@@ -174,7 +230,8 @@ Collaborative Filtering Methods
let lenskitdata = await d3.csv("lenskit_top_scores.csv");
let lenskitscores = lenskitdata.map(x => x.score);
-let lenskitdomain = [Math.min(...lenskitscores), Math.max(...lenskitscores)];
+// add some padding to domain so ticks don't overlap labels
+let lenskitdomain = [Math.min(...lenskitscores) - 0.2, Math.max(...lenskitscores) + 0.2];
let cfdata = await d3.csv("memorycf_top_scores.csv");
let cfscores = cfdata.map(x => x.score);
let cfdomain = [Math.min(...cfscores), Math.max(...cfscores)];
@@ -183,26 +240,32 @@ Collaborative Filtering Methods
// create a dict to lookup item title by item id
const item_label = titles.reduce(
(dict, el, index) => (
- // dict[el.id] = `${el.title}, ${el.author}` + (el.year ? ` (${new Number(el.year).toFixed(0)})` : '')
+ // title on first line; author and optionally (year) on second line
dict[el.id] = `${el.title}\n${el.author}` + (el.year ? ` (${new Number(el.year).toFixed(0)})`:'')
, dict),
{}
);
-// sort highest score first
-lenskitdata = d3.sort(lenskitdata, (a, b) => d3.descending(a.score, b.score));
+// calculate aggregrate item scores for lenskit sorting options
+const lenskitScores = {
+ median: d3.rollup(lenskitdata, (D) => d3.median(D, (d) => d.score), (d) => d.item_id),
+ high: d3.rollup(lenskitdata, (D) => d3.max(D, (d) => d.score), (d) => d.item_id),
+ low: d3.rollup(lenskitdata, (D) => d3.min(D, (d) => d.score), (d) => d.item_id)
+};
+
cfdata = d3.sort(cfdata, (a, b) => d3.descending(a.score, b.score));
// common plot config settings
const plotConfig = {
width: 475,
+ lenskitWidth: 525,
marginLeft: 300,
};
function plotLenskitAxis() {
/* plot axis only so it can be made sticky when actual plot scrolls */
const plot = Plot.plot({
- width: plotConfig.width,
+ width: plotConfig.lenskitWidth,
height: 30,
x: {
grid: true,
@@ -229,25 +292,35 @@ Collaborative Filtering Methods
}
-function plotLenskitData(data) {
- let item_ids = new Set(data.map((x) => x.item_id));
+const periodColors = [ "#1f77b4","#ff7f0e"];
-// TODO: on first draw, copy top x-axis and add to svg in container
-// so top axis can be made sticky
- // (or draw empty plot with legend and axis only?)
+const colorByPeriod = {
+ "1921":"#1f77b4",
+ "1924": "#ff7f0e"
+}
+
+function plotLenskitData(data, colors = periodColors) {
+ const currentSort = document.querySelector('input[name="score_sort"]:checked').value;
+ const sortScore = lenskitScores[currentSort];
+
+ // get set of unique item ids and sort by currently configured sort option
+ let item_ids = new Array(... new Set(data.map((x) => x.item_id)));
+ item_ids = new Set(d3.sort(item_ids, (a, b) => {
+ return d3.descending(sortScore.get(a), sortScore.get(b));
+ }));
const plot = Plot.plot({
- width: plotConfig.width,
+ width: plotConfig.lenskitWidth,
// set height to make more space between titles
height: item_ids.size * 28 + 60,
marginTop: 0, // no top margin since axis is plotted separately
- color: {type: 'categorical', scheme: "Category10"},
+ // color: {type: 'categorical', scheme: "Tableau10"},
+ color: {range: colors},
x: {
grid: true,
label: "Score",
labelAnchor: "center",
- // draw both top and bottom unless we have very few items
- // axis: item_ids.size > 5 ? "both" : "top",
+ // axis at bottom only since we draw a second fixed top axis
axis: "bottom",
domain: lenskitdomain,
},
@@ -255,11 +328,6 @@ Collaborative Filtering Methods
domain: item_ids,
},
marks: [
- // Plot.axisX({
- // // label: "Score",
- // // axis: "bottom",
- // // labelAnchor: "center",
- // }),
Plot.axisY({
label: null, // suppress
marginLeft: plotConfig.marginLeft,
@@ -268,14 +336,12 @@ Collaborative Filtering Methods
tickFormat: (d) => item_label[d],
}),
Plot.tickX(data,
- // Plot.normalizeX({x: "score", y: "item_id", stroke: d => d.subscription_start.startsWith("1921") ? "lightslategray" : "orange" }))
Plot.normalizeX({x: "score", y: "item_id", stroke: "period"}))
]
});
const div = document.querySelector("#imfplot");
div.replaceChildren(plot)
- // can't make axis sticky; but could copy it to another element that is
}
const measureLabel = {
@@ -315,7 +381,7 @@ Collaborative Filtering Methods
document.querySelector("#cfplot-axis").replaceChildren(plot)
}
-function plotCFData(data) {
+function plotCFData(data, colors = periodColors) {
let item_ids = new Set(data.map((x) => x.item_id));
// TODO: on first draw, copy top x-axis and add to svg in container
@@ -326,7 +392,8 @@ Collaborative Filtering Methods
width: plotConfig.width,
height: item_ids.size * 28 + 60,
marginTop: 0, // no top margin since axis is plotted separately
- color: {type: 'categorical', scheme: "Tableau10"},
+ // color: {type: 'categorical', scheme: "Tableau10"},
+ color: {range: colors},
// symbol: {legend: true}, // plotted in axis
x: {
grid: true,
@@ -364,11 +431,6 @@ Collaborative Filtering Methods
plotCFAxis(cfdata);
// filtering requires removing and redrawing the plot
-
-
-const searchInput = document.getElementById("search");
-const periodInput = document.querySelectorAll("input[name=subscription]");
-
function redrawPlot() {
let currentLenskit = lenskitdata;
let currentCF = cfdata;
@@ -382,21 +444,34 @@ Collaborative Filtering Methods
// optionally filter by subscription time period
const selectedSubs = document.querySelector('input[name="subscription"]:checked').value;
+ // define custom color scheme to avoid colors changing when data changes
+ let currentColors = periodColors;
if (selectedSubs != "both") {
currentLenskit = currentLenskit.filter((x) => x.period.startsWith(selectedSubs));
currentCF = currentCF.filter((x) => x.period.startsWith(selectedSubs));
+ currentColors = new Array(colorByPeriod[selectedSubs]);
}
- plotLenskitData(currentLenskit);
- plotCFData(currentCF);
+ plotLenskitData(currentLenskit, currentColors);
+ plotCFData(currentCF, currentColors);
}
+// bind handlers to redraw when the options are changed
+const searchInput = document.getElementById("search");
+const periodInput = document.querySelectorAll("input[name=subscription]");
+const sortInput = document.querySelectorAll("input[name=score_sort]");
+
searchInput.addEventListener("input", (event) => {
redrawPlot();
});
periodInput.forEach((elem) => elem.addEventListener("change", (event) => {
redrawPlot();
}));
+sortInput.forEach((elem) => elem.addEventListener("change", (event) => {
+ // this one only really needs to redraw the lenskit plot...
+ redrawPlot();
+}));
+