-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
contact events noisiness stat (#526)
- Loading branch information
1 parent
7d212a3
commit 676e6c7
Showing
37 changed files
with
1,191 additions
and
184 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file modified
BIN
+2.08 KB
(120%)
playwright/snapshots/ContactList/contactlist--few-items.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
75 changes: 75 additions & 0 deletions
75
src/Components/ContactEventStats/Components/ContactEventsChart.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import React, { useMemo, useState } from "react"; | ||
import { Bar } from "react-chartjs-2"; | ||
import { Chart as ChartJS, ChartOptions, registerables } from "chart.js"; | ||
import { | ||
EContactEventsInterval, | ||
groupEventsByInterval, | ||
IContactEvent, | ||
} from "../../../Domain/Contact"; | ||
import { getStatusColor, Status } from "../../../Domain/Status"; | ||
import { createHtmlLegendPlugin } from "./htmlLegendPlugin"; | ||
import { Select } from "@skbkontur/react-ui/components/Select"; | ||
import zoomPlugin from "chartjs-plugin-zoom"; | ||
import { getContactEventsChartOptions } from "../../../helpers/getChartOptions"; | ||
|
||
ChartJS.register(...registerables); | ||
|
||
interface IContactEventsBarChartProps { | ||
events: IContactEvent[]; | ||
} | ||
|
||
export const ContactEventsChart: React.FC<IContactEventsBarChartProps> = ({ events }) => { | ||
const [interval, setInterval] = useState<EContactEventsInterval>(EContactEventsInterval.hour); | ||
|
||
const groupedTransitions = useMemo(() => groupEventsByInterval(events, interval), [ | ||
events, | ||
interval, | ||
]); | ||
|
||
const labels = useMemo(() => Object.keys(groupedTransitions), [events, interval]); | ||
|
||
const transitionTypes = useMemo(() => { | ||
const types = new Set<string>(); | ||
Object.values(groupedTransitions).forEach((transitions) => | ||
Object.keys(transitions).forEach((transition) => types.add(transition)) | ||
); | ||
return types; | ||
}, [events, interval]); | ||
|
||
const datasets = useMemo(() => { | ||
return Array.from(transitionTypes).map((transition) => ({ | ||
label: transition, | ||
data: labels.map((timestamp) => groupedTransitions[timestamp][transition]), | ||
backgroundColor: getStatusColor(transition.split(" to ")[1] as Status), | ||
})); | ||
}, [events, interval]); | ||
|
||
return ( | ||
<div> | ||
<div | ||
style={{ | ||
display: "flex", | ||
justifyContent: "space-between", | ||
marginBottom: "10px", | ||
alignItems: "baseline", | ||
}} | ||
> | ||
<span style={{ fontSize: "18px" }}>Trigger transitions</span> | ||
<span> | ||
<label>Select Interval </label> | ||
<Select | ||
value={interval} | ||
onValueChange={setInterval} | ||
items={Object.values(EContactEventsInterval)} | ||
/> | ||
</span> | ||
</div> | ||
<div id="contact-events-legend-container" /> | ||
<Bar | ||
plugins={[createHtmlLegendPlugin(false), zoomPlugin]} | ||
data={{ labels, datasets }} | ||
options={getContactEventsChartOptions(interval) as ChartOptions<"bar">} | ||
/> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
.legend-list { | ||
display: flex; | ||
gap: 5px; | ||
justify-content: center; | ||
flex-wrap: wrap; | ||
margin: 0; | ||
padding: 0; | ||
max-width: 100%; | ||
} | ||
|
||
.legend-item { | ||
align-items: center; | ||
cursor: pointer; | ||
display: flex; | ||
margin-left: 10px; | ||
white-space: nowrap; | ||
} | ||
|
||
.legend-item.hidden { | ||
display: none; | ||
} | ||
|
||
.legend-item.active { | ||
opacity: 1; | ||
font-weight: bold; | ||
} | ||
|
||
.legend-box { | ||
display: inline-block; | ||
border-radius: 9999px; | ||
height: 6px; | ||
margin-right: 10px; | ||
width: 17px; | ||
} | ||
|
||
.legend-text { | ||
margin: 0; | ||
padding: 0; | ||
} | ||
|
||
.legend-link { | ||
margin-left: 5px; | ||
} | ||
|
||
.legend-link-icon { | ||
height: 16px; | ||
width: 16px; | ||
} | ||
|
||
.legend-toggle-icon { | ||
cursor: pointer; | ||
} |
54 changes: 54 additions & 0 deletions
54
src/Components/ContactEventStats/Components/TriggerEventsChart.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import "chartjs-adapter-date-fns"; | ||
import React, { useMemo } from "react"; | ||
import { Bar } from "react-chartjs-2"; | ||
import { Chart as ChartJS, ChartOptions, registerables } from "chart.js"; | ||
import { IContactEvent } from "../../../Domain/Contact"; | ||
import { getColor } from "../../Tag/Tag"; | ||
import { createHtmlLegendPlugin } from "./htmlLegendPlugin"; | ||
import { triggerEventsChartOptions } from "../../../helpers/getChartOptions"; | ||
|
||
ChartJS.register(...registerables); | ||
|
||
interface ITriggerEventsBarChartProps { | ||
events: IContactEvent[]; | ||
} | ||
|
||
export const TriggerEventsChart: React.FC<ITriggerEventsBarChartProps> = ({ events }) => { | ||
const groupedEvents = useMemo( | ||
() => | ||
events.reduce<Record<string, number>>((acc, event) => { | ||
acc[event.trigger_id] = (acc[event.trigger_id] || 0) + 1; | ||
return acc; | ||
}, {}), | ||
[events] | ||
); | ||
|
||
const sortedEvents = useMemo(() => { | ||
return Object.entries(groupedEvents).sort(([, a], [, b]) => b - a); | ||
}, [events]); | ||
|
||
const datasets = sortedEvents.map(([triggerId, count]) => ({ | ||
label: triggerId, | ||
data: [count], | ||
backgroundColor: getColor(triggerId).backgroundColor, | ||
})); | ||
|
||
const data = { | ||
labels: [""], | ||
datasets, | ||
}; | ||
|
||
return ( | ||
<> | ||
<span style={{ fontSize: "18px", marginBottom: "10px", display: "inline-block" }}> | ||
Grouped by trigger | ||
</span> | ||
<div id="trigger-events-legend-container" /> | ||
<Bar | ||
data={data} | ||
plugins={[createHtmlLegendPlugin(true)]} | ||
options={triggerEventsChartOptions as ChartOptions<"bar">} | ||
/> | ||
</> | ||
); | ||
}; |
154 changes: 154 additions & 0 deletions
154
src/Components/ContactEventStats/Components/htmlLegendPlugin.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import React from "react"; | ||
import ReactDOM from "react-dom"; | ||
import { Chart, LegendItem, Plugin } from "chart.js"; | ||
import LinkIcon from "@skbkontur/react-icons/Link"; | ||
import { Link } from "@skbkontur/react-ui/components/Link"; | ||
import ArrowUpIcon from "@skbkontur/react-icons/ArrowChevronUp"; | ||
import ArrowDownIcon from "@skbkontur/react-icons/ArrowChevronDown"; | ||
import classNames from "classnames/bind"; | ||
|
||
import styles from "./Legend.less"; | ||
|
||
const cn = classNames.bind(styles); | ||
|
||
let lastClickedIndex: number | null | undefined = null; | ||
let isExpanded = false; | ||
const maxVisibleItems = 7; | ||
|
||
const LegendItemComponent: React.FC<{ | ||
item: LegendItem; | ||
index: number; | ||
chart: Chart; | ||
showLinks: boolean; | ||
updateLegendStyles: () => void; | ||
}> = ({ item, index, chart, showLinks, updateLegendStyles }) => { | ||
if (!item.text || item.text.trim() === "") { | ||
return null; | ||
} | ||
|
||
const handleClick = () => { | ||
const isVisible = chart.isDatasetVisible(item.datasetIndex as number); | ||
if (lastClickedIndex === item.datasetIndex && isVisible) { | ||
chart.data.datasets.forEach((_, idx: number) => { | ||
chart.setDatasetVisibility(idx, true); | ||
}); | ||
lastClickedIndex = null; | ||
} else { | ||
chart.data.datasets.forEach((_, idx: number) => { | ||
chart.setDatasetVisibility(idx, idx === item.datasetIndex); | ||
}); | ||
lastClickedIndex = item.datasetIndex; | ||
} | ||
chart.update(); | ||
updateLegendStyles(); | ||
}; | ||
|
||
return ( | ||
<li | ||
id={`legend-item-${item.datasetIndex}`} | ||
className={cn("legend-item", { | ||
hidden: index >= maxVisibleItems && !isExpanded, | ||
active: lastClickedIndex === item.datasetIndex, | ||
})} | ||
onClick={handleClick} | ||
> | ||
<span | ||
className={cn("legend-box")} | ||
style={{ | ||
background: item.fillStyle as string, | ||
borderColor: item.strokeStyle as string, | ||
borderWidth: item.lineWidth + "px", | ||
}} | ||
/> | ||
<span className={cn("legend-text")} style={{ color: item.fontColor as string }}> | ||
{item.text} | ||
</span> | ||
{showLinks && ( | ||
<Link | ||
href={`/trigger/${item.text}`} | ||
target="_blank" | ||
className={cn("legend-link")} | ||
onClick={(e) => e.stopPropagation()} | ||
icon={<LinkIcon />} | ||
/> | ||
)} | ||
</li> | ||
); | ||
}; | ||
|
||
const Legend: React.FC<{ | ||
chart: Chart; | ||
items: LegendItem[]; | ||
showLinks: boolean; | ||
updateLegendStyles: () => void; | ||
}> = ({ chart, items, showLinks, updateLegendStyles }) => { | ||
const IconComponent = isExpanded ? ArrowUpIcon : ArrowDownIcon; | ||
|
||
const toggleExpand = () => { | ||
isExpanded = !isExpanded; | ||
chart.update(); | ||
}; | ||
|
||
return ( | ||
<ul className={cn("legend-list")}> | ||
{items.map((item, index) => ( | ||
<LegendItemComponent | ||
key={item.datasetIndex} | ||
item={item} | ||
index={index} | ||
chart={chart} | ||
showLinks={showLinks} | ||
updateLegendStyles={updateLegendStyles} | ||
/> | ||
))} | ||
{items.length > maxVisibleItems && ( | ||
<IconComponent className={cn("legend-toggle-icon")} onClick={toggleExpand} /> | ||
)} | ||
</ul> | ||
); | ||
}; | ||
|
||
export const createHtmlLegendPlugin = (showLinks: boolean): Plugin<"bar"> => ({ | ||
id: "htmlLegend", | ||
afterUpdate(chart) { | ||
const containerID = chart.options.plugins?.htmlLegend?.containerID || ""; | ||
const legendContainer = document.getElementById(containerID); | ||
|
||
if (legendContainer) { | ||
const items = chart.options.plugins?.legend?.labels?.generateLabels?.( | ||
chart | ||
) as LegendItem[]; | ||
const updateLegendStyles = () => { | ||
const ul = legendContainer.querySelector("ul"); | ||
if (ul) { | ||
const legendItems = ul.querySelectorAll("li"); | ||
legendItems.forEach((legendItem) => { | ||
if ( | ||
parseInt(legendItem.id.replace("legend-item-", "")) === lastClickedIndex | ||
) { | ||
legendItem.classList.add(cn("active")); | ||
} else { | ||
legendItem.classList.remove(cn("active")); | ||
} | ||
}); | ||
|
||
if (lastClickedIndex === null) { | ||
legendItems.forEach((legendItem) => { | ||
legendItem.classList.remove(cn("active")); | ||
}); | ||
} | ||
} | ||
}; | ||
|
||
ReactDOM.render( | ||
<Legend | ||
chart={chart} | ||
items={items} | ||
showLinks={showLinks} | ||
updateLegendStyles={updateLegendStyles} | ||
/>, | ||
legendContainer | ||
); | ||
} | ||
}, | ||
}); |
Oops, something went wrong.