[Tex/LaTex] How to exclude or remove weekends from ganttchart

pgfgantttikz-pgf

Hi there and sorry for the incomplete question which only a psychic could answer 🙂

My problem is that I want to have my nice ganttchart or time schedule, but without including weekends. As my problem is, that if I set the dates for a project part for eg. 3 days and the next one too, than it occurs that the second one is longer as it includes the weekend.
Easiest to see on AP1100 and AP1200 which should be equal in length.
I would really appreciate help, thanks.

\documentclass[a4paper]{article}
\usepackage{fullpage}
\usepackage{lscape}
\usepackage{pgfgantt}

\begin{document}

\begin{landscape}
\noindent\resizebox*{\linewidth}{!}{ % Rescale the chart to linewidth
\begin{ganttchart}[hgrid,time slot format = isodate]{2014-05-12}{2014-09-14}

  \gantttitlecalendar{year, month=shortname, week}\\
  \ganttgroup[progress=00]     {AP 1000: test eee}{2014-05-12}{2014-05-23}\\    %
  \ganttbar[progress=00]       {AP 1100: test eee}{2014-05-12}{2014-05-14}\\
  \ganttlinkedbar[progress=00] {AP 1200: test eee}{2014-05-15}{2014-05-19}\\
    \ganttlinkedbar[progress=00] {AP 1300: test eee}{2014-05-20}{2014-05-23}\\

  \ganttgroup{AP 2000: test eee}{2014-05-26}{2014-06-11}\\
  \ganttbar  {AP 2100: test eee}{2014-05-26}{2014-05-28}\\
  \ganttbar  {AP 2200: test eee}{2014-05-29}{2014-05-30}\\
  \ganttbar  {AP 2300: test eee}{2014-06-02}{2014-06-06}\\
    \ganttbar  {AP 2400: test eee}{2014-06-09}{2014-06-11}\\

    \ganttgroup{AP 3000: test eee}{2014-06-12}{2014-08-13}\\
  \ganttbar  {AP 3100: test eee}{2014-06-12}{2014-07-17}\\
  \ganttbar  {AP 3200: test eee}{2014-07-18}{2014-07-21}\\
    \ganttbar  {AP 3300: test eee}{2014-07-22}{2014-08-05}\\
    \ganttbar  {AP 3300: test eee}{2014-08-06}{2014-08-13}\\

    \ganttgroup{AP 4000: test eee}{2014-08-14}{2014-08-29}\\
  \ganttbar  {AP 4100: test eee}{2014-08-14}{2014-08-20}\\
  \ganttbar  {AP 4200: test eee}{2014-08-21}{2014-08-27}\\
    \ganttbar  {AP 4300: test eee}{2014-08-28}{2014-08-29}\\

  \ganttgroup{AP 5000: test eee}{2014-09-01}{2014-09-12}\\
  \ganttbar  {AP 5100: test eee}{2014-09-01}{2014-09-05}\\
  \ganttbar  {AP 5200: test eee}{2014-09-08}{2014-09-10}\\
  \ganttbar  {AP 5300: test eee}{2014-09-11}{2014-09-12}\\

  \ganttmilestone{Status}{2014-05-12}\\

 \end{ganttchart}
}
\end{landscape}
\end{document}

Best Answer

Update: Marijn has indicated how to update this to work with gant 5.0 here.

The main trick is to redefine the way that pgfgantt indexes into the chart. The package takes a date in a format specified by saying time slot format = someformat and turns it into a "Julian day number" which is " the (integer) number of days that have elapsed since the initial epoch at noon Universal Time (UT) Monday, January 1, 4713 BC in the proleptic Julian calendar.”

Then pgfgantt takes the Julian day number of a given date and turns it into "time slot," which if the compress calendar option is not present, is just (date - start day + 1) where start day is the first day of the calendar:

\newcommand\gtt@juliantotimeslot[2]{%
  \begingroup%
  \@tempcnta=#1\relax%
  \ifgtt@compresscalendar % test for `compress calendar` option
     % do something here we don't care about
  \else % set \@tempcnta = current date - start date + 1 
    \advance\@tempcnta by-\gtt@startjulian\relax% 
    \advance\@tempcnta by1\relax%
  \fi%
  #2=\@tempcnta\relax % output the result into #2
  \gtt@smugglecount#2%
  \endgroup%
}

So we need to change this indexing command to turn a Julian calendar number into the number of weekdays between the start date of the chart and the current day.

The other change we need to make is to fix the \gantttitlecalendar command. This command iterates through the days between the start day and the end day and makes a header of width (end day) - (start day) + 1. We need to fix this so that the header only counts weekdays. The main work of this command is done in the following command:

\newcommand\@@@gantttitlecalendar[3]{%
  \pgfcalendarjuliantodate{#1}{\@tempa}{\@tempb}{\@tempc}%
  \edef\gtt@calendar@startdate{\@tempa-\@tempb-\@tempc}%
  \pgfcalendarjuliantodate{#2}{\@tempa}{\@tempb}{\@tempc}%
  \edef\gtt@calendar@enddate{\@tempa-\@tempb-\@tempc}%
  \gtt@calendar@eolfalse%
  \pgfqkeys{/pgfgantt/calendar}{#3}%
  \endgroup%
}

The arguments year, month, week, day, and weekday to \ganttitlecalendar are defined as keys in /pgfgantt/calendar. We are going to define new variants of these, in /pgfgantt/calendar week days only that don't count weekdays. This is a pretty minor change, for example the days code looks like:

day/.code={%
    \ifgtt@calendar@eol\ganttnewline\fi%
    \begingroup%
    \pgfcalendar{}{\gtt@calendar@startdate}{\gtt@calendar@enddate}{%
        %%% This is the command that draws the day:
        \gantttitle{\pgfcalendarcurrentday}{1}
    }%
    \endgroup%
    \gtt@calendar@eoltrue%
}

and we need to only draw the day if the day is a weekday. So we add the code

\ifnum\pgfcalendarcurrentweekday<5\relax % 0 -- 4 are Monday -- Friday
   \gantttitle{\pgfcalendarcurrentday}{1}
\fi

The changes to the other keys are pretty similar.

One last question is what happens when you give the code dates that are on the weekend. I set it up so that start days are rounded up to Monday and end days are rounded down to Friday. In order to do this, I have to make two variants of \gtt@juliantotimeslot: \gtt@juliantotimeslot@roundup and \gtt@juliantotimeslot@rounddown and patch these into the rest of the commands in the package where \gtt@juliantotimeslot is used.

Here's the full code:

\documentclass[a4paper]{article}
\usepackage{fullpage}
\usepackage{lscape}
\usepackage{etoolbox}
\usepackage{pgfgantt}
\parindent=0pt
\parskip=60pt

\makeatletter
% some extra count registers
\newcount\gtt@tempweekdaya
\newcount\gtt@tempweekdayb
\newcount\gtt@tempcounta
\newcount\gtt@tempcountb

% Define the "weekdays only" key
\ganttset{weekdays only/.code={% install the modified commands
    \let\gtt@juliantotimeslot@roundup\gtt@juliantotimeslot@roundup@weekdaysonly
    \let\gtt@juliantotimeslot@rounddown\gtt@juliantotimeslot@rounddown@weekdaysonly
    \let\@@@gantttitlecalendar\@@@gantttitlecalendar@weekdaysonly
}}

\def\gtt@patchcmd#1{\expandafter\patchcmd\csname\string#1\endcsname}

% Add check whether start date is on the weekend, if so round it up to Monday:
\gtt@patchcmd\ganttchart{\gtt@tsstojulian{#2}{\gtt@startjulian}}{%
    \gtt@tsstojulian{#2}{\gtt@startjulian}%
    \ifx\@@@gantttitlecalendar\@@@gantttitlecalendar@weekdaysonly % If weekdays only key is present
        \pgfcalendarjuliantoweekday{\gtt@startjulian}{\gtt@tempweekdaya}
        \ifnum\gtt@tempweekdaya>4\relax % and start date is a weekend
            \advance\gtt@startjulian7\relax
            \advance\gtt@startjulian-\gtt@tempweekdaya\relax % round to the nearest Monday
            \@gtt@PackageWarning{Given start date was on the weekend, rounding to the next Monday}
        \fi
    \fi
}{}{\error}

% Fix \gtt@juliantotimeslot calls to either refer to \gtt@juliantotimeslot@rounddown or \gtt@juliantotimeslot@roundup
\gtt@patchcmd\ganttchart{\gtt@juliantotimeslot{\gtt@endjulian}}{\gtt@juliantotimeslot@rounddown{\gtt@endjulian}}{}{\error}
\gtt@patchcmd\ganttchart{\gtt@juliantotimeslot{\gtt@today@slot}}{\gtt@juliantotimeslot@rounddown{\gtt@today@slot}}{}{\error}
\gtt@patchcmd\gtt@chartelement{\gtt@juliantotimeslot{\gtt@left@slot}}{\gtt@juliantotimeslot@roundup{\gtt@left@slot}}{}{\error}
\gtt@patchcmd\gtt@chartelement{\gtt@juliantotimeslot{\gtt@right@slot}}{\gtt@juliantotimeslot@rounddown{\gtt@right@slot}}{}{\error}

% Both rounddown and roundup by default are just synonyms for \gtt@juliantotimeslot
\let\gtt@juliantotimeslot@rounddown\gtt@juliantotimeslot
\let\gtt@juliantotimeslot@roundup\gtt@juliantotimeslot

% \gtt@juliantotimeslot computes the number of days between the start date and the Julian day #1 and stores the result in #2.
% Our modified versions compute the number of weekdays between the start date and the Julian day #1 and stores the result in #2.
% \gtt@juliantotimeslot@roundup rounds weekend days to the next Monday
% \gtt@juliantotimeslot@rounddown rounds weekend days to the previous Monday
\newcommand\gtt@juliantotimeslot@roundup@weekdaysonly[2]{
    \begingroup
    \@tempcnta=#1\relax%
    \pgfcalendarjuliantoweekday{\@tempcnta}{\gtt@tempweekdaya}
    \ifnum\gtt@tempweekdaya>4\relax % if start date is a weekend
        \advance\@tempcnta7\relax
        \advance\@tempcnta-\gtt@tempweekdaya\relax % round to the nearest Monday
        \@gtt@PackageWarning{Given start date was on the weekend, rounding to the next Monday}
    \fi
    \gtt@tempcounta=\@tempcnta\relax
    \gtt@juliantotimeslot@rounddown@weekdaysonly{\gtt@tempcounta}{\gtt@tempcountb}
    #2=\gtt@tempcountb\relax
    \gtt@smugglecount#2%
    \endgroup%
}    

\newcommand\gtt@juliantotimeslot@rounddown@weekdaysonly[2]{%
  \begingroup%
  \@tempcnta=#1\relax%
  \ifgtt@compresscalendar%
    \pgfcalendarjuliantodate{\@tempcnta}{\@tempa}{\@tempb}{\@tempc}%
    \@tempcnta=\@tempa\relax%
    \advance\@tempcnta by-\gtt@startyear\relax%
    \multiply\@tempcnta by12\relax%
    \advance\@tempcnta by\@tempb\relax%
    \advance\@tempcnta by-\gtt@startmonth\relax%
    \advance\@tempcnta by1\relax%
  \else%
    \pgfcalendarjuliantoweekday{\gtt@startjulian}{\gtt@tempweekdaya} % Put the day of the week of the start and end days into counters
    \pgfcalendarjuliantoweekday{\@tempcnta}{\gtt@tempweekdayb}
    \ifnum\gtt@tempweekdayb>4\relax
        \@gtt@PackageWarning{Weekend date rounded down to the preceding Friday}
    \fi
    
    \advance\@tempcnta by-\gtt@startjulian\relax % Now \@tempcnta has the number of days in the period minus one
    \divide\@tempcnta7\relax % Now it has how many whole weeks occur
    \multiply\@tempcnta5\relax % Now it has the number of weekdays that occur in whole weeks minus one
    \advance\@tempcnta1\relax % Now the number of weekdays that occur in whole weeks
    %
    % Okay now let's correct for the last partial week. First we need to calculate how many days we have left over.
    \@tempcntb=#1\relax% 
    \advance\@tempcntb by-\gtt@startjulian\relax% Number of days
    \divide\@tempcntb7\relax
    \multiply\@tempcntb-7\relax % negative number of days that occur in whole weaks
    \advance\@tempcntb#1\relax
    \advance\@tempcntb by-\gtt@startjulian\relax% Number of left over days
    \advance\@tempcnta\@tempcntb % \@tempcnta has (weekdays occurring in whole weeks) + (all left over days). 
    %
    % We still need to subtract off the left over days that land on weekends.
    \ifnum\gtt@tempweekdayb<\gtt@tempweekdaya\relax
        \advance\gtt@tempweekdayb7\relax % make sure the end day is greater than or equal to start day
    \fi
    %
    % Is Saturday in left over days?
    \@tempcntb=5\relax % Saturday is day 5
    \ifnum\@tempcntb<\gtt@tempweekdaya\relax
        \advance\@tempcntb7\relax % make sure Saturday is greater than or equal to start day
    \fi
    \ifnum\@tempcntb>\gtt@tempweekdayb\relax\else % If Saturday is one of left over days
        \advance\@tempcnta-1\relax % subtract it
    \fi
    % Is Sunday in left over days?
    \@tempcntb=6 % Sunday is day 6
    \ifnum\@tempcntb<\gtt@tempweekdaya\relax
        \advance\@tempcntb7\relax
    \fi
    \ifnum\@tempcntb>\gtt@tempweekdayb\relax\else
        \advance\@tempcnta-1\relax 
    \fi
  \fi%
  #2=\@tempcnta\relax%

  \gtt@smugglecount#2%
  \endgroup%
}

% Now it's time to fix \ganttitlecalendar
% Our modified version of \ganttitlecalendar passes control to "/pgfgantt/calendar weekdays only" instead of "/pgfgantt/calendar"
\newcommand\@@@gantttitlecalendar@weekdaysonly[3]{%
  \pgfcalendarjuliantodate{#1}{\@tempa}{\@tempb}{\@tempc}%
  \edef\gtt@calendar@startdate{\@tempa-\@tempb-\@tempc}%
  \pgfcalendarjuliantodate{#2}{\@tempa}{\@tempb}{\@tempc}%
  \edef\gtt@calendar@enddate{\@tempa-\@tempb-\@tempc}%
  \gtt@calendar@eolfalse%
  \pgfqkeys{/pgfgantt/calendar weekdays only}{#3}% Only difference is here 
  \endgroup%
}

% Here is the modified calendar printing code. It's mostly the same as the original code, with a bunch of extra tests for weekdays and 
% some minor differences in the edge cases
\pgfqkeys{/pgfgantt/calendar weekdays only}{
  year/.code={%
    \ifgtt@calendar@eol\ganttnewline\fi%
    \begingroup%
    \gtt@calendar@slots=0\relax%
    \ifgtt@compresscalendar%
      \pgfcalendar{}{\gtt@calendar@startdate}{\gtt@calendar@enddate}{%
        \ifdate{equals=12-31}{%
          \advance\gtt@calendar@slots by1\relax %added this line 
          \gantttitle{\pgfcalendarcurrentyear}{\the\gtt@calendar@slots}%
          \gtt@calendar@slots=1\relax%
        }{%
          \ifdate{end of month=1}{%
            \advance\gtt@calendar@slots by1\relax%
          }{}%
        }%
        \ifdate{equals=\pgfcalendarendiso}{%
          \ifdate{end of month=1}{%
            % Used to be a decrement here
          }{}%
          \ifdate{equals=12-31}{}{%
            \gantttitle{\pgfcalendarcurrentyear}{\the\gtt@calendar@slots}%
          }%
        }{}%
      }%
    \else%
      \pgfcalendar{}{\gtt@calendar@startdate}{\gtt@calendar@enddate}{%
        \ifdate{equals=12-31}{%
          \ifnum\pgfcalendarcurrentweekday<5\relax % Only increment if it's a weekday
            \advance\gtt@calendar@slots by1\relax%
          \fi
          \gantttitle{\pgfcalendarcurrentyear}{\the\gtt@calendar@slots}%
          \gtt@calendar@slots=0\relax% used to be 1
        }{%
          \ifnum\pgfcalendarcurrentweekday<5\relax % Only increment if it's a weekday
            \advance\gtt@calendar@slots by1\relax%
          \fi
        }%
        \ifdate{equals=\pgfcalendarendiso}{%
          \ifnum\gtt@calendar@slots=1\relax\else%
            % Used to be a decrement here
            \gantttitle{\pgfcalendarcurrentyear}{\the\gtt@calendar@slots}%
          \fi%
        }{}%
      }%
    \fi%
    \endgroup%
    \gtt@calendar@eoltrue%
  },%
  month/.code={%
    \ifgtt@calendar@eol\ganttnewline\fi%
    \begingroup%
    \gtt@calendar@slots=0\relax
    \pgfcalendar{}{\gtt@calendar@startdate}{\gtt@calendar@enddate}{%
      \ifdate{end of month=1}{%
        \ifnum\pgfcalendarcurrentweekday<5\relax % Only increment if it's a weekday
            \advance\gtt@calendar@slots by1\relax%
        \fi
        \gantttitle{%
          \csname pgfcalendarmonth#1\endcsname{\pgfcalendarcurrentmonth}%
        }{%
          \ifgtt@compresscalendar1\else\the\gtt@calendar@slots\fi%
        }%
        \gtt@calendar@slots=0\relax% used to be 1
      }{%
        \ifnum\pgfcalendarcurrentweekday<5\relax % Only increment if it's a weekday
            \advance\gtt@calendar@slots by1\relax%
        \fi
      }%
      \ifdate{equals=\pgfcalendarendiso}{%
        \ifnum\gtt@calendar@slots=0\relax\else % used to test for 1
          % Used to be a decrement here
          \gantttitle{%
            \csname pgfcalendarmonth#1\endcsname{\pgfcalendarcurrentmonth}%
          }{%
            \ifgtt@compresscalendar1\else\the\gtt@calendar@slots\fi%
          }%
        \fi%
      }{}%
    }%
    \endgroup%
    \gtt@calendar@eoltrue%
  },%
  week/.code={%
    \ifgtt@calendar@eol\ganttnewline\fi%
    \begingroup%
    \gtt@calendar@slots=0\relax%
    \gtt@calendar@weeknumber=#1\relax%
    \pgfcalendar{}{\gtt@calendar@startdate}{\gtt@calendar@enddate}{%
      \ifdate{Sunday}{%
        \gtt@calendar@startofweek=\pgfcalendarcurrentjulian\relax%
        \advance\gtt@calendar@startofweek by1\relax%
        \advance\gtt@calendar@startofweek by-\gtt@calendar@slots\relax%
        \pgfcalendarjuliantodate{\gtt@calendar@startofweek}%
          {\startyear}{\startmonth}{\startday}%
        \def\currentweek{\the\gtt@calendar@weeknumber}%
        \gantttitle{%
          \ganttvalueof{calendar week text}%
        }{%
          \the\gtt@calendar@slots%
        }%
        \gtt@calendar@slots=0\relax% Used to be 1
        \advance\gtt@calendar@weeknumber by1\relax%
      }{%
        \ifnum\pgfcalendarcurrentweekday<5\relax % Only increment if it's a weekday
            \advance\gtt@calendar@slots by1
        \fi
      }%
      \ifdate{equals=\pgfcalendarendiso}{%
        \ifnum\gtt@calendar@slots=0\relax\else% used to test for 1
          % Deleted decrement line used to be here
          \gtt@calendar@startofweek=\pgfcalendarcurrentjulian\relax%
          \advance\gtt@calendar@startofweek by1\relax%
          \advance\gtt@calendar@startofweek by-\gtt@calendar@slots\relax%
          \pgfcalendarjuliantodate{\gtt@calendar@startofweek}%
            {\startyear}{\startmonth}{\startday}%
          \def\currentweek{\the\gtt@calendar@weeknumber}%
          \gantttitle{%
            \ganttvalueof{calendar week text}%
          }{%
            \the\gtt@calendar@slots%
          }%
        \fi%
      }{}%
    }%
    \endgroup%
    \gtt@calendar@eoltrue%
  },%
  week/.default=1,
  weekday/.code={%
    \ifgtt@calendar@eol\ganttnewline\fi%
    \begingroup%
    \pgfcalendar{}{\gtt@calendar@startdate}{\gtt@calendar@enddate}{%
        \ifnum\pgfcalendarcurrentweekday<5\relax % only increment if it's a weekday
          \gantttitle{%
            \csname pgfcalendarweekday#1\endcsname{\pgfcalendarcurrentweekday}%
          }{1}%
        \fi
    }%
    \endgroup%
    \gtt@calendar@eoltrue%
  },
  day/.code={%
    \ifgtt@calendar@eol\ganttnewline\fi%
    \begingroup%
    \pgfcalendar{}{\gtt@calendar@startdate}{\gtt@calendar@enddate}{%
        \ifnum\pgfcalendarcurrentweekday<5\relax % only increment if it's a weekday
            \gantttitle{\pgfcalendarcurrentday}{1}
        \fi
    }%
    \endgroup%
    \gtt@calendar@eoltrue%
  }%
}
\makeatother


\begin{document}
\def\pgfcalendarweekdayletter#1{\ifcase#1M\or Tu\or W\or Th\or F\or Sa\or Su\fi}


\begin{ganttchart}[
    time slot format=little-endian,
    progress=today,
    today=4.5.13,
    calendar week text = {W~\currentweek},
]{1.5.13}{14.5.13}
      \gantttitlecalendar{week,day,weekday=letter}\\
\ganttgroup{Group 1}{1.5.13}{14.5.13} \\
\ganttbar{Subtask 1}{1.5.13}{3.5.13} \\
\ganttbar{Subtask 2}{3.5.13}{8.5.13} \\
\ganttbar{Subtask 3}{9.5.13}{14.5.13}
\end{ganttchart}


\begin{ganttchart}[
    time slot format=little-endian,
    progress=today,
    today=4.5.13,
    calendar week text = {W~\currentweek},
    weekdays only
]{1.5.13}{14.5.13}
      \gantttitlecalendar{week,day,weekday=letter}\\
\ganttgroup{Group 1}{1.5.13}{14.5.13} \\
\ganttbar{Subtask 1}{1.5.13}{3.5.13} \\
\ganttbar{Subtask 2}{3.5.13}{8.5.13} \\
\ganttbar{Subtask 3}{9.5.13}{14.5.13}
\end{ganttchart}

\newpage

\begin{ganttchart}[
    time slot format=little-endian,
    progress=today,
    today=3.12.16,
    calendar week text = {W~\currentweek},
]{26.11.16}{11.12.16}
      \gantttitlecalendar{week,day,weekday=letter}\\
\ganttgroup{Group 1}{26.11.16}{11.12.16} \\
\ganttbar{Subtask 1}{26.11.16}{3.12.16} \\
\ganttbar{Subtask 2}{3.12.16}{11.12.16} \\
\end{ganttchart}

\begin{ganttchart}[
    time slot format=little-endian,
    progress=today,
    today=3.12.16,
    calendar week text = {W~\currentweek},
    weekdays only
]{26.11.16}{11.12.16}
      \gantttitlecalendar{week,day,weekday=letter}\\
\ganttgroup{Group 1}{26.11.16}{11.12.16} \\
\ganttbar{Subtask 1}{26.11.16}{3.12.16} \\
\ganttbar{Subtask 2}{3.12.16}{11.12.16} \\
\end{ganttchart}

\end{document} 

Here's the output (same input, first without and then with the weekdays only key):

enter image description here

Related Question