Skip to content

Commit

Permalink
Merge pull request #23 from rlskoeser/fig14-higherqual
Browse files Browse the repository at this point in the history
Standardize sorting for figure 14 and export higher quality versions
  • Loading branch information
rlskoeser authored May 23, 2024
2 parents ca55860 + b5878e3 commit 1b0d90e
Show file tree
Hide file tree
Showing 5 changed files with 2,988 additions and 2,201 deletions.
2 changes: 1 addition & 1 deletion figures/fig14-prediction-scores.html

Large diffs are not rendered by default.

Binary file modified figures/fig14-prediction-scores.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions figures/fig14-prediction-scores.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
159 changes: 117 additions & 42 deletions figures/interactive/top_recommendations.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
overflow: hidden;
display: grid;
font-family: system-ui, sans-serif;
background-color: white;
}
h1, h2 { text-align: center; }
h2 {
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -137,30 +172,51 @@
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;
}

</style>
</head>
<body>
<div class="controls">
<h1>Top Recommendations</h1>
<h1 class="visually-hidden">Top Recommendations</h1>
<form>
<fieldset>
Subscription period:
<label>
1921/1922<span class="swatch period1921"/>
<input value="1921" name="subscription" type="radio" />
</label>
<input value="both" name="subscription" type="radio" checked="checked" aria-label="both time periods"/>
<label>
<input value="1924" name="subscription" type="radio" />
<span class="swatch period1924">1924/1925</span>
</label>
<label>1921/1922<span class="swatch period1921"/><input value="1921" name="subscription" type="radio" /></label>
<label><span class="swatch periodboth"/><input value="both" name="subscription" type="radio" checked="checked" aria-label="both time periods"/></label>
<label><input value="1924" name="subscription" type="radio" /><span class="swatch period1924">1924/1925</span></label>
<input id="search" type="text" placeholder="Filter by author or title">
<!-- <input id="reset" type="reset" value="reset"> -->
</fieldset>
</form>
</div>
<div class="container" id="imf">
<h2>Implicit Matrix Factorization Model</h2>
<form>
<fieldset>
Sort by score:
<label>
<input value="median" name="score_sort" type="radio" checked="checked"/>
median
</label>
<label>
<input value="high" name="score_sort" type="radio" />
highest
</label>
<label>
<input value="low" name="score_sort" type="radio" />
lowest
</label>
</fieldset>
</form>
<div id="imfplot-axis" class="plot-axis"></div>
<div id="imfplot" class="plot"></div>
</div>
Expand All @@ -174,7 +230,8 @@ <h2>Collaborative Filtering Methods</h2>

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)];
Expand All @@ -183,26 +240,32 @@ <h2>Collaborative Filtering Methods</h2>
// 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,
Expand All @@ -229,37 +292,42 @@ <h2>Collaborative Filtering Methods</h2>
}


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,
},
y: {
domain: item_ids,
},
marks: [
// Plot.axisX({
// // label: "Score",
// // axis: "bottom",
// // labelAnchor: "center",
// }),
Plot.axisY({
label: null, // suppress
marginLeft: plotConfig.marginLeft,
Expand All @@ -268,14 +336,12 @@ <h2>Collaborative Filtering Methods</h2>
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 = {
Expand Down Expand Up @@ -315,7 +381,7 @@ <h2>Collaborative Filtering Methods</h2>
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
Expand All @@ -326,7 +392,8 @@ <h2>Collaborative Filtering Methods</h2>
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,
Expand Down Expand Up @@ -364,11 +431,6 @@ <h2>Collaborative Filtering Methods</h2>
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;
Expand All @@ -382,21 +444,34 @@ <h2>Collaborative Filtering Methods</h2>

// 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();
}));


</script>
</body>
Expand Down
Loading

0 comments on commit 1b0d90e

Please sign in to comment.