ui: move timeseries to apache echarts
This commit is contained in:
parent
d3acbdcc1b
commit
251cd5a6c4
16 changed files with 870 additions and 522 deletions
88
frontend/app/components/Charts/BarChart.tsx
Normal file
88
frontend/app/components/Charts/BarChart.tsx
Normal 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;
|
||||
103
frontend/app/components/Charts/LineChart.tsx
Normal file
103
frontend/app/components/Charts/LineChart.tsx
Normal 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;
|
||||
119
frontend/app/components/Charts/PieChart.tsx
Normal file
119
frontend/app/components/Charts/PieChart.tsx
Normal 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;
|
||||
52
frontend/app/components/Charts/barUtils.ts
Normal file
52
frontend/app/components/Charts/barUtils.ts
Normal 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 };
|
||||
}
|
||||
|
||||
69
frontend/app/components/Charts/init.ts
Normal file
69
frontend/app/components/Charts/init.ts
Normal 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 };
|
||||
31
frontend/app/components/Charts/pieUtils.ts
Normal file
31
frontend/app/components/Charts/pieUtils.ts
Normal 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];
|
||||
}
|
||||
246
frontend/app/components/Charts/utils.ts
Normal file
246
frontend/app/components/Charts/utils.ts
Normal 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 partner’s 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[];
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './CustomMetricLineChart';
|
||||
|
|
@ -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={{
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
241
third-party.md
241
third-party.md
|
|
@ -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 |
|
||||
Loading…
Add table
Reference in a new issue