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 [isAutoRefresh, setIsAutoRefresh] = createSignal(false);
|
||||||
const [refreshInterval, setRefreshInterval] = createSignal('10');
|
const [refreshInterval, setRefreshInterval] = createSignal('10');
|
||||||
const [refreshKey, setRefreshKey] = createSignal(0);
|
const [refreshKey, setRefreshKey] = createSignal(0);
|
||||||
|
const [dailyVolumeScale, setDailyVolumeScale] = createSignal<'linear' | 'log'>('linear');
|
||||||
|
|
||||||
const filters = createMemo(() => ({
|
const filters = createMemo(() => ({
|
||||||
days: Number(days()),
|
days: Number(days()),
|
||||||
|
|
@ -234,14 +235,25 @@ export const Analytics: Component = () => {
|
||||||
title="Daily Volume"
|
title="Daily Volume"
|
||||||
description="Daily request and token totals on shared time axis."
|
description="Daily request and token totals on shared time axis."
|
||||||
actions={
|
actions={
|
||||||
<ChartLegend
|
<div style="display: flex; align-items: center; gap: 16px;">
|
||||||
items={[
|
<ChartLegend
|
||||||
{ key: 'requests', label: 'Requests', color: '#2357d8' },
|
items={[
|
||||||
{ key: 'tokens', label: 'Tokens', color: '#1f7a45' },
|
{ key: 'requests', label: 'Requests', color: '#2357d8' },
|
||||||
]}
|
{ key: 'tokens', label: 'Tokens', color: '#1f7a45' },
|
||||||
mutedKeys={hiddenDailySeries()}
|
]}
|
||||||
onToggle={(key) => toggleHiddenKey(setHiddenDailySeries, key)}
|
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
|
<TimeSeriesChart
|
||||||
|
|
@ -258,6 +270,7 @@ export const Analytics: Component = () => {
|
||||||
formatLeftValue={(value) => new Intl.NumberFormat('en-US').format(Math.round(value))}
|
formatLeftValue={(value) => new Intl.NumberFormat('en-US').format(Math.round(value))}
|
||||||
formatRightValue={(value) => new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 }).format(value)}
|
formatRightValue={(value) => new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 }).format(value)}
|
||||||
tooltipTitle="Daily request and token totals"
|
tooltipTitle="Daily request and token totals"
|
||||||
|
yScaleType={dailyVolumeScale()}
|
||||||
/>
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -214,6 +214,7 @@ interface TimeSeriesChartProps {
|
||||||
formatLeftValue?: (value: number) => string;
|
formatLeftValue?: (value: number) => string;
|
||||||
formatRightValue?: (value: number) => string;
|
formatRightValue?: (value: number) => string;
|
||||||
tooltipTitle?: string;
|
tooltipTitle?: string;
|
||||||
|
yScaleType?: 'linear' | 'log';
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChartLegendProps {
|
interface ChartLegendProps {
|
||||||
|
|
@ -287,32 +288,82 @@ export function TimeSeriesChart(props: TimeSeriesChartProps) {
|
||||||
const leftSeries = createMemo(() => visibleSeries().filter((series) => series.axis !== 'right'));
|
const leftSeries = createMemo(() => visibleSeries().filter((series) => series.axis !== 'right'));
|
||||||
const rightSeries = 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 leftScale = createMemo(() => {
|
||||||
const maxValue =
|
const values = points().flatMap((point: ParsedTimeSeriesDatum) =>
|
||||||
d3.max(points(), (point: ParsedTimeSeriesDatum) =>
|
leftSeries().map((series: TimeSeriesChartSeries) => Number(point[series.key] ?? 0))
|
||||||
d3.max(leftSeries(), (series: TimeSeriesChartSeries) => Number(point[series.key] ?? 0))
|
);
|
||||||
) ?? 0;
|
const maxValue = d3.max(values) ?? 0;
|
||||||
return d3.scaleLinear().domain([0, maxValue === 0 ? 1 : maxValue * 1.1]).nice().range([dimensions().marginTop + getInnerHeight(dimensions()), dimensions().marginTop]);
|
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 rightScale = createMemo(() => {
|
||||||
const maxValue =
|
const values = points().flatMap((point: ParsedTimeSeriesDatum) =>
|
||||||
d3.max(points(), (point: ParsedTimeSeriesDatum) =>
|
rightSeries().map((series: TimeSeriesChartSeries) => Number(point[series.key] ?? 0))
|
||||||
d3.max(rightSeries(), (series: TimeSeriesChartSeries) => Number(point[series.key] ?? 0))
|
);
|
||||||
) ?? 0;
|
const maxValue = d3.max(values) ?? 0;
|
||||||
return d3.scaleLinear().domain([0, maxValue === 0 ? 1 : maxValue * 1.1]).nice().range([dimensions().marginTop + getInnerHeight(dimensions()), dimensions().marginTop]);
|
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 leftTicks = createMemo(() => {
|
||||||
const rightTicks = createMemo(() => rightSeries().length > 0 ? rightScale().ticks(4) : []);
|
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 xTicks = createMemo(() => xScale().ticks(Math.max(2, Math.floor(getInnerWidth(dimensions()) / 120))));
|
||||||
const dateTicks = createMemo(() => getDateTicks(points().map((point) => point.parsedDate), getInnerWidth(dimensions())));
|
const dateTicks = createMemo(() => getDateTicks(points().map((point) => point.parsedDate), getInnerWidth(dimensions())));
|
||||||
|
|
||||||
const linePath = (series: TimeSeriesChartSeries) =>
|
const linePath = (series: TimeSeriesChartSeries) => {
|
||||||
d3.line()
|
const scale = series.axis === 'right' ? rightScale() : leftScale();
|
||||||
.defined((point: ParsedTimeSeriesDatum) => typeof point[series.key] === 'number')
|
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))
|
.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 hoveredPoint = createMemo(() => {
|
||||||
const index = hoverIndex();
|
const index = hoverIndex();
|
||||||
|
|
@ -326,7 +377,15 @@ export function TimeSeriesChart(props: TimeSeriesChartProps) {
|
||||||
...series,
|
...series,
|
||||||
value: Number(hoveredPoint()?.[series.key] ?? 0),
|
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