Skip to content

Commit be7b23b

Browse files
committed
D3 chart integration sample added.
1 parent 72665d9 commit be7b23b

12 files changed

Lines changed: 2546 additions & 0 deletions

index.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<meta name="title" content="Mastering Data Visualization: Combining Syncfusion React Pivot Table with D3.js for Custom Charts" />
7+
<meta name="description" content="Learn how to combine Syncfusion React Pivot Table with D3.js custom charts for interactive React data visualization." />
8+
<title>Syncfusion React Pivot Table + D3.js</title>
9+
</head>
10+
<body>
11+
<div id="root"></div>
12+
<script type="module" src="/src/main.jsx"></script>
13+
</body>
14+
</html>

package.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "syncfusion-react-pivot-d3-sample",
3+
"version": "1.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "vite build",
9+
"preview": "vite preview --port 5173"
10+
},
11+
"dependencies": {
12+
"@syncfusion/ej2-react-pivotview": "32.1.22",
13+
"@syncfusion/ej2-react-dropdowns": "32.1.22",
14+
"d3": "7.9.0",
15+
"react": "19.2.3",
16+
"react-dom": "19.2.3"
17+
},
18+
"devDependencies": {
19+
"@vitejs/plugin-react": "5.1.2",
20+
"vite": "7.3.1"
21+
}
22+
}

src/App.jsx

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
import React, { useRef, useState, useCallback, useEffect } from 'react'
2+
import { PivotViewComponent, Inject, FieldList, Toolbar } from '@syncfusion/ej2-react-pivotview'
3+
import { DropDownListComponent } from '@syncfusion/ej2-react-dropdowns'
4+
import D3GroupedBar from './D3GroupedBar.jsx'
5+
import D3Line from './D3Line.jsx'
6+
import D3StackedBar from './D3StackedBar.jsx'
7+
import D3Area from './D3Area.jsx'
8+
import D3Pie from './D3Pie.jsx'
9+
import D3Donut from './D3Donut.jsx'
10+
import D3Scatter from './D3Scatter.jsx'
11+
12+
// Larger mock dataset: SaaS KPIs by Year/Region/Product with two measures
13+
const regions = ['NA', 'EU', 'APAC']
14+
const products = ['Analytics', 'Platform', 'Security', 'Mobile']
15+
const years = ['2021','2022','2023','2024','2025']
16+
17+
function generateData() {
18+
const data = []
19+
for (const y of years) {
20+
for (const r of regions) {
21+
for (const p of products) {
22+
// Simple deterministic pseudo-random based on string char codes
23+
const seed = (y.charCodeAt(0) + r.charCodeAt(0) + p.charCodeAt(0)) % 97
24+
const base = 150000 + (seed % 50) * 5000
25+
const growth = (Number(y) - 2021) * 12000
26+
const seasonal = ['Analytics','Platform'].includes(p) ? 20000 : 0
27+
const regional = r === 'NA' ? 25000 : r === 'EU' ? 15000 : 12000
28+
const amount = base + growth + seasonal + regional
29+
const quantity = Math.round(amount / 200)
30+
data.push({ Year: y, Region: r, Product: p, Amount: amount, Quantity: quantity })
31+
}
32+
}
33+
}
34+
return data
35+
}
36+
37+
const rawData = generateData()
38+
39+
// Extract chart-ready model directly from PivotView's engine (enginePopulated)
40+
function extractFromEngine(pivotView) {
41+
const engine = pivotView?.engineModule
42+
const pivotValues = engine?.pivotValues
43+
if (!pivotValues) return { rowKeys: [], colKeys: [], valueOf: () => 0, valueField: '', valueType: '' }
44+
45+
// Find the deepest column header row (leaf columns)
46+
let headerRowIndex = -1
47+
for (let r = 0; r < pivotValues.length; r++) {
48+
const row = pivotValues[r]
49+
if (row && row.some((c) => c && c.axis === 'column')) headerRowIndex = r
50+
else if (headerRowIndex >= 0) break
51+
}
52+
const headerRow = headerRowIndex >= 0 ? pivotValues[headerRowIndex] : []
53+
54+
// Gather column keys and their column indices
55+
const colMap = [] // [{key, colIndex}]
56+
for (let c = 0; c < headerRow.length; c++) {
57+
const cell = headerRow[c]
58+
if (!cell || cell.axis !== 'column') continue
59+
if (cell.type && String(cell.type).toLowerCase().includes('grand')) continue
60+
const key = cell.formattedText || cell.value || cell.memberCaption || cell.memberUniqueName || `C${c}`
61+
colMap.push({ key: String(key), colIndex: c })
62+
}
63+
const colKeys = colMap.map((m) => m.key)
64+
65+
// Parse data rows: those that contain row headers then value cells
66+
const rows = [] // rowKeys
67+
const matrix = new Map() // `${rk}||${ck}` -> value
68+
69+
for (let r = headerRowIndex + 1; r < pivotValues.length; r++) {
70+
const row = pivotValues[r]
71+
if (!row) continue
72+
// Build row key by concatenating leading row-header cells
73+
const parts = []
74+
for (let c = 0; c < row.length; c++) {
75+
const cell = row[c]
76+
if (!cell) continue
77+
if (cell.axis === 'row') {
78+
if (cell.type && String(cell.type).toLowerCase().includes('grand')) { parts.length = 0; break }
79+
const txt = cell.formattedText || cell.value || cell.memberCaption || cell.memberUniqueName
80+
if (txt !== undefined && txt !== null) parts.push(String(txt))
81+
} else {
82+
// first non-row header -> data begins
83+
break
84+
}
85+
}
86+
if (!parts.length) continue
87+
const rk = parts.join(' / ')
88+
if (!rows.includes(rk)) rows.push(rk)
89+
90+
// Read values at collected column indices
91+
for (const { key: ck, colIndex } of colMap) {
92+
const cell = row[colIndex]
93+
if (!cell) continue
94+
if (cell.value == null || isNaN(Number(cell.value))) continue
95+
matrix.set(`${rk}||${ck}`, Number(cell.value))
96+
}
97+
}
98+
99+
const valueOf = (rk, ck) => matrix.get(`${rk}||${ck}`) || 0
100+
101+
// Derive value field and type from dataSourceSettings
102+
const v = pivotView?.dataSourceSettings?.values?.[0]
103+
const valueField = v?.name || ''
104+
const valueType = v?.type || ''
105+
106+
return { rowKeys: rows, colKeys, valueOf, valueField, valueType }
107+
}
108+
109+
export default function App() {
110+
const pivotRef = useRef(null)
111+
112+
// Pivot configuration
113+
const [dataSourceSettings, setDataSourceSettings] = useState({
114+
dataSource: rawData,
115+
expandAll: false,
116+
enableSorting: true,
117+
rows: [{ name: 'Year' }],
118+
columns: [{ name: 'Product' }, { name: 'Region' }],
119+
values: [
120+
{ name: 'Amount', caption: 'Revenue ($)', type: 'Sum' },
121+
],
122+
formatSettings: [{ name: 'Amount', format: 'C0' }],
123+
filterSettings: [
124+
// Example filter: include only USA and Germany (both already present here)
125+
// { name: 'Country', type: 'Include', items: ['USA', 'Germany'] },
126+
],
127+
drilledMembers: [],
128+
})
129+
130+
const [chartModel, setChartModel] = useState({ rowKeys: [], colKeys: [], valueOf: () => 0, valueField: 'Amount', valueType: 'Sum' })
131+
const [chartType, setChartType] = useState('bar') // 'bar' | 'line' | 'stacked' | 'area' | 'pie' | 'donut' | 'scatter'
132+
const [measure, setMeasure] = useState('Amount') // 'Amount' | 'Quantity'
133+
const chartWrapRef = useRef(null)
134+
const [chartWidth, setChartWidth] = useState(700)
135+
const [refreshTrigger, setRefreshTrigger] = useState(0)
136+
137+
// Use enginePopulated to pull pivot values directly from engine
138+
const onEnginePopulated = useCallback(() => {
139+
if (!pivotRef.current) return
140+
const model = extractFromEngine(pivotRef.current)
141+
setChartModel({ ...model })
142+
}, [])
143+
144+
// Initialize once on mount and observe chart container for responsive width
145+
useEffect(() => {
146+
if (pivotRef.current) setChartModel(extractFromEngine(pivotRef.current))
147+
const el = chartWrapRef.current
148+
if (!el) return
149+
const update = () => setChartWidth(Math.max(320, el.clientWidth - 24))
150+
update()
151+
const ro = new ResizeObserver(update)
152+
ro.observe(el)
153+
return () => ro.disconnect()
154+
}, [])
155+
156+
// Trigger chart update when dataSourceSettings changes (including measure change)
157+
useEffect(() => {
158+
setRefreshTrigger(prev => prev + 1)
159+
}, [dataSourceSettings])
160+
161+
// Extract chart data whenever refresh trigger changes
162+
useEffect(() => {
163+
if (pivotRef.current && refreshTrigger > 0) {
164+
// Give pivot time to refresh
165+
const timer = setTimeout(() => {
166+
const model = extractFromEngine(pivotRef.current)
167+
setChartModel({ ...model })
168+
}, 150)
169+
return () => clearTimeout(timer)
170+
}
171+
}, [refreshTrigger])
172+
173+
return (
174+
<div className="app-shell">
175+
<h1 style={{fontSize:20, marginBottom:4}}>Syncfusion React Pivot Table + D3.js Custom Chart</h1>
176+
<p className="subtitle" style={{marginBottom:8}}>Two-way interaction: pivot changes update the D3 chart automatically.</p>
177+
178+
<div className="controls">
179+
<DropDownListComponent
180+
width={160}
181+
dataSource={[
182+
{text:'Bar', value:'bar'},
183+
{text:'Line', value:'line'},
184+
{text:'Stacked Bar', value:'stacked'},
185+
{text:'Area', value:'area'},
186+
{text:'Pie', value:'pie'},
187+
{text:'Donut', value:'donut'},
188+
{text:'Scatter', value:'scatter'}
189+
]}
190+
fields={{ text:'text', value:'value' }}
191+
value={chartType}
192+
change={(e)=> setChartType(e.value)}
193+
placeholder="Chart type"
194+
/>
195+
<DropDownListComponent
196+
width={170}
197+
dataSource={[{text:'Revenue ($)', value:'Amount'}, {text:'Quantity', value:'Quantity'}]}
198+
fields={{ text:'text', value:'value' }}
199+
value={measure}
200+
change={(e)=>{
201+
const m = e.value
202+
setMeasure(m)
203+
setDataSourceSettings(prev => ({
204+
...prev,
205+
values: [{ name: m, caption: m === 'Amount' ? 'Revenue ($)' : 'Units', type: 'Sum' }],
206+
formatSettings: m === 'Amount' ? [{ name: 'Amount', format: 'C0' }] : [{ name: 'Quantity', format: 'N0' }]
207+
}))
208+
}}
209+
placeholder="Measure"
210+
/>
211+
</div>
212+
213+
<div className="layout">
214+
<div className="panel pivot">
215+
<PivotViewComponent
216+
id="PivotView"
217+
ref={pivotRef}
218+
width={'100%'}
219+
height={'520px'}
220+
dataSourceSettings={dataSourceSettings}
221+
showFieldList={true}
222+
showToolbar={true}
223+
toolbar={['FieldList']}
224+
enginePopulated={onEnginePopulated}
225+
dataBound={onEnginePopulated}
226+
>
227+
<Inject services={[FieldList, Toolbar]} />
228+
</PivotViewComponent>
229+
</div>
230+
<div className="panel chart" ref={chartWrapRef}>
231+
{chartType === 'bar' ? (
232+
<D3GroupedBar
233+
title={`${chartModel.valueType} of ${chartModel.valueField}`}
234+
data={chartModel}
235+
height={520}
236+
width={chartWidth}
237+
/>
238+
) : chartType === 'line' ? (
239+
<D3Line
240+
title={`${chartModel.valueType} of ${chartModel.valueField}`}
241+
data={chartModel}
242+
height={520}
243+
width={chartWidth}
244+
/>
245+
) : chartType === 'stacked' ? (
246+
<D3StackedBar
247+
title={`${chartModel.valueType} of ${chartModel.valueField}`}
248+
data={chartModel}
249+
height={520}
250+
width={chartWidth}
251+
/>
252+
) : chartType === 'area' ? (
253+
<D3Area
254+
title={`${chartModel.valueType} of ${chartModel.valueField}`}
255+
data={chartModel}
256+
height={520}
257+
width={chartWidth}
258+
/>
259+
) : chartType === 'pie' ? (
260+
<D3Pie
261+
title={`${chartModel.valueType} of ${chartModel.valueField}`}
262+
data={chartModel}
263+
height={520}
264+
width={chartWidth}
265+
/>
266+
) : chartType === 'donut' ? (
267+
<D3Donut
268+
title={`${chartModel.valueType} of ${chartModel.valueField}`}
269+
data={chartModel}
270+
height={520}
271+
width={chartWidth}
272+
/>
273+
) : chartType === 'scatter' ? (
274+
<D3Scatter
275+
title={`${chartModel.valueType} of ${chartModel.valueField}`}
276+
data={chartModel}
277+
height={520}
278+
width={chartWidth}
279+
/>
280+
) : (
281+
<D3GroupedBar
282+
title={`${chartModel.valueType} of ${chartModel.valueField}`}
283+
data={chartModel}
284+
height={520}
285+
width={chartWidth}
286+
/>
287+
)}
288+
289+
</div>
290+
</div>
291+
292+
<footer className="note">
293+
<strong>Tip:</strong> Use the Field List to change rows/columns/filters. The chart reflects the current pivot.
294+
</footer>
295+
</div>
296+
)
297+
}

0 commit comments

Comments
 (0)