mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 14:32:22 -04:00
Minor changes to graphs (#1566)
* Add thousands comma separator for card counts graph * Fix Answer Buttons graph's tooltip Changes to the "times pressed" heading * Shows the percent of that button out of all the presses * Comma separates total on thousands * Update CONTRIBUTERS * Wider spacing for graph tables * Switch to locale-based stats numbers * Update CONTRIBUTORS Wrong email? * Fix counts graph on narrow devices Graph and table now align in a column when the device's screen is narrow. Columns widths are bounded to not get too wide * Rename toLocaleXXX functions * toLocaleNumber -> localizedNumber * toLocaleString -> localizedDate Also cleans up sketchy "card counts" table formatting * Localize more numbers Uses locale-based rounding for more numbers now * Localize graph axis ticks * Fix future-due graph tooltip * avoid div by zero (dae) Ignoring NaN in localizedNumber() could potentially mask a mistake in the future - better to explicitly handle the invalid case at the source instead.
This commit is contained in:
parent
972c9da12e
commit
2df3698e8c
16 changed files with 113 additions and 47 deletions
|
@ -88,6 +88,7 @@ Ren Tatsumoto <tatsu@autistici.org>
|
||||||
lolilolicon <lolilolicon@gmail.com>
|
lolilolicon <lolilolicon@gmail.com>
|
||||||
Gesa Stupperich <gesa.stupperich@gmail.com>
|
Gesa Stupperich <gesa.stupperich@gmail.com>
|
||||||
git9527 <github.com/git9527>
|
git9527 <github.com/git9527>
|
||||||
|
Vova Selin <vselin12@gmail.com>
|
||||||
|
|
||||||
********************
|
********************
|
||||||
|
|
||||||
|
|
|
@ -4,13 +4,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { pageTheme } from "../sveltelib/theme";
|
import { pageTheme } from "../sveltelib/theme";
|
||||||
|
import { localizedNumber } from "../lib/i18n";
|
||||||
|
|
||||||
export let value: number;
|
export let value: number;
|
||||||
export let min = 1;
|
export let min = 1;
|
||||||
export let max = 9999;
|
export let max = 9999;
|
||||||
|
|
||||||
let stringValue: string;
|
let stringValue: string;
|
||||||
$: stringValue = value.toFixed(2);
|
$: stringValue = localizedNumber(value, 2);
|
||||||
|
|
||||||
function update(this: HTMLInputElement): void {
|
function update(this: HTMLInputElement): void {
|
||||||
value = Math.min(max, Math.max(min, parseFloat(this.value)));
|
value = Math.min(max, Math.max(min, parseFloat(this.value)));
|
||||||
|
|
|
@ -48,15 +48,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</InputBox>
|
</InputBox>
|
||||||
|
|
||||||
<div class="counts-outer">
|
<div class="counts-outer">
|
||||||
|
<div class="svg-container" width={bounds.width} height={bounds.height}>
|
||||||
<svg
|
<svg
|
||||||
bind:this={svg}
|
bind:this={svg}
|
||||||
viewBox={`0 0 ${bounds.width} ${bounds.height}`}
|
viewBox={`0 0 ${bounds.width} ${bounds.height}`}
|
||||||
width={bounds.width}
|
|
||||||
height={bounds.height}
|
|
||||||
style="opacity: {graphData.totalCards ? 1 : 0}"
|
style="opacity: {graphData.totalCards ? 1 : 0}"
|
||||||
>
|
>
|
||||||
<g class="counts" />
|
<g class="counts" />
|
||||||
</svg>
|
</svg>
|
||||||
|
</div>
|
||||||
<div class="counts-table">
|
<div class="counts-table">
|
||||||
<table>
|
<table>
|
||||||
{#each tableData as d, _idx}
|
{#each tableData as d, _idx}
|
||||||
|
@ -93,6 +93,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
.counts-outer {
|
.counts-outer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
margin: 0 4vw;
|
||||||
|
|
||||||
|
.svg-container {
|
||||||
|
width: 225px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.counts-table {
|
.counts-table {
|
||||||
|
@ -100,17 +104,41 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
td {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
table {
|
||||||
border-spacing: 1em 0;
|
border-spacing: 1em 0;
|
||||||
|
padding-left: 4vw;
|
||||||
|
|
||||||
|
td {
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0 min(4vw, 40px);
|
||||||
|
|
||||||
|
&.right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.right {
|
/* On narrow devices, stack graph and table in a column */
|
||||||
text-align: right;
|
@media only screen and (max-width: 600px) {
|
||||||
|
.counts-outer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.svg-container {
|
||||||
|
width: 180px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin-left: 4vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.counts-table table td {
|
||||||
|
padding: 0 min(6vw, 30px);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-link:hover {
|
.search-link:hover {
|
||||||
|
|
|
@ -15,7 +15,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
:global(rect:hover) {
|
:global(rect:hover) {
|
||||||
fill: grey;
|
fill: grey;
|
||||||
opacity: 0.05;
|
opacity: 0.2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -25,6 +25,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.align-end {
|
.align-end {
|
||||||
text-align: end;
|
text-align: end;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
axisLeft,
|
axisLeft,
|
||||||
sum,
|
sum,
|
||||||
} from "d3";
|
} from "d3";
|
||||||
|
import { localizedNumber } from "../lib/i18n";
|
||||||
import { Stats } from "../lib/proto";
|
import { Stats } from "../lib/proto";
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
import { showTooltip, hideTooltip } from "./tooltip";
|
import { showTooltip, hideTooltip } from "./tooltip";
|
||||||
|
@ -130,10 +131,20 @@ export function renderButtons(
|
||||||
groupData.filter((d) => d.buttonNum > 1),
|
groupData.filter((d) => d.buttonNum > 1),
|
||||||
(d) => d.count,
|
(d) => d.count,
|
||||||
);
|
);
|
||||||
const percent = total ? ((correct / total) * 100).toFixed(2) : "0";
|
const percent = total ? localizedNumber((correct / total) * 100) : "0";
|
||||||
return { total, correct, percent };
|
return { total, correct, percent };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const totalPressedStr = (data: Datum): string => {
|
||||||
|
const groupTotal = totalCorrect(data.group).total;
|
||||||
|
const buttonTotal = data.count;
|
||||||
|
const percent = groupTotal
|
||||||
|
? localizedNumber((buttonTotal / groupTotal) * 100)
|
||||||
|
: "0";
|
||||||
|
|
||||||
|
return `${localizedNumber(buttonTotal)} (${percent}%)`;
|
||||||
|
};
|
||||||
|
|
||||||
const yMax = Math.max(...data.map((d) => d.count));
|
const yMax = Math.max(...data.map((d) => d.count));
|
||||||
|
|
||||||
const svg = select(svgElem);
|
const svg = select(svgElem);
|
||||||
|
@ -183,6 +194,7 @@ export function renderButtons(
|
||||||
const colour = scaleSequential(interpolateRdYlGn).domain([1, 4]);
|
const colour = scaleSequential(interpolateRdYlGn).domain([1, 4]);
|
||||||
|
|
||||||
// y scale
|
// y scale
|
||||||
|
const yTickFormat = (n: number): string => localizedNumber(n);
|
||||||
|
|
||||||
const y = scaleLinear()
|
const y = scaleLinear()
|
||||||
.range([bounds.height - bounds.marginBottom, bounds.marginTop])
|
.range([bounds.height - bounds.marginBottom, bounds.marginTop])
|
||||||
|
@ -192,7 +204,8 @@ export function renderButtons(
|
||||||
selection.transition(trans).call(
|
selection.transition(trans).call(
|
||||||
axisLeft(y)
|
axisLeft(y)
|
||||||
.ticks(bounds.height / 50)
|
.ticks(bounds.height / 50)
|
||||||
.tickSizeOuter(0),
|
.tickSizeOuter(0)
|
||||||
|
.tickFormat(yTickFormat as any),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.attr("direction", "ltr");
|
.attr("direction", "ltr");
|
||||||
|
@ -242,7 +255,8 @@ export function renderButtons(
|
||||||
const button = tr.statisticsAnswerButtonsButtonNumber();
|
const button = tr.statisticsAnswerButtonsButtonNumber();
|
||||||
const timesPressed = tr.statisticsAnswerButtonsButtonPressed();
|
const timesPressed = tr.statisticsAnswerButtonsButtonPressed();
|
||||||
const correctStr = tr.statisticsHoursCorrect(totalCorrect(d.group));
|
const correctStr = tr.statisticsHoursCorrect(totalCorrect(d.group));
|
||||||
return `${button}: ${d.buttonNum}<br>${timesPressed}: ${d.count}<br>${correctStr}`;
|
const pressedStr = `${timesPressed}: ${totalPressedStr(d)}`;
|
||||||
|
return `${button}: ${d.buttonNum}<br>${pressedStr}<br>${correctStr}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg.select("g.hover-columns")
|
svg.select("g.hover-columns")
|
||||||
|
|
|
@ -25,7 +25,7 @@ import {
|
||||||
SearchDispatch,
|
SearchDispatch,
|
||||||
} from "./graph-helpers";
|
} from "./graph-helpers";
|
||||||
import { clickableClass } from "./graph-styles";
|
import { clickableClass } from "./graph-styles";
|
||||||
import { weekdayLabel, toLocaleString } from "../lib/i18n";
|
import { weekdayLabel, localizedDate } from "../lib/i18n";
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
|
|
||||||
export interface GraphData {
|
export interface GraphData {
|
||||||
|
@ -151,7 +151,7 @@ export function renderCalendar(
|
||||||
.interpolator((n) => interpolateBlues(cappedRange(n)!));
|
.interpolator((n) => interpolateBlues(cappedRange(n)!));
|
||||||
|
|
||||||
function tooltipText(d: DayDatum): string {
|
function tooltipText(d: DayDatum): string {
|
||||||
const date = toLocaleString(d.date, {
|
const date = localizedDate(d.date, {
|
||||||
weekday: "long",
|
weekday: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
|
|
|
@ -20,13 +20,14 @@ import {
|
||||||
interpolate,
|
interpolate,
|
||||||
cumsum,
|
cumsum,
|
||||||
} from "d3";
|
} from "d3";
|
||||||
|
import { localizedNumber } from "../lib/i18n";
|
||||||
import type { GraphBounds } from "./graph-helpers";
|
import type { GraphBounds } from "./graph-helpers";
|
||||||
|
|
||||||
type Count = [string, number, boolean, string];
|
type Count = [string, number, boolean, string];
|
||||||
export interface GraphData {
|
export interface GraphData {
|
||||||
title: string;
|
title: string;
|
||||||
counts: Count[];
|
counts: Count[];
|
||||||
totalCards: number;
|
totalCards: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const barColours = [
|
const barColours = [
|
||||||
|
@ -125,7 +126,7 @@ export function gatherData(
|
||||||
data: Stats.GraphsResponse,
|
data: Stats.GraphsResponse,
|
||||||
separateInactive: boolean,
|
separateInactive: boolean,
|
||||||
): GraphData {
|
): GraphData {
|
||||||
const totalCards = data.cards.length;
|
const totalCards = localizedNumber(data.cards.length);
|
||||||
const counts = countCards(data.cards, separateInactive);
|
const counts = countCards(data.cards, separateInactive);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -148,7 +149,7 @@ export interface SummedDatum {
|
||||||
|
|
||||||
export interface TableDatum {
|
export interface TableDatum {
|
||||||
label: string;
|
label: string;
|
||||||
count: number;
|
count: string;
|
||||||
query: string;
|
query: string;
|
||||||
percent: string;
|
percent: string;
|
||||||
colour: string;
|
colour: string;
|
||||||
|
@ -210,11 +211,11 @@ export function renderCards(
|
||||||
x.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
|
x.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
|
||||||
|
|
||||||
const tableData = data.flatMap((d: SummedDatum, idx: number) => {
|
const tableData = data.flatMap((d: SummedDatum, idx: number) => {
|
||||||
const percent = ((d.count / xMax) * 100).toFixed(1);
|
const percent = localizedNumber((d.count / xMax) * 100, 2);
|
||||||
return d.show
|
return d.show
|
||||||
? ({
|
? ({
|
||||||
label: d.label,
|
label: d.label,
|
||||||
count: d.count,
|
count: localizedNumber(d.count),
|
||||||
percent: `${percent}%`,
|
percent: `${percent}%`,
|
||||||
colour: barColours[idx],
|
colour: barColours[idx],
|
||||||
query: d.query,
|
query: d.query,
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
scaleSequential,
|
scaleSequential,
|
||||||
interpolateRdYlGn,
|
interpolateRdYlGn,
|
||||||
} from "d3";
|
} from "d3";
|
||||||
|
import { localizedNumber } from "../lib/i18n";
|
||||||
import type { Bin, ScaleLinear } from "d3";
|
import type { Bin, ScaleLinear } from "d3";
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
import { CardType } from "../lib/cards";
|
import { CardType } from "../lib/cards";
|
||||||
|
@ -107,7 +108,7 @@ export function prepareData(
|
||||||
dispatch("search", { query });
|
dispatch("search", { query });
|
||||||
}
|
}
|
||||||
|
|
||||||
const xTickFormat = (num: number): string => `${num.toFixed(0)}%`;
|
const xTickFormat = (num: number): string => localizedNumber(num, 0) + "%";
|
||||||
const tableData = [
|
const tableData = [
|
||||||
{
|
{
|
||||||
label: tr.statisticsAverageEase(),
|
label: tr.statisticsAverageEase(),
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
interpolateGreens,
|
interpolateGreens,
|
||||||
} from "d3";
|
} from "d3";
|
||||||
import type { Bin } from "d3";
|
import type { Bin } from "d3";
|
||||||
|
import { localizedNumber } from "../lib/i18n";
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
import { CardQueue } from "../lib/cards";
|
import { CardQueue } from "../lib/cards";
|
||||||
import type { HistogramData } from "./histogram-graph";
|
import type { HistogramData } from "./histogram-graph";
|
||||||
|
@ -140,6 +141,7 @@ export function buildHistogram(
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const xTickFormat = (n: number): string => localizedNumber(n);
|
||||||
const adjustedRange = scaleLinear().range([0.7, 0.3]);
|
const adjustedRange = scaleLinear().range([0.7, 0.3]);
|
||||||
const colourScale = scaleSequential((n) =>
|
const colourScale = scaleSequential((n) =>
|
||||||
interpolateGreens(adjustedRange(n)!),
|
interpolateGreens(adjustedRange(n)!),
|
||||||
|
@ -158,7 +160,7 @@ export function buildHistogram(
|
||||||
});
|
});
|
||||||
const totalLabel = tr.statisticsRunningTotal();
|
const totalLabel = tr.statisticsRunningTotal();
|
||||||
|
|
||||||
return `${days}:<br>${cards}<br>${totalLabel}: ${cumulative}`;
|
return `${days}:<br>${cards}<br>${totalLabel}: ${localizedNumber(cumulative)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClick(bin: Bin<number, number>): void {
|
function onClick(bin: Bin<number, number>): void {
|
||||||
|
@ -198,6 +200,7 @@ export function buildHistogram(
|
||||||
showArea: true,
|
showArea: true,
|
||||||
colourScale,
|
colourScale,
|
||||||
binValue,
|
binValue,
|
||||||
|
xTickFormat,
|
||||||
},
|
},
|
||||||
tableData,
|
tableData,
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,7 +17,7 @@ import {
|
||||||
area,
|
area,
|
||||||
curveBasis,
|
curveBasis,
|
||||||
} from "d3";
|
} from "d3";
|
||||||
|
import { localizedNumber } from "../lib/i18n";
|
||||||
import type { ScaleLinear, ScaleSequential, Bin } from "d3";
|
import type { ScaleLinear, ScaleSequential, Bin } from "d3";
|
||||||
import { showTooltip, hideTooltip } from "./tooltip";
|
import { showTooltip, hideTooltip } from "./tooltip";
|
||||||
import { GraphBounds, setDataAvailable } from "./graph-helpers";
|
import { GraphBounds, setDataAvailable } from "./graph-helpers";
|
||||||
|
@ -46,6 +46,7 @@ export function histogramGraph(
|
||||||
): void {
|
): void {
|
||||||
const svg = select(svgElem);
|
const svg = select(svgElem);
|
||||||
const trans = svg.transition().duration(600) as any;
|
const trans = svg.transition().duration(600) as any;
|
||||||
|
const axisTickFormat = (n: number): string => localizedNumber(n);
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
setDataAvailable(svg, false);
|
setDataAvailable(svg, false);
|
||||||
|
@ -63,7 +64,7 @@ export function histogramGraph(
|
||||||
axisBottom(x)
|
axisBottom(x)
|
||||||
.ticks(7)
|
.ticks(7)
|
||||||
.tickSizeOuter(0)
|
.tickSizeOuter(0)
|
||||||
.tickFormat((data.xTickFormat ?? null) as any),
|
.tickFormat((data.xTickFormat ?? axisTickFormat) as any),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.attr("direction", "ltr");
|
.attr("direction", "ltr");
|
||||||
|
@ -80,7 +81,8 @@ export function histogramGraph(
|
||||||
selection.transition(trans).call(
|
selection.transition(trans).call(
|
||||||
axisLeft(y)
|
axisLeft(y)
|
||||||
.ticks(bounds.height / 50)
|
.ticks(bounds.height / 50)
|
||||||
.tickSizeOuter(0),
|
.tickSizeOuter(0)
|
||||||
|
.tickFormat(axisTickFormat as any),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.attr("direction", "ltr");
|
.attr("direction", "ltr");
|
||||||
|
@ -134,7 +136,8 @@ export function histogramGraph(
|
||||||
selection.transition(trans).call(
|
selection.transition(trans).call(
|
||||||
axisRight(yAreaScale)
|
axisRight(yAreaScale)
|
||||||
.ticks(bounds.height / 50)
|
.ticks(bounds.height / 50)
|
||||||
.tickSizeOuter(0),
|
.tickSizeOuter(0)
|
||||||
|
.tickFormat(axisTickFormat as any),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.attr("direction", "ltr");
|
.attr("direction", "ltr");
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {
|
||||||
area,
|
area,
|
||||||
curveBasis,
|
curveBasis,
|
||||||
} from "d3";
|
} from "d3";
|
||||||
|
import { localizedNumber } from "../lib/i18n";
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
import { showTooltip, hideTooltip } from "./tooltip";
|
import { showTooltip, hideTooltip } from "./tooltip";
|
||||||
import {
|
import {
|
||||||
|
@ -110,6 +110,7 @@ export function renderHours(
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// y scale
|
// y scale
|
||||||
|
const yTickFormat = (n: number): string => localizedNumber(n);
|
||||||
|
|
||||||
const y = scaleLinear()
|
const y = scaleLinear()
|
||||||
.range([bounds.height - bounds.marginBottom, bounds.marginTop])
|
.range([bounds.height - bounds.marginBottom, bounds.marginTop])
|
||||||
|
@ -120,7 +121,8 @@ export function renderHours(
|
||||||
selection.transition(trans).call(
|
selection.transition(trans).call(
|
||||||
axisLeft(y)
|
axisLeft(y)
|
||||||
.ticks(bounds.height / 50)
|
.ticks(bounds.height / 50)
|
||||||
.tickSizeOuter(0),
|
.tickSizeOuter(0)
|
||||||
|
.tickFormat(yTickFormat as any),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.attr("direction", "ltr");
|
.attr("direction", "ltr");
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
scaleSequential,
|
scaleSequential,
|
||||||
interpolateBlues,
|
interpolateBlues,
|
||||||
} from "d3";
|
} from "d3";
|
||||||
|
import { localizedNumber } from "../lib/i18n";
|
||||||
import type { Bin } from "d3";
|
import type { Bin } from "d3";
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
import { timeSpan } from "../lib/time";
|
import { timeSpan } from "../lib/time";
|
||||||
|
@ -147,7 +148,7 @@ export function prepareIntervalData(
|
||||||
// const day = dayLabel(bin.x0!, bin.x1!);
|
// const day = dayLabel(bin.x0!, bin.x1!);
|
||||||
const interval = intervalLabel(bin.x0!, bin.x1!, bin.length);
|
const interval = intervalLabel(bin.x0!, bin.x1!, bin.length);
|
||||||
const total = tr.statisticsRunningTotal();
|
const total = tr.statisticsRunningTotal();
|
||||||
return `${interval}<br>${total}: \u200e${percent.toFixed(1)}%`;
|
return `${interval}<br>${total}: \u200e${localizedNumber(percent, 1)}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClick(bin: Bin<number, number>): void {
|
function onClick(bin: Bin<number, number>): void {
|
||||||
|
|
|
@ -30,7 +30,7 @@ import {
|
||||||
ScaleSequential,
|
ScaleSequential,
|
||||||
} from "d3";
|
} from "d3";
|
||||||
import type { Bin } from "d3";
|
import type { Bin } from "d3";
|
||||||
|
import { localizedNumber } from "../lib/i18n";
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
import type { TableDatum } from "./graph-helpers";
|
import type { TableDatum } from "./graph-helpers";
|
||||||
import { GraphBounds, setDataAvailable, GraphRange } from "./graph-helpers";
|
import { GraphBounds, setDataAvailable, GraphRange } from "./graph-helpers";
|
||||||
|
@ -190,7 +190,7 @@ export function renderReviews(
|
||||||
if (Math.round(n) != n) {
|
if (Math.round(n) != n) {
|
||||||
return "";
|
return "";
|
||||||
} else {
|
} else {
|
||||||
return n.toLocaleString();
|
return localizedNumber(n);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import { Stats } from "../lib/proto";
|
import { Stats } from "../lib/proto";
|
||||||
import { studiedToday } from "../lib/time";
|
import { studiedToday } from "../lib/time";
|
||||||
|
import { localizedNumber } from "../lib/i18n";
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
|
|
||||||
export interface TodayData {
|
export interface TodayData {
|
||||||
|
@ -73,8 +74,8 @@ export function gatherData(data: Stats.GraphsResponse): TodayData {
|
||||||
const studiedTodayText = studiedToday(answerCount, answerMillis / 1000);
|
const studiedTodayText = studiedToday(answerCount, answerMillis / 1000);
|
||||||
const againCount = answerCount - correctCount;
|
const againCount = answerCount - correctCount;
|
||||||
let againCountText = tr.statisticsTodayAgainCount();
|
let againCountText = tr.statisticsTodayAgainCount();
|
||||||
againCountText += ` ${againCount} (${((againCount / answerCount) * 100).toFixed(
|
againCountText += ` ${againCount} (${localizedNumber(
|
||||||
2,
|
(againCount / answerCount) * 100,
|
||||||
)}%)`;
|
)}%)`;
|
||||||
const typeCounts = tr.statisticsTodayTypeCounts({
|
const typeCounts = tr.statisticsTodayTypeCounts({
|
||||||
learnCount,
|
learnCount,
|
||||||
|
|
|
@ -41,13 +41,19 @@ export function weekdayLabel(n: number): string {
|
||||||
|
|
||||||
let langs: string[] = [];
|
let langs: string[] = [];
|
||||||
|
|
||||||
export function toLocaleString(
|
export function localizedDate(
|
||||||
date: Date,
|
date: Date,
|
||||||
options?: Intl.DateTimeFormatOptions,
|
options?: Intl.DateTimeFormatOptions,
|
||||||
): string {
|
): string {
|
||||||
return date.toLocaleDateString(langs, options);
|
return date.toLocaleDateString(langs, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function localizedNumber(n: number, precision = 2): string {
|
||||||
|
const round = Math.pow(10, precision);
|
||||||
|
const rounded = Math.round(n * round) / round;
|
||||||
|
return rounded.toLocaleString(langs);
|
||||||
|
}
|
||||||
|
|
||||||
export function localeCompare(
|
export function localeCompare(
|
||||||
first: string,
|
first: string,
|
||||||
second: string,
|
second: string,
|
||||||
|
|
Loading…
Reference in a new issue