mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-27 13:47:58 +01:00
graphs and sliders
This commit is contained in:
parent
0374d0482b
commit
694bc31e50
8 changed files with 299 additions and 44 deletions
|
@ -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. */
|
||||
|
|
29
assets/css/elements/slider.css
Normal file
29
assets/css/elements/slider.css
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
164
assets/js/slider.ts
Normal 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 };
|
|
@ -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();
|
||||
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue