From 251cd5a6c462d639832f91a360110cd574686baa Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Mon, 6 Jan 2025 16:42:09 +0100 Subject: [PATCH] ui: move timeseries to apache echarts --- frontend/app/components/Charts/BarChart.tsx | 88 +++++++ frontend/app/components/Charts/LineChart.tsx | 103 ++++++++ frontend/app/components/Charts/PieChart.tsx | 119 +++++++++ frontend/app/components/Charts/barUtils.ts | 52 ++++ frontend/app/components/Charts/init.ts | 69 +++++ frontend/app/components/Charts/pieUtils.ts | 31 +++ frontend/app/components/Charts/utils.ts | 246 ++++++++++++++++++ .../Widgets/CustomMetricsWidgets/BarChart.tsx | 209 --------------- .../CustomMetricLineChart.tsx | 174 ------------- .../CustomMetricLineChart/index.ts | 1 - .../NewDashModal/Examples/Trend.tsx | 5 +- .../components/WidgetChart/WidgetChart.tsx | 24 +- .../Dashboard/components/WidgetOptions.tsx | 2 +- frontend/package.json | 1 + frontend/yarn.lock | 27 ++ third-party.md | 241 ++++++++--------- 16 files changed, 870 insertions(+), 522 deletions(-) create mode 100644 frontend/app/components/Charts/BarChart.tsx create mode 100644 frontend/app/components/Charts/LineChart.tsx create mode 100644 frontend/app/components/Charts/PieChart.tsx create mode 100644 frontend/app/components/Charts/barUtils.ts create mode 100644 frontend/app/components/Charts/init.ts create mode 100644 frontend/app/components/Charts/pieUtils.ts create mode 100644 frontend/app/components/Charts/utils.ts delete mode 100644 frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/BarChart.tsx delete mode 100644 frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricLineChart/CustomMetricLineChart.tsx delete mode 100644 frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricLineChart/index.ts diff --git a/frontend/app/components/Charts/BarChart.tsx b/frontend/app/components/Charts/BarChart.tsx new file mode 100644 index 000000000..e5b98280a --- /dev/null +++ b/frontend/app/components/Charts/BarChart.tsx @@ -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(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
; +} + +export default ORBarChart; diff --git a/frontend/app/components/Charts/LineChart.tsx b/frontend/app/components/Charts/LineChart.tsx new file mode 100644 index 000000000..a90706d14 --- /dev/null +++ b/frontend/app/components/Charts/LineChart.tsx @@ -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(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
; +} + +export default ORLineChart; diff --git a/frontend/app/components/Charts/PieChart.tsx b/frontend/app/components/Charts/PieChart.tsx new file mode 100644 index 000000000..c6ce13ef6 --- /dev/null +++ b/frontend/app/components/Charts/PieChart.tsx @@ -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(null); + + useEffect(() => { + if (!chartRef.current) return; + if (!data.chart || data.chart.length === 0) { + chartRef.current.innerHTML = `
No data available
`; + return; + } + + const chartInstance = echarts.init(chartRef.current); + + const pieData = buildPieData(data.chart, data.namesMap); + if (!pieData.length) { + chartRef.current.innerHTML = `
No data available
`; + 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 ( +
+ ); +} + +export default PieChart; diff --git a/frontend/app/components/Charts/barUtils.ts b/frontend/app/components/Charts/barUtils.ts new file mode 100644 index 000000000..70eec9cae --- /dev/null +++ b/frontend/app/components/Charts/barUtils.ts @@ -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 | null = null; + let compSeries: Record[] = []; + 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 }; +} + diff --git a/frontend/app/components/Charts/init.ts b/frontend/app/components/Charts/init.ts new file mode 100644 index 000000000..37549c08c --- /dev/null +++ b/frontend/app/components/Charts/init.ts @@ -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 }; \ No newline at end of file diff --git a/frontend/app/components/Charts/pieUtils.ts b/frontend/app/components/Charts/pieUtils.ts new file mode 100644 index 000000000..2c5426cf4 --- /dev/null +++ b/frontend/app/components/Charts/pieUtils.ts @@ -0,0 +1,31 @@ +import { colors } from './utils'; +import { numberWithCommas } from 'App/utils'; + + +export function buildPieData( + chart: Array>, + 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 ` +
+
${marker} ${name}
+
${numberWithCommas(value)} (${percent}%)
+
+ `; +} + +export function pickColorByIndex(idx: number) { + return colors[idx % colors.length]; +} \ No newline at end of file diff --git a/frontend/app/components/Charts/utils.ts b/frontend/app/components/Charts/utils.ts new file mode 100644 index 000000000..e69376335 --- /dev/null +++ b/frontend/app/components/Charts/utils.ts @@ -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 = {}; + 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 = ` +
+
+
+
+
${seriesName}
+
+ +
+
+ ${isPrevious ? '' : timestamp ? formatTimeOrDate(timestamp) : categoryLabel} +
+
+
${value ?? '—'}
+ ${buildCompareTag(value, partnerVal)} +
+
+ `; + + if (partnerVal !== undefined) { + const partnerColor = (window as any).__seriesColorMap?.[partnerName] || '#999'; + tooltipContent += ` +
+
+
+
${partnerName}
+
+
+
+ ${!isPrevious ? '' : timestamp ? formatTimeOrDate(timestamp) : categoryLabel} +
+
+
${partnerVal ?? '—'}
+ ${buildCompareTag(partnerVal, value)} +
+
+ `; + } + + tooltipContent += '
'; + 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 ` +
+ ${arrow} + ${absDelta} + (${ratio}%) +
+ `; +} + + +/** + * 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 | null = null; + let compSeries: Record[] = []; + 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[]; + }; +} diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/BarChart.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/BarChart.tsx deleted file mode 100644 index 498e0593d..000000000 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/BarChart.tsx +++ /dev/null @@ -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 ( - - - {striped && ( - - )} - - ); -}; - -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(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 = {}; - 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 ( - - - - - - - - - - - {!hideLegend && ( - ({ - value: item.data, - type: 'rect', - color: colors[item.index], - id: item.data - }))} - /> - )} - - - Styles.tickFormatter(val)} - label={{ - ...Styles.axisLabelLeft, - value: label || 'Number of Sessions', - }} - /> - } /> - {mergedNameMap.map((item) => ( - ( - - )} - fillOpacity={ - hoveredSeries && - hoveredSeries !== item.data && - hoveredSeries !== `${item.data} (Comparison)` ? 0.2 : 1 - } - legendType="rect" - activeBar={ - - } - /> - ))} - - - ); -} - -export default CustomBarChart; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricLineChart/CustomMetricLineChart.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricLineChart/CustomMetricLineChart.tsx deleted file mode 100644 index c460009e0..000000000 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricLineChart/CustomMetricLineChart.tsx +++ /dev/null @@ -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(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 = {}; - 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 ( - - - {!hideLegend && ( - ({ - value: key, - type: 'line', - color: colors[index], - id: key, - })) - } - /> - )} - - - Styles.tickFormatter(val)} - label={{ ...Styles.axisLabelLeft, value: label || 'Number of Sessions' }} - /> - } /> - - {Array.isArray(data.namesMap) && - data.namesMap.map((key, index) => - key ? ( - - ) : null - )} - -{compData?.namesMap?.map((key, i) => - data.namesMap[i] ? ( - - ) : null -)} - - - ); -} - -export default observer(CustomMetricLineChart); \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricLineChart/index.ts b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricLineChart/index.ts deleted file mode 100644 index b8570c250..000000000 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricLineChart/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './CustomMetricLineChart'; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Trend.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Trend.tsx index cd351cb36..7c77782d4 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Trend.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Trend.tsx @@ -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) { } > {/**/} - - - - { const WidgetViewTypeOptions = observer(({ metric }: { metric: any }) => { const chartTypes = { lineChart: 'Line', - areaChart: 'Area', + areaChart: 'Stacked Area', barChart: 'Column', progressChart: 'Vertical Bar', columnChart: 'Horizontal Bar', diff --git a/frontend/package.json b/frontend/package.json index 189140af1..32ec5327d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 173942145..21acd9eef 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -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 diff --git a/third-party.md b/third-party.md index 9eb006e3e..096446172 100644 --- a/third-party.md +++ b/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 | \ No newline at end of file +| 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 | \ No newline at end of file