mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
add 'no data' overlay when graph empty
This commit is contained in:
parent
19541c4a9d
commit
0d287330c3
24 changed files with 244 additions and 150 deletions
|
@ -157,3 +157,5 @@ statistics-hours-range = From { $hourStart }:00~{ $hourEnd }:00
|
|||
statistics-hours-correct = { $correct }/{ $total } correct ({ $percent }%)
|
||||
statistics-hours-title = Hourly Breakdown
|
||||
statistics-hours-subtitle = Review success rate for each hour of the day.
|
||||
# shown when graph is empty
|
||||
statistics-no-data = NO DATA
|
||||
|
|
|
@ -44,31 +44,29 @@
|
|||
const subtitle = i18n.tr(i18n.TR.STATISTICS_ADDED_SUBTITLE);
|
||||
</script>
|
||||
|
||||
{#if histogramData}
|
||||
<div class="graph">
|
||||
<h1>{title}</h1>
|
||||
<div class="graph">
|
||||
<h1>{title}</h1>
|
||||
|
||||
<div class="range-box-inner">
|
||||
<label>
|
||||
<input type="radio" bind:group={range} value={AddedRange.Month} />
|
||||
{month}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" bind:group={range} value={AddedRange.Quarter} />
|
||||
{month3}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" bind:group={range} value={AddedRange.Year} />
|
||||
{year}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" bind:group={range} value={AddedRange.AllTime} />
|
||||
{all}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="subtitle">{subtitle}</div>
|
||||
|
||||
<HistogramGraph data={histogramData} />
|
||||
<div class="range-box-inner">
|
||||
<label>
|
||||
<input type="radio" bind:group={range} value={AddedRange.Month} />
|
||||
{month}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" bind:group={range} value={AddedRange.Quarter} />
|
||||
{month3}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" bind:group={range} value={AddedRange.Year} />
|
||||
{year}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" bind:group={range} value={AddedRange.AllTime} />
|
||||
{all}
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="subtitle">{subtitle}</div>
|
||||
|
||||
<HistogramGraph data={histogramData} {i18n} />
|
||||
</div>
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { gatherData, GraphData, renderButtons } from "./buttons";
|
||||
import pb from "../backend/proto";
|
||||
import { I18n } from "../i18n";
|
||||
import NoDataOverlay from "./NoDataOverlay.svelte";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
export let i18n: I18n;
|
||||
|
@ -29,5 +30,6 @@
|
|||
<g class="bars" />
|
||||
<g class="hoverzone" />
|
||||
<AxisTicks {bounds} />
|
||||
<NoDataOverlay {bounds} {i18n} />
|
||||
</svg>
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
<script lang="typescript">
|
||||
import ReviewsGraph from "./ReviewsGraph.svelte";
|
||||
|
||||
import NoDataOverlay from "./NoDataOverlay.svelte";
|
||||
import AxisTicks from "./AxisTicks.svelte";
|
||||
import { defaultGraphBounds, RevlogRange } from "./graphs";
|
||||
import { GraphData, gatherData, renderCalendar, ReviewRange } from "./calendar";
|
||||
import { GraphData, gatherData, renderCalendar } from "./calendar";
|
||||
import pb from "../backend/proto";
|
||||
import { timeSpan, MONTH, YEAR } from "../time";
|
||||
import { I18n } from "../i18n";
|
||||
|
@ -76,6 +75,7 @@
|
|||
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
||||
<g class="days" />
|
||||
<AxisTicks {bounds} />
|
||||
<NoDataOverlay {bounds} {i18n} />
|
||||
</svg>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import pb from "../backend/proto";
|
||||
import { I18n } from "../i18n";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
export let sourceData: pb.BackendProto.GraphsOut;
|
||||
export let i18n: I18n;
|
||||
|
||||
let svg = null as HTMLElement | SVGElement | null;
|
||||
|
@ -15,27 +15,31 @@
|
|||
bounds.marginRight = 20;
|
||||
bounds.marginTop = 0;
|
||||
|
||||
let graphData: GraphData | null = null;
|
||||
$: if (sourceData) {
|
||||
let graphData: GraphData;
|
||||
$: {
|
||||
graphData = gatherData(sourceData, i18n);
|
||||
}
|
||||
|
||||
$: if (graphData) {
|
||||
renderCards(svg as any, bounds, graphData);
|
||||
}
|
||||
|
||||
const total = i18n.tr(i18n.TR.STATISTICS_COUNTS_TOTAL_CARDS);
|
||||
</script>
|
||||
|
||||
{#if graphData}
|
||||
<div class="graph">
|
||||
<h1>{graphData.title}</h1>
|
||||
<style>
|
||||
svg {
|
||||
transition: opacity 1s;
|
||||
}
|
||||
</style>
|
||||
|
||||
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
||||
<g class="days" />
|
||||
</svg>
|
||||
<div class="graph">
|
||||
<h1>{graphData.title}</h1>
|
||||
|
||||
<div class="centered">{total}: {graphData.totalCards}</div>
|
||||
<svg
|
||||
bind:this={svg}
|
||||
viewBox={`0 0 ${bounds.width} ${bounds.height}`}
|
||||
style="opacity: {graphData.totalCards ? 1 : 0}">
|
||||
<g class="days" />
|
||||
</svg>
|
||||
|
||||
</div>
|
||||
{/if}
|
||||
<div class="centered">{total}: {graphData.totalCards}</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -19,12 +19,10 @@
|
|||
const subtitle = i18n.tr(i18n.TR.STATISTICS_CARD_EASE_SUBTITLE);
|
||||
</script>
|
||||
|
||||
{#if histogramData}
|
||||
<div class="graph">
|
||||
<h1>{title}</h1>
|
||||
<div class="graph">
|
||||
<h1>{title}</h1>
|
||||
|
||||
<div class="subtitle">{subtitle}</div>
|
||||
<div class="subtitle">{subtitle}</div>
|
||||
|
||||
<HistogramGraph data={histogramData} />
|
||||
</div>
|
||||
{/if}
|
||||
<HistogramGraph data={histogramData} {i18n} />
|
||||
</div>
|
||||
|
|
|
@ -52,38 +52,35 @@
|
|||
const backlogLabel = i18n.tr(i18n.TR.STATISTICS_BACKLOG_CHECKBOX);
|
||||
</script>
|
||||
|
||||
{#if histogramData}
|
||||
<div class="graph">
|
||||
<h1>{title}</h1>
|
||||
|
||||
<div class="graph">
|
||||
<h1>{title}</h1>
|
||||
|
||||
<div class="range-box-inner">
|
||||
<label>
|
||||
<input type="checkbox" bind:checked={backlog} />
|
||||
{backlogLabel}
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="radio" bind:group={range} value={FutureDueRange.Month} />
|
||||
{month}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" bind:group={range} value={FutureDueRange.Quarter} />
|
||||
{month3}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" bind:group={range} value={FutureDueRange.Year} />
|
||||
{year}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" bind:group={range} value={FutureDueRange.AllTime} />
|
||||
{all}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="subtitle">{subtitle}</div>
|
||||
|
||||
<HistogramGraph data={histogramData} />
|
||||
<div class="range-box-inner">
|
||||
<label>
|
||||
<input type="checkbox" bind:checked={backlog} />
|
||||
{backlogLabel}
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="radio" bind:group={range} value={FutureDueRange.Month} />
|
||||
{month}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" bind:group={range} value={FutureDueRange.Quarter} />
|
||||
{month3}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" bind:group={range} value={FutureDueRange.Year} />
|
||||
{year}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" bind:group={range} value={FutureDueRange.AllTime} />
|
||||
{all}
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="subtitle">{subtitle}</div>
|
||||
|
||||
<HistogramGraph data={histogramData} {i18n} />
|
||||
|
||||
</div>
|
||||
|
|
|
@ -141,13 +141,15 @@
|
|||
</div>
|
||||
<div class="range-box-pad" />
|
||||
|
||||
<TodayStats {sourceData} {i18n} />
|
||||
<CardCounts {sourceData} {i18n} />
|
||||
<CalendarGraph {sourceData} {revlogRange} {i18n} {nightMode} />
|
||||
<FutureDue {sourceData} {revlogRange} {i18n} />
|
||||
<ReviewsGraph {sourceData} {revlogRange} {i18n} />
|
||||
<IntervalsGraph {sourceData} {i18n} />
|
||||
<EaseGraph {sourceData} {i18n} />
|
||||
<HourGraph {sourceData} {i18n} />
|
||||
<ButtonsGraph {sourceData} {i18n} />
|
||||
<AddedGraph {sourceData} {revlogRange} {i18n} />
|
||||
{#if sourceData}
|
||||
<TodayStats {sourceData} {i18n} />
|
||||
<CardCounts {sourceData} {i18n} />
|
||||
<CalendarGraph {sourceData} {revlogRange} {i18n} {nightMode} />
|
||||
<FutureDue {sourceData} {revlogRange} {i18n} />
|
||||
<ReviewsGraph {sourceData} {revlogRange} {i18n} />
|
||||
<IntervalsGraph {sourceData} {i18n} />
|
||||
<EaseGraph {sourceData} {i18n} />
|
||||
<HourGraph {sourceData} {i18n} />
|
||||
<ButtonsGraph {sourceData} {i18n} />
|
||||
<AddedGraph {sourceData} {revlogRange} {i18n} />
|
||||
{/if}
|
||||
|
|
|
@ -2,15 +2,16 @@
|
|||
import { HistogramData, histogramGraph } from "./histogram-graph";
|
||||
import AxisTicks from "./AxisTicks.svelte";
|
||||
import { defaultGraphBounds } from "./graphs";
|
||||
import NoDataOverlay from "./NoDataOverlay.svelte";
|
||||
import { I18n } from "../i18n";
|
||||
|
||||
export let data: HistogramData | null = null;
|
||||
export let i18n: I18n;
|
||||
|
||||
let bounds = defaultGraphBounds();
|
||||
let svg = null as HTMLElement | SVGElement | null;
|
||||
|
||||
$: if (data) {
|
||||
histogramGraph(svg as SVGElement, bounds, data);
|
||||
}
|
||||
$: histogramGraph(svg as SVGElement, bounds, data);
|
||||
</script>
|
||||
|
||||
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
||||
|
@ -18,4 +19,5 @@
|
|||
<g class="hoverzone" />
|
||||
<path class="area" />
|
||||
<AxisTicks {bounds} />
|
||||
<NoDataOverlay {bounds} {i18n} />
|
||||
</svg>
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { gatherData, GraphData, renderHours } from "./hours";
|
||||
import pb from "../backend/proto";
|
||||
import { I18n } from "../i18n";
|
||||
import NoDataOverlay from "./NoDataOverlay.svelte";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
export let i18n: I18n;
|
||||
|
@ -30,5 +31,6 @@
|
|||
<path class="area" />
|
||||
<g class="hoverzone" />
|
||||
<AxisTicks {bounds} />
|
||||
<NoDataOverlay {bounds} {i18n} />
|
||||
</svg>
|
||||
</div>
|
||||
|
|
|
@ -34,44 +34,36 @@
|
|||
const subtitle = i18n.tr(i18n.TR.STATISTICS_INTERVALS_SUBTITLE);
|
||||
</script>
|
||||
|
||||
{#if histogramData}
|
||||
<div class="graph intervals">
|
||||
<h1>{title}</h1>
|
||||
<div class="graph intervals">
|
||||
<h1>{title}</h1>
|
||||
|
||||
<div class="range-box-inner">
|
||||
<label>
|
||||
<input type="radio" bind:group={range} value={IntervalRange.Month} />
|
||||
{month}
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={range}
|
||||
value={IntervalRange.Percentile50} />
|
||||
50%
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={range}
|
||||
value={IntervalRange.Percentile95} />
|
||||
95%
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={range}
|
||||
value={IntervalRange.Percentile999} />
|
||||
99.9%
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" bind:group={range} value={IntervalRange.All} />
|
||||
{all}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="subtitle">{subtitle}</div>
|
||||
|
||||
<HistogramGraph data={histogramData} />
|
||||
<div class="range-box-inner">
|
||||
<label>
|
||||
<input type="radio" bind:group={range} value={IntervalRange.Month} />
|
||||
{month}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" bind:group={range} value={IntervalRange.Percentile50} />
|
||||
50%
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" bind:group={range} value={IntervalRange.Percentile95} />
|
||||
95%
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={range}
|
||||
value={IntervalRange.Percentile999} />
|
||||
99.9%
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" bind:group={range} value={IntervalRange.All} />
|
||||
{all}
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="subtitle">{subtitle}</div>
|
||||
|
||||
<HistogramGraph data={histogramData} {i18n} />
|
||||
</div>
|
||||
|
|
14
ts/src/stats/NoDataOverlay.svelte
Normal file
14
ts/src/stats/NoDataOverlay.svelte
Normal file
|
@ -0,0 +1,14 @@
|
|||
<script lang="typescript">
|
||||
import { I18n } from "../i18n";
|
||||
import { GraphBounds } from "./graphs";
|
||||
export let bounds: GraphBounds;
|
||||
export let i18n: I18n;
|
||||
const noData = i18n.tr(i18n.TR.STATISTICS_NO_DATA);
|
||||
</script>
|
||||
|
||||
<g class="no-data">
|
||||
<rect x="0" y="0" width={bounds.width} height={bounds.height} />
|
||||
<text x="{bounds.width / 2}," y={bounds.height / 2} letter-spacing="3">
|
||||
{noData}
|
||||
</text>
|
||||
</g>
|
|
@ -6,6 +6,7 @@
|
|||
import pb from "../backend/proto";
|
||||
import { timeSpan, MONTH, YEAR } from "../time";
|
||||
import { I18n } from "../i18n";
|
||||
import NoDataOverlay from "./NoDataOverlay.svelte";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
export let revlogRange: RevlogRange = RevlogRange.Month;
|
||||
|
@ -93,6 +94,7 @@
|
|||
<path class="area" />
|
||||
<g class="hoverzone" />
|
||||
<AxisTicks {bounds} />
|
||||
<NoDataOverlay {bounds} {i18n} />
|
||||
</svg>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import pb from "../backend/proto";
|
||||
import { extent, histogram } from "d3-array";
|
||||
import { extent, histogram, sum } from "d3-array";
|
||||
import { scaleLinear, scaleSequential } from "d3-scale";
|
||||
import { HistogramData } from "./histogram-graph";
|
||||
import { interpolateBlues } from "d3-scale-chromatic";
|
||||
|
@ -69,6 +69,11 @@ export function buildHistogram(
|
|||
.domain(scale.domain() as any)
|
||||
.thresholds(scale.ticks(desiredBars))(data.daysAdded);
|
||||
|
||||
// empty graph?
|
||||
if (!sum(bins, (bin) => bin.length)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const colourScale = scaleSequential(interpolateBlues).domain([xMin!, xMax]);
|
||||
|
||||
function hoverText(
|
||||
|
|
|
@ -13,7 +13,7 @@ import { select, mouse } from "d3-selection";
|
|||
import { scaleLinear, scaleBand, scaleSequential } from "d3-scale";
|
||||
import { axisBottom, axisLeft } from "d3-axis";
|
||||
import { showTooltip, hideTooltip } from "./tooltip";
|
||||
import { GraphBounds } from "./graphs";
|
||||
import { GraphBounds, setDataAvailable } from "./graphs";
|
||||
import { I18n } from "../i18n";
|
||||
|
||||
type ButtonCounts = [number, number, number, number];
|
||||
|
@ -103,6 +103,13 @@ export function renderButtons(
|
|||
const svg = select(svgElem);
|
||||
const trans = svg.transition().duration(600) as any;
|
||||
|
||||
if (!yMax) {
|
||||
setDataAvailable(svg, false);
|
||||
return;
|
||||
} else {
|
||||
setDataAvailable(svg, true);
|
||||
}
|
||||
|
||||
const xGroup = scaleBand()
|
||||
.domain(["learning", "young", "mature"])
|
||||
.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
|
||||
|
|
|
@ -12,7 +12,7 @@ import "d3-transition";
|
|||
import { select, mouse } from "d3-selection";
|
||||
import { scaleLinear, scaleSequential } from "d3-scale";
|
||||
import { showTooltip, hideTooltip } from "./tooltip";
|
||||
import { GraphBounds } from "./graphs";
|
||||
import { GraphBounds, setDataAvailable } from "./graphs";
|
||||
import { timeDay, timeYear, timeWeek } from "d3-time";
|
||||
import { I18n } from "../i18n";
|
||||
|
||||
|
@ -80,6 +80,14 @@ export function renderCalendar(
|
|||
maxCount = count;
|
||||
}
|
||||
}
|
||||
|
||||
if (!maxCount) {
|
||||
setDataAvailable(svg, false);
|
||||
return;
|
||||
} else {
|
||||
setDataAvailable(svg, true);
|
||||
}
|
||||
|
||||
// fill in any blanks
|
||||
const startDate = timeYear(nowForYear);
|
||||
for (let i = 0; i < 366; i++) {
|
||||
|
|
|
@ -94,12 +94,14 @@ export function renderCards(
|
|||
total: n,
|
||||
};
|
||||
});
|
||||
const xMax = summed.slice(-1)[0];
|
||||
// ensuring a non-zero range makes a better animation
|
||||
// in the empty data case
|
||||
const xMax = Math.max(1, summed.slice(-1)[0]);
|
||||
const x = scaleLinear().domain([0, xMax]);
|
||||
const svg = select(svgElem);
|
||||
const trans = svg.transition().duration(600) as any;
|
||||
|
||||
x.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
|
||||
x.range([bounds.marginLeft, bounds.width - bounds.marginRight - bounds.marginLeft]);
|
||||
|
||||
const tooltipText = (d: any): string => {
|
||||
const pct = ((d.count[1] / xMax) * 100).toFixed(2);
|
||||
|
@ -113,7 +115,7 @@ export function renderCards(
|
|||
showTooltip(tooltipText(d), x, y);
|
||||
})
|
||||
.transition(trans)
|
||||
.attr("width", (d) => x(d.total) - bounds.marginLeft);
|
||||
.attr("width", (d) => x(d.total));
|
||||
};
|
||||
|
||||
data.reverse();
|
||||
|
|
|
@ -88,6 +88,11 @@ export function buildHistogram(
|
|||
.domain(x.domain() as any)
|
||||
.thresholds(x.ticks(desiredBars))(data.entries() as any);
|
||||
|
||||
// empty graph?
|
||||
if (!sum(bins, (bin) => bin.length)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const adjustedRange = scaleLinear().range([0.8, 0.3]);
|
||||
const colourScale = scaleSequential((n) =>
|
||||
interpolateGreens(adjustedRange(n))
|
||||
|
|
|
@ -126,6 +126,16 @@ body.night-mode {
|
|||
color: $night-fg;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text {
|
||||
text-anchor: middle;
|
||||
fill: grey;
|
||||
}
|
||||
rect {
|
||||
fill: $day-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.night-mode {
|
||||
.graph-tooltip {
|
||||
background: $night-bg;
|
||||
|
@ -139,6 +149,9 @@ body.night-mode {
|
|||
fill: $night-fg;
|
||||
opacity: 0.1;
|
||||
}
|
||||
.no-data rect {
|
||||
fill: $night-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.centered {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
@typescript-eslint/ban-ts-ignore: "off" */
|
||||
|
||||
import pb from "../backend/proto";
|
||||
import { Selection } from "d3-selection";
|
||||
|
||||
async function fetchData(search: string, days: number): Promise<Uint8Array> {
|
||||
const resp = await fetch("/_anki/graphData", {
|
||||
|
@ -66,3 +67,14 @@ export function defaultGraphBounds(): GraphBounds {
|
|||
marginBottom: 40,
|
||||
};
|
||||
}
|
||||
|
||||
export function setDataAvailable(
|
||||
svg: Selection<SVGElement, any, any, any>,
|
||||
available: boolean
|
||||
): void {
|
||||
svg.select(".no-data")
|
||||
.attr("pointer-events", available ? "none" : "all")
|
||||
.transition()
|
||||
.duration(600)
|
||||
.attr("opacity", available ? 0 : 1);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import { scaleLinear, ScaleLinear, ScaleSequential } from "d3-scale";
|
|||
import { axisBottom, axisLeft } from "d3-axis";
|
||||
import { area, curveBasis } from "d3-shape";
|
||||
import { showTooltip, hideTooltip } from "./tooltip";
|
||||
import { GraphBounds } from "./graphs";
|
||||
import { GraphBounds, setDataAvailable } from "./graphs";
|
||||
|
||||
export interface HistogramData {
|
||||
scale: ScaleLinear<number, number>;
|
||||
|
@ -33,13 +33,20 @@ export interface HistogramData {
|
|||
export function histogramGraph(
|
||||
svgElem: SVGElement,
|
||||
bounds: GraphBounds,
|
||||
data: HistogramData
|
||||
data: HistogramData | null
|
||||
): void {
|
||||
const binValue = data.binValue ?? ((bin: any): number => bin.length as number);
|
||||
|
||||
const svg = select(svgElem);
|
||||
const trans = svg.transition().duration(600) as any;
|
||||
|
||||
if (!data) {
|
||||
setDataAvailable(svg, false);
|
||||
return;
|
||||
} else {
|
||||
setDataAvailable(svg, true);
|
||||
}
|
||||
|
||||
const binValue = data.binValue ?? ((bin: any): number => bin.length as number);
|
||||
|
||||
const x = data.scale.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
|
||||
svg.select<SVGGElement>(".x-ticks")
|
||||
.transition(trans)
|
||||
|
|
|
@ -13,7 +13,7 @@ import { select, mouse } from "d3-selection";
|
|||
import { scaleLinear, scaleBand, scaleSequential } from "d3-scale";
|
||||
import { axisBottom, axisLeft } from "d3-axis";
|
||||
import { showTooltip, hideTooltip } from "./tooltip";
|
||||
import { GraphBounds } from "./graphs";
|
||||
import { GraphBounds, setDataAvailable } from "./graphs";
|
||||
import { area, curveBasis } from "d3-shape";
|
||||
import { I18n } from "../i18n";
|
||||
|
||||
|
@ -66,6 +66,13 @@ export function renderHours(
|
|||
const svg = select(svgElem);
|
||||
const trans = svg.transition().duration(600) as any;
|
||||
|
||||
if (!yMax) {
|
||||
setDataAvailable(svg, false);
|
||||
return;
|
||||
} else {
|
||||
setDataAvailable(svg, true);
|
||||
}
|
||||
|
||||
const x = scaleBand()
|
||||
.domain(data.map((d) => d.hour.toString()))
|
||||
.range([bounds.marginLeft, bounds.width - bounds.marginRight])
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import pb from "../backend/proto";
|
||||
import { extent, histogram, quantile } from "d3-array";
|
||||
import { extent, histogram, quantile, sum } from "d3-array";
|
||||
import { scaleLinear, scaleSequential } from "d3-scale";
|
||||
import { CardQueue } from "../cards";
|
||||
import { HistogramData } from "./histogram-graph";
|
||||
|
@ -98,6 +98,11 @@ export function prepareIntervalData(
|
|||
.domain(scale.domain() as any)
|
||||
.thresholds(scale.ticks(desiredBars))(allIntervals);
|
||||
|
||||
// empty graph?
|
||||
if (!sum(bins, (bin) => bin.length)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// start slightly darker
|
||||
const shiftedMin = xMin! - Math.round((xMax - xMin!) / 10);
|
||||
const colourScale = scaleSequential(interpolateBlues).domain([shiftedMin, xMax]);
|
||||
|
|
|
@ -18,7 +18,7 @@ import { select, mouse } from "d3-selection";
|
|||
import { scaleLinear, scaleSequential } from "d3-scale";
|
||||
import { axisBottom, axisLeft } from "d3-axis";
|
||||
import { showTooltip, hideTooltip } from "./tooltip";
|
||||
import { GraphBounds } from "./graphs";
|
||||
import { GraphBounds, setDataAvailable } from "./graphs";
|
||||
import { area, curveBasis } from "d3-shape";
|
||||
import { min, histogram, sum, max, Bin, cumsum } from "d3-array";
|
||||
import { timeSpan, dayLabel } from "../time";
|
||||
|
@ -116,6 +116,9 @@ export function renderReviews(
|
|||
showTime: boolean,
|
||||
i18n: I18n
|
||||
): void {
|
||||
const svg = select(svgElem);
|
||||
const trans = svg.transition().duration(600) as any;
|
||||
|
||||
const xMax = 1;
|
||||
let xMin = 0;
|
||||
// cap max to selected range
|
||||
|
@ -144,8 +147,13 @@ export function renderReviews(
|
|||
.domain(x.domain() as any)
|
||||
.thresholds(x.ticks(desiredBars))(sourceMap.entries() as any);
|
||||
|
||||
const svg = select(svgElem);
|
||||
const trans = svg.transition().duration(600) as any;
|
||||
// empty graph?
|
||||
if (!sum(bins, (bin) => bin.length)) {
|
||||
setDataAvailable(svg, false);
|
||||
return;
|
||||
} else {
|
||||
setDataAvailable(svg, true);
|
||||
}
|
||||
|
||||
x.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
|
||||
svg.select<SVGGElement>(".x-ticks")
|
||||
|
|
Loading…
Reference in a new issue