ui: move timeseries to apache echarts

This commit is contained in:
nick-delirium 2025-01-06 16:42:09 +01:00
parent d3acbdcc1b
commit 251cd5a6c4
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
16 changed files with 870 additions and 522 deletions

View file

@ -0,0 +1,88 @@
import React from 'react';
import {
DataProps,
buildCategories,
customTooltipFormatter
} from './utils';
import { buildBarDatasetsAndSeries } from './barUtils';
import { defaultOptions, echarts } from './init';
import { BarChart } from 'echarts/charts';
echarts.use([BarChart]);
interface BarChartProps extends DataProps {
label?: string;
horizontal?: boolean;
}
function ORBarChart(props: BarChartProps) {
const chartRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (!chartRef.current) return;
const chart = echarts.init(chartRef.current);
const categories = buildCategories(props.data);
const { datasets, series } = buildBarDatasetsAndSeries(props, props.horizontal ?? false);
(window as any).__seriesValueMap = {};
(window as any).__seriesColorMap = {};
(window as any).__timestampMap = props.data.chart.map((item) => item.timestamp);
(window as any).__categoryMap = categories;
series.forEach((s: any) => {
(window as any).__seriesColorMap[s.name] = s.itemStyle?.color ?? '#999';
const ds = datasets.find((d) => d.id === s.datasetId);
if (!ds) return;
const yDim = props.horizontal ? s.encode.x : s.encode.y;
const yDimIndex = ds.dimensions.indexOf(yDim);
if (yDimIndex < 0) return;
(window as any).__seriesValueMap[s.name] = {};
ds.source.forEach((row: any[]) => {
const rowIdx = row[0]; // 'idx'
(window as any).__seriesValueMap[s.name][rowIdx] = row[yDimIndex];
});
});
const xAxis: any = {
type: props.horizontal ? 'value' : 'category',
data: props.horizontal ? undefined : categories,
};
const yAxis: any = {
type: props.horizontal ? 'category' : 'value',
data: props.horizontal ? categories : undefined,
name: props.label ?? 'Number of Sessions',
nameLocation: 'middle',
nameGap: 35,
};
chart.setOption({
...defaultOptions,
legend: {
...defaultOptions.legend,
data: series.filter((s: any) => !s._hideInLegend).map((s: any) => s.name),
},
tooltip: {
...defaultOptions.tooltip,
formatter: customTooltipFormatter,
},
xAxis,
yAxis,
dataset: datasets,
series,
});
return () => {
chart.dispose();
delete (window as any).__seriesValueMap;
delete (window as any).__seriesColorMap;
delete (window as any).__categoryMap;
delete (window as any).__timestampMap;
};
}, [props.data, props.compData, props.horizontal]);
return <div ref={chartRef} style={{ width: '100%', height: 240 }} />;
}
export default ORBarChart;

View file

@ -0,0 +1,103 @@
import React from 'react';
import { echarts, defaultOptions } from './init';
import { customTooltipFormatter, buildCategories, buildDatasetsAndSeries } from './utils'
import type { DataProps } from './utils'
import { LineChart } from 'echarts/charts';
echarts.use([LineChart]);
interface Props extends DataProps {
label?: string;
inGrid?: boolean;
isArea?: boolean;
chartName?: string;
onClick?: (event: any) => void;
}
function ORLineChart(props: Props) {
const chartRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (!chartRef.current) return;
const chart = echarts.init(chartRef.current);
const categories = buildCategories(props.data);
const { datasets, series } = buildDatasetsAndSeries(props);
// Create a quick map of name => dataIndex => value, for partner lookups
// and a map for colors. We'll store them on window in this example for brevity.
(window as any).__seriesValueMap = {};
(window as any).__seriesColorMap = {};
(window as any).__timestampMap = props.data.chart.map(item => item.timestamp);
(window as any).__categoryMap = categories;
series.forEach((s: any) => {
if (props.isArea) {
s.areaStyle = {};
s.stack = 'Total'
// s.emphasis = { focus: 'series' };
} else {
s.areaStyle = null;
}
(window as any).__seriesColorMap[s.name] = s.itemStyle?.color ?? '#999';
const datasetId = s.datasetId || 'current';
const ds = datasets.find((d) => d.id === datasetId);
if (!ds) return;
const yDim = s.encode.y;
const yDimIndex = ds.dimensions.indexOf(yDim);
if (yDimIndex < 0) return;
(window as any).__seriesValueMap[s.name] = {};
ds.source.forEach((row: any[]) => {
const rowIdx = row[0];
(window as any).__seriesValueMap[s.name][rowIdx] = row[yDimIndex];
});
});
chart.setOption({
...defaultOptions,
title: {
text: props.chartName ?? "Line Chart",
show: false,
},
legend: {
...defaultOptions.legend,
// Only show legend for “current” series
data: series.filter((s: any) => !s._hideInLegend).map((s: any) => s.name),
},
xAxis: {
type: 'category',
boundaryGap: false,
data: categories,
},
yAxis: {
name: props.label ?? 'Number of Sessions',
nameLocation: 'middle',
nameGap: 35,
},
tooltip: {
...defaultOptions.tooltip,
formatter: customTooltipFormatter,
},
dataset: datasets,
series,
});
chart.on('click', (event) => {
const index = event.dataIndex;
const timestamp = (window as any).__timestampMap?.[index];
props.onClick?.({ activePayload: [{ payload: { timestamp }}]})
})
return () => {
chart.dispose();
delete (window as any).__seriesValueMap;
delete (window as any).__seriesColorMap;
delete (window as any).__categoryMap;
delete (window as any).__timestampMap;
};
}, [props.data, props.compData]);
return <div ref={chartRef} style={{ width: '100%', height: 240 }} />;
}
export default ORLineChart;

View file

@ -0,0 +1,119 @@
import React, { useEffect, useRef } from 'react';
import { PieChart as EchartsPieChart } from 'echarts/charts';
import { echarts, defaultOptions } from './init';
import { buildPieData, pieTooltipFormatter, pickColorByIndex } from './pieUtils';
echarts.use([EchartsPieChart]);
interface DataItem {
time: string;
timestamp: number;
[seriesName: string]: number | string;
}
interface PieChartProps {
data: {
chart: DataItem[];
namesMap: string[];
};
label?: string;
inGrid?: boolean;
onClick?: (filters: any[]) => void;
}
function PieChart(props: PieChartProps) {
const { data, label, onClick = () => {}, inGrid = false } = props;
const chartRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!chartRef.current) return;
if (!data.chart || data.chart.length === 0) {
chartRef.current.innerHTML = `<div style="text-align:center;padding:20px;">No data available</div>`;
return;
}
const chartInstance = echarts.init(chartRef.current);
const pieData = buildPieData(data.chart, data.namesMap);
if (!pieData.length) {
chartRef.current.innerHTML = `<div style="text-align:center;padding:20px;">No data available</div>`;
return;
}
const largestSlice = pieData.reduce((acc, curr) =>
curr.value > acc.value ? curr : acc
);
const largestVal = largestSlice.value || 1; // avoid divide-by-zero
const option = {
...defaultOptions,
tooltip: {
...defaultOptions.tooltip,
trigger: 'item',
formatter: pieTooltipFormatter,
},
grid: {
top: 10,
bottom: 10,
left: 10,
right: 10,
},
legend: {
...defaultOptions.legend,
type: 'plain',
show: true,
top: inGrid ? undefined : 0,
},
series: [
{
type: 'pie',
name: label ?? 'Data',
radius: [50, 100],
center: ['50%', '55%'],
data: pieData.map((d, idx) => {
return {
name: d.name,
value: d.value,
label: {
show: d.value / largestVal >= 0.03,
position: 'outside',
formatter: (params: any) => {
return params.value;
},
},
labelLine: {
show: d.value / largestVal >= 0.03,
length: 10,
length2: 20,
lineStyle: { color: '#3EAAAF' },
},
itemStyle: {
color: pickColorByIndex(idx),
},
};
}),
emphasis: {
scale: true,
scaleSize: 4,
},
},
],
};
chartInstance.setOption(option);
chartInstance.on('click', function (params) {
onClick([{ name: params.name, value: params.value }]);
});
return () => {
chartInstance.dispose();
};
}, [data, label, onClick, inGrid]);
return (
<div style={{ width: '100%', height: 240, position: 'relative' }} ref={chartRef} />
);
}
export default PieChart;

View file

@ -0,0 +1,52 @@
import type { DataProps } from './utils';
import { createDataset, assignColorsByBaseName } from './utils';
export function createBarSeries(
data: DataProps['data'],
datasetId: string,
dashed: boolean,
hideFromLegend: boolean,
horizontal: boolean
) {
return data.namesMap.filter(Boolean).map((fullName) => {
const baseName = fullName.replace(/^Previous\s+/, '');
const encode = horizontal
? { x: fullName, y: 'idx' }
: { x: 'idx', y: fullName };
const borderRadius = horizontal ? [0, 6, 6, 0] : [6, 6, 0, 0];
const decal = dashed ? { symbol: 'line', symbolSize: 10, rotation: 1 } : { symbol: 'none' };
return {
name: fullName,
_baseName: baseName,
type: 'bar',
datasetId,
animation: false,
encode,
showSymbol: false,
itemStyle: { borderRadius, decal },
_hideInLegend: hideFromLegend,
};
});
}
export function buildBarDatasetsAndSeries(props: DataProps, horizontal = false) {
const mainDataset = createDataset('current', props.data);
const mainSeries = createBarSeries(props.data, 'current', false, false, horizontal);
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 = createBarSeries(props.compData, 'previous', true, true, horizontal);
}
const datasets = compDataset ? [mainDataset, compDataset] : [mainDataset];
const series = [...mainSeries, ...compSeries];
assignColorsByBaseName(series);
return { datasets, series };
}

View file

@ -0,0 +1,69 @@
import * as echarts from 'echarts/core';
import {
DatasetComponent,
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent,
// TransformComponent,
ToolboxComponent,
} from 'echarts/components';
import { SVGRenderer } from 'echarts/renderers';
echarts.use([
DatasetComponent,
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent,
// TransformComponent,
SVGRenderer,
ToolboxComponent
]);
const defaultOptions = {
aria: {
enabled: true,
decal: {
show: true,
},
},
tooltip: {
trigger: 'item',
backgroundColor: 'transparent',
borderWidth: 0,
padding: 0,
extraCssText: 'box-shadow: none; pointer-events: auto;',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
},
}
},
grid: {
bottom: 20,
top: 40,
left: 55,
right: 15,
containLabel: true,
},
toolbox: {
show: true,
right: 10,
top: 10,
feature: {
saveAsImage: {
pixelRatio: 1.5,
}
}
},
legend: {
type: 'plain',
show: true,
top: 10,
icon: 'pin'
},
}
export { echarts, defaultOptions };

View file

@ -0,0 +1,31 @@
import { colors } from './utils';
import { numberWithCommas } from 'App/utils';
export function buildPieData(
chart: Array<Record<string, any>>,
namesMap: string[]
) {
const result: { name: string; value: number }[] = namesMap.map((name) => {
let sum = 0;
chart.forEach((row) => {
sum += Number(row[name] ?? 0);
});
return { name, value: sum };
});
return result;
}
export function pieTooltipFormatter(params: any) {
const { name, value, marker, percent } = params;
return `
<div class="flex flex-col gap-1 bg-white shadow border rounded p-2 z-50">
<div style="margin-bottom: 2px;">${marker} <b>${name}</b></div>
<div>${numberWithCommas(value)} (${percent}%)</div>
</div>
`;
}
export function pickColorByIndex(idx: number) {
return colors[idx % colors.length];
}

View file

@ -0,0 +1,246 @@
import { formatTimeOrDate } from "App/date";
export const colors = ['#6774E2', '#929ACD', '#3EAAAF', '#565D97', '#8F9F9F', '#376F72'];
// const colorsTeal = ['#1E889A', '#239DB2', '#28B2C9', '#36C0D7', '#65CFE1'];
// const colorsx = ['#256669', '#38999e', '#3eaaaf', '#51b3b7', '#78c4c7', '#9fd5d7', '#c5e6e7'].reverse();
// const compareColors = ['#192EDB', '#6272FF', '#808DFF', '#B3BBFF', '#C9CFFF'];
// const compareColorsx = ["#222F99", "#2E3ECC", "#394EFF", "#6171FF", "#8895FF", "#B0B8FF", "#D7DCFF"].reverse();
// const customMetricColors = ['#394EFF', '#3EAAAF', '#565D97'];
// const colorsPie = colors.concat(["#DDDDDD"]);
// const safeColors = ['#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++;
}
}
});
// Then apply color to each series
series.forEach((s) => {
const baseName = s._baseName || s.name;
const color = colorMap[baseName];
s.itemStyle = { ...s.itemStyle, color };
s.lineStyle = { ...(s.lineStyle || {}), color };
});
}
/**
* Show the hovered current or previous line + the matching partner (if it exists).
*/
export function customTooltipFormatter(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;
// 'value' of the hovered point
const yKey = params.encode.y[0]; // "Series 1"
const value = params.data?.[yKey];
const isPrevious = /^Previous\s+/.test(seriesName);
const baseName = seriesName.replace(/^Previous\s+/, '');
const partnerName = isPrevious ? baseName : `Previous ${baseName}`;
// Get partners value from some global map
const partnerVal = (window as any).__seriesValueMap?.[partnerName]?.[dataIndex];
const timestamp = (window as any).__timestampMap?.[dataIndex];
const categoryLabel = (window as any).__categoryMap
? (window as any).__categoryMap[dataIndex]
: dataIndex;
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">
${isPrevious ? '' : timestamp ? formatTimeOrDate(timestamp) : 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?.[partnerName] || '#999';
tooltipContent += `
<div class="flex gap-2 items-center mt-2">
<div style="
border-radius: 99px;
background: ${partnerColor};
width: 1rem;
height: 1rem;">
</div>
<div class="font-medium">${partnerName}</div>
</div>
<div style="border-left: 2px dashed ${partnerColor};" class="flex flex-col px-2 ml-2">
<div class="text-neutral-600 text-sm">
${!isPrevious ? '' : timestamp ? formatTimeOrDate(timestamp) : categoryLabel}
</div>
<div class="flex items-center gap-1">
<div class="font-medium">${partnerVal ?? '—'}</div>
${buildCompareTag(partnerVal, value)}
</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) : '∞';
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}%)</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: true,
symbolSize: 9,
symbol: 'circle',
// custom flag to hide prev data from legend
_hideInLegend: hideFromLegend,
};
});
}
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 };
}
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[];
};
}

View file

@ -1,209 +0,0 @@
import React, { useState } from 'react';
import CustomTooltip from "./CustomChartTooltip";
import { Styles } from '../common';
import {
ResponsiveContainer,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
BarChart,
Bar,
Legend,
} from 'recharts';
interface Props {
data: { chart: any[], namesMap: string[] };
compData: { chart: any[], namesMap: string[] } | null;
params: any;
colors: any;
onClick?: (event, index) => void;
yaxis?: any;
label?: string;
hideLegend?: boolean;
inGrid?: boolean;
}
const getPath = (x, y, width, height) => {
const radius = 4;
return `
M${x + radius},${y}
H${x + width - radius}
Q${x + width},${y} ${x + width},${y + radius}
V${y + height}
H${x}
V${y + radius}
Q${x},${y} ${x + radius},${y}
Z
`;
};
const PillBar = (props) => {
const { fill, x, y, width, height, striped } = props;
return (
<g transform={`translate(${x}, ${y})`}>
<path
d={getPath(0, 0, width, height)}
fill={fill}
/>
{striped && (
<path
d={getPath(0, 0, width, height)}
clipPath="url(#pillClip)"
fill="url(#diagonalStripes)"
/>
)}
</g>
);
};
function CustomBarChart(props: Props) {
const {
data = { chart: [], namesMap: [] },
compData = { chart: [], namesMap: [] },
params,
colors,
onClick = () => null,
yaxis = { ...Styles.yaxis },
label = 'Number of Sessions',
hideLegend = false,
inGrid,
} = props;
const [hoveredSeries, setHoveredSeries] = useState<string | null>(null);
const handleMouseOver = (key) => () => {
setHoveredSeries(key);
};
const handleMouseLeave = () => {
setHoveredSeries(null);
};
const resultChart = data.chart.map((item, i) => {
if (compData && compData.chart[i]) {
const comparisonItem: Record<string, any> = {};
Object.keys(compData.chart[i]).forEach(key => {
if (key !== 'time') {
comparisonItem[`${key}_comparison`] = (compData.chart[i] as any)[key];
}
});
return { ...item, ...comparisonItem };
}
return item;
});
const mergedNameMap: { data: any, isComp: boolean, index: number }[] = [];
for (let i = 0; i < data.namesMap.length; i++) {
mergedNameMap.push({ data: data.namesMap[i], isComp: false, index: i });
if (compData && compData.namesMap[i]) {
mergedNameMap.push({ data: compData.namesMap[i], isComp: true, index: i });
}
}
const legendItems = mergedNameMap.filter(item => !item.isComp);
return (
<ResponsiveContainer height={240} width="100%">
<BarChart
data={resultChart}
margin={Styles.chartMargins}
onClick={onClick}
onMouseLeave={handleMouseLeave}
barSize={10}
style={{ backgroundColor: 'transparent' }}
>
<defs>
<clipPath id="pillClip">
<rect x="0" y="0" width="100%" height="100%" rx="4" ry="4" />
</clipPath>
<pattern
id="diagonalStripes"
patternUnits="userSpaceOnUse"
width="4"
height="4"
patternTransform="rotate(45)"
>
<line
x1="0"
y1="0"
x2="0"
y2="4"
stroke="#FFFFFF"
strokeWidth="2"
/>
</pattern>
</defs>
{!hideLegend && (
<Legend
iconType="rect"
wrapperStyle={{ top: inGrid ? undefined : -18 }}
payload={legendItems.map(item => ({
value: item.data,
type: 'rect',
color: colors[item.index],
id: item.data
}))}
/>
)}
<CartesianGrid
strokeDasharray="1 3"
vertical={false}
stroke="rgba(0,0,0,.15)"
/>
<XAxis
{...Styles.xaxis}
dataKey="time"
interval={'equidistantPreserveStart'}
/>
<YAxis
{...yaxis}
allowDecimals={false}
tickFormatter={(val) => Styles.tickFormatter(val)}
label={{
...Styles.axisLabelLeft,
value: label || 'Number of Sessions',
}}
/>
<Tooltip {...Styles.tooltip} content={<CustomTooltip hoveredSeries={hoveredSeries} />} />
{mergedNameMap.map((item) => (
<Bar
key={item.data}
name={item.isComp ? `${item.data} (Comparison)` : item.data}
type="monotone"
dataKey={item.isComp ? `${item.data}_comparison` : item.data}
fill={colors[item.index]}
stroke={colors[item.index]}
onMouseOver={handleMouseOver(item.isComp ? `${item.data} (Comparison)` : item.data)}
shape={(barProps: any) => (
<PillBar
{...barProps}
fill={colors[item.index]}
barKey={item.index}
stroke={colors[item.index]}
striped={item.isComp}
/>
)}
fillOpacity={
hoveredSeries &&
hoveredSeries !== item.data &&
hoveredSeries !== `${item.data} (Comparison)` ? 0.2 : 1
}
legendType="rect"
activeBar={
<PillBar
fill={colors[item.index]}
stroke={colors[item.index]}
barKey={item.index}
striped={item.isComp}
/>
}
/>
))}
</BarChart>
</ResponsiveContainer>
);
}
export default CustomBarChart;

View file

@ -1,174 +0,0 @@
import React, { useState } from 'react';
import CustomTooltip from "../CustomChartTooltip";
import { Styles } from '../../common';
import {
ResponsiveContainer,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
LineChart,
Line,
Legend,
} from 'recharts';
import { observer } from 'mobx-react-lite';
interface Props {
data: any;
compData: any | null;
params: any;
colors: any;
onClick?: (event, index) => void;
yaxis?: any;
label?: string;
hideLegend?: boolean;
inGrid?: boolean;
}
function CustomMetricLineChart(props: Props) {
const {
data = { chart: [], namesMap: [] },
compData = { chart: [], namesMap: [] },
colors,
onClick = () => null,
yaxis = { ...Styles.yaxis },
label = 'Number of Sessions',
hideLegend = false,
inGrid,
} = props;
const [hoveredSeries, setHoveredSeries] = useState<string | null>(null);
const handleMouseOver = (key) => () => {
setHoveredSeries(key);
};
const handleMouseLeave = () => {
setHoveredSeries(null);
};
// const resultChart = data.chart.map((item, i) => {
// if (compData && compData.chart[i]) return { ...compData.chart[i], ...item };
// return item;
// });
const resultChart = data.chart.map((item, i) => {
if (compData && compData.chart[i]) {
const comparisonItem: Record<string, any> = {};
Object.keys(compData.chart[i]).forEach(key => {
if (key !== 'time') {
comparisonItem[`${key}_comparison`] = (compData.chart[i] as any)[key];
}
});
return { ...item, ...comparisonItem };
}
return item;
});
return (
<ResponsiveContainer height={240} width="100%">
<LineChart
data={resultChart}
margin={Styles.chartMargins}
onClick={onClick}
onMouseLeave={handleMouseLeave}
>
{!hideLegend && (
<Legend wrapperStyle={{ top: inGrid ? undefined : -18 }}
payload={
(data.namesMap as string[]).map((key: string, index: number) => ({
value: key,
type: 'line',
color: colors[index],
id: key,
}))
}
/>
)}
<CartesianGrid strokeDasharray="1 3" vertical={false} stroke="rgba(0,0,0,.15)" />
<XAxis {...Styles.xaxis} dataKey="time" interval={'equidistantPreserveStart'} />
<YAxis
{...yaxis}
allowDecimals={false}
tickFormatter={(val) => Styles.tickFormatter(val)}
label={{ ...Styles.axisLabelLeft, value: label || 'Number of Sessions' }}
/>
<Tooltip {...Styles.tooltip} content={<CustomTooltip hoveredSeries={hoveredSeries} />} />
{Array.isArray(data.namesMap) &&
data.namesMap.map((key, index) =>
key ? (
<Line
key={key}
name={key}
type="linear"
dataKey={key}
stroke={colors[index]}
fillOpacity={1}
strokeWidth={2}
strokeOpacity={
hoveredSeries && hoveredSeries !== key && hoveredSeries !== `${key} (Comparison)` ? 0.2 : 1
}
legendType={key === 'Total' ? 'none' : 'line'}
dot={false}
activeDot={
hoveredSeries === key
? {
r: 8,
stroke: '#fff',
strokeWidth: 2,
fill: colors[index],
filter: 'drop-shadow(0px 0px 1px rgba(0, 0, 0, 0.2))',
}
: false
}
onMouseOver={handleMouseOver(key)}
style={{ cursor: 'pointer' }}
animationDuration={200}
animationEasing="ease-in-out"
/>
) : null
)}
{compData?.namesMap?.map((key, i) =>
data.namesMap[i] ? (
<Line
key={key}
name={`${key} (Comparison)`}
type="linear"
dataKey={`${key}_comparison`}
stroke={colors[i]}
fillOpacity={1}
strokeWidth={2}
strokeOpacity={
hoveredSeries && hoveredSeries !== key && hoveredSeries !== `${key} (Comparison)` ? 0.2 : 1
}
legendType="line"
dot={false}
strokeDasharray="4 2"
onMouseOver={handleMouseOver(`${key} (Comparison)`)}
activeDot={
hoveredSeries === `${key} (Comparison)`
? {
r: 8,
stroke: '#fff',
strokeWidth: 2,
fill: colors[i],
filter: 'drop-shadow(0px 0px 0px rgba(0, 0, 0, 0.2))',
}
: false
}
style={{ cursor: 'pointer' }}
animationDuration={1000}
animationEasing="ease-in-out"
/>
) : null
)}
</LineChart>
</ResponsiveContainer>
);
}
export default observer(CustomMetricLineChart);

View file

@ -1 +0,0 @@
export { default } from './CustomMetricLineChart';

View file

@ -1,8 +1,7 @@
import React from 'react';
import ExCard from './ExCard';
import AreaChartCard from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/AreaChartCard";
import CustomMetricLineChart from "Components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricLineChart";
import LineChart from 'App/components/Charts/LineChart'
import {Styles} from "Components/Dashboard/Widgets/common";
interface Props {
@ -24,7 +23,7 @@ function ExampleTrend(props: Props) {
}
>
{/*<AreaChartCard data={props.data} label={props.data?.label}/>*/}
<CustomMetricLineChart
<LineChart
data={props.data}
colors={Styles.compareColors}
params={{

View file

@ -1,14 +1,13 @@
import React, { useState, useRef, useEffect } from 'react';
import CustomMetricLineChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricLineChart';
import LineChart from 'App/components/Charts/LineChart'
import BarChart from 'App/components/Charts/BarChart'
import PieChart from 'App/components/Charts/PieChart'
import CustomMetricPercentage from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage';
import CustomMetricPieChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart';
import { Styles } from 'App/components/Dashboard/Widgets/common';
import { observer } from 'mobx-react-lite';
import { Icon, Loader } from 'UI';
import { useStore } from 'App/mstore';
import FunnelTable from "../../../Funnels/FunnelWidget/FunnelTable";
import AreaChart from '../../Widgets/CustomMetricsWidgets/AreaChart';
import BarChart from '../../Widgets/CustomMetricsWidgets/BarChart';
import ProgressBarChart from '../../Widgets/CustomMetricsWidgets/ProgressBarChart';
import BugNumChart from '../../Widgets/CustomMetricsWidgets/BigNumChart';
import WidgetDatatable from '../WidgetDatatable/WidgetDatatable';
@ -283,12 +282,11 @@ function WidgetChart(props: Props) {
if (viewType === 'lineChart') {
return (
<div className='pt-3'>
<CustomMetricLineChart
<LineChart
chartName={_metric.name}
inGrid={!props.isPreview}
data={chartData}
compData={compData}
colors={colors}
params={params}
onClick={onChartClick}
label={
_metric.metricOf === 'sessionCount'
@ -302,11 +300,11 @@ function WidgetChart(props: Props) {
if (viewType === 'areaChart') {
return (
<div className='pt-3'>
<AreaChart
<LineChart
isArea
chartName={_metric.name}
data={chartData}
inGrid={!props.isPreview}
params={params}
colors={colors}
onClick={onChartClick}
label={
_metric.metricOf === 'sessionCount'
@ -357,11 +355,9 @@ function WidgetChart(props: Props) {
if (viewType === 'pieChart') {
return (
<div className='pt-3'>
<CustomMetricPieChart
<PieChart
inGrid={!props.isPreview}
metric={_metric}
data={chartData}
colors={colors}
onClick={onChartClick}
label={
_metric.metricOf === 'sessionCount'
@ -497,7 +493,7 @@ function WidgetChart(props: Props) {
if (metricType === RETENTION) {
if (viewType === 'trend') {
return (
<CustomMetricLineChart
<LineChart
data={data}
colors={colors}
params={params}

View file

@ -136,7 +136,7 @@ const SeriesTypeOptions = observer(({ metric }: { metric: any }) => {
const WidgetViewTypeOptions = observer(({ metric }: { metric: any }) => {
const chartTypes = {
lineChart: 'Line',
areaChart: 'Area',
areaChart: 'Stacked Area',
barChart: 'Column',
progressChart: 'Vertical Bar',
columnChart: 'Horizontal Bar',

View file

@ -35,6 +35,7 @@
"classnames": "^2.3.1",
"copy-to-clipboard": "^3.3.1",
"country-flag-icons": "^1.5.7",
"echarts": "^5.6.0",
"fflate": "^0.8.2",
"fzstd": "^0.1.1",
"hls.js": "^1.5.13",

View file

@ -6838,6 +6838,16 @@ __metadata:
languageName: node
linkType: hard
"echarts@npm:^5.6.0":
version: 5.6.0
resolution: "echarts@npm:5.6.0"
dependencies:
tslib: "npm:2.3.0"
zrender: "npm:5.6.1"
checksum: 10c1/0695d5951f0cfccfb54a0e223a2c23313bd789aa7529caa1b6d8f8b48cc24f480aa6dcc7fecbd9cef260492b5806470a0a0dafa2305a68d0326c4b6a0be0e3ba
languageName: node
linkType: hard
"ee-first@npm:1.1.1":
version: 1.1.1
resolution: "ee-first@npm:1.1.1"
@ -11607,6 +11617,7 @@ __metadata:
cypress: "npm:13.17.0"
cypress-image-snapshot: "npm:^4.0.1"
dotenv: "npm:^6.2.0"
echarts: "npm:^5.6.0"
esbuild-loader: "npm:^4.2.2"
eslint: "npm:^8.15.0"
eslint-plugin-react: "npm:^7.29.4"
@ -15762,6 +15773,13 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:2.3.0":
version: 2.3.0
resolution: "tslib@npm:2.3.0"
checksum: 10c1/851bc4c307068b0a65c428e0dab91abb30b8c145fa249d7204281efd1146edd170c8152780347270dc550c2eaf0e47743e3c3fabcf4f66180457471e6e66179e
languageName: node
linkType: hard
"tslib@npm:^1.8.1, tslib@npm:^1.9.3":
version: 1.14.1
resolution: "tslib@npm:1.14.1"
@ -16876,3 +16894,12 @@ __metadata:
checksum: 10c1/3c46904537cf83a416bb8373a2e6c1cac4568528888b605a6f8a319c4a84ae4ac368b31f0a54df59d3e33efd7aef2d2499ca8210d329e18028a1d039e1bb5722
languageName: node
linkType: hard
"zrender@npm:5.6.1":
version: 5.6.1
resolution: "zrender@npm:5.6.1"
dependencies:
tslib: "npm:2.3.0"
checksum: 10c1/593dd84b3ea01f4e7941fd15f103550251b769218f4d524af935595bb628fa6e94e4c091654ec390e1c7176dd60ca8a7c7341d372d310ee3beebfe602792c1c6
languageName: node
linkType: hard

View file

@ -3,123 +3,124 @@
Below is the list of dependencies used in OpenReplay software. Licenses may change between versions, so please keep this
up to date with every new library you use.
| Library | License | Scope |
|----------------------------|------------------|------------------|
| btcutil | IST | Go |
| confluent-kafka-go | Apache2 | Go |
| compress | Apache2 | Go |
| uuid | BSD3 | Go |
| mux | BSD3 | Go |
| lib/pq | MIT | Go |
| pgconn | MIT | Go |
| pgx | MIT | Go |
| go-redis | BSD2 | Go |
| pgerrcode | MIT | Go |
| pgzip | MIT | Go |
| maxminddb-golang | IST | Go |
| realip | MIT | Go |
| uap-go | Apache2 | Go |
| clickhouse-go | MIT | Go |
| aws-sdk-go | Apache2 | Go |
| logging | Apache2 | Go |
| squirrel | MIT | Go |
| go-elasticsearch | Apache2 | Go |
| gorilla/websocket | BSD2 | Go |
| radix | MIT | Go |
| api | BSD3 | Go |
| urllib3 | MIT | Python |
| boto3 | Apache2 | Python |
| requests | Apache2 | Python |
| pyjwt | MIT | Python |
| jsbeautifier | MIT | Python |
| psycopg2-binary | LGPL | Python |
| fastapi | MIT | Python |
| uvicorn | BSD | Python |
| python-decouple | MIT | Python |
| pydantic | MIT | Python |
| apscheduler | MIT | Python |
| python-multipart | Apache | Python |
| elasticsearch-py | Apache2 | Python |
| jira | BSD2 | Python |
| redis-py | MIT | Python |
| clickhouse-driver | MIT | Python |
| python3-saml | MIT | Python |
| kubernetes | Apache2 | Python |
| chalice | Apache2 | Python |
| pandas | BSD3 | Python |
| numpy | BSD3 | Python |
| scikit-learn | BSD3 | Python |
| apache-airflow | Apache2 | Python |
| airflow-code-editor | Apache2 | Python |
| mlflow | Apache2 | Python |
| sqlalchemy | MIT | Python |
| pandas-redshift | MIT | Python |
| confluent-kafka | Apache2 | Python |
| cachetools | MIT | Python |
| amplitude-js | MIT | JavaScript |
| classnames | MIT | JavaScript |
| codemirror | MIT | JavaScript |
| copy-to-clipboard | MIT | JavaScript |
| jsonwebtoken | MIT | JavaScript |
| datamaps | MIT | JavaScript |
| microdiff | MIT | JavaScript |
| immutable | MIT | JavaScript |
| jshint | MIT | JavaScript |
| luxon | MIT | JavaScript |
| mobx | MIT | JavaScript |
| mobx-react-lite | MIT | JavaScript |
| optimal-select | MIT | JavaScript |
| rc-time-picker | MIT | JavaScript |
| snabbdom | MIT | JavaScript |
| react | MIT | JavaScript |
| react-codemirror2 | MIT | JavaScript |
| react-confirm | MIT | JavaScript |
| react-datepicker | MIT | JavaScript |
| react-daterange-picker | Apache2 | JavaScript |
| react-dnd | MIT | JavaScript |
| react-dnd-html5-backend | MIT | JavaScript |
| react-dom | MIT | JavaScript |
| react-google-recaptcha | MIT | JavaScript |
| react-json-view | MIT | JavaScript |
| react-redux | MIT | JavaScript |
| react-router | MIT | JavaScript |
| react-router-dom | MIT | JavaScript |
| react-stripe-elements | MIT | JavaScript |
| react-toastify | MIT | JavaScript |
| recharts | MIT | JavaScript |
| redux | MIT | JavaScript |
| redux-immutable | BSD3 | JavaScript |
| redux-thunk | MIT | JavaScript |
| socket.io | MIT | JavaScript |
| socket.io-client | MIT | JavaScript |
| uWebSockets.js | Apache2 | JavaScript |
| aws-sdk | Apache2 | JavaScript |
| serverless | MIT | JavaScript |
| peerjs | MIT | JavaScript |
| geoip-lite | Apache2 | JavaScript |
| ua-parser-js | MIT | JavaScript |
| express | MIT | JavaScript |
| jspdf | MIT | JavaScript |
| html-to-image | MIT | JavaScript |
| kafka | Apache2 | Infrastructure |
| stern | Apache2 | Infrastructure |
| k9s | Apache2 | Infrastructure |
| minio | AGPLv3 | Infrastructure |
| postgreSQL | PostgreSQL License | Infrastructure |
| k3s | Apache2 | Infrastructure |
| nginx | BSD2 | Infrastructure |
| clickhouse | Apache2 | Infrastructure |
| redis | BSD3 | Infrastructure |
| yq | MIT | Infrastructure |
| html2canvas | MIT | JavaScript |
| eget | MIT | Infrastructure |
| @medv/finder | MIT | JavaScript |
| fflate | MIT | JavaScript |
| fzstd | MIT | JavaScript |
| prom-client | Apache2 | JavaScript |
| winston | MIT | JavaScript |
| @wojtekmaj/react-daterange-picker | MIT | JavaScript |
| prismjs | MIT | JavaScript |
| virtua | MIT | JavaScript |
| babel-plugin-prismjs | MIT | JavaScript |
| react-intersection-observer | MIT | JavaScript |
| Library | License | Scope |
|-----------------------------------|--------------------|-----------------|
| btcutil | IST | Go |
| confluent-kafka-go | Apache2 | Go |
| compress | Apache2 | Go |
| uuid | BSD3 | Go |
| mux | BSD3 | Go |
| lib/pq | MIT | Go |
| pgconn | MIT | Go |
| pgx | MIT | Go |
| go-redis | BSD2 | Go |
| pgerrcode | MIT | Go |
| pgzip | MIT | Go |
| maxminddb-golang | IST | Go |
| realip | MIT | Go |
| uap-go | Apache2 | Go |
| clickhouse-go | MIT | Go |
| aws-sdk-go | Apache2 | Go |
| logging | Apache2 | Go |
| squirrel | MIT | Go |
| go-elasticsearch | Apache2 | Go |
| gorilla/websocket | BSD2 | Go |
| radix | MIT | Go |
| api | BSD3 | Go |
| urllib3 | MIT | Python |
| boto3 | Apache2 | Python |
| requests | Apache2 | Python |
| pyjwt | MIT | Python |
| jsbeautifier | MIT | Python |
| psycopg2-binary | LGPL | Python |
| fastapi | MIT | Python |
| uvicorn | BSD | Python |
| python-decouple | MIT | Python |
| pydantic | MIT | Python |
| apscheduler | MIT | Python |
| python-multipart | Apache | Python |
| elasticsearch-py | Apache2 | Python |
| jira | BSD2 | Python |
| redis-py | MIT | Python |
| clickhouse-driver | MIT | Python |
| python3-saml | MIT | Python |
| kubernetes | Apache2 | Python |
| chalice | Apache2 | Python |
| pandas | BSD3 | Python |
| numpy | BSD3 | Python |
| scikit-learn | BSD3 | Python |
| apache-airflow | Apache2 | Python |
| airflow-code-editor | Apache2 | Python |
| mlflow | Apache2 | Python |
| sqlalchemy | MIT | Python |
| pandas-redshift | MIT | Python |
| confluent-kafka | Apache2 | Python |
| cachetools | MIT | Python |
| amplitude-js | MIT | JavaScript |
| classnames | MIT | JavaScript |
| codemirror | MIT | JavaScript |
| copy-to-clipboard | MIT | JavaScript |
| jsonwebtoken | MIT | JavaScript |
| datamaps | MIT | JavaScript |
| microdiff | MIT | JavaScript |
| immutable | MIT | JavaScript |
| jshint | MIT | JavaScript |
| luxon | MIT | JavaScript |
| mobx | MIT | JavaScript |
| mobx-react-lite | MIT | JavaScript |
| optimal-select | MIT | JavaScript |
| rc-time-picker | MIT | JavaScript |
| snabbdom | MIT | JavaScript |
| react | MIT | JavaScript |
| react-codemirror2 | MIT | JavaScript |
| react-confirm | MIT | JavaScript |
| react-datepicker | MIT | JavaScript |
| react-daterange-picker | Apache2 | JavaScript |
| react-dnd | MIT | JavaScript |
| react-dnd-html5-backend | MIT | JavaScript |
| react-dom | MIT | JavaScript |
| react-google-recaptcha | MIT | JavaScript |
| react-json-view | MIT | JavaScript |
| react-redux | MIT | JavaScript |
| react-router | MIT | JavaScript |
| react-router-dom | MIT | JavaScript |
| react-stripe-elements | MIT | JavaScript |
| react-toastify | MIT | JavaScript |
| recharts | MIT | JavaScript |
| redux | MIT | JavaScript |
| redux-immutable | BSD3 | JavaScript |
| redux-thunk | MIT | JavaScript |
| socket.io | MIT | JavaScript |
| socket.io-client | MIT | JavaScript |
| uWebSockets.js | Apache2 | JavaScript |
| aws-sdk | Apache2 | JavaScript |
| serverless | MIT | JavaScript |
| peerjs | MIT | JavaScript |
| geoip-lite | Apache2 | JavaScript |
| ua-parser-js | MIT | JavaScript |
| express | MIT | JavaScript |
| jspdf | MIT | JavaScript |
| html-to-image | MIT | JavaScript |
| kafka | Apache2 | Infrastructure |
| stern | Apache2 | Infrastructure |
| k9s | Apache2 | Infrastructure |
| minio | AGPLv3 | Infrastructure |
| postgreSQL | PostgreSQL License | Infrastructure |
| k3s | Apache2 | Infrastructure |
| nginx | BSD2 | Infrastructure |
| clickhouse | Apache2 | Infrastructure |
| redis | BSD3 | Infrastructure |
| yq | MIT | Infrastructure |
| html2canvas | MIT | JavaScript |
| eget | MIT | Infrastructure |
| @medv/finder | MIT | JavaScript |
| fflate | MIT | JavaScript |
| fzstd | MIT | JavaScript |
| prom-client | Apache2 | JavaScript |
| winston | MIT | JavaScript |
| @wojtekmaj/react-daterange-picker | MIT | JavaScript |
| prismjs | MIT | JavaScript |
| virtua | MIT | JavaScript |
| babel-plugin-prismjs | MIT | JavaScript |
| react-intersection-observer | MIT | JavaScript |
| echarts | Apache2 | JavaScript |