Skip to content

Commit a7a4520

Browse files
Copilotgedinakova
andauthored
refactor: add 5 chart functional counterparts + update README (samples 8-12)
Agent-Logs-Url: https://github.com/IgniteUI/igniteui-react-examples/sessions/a777979d-98e9-46cf-adc5-3df39b356ec9 Co-authored-by: gedinakova <16817847+gedinakova@users.noreply.github.com>
1 parent be51d4a commit a7a4520

6 files changed

Lines changed: 553 additions & 1 deletion

File tree

FUNCTIONAL_REFACTORING_README.md

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Functional Component Refactoring – Summary
22

3-
This document describes the refactoring of **7 class-based React samples** to **functional components** following modern React best practices (React 16.8+).
3+
This document describes the refactoring of **12 class-based React samples** to **functional components** following modern React best practices (React 16.8+).
44

55
Each original `index.tsx` is left unchanged. A new `*Functional.tsx` file has been created alongside it as the functional counterpart.
66

@@ -144,6 +144,95 @@ Each original `index.tsx` is left unchanged. A new `*Functional.tsx` file has be
144144

145145
---
146146

147+
## Sample 8 – Data Pie Chart: Legend (Cross-Ref Wiring + Lazy Getters + ComponentRenderer)
148+
149+
**Files**
150+
- Original: `samples/charts/data-pie-chart/legend/src/index.tsx`
151+
- Functional: `samples/charts/data-pie-chart/legend/src/DataPieChartLegendFunctional.tsx`
152+
153+
**Feature coverage:** `cross-component ref wiring`, `lazy data getter`, `ComponentRenderer`, `props as component instances`
154+
155+
| Class pattern | Functional equivalent | Why |
156+
|---|---|---|
157+
| `private legend: IgrItemLegend` + callback ref `legendRef(r)` calling `setState({})` | `const [legend, setLegend] = useState<IgrItemLegend \| null>(null)` | Storing the component instance in state causes a re-render when it is first set, allowing it to be passed as a prop to the chart |
158+
| `private chart: IgrDataPieChart` + callback ref `chartRef(r)` calling `setState({})` | `const [chart, setChart] = useState<IgrDataPieChart \| null>(null)` (unused beyond triggering re-render) | Same pattern—state update triggers the render that delivers the `legend` prop to the chart |
159+
| Lazy getter `get energyGlobalDemand()` with backing field `_energyGlobalDemand` | `useMemo(() => new EnergyGlobalDemand(), [])` | Created once, stable across renders |
160+
| Lazy getter `get renderer()` with backing field `_componentRenderer` that registers modules | `useMemo(() => { const r = new ComponentRenderer(); ...; return r; }, [])` | ComponentRenderer and its context registrations are created once |
161+
| `legend={this.legend}` in JSX (initially `undefined` until ref fires) | `legend={legend ?? undefined}` | `null ?? undefined` falls back to `undefined`, keeping the prop absent until the legend is ready |
162+
163+
---
164+
165+
## Sample 9 – Doughnut Chart: Legend (Cross-Ref Wiring on Ring Series)
166+
167+
**Files**
168+
- Original: `samples/charts/doughnut-chart/legend/src/index.tsx`
169+
- Functional: `samples/charts/doughnut-chart/legend/src/DoughnutChartLegendFunctional.tsx`
170+
171+
**Feature coverage:** `cross-component ref wiring`, `ring series legend prop`, `lazy data getter`
172+
173+
| Class pattern | Functional equivalent | Why |
174+
|---|---|---|
175+
| `private legend: IgrItemLegend` + callback ref calling `setState({})` | `useState<IgrItemLegend \| null>(null)` | Same cross-wiring pattern as the DataPieChart legend sample |
176+
| `private chart: IgrDoughnutChart` + callback ref | `useRef` not needed—the chart does not need to be accessed imperatively | The only wiring needed is passing `legend` as a prop on `IgrRingSeries`; no ref required on the chart itself |
177+
| `<IgrRingSeries legend={this.legend}>` | `<IgrRingSeries legend={legend ?? undefined}>` | Prop supplied once the legend state is set |
178+
| Lazy getter `get energyGlobalDemand()` | `useMemo(() => new EnergyGlobalDemand(), [])` | Stable data source reference |
179+
180+
---
181+
182+
## Sample 10 – Financial Chart: Overview (Async Data + Imperative Legend Wiring)
183+
184+
**Files**
185+
- Original: `samples/charts/financial-chart/overview/src/index.tsx`
186+
- Functional: `samples/charts/financial-chart/overview/src/FinancialChartOverviewFunctional.tsx`
187+
188+
**Feature coverage:** `async data fetching`, `cross-ref imperative wiring`, `useEffect`, `state initialization`
189+
190+
| Class pattern | Functional equivalent | Why |
191+
|---|---|---|
192+
| `this.initData()` called in constructor, which calls `this.setState({ data: stocks })` on resolution | `useEffect(() => { StocksHistory.getMultipleStocks().then(stocks => setData(stocks)); }, [])` | `useEffect` with empty deps runs once after mount—equivalent to constructor-time async initiation |
193+
| `public data: any[]` as class field initialized to `[]` | `const [data, setData] = useState<any[]>([])` | Async result stored in state; initial render uses empty array |
194+
| Callback refs that cross-wire each other: `this.chart.legend = this.legend` / `this.legend` = ... | `useRef` for both + `useEffect(() => { if (chart && legend) chart.legend = legend; })` running after every render | The effectless dependency array means the wiring is applied whenever either ref changes, guaranteeing the assignment happens once both are available |
195+
196+
---
197+
198+
## Sample 11 – Category Chart: High Frequency (setInterval + componentWillUnmount + notifyInsertItem)
199+
200+
**Files**
201+
- Original: `samples/charts/category-chart/high-frequency/src/index.tsx`
202+
- Functional: `samples/charts/category-chart/high-frequency/src/CategoryChartHighFrequencyFunctional.tsx`
203+
204+
**Feature coverage:** `setInterval`, `componentWillUnmount`, `imperative chart notification API`, `mixed ref/state pattern`
205+
206+
| Class pattern | Functional equivalent | Why |
207+
|---|---|---|
208+
| `public interval: number = -1` class field | `const intervalRef = useRef<number>(-1)` | Interval ID must survive re-renders but must never cause them; `useRef` is the correct container |
209+
| `public chart: IgrCategoryChart` class field | `const chartRef = useRef<IgrCategoryChart>(null)` | Same—chart ref needs to be stable and readable inside the interval callback |
210+
| `public data: any[]` mutable array class field | `const dataRef = useRef<any[]>(...)` | The data array is mutated in-place for the chart's `notifyInsertItem`/`notifyRemoveItem` API; storing it in a ref avoids triggering re-renders on every tick |
211+
| `public dataPoints: number` / `public dataIndex: number` class fields | `useRef` for each | Values mutated in event handlers without needing re-renders |
212+
| `componentWillUnmount` clearing the interval | Cleanup function returned from `useEffect`: `return () => { clearInterval(intervalRef.current) }` | Collocates setup and teardown; cleanup runs when component unmounts |
213+
| `onChartRef` callback ref calling `this.onChartInit()` | `onChartRef` callback calling `setupInterval()` | `useCallback`-wrapped ref callback starts the interval once the chart is available |
214+
| `this.state.dataFeedAction === "Stop"` read inside `tick()` | Accessing `setState` updater's `prev` arg inside tick | The tick is inside a `setInterval` closure; reading the latest state safely requires using the functional update form of `setState` |
215+
216+
---
217+
218+
## Sample 12 – Category Chart: Line Chart with Animations (State + Event Handlers + Replay)
219+
220+
**Files**
221+
- Original: `samples/charts/category-chart/line-chart-with-animations/src/index.tsx`
222+
- Functional: `samples/charts/category-chart/line-chart-with-animations/src/CategoryChartLineChartWithAnimationsFunctional.tsx`
223+
224+
**Feature coverage:** `state`, `event handlers`, `chart ref for imperative API`, `module-level constants`
225+
226+
| Class pattern | Functional equivalent | Why |
227+
|---|---|---|
228+
| `public data: any[]` field assigned in `initData()` then used as prop | Module-level `CHART_DATA` constant | Data is static; no side effects needed; module constant is cleaner and avoids recreation |
229+
| `this.initData()` called inside `onTransitionInModeChanged` (to reset chart) | Removed—`CHART_DATA` is already stable | The original called `initData()` on mode change to "re-trigger" the chart; with a stable reference the chart already re-renders on `transitionInMode` state change |
230+
| `this.state.transitionLabel`, `transitionInDuration`, `transitionInMode` | Three `useState` calls | Fine-grained state; each setter is independent |
231+
| `public chart: IgrCategoryChart` field + `this.onChartRef` callback | `const chartRef = useRef` + `const onChartRef = useCallback` | `useRef` for the chart instance; `useCallback` for a stable ref callback |
232+
| `this.chart.replayTransitionIn()` inside `onReloadChartClick` | `chartRef.current?.replayTransitionIn()` | Optional chaining guards against the chart not yet being set |
233+
234+
---
235+
147236
## General Patterns Applied Across All Samples
148237

149238
| Class component pattern | Functional equivalent | Notes |
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import React, { useState, useRef, useEffect, useCallback } from 'react';
2+
import ReactDOM from 'react-dom/client';
3+
import './index.css';
4+
import { IgrCategoryChart } from 'igniteui-react-charts';
5+
import { IgrCategoryChartModule } from 'igniteui-react-charts';
6+
import { CategoryChartSharedData } from './CategoryChartSharedData';
7+
8+
IgrCategoryChartModule.register();
9+
10+
export default function CategoryChartHighFrequency() {
11+
const INITIAL_DATA_POINTS = 500;
12+
13+
// Mutable values that must not trigger re-renders are kept in refs
14+
const chartRef = useRef<IgrCategoryChart>(null);
15+
const dataRef = useRef<any[]>(CategoryChartSharedData.generateItems(100, INITIAL_DATA_POINTS, false));
16+
const dataIndexRef = useRef<number>(dataRef.current.length);
17+
const dataPointsRef = useRef<number>(INITIAL_DATA_POINTS);
18+
const refreshMsRef = useRef<number>(5);
19+
const intervalRef = useRef<number>(-1);
20+
21+
const [state, setState] = useState({
22+
dataFeedAction: 'Start',
23+
dataInfo: CategoryChartSharedData.toShortString(INITIAL_DATA_POINTS),
24+
dataPoints: INITIAL_DATA_POINTS,
25+
dataSource: dataRef.current,
26+
refreshInterval: 5,
27+
refreshInfo: '5ms',
28+
});
29+
30+
const setupInterval = useCallback(() => {
31+
if (intervalRef.current >= 0) {
32+
window.clearInterval(intervalRef.current);
33+
intervalRef.current = -1;
34+
}
35+
intervalRef.current = window.setInterval(() => {
36+
// Only tick when feed is "Stop" (i.e. actively streaming)
37+
if (setState === null) return; // guard against unmounted ref
38+
setState(prev => {
39+
if (prev.dataFeedAction !== 'Stop') return prev;
40+
41+
const chart = chartRef.current;
42+
if (!chart) return prev;
43+
44+
dataIndexRef.current++;
45+
const oldItem = dataRef.current[0];
46+
const newItem = CategoryChartSharedData.getNewItem(dataRef.current, dataIndexRef.current);
47+
48+
dataRef.current.push(newItem);
49+
chart.notifyInsertItem(dataRef.current, dataRef.current.length - 1, newItem);
50+
dataRef.current.shift();
51+
chart.notifyRemoveItem(dataRef.current, 0, oldItem);
52+
53+
return prev; // no state change – data mutated imperatively on the chart
54+
});
55+
}, refreshMsRef.current);
56+
}, []);
57+
58+
// Start interval after chart mounts; clear on unmount (replaces componentWillUnmount)
59+
useEffect(() => {
60+
if (chartRef.current) {
61+
setupInterval();
62+
}
63+
return () => {
64+
if (intervalRef.current >= 0) {
65+
window.clearInterval(intervalRef.current);
66+
intervalRef.current = -1;
67+
}
68+
};
69+
}, [setupInterval]);
70+
71+
const onChartRef = useCallback((chart: IgrCategoryChart) => {
72+
if (!chart) return;
73+
chartRef.current = chart;
74+
setupInterval();
75+
}, [setupInterval]);
76+
77+
const onDataGenerateClick = useCallback(() => {
78+
dataRef.current = CategoryChartSharedData.generateItems(100, dataPointsRef.current, false);
79+
dataIndexRef.current = dataRef.current.length;
80+
setState(prev => ({ ...prev, dataSource: dataRef.current }));
81+
}, []);
82+
83+
const onDataFeedClick = useCallback(() => {
84+
setState(prev => ({
85+
...prev,
86+
dataFeedAction: prev.dataFeedAction === 'Start' ? 'Stop' : 'Start',
87+
}));
88+
}, []);
89+
90+
const onDataPointsChanged = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
91+
let num = parseInt(e.target.value, 10);
92+
if (isNaN(num)) num = 10000;
93+
if (num < 100) num = 100;
94+
if (num > 2000) num = 2000;
95+
dataPointsRef.current = num;
96+
setState(prev => ({
97+
...prev,
98+
dataPoints: num,
99+
dataInfo: CategoryChartSharedData.toShortString(num),
100+
}));
101+
}, []);
102+
103+
const onRefreshFrequencyChanged = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
104+
let num = parseInt(e.target.value, 10);
105+
if (isNaN(num)) num = 10;
106+
if (num < 10) num = 10;
107+
if (num > 500) num = 500;
108+
refreshMsRef.current = num;
109+
setState(prev => ({
110+
...prev,
111+
refreshInterval: num,
112+
refreshInfo: num + 'ms',
113+
}));
114+
setupInterval();
115+
}, [setupInterval]);
116+
117+
return (
118+
<div className="container sample">
119+
<div className="options horizontal">
120+
<button onClick={onDataFeedClick}>{state.dataFeedAction}</button>
121+
<label className="options-label">Refresh: </label>
122+
<label className="options-value">{state.refreshInfo}</label>
123+
<input
124+
className="options-slider"
125+
type="range"
126+
min="5"
127+
max="250"
128+
step="5"
129+
value={state.refreshInterval}
130+
onChange={onRefreshFrequencyChanged}
131+
/>
132+
<button onClick={onDataGenerateClick}>Generate</button>
133+
<label className="options-label">Data Points: </label>
134+
<label className="options-value">{state.dataInfo}</label>
135+
<input
136+
className="options-slider"
137+
type="range"
138+
min="100"
139+
max="2000"
140+
step="100"
141+
value={state.dataPoints}
142+
onChange={onDataPointsChanged}
143+
/>
144+
</div>
145+
<div className="container" style={{ height: 'calc(100% - 45px)' }}>
146+
<IgrCategoryChart
147+
ref={onChartRef}
148+
width="100%"
149+
height="100%"
150+
chartType="Line"
151+
dataSource={state.dataSource}
152+
yAxisExtent={40}
153+
xAxisEnhancedIntervalPreferMoreCategoryLabels="false"
154+
shouldConsiderAutoRotationForInitialLabels="false"
155+
shouldAutoExpandMarginForInitialLabels="false"
156+
crosshairsDisplayMode="None"
157+
autoMarginAndAngleUpdateMode="None"
158+
markerTypes="None"
159+
/>
160+
</div>
161+
</div>
162+
);
163+
}
164+
165+
// rendering above component to the React DOM
166+
const root = ReactDOM.createRoot(document.getElementById('root'));
167+
root.render(<CategoryChartHighFrequency/>);
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import React, { useState, useRef, useCallback, useMemo } from 'react';
2+
import ReactDOM from 'react-dom/client';
3+
import './index.css';
4+
import { IgrCategoryChart } from 'igniteui-react-charts';
5+
import { IgrCategoryChartModule } from 'igniteui-react-charts';
6+
7+
IgrCategoryChartModule.register();
8+
9+
const CHART_DATA = [
10+
{ Year: '2009', Europe: 31, China: 21, USA: 19 },
11+
{ Year: '2010', Europe: 43, China: 26, USA: 24 },
12+
{ Year: '2011', Europe: 66, China: 29, USA: 28 },
13+
{ Year: '2012', Europe: 69, China: 32, USA: 26 },
14+
{ Year: '2013', Europe: 58, China: 47, USA: 38 },
15+
{ Year: '2014', Europe: 40, China: 46, USA: 31 },
16+
{ Year: '2015', Europe: 78, China: 50, USA: 19 },
17+
{ Year: '2016', Europe: 13, China: 90, USA: 52 },
18+
{ Year: '2017', Europe: 78, China: 132, USA: 50 },
19+
{ Year: '2018', Europe: 40, China: 134, USA: 34 },
20+
{ Year: '2019', Europe: 80, China: 96, USA: 38 },
21+
];
22+
23+
export default function CategoryChartLineChartWithAnimations() {
24+
const chartRef = useRef<IgrCategoryChart>(null);
25+
26+
const [transitionLabel, setTransitionLabel] = useState('1000ms');
27+
const [transitionInDuration, setTransitionInDuration] = useState(1000);
28+
const [transitionInMode, setTransitionInMode] = useState('Auto');
29+
30+
const onChartRef = useCallback((chart: IgrCategoryChart) => {
31+
if (!chart) return;
32+
chartRef.current = chart;
33+
}, []);
34+
35+
const onTransitionInModeChanged = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
36+
setTransitionInMode(e.target.value);
37+
}, []);
38+
39+
const onTransitionInDurationChanged = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
40+
const val = parseInt(e.target.value, 10);
41+
setTransitionInDuration(val);
42+
setTransitionLabel(val + 'ms');
43+
}, []);
44+
45+
const onReloadChartClick = useCallback(() => {
46+
chartRef.current?.replayTransitionIn();
47+
}, []);
48+
49+
return (
50+
<div className="container sample">
51+
<div className="options horizontal">
52+
<span className="options-label">Transition Type </span>
53+
<select onChange={onTransitionInModeChanged}>
54+
<option>Auto</option>
55+
<option>AccordionFromBottom</option>
56+
<option>AccordionFromCategoryAxisMaximum</option>
57+
<option>AccordionFromCategoryAxisMinimum</option>
58+
<option>AccordionFromLeft</option>
59+
<option>AccordionFromRight</option>
60+
<option>AccordionFromTop</option>
61+
<option>AccordionFromValueAxisMaximum</option>
62+
<option>AccordionFromValueAxisMinimum</option>
63+
<option>Expand</option>
64+
<option>FromZero</option>
65+
<option>SweepFromBottom</option>
66+
<option>SweepFromCategoryAxisMaximum</option>
67+
<option>SweepFromCategoryAxisMinimum</option>
68+
<option>SweepFromCenter</option>
69+
<option>SweepFromLeft</option>
70+
<option>SweepFromRight</option>
71+
<option>SweepFromTop</option>
72+
<option>SweepFromValueAxisMaximum</option>
73+
<option>SweepFromValueAxisMinimum</option>
74+
</select>
75+
<label className="options-value" style={{ width: '75px' }}>{transitionLabel}</label>
76+
<input
77+
className="options-slider"
78+
type="range"
79+
min="50"
80+
max="2000"
81+
step="50"
82+
defaultValue="1000"
83+
onChange={onTransitionInDurationChanged}
84+
/>
85+
<button onClick={onReloadChartClick}>Reload Chart</button>
86+
</div>
87+
88+
<IgrCategoryChart
89+
ref={onChartRef}
90+
width="100%"
91+
height="calc(100% - 30px)"
92+
dataSource={CHART_DATA}
93+
chartType="Line"
94+
isTransitionInEnabled={true}
95+
isHorizontalZoomEnabled={false}
96+
isVerticalZoomEnabled={false}
97+
transitionInDuration={transitionInDuration}
98+
transitionInMode={transitionInMode}
99+
yAxisTitle="TWh"
100+
yAxisTitleLeftMargin={10}
101+
yAxisTitleRightMargin={5}
102+
yAxisLabelLeftMargin={0}
103+
computedPlotAreaMarginMode="Series"
104+
/>
105+
</div>
106+
);
107+
}
108+
109+
// rendering above component to the React DOM
110+
const root = ReactDOM.createRoot(document.getElementById('root'));
111+
root.render(<CategoryChartLineChartWithAnimations/>);

0 commit comments

Comments
 (0)