我在 HTML 中使用 AG Grid,我需要实现合并列中具有相同值的单元格的功能。当连续行中存在重复值时,这应该使视觉上清晰可见。如果连续行具有相同的值,则应将它们合并为跨多行的单个单元格,且值垂直居中。
此外,我希望列根据用户的选择是动态的。如果删除一列,数据和求和值应自动重新透视并提供新的透视值。
附加信息: 我正在使用 AG Grid,您可以在此处找到文档。 https://www.ag-grid.com/javascript-data-grid/grid-interface/
这是我的代码供审查的链接:
https://codesandbox.io/invite/jn5cyy2572zpm2h7
下面是我的Python代码,可以在上面的链接中找到,
@app.route('/get-data-CF')
def get_data_CF():
try:
correct_order = ['Cash Flow Main 4', 'Cash Flow Main 3',
'Cash Flow Main 2', 'Cash Flow Main 1', 'Cash Flow Category']
include_currency = request.args.get(
'include_currency', 'false').lower() == 'true'
if include_currency:
correct_order.insert(0, 'transaction_currency_code')
start_date = request.args.get('start_date')
end_date = request.args.get('end_date')
cashforce_df = pd.read_csv('static/excel/Modified_Cash_Pool_Data.csv')
bank_balance_df = pd.read_csv(
'static/excel/Modified_Bank_Balances_Data.csv')
cashforce_df['transaction_dt'] = pd.to_datetime(
cashforce_df['transaction_dt'])
if start_date and end_date:
cashforce_df = cashforce_df[(cashforce_df['transaction_dt'] >= start_date) & (
cashforce_df['transaction_dt'] <= end_date)]
else:
current_month_start = datetime.now().replace(day=1)
current_month_end = (
current_month_start + pd.DateOffset(months=1)) - pd.DateOffset(days=1)
cashforce_df = cashforce_df[(cashforce_df['transaction_dt'] >= current_month_start) & (
cashforce_df['transaction_dt'] <= current_month_end)]
grouped_df = cashforce_df.groupby(
correct_order + ['transaction_dt']).agg({'transaction_amount': 'sum'}).reset_index()
pivot_df = grouped_df.pivot_table(
index=correct_order, columns='transaction_dt', values='transaction_amount', aggfunc='sum', fill_value=0)
pivot_df.columns = [col.strftime(
'%Y-%m-%d') if isinstance(col, pd.Timestamp) else col for col in pivot_df.columns]
pivot_df.reset_index(inplace=True)
combined_rows = []
for currency in bank_balance_df['transaction_currency_code'].unique() if include_currency else ['GBP']:
opening_balance_row = {col: '' for col in pivot_df.columns}
closing_balance_row = {col: '' for col in pivot_df.columns}
if include_currency:
opening_balance_row['transaction_currency_code'] = currency
closing_balance_row['transaction_currency_code'] = currency
opening_balance_row['Cash Flow Main 4'] = 'Opening Balance'
closing_balance_row['Cash Flow Main 4'] = 'Closing Balance'
opening_balance = bank_balance_df.loc[bank_balance_df['transaction_currency_code'] == currency, 'balance'].sum(
) if include_currency else 0
closing_balance = opening_balance
for date_col in pivot_df.columns:
if date_col not in correct_order:
opening_balance_row[date_col] = f"{opening_balance:.2f}"
closing_balance_row[date_col] = f"{closing_balance:.2f}"
combined_rows.append(pd.DataFrame([opening_balance_row]))
combined_rows.append(
pivot_df[pivot_df['transaction_currency_code'] == currency] if include_currency else pivot_df)
combined_rows.append(pd.DataFrame([closing_balance_row]))
combined_df = pd.concat(combined_rows, ignore_index=True)
numeric_columns = combined_df.select_dtypes(include=['number']).columns
for col in numeric_columns:
combined_df[col] = combined_df[col].apply(lambda x: f"{x:.2f}")
json_data_CF = combined_df.to_json(orient='records', date_format='iso')
print(json_data_CF)
return jsonify(json_data_CF=json_data_CF)
except Exception as e:
return jsonify(error=str(e)), 500
下面是我的js代码,也可以在codesandbox中找到
document.addEventListener("DOMContentLoaded", () => {
const basePinnedColumns = [
"Cash Flow Main 4",
"Cash Flow Main 3",
"Cash Flow Main 2",
"Cash Flow Main 1",
"Cash Flow Category",
];
const allPinnedColumns = ["transaction_currency_code"].concat(
basePinnedColumns,
);
let availablePinnedColumns = [];
function rowSpan(params) {
if (!params.data || !params.column || !params.api) return 1;
const column = params.column.colId;
const rowIndex = params.rowIndex;
const rowData = params.data;
let spanCount = 1;
for (let i = rowIndex + 1; i < params.api.getDisplayedRowCount(); i++) {
const nextData = params.api.getDisplayedRowAtIndex(i).data;
if (nextData && nextData[column] === rowData[column]) {
spanCount++;
} else {
break;
}
}
return spanCount;
}
const gridOptions = {
defaultColDef: {
flex: 1,
minWidth: 100,
resizable: true,
sortable: true,
filter: true,
cellClass: (params) => {
if (
params.data &&
(params.data["Cash Flow Main 4"] === "Opening Balance" ||
params.data["Cash Flow Main 4"] === "Closing Balance")
) {
return params.colDef.field === "Cash Flow Main 4"
? "balance-label-cell"
: "balance-value-cell";
}
return null;
},
cellStyle: (params) => {
if (
params.data &&
(params.data["Cash Flow Main 4"] === "Opening Balance" ||
params.data["Cash Flow Main 4"] === "Closing Balance")
) {
return params.colDef.field === "Cash Flow Main 4"
? { textAlign: "left" }
: { textAlign: "right" };
}
return null;
},
},
autoGroupColumnDef: {
headerName: "Account Currency",
field: "transaction_currency_code",
cellRenderer: "agGroupCellRenderer",
cellRendererParams: {
suppressCount: true,
innerRenderer: (params) => {
if (
params.node.group &&
params.node.field === "transaction_currency_code"
) {
return `<div class="currency-cell">${params.node.key}</div>`;
} else {
return params.value;
}
},
},
},
rowGroupPanelShow: "always",
groupDefaultExpanded: -1,
columnDefs: [],
rowData: [],
suppressRowTransform: true,
};
const gridDiv = document.querySelector("#myGrid");
const gridApi = agGrid.createGrid(gridDiv, gridOptions);
function updateColumns(columnNames, includeCurrency) {
const pinnedColumns = includeCurrency
? allPinnedColumns
: basePinnedColumns;
const orderedColumnNames = pinnedColumns.concat(
columnNames.filter((name) => !pinnedColumns.includes(name)),
);
const columnDefs = orderedColumnNames.map((key) => ({
headerName:
key === "transaction_currency_code" ? "Account Currency" : key,
field: key,
sortable: true,
filter: true,
pinned: pinnedColumns.includes(key) ? "left" : null,
rowSpan: rowSpan,
cellClassRules: {
"cell-span": 'value && value !== ""',
},
}));
gridApi.updateGridOptions({ columnDefs: columnDefs });
updateColumnControlPanel(
columnDefs.filter((col) => basePinnedColumns.includes(col.field)),
);
}
function fetchData(startDate, endDate, includeCurrency = false) {
let url = "/get-data-CF";
if (startDate && endDate) {
url += `?start_date=${startDate}&end_date=${endDate}`;
}
if (includeCurrency) {
url += url.includes("?") ? "&" : "?";
url += "include_currency=true";
}
fetch(url)
.then((response) => {
if (!response.ok) {
throw new Error(
"Network response was not ok: " + response.statusText,
);
}
return response.json();
})
.then((response) => {
const data = JSON.parse(response.json_data_CF);
if (data && data.length > 0) {
const columnNames = Object.keys(data[0]);
updateColumns(columnNames, includeCurrency);
gridApi.updateGridOptions({ rowData: data });
availablePinnedColumns = allPinnedColumns.filter(
(col) => !columnNames.includes(col),
);
updateAvailableColumnsDropdown();
}
})
.catch((error) => {
console.error("Error fetching data:", error);
fetch(url)
.then((response) => response.text())
.then((text) => console.log("Response was:", text));
});
}
const startDatePicker = flatpickr('input[name="start_date"]', {
dateFormat: "Y-m-d",
});
const endDatePicker = flatpickr('input[name="end_date"]', {
dateFormat: "Y-m-d",
});
document.getElementById("dateFilterBtn").addEventListener("click", () => {
const startDate = startDatePicker.input.value;
const endDate = endDatePicker.input.value;
if (startDate && endDate) {
fetchData(
startDate,
endDate,
document.getElementById("currencyToggle").checked,
);
}
});
fetchData();
document.getElementById("currencyToggle").addEventListener("change", () => {
const startDate = startDatePicker.input.value;
const endDate = endDatePicker.input.value;
fetchData(
startDate,
endDate,
document.getElementById("currencyToggle").checked,
);
});
function isColumnOrderValid(currentOrder) {
let indexMap = currentOrder
.map((column) => allPinnedColumns.indexOf(column))
.filter((index) => index !== -1);
return indexMap.every((val, i, arr) => !i || val > arr[i - 1]);
}
function updateColumnControlPanel(columnDefs) {
const panel = document.getElementById("activeColumns");
panel.innerHTML = "";
columnDefs.forEach((col) => {
const colDiv = document.createElement("div");
colDiv.textContent = `${col.headerName} `;
const removeBtn = document.createElement("button");
removeBtn.textContent = "x";
removeBtn.onclick = () => removeColumn(col.field);
colDiv.appendChild(removeBtn);
panel.appendChild(colDiv);
});
updateAvailableColumnsDropdown();
}
function removeColumn(field) {
const allColumns = gridApi.getColumnDefs();
const newColumnDefs = allColumns.filter((col) => col.field !== field);
if (!isColumnOrderValid(newColumnDefs.map((def) => def.field))) {
alert("Removing this column would result in invalid data aggregation.");
return;
}
availablePinnedColumns.push(field);
gridApi.updateGridOptions({ columnDefs: newColumnDefs });
updateColumnControlPanel(
newColumnDefs.filter((col) => basePinnedColumns.includes(col.field)),
);
updateAvailableColumnsDropdown();
}
function updateAvailableColumnsDropdown() {
const dropdown = document.getElementById("availableColumns");
if (!dropdown) {
console.error("Dropdown element not found");
return;
}
dropdown.innerHTML = "";
availablePinnedColumns.forEach((field) => {
const option = new Option(field, field);
dropdown.add(option);
});
}
window.addColumn = function () {
const dropdown = document.getElementById("availableColumns");
const fieldToAdd = dropdown.value;
if (!fieldToAdd) return;
const allColumns = gridApi.getColumnDefs();
const columnToAdd = {
headerName: fieldToAdd,
field: fieldToAdd,
sortable: true,
filter: true,
pinned: "left",
};
const newColumnDefs = [...allColumns, columnToAdd];
if (!isColumnOrderValid(newColumnDefs.map((def) => def.field))) {
alert("Adding this column would result in invalid data aggregation.");
return;
}
gridApi.updateGridOptions({ columnDefs: newColumnDefs });
availablePinnedColumns = availablePinnedColumns.filter(
(col) => col !== fieldToAdd,
);
updateColumnControlPanel(
newColumnDefs.filter((col) => basePinnedColumns.includes(col.field)),
);
updateAvailableColumnsDropdown();
};
});
要显示网格,请选择从 4 月 1 日到结束日期 4 月 30 日 的开始日期,然后单击筛选按钮,因为这些是唯一可用的数据,然后单击筛选按钮以显示网格。
我查看了 AG Grid 的文档并尝试实现行跨越功能,但我没有获得连续重复项所需的合并效果。我还尝试使列动态化,但无法实现根据用户选择自动旋转和求和值。
根据您的要求和提供的屏幕截图,您可以通过正确实现 rowSpan 功能并处理动态列更新,在 AG Grid 中实现所需的单元格合并和动态列功能
确保您拥有必要的 HTML 和 CSS 来显示 AG 网格和处理行跨度。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AG Grid Example</title>
<link rel="stylesheet" href="https://unpkg.com/ag-grid-community/styles/ag-grid.css">
<link rel="stylesheet" href="https://unpkg.com/ag-grid-community/styles/ag-theme-alpine.css">
<style>
.ag-theme-alpine .ag-row .ag-cell {
line-height: 30px; /* Adjust line-height as needed */
}
.cell-span {
vertical-align: middle;
text-align: center;
}
</style>
</head>
<body>
<div style="width: 100%; height: 500px;" id="myGrid" class="ag-theme-alpine"></div>
<script src="https://unpkg.com/ag-grid-community/dist/ag-grid-community.noStyle.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.9/flatpickr.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.9/flatpickr.min.css">
<script src="your-javascript-file.js"></script>
</body>
</html>
第 2 步:AG 网格配置的 JavaScript 确保您的 JavaScript 代码正确处理行跨越和动态列。这是精炼的 JavaScript 代码
document.addEventListener("DOMContentLoaded", () => {
const basePinnedColumns = [
"Cash Flow Main 4",
"Cash Flow Main 3",
"Cash Flow Main 2",
"Cash Flow Main 1",
"Cash Flow Category",
];
const allPinnedColumns = ["transaction_currency_code"].concat(basePinnedColumns);
let availablePinnedColumns = [];
function rowSpan(params) {
if (!params.data || !params.column || !params.api) return 1;
const column = params.column.colId;
const rowIndex = params.rowIndex;
const rowData = params.data;
let spanCount = 1;
for (let i = rowIndex + 1; i < params.api.getDisplayedRowCount(); i++) {
const nextData = params.api.getDisplayedRowAtIndex(i).data;
if (nextData && nextData[column] === rowData[column]) {
spanCount++;
} else {
break;
}
}
return spanCount;
}
const gridOptions = {
defaultColDef: {
flex: 1,
minWidth: 100,
resizable: true,
sortable: true,
filter: true,
cellClass: (params) => {
if (params.data && (params.data["Cash Flow Main 4"] === "Opening Balance" || params.data["Cash Flow Main 4"] === "Closing Balance")) {
return params.colDef.field === "Cash Flow Main 4" ? "balance-label-cell" : "balance-value-cell";
}
return null;
},
cellStyle: (params) => {
if (params.data && (params.data["Cash Flow Main 4"] === "Opening Balance" || params.data["Cash Flow Main 4"] === "Closing Balance")) {
return params.colDef.field === "Cash Flow Main 4" ? { textAlign: "left" } : { textAlign: "right" };
}
return null;
},
},
autoGroupColumnDef: {
headerName: "Account Currency",
field: "transaction_currency_code",
cellRenderer: "agGroupCellRenderer",
cellRendererParams: {
suppressCount: true,
innerRenderer: (params) => {
if (params.node.group && params.node.field === "transaction_currency_code") {
return `<div class="currency-cell">${params.node.key}</div>`;
} else {
return params.value;
}
},
},
},
rowGroupPanelShow: "always",
groupDefaultExpanded: -1,
columnDefs: [],
rowData: [],
suppressRowTransform: true,
};
const gridDiv = document.querySelector("#myGrid");
const gridApi = agGrid.createGrid(gridDiv, gridOptions);
function updateColumns(columnNames, includeCurrency) {
const pinnedColumns = includeCurrency ? allPinnedColumns : basePinnedColumns;
const orderedColumnNames = pinnedColumns.concat(columnNames.filter((name) => !pinnedColumns.includes(name)));
const columnDefs = orderedColumnNames.map((key) => ({
headerName: key === "transaction_currency_code" ? "Account Currency" : key,
field: key,
sortable: true,
filter: true,
pinned: pinnedColumns.includes(key) ? "left" : null,
rowSpan: rowSpan,
cellClassRules: {
"cell-span": 'value && value !== ""',
},
}));
gridApi.setColumnDefs(columnDefs);
updateColumnControlPanel(columnDefs.filter((col) => basePinnedColumns.includes(col.field)));
}
function fetchData(startDate, endDate, includeCurrency = false) {
let url = "/get-data-CF";
if (startDate && endDate) {
url += `?start_date=${startDate}&end_date=${endDate}`;
}
if (includeCurrency) {
url += url.includes("?") ? "&" : "?";
url += "include_currency=true";
}
fetch(url)
.then((response) => {
if (!response.ok) {
throw new Error("Network response was not ok: " + response.statusText);
}
return response.json();
})
.then((response) => {
const data = JSON.parse(response.json_data_CF);
if (data && data.length > 0) {
const columnNames = Object.keys(data[0]);
updateColumns(columnNames, includeCurrency);
gridApi.setRowData(data);
availablePinnedColumns = allPinnedColumns.filter((col) => !columnNames.includes(col));
updateAvailableColumnsDropdown();
}
})
.catch((error) => {
console.error("Error fetching data:", error);
});
}
const startDatePicker = flatpickr('input[name="start_date"]', { dateFormat: "Y-m-d" });
const endDatePicker = flatpickr('input[name="end_date"]', { dateFormat: "Y-m-d" });
document.getElementById("dateFilterBtn").addEventListener("click", () => {
const startDate = startDatePicker.input.value;
const endDate = endDatePicker.input.value;
if (startDate && endDate) {
fetchData(startDate, endDate, document.getElementById("currencyToggle").checked);
}
});
fetchData();
document.getElementById("currencyToggle").addEventListener("change", () => {
const startDate = startDatePicker.input.value;
const endDate = endDatePicker.input.value;
fetchData(startDate, endDate, document.getElementById("currencyToggle").checked);
});
function isColumnOrderValid(currentOrder) {
let indexMap = currentOrder.map((column) => allPinnedColumns.indexOf(column)).filter((index) => index !== -1);
return indexMap.every((val, i, arr) => !i || val > arr[i - 1]);
}
function updateColumnControlPanel(columnDefs) {
const panel = document.getElementById("activeColumns");
panel.innerHTML = "";
columnDefs.forEach((col) => {
const colDiv = document.createElement("div");
colDiv.textContent = `${col.headerName} `;
const removeBtn = document.createElement("button");
removeBtn.textContent = "x";
removeBtn.onclick = () => removeColumn(col.field);
colDiv.appendChild(removeBtn);
panel.appendChild(colDiv);
});
updateAvailableColumnsDropdown();
}
function removeColumn(field) {
const allColumns = gridApi.getColumnDefs();
const newColumnDefs = allColumns.filter((col) => col.field !== field);
if (!isColumnOrderValid(newColumnDefs.map((def) => def.field))) {
alert("Removing this column would result in invalid data aggregation.");
return;
}
availablePinnedColumns.push(field);
gridApi.setColumnDefs(newColumnDefs);
updateColumnControlPanel(newColumnDefs.filter((col) => basePinnedColumns.includes(col.field)));
updateAvailableColumnsDropdown();
}
function updateAvailableColumnsDropdown() {
const dropdown = document.getElementById("availableColumns");
if (!dropdown) {
console.error("Dropdown element not found");
return;
}
dropdown.innerHTML = "";
availablePinnedColumns.forEach((field) => {
const option = new Option(field, field);
dropdown.add(option);
});
}
window.addColumn = function () {
const dropdown = document.getElementById("availableColumns");
const fieldToAdd = dropdown.value;
if (!fieldToAdd) return;
const allColumns = gridApi.getColumnDefs();
const columnToAdd = {
headerName: fieldToAdd,
field: fieldToAdd,
sortable: true,
filter: true,
pinned: "left",
};
const newColumnDefs = [...allColumns, columnToAdd];
if (!isColumnOrderValid(newColumnDefs.map((def) => def.field))) {
alert("Adding this column would result in invalid data aggregation.");
return;
}
gridApi.setColumnDefs(newColumnDefs);
availablePinnedColumns = availablePinnedColumns.filter((col) => col !== fieldToAdd);
updateColumnControlPanel(newColumnDefs.filter((col) => basePinnedColumns.includes(col.field)));
updateAvailableColumnsDropdown();
};
});
第 3 步:与您的后端集成 确保您的后端以前端期望的格式提供数据。数据应根据需要进行分组和旋转。
这应该可以帮助您实现 AG Grid 中的单元格合并和动态列功能。