multiple breakpoints
This commit is contained in:
@@ -16,7 +16,16 @@ import type { IInterval } from '@openpanel/validation';
|
||||
|
||||
// Define the data structure
|
||||
export interface ISerieDataItem {
|
||||
label: string | null | undefined;
|
||||
label_0: string | null | undefined;
|
||||
label_1?: string | null | undefined;
|
||||
label_2?: string | null | undefined;
|
||||
label_3?: string | null | undefined;
|
||||
count: number;
|
||||
date: string;
|
||||
}
|
||||
|
||||
export interface ISerieDataItemComplete {
|
||||
labels: string[];
|
||||
count: number;
|
||||
date: string;
|
||||
}
|
||||
@@ -37,6 +46,39 @@ function roundDate(date: Date, interval: IInterval): Date {
|
||||
}
|
||||
}
|
||||
|
||||
function filterFalsyAfterTruthy(array: (string | undefined | null)[]) {
|
||||
let foundTruthy = false;
|
||||
const filtered = array.filter((item) => {
|
||||
if (foundTruthy) {
|
||||
// After a truthy, filter out falsy values
|
||||
return !!item;
|
||||
}
|
||||
if (item) {
|
||||
// Mark when the first truthy is encountered
|
||||
foundTruthy = true;
|
||||
}
|
||||
// Return all elements until the first truthy is found
|
||||
return true;
|
||||
});
|
||||
|
||||
if (filtered.some((item) => !!item)) {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
return [null];
|
||||
}
|
||||
|
||||
function concatLabels(entry: ISerieDataItem): string {
|
||||
return filterFalsyAfterTruthy([
|
||||
entry.label_0,
|
||||
entry.label_1,
|
||||
entry.label_2,
|
||||
entry.label_3,
|
||||
])
|
||||
.map((label) => label || NOT_SET_VALUE)
|
||||
.join(':::');
|
||||
}
|
||||
|
||||
// Function to complete the timeline for each label
|
||||
export function completeSerie(
|
||||
data: ISerieDataItem[],
|
||||
@@ -51,23 +93,23 @@ export function completeSerie(
|
||||
data.forEach((entry) => {
|
||||
const roundedDate = roundDate(parseISO(entry.date), interval);
|
||||
const dateKey = format(roundedDate, 'yyyy-MM-dd HH:mm:ss');
|
||||
const label = entry.label || NOT_SET_VALUE;
|
||||
const label = concatLabels(entry) || NOT_SET_VALUE;
|
||||
if (!labelsMap.has(label)) {
|
||||
labelsMap.set(label, new Map());
|
||||
}
|
||||
const labelData = labelsMap.get(label);
|
||||
labelData?.set(dateKey, (labelData.get(dateKey) || 0) + (entry.count || 0));
|
||||
const labelData = labelsMap.get(label)!;
|
||||
labelData.set(dateKey, (labelData.get(dateKey) || 0) + (entry.count || 0));
|
||||
});
|
||||
|
||||
// Complete the timeline for each label
|
||||
const result: Record<string, ISerieDataItem[]> = {};
|
||||
const result: Record<string, ISerieDataItemComplete[]> = {};
|
||||
labelsMap.forEach((counts, label) => {
|
||||
let currentDate = roundDate(startDate, interval);
|
||||
result[label] = [];
|
||||
while (currentDate <= endDate) {
|
||||
const dateKey = format(currentDate, 'yyyy-MM-dd HH:mm:ss');
|
||||
result[label]!.push({
|
||||
label: label,
|
||||
labels: label.split(':::'),
|
||||
date: dateKey,
|
||||
count: counts.get(dateKey) || 0,
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ export function toDots(
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[`${path}${key}`]: value,
|
||||
[`${path}${key}`]: typeof value === 'string' ? value.trim() : value,
|
||||
};
|
||||
}, {});
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@ export function getChartSql({
|
||||
sb.where.projectId = `project_id = ${escape(projectId)}`;
|
||||
|
||||
if (event.name !== '*') {
|
||||
sb.select.label = `${escape(event.name)} as label`;
|
||||
sb.select.label_0 = `${escape(event.name)} as label_0`;
|
||||
sb.where.eventName = `name = ${escape(event.name)}`;
|
||||
} else {
|
||||
sb.select.label = `'*' as label`;
|
||||
sb.select.label_0 = `'*' as label_0`;
|
||||
}
|
||||
|
||||
sb.select.count = `count(*) as count`;
|
||||
@@ -60,11 +60,11 @@ export function getChartSql({
|
||||
}
|
||||
|
||||
breakdowns.forEach((breakdown, index) => {
|
||||
const key = index === 0 ? 'label' : `label_${index}`;
|
||||
const key = `label_${index}`;
|
||||
const value = breakdown.name.startsWith('properties.')
|
||||
? `mapValues(mapExtractKeyLike(properties, ${escape(
|
||||
? `arrayMap(x -> trim(x), mapValues(mapExtractKeyLike(properties, ${escape(
|
||||
breakdown.name.replace(/^properties\./, '').replace('.*.', '.%.')
|
||||
)}))`
|
||||
)})))`
|
||||
: escape(breakdown.name);
|
||||
sb.select[key] = breakdown.name.startsWith('properties.')
|
||||
? `arrayElement(${value}, 1) as ${key}`
|
||||
@@ -125,9 +125,9 @@ export function getEventFiltersWhereClause(filters: IChartEventFilter[]) {
|
||||
}
|
||||
|
||||
if (name.startsWith('properties.')) {
|
||||
const whereFrom = `mapValues(mapExtractKeyLike(properties, ${escape(
|
||||
const whereFrom = `arrayMap(x -> trim(x), mapValues(mapExtractKeyLike(properties, ${escape(
|
||||
name.replace(/^properties\./, '').replace('.*.', '.%.')
|
||||
)}))`;
|
||||
)})))`;
|
||||
|
||||
switch (operator) {
|
||||
case 'is': {
|
||||
|
||||
@@ -458,14 +458,17 @@ export async function getChartSerie(payload: IGetChartDataInput) {
|
||||
completeSerie(data, payload.startDate, payload.endDate, payload.interval)
|
||||
)
|
||||
.then((series) => {
|
||||
return Object.keys(series).map((label) => {
|
||||
return Object.keys(series).map((key) => {
|
||||
const firstDataItem = series[key]![0]!;
|
||||
const isBreakdown =
|
||||
payload.breakdowns.length && !alphabetIds.includes(label as 'A');
|
||||
const serieLabel = isBreakdown ? label : getEventLegend(payload.event);
|
||||
payload.breakdowns.length && firstDataItem.labels.length;
|
||||
const serieLabel = isBreakdown
|
||||
? firstDataItem.labels
|
||||
: [getEventLegend(payload.event)];
|
||||
return {
|
||||
name: serieLabel,
|
||||
event: payload.event,
|
||||
data: series[label]!.map((item) => ({
|
||||
data: series[key]!.map((item) => ({
|
||||
...item,
|
||||
date: toDynamicISODateWithTZ(
|
||||
item.date,
|
||||
@@ -523,7 +526,7 @@ export async function getChart(input: IChartInput) {
|
||||
const final: FinalChart = {
|
||||
series: series.map((serie) => {
|
||||
const previousSerie = previousSeries?.find(
|
||||
(item) => item.name === serie.name
|
||||
(item) => item.name.join('-') === serie.name.join('-')
|
||||
);
|
||||
const metrics = {
|
||||
sum: sum(serie.data.map((item) => item.count)),
|
||||
@@ -533,8 +536,8 @@ export async function getChart(input: IChartInput) {
|
||||
};
|
||||
|
||||
return {
|
||||
id: slug(serie.name),
|
||||
name: serie.name,
|
||||
id: slug(serie.name.join('-')),
|
||||
names: serie.name,
|
||||
event: {
|
||||
id: serie.event.id!,
|
||||
name: serie.event.displayName ?? serie.event.name,
|
||||
|
||||
@@ -102,9 +102,9 @@ export const chartRouter = createTRPCRouter({
|
||||
sb.where.event = `name = ${escape(event)}`;
|
||||
}
|
||||
if (property.startsWith('properties.')) {
|
||||
sb.select.values = `distinct mapValues(mapExtractKeyLike(properties, ${escape(
|
||||
sb.select.values = `distinct arrayMap(x -> trim(x), mapValues(mapExtractKeyLike(properties, ${escape(
|
||||
property.replace(/^properties\./, '').replace('.*.', '.%.')
|
||||
)})) as values`;
|
||||
)}))) as values`;
|
||||
} else {
|
||||
sb.select.values = `distinct ${property} as values`;
|
||||
}
|
||||
|
||||
@@ -40,9 +40,9 @@ export const profileRouter = createTRPCRouter({
|
||||
sb.from = 'profiles';
|
||||
sb.where.project_id = `project_id = ${escape(projectId)}`;
|
||||
if (property.startsWith('properties.')) {
|
||||
sb.select.values = `distinct mapValues(mapExtractKeyLike(properties, ${escape(
|
||||
sb.select.values = `distinct arrayMap(x -> trim(x), mapValues(mapExtractKeyLike(properties, ${escape(
|
||||
property.replace(/^properties\./, '').replace('.*.', '.%.')
|
||||
)})) as values`;
|
||||
)}))) as values`;
|
||||
} else {
|
||||
sb.select.values = `${property} as values`;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export type Metrics = {
|
||||
|
||||
export type IChartSerie = {
|
||||
id: string;
|
||||
name: string;
|
||||
names: string[];
|
||||
event: {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
Reference in New Issue
Block a user