feat(analytics): add logarithmic scale option for daily volume chart
This commit is contained in:
parent
96c9b963b4
commit
43664819d4
2 changed files with 96 additions and 24 deletions
|
|
@ -37,6 +37,7 @@ export const Analytics: Component = () => {
|
|||
const [isAutoRefresh, setIsAutoRefresh] = createSignal(false);
|
||||
const [refreshInterval, setRefreshInterval] = createSignal('10');
|
||||
const [refreshKey, setRefreshKey] = createSignal(0);
|
||||
const [dailyVolumeScale, setDailyVolumeScale] = createSignal<'linear' | 'log'>('linear');
|
||||
|
||||
const filters = createMemo(() => ({
|
||||
days: Number(days()),
|
||||
|
|
@ -234,14 +235,25 @@ export const Analytics: Component = () => {
|
|||
title="Daily Volume"
|
||||
description="Daily request and token totals on shared time axis."
|
||||
actions={
|
||||
<ChartLegend
|
||||
items={[
|
||||
{ key: 'requests', label: 'Requests', color: '#2357d8' },
|
||||
{ key: 'tokens', label: 'Tokens', color: '#1f7a45' },
|
||||
]}
|
||||
mutedKeys={hiddenDailySeries()}
|
||||
onToggle={(key) => toggleHiddenKey(setHiddenDailySeries, key)}
|
||||
/>
|
||||
<div style="display: flex; align-items: center; gap: 16px;">
|
||||
<ChartLegend
|
||||
items={[
|
||||
{ key: 'requests', label: 'Requests', color: '#2357d8' },
|
||||
{ key: 'tokens', label: 'Tokens', color: '#1f7a45' },
|
||||
]}
|
||||
mutedKeys={hiddenDailySeries()}
|
||||
onToggle={(key) => toggleHiddenKey(setHiddenDailySeries, key)}
|
||||
/>
|
||||
<Select
|
||||
label="Scale"
|
||||
value={dailyVolumeScale()}
|
||||
options={[
|
||||
{ value: 'linear', label: 'Linear' },
|
||||
{ value: 'log', label: 'Log' },
|
||||
]}
|
||||
onChange={setDailyVolumeScale}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<TimeSeriesChart
|
||||
|
|
@ -258,6 +270,7 @@ export const Analytics: Component = () => {
|
|||
formatLeftValue={(value) => new Intl.NumberFormat('en-US').format(Math.round(value))}
|
||||
formatRightValue={(value) => new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 }).format(value)}
|
||||
tooltipTitle="Daily request and token totals"
|
||||
yScaleType={dailyVolumeScale()}
|
||||
/>
|
||||
</Panel>
|
||||
|
||||
|
|
|
|||
|
|
@ -214,6 +214,7 @@ interface TimeSeriesChartProps {
|
|||
formatLeftValue?: (value: number) => string;
|
||||
formatRightValue?: (value: number) => string;
|
||||
tooltipTitle?: string;
|
||||
yScaleType?: 'linear' | 'log';
|
||||
}
|
||||
|
||||
interface ChartLegendProps {
|
||||
|
|
@ -287,32 +288,82 @@ export function TimeSeriesChart(props: TimeSeriesChartProps) {
|
|||
const leftSeries = createMemo(() => visibleSeries().filter((series) => series.axis !== 'right'));
|
||||
const rightSeries = createMemo(() => visibleSeries().filter((series) => series.axis === 'right'));
|
||||
|
||||
const isLogScale = () => props.yScaleType === 'log';
|
||||
|
||||
const leftScale = createMemo(() => {
|
||||
const maxValue =
|
||||
d3.max(points(), (point: ParsedTimeSeriesDatum) =>
|
||||
d3.max(leftSeries(), (series: TimeSeriesChartSeries) => Number(point[series.key] ?? 0))
|
||||
) ?? 0;
|
||||
return d3.scaleLinear().domain([0, maxValue === 0 ? 1 : maxValue * 1.1]).nice().range([dimensions().marginTop + getInnerHeight(dimensions()), dimensions().marginTop]);
|
||||
const values = points().flatMap((point: ParsedTimeSeriesDatum) =>
|
||||
leftSeries().map((series: TimeSeriesChartSeries) => Number(point[series.key] ?? 0))
|
||||
);
|
||||
const maxValue = d3.max(values) ?? 0;
|
||||
const range = [dimensions().marginTop + getInnerHeight(dimensions()), dimensions().marginTop];
|
||||
|
||||
if (isLogScale()) {
|
||||
const positiveValues = values.filter((v) => v > 0);
|
||||
const minLog = positiveValues.length > 0 ? d3.min(positiveValues) ?? 1 : 1;
|
||||
const maxLog = maxValue === 0 ? 10 : maxValue * 1.1;
|
||||
return d3.scaleLog().domain([minLog, maxLog]).range(range);
|
||||
}
|
||||
|
||||
return d3.scaleLinear().domain([0, maxValue === 0 ? 1 : maxValue * 1.1]).nice().range(range);
|
||||
});
|
||||
|
||||
const rightScale = createMemo(() => {
|
||||
const maxValue =
|
||||
d3.max(points(), (point: ParsedTimeSeriesDatum) =>
|
||||
d3.max(rightSeries(), (series: TimeSeriesChartSeries) => Number(point[series.key] ?? 0))
|
||||
) ?? 0;
|
||||
return d3.scaleLinear().domain([0, maxValue === 0 ? 1 : maxValue * 1.1]).nice().range([dimensions().marginTop + getInnerHeight(dimensions()), dimensions().marginTop]);
|
||||
const values = points().flatMap((point: ParsedTimeSeriesDatum) =>
|
||||
rightSeries().map((series: TimeSeriesChartSeries) => Number(point[series.key] ?? 0))
|
||||
);
|
||||
const maxValue = d3.max(values) ?? 0;
|
||||
const range = [dimensions().marginTop + getInnerHeight(dimensions()), dimensions().marginTop];
|
||||
|
||||
if (isLogScale()) {
|
||||
const positiveValues = values.filter((v) => v > 0);
|
||||
const minLog = positiveValues.length > 0 ? d3.min(positiveValues) ?? 1 : 1;
|
||||
const maxLog = maxValue === 0 ? 10 : maxValue * 1.1;
|
||||
return d3.scaleLog().domain([minLog, maxLog]).range(range);
|
||||
}
|
||||
|
||||
return d3.scaleLinear().domain([0, maxValue === 0 ? 1 : maxValue * 1.1]).nice().range(range);
|
||||
});
|
||||
|
||||
const leftTicks = createMemo(() => leftScale().ticks(4));
|
||||
const rightTicks = createMemo(() => rightSeries().length > 0 ? rightScale().ticks(4) : []);
|
||||
const leftTicks = createMemo(() => {
|
||||
if (isLogScale()) {
|
||||
const maxValue = d3.max(points(), (point: ParsedTimeSeriesDatum) =>
|
||||
d3.max(leftSeries(), (series: TimeSeriesChartSeries) => Number(point[series.key] ?? 0))
|
||||
) ?? 0;
|
||||
return getSymlogTicks(maxValue);
|
||||
}
|
||||
return leftScale().ticks(4);
|
||||
});
|
||||
const rightTicks = createMemo(() => {
|
||||
if (rightSeries().length === 0) {
|
||||
return [];
|
||||
}
|
||||
if (isLogScale()) {
|
||||
const maxValue = d3.max(points(), (point: ParsedTimeSeriesDatum) =>
|
||||
d3.max(rightSeries(), (series: TimeSeriesChartSeries) => Number(point[series.key] ?? 0))
|
||||
) ?? 0;
|
||||
return getSymlogTicks(maxValue);
|
||||
}
|
||||
return rightScale().ticks(4);
|
||||
});
|
||||
const xTicks = createMemo(() => xScale().ticks(Math.max(2, Math.floor(getInnerWidth(dimensions()) / 120))));
|
||||
const dateTicks = createMemo(() => getDateTicks(points().map((point) => point.parsedDate), getInnerWidth(dimensions())));
|
||||
|
||||
const linePath = (series: TimeSeriesChartSeries) =>
|
||||
d3.line()
|
||||
.defined((point: ParsedTimeSeriesDatum) => typeof point[series.key] === 'number')
|
||||
const linePath = (series: TimeSeriesChartSeries) => {
|
||||
const scale = series.axis === 'right' ? rightScale() : leftScale();
|
||||
return d3.line()
|
||||
.defined((point: ParsedTimeSeriesDatum) => {
|
||||
const value = Number(point[series.key] ?? 0);
|
||||
if (!Number.isFinite(value)) {
|
||||
return false;
|
||||
}
|
||||
if (isLogScale() && value <= 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.x((point: ParsedTimeSeriesDatum) => xScale()(point.parsedDate))
|
||||
.y((point: ParsedTimeSeriesDatum) => (series.axis === 'right' ? rightScale() : leftScale())(Number(point[series.key] ?? 0)))(points()) ?? '';
|
||||
.y((point: ParsedTimeSeriesDatum) => scale(Number(point[series.key] ?? 0)))(points()) ?? '';
|
||||
};
|
||||
|
||||
const hoveredPoint = createMemo(() => {
|
||||
const index = hoverIndex();
|
||||
|
|
@ -326,7 +377,15 @@ export function TimeSeriesChart(props: TimeSeriesChartProps) {
|
|||
...series,
|
||||
value: Number(hoveredPoint()?.[series.key] ?? 0),
|
||||
}))
|
||||
.filter((series) => Number.isFinite(series.value))
|
||||
.filter((series) => {
|
||||
if (!Number.isFinite(series.value)) {
|
||||
return false;
|
||||
}
|
||||
if (isLogScale() && series.value <= 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
: []
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue