* ui: start redesign for live search/list
* ui: remove search field, show filters picker by default for assist
* ui: filter modal wip
* ui: filter modal wip
* ui: finish with omnisearch thing
* ui: start new dashboard redesign
* refining new card section
* ui: some "new dashboard" view improvs, fix icons fill inheritance, add ai button colors
* ui: split up search component (1.22+ tbd?), restrict filter type to own modals
* ui: mimic ant card
* ui: some changes for card creation flow, add series table to CustomMetricLineChart.tsx
* ui: more chart types, add table with filtering out series, start "compare to" thing
* ui: comparison designs
* ui: better granularity support, comparison view for bar chart
* ui: add comparison to more charts, add "metric" chart (BigNumChart.tsx)
* ui: cleanup logs
* ui: fix defualt import, fix sessheader crash, fix condition set ui
* ui: some refactoring and type coverage...
* ui: more refactoring; silence warnings for list renderers
* ui: moveing and renaming filters
* ui: add metricOf selector
* ui: check for metric type
* ui: fix crashes, add widget library table
* ui: change new series btn
* ui: restrict filterselection
* ui: fix timeseries table format
* ui: autoclose autocomplete modal
* ui: some fixes to issue filters default value, display and placeholder consistency
* ui: some dashboard issues with card selection modal and empty states
* ui: comparing for funnels, alternate column view, some refactoring to prepare for customizations...
* Style improvements in omnisearch headers
* Revert "Style improvements in omnisearch headers"
This reverts commit 89e51b0531.
* ui: show health status fetch error
* ui: table, bignum and comp for funnel, add csv export
* Omni-search improvements. (#2823)
Co-authored-by: Sudheer Salavadi <connect.uxmaster@gmail.com>
* ui: fix bad merge (git hallo?)
* ui: fix filter mapper
* rm husky
* ui: add card floater
* ui: add card floater
* ui: refactor local autocomplete input
* ui: filterout empty options
* UI improvements in New Cards (#2864)
* ui: some minor dashb improvements
* ui: metric type selector for head
* ui: change card type selector, add automapping
* ui: check chart/widget components for crashes
* ui: fix crash with table metrics
* ui: fix crashes related to metric type changes
* ui: filter category for clickmap filt
* ui: fix dash options menu, fix cr/up button
* ui: fix dash list menu propagation
* ui: hide addevent in heatmaps
* ui: fix time mapping for charts
* ui: fix exclusion component for path
* ui: fix series amount for path analysis, rm grid/list selector
* ui: fix icons in list view
* ui: fix for dlt button in widgets
* Various improvements Cards, OmniSearch and Cards Listing (#2881)
* ui: some improvements for cards list view, funnels and general filter display
* ui: longer node width for journey
* Product Analytics UI Improvements. (#2896)
* Various improvements Cards, OmniSearch and Cards Listing
* Improved cards listing page
* Various improvements in product analytics
* Charts UI improvements
---------
Co-authored-by: nick-delirium <nikita@openreplay.com>
* Live se red s2 (#2902)
* Various improvements Cards, OmniSearch and Cards Listing
* Improved cards listing page
* Various improvements in product analytics
* Charts UI improvements
* ui crash
---------
Co-authored-by: Sudheer Salavadi <connect.uxmaster@gmail.com>
* ui: fix lucide version
* ui: fix custom comparison period
* ui: fix custom comparison period
* ui: handle minor paths on frontend for path/sankey
* ui: assign icon for event types in sankey nodes
* ui: some strings changed
* ui: hide btn control for table view
* Various improvements in graphs, and analytics pages. (#2908)
* Various improvements Cards, OmniSearch and Cards Listing
* Improved cards listing page
* Various improvements in product analytics
* Charts UI improvements
* ui crash
* Chart improvements and layout toggling
* Various improvements
* Tooltips
---------
Co-authored-by: nick-delirium <nikita@openreplay.com>
* ui: fix weekday mapper for x axis on >7d range
* ui: lower default density to 35, fix table card display
* ui: filterMinorPaths -> return input data if nodes arr. is empty
* ui: use default filter for sessions, move around saved search actions, remove tags modal
* ui: fix card creator visibility in grid, fix table exporter visiblility in grid
* ui: fix some proptype warnings
* ui: change new series default expand state
* ui: save comp range in widget details
* ui: move timeseries to apache echarts
* ui: use unique id for window values
* ui: add timestamp for comp tooltip row
* ui: rename var for readability
* ui: fix comparison for 24hr
* Streamlined icons and improved echarts trends (#2920)
* Various improvements Cards, OmniSearch and Cards Listing
* Improved cards listing page
* Various improvements in product analytics
* Charts UI improvements
* ui crash
* Chart improvements and layout toggling
* Various improvements
* Tooltips
* Improved icons in cards listing page
* Update WidgetFormNew.tsx
* Sankey improvements
* Icon and text updates
Text alignment and color changes in x-ray
Icon Mapping with appropriate names and shapes
* Colors and Trend Chart Interaction updates
* ui
---------
Co-authored-by: nick-delirium <nikita@openreplay.com>
* ui: series update observe
* ui: resize chart on window
* ui: move barchart to echarts
* ui: fixing bars under comparison
* ui: fixing horizontal bar tooltip
* ui: rm unused
* ui: keep state in storage
* ui: small fixes for granularity and comparisons
* ui: fix savesearch button, fix comparison period tracking
* ui: fix funnel type selection
* ui: fixing saved search button
* ui: enable error logging, remove immutable reference
* ui: update savedsearch drop
* ui: disable button if no saved
* ui: small ui fixes
* ui: add drill to summary charts, add more options to card category picker
* ui: filter compSeries with table
* ui: swap tag_el operator and value
* ui: fix top countries
* ui: further changes for search/cards
* ui: move focus to session list on line click
* ui: fix issue filter mapper
* ui: fix alert pre-init function, fix metric list options, fix legend placement
* ui: fixes for card library
* ui: work on new sankey chart
* ui: fix metadata prefetch
* ui: moving snakey to echarts
* ui: fix funnel comparison focus
* ui: stale loader
---------
Co-authored-by: Sudheer Salavadi <connect.uxmaster@gmail.com>
346 lines
10 KiB
TypeScript
346 lines
10 KiB
TypeScript
import { formatTimeOrDate } from 'App/date';
|
||
|
||
export const colors = [
|
||
'#394EFF',
|
||
'#3EAAAF',
|
||
'#9276da',
|
||
'#ceba64',
|
||
'#bc6f9d',
|
||
'#966fbc',
|
||
'#64ce86',
|
||
'#e06da3',
|
||
'#6dabe0',
|
||
];
|
||
|
||
/**
|
||
* Match colors by baseName so “Previous Series 1” uses the same color as “Series 1”.
|
||
*/
|
||
export function assignColorsByBaseName(series: any[]) {
|
||
const palette = colors;
|
||
|
||
const colorMap: Record<string, string> = {};
|
||
let colorIndex = 0;
|
||
|
||
// Assign to current lines first
|
||
series.forEach((s) => {
|
||
if (!s._hideInLegend) {
|
||
const baseName = s._baseName || s.name;
|
||
if (!colorMap[baseName]) {
|
||
colorMap[baseName] = palette[colorIndex % palette.length];
|
||
colorIndex++;
|
||
}
|
||
}
|
||
});
|
||
|
||
series.forEach((s) => {
|
||
const baseName = s._baseName || s.name;
|
||
const color = colorMap[baseName];
|
||
s.itemStyle = { ...s.itemStyle, color };
|
||
s.lineStyle = { ...(s.lineStyle || {}), color };
|
||
});
|
||
}
|
||
|
||
function buildCategoryColorMap(categories: string[]): Record<number, string> {
|
||
const colorMap: Record<number, string> = {};
|
||
categories.forEach((_, i) => {
|
||
colorMap[i] = colors[i % colors.length];
|
||
});
|
||
return colorMap;
|
||
}
|
||
|
||
/**
|
||
* For each series, transform its data array to an array of objects
|
||
* with `value` and `itemStyle.color` based on the category index.
|
||
*/
|
||
export function assignColorsByCategory(
|
||
series: any[],
|
||
categories: string[]
|
||
) {
|
||
const categoryColorMap = buildCategoryColorMap(categories);
|
||
|
||
series.forEach((s, si) => {
|
||
s.data = s.data.map((val: any, i: number) => {
|
||
const color = categoryColorMap[i];
|
||
if (typeof val === 'number') {
|
||
return {
|
||
value: val,
|
||
itemStyle: { color },
|
||
};
|
||
}
|
||
return {
|
||
...val,
|
||
itemStyle: {
|
||
...(val.itemStyle || {}),
|
||
color,
|
||
},
|
||
};
|
||
});
|
||
s.itemStyle = { ...s.itemStyle, color: colors[si] };
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Show the hovered “current” or “previous” line + the matching partner (if it exists).
|
||
*/
|
||
export function customTooltipFormatter(uuid: string) {
|
||
return (params: any): string => {
|
||
// With trigger='item', params is a single object describing the hovered point
|
||
// { seriesName, dataIndex, data, marker, color, encode, ... }
|
||
if (!params) return '';
|
||
const { seriesName, dataIndex } = params;
|
||
const baseName = seriesName.replace(/^Previous\s+/, '');
|
||
|
||
if (!Array.isArray(params.data)) {
|
||
const isPrevious = /Previous/.test(seriesName);
|
||
const categoryName = (window as any).__yAxisData?.[uuid]?.[dataIndex];
|
||
const fullname = isPrevious ? `Previous ${categoryName}` : categoryName;
|
||
const partnerName = isPrevious ? categoryName : `Previous ${categoryName}`;
|
||
const partnerValue = (window as any).__seriesValueMap?.[uuid]?.[
|
||
partnerName
|
||
];
|
||
|
||
let str = `
|
||
<div class="flex flex-col gap-1 bg-white shadow border rounded p-2 z-50">
|
||
<div class="flex gap-2 items-center">
|
||
<div style="
|
||
border-radius: 99px;
|
||
background: ${params.color};
|
||
width: 1rem;
|
||
height: 1rem;">
|
||
</div>
|
||
<div class="font-medium text-black">${fullname}</div>
|
||
</div>
|
||
|
||
<div style="border-left: 2px solid ${
|
||
params.color
|
||
};" class="flex flex-col px-2 ml-2">
|
||
<div class="text-neutral-600 text-sm">
|
||
Total:
|
||
</div>
|
||
<div class="flex items-center gap-1">
|
||
<div class="font-medium text-black">${params.value}</div>
|
||
${buildCompareTag(params.value, partnerValue)}
|
||
</div>
|
||
</div>
|
||
`;
|
||
if (partnerValue !== undefined) {
|
||
const partnerColor =
|
||
(window as any).__seriesColorMap?.[uuid]?.[partnerName] || '#999';
|
||
str += `
|
||
<div style="border-left: 2px dashed ${partnerColor};" class="flex flex-col px-2 ml-2">
|
||
<div class="text-neutral-600 text-sm">
|
||
${isPrevious ? 'Current' : 'Previous'} Total:
|
||
</div>
|
||
<div class="flex items-center gap-1">
|
||
<div class="font-medium">${partnerValue ?? '—'}</div>
|
||
${buildCompareTag(partnerValue, params.value)}
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
str += '</div>';
|
||
|
||
return str;
|
||
}
|
||
const isPrevious = /^Previous\s+/.test(seriesName);
|
||
const partnerName = isPrevious ? baseName : `Previous ${baseName}`;
|
||
// 'value' of the hovered point
|
||
const yKey = params.encode.y[0]; // "Series 1"
|
||
const value = params.data?.[yKey];
|
||
|
||
const timestamp = (window as any).__timestampMap?.[uuid]?.[dataIndex];
|
||
const comparisonTimestamp = (window as any).__timestampCompMap?.[uuid]?.[
|
||
dataIndex
|
||
];
|
||
|
||
// Get partner’s value from some global map
|
||
|
||
const partnerVal = (window as any).__seriesValueMap?.[uuid]?.[
|
||
partnerName
|
||
]?.[dataIndex];
|
||
|
||
const categoryLabel = (window as any).__categoryMap[uuid]
|
||
? (window as any).__categoryMap[uuid][dataIndex]
|
||
: dataIndex;
|
||
|
||
const firstTs = isPrevious ? comparisonTimestamp : timestamp;
|
||
const secondTs = isPrevious ? timestamp : comparisonTimestamp;
|
||
let tooltipContent = `
|
||
<div class="flex flex-col gap-1 bg-white shadow border rounded p-2 z-50">
|
||
<div class="flex gap-2 items-center">
|
||
<div style="
|
||
border-radius: 99px;
|
||
background: ${params.color};
|
||
width: 1rem;
|
||
height: 1rem;">
|
||
</div>
|
||
<div class="font-medium text-black">${seriesName}</div>
|
||
</div>
|
||
|
||
<div style="border-left: 2px solid ${
|
||
params.color
|
||
};" class="flex flex-col px-2 ml-2">
|
||
<div class="text-neutral-600 text-sm">
|
||
${firstTs ? formatTimeOrDate(firstTs) : categoryLabel}
|
||
</div>
|
||
<div class="flex items-center gap-1">
|
||
<div class="font-medium text-black">${value ?? '—'}</div>
|
||
${buildCompareTag(value, partnerVal)}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
if (partnerVal !== undefined) {
|
||
const partnerColor =
|
||
(window as any).__seriesColorMap?.[uuid]?.[partnerName] || '#999';
|
||
tooltipContent += `
|
||
<div style="border-left: 2px dashed ${partnerColor};" class="flex flex-col px-2 ml-2">
|
||
<div class="text-neutral-600 text-sm">
|
||
${secondTs ? formatTimeOrDate(secondTs) : categoryLabel}
|
||
</div>
|
||
<div class="flex items-center gap-1">
|
||
<div class="font-medium">${partnerVal ?? '—'}</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
tooltipContent += '</div>';
|
||
return tooltipContent;
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Build a small "compare" tag to show ▲ or ▼ plus absolute delta plus percent change.
|
||
* For example, if val=120, prevVal=100 => ▲ 20 (20%)
|
||
*/
|
||
function buildCompareTag(val: number, prevVal: number): string {
|
||
if (val == null || prevVal == null) {
|
||
return '';
|
||
}
|
||
|
||
const delta = val - prevVal;
|
||
const isHigher = delta > 0;
|
||
const arrow = isHigher ? '▲' : '▼';
|
||
const absDelta = Math.abs(delta);
|
||
const ratio = prevVal !== 0 ? ((delta / prevVal) * 100).toFixed(2) : null;
|
||
|
||
const tagColor = isHigher ? '#D1FADF' : '#FEE2E2';
|
||
const arrowColor = isHigher ? '#059669' : '#DC2626';
|
||
|
||
return `
|
||
<div style="
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
background: ${tagColor};
|
||
color: ${arrowColor};
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
font-size: 0.75rem;">
|
||
<span>${arrow}</span>
|
||
<span>${absDelta}</span>
|
||
<span>${ratio ? `(${ratio}%)` : ''}</span>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* Build category labels (["Sun", "Mon", ...]) from the "current" data only
|
||
*/
|
||
export function buildCategories(data: DataProps['data']): string[] {
|
||
return data.chart.map((item) => item.time);
|
||
}
|
||
|
||
/**
|
||
* Create a dataset with dimension [idx, ...names].
|
||
* The `idx` dimension aligns with xAxis = "category"
|
||
* (which is dates in our case)
|
||
*/
|
||
export function createDataset(id: string, data: DataProps['data']) {
|
||
const dimensions = ['idx', ...data.namesMap];
|
||
const source = data.chart.map((item, idx) => {
|
||
const row: (number | undefined)[] = [idx];
|
||
data.namesMap.forEach((name) => {
|
||
const val =
|
||
typeof item[name] === 'number' ? (item[name] as number) : undefined;
|
||
row.push(val);
|
||
});
|
||
return row;
|
||
});
|
||
return { id, dimensions, source };
|
||
}
|
||
|
||
/**
|
||
* Create line series referencing the dataset dimension by name.
|
||
* `_baseName` is used to match “Series 1” <-> “Previous Series 1”.
|
||
*/
|
||
export function createSeries(
|
||
data: DataProps['data'],
|
||
datasetId: string,
|
||
dashed: boolean,
|
||
hideFromLegend: boolean
|
||
) {
|
||
return data.namesMap.filter(Boolean).map((fullName) => {
|
||
const baseName = fullName.replace(/^Previous\s+/, '');
|
||
return {
|
||
name: fullName,
|
||
_baseName: baseName,
|
||
type: 'line',
|
||
animation: false,
|
||
datasetId,
|
||
encode: { x: 'idx', y: fullName },
|
||
lineStyle: dashed ? { type: 'dashed' } : undefined,
|
||
showSymbol: false,
|
||
// custom flag to hide prev data from legend
|
||
_hideInLegend: hideFromLegend,
|
||
itemStyle: { opacity: 1 },
|
||
emphasis: {
|
||
focus: 'series',
|
||
itemStyle: { opacity: 1 },
|
||
lineStyle: { opacity: 1 },
|
||
},
|
||
blur: {
|
||
itemStyle: { opacity: 0.2 },
|
||
lineStyle: { opacity: 0.2 },
|
||
},
|
||
};
|
||
});
|
||
}
|
||
|
||
export function buildDatasetsAndSeries(props: DataProps) {
|
||
const mainDataset = createDataset('current', props.data);
|
||
const mainSeries = createSeries(props.data, 'current', false, false);
|
||
|
||
let compDataset: Record<string, any> | null = null;
|
||
let compSeries: Record<string, any>[] = [];
|
||
if (props.compData && props.compData.chart?.length) {
|
||
compDataset = createDataset('previous', props.compData);
|
||
compSeries = createSeries(props.compData, 'previous', true, true);
|
||
}
|
||
|
||
const datasets = compDataset ? [mainDataset, compDataset] : [mainDataset];
|
||
const series = [...mainSeries, ...compSeries];
|
||
assignColorsByBaseName(series as any);
|
||
|
||
return { datasets, series };
|
||
}
|
||
|
||
export interface DataItem {
|
||
time: string;
|
||
timestamp: number;
|
||
[seriesName: string]: number | string;
|
||
}
|
||
|
||
export interface DataProps {
|
||
data: {
|
||
chart: DataItem[];
|
||
// series names
|
||
namesMap: string[];
|
||
};
|
||
compData?: {
|
||
chart: DataItem[];
|
||
// same as data.namesMap, but with "Previous" prefix
|
||
namesMap: string[];
|
||
};
|
||
}
|