graphs and sliders

This commit is contained in:
Luna D. 2024-06-03 23:07:10 +02:00
parent 0374d0482b
commit 694bc31e50
No known key found for this signature in database
GPG key ID: 4B1C63448394F688
8 changed files with 299 additions and 44 deletions

View file

@ -38,6 +38,7 @@
@import "elements/media";
@import "elements/mobile";
@import "elements/separator";
@import "elements/slider";
@import "elements/table";
/* Style elements specific to certain pages. */

View file

@ -0,0 +1,29 @@
.slider {
display: flex;
flex: 1 0 auto;
}
.slider__body {
background: var(--primary-color);
min-height: var(--padding-small);
min-width: 100%;
}
.slider__head {
position: absolute;
background: var(--text-color);
width: var(--padding-normal);
height: var(--padding-normal);
border-radius: 100%;
transform: translateY(-2px);
}
.slider__minmax {
display: flex;
flex: 1 0 auto;
}
.slider__minmax .slider:last-child {
right: calc(100% - 2.2rem * 2);
position: relative;
}

View file

@ -1,29 +1,20 @@
.statistics {
display: flex;
flex-direction: column;
border: 1.5px solid var(--secondary-color);
border-radius: var(--border-radius-inner);
overflow: hidden;
flex-flow: column;
gap: var(--padding-small);
}
.statistics__statistic {
display: grid;
grid-template-columns: 15% 15% auto;
background: var(--secondary-color);
}
@mixin even-odd statistics__statistic;
.statistics__column {
text-align: center;
padding: var(--padding-small);
display: flex;
flex-flow: column;
}
.sparkline {
border-bottom: 1px solid var(--primary-border-color);
background: var(--primary-muted-color);
border-radius: var(--border-radius-inner);
display: flex;
height: var(--padding-large);
padding: var(--padding-tiny);
padding: var(--padding-small);
overflow: hidden;
}

View file

@ -6,24 +6,68 @@
import { $, $$ } from './utils/dom';
function setGraphWidth(el: SVGSVGElement, width: number) {
el.viewBox.baseVal.width = Math.max(width, 0);
const graph: SVGPathElement | null = $<SVGPathElement>('#js-graph', el);
if (graph) {
graph.style.transform = `scaleX(${Math.max(width, 0) / 375})`;
}
}
function graphSlice(el: SVGSVGElement, width: number, offset: number) {
setGraphWidth(el, width);
el.viewBox.baseVal.x = Math.max(offset, 0);
}
function resizeGraphs() {
$$<SVGSVGElement>('#js-sparkline-svg').forEach(el => {
$$<SVGSVGElement>('#js-graph-svg').forEach(el => {
const parent: HTMLElement | null = el.parentElement;
if (parent) {
el.viewBox.baseVal.width = parent.clientWidth;
const graph: SVGPathElement | null = $<SVGPathElement>('#js-barline-graph', el);
if (graph) {
graph.style.transform = `scaleX(${parent.clientWidth / 375})`;
}
setGraphWidth(el, parent.clientWidth);
}
});
}
function scaleGraph(target: HTMLElement, min: number, max: number) {
const targetSvg = $<SVGSVGElement>('#js-graph-svg', target);
if (!targetSvg) { return; }
const cw = target.clientWidth;
const diff = 100 - (max - min);
const targetWidth = cw + cw * (diff / 100);
const targetOffset = targetWidth * (min / 100);
targetSvg.style.minWidth = `${targetWidth}px`;
graphSlice(targetSvg, targetWidth, targetOffset);
}
function setupSliders() {
$$<HTMLInputElement>('#js-graph-slider').forEach(el => {
const targetId = el.getAttribute('data-target');
if (!targetId) { return; }
const target = $<HTMLElement>(targetId);
if (!target) { return; }
el.addEventListener('input', () => {
const min = Number(el.getAttribute('valuemin') || '0');
const max = Number(el.getAttribute('valuemax') || '0');
scaleGraph(target, min, max);
});
});
}
function sizeGraphs() {
resizeGraphs();
setupSliders();
window.addEventListener('resize', resizeGraphs);
}

164
assets/js/slider.ts Normal file
View file

@ -0,0 +1,164 @@
/**
* Slider Logic
*
* Provides functionality for <input type="dualrange">
*
* Example usage:
*
* <input type="dualrange" min="0" max="100" valuemin="0" valuemax="100">
*/
import { $$ } from './utils/dom';
function lerp(delta: number, from: number, to: number): number {
if (delta >= 1) { return to; }
else if (delta <= 0) { return from; }
return from + (to - from) * delta;
}
function setupDrag(el: HTMLDivElement, dataEl: HTMLInputElement, valueProperty: string, limitProperty: string) {
const parent = el.parentElement;
if (!parent) {
return;
}
let minPos = 0;
let maxPos = 0;
let curValue = 0;
let dragging = false;
function initVars() {
if (!parent) { return; }
const rect = parent.getBoundingClientRect();
minPos = rect.x;
maxPos = rect.x + rect.width - el.clientWidth;
curValue = Number(dataEl.getAttribute(valueProperty) || '0');
}
function clampValue(value: number): number {
const storedValue = Number(dataEl.getAttribute(valueProperty) || '0');
const limitValue = Number(dataEl.getAttribute(limitProperty) || '0');
if (storedValue >= limitValue && value < limitValue) {
return limitValue;
}
else if (storedValue < limitValue && value >= limitValue) {
return limitValue - 1; // Offset by 1 to ensure stored value is less than limit.
}
return value;
}
function getMin(): number {
return Number(dataEl.getAttribute('min') || '0');
}
function getMax(): number {
return Number(dataEl.getAttribute('max') || '0');
}
// Define functions to control the drag behavior of the slider.
function dragMove(e: PointerEvent) {
if (!dragging) { return; }
e.preventDefault();
let desiredPos = e.clientX;
if (desiredPos > maxPos) {
desiredPos = maxPos;
}
else if (desiredPos < minPos) {
desiredPos = minPos;
}
curValue = clampValue(
lerp(
(desiredPos - minPos) / (maxPos - minPos),
getMin(),
getMax()
)
);
desiredPos = lerp(curValue / getMax(), minPos, maxPos);
el.style.left = `${desiredPos}px`;
dataEl.setAttribute(valueProperty, curValue.toString());
dataEl.dispatchEvent(new InputEvent('input'));
}
function dragEnd(e: PointerEvent) {
if (!dragging) { return; }
e.preventDefault();
dataEl.setAttribute(valueProperty, curValue.toString());
dataEl.dispatchEvent(new InputEvent('input'));
dragging = false;
}
function dragBegin(e: PointerEvent) {
if (!parent) { return; }
e.preventDefault();
initVars();
dragging = true;
}
// Set initial position;
initVars();
el.style.left = `${lerp(curValue / getMax(), minPos, maxPos)}px`;
// Attach event listeners for dragging the head.
el.addEventListener('pointerdown', dragBegin);
window.addEventListener('pointerup', dragEnd);
window.addEventListener('pointermove', dragMove);
}
function setupSlider(el: HTMLInputElement) {
const parent = el.parentElement;
if (!parent) {
return;
}
// Create a bunch of divs for presentation.
const sliderContainer: HTMLDivElement = document.createElement('div');
const minHead: HTMLDivElement = document.createElement('div');
const maxHead: HTMLDivElement = document.createElement('div');
const body: HTMLDivElement = document.createElement('div');
// Hide the real input, and add CSS classes to our divs.
el.classList.add('hidden');
sliderContainer.classList.add('slider');
minHead.classList.add('slider__head');
minHead.classList.add('slider__head--min');
maxHead.classList.add('slider__head');
maxHead.classList.add('slider__head--max');
body.classList.add('slider__body');
// Insert divs into other divs and subsequently into the document.
sliderContainer.appendChild(body);
sliderContainer.appendChild(minHead);
sliderContainer.appendChild(maxHead);
parent.insertBefore(sliderContainer, el);
// Setup drag events on head elements.
setupDrag(minHead, el, 'valuemin', 'valuemax');
setupDrag(maxHead, el, 'valuemax', 'valuemin');
}
function setupSliders() {
$$<HTMLInputElement>('input[type="dualrange"]').forEach(el => {
setupSlider(el);
});
}
export { setupSliders };

View file

@ -36,6 +36,7 @@ import { pollOptionCreator } from './poll';
import { warnAboutPMs } from './pmwarning';
import { imageSourcesCreator } from './sources';
import { sizeGraphs } from './graph';
import { setupSliders } from './slider';
whenReady(() => {
@ -69,6 +70,7 @@ whenReady(() => {
pollOptionCreator();
warnAboutPMs();
imageSourcesCreator();
setupSliders();
sizeGraphs();
});

View file

@ -3,26 +3,50 @@
.block__content
.statistics
.statistics__statistic
.statistics__column Uploads
.statistics__column = number_with_delimiter(@user.uploads_count)
.statistics__column: .sparkline = sparkline_data(@statistics.uploads)
h5
| Uploads (
= number_with_delimiter(@user.uploads_count)
| )
.sparkline#js-upload-graph = sparkline_data(@statistics.uploads)
.slider__minmax
input#js-graph-slider type="dualrange" min="0" max="100" valuemin="0" valuemax="100" data-target="#js-upload-graph"
.statistics__statistic
.statistics__column Favorites
.statistics__column = number_with_delimiter(@user.images_favourited_count)
.statistics__column: .sparkline = sparkline_data(@statistics.images_favourited)
h5
| Favorites (
= number_with_delimiter(@user.images_favourited_count)
| )
.sparkline#js-favorite-graph = sparkline_data(@statistics.images_favourited)
.slider__minmax
input#js-graph-slider type="dualrange" min="0" max="100" valuemin="0" valuemax="100" data-target="#js-favorite-graph"
.statistics__statistic
.statistics__column Comments
.statistics__column = number_with_delimiter(@user.comments_posted_count)
.statistics__column: .sparkline = sparkline_data(@statistics.comments_posted)
h5
| Comments (
= number_with_delimiter(@user.comments_posted_count)
| )
.sparkline#js-comment-graph = sparkline_data(@statistics.comments_posted)
.slider__minmax
input#js-graph-slider type="dualrange" min="0" max="100" valuemin="0" valuemax="100" data-target="#js-comment-graph"
.statistics__statistic
.statistics__column Votes
.statistics__column = number_with_delimiter(@user.votes_cast_count)
.statistics__column: .sparkline = sparkline_data(@statistics.votes_cast)
h5
| Votes (
= number_with_delimiter(@user.votes_cast_count)
| )
.sparkline#js-vote-graph = sparkline_data(@statistics.votes_cast)
.slider__minmax
input#js-graph-slider type="dualrange" min="0" max="100" valuemin="0" valuemax="100" data-target="#js-vote-graph"
.statistics__statistic
.statistics__column Metadata Updates
.statistics__column = number_with_delimiter(@user.metadata_updates_count)
.statistics__column: .sparkline = sparkline_data(@statistics.metadata_updates)
h5
| Metadata Updates (
= number_with_delimiter(@user.metadata_updates_count)
| )
.sparkline#js-metadata-graph = sparkline_data(@statistics.metadata_updates)
.slider__minmax
input#js-graph-slider type="dualrange" min="0" max="100" valuemin="0" valuemax="100" data-target="#js-metadata-graph"
.statistics__statistic
.statistics__column Forum Posts
.statistics__column = number_with_delimiter(@user.forum_posts_count)
.statistics__column: .sparkline = sparkline_data(@statistics.forum_posts)
h5
| Forum Posts (
= number_with_delimiter(@user.forum_posts_count)
| )
.sparkline#js-post-graph = sparkline_data(@statistics.forum_posts)
.slider__minmax
input#js-graph-slider type="dualrange" min="0" max="100" valuemin="0" valuemax="100" data-target="#js-post-graph"

View file

@ -54,7 +54,7 @@ defmodule PhilomenaWeb.ProfileView do
sy = height / 20
factor = 100 / 90
content_tag :svg, id: "js-sparkline-svg", width: "100%", preserveAspectRatio: "xMinYMin", viewBox: "0 0 #{width} #{height}" do
content_tag :svg, id: "js-graph-svg", width: "100%", preserveAspectRatio: "xMinYMin", viewBox: "0 0 #{width} #{height}" do
first = List.first(data)
last = List.last(data)
first_y = sparkline_y(first, max) * sy
@ -80,7 +80,7 @@ defmodule PhilomenaWeb.ProfileView do
end
end
graph = content_tag :path, "", id: "js-barline-graph", class: "barline__bar", d: "M0,#{first_y}#{points}L#{width - sx},#{last_y}L#{width - sx},#{height}L0,#{height}L0,#{first_y}"
graph = content_tag :path, "", id: "js-graph", class: "barline__bar", d: "M0,#{first_y}#{points}L#{width - sx},#{last_y}L#{width - sx},#{height}L0,#{height}L0,#{first_y}"
[graph, circles]
end