Our state of the art Gantt chart


Post by jhill@achieveit.com »

I am still trying to figure out the exact steps to reproduce this outside of my project, but I am hoping there may be something obvious I am doing wrong that you could point out. I'm running into an issue where columns for some tasks are displaying field data for other tasks.

I noticed each row element has a data-id="someId" attribute and the cells within each column of the row have a data-portal-id="portal-someId-columnName" attribute. Usually the "someId" portion of these attributes are the same, but in some case they are not.

In the attached screenshot, you can see the data-id on the row is '6e0916ca-8894-4e1e-a8c2-08dc318de4b6", but in the start column the data-portal-id attribute is portal-188f63d3-309b-4263-a891-08dc318de4b6-start. Clearly the "ID" portion of this is different and I assume this is what's causing my issue. It seems like this happens for the first row / record to be added / updated by the scroll virtualization.

Is there anything obvious that might cause this? I know the snippet below isn't reproducible on it's own, but if it helps this is an example of the column config used for one of the columns that has this issue. It's also happening on other columns, not just date columns. I'm aware of the type: date config that could be used, but I need more control over rendering & formatting so I can't use that.

{
    field: 'start',
    id: 'start',
    text: 'Start Date',
    width: 100,
    renderer: (args) => {
      const date = record.getData('start');

  if (!date) {
    return ''";
  }

  return <span>{date.toDateString()}</span>
}
  },
Attachments
Screenshot 2024-02-19 at 8.20.34 PM.png
Screenshot 2024-02-19 at 8.20.34 PM.png (178.35 KiB) Viewed 176 times

Post by ghulam.ghous »

Hi there,

There's nothing wrong with the config as far as I see. Is there any chance that there's something wrong with your data? Because I have checked your config and used it inside our react javascript advanced demo which uses virtualization on scroll but it does show the correct data as you have mentioned. Probably you can try adding your data in that demo and see if the issue is reproducible there. In any case we need a test case to repro and assist you better.

I'm aware of the type: date config that could be used, but I need more control over rendering & formatting so I can't use that

Why not use the renderer on the date column that will help you to achieve the same format and control over rendering?

Regards,
Ghous


Post by jhill@achieveit.com »

This public repo reproduces the issue: https://github.com/jamesonhill/-bryntum-issue

It seems like this happens when you call row.addCls inside of a column renderer. In my example, the name column renderer is supposed to conditionally add a class to the row if the row data has a value for the start field. I'm only setting a start date on the first record, yet if you scroll down you will see many rows with the blue background.

import './App.css'; // this has the the css .myRow { background-color: lightblue; }
import { useState, useRef, useMemo, useEffect } from 'react';
import '@bryntum/gantt/gantt.stockholm.css';
import { BryntumGantt } from '@bryntum/gantt-react';

const DateDisplay = ({ date }) => {
  return <span>{date.toDateString()}</span>
}

function BryntumComponent({ data }) {
  const ganttRef = useRef(null);

  const tasksRef = useRef();

  const tasks = useMemo(() => {
    return data.map((task) => {
      return {
        ...task,
        startDate: task.start,
        endDate: task.due,
        manuallyScheduled: true
      };
    });
  }, [data]);

  const [config] = useState({
    viewPreset: {
      timeResolution: { unit: 'day', increment: 1 },
      headers: [
        { unit: 'year', dateFormat: 'YYYY' },
        {
          unit: 'quarter',
          dateFormat: 'Q',
          renderer: (date, __, { value }) => {
            return `Q${value} ${date.getFullYear()}`;
          }
        },
        {
          unit: 'month',
          dateFormat: 'MMM'
        }
      ]
    },
    // autoAdjustTimeAxis: false,
    projectLinesFeature: { showCurrentTimeline: true },
    subGridConfigs: { locked: { width: '40%' } },
  })

  const [columns] = useState([
    {
      type: 'name',
      field: 'name',
      id: 'name',
      autoHeight: true,
      width: 100,
      renderer: (args) => {
        if (args.record.getData('start')) {
          args.row.addCls('myRow');
        }
        return <div>{args.value}</div>;
      },
      sortable: false,
      leafIconCls: null
    },
    {
      field: 'start',
      id: 'start',
      text: 'Start Date',
      type: 'date',
      width: 100,
      renderer: ({ record }) => {
  
const date = record.getData('start'); if (!date) { return ''; } return <DateDisplay date={date} />; } }, ]); useEffect(() => { if (ganttRef.current && tasksRef.current !== tasks) { ganttRef.current.instance.taskStore.data = tasks; tasksRef.current = tasks; } }, [tasks]); return ( <div style={{ height: 500}}> <BryntumGantt ref={ganttRef} columns={columns} project={{ tasks: data }} {...config} /> </div> ); } function App() { const [data, setData] = useState([]); useEffect(() => { const loadData = async () => { await new Promise(resolve => setTimeout(resolve, 1000)); setData(Array.from({ length: 100 }, (_, i) => ({ id: i, name: `task ${i === 0 ? i : ''}`, start: i === 0 ? new Date(2024, 0, 1) : '', due: i === 0 ? new Date(2024,11, 30) : ''}))); } loadData(); }, []) return ( <div> <BryntumComponent data={data} /> </div> ); }

Post by ghulam.ghous »

Hi there,

This is the issue. As you know we use virtualization and reuse the same rows, so when a cls is applied to a row, the same cls exists on the rows causing the issue. You just need to remove it. In your case you can do it in the else part:

      renderer: (args) => {
        if (args.record.getData('start')) {
          args.row.addCls('myRow');
        }else{
                args.row.removeCls('myRow')
        }
        return <div>{args.value}</div>;
      },

This has solved the issue. You can make the logic more robust by checking if the row has already cls and no start date then just call the remove.

Regards,
Ghous


Post by jhill@achieveit.com »

That does work for the CSS class, but it's a bit more complex than just the CSS. There is "row pollution" when adding htmlEncode: false to the column renderer. If you run the below code, the first row only should have start / end dates. If you scroll to the bottom of the list, scroll back to top, and repeat that a few times you will see dates start being populated in other rows that should not have dates.

import './App.css';
import { useState, useRef, useMemo, useEffect } from 'react';
import '@bryntum/gantt/gantt.stockholm.css';
import { BryntumGantt } from '@bryntum/gantt-react';

const DateDisplay = ({ date }) => {
  return <span>{date.toDateString()}</span>
}

function BryntumComponent({ data }) {
  const ganttRef = useRef(null);

  const tasksRef = useRef();

  const tasks = useMemo(() => {
    return data.map((task) => {
      return {
        ...task,
        startDate: task.start,
        endDate: task.due,
        manuallyScheduled: true
      };
    });
  }, [data]);

  const [config] = useState({
    viewPreset: {
      timeResolution: { unit: 'day', increment: 1 },
      headers: [
        { unit: 'year', dateFormat: 'YYYY' },
        {
          unit: 'quarter',
          dateFormat: 'Q',
          renderer: (date, __, { value }) => {
            return `Q${value} ${date.getFullYear()}`;
          }
        },
        {
          unit: 'month',
          dateFormat: 'MMM'
        }
      ]
    },
    subGridConfigs: { locked: { width: '40%' } },
    autoAdjustTimeAxis: false,
    fixedRowHeight: false,
    cellEditFeature: false,
    cellMenuFeature: false,
    columnReorderFeature: false,
    taskMenuFeature: false,
    taskEditFeature: false,
    rowReorderFeature: false,
    columnLines: false,
    projectLinesFeature: false,
    headerMenuFeature: false,
    zoomOnTimeAxisDoubleClick: false,
    zoomOnMouseWheel: false,
    dependenciesFeature: { allowCreate: false },
    sortFeature: false
  })

  const [columns] = useState([
    {
      type: 'name',
      field: 'name',
      id: 'name',
      autoHeight: true,
      width: 100,
      renderer: (args) => {
        if (args.record.getData('start')) {
          args.row.addCls('myRow');
        } else {
          args.row.removeCls('myRow');
        }
        return <div>{args.value}</div>;
      },
      sortable: false,
      leafIconCls: null,
      htmlEncode: false
    },
    {
      field: 'start',
      id: 'start',
      text: 'Start Date',
      type: 'date',
      width: 100,
      htmlEncode: false,
      renderer: ({ record, value }) => {
  
// const date = record.getData('start'); if (!value) { return ''; } return <DateDisplay date={value} />; } }, { field: 'due', id: 'due', text: 'Due Date', type: 'date', width: 100, htmlEncode: false, renderer: ({ record, value }) => { // const date = record.getData('start'); if (!value) { return ''; } return <DateDisplay date={value} />; } }, ]); useEffect(() => { if (ganttRef.current && tasksRef.current !== tasks) { ganttRef.current.instance.taskStore.data = tasks; tasksRef.current = tasks; } }, [tasks]); return ( <div style={{ height: '100%', flex: 1, border: '1px solid black' }}> <BryntumGantt ref={ganttRef} columns={columns} project={{ tasks: data }} // projectLinesFeature={false} {...config} /> </div> ); } function App() { const [data, setData] = useState([]); useEffect(() => { const loadData = async () => { await new Promise(resolve => setTimeout(resolve, 1000)); setData(Array.from({ length: 100 }, (_, i) => ({ id: i, name: `task ${i === 0 ? i : ''}`, start: i === 0 ? new Date(2024, 0, 1) : '', due: i === 0 ? new Date(2024,11, 30) : ''}))); } loadData(); }, []) console.log('data', data); return ( <div style={{ height: '100%', display: 'flex', padding: 10}}> <BryntumComponent data={data} /> </div> ); } export default App;

Post by jhill@achieveit.com »

Attached is a screen recording illustrating the issue.


Post by jhill@achieveit.com »

For some reason there was an error attaching the recording, but if you run the above code you can re-create the issue.


Post by ghulam.ghous »

Hi there,

I have created a ticket https://github.com/bryntum/support/issues/8643 to fix this issue, meanwhile please use the workaround:

      renderer: ({ record, value }) => {

    if (!value) {
      return <span></span>;
    }

    return <DateDisplay date={value} />;
  },

Regards,
Ghous


Post by jhill@achieveit.com »

Thanks


Post by johan.isaksson »

Hi,

for performance reasons, we reuse rows and cells as you scroll the grid. And by default cells are not cleared before being reused, since DOM might have been directly manipulated in the renderer without us knowing about it. You can change this behavior by configuring your column with alwaysClearCell: true. Does that help?

From v6.0.0, this setting will be enabled by default.

Best regards,
Johan Isaksson

Post Reply