mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-27 21:47:59 +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/media";
|
||||||
@import "elements/mobile";
|
@import "elements/mobile";
|
||||||
@import "elements/separator";
|
@import "elements/separator";
|
||||||
|
@import "elements/slider";
|
||||||
@import "elements/table";
|
@import "elements/table";
|
||||||
|
|
||||||
/* Style elements specific to certain pages. */
|
/* 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 {
|
.statistics {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-flow: column;
|
||||||
border: 1.5px solid var(--secondary-color);
|
gap: var(--padding-small);
|
||||||
border-radius: var(--border-radius-inner);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.statistics__statistic {
|
.statistics__statistic {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 15% 15% auto;
|
flex-flow: column;
|
||||||
background: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin even-odd statistics__statistic;
|
|
||||||
|
|
||||||
.statistics__column {
|
|
||||||
text-align: center;
|
|
||||||
padding: var(--padding-small);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sparkline {
|
.sparkline {
|
||||||
border-bottom: 1px solid var(--primary-border-color);
|
background: var(--primary-muted-color);
|
||||||
|
border-radius: var(--border-radius-inner);
|
||||||
display: flex;
|
display: flex;
|
||||||
height: var(--padding-large);
|
height: var(--padding-large);
|
||||||
padding: var(--padding-tiny);
|
padding: var(--padding-small);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,24 +6,68 @@
|
||||||
|
|
||||||
import { $, $$ } from './utils/dom';
|
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() {
|
function resizeGraphs() {
|
||||||
$$<SVGSVGElement>('#js-sparkline-svg').forEach(el => {
|
$$<SVGSVGElement>('#js-graph-svg').forEach(el => {
|
||||||
const parent: HTMLElement | null = el.parentElement;
|
const parent: HTMLElement | null = el.parentElement;
|
||||||
|
|
||||||
if (parent) {
|
if (parent) {
|
||||||
el.viewBox.baseVal.width = parent.clientWidth;
|
setGraphWidth(el, parent.clientWidth);
|
||||||
|
|
||||||
const graph: SVGPathElement | null = $<SVGPathElement>('#js-barline-graph', el);
|
|
||||||
|
|
||||||
if (graph) {
|
|
||||||
graph.style.transform = `scaleX(${parent.clientWidth / 375})`;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
function sizeGraphs() {
|
||||||
resizeGraphs();
|
resizeGraphs();
|
||||||
|
setupSliders();
|
||||||
window.addEventListener('resize', resizeGraphs);
|
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 { warnAboutPMs } from './pmwarning';
|
||||||
import { imageSourcesCreator } from './sources';
|
import { imageSourcesCreator } from './sources';
|
||||||
import { sizeGraphs } from './graph';
|
import { sizeGraphs } from './graph';
|
||||||
|
import { setupSliders } from './slider';
|
||||||
|
|
||||||
whenReady(() => {
|
whenReady(() => {
|
||||||
|
|
||||||
|
@ -69,6 +70,7 @@ whenReady(() => {
|
||||||
pollOptionCreator();
|
pollOptionCreator();
|
||||||
warnAboutPMs();
|
warnAboutPMs();
|
||||||
imageSourcesCreator();
|
imageSourcesCreator();
|
||||||
|
setupSliders();
|
||||||
sizeGraphs();
|
sizeGraphs();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,26 +3,50 @@
|
||||||
.block__content
|
.block__content
|
||||||
.statistics
|
.statistics
|
||||||
.statistics__statistic
|
.statistics__statistic
|
||||||
.statistics__column Uploads
|
h5
|
||||||
.statistics__column = number_with_delimiter(@user.uploads_count)
|
| Uploads (
|
||||||
.statistics__column: .sparkline = sparkline_data(@statistics.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__statistic
|
||||||
.statistics__column Favorites
|
h5
|
||||||
.statistics__column = number_with_delimiter(@user.images_favourited_count)
|
| Favorites (
|
||||||
.statistics__column: .sparkline = sparkline_data(@statistics.images_favourited)
|
= 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__statistic
|
||||||
.statistics__column Comments
|
h5
|
||||||
.statistics__column = number_with_delimiter(@user.comments_posted_count)
|
| Comments (
|
||||||
.statistics__column: .sparkline = sparkline_data(@statistics.comments_posted)
|
= 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__statistic
|
||||||
.statistics__column Votes
|
h5
|
||||||
.statistics__column = number_with_delimiter(@user.votes_cast_count)
|
| Votes (
|
||||||
.statistics__column: .sparkline = sparkline_data(@statistics.votes_cast)
|
= 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__statistic
|
||||||
.statistics__column Metadata Updates
|
h5
|
||||||
.statistics__column = number_with_delimiter(@user.metadata_updates_count)
|
| Metadata Updates (
|
||||||
.statistics__column: .sparkline = sparkline_data(@statistics.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__statistic
|
||||||
.statistics__column Forum Posts
|
h5
|
||||||
.statistics__column = number_with_delimiter(@user.forum_posts_count)
|
| Forum Posts (
|
||||||
.statistics__column: .sparkline = sparkline_data(@statistics.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
|
sy = height / 20
|
||||||
factor = 100 / 90
|
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)
|
first = List.first(data)
|
||||||
last = List.last(data)
|
last = List.last(data)
|
||||||
first_y = sparkline_y(first, max) * sy
|
first_y = sparkline_y(first, max) * sy
|
||||||
|
@ -80,7 +80,7 @@ defmodule PhilomenaWeb.ProfileView do
|
||||||
end
|
end
|
||||||
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]
|
[graph, circles]
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue