如何合并 AG 网格中连续重复值的单元格并根据用户选择使列动态化?

问题描述 投票:0回答:1

我在 HTML 中使用 AG Grid,我需要实现合并列中具有相同值的单元格的功能。当连续行中存在重复值时,这应该使视觉上清晰可见。如果连续行具有相同的值,则应将它们合并为跨多行的单个单元格,且值垂直居中。

这是一个图片示例,展示了没有单元格合并的情况 What my grid looks like without cell merging

这是我想要实现的目标的图片 What my grid should look like after successful implementation of the cell merging

此外,我希望列根据用户的选择是动态的。如果删除一列,数据和求和值应自动重新透视并提供新的透视值。

附加信息: 我正在使用 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 的文档并尝试实现行跨越功能,但我没有获得连续重复项所需的合并效果。我还尝试使列动态化,但无法实现根据用户选择自动旋转和求和值。

javascript python flask ag-grid
1个回答
0
投票

根据您的要求和提供的屏幕截图,您可以通过正确实现 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 中的单元格合并和动态列功能。

© www.soinside.com 2019 - 2024. All rights reserved.