From 5c3384dceb8d9633494944893f8d0c2834bee3eb Mon Sep 17 00:00:00 2001 From: William O'Mullane Date: Fri, 9 Dec 2022 12:05:04 -0300 Subject: [PATCH 01/12] have two updates for docs with and without automerge .. --- .github/workflows/trigger-updates.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/trigger-updates.yaml b/.github/workflows/trigger-updates.yaml index 0af6cec..1566273 100644 --- a/.github/workflows/trigger-updates.yaml +++ b/.github/workflows/trigger-updates.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - repo: [lsst-sitcom/sitcomtn-052, lsst-dm/dmtn-158, lsst/ldm-564, lsst/ldm-503, lsst-dm/dmtn-232] + repo: [lsst-sitcom/sitcomtn-052, lsst-dm/dmtn-158, lsst/ldm-564, lsst/ldm-503] steps: - uses: actions/checkout@v3 with: @@ -61,7 +61,7 @@ jobs: # auto merge for dmtn-232 is a too public so will manually check and merge it - name: Enable Pull Request Automerge - if: steps.cpr.outputs.pull-request-operation == 'created' and matrix.repo != 'lsst-dm/dmtn-232' + if: steps.cpr.outputs.pull-request-operation == 'created' uses: peter-evans/enable-pull-request-automerge@v1 # requires branch protection with: From 6cd2022f52c11721b5341278e5153d51d6246490 Mon Sep 17 00:00:00 2001 From: William O'Mullane Date: Fri, 9 Dec 2022 12:50:09 -0300 Subject: [PATCH 02/12] lists summary --- Makefile | 6 ++++++ data/pmcs/202210-ME.xls | 4 ++-- milestones.py | 7 +++++++ milestones/__init__.py | 1 + milestones/blockschedule.py | 16 ++++++++++++++++ milestones/excel.py | 4 +++- milestones/milestone.py | 1 + 7 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 milestones/blockschedule.py diff --git a/Makefile b/Makefile index 6a858b4..9959764 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,11 @@ VENVDIR = venv +blockschedule.pdf: venv + ( \ + source $(VENVDIR)/bin/activate; \ + python milestones.py blockschedule; \ + ) + burndown.png: venv ( \ source $(VENVDIR)/bin/activate; \ diff --git a/data/pmcs/202210-ME.xls b/data/pmcs/202210-ME.xls index d02f645..60dc4c4 100644 --- a/data/pmcs/202210-ME.xls +++ b/data/pmcs/202210-ME.xls @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12ec9fea9976ea7d1a677f764e6d09e2f6cf0c1d4072c27893d549c372c7a68f -size 4102144 +oid sha256:9aa8f893e20b9e7e3b33311489b00cc5a906f691ed76a182ed810c30449f8f3c +size 4105216 diff --git a/milestones.py b/milestones.py index 9b27f6c..9e9e032 100644 --- a/milestones.py +++ b/milestones.py @@ -131,6 +131,13 @@ def parse_args(): ) graph.set_defaults(func=milestones.graph) + # RHL blockchart + blockschedule = subparsers.add_parser( + "blockschedule", help="Generate the summry block schedule." + ) + blockschedule.add_argument("--output", help="Filename for output", default="blockschedule.pdf") + blockschedule.set_defaults(func=milestones.blockschedule) + args = parser.parse_args() log_levels = [logging.WARN, logging.INFO, logging.DEBUG, logging.NOTSET] diff --git a/milestones/__init__.py b/milestones/__init__.py index 1e0762d..0244556 100644 --- a/milestones/__init__.py +++ b/milestones/__init__.py @@ -1,4 +1,5 @@ from .burndown import * +from .blockschedule import * from .csv import * from .celeb import * from .delayed import * diff --git a/milestones/blockschedule.py b/milestones/blockschedule.py new file mode 100644 index 0000000..0c8c50c --- /dev/null +++ b/milestones/blockschedule.py @@ -0,0 +1,16 @@ +from io import StringIO +from .utility import get_version_info, write_output, load_milestones + + + + +def blockschedule(args, milestones): + # pullout Summary Chart milestones + milestones = [ + ms + for ms in milestones + if ms.summarychart + ] + + for ms in milestones: + print (f"{ms.summarychart}, {ms.due}") diff --git a/milestones/excel.py b/milestones/excel.py index 1c46c32..d317794 100644 --- a/milestones/excel.py +++ b/milestones/excel.py @@ -126,9 +126,11 @@ def extract_task_details(task_sheet): wbs = extract_wbs(fetcher("wbs_id", task_sheet.row(rownum))) celebrate = fetcher("actv_code_celebratory_achievements_id", task_sheet.row(rownum)) + summarychart = fetcher("actv_code_summary_chart_id", + task_sheet.row(rownum)) milestones.append(Milestone(code, name, wbs, level, due, fdue, - completed, celebrate)) + completed, celebrate, summarychart)) return milestones diff --git a/milestones/milestone.py b/milestones/milestone.py index 7447549..ed5771f 100644 --- a/milestones/milestone.py +++ b/milestones/milestone.py @@ -18,6 +18,7 @@ class Milestone(object): fdue: datetime completed: Optional[datetime] = None celebrate: Optional[str] = "" + summarychart: Optional[str] = "" predecessors: Set[str] = field(default_factory=set) successors: Set[str] = field(default_factory=set) From 9084256f1125652732d5284ced0b0f41d0fb906b Mon Sep 17 00:00:00 2001 From: William O'Mullane Date: Fri, 9 Dec 2022 13:53:22 -0300 Subject: [PATCH 03/12] tidy up --- data/pmcs/202210-ME.xls | 2 +- milestones.py | 3 ++- milestones/blockschedule.py | 8 +++----- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/data/pmcs/202210-ME.xls b/data/pmcs/202210-ME.xls index 60dc4c4..8706a5e 100644 --- a/data/pmcs/202210-ME.xls +++ b/data/pmcs/202210-ME.xls @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9aa8f893e20b9e7e3b33311489b00cc5a906f691ed76a182ed810c30449f8f3c +oid sha256:663d942ea9c5e27990171c5cbc8d5beb5eedadb23c6b4b24e3e3346c467e6765 size 4105216 diff --git a/milestones.py b/milestones.py index 9e9e032..807074f 100644 --- a/milestones.py +++ b/milestones.py @@ -135,7 +135,8 @@ def parse_args(): blockschedule = subparsers.add_parser( "blockschedule", help="Generate the summry block schedule." ) - blockschedule.add_argument("--output", help="Filename for output", default="blockschedule.pdf") + blockschedule.add_argument("--output", help="Filename for output", + default="blockschedule.pdf") blockschedule.set_defaults(func=milestones.blockschedule) args = parser.parse_args() diff --git a/milestones/blockschedule.py b/milestones/blockschedule.py index 0c8c50c..ce70366 100644 --- a/milestones/blockschedule.py +++ b/milestones/blockschedule.py @@ -1,7 +1,5 @@ -from io import StringIO -from .utility import get_version_info, write_output, load_milestones - - +# RHL generate the block schedule diagram from the milestones with +# Summary Chart entries. def blockschedule(args, milestones): @@ -13,4 +11,4 @@ def blockschedule(args, milestones): ] for ms in milestones: - print (f"{ms.summarychart}, {ms.due}") + print(f"{ms.summarychart}, {ms.due}") From de07ace85d6c4d655311f3c3890db1d14d8f4adc Mon Sep 17 00:00:00 2001 From: William O'Mullane Date: Fri, 9 Dec 2022 13:56:14 -0300 Subject: [PATCH 04/12] tidy up --- milestones/excel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/milestones/excel.py b/milestones/excel.py index d317794..c35a70a 100644 --- a/milestones/excel.py +++ b/milestones/excel.py @@ -127,7 +127,7 @@ def extract_task_details(task_sheet): celebrate = fetcher("actv_code_celebratory_achievements_id", task_sheet.row(rownum)) summarychart = fetcher("actv_code_summary_chart_id", - task_sheet.row(rownum)) + task_sheet.row(rownum)) milestones.append(Milestone(code, name, wbs, level, due, fdue, completed, celebrate, summarychart)) From 3cf511d9127cb827e0a199672acd71e922ce92ac Mon Sep 17 00:00:00 2001 From: William O'Mullane Date: Fri, 9 Dec 2022 15:22:39 -0300 Subject: [PATCH 05/12] all tasks --- data/pmcs/202210-ME.xls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/pmcs/202210-ME.xls b/data/pmcs/202210-ME.xls index 8706a5e..50d9634 100644 --- a/data/pmcs/202210-ME.xls +++ b/data/pmcs/202210-ME.xls @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:663d942ea9c5e27990171c5cbc8d5beb5eedadb23c6b4b24e3e3346c467e6765 -size 4105216 +oid sha256:bedcb0745d0f329c878b60ac759358144a4abba24adc3bea4abc329df3b3d484 +size 5847040 From 28b91a6e45f72cd38d22f294f3373823052fb72c Mon Sep 17 00:00:00 2001 From: William O'Mullane Date: Fri, 9 Dec 2022 16:45:49 -0300 Subject: [PATCH 06/12] deal with milestone filter --- data/pmcs/202210-ME.xls | 4 ++-- milestones.py | 4 +++- milestones/excel.py | 3 ++- milestones/milestone.py | 1 + milestones/utility.py | 4 +++- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/data/pmcs/202210-ME.xls b/data/pmcs/202210-ME.xls index 50d9634..7635699 100644 --- a/data/pmcs/202210-ME.xls +++ b/data/pmcs/202210-ME.xls @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bedcb0745d0f329c878b60ac759358144a4abba24adc3bea4abc329df3b3d484 -size 5847040 +oid sha256:16f045c2469eb5a2650ab0b1dff516b0e3a4d56f1d11836e3e32b52e02d17b9c +size 5999616 diff --git a/milestones.py b/milestones.py index 807074f..770f10c 100644 --- a/milestones.py +++ b/milestones.py @@ -153,7 +153,9 @@ def parse_args(): if __name__ == "__main__": args = parse_args() print("Working with "+args.pmcs_data) - milestones = milestones.load_milestones(args.pmcs_data, args.local_data) + load_tasks = (args.func == milestones.blockschedule) + milestones = milestones.load_milestones(args.pmcs_data, args.local_data, + load_tasks) if "months" in args and args.months > 0: fpath = get_pmcs_path_months(args.pmcs_data, args.months) load_f2due_pmcs_excel(fpath, milestones) diff --git a/milestones/excel.py b/milestones/excel.py index c35a70a..2766fc4 100644 --- a/milestones/excel.py +++ b/milestones/excel.py @@ -75,6 +75,7 @@ def extract_task_details(task_sheet): fetcher = CellFetcher(task_sheet.row(0)) for rownum in range(START_ROW, task_sheet.nrows): code = fetcher("task_code", task_sheet.row(rownum)) + tasktype = fetcher("task_type", task_sheet.row(rownum)) name = fetcher("task_name", task_sheet.row(rownum)) # "user_field_859" is just a magic value extracted from the spreadsheet @@ -129,7 +130,7 @@ def extract_task_details(task_sheet): summarychart = fetcher("actv_code_summary_chart_id", task_sheet.row(rownum)) - milestones.append(Milestone(code, name, wbs, level, due, fdue, + milestones.append(Milestone(code, tasktype, name, wbs, level, due, fdue, completed, celebrate, summarychart)) return milestones diff --git a/milestones/milestone.py b/milestones/milestone.py index ed5771f..36b7270 100644 --- a/milestones/milestone.py +++ b/milestones/milestone.py @@ -11,6 +11,7 @@ class Milestone(object): # Sound a warning if we override locally code: str + tasktype: str name: str wbs: str level: Optional[int] diff --git a/milestones/utility.py b/milestones/utility.py index 0123434..cc22b7b 100644 --- a/milestones/utility.py +++ b/milestones/utility.py @@ -125,7 +125,7 @@ def add_rst_citations(text, cite_handles=DOC_HANDLES): return add_citations(text, cite_handles, r"\1 :cite:`\1`") -def load_milestones(pmcs_filename, local_data_filename): +def load_milestones(pmcs_filename, local_data_filename, load_tasks=False): logger = logging.getLogger(__name__) logger.info(f"Loading PMCS data from: {pmcs_filename}") @@ -135,6 +135,8 @@ def load_milestones(pmcs_filename, local_data_filename): with open(local_data_filename) as f: local = yaml.safe_load(f) for ms in milestones: + if "Milestone" not in ms.tasktype and load_tasks: + continue if ms.code in local: # These are core PMCS attributes; we should warn if we # over-write them. From 39e64357b62dcf6b76f867a1e72f61e27e973e1a Mon Sep 17 00:00:00 2001 From: William O'Mullane Date: Fri, 9 Dec 2022 17:00:43 -0300 Subject: [PATCH 07/12] better filter at load for milestones --- milestones/excel.py | 11 +++++++---- milestones/utility.py | 4 +--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/milestones/excel.py b/milestones/excel.py index 2766fc4..e5b5dd9 100644 --- a/milestones/excel.py +++ b/milestones/excel.py @@ -69,13 +69,16 @@ def extract_fcast(task_sheet, milestones): return milestones -def extract_task_details(task_sheet): +def extract_task_details(task_sheet, load_tasks): assert task_sheet.name == TASK_SHEET_NAME milestones = list() fetcher = CellFetcher(task_sheet.row(0)) for rownum in range(START_ROW, task_sheet.nrows): - code = fetcher("task_code", task_sheet.row(rownum)) tasktype = fetcher("task_type", task_sheet.row(rownum)) + # File now has milestones and tasks many things only want milestones + if not (load_tasks or "Milestone" in tasktype): + continue + code = fetcher("task_code", task_sheet.row(rownum)) name = fetcher("task_name", task_sheet.row(rownum)) # "user_field_859" is just a magic value extracted from the spreadsheet @@ -149,9 +152,9 @@ def set_successors(milestones, relation_sheet): ms.predecessors.add(preds[i]) -def load_pmcs_excel(path): +def load_pmcs_excel(path, load_tasks=False): workbook = xlrd.open_workbook(path, logfile=sys.stderr) - milestones = extract_task_details(workbook.sheets()[0]) + milestones = extract_task_details(workbook.sheets()[0], load_tasks) set_successors(milestones, workbook.sheets()[1]) return milestones diff --git a/milestones/utility.py b/milestones/utility.py index cc22b7b..86b3dcf 100644 --- a/milestones/utility.py +++ b/milestones/utility.py @@ -130,13 +130,11 @@ def load_milestones(pmcs_filename, local_data_filename, load_tasks=False): logger.info(f"Loading PMCS data from: {pmcs_filename}") logger.info(f"Loading local annotations from: {local_data_filename}") - milestones = load_pmcs_excel(pmcs_filename) + milestones = load_pmcs_excel(pmcs_filename, load_tasks) with open(local_data_filename) as f: local = yaml.safe_load(f) for ms in milestones: - if "Milestone" not in ms.tasktype and load_tasks: - continue if ms.code in local: # These are core PMCS attributes; we should warn if we # over-write them. From 54f3d50ec138f6453c51c47b765e3b5314eee4fd Mon Sep 17 00:00:00 2001 From: Robert Lupton the Good Date: Fri, 9 Dec 2022 20:21:06 -0500 Subject: [PATCH 08/12] Don't print the actions when making blockschedule --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 9959764..c2ee33f 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ VENVDIR = venv blockschedule.pdf: venv - ( \ + @( \ source $(VENVDIR)/bin/activate; \ python milestones.py blockschedule; \ ) From aaf4bf29800456297c8f193fb32b3e8da08828d8 Mon Sep 17 00:00:00 2001 From: Robert Lupton the Good Date: Fri, 9 Dec 2022 20:21:59 -0500 Subject: [PATCH 09/12] Include start in Milestone --- milestones/excel.py | 11 ++++++++++- milestones/milestone.py | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/milestones/excel.py b/milestones/excel.py index e5b5dd9..e63ac87 100644 --- a/milestones/excel.py +++ b/milestones/excel.py @@ -85,6 +85,15 @@ def extract_task_details(task_sheet, load_tasks): level = fetcher("user_field_859", task_sheet.row(rownum)) level = int(level) if level else None + date_field = "start_date" + d = fetcher(date_field, task_sheet.row(rownum)) + start = None + if d: + try: + start = extract_date(d) + except ValueError: + pass + # There are three possible end dates: # # base_end_date - according to the baseline project @@ -134,7 +143,7 @@ def extract_task_details(task_sheet, load_tasks): task_sheet.row(rownum)) milestones.append(Milestone(code, tasktype, name, wbs, level, due, fdue, - completed, celebrate, summarychart)) + start, completed, celebrate, summarychart)) return milestones diff --git a/milestones/milestone.py b/milestones/milestone.py index 36b7270..e345a62 100644 --- a/milestones/milestone.py +++ b/milestones/milestone.py @@ -17,6 +17,7 @@ class Milestone(object): level: Optional[int] due: datetime fdue: datetime + start: Optional[datetime] = None completed: Optional[datetime] = None celebrate: Optional[str] = "" summarychart: Optional[str] = "" From 4db42c9ba8c7cb85d4cc8e18c088dcacfb18f981 Mon Sep 17 00:00:00 2001 From: Robert Lupton the Good Date: Fri, 9 Dec 2022 20:25:46 -0500 Subject: [PATCH 10/12] Initial version. Write a csv file that activities code can process N.b. the aforementioned activities is not yet added to this package --- milestones/blockschedule.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/milestones/blockschedule.py b/milestones/blockschedule.py index ce70366..a9b07b6 100644 --- a/milestones/blockschedule.py +++ b/milestones/blockschedule.py @@ -3,12 +3,16 @@ def blockschedule(args, milestones): - # pullout Summary Chart milestones - milestones = [ - ms - for ms in milestones - if ms.summarychart - ] + # Process Summary Chart activities/milestones and celebratory milestones + print("Summary Chart, Code, Start, Finish, Celebrate") for ms in milestones: - print(f"{ms.summarychart}, {ms.due}") + celebrate = False + if ms.summarychart: + pass + elif ms.celebrate: + celebrate = ms.celebrate + else: + continue + + print(f"{ms.summarychart}, {ms.code}, {ms.start}, {ms.due}, {celebrate}") From 148eddea15b4f767a2055e75a4edecbe45712603 Mon Sep 17 00:00:00 2001 From: Robert Lupton the Good Date: Sat, 10 Dec 2022 13:23:34 -0500 Subject: [PATCH 11/12] Initial support for building the LSST status cartoon --- .gitignore | 1 + Makefile | 2 +- milestones.py | 8 +- milestones/blockschedule.py | 158 +++++++- milestones/cartoon.py | 720 +++++++++++++++++++++++++++++++++++ milestones/cartoon_config.py | 70 ++++ 6 files changed, 953 insertions(+), 6 deletions(-) create mode 100644 milestones/cartoon.py create mode 100644 milestones/cartoon_config.py diff --git a/.gitignore b/.gitignore index 78332f9..88c1fb3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ __pycache__ venv gantt.* .DS_Store +cartoon.pdf diff --git a/Makefile b/Makefile index c2ee33f..2ef8ca3 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ VENVDIR = venv blockschedule.pdf: venv @( \ source $(VENVDIR)/bin/activate; \ - python milestones.py blockschedule; \ + python milestones.py blockschedule --start-date -20 \ ) burndown.png: venv diff --git a/milestones.py b/milestones.py index 770f10c..4774d74 100644 --- a/milestones.py +++ b/milestones.py @@ -131,12 +131,16 @@ def parse_args(): ) graph.set_defaults(func=milestones.graph) - # RHL blockchart + # RHL cartoon based on P6 "summary chart" and "celebratory milestone" entries blockschedule = subparsers.add_parser( - "blockschedule", help="Generate the summry block schedule." + "blockschedule", help="Generate the cartoon of the schedule." ) blockschedule.add_argument("--output", help="Filename for output", default="blockschedule.pdf") + blockschedule.add_argument("--start-date", help="Starting date for cartoon (ISO 8601 format)") + blockschedule.add_argument("--end-date", help="Ending date for cartoon (ISO 8601 format)") + blockschedule.add_argument("--show-weeks", help="Show week boundaries", action='store_true', + default=False) blockschedule.set_defaults(func=milestones.blockschedule) args = parser.parse_args() diff --git a/milestones/blockschedule.py b/milestones/blockschedule.py index a9b07b6..1be1938 100644 --- a/milestones/blockschedule.py +++ b/milestones/blockschedule.py @@ -1,18 +1,170 @@ # RHL generate the block schedule diagram from the milestones with # Summary Chart entries. +import os +import sys +import textwrap + +import numpy as np +import matplotlib.pyplot as plt + +from .cartoon import * +from .cartoon_config import \ + categoryNrow, categoryColors, wrappedDescrip, categoryGrouping, specials, \ + nRowOfMilestones, milestoneHeight, legend_location + def blockschedule(args, milestones): # Process Summary Chart activities/milestones and celebratory milestones - print("Summary Chart, Code, Start, Finish, Celebrate") + activities, celebrations = process_milestones(milestones) + + blocks = create_blocks(activities, celebrations) + + fig = plt.figure(figsize=(10, 8)) + show_activities(blocks, height=1, fontsize=5, show_today=True, title=os.path.split(args.pmcs_data)[1], + show_weeks=args.show_weeks, startDate=args.start_date, endDate=args.end_date) + + add_legend(categoryColors, blocks, categoryGrouping, legend_location=legend_location) + + plt.savefig(args.output) + + +def create_blocks(activities, celebrations): + # + # Convert those activities to a set of block descriptions + # + blocks = {} + row = 1 + for category in activities: + blocks[category] = [] + + nrow = categoryNrow.get(category, 5) + nrow, rot = (nrow, None) if isinstance(nrow, int) else nrow + + blocks[category] = [Nrow(nrow), Rotation(rot)] + + color = categoryColors.get(category) + if color is None: + print(f"No colour is defined for category {category}", file=sys.stderr) + color = ("white", "red") + color, border = (color, None) if isinstance(color, str) else color + + row += 1 + + for descrip in sorted(activities[category]): + code = [code for (code, start, due) in activities[category][descrip]] + start = np.min([start for (code, start, due) in activities[category][descrip]]) + due = np.max([due for (code, start, due) in activities[category][descrip]]) + + if descrip in specials: + nrow, rot, advanceRow = specials[descrip] + if nrow is not None: + blocks[category].append(Nrow(nrow)) + if rot is not None: + blocks[category].append(Rotation(rot)) + if advanceRow is not None: + blocks[category].append(AdvanceRow(advanceRow)) + + if descrip[0] == '"' and descrip[-1] == '"': + descrip = descrip[1:-1] + + blocks[category].append(Activity(descrip, start, due, color, border, + wrappedDescrip=wrappedDescrip.get(descrip))) + + # + # Order/group according to categoryGrouping[] + # + if categoryGrouping is None: + grouping = [[cat] for cat in activities] + else: + grouping = categoryGrouping + + _blocks = [] + for cats in grouping: + sub = [] + for cat in cats: + for a in blocks[cat]: + sub.append(a) + + _blocks.append(sub) + + blocks = _blocks + # + # Handle celebrations milestones now that we've ordered the blocks + # + Milestone.height = milestoneHeight + Milestone.rotation = 0 + + milestones = [] + for c in celebrations: + name, start = c + milestones.append(Milestone(name, start)) + + milestones = sorted(milestones, key=lambda a: a.t0) + for i, ml in enumerate(milestones): + ml.drow = i%nRowOfMilestones + + milestones.append(AdvanceRow((nRowOfMilestones - 1)*milestoneHeight)) + + blocks = [milestones] + blocks + + return blocks + +def process_milestones(milestones): + # Process Summary Chart activities/milestones and celebrations milestones + + activities = {} + celebrations = [] + for ms in milestones: + summarychart, code, start, due = ms.summarychart, ms.code, ms.start, ms.due + celebrate = False if ms.summarychart: pass elif ms.celebrate: - celebrate = ms.celebrate + celebrate = ms.celebrate.lower() else: continue + # + # handle dates, either of which may have been omitted + # + if start is None: + if due is None: # HACK + print(f"{ms} has no date field; skipping", file=sys.stderr) + continue + else: + start = due + elif due is None: + due = start + # + # Is it a celebrations milestone rather than an activity? + # + if celebrate in ("top", "y"): # it's a celebrations milestone + if celebrate == "y": + continue + + celebrations.append((ms.name, due)) + continue + + if '.' not in summarychart: + summarychart = f"{summarychart}.{summarychart}" + + category, descrip = summarychart.split(".") + category = category.replace(' ', '_') + + if "," in descrip: + descrip = f'"{descrip}"' + + if category not in activities: + activities[category] = {} + + if descrip not in activities[category]: + activities[category][descrip] = [] + + activities[category][descrip].append((code, start, due)) + + return activities, celebrations - print(f"{ms.summarychart}, {ms.code}, {ms.start}, {ms.due}, {celebrate}") + diff --git a/milestones/cartoon.py b/milestones/cartoon.py new file mode 100644 index 0000000..8b41861 --- /dev/null +++ b/milestones/cartoon.py @@ -0,0 +1,720 @@ +import csv +from datetime import datetime, timedelta +import re +import sys +import textwrap + +import numpy as np + +import matplotlib.pyplot as plt +import matplotlib.dates as mdates +import matplotlib.patches as patches + +__all__ = ["show_activities", "add_legend", "print_details", + "Activity", "Milestone", "AdvanceRow", "Nrow", "Rotation", +] + + +class Activity: + border = None + color = None + nrow = 1 # number of rows in an Activity + markerWidth = 0 + fontsize = 8 + height = 0.3 + row = 0 + rotation = None + + time = None + + def __init__(self, descrip, t0, duration, color=None, border=None, markerWidth=0, rotation=0, + drow=0, nrow=0, wrappedDescrip=None): + """An activity to be carried out + descrip: string description; will be wrapped based on a guess on available width + t0: starting date as string (e.g. 1958-02-05) + duration: duration in days, or ending date in same format as t0 + color: color to use, or None to use current default + border: border color to use, or None to use current default + markerwidth: XX, or None to use current default + rotation: rotate text by this many degrees + drow: offset Activity by drow when drawing + (see also AdvanceRow in show_activities()) + wrappedDescrip: hand-line-wrapped version of descrip; usually None, which uses textwrap.fill + """ + self.descrip = descrip + self.wrappedDescrip = wrappedDescrip # hand-line-wrapped version of descrip; usually None + + if t0: + if not isinstance(t0, datetime): + t0 = datetime.fromisoformat(t0) + + self.t0 = t0 + else: + self.t0 = Activity.time + + try: + try: + duration = float(duration) + except (TypeError, ValueError): + pass + if not isinstance(duration, timedelta): + duration = timedelta(duration) + self.duration = duration + except TypeError: + if not isinstance(duration, datetime): + duration = datetime.fromisoformat(duration) + + self.duration = duration - self.t0 + + Activity.time = self.t0 + self.duration + + self.drow = drow + self._nrow = nrow if nrow > 0 else None + self._color = color if color else None + self._border = border if border else None + self._markerWidth = markerWidth if markerWidth else None + self._rotation = rotation if rotation else None + + def __repr__(self): + return f"Activity({self.descrip}, {self.t0}, {self.duration.days}, {self._color}, {self._border})" + + def __str__(self): + descrip = self.descrip + if ',' in descrip and descrip[0] != '"': + descrip = f'"{descrip}"' + + return f"Activity, {descrip}, {self.t0}, {self.t0 + self.duration}, {self._color}, {self._border}" + + def getData(self): + return [self.descrip, self.t0.strftime('%Y-%m-%d'), (self.t0 + self.duration).strftime('%Y-%m-%d'), + self._color, self._border, self._markerWidth, self.drow, + ] + + def getNrow(self): + return self.nrow if self._nrow is None else self._nrow + + def draw(self, totalDuration=0, startDate="1958-02-05", endDate="2099-12-31", **kwargs): + kwargs = kwargs.copy() + if "nrowTot" in kwargs: + nrowTot = kwargs["nrowTot"] + del kwargs["nrowTot"] + else: + nrowTot = 1 + + if "alpha" not in kwargs: + kwargs["alpha"] = 0.9 + if "color" not in kwargs: + kwargs["color"] = self.color if self._color is None else self._color + nrow = self.getNrow() + + rotation = (90 if nrow > 1 else 0) if self.rotation is None else self.rotation + + if isinstance(endDate, str): + endDate = datetime.fromisoformat(endDate) + + t0 = self.t0 + timedelta(hours=12) + t1 = t0 + self.duration + + if t1 < startDate: + return 0 + elif t0 < startDate: + t0 = startDate + + if t0 > endDate: + return 0 + elif t1 > endDate: + t1 = endDate + + y0 = self.height*(1.1*(self.row - self.drow)) + x = t0 + (t1 - t0)*np.array([0, 1, 1, 0, 0]) + y = y0 + (1 + 1.1*(nrow - 1))*self.height*np.array([0, 0, 1, 1, 0]) + plt.fill(x, y, '-', label=self.descrip, **kwargs) + + if not (self.border is None and self._border is None): + kwargs["color"] = self.border if self._border is None else self._border + + plt.plot(x, y, '-', **kwargs) + + textwidth = 0 + fontsize = self.fontsize if self.fontsize else 10 + if rotation == 90: + width_pts = plt.gcf().get_size_inches()[1]*72 + + fiddleFactor = 1.5*width_pts/nrowTot + textwidth = int(fiddleFactor*nrow/self.fontsize) + 1 + else: + if totalDuration == 0: + fiddleFactor = 3 + else: + width_pts = plt.gcf().get_size_inches()[0]*72 + fiddleFactor = width_pts/totalDuration.days + + textwidth = int(fiddleFactor*self.duration.days/self.fontsize) + 1 + + if self.wrappedDescrip is None: + text = textwrap.fill(self.descrip, width=textwidth, break_long_words=False) + else: + text = self.wrappedDescrip + + if textwidth <= 0: + textwidth = 10 + plt.text(t0 + 0.5*(t1 - t0), 0.5*(y[1] + y[2]), text, + rotation=rotation, + horizontalalignment='center', verticalalignment='center', + fontsize=self.fontsize) + + return nrow + + +class Milestone(Activity): + axvline = False # draw a vertical line through Milestones + markerWidth = 2 + nrow = 1 + rotation = 0 # text rotation + color = None + border = None + + def __init__(self, descrip, t0, color=None, border=None, align="right", valign="top", + markerWidth=None, drow=0): + super().__init__(descrip, t0, 0.0, color=color, border=border, drow=drow, markerWidth=markerWidth) + self.align = align + self.valign = valign + + def __repr__(self): + return f"Milestone({self.descrip})" + + def __str__(self): + return f"Milestone, {self.descrip}, {self.t0}, {self._color}, {self._border}, " \ + f"{self.align}, {self.valign}, {self._markerWidth}, {self.drow}, {self.height}" + + def getData(self): + return [self.descrip, self.t0.strftime('%Y-%m-%d'), + self._color, self._border, self.align, self.valign, self._markerWidth, self.drow] + + def __getNrow(self): + return self.height + + def draw(self, totalDuration=0, startDate="1958-02-05", endDate="2099-12-31", **kwargs): + kwargs = kwargs.copy() + del kwargs["nrowTot"] + if "color" not in kwargs: + kwargs["facecolor"] = self.color if self._color is None else self._color + + if not (self.border is None and self._border is None): + kwargs["edgecolor"] = self.border if self._border is None else self._border + else: + kwargs["edgecolor"] = kwargs.get("facecolor") + + if kwargs["edgecolor"]: + import matplotlib + kwargs["edgecolor"] = matplotlib.colors.to_rgba(kwargs["edgecolor"], kwargs.get("alpha", 1)) + + kwargs["linewidth"] = 2 + + if "alpha" not in kwargs: + kwargs["alpha"] = 0.5 + + if isinstance(startDate, str): + startDate = datetime.fromisoformat(startDate) + if isinstance(endDate, str): + endDate = datetime.fromisoformat(endDate) + + t0 = self.t0 + timedelta(hours=12) + if t0 + self.duration < startDate: + return 0 + elif t0 < startDate: + t0 = startDate + + if t0 > endDate: + return 0 + + y0 = self.height*(1.1*(self.row - self.drow)) + + markerWidth = timedelta(self.markerWidth if self._markerWidth is None else self._markerWidth) + x = t0 + 0.9*markerWidth*np.array([0, 1, 0, -1, 0]) + y = y0 + 0.9*self.height*np.array([0, 0.5, 1, 0.5, 0]) + plt.fill(x, y, '-', **kwargs) + + if Milestone.axvline: + plt.axvline(t0, ls='-', alpha=0.1, zorder=-1) + + horizontalalignment = "left" if self.align == "right" else "right" # matplotlib is confusing + plt.text(t0 + markerWidth/2*(1 if self.align == "right" else -1), + y0 + (0.9 if self.valign == "top" else 0.1)*self.height, self.descrip, + rotation=self.rotation, + horizontalalignment=horizontalalignment, verticalalignment='center', + fontsize=self.fontsize, zorder=10) + + return 1 + + +class Manipulation: + """Modify the state of the system, rather than describing an activity or milestone""" + pass + +class AdvanceRow(Manipulation): + """A class used to advance the row counter""" + + def __init__(self, drow): + self.drow = drow + + if False: + def getNrow(self): + return self.drow + + def __repr__(self): + return f"AdvanceRow({self.drow})" + + def __str__(self): + return f"AdvanceRow, {self.drow}" + + def getData(self): + return [self.drow] + + +class Nrow(Manipulation): + """A class used to set the height of the activities""" + + def __init__(self, nrow): + self.nrow = nrow + + def __repr__(self): + return f"Nrow({self.nrow})" + + def __str__(self): + return f"Nrow, {self.nrow}" + + def getNrow(self): + return self.nrow + + def set_default_Nrow(self): + Activity.nrow = self.nrow + + def getData(self): + return [self.nrow] + + +class Rotation(Manipulation): + """A class used to set the text rotation""" + + def __init__(self, rotation): + self.rotation = rotation # angle in degrees + + def __repr__(self): + return f"Rotation({self.rotation})" + + def __str__(self): + return f"Rotation, {self.rotation}" + + def getData(self): + return [self.rotation] + + def set_default_Rotation(self): + Activity.rotation = self.rotation + + +def calculate_height(activities): + rot = 90 + heights = [0] + baseline = 0 + + for a in activities: + if isinstance(a, Rotation): + rot = a.rotation + elif isinstance(a, (Activity, Milestone, Nrow)): + nrow = a.getNrow() + heights.append(baseline + (nrow if rot in (None, 90) else 1)) + elif isinstance(a, AdvanceRow): + baseline -= a.drow + + return max(heights) + + +def show_activities(activities, height=0.1, fontsize=7, + rowSpacing=0.5, show_today=True, show_time_axis=True, + title="", show_milestone_vlines=True, + startDate=None, endDate=None, show_weeks=True): + """Plot a set of activities + + activities: list of list of Activities + height: height of each activity bar + fontsize: fontsize for labels (passed to plt.text) + show_today: indicate today by a dashed vertical line + + In general each inner list of activities is drawn on its own row but you can + modify this by using pseudo-activity `AdvanceRow`. Also + available is `Color` to set the default colour (and optionally border) + N.b. `Color(c)` resets the border, `Activity(..., color=c)` does not + + E.g. + activities = [ + [ + Color("white", border='red'), + Activity("A", "2021-02-28", 35), + Activity("B", "2021-04-10", "2021-05-10") + ], [ + Color("blue"), + Activity("C", "2021-01-01", 30), + Activity("D", "2021-01-20", "2021-04-01", drow=1), + AdvanceRow(1), + ], [ + Activity("E", "2021-01-05", "2021-01-31", color="green"), + ], + ] +""" + + if not startDate: + startDate = "1958-02-05" + if not endDate: + endDate = "2099-12-31" + + Activity.height = height + Activity.fontsize = fontsize + Milestone.axvline = show_milestone_vlines + + if re.search("^[-+]?\d+$", startDate): # a date relative to now, in days + nday = int(startDate) + startDate = datetime.now() + timedelta(days=nday) + elif isinstance(startDate, str): + startDate = datetime.fromisoformat(startDate) + + if re.search("^[-+]?\d+$", endDate): # a date relative to now, in days + nday = int(endDate) + endDate = datetime.now() + timedelta(days=nday) + elif isinstance(endDate, str): + endDate = datetime.fromisoformat(endDate) + + dateMin = None + dateMax = None + for aa in activities: + for a in aa: + if isinstance(a, Manipulation): + continue + + t0 = a.t0 + t1 = a.t0 + a.duration + + if t1 < startDate: + continue + if t0 < startDate: + t0 = startDate + + if t0 > endDate: + continue + if t1 > endDate: + t1 = endDate + + if dateMin is None or t0 < dateMin: + dateMin = t0 + if dateMax is None or t1 > dateMax: + dateMax = t1 + + if dateMax is None: + totalDuration = 0 + else: + totalDuration = dateMax - dateMin # used in line-wrapping the labels + # + # Count the number of rows of activities in the figure + # + nrowTot = 0 + for aa in activities: + nrowTot0 = nrowTot + nrowTot += calculate_height(aa) + + Activity.row = 0 + for aa in activities: + Activity.nrow = 0 + Activity.row -= calculate_height(aa) + rowSpacing + + for a in aa: + if isinstance(a, Manipulation): + if isinstance(a, AdvanceRow): + Activity.row -= a.drow + elif isinstance(a, Rotation): + a.set_default_Rotation() + elif isinstance(a, Nrow): + a.set_default_Nrow() + else: + raise NotImplemented(a) + + continue + + a.draw(totalDuration=totalDuration, nrowTot=nrowTot, + startDate=startDate, endDate=endDate) + + if show_today and datetime.now() > startDate: + plt.axvline(datetime.now(), ls='--', color='black', alpha=0.5, zorder=-1) + plt.grid(axis='x') + + if not show_time_axis: + plt.xticks(ticks=[], labels=[]) + if True: # turn off y-axis labels + plt.yticks(ticks=[], labels=[]) + + ax = plt.gca() + ax.fmt_xdata = mdates.DateFormatter('%Y-%m-%d %H:%M:%S.02 ') + + ax.tick_params('x', top=True, labeltop=True) + + plt.gcf().autofmt_xdate(ha='center') # rotate and make space; center works at top and bottom + + if show_weeks: + ax.xaxis.set_major_locator(mdates.WeekdayLocator(byweekday=mdates.MO)) + + startDate, endDate = [datetime.utcfromtimestamp(x) for x in ax.set_xlim()] + + daylen = timedelta(days=1) + for i in range(((endDate + 2*daylen) - (startDate - 2*daylen)).days): + d = startDate + i*daylen + if d.weekday() in [5, 6]: + pos = mdates.date2num(d) + ax.axvspan(pos - 0, pos + 1, color='lightgray', zorder=-10) + + if title: + plt.title(title) + + plt.tight_layout() + + +def add_legend(categoryColors, activities=None, categoryGrouping=None, legend_location=None): + """Add a legend to a show_activities() plot + categoryColors: dict mapping categories to colours + activities: list of activities shown; if None assume that all categories are present + categoryGrouping: list of lists giving the order of categories; if None, alphabetical + legend_location: location of legend; `loc` parameter to plt.legend + """ + # Lookup which colours are actually used + if activities is None: + usedColors = None # assume all are used + else: + usedColors = set([a._color for a in sum(activities, []) if + isinstance(a, Activity) and not isinstance(a, Milestone)]) + + if categoryGrouping is None: + categoryGrouping = [sorted(categoryColors)] + + # Construct a legend in the order of categoryGrouping + handles = [] + for cc in categoryGrouping: + for c in cc: + color = categoryColors.get(c) + if isinstance(color, str): + border = None + else: + color, border = color + + if usedColors is None or color in usedColors: + handles.append(patches.Polygon([(0,0), (10,0), (0,-10)], + facecolor=color, edgecolor=border, + label=f"{c.replace('_', ' ')}")) + # and add it to the figure + plt.legend(handles=handles, loc=legend_location) + + plt.tight_layout() + + +def p6dateStrToIso(dateStr): + """Convert a date string such as "5-Feb-58" to ISO standard "2058-02-05" + + Not Y2K compliant! + """ + if re.search(r"^\d{4}-\d{2}-\d{2}", dateStr): + return dateStr + + dateStr = re.sub("(\s+A|\*)$", "", dateStr) # "Actual"; Kevin Long says to ignore it + + day, monName, year2 = dateStr.split('-') + day = int(day) + year2 = int(year2) + + mon = dict(Jan=1, Feb=2, Mar=3, Apr=4, May=5, Jun=6, Jul=7, Aug=8, Sep=9, Oct=10, Nov=11, Dec=12)[monName] + + return f"20{year2}-{mon:02}-{day:02}" + + +def read_activities_from_P6(fileName, milestonesFileName=None, categoryGrouping=None, specials={}, + nRowOfMilestones=5, milestoneHeight=4, ignoreZeroLengthActivities=False, + categoryColors={}, categoryNrow={}, wrappedDescrip={}): + """Read Kevin Long's csv dump from P6 + +The file's in the format: +... + +Summary Chart,Activity ID,Start,Finish,Total Float,Original Duration +M1M3.Install M1M3 with Surrogate,Summit-K1310,17-Jan-23,30-Jan-23,0,10 +... +M1M3.Install M1M3 with Surrogate,SUMMIT-1923,7-Feb-23,13-Feb-23,0,5 +M1M3.FunctionalTesting with Surrogate,SUMMIT-1927,7-Mar-23,13-Mar-23,0,5 +... +M1M3.FunctionalTesting with Surrogate,SUMMIT-1929,14-Mar-23,15-Mar-23,0,2 +M1M3.Dynamic Testing with Surrogate,SUMMIT-1928,23-Mar-23,19-Apr-23,0,20 +... +M1M3.M1M3 on TMA + Thermal control t,SUMMIT-1909,5-Mar-24,15-Mar-24,0,9 +M2.M2 functional testing on TMA,SUMMIT-1916,24-Jul-23,4-Aug-23,0,10 +... +M2.M2 on TMA + reintallation,SUMMIT-3081,10-Jul-23,21-Jul-23,0,10 +Refrigeration PathFinder.prep & test on TMA,SUMMIT-3013,,22-Mar-23,352,0 +... + """ + + activities = {} + celebratory = [] + with open(fileName, newline='\n') as fd: + csvin = csv.reader(fd, skipinitialspace=True) + + for lineNo, line in enumerate(csvin, 1): + if False: + print(lineNo, line) + + if lineNo == 1: + continue + + summary_chart, aid, start, finish, celebrate = line + # + # handle dates + start = None if (start == 'None' or start is None) else start + finish = None if (finish == 'None' or finish is None) else finish + + if ignoreZeroLengthActivities: + if start is None or finish is None: + continue + + if start is None: + if finish is None: # HACK + print(f"Skipping {lineNo}: {line}") + continue + else: + start = finish + elif finish is None: + finish = start + + start = datetime.fromisoformat(p6dateStrToIso(start)) + finish = datetime.fromisoformat(p6dateStrToIso(finish)) + # + # Is it a celebratory milestone rather than an activity? + # + if celebrate.lower() in ("top", "y"): # it's a celebratory milestone + if celebrate.lower() == "y": + continue + + celebratory.append((aid, finish)) + continue + + if '.' not in summary_chart: + summary_chart = f"{summary_chart}.{summary_chart}" + + category, descrip = summary_chart.split(".") + category = category.replace(' ', '_') + + if "," in descrip: + descrip = f'"{descrip}"' + + if category not in activities: + activities[category] = {} + + if descrip not in activities[category]: + activities[category][descrip] = [] + + activities[category][descrip].append((aid, start, finish)) + + summaries = {} + row = 1 + for category in activities: + summaries[category] = [] + + nrow = categoryNrow.get(category, 5) + nrow, rot = (nrow, None) if isinstance(nrow, int) else nrow + + summaries[category] = [Nrow(nrow), Rotation(rot)] + + color = categoryColors.get(category) + if color is None: + print(f"No colour is defined for category {category}", file=sys.stderr) + color = ("white", "red") + color, border = (color, None) if isinstance(color, str) else color + + row += 1 + + for descrip in sorted(activities[category]): + aids = [aid for (aid, start, finish) in activities[category][descrip]] + start = np.min([start for (aid, start, finish) in activities[category][descrip]]) + finish = np.max([finish for (aid, start, finish) in activities[category][descrip]]) + + if descrip in specials: + nrow, rot, advanceRow = specials[descrip] + if nrow is not None: + summaries[category].append(Nrow(nrow)) + if rot is not None: + summaries[category].append(Rotation(rot)) + if advanceRow is not None: + summaries[category].append(AdvanceRow(advanceRow)) + + if descrip[0] == '"' and descrip[-1] == '"': + descrip = descrip[1:-1] + + summaries[category].append(Activity(descrip, start, finish, color, border, + wrappedDescrip=wrappedDescrip.get(descrip))) + + # + # Order/group according to grouping[] + if categoryGrouping is not None: + _summaries = [] + for cats in categoryGrouping: + sub = [] + for cat in cats: + for a in summaries[cat]: + sub.append(a) + + _summaries.append(sub) + + summaries = _summaries + + if False: + if milestonesFileName is not None: + milestones = read_celeb_milestones(milestonesFileName, + nRowOfMilestones=nRowOfMilestones, milestoneHeight=milestoneHeight) + summaries = [milestones] + summaries + else: + Milestone.height = milestoneHeight + Milestone.rotation = 0 + + milestones = [] + for c in celebratory: + name, start = c + milestones.append(Milestone(name, start)) + + milestones = sorted(milestones, key=lambda a: a.t0) + for i, ml in enumerate(milestones): + ml.drow = i%nRowOfMilestones + + milestones.append(AdvanceRow((nRowOfMilestones - 1)*milestoneHeight)) + + summaries = [milestones] + summaries + + return summaries, activities + + +def print_details(inputs, systems=None, fd=None, indent=" "): + """Print the details of cartoon boxes + + E.g. + activities, inputs = read_activities_from_P6(...) + print_details(inputs, fd=None, systems=["Calibration"]) + """ + if fd is None: + fd = sys.stdout + + if systems is not None and isinstance(systems, (list, tuple)): + systems = set(systems) + + for system, aa in inputs.items(): + system = system.replace('_', ' ') + + if systems is not None and system not in systems: + continue + print(system, file=fd) + for descrip, cpts in aa.items(): + print(f"{indent}{descrip}", file=fd) + for (aid, start, finish) in sorted(cpts, key=lambda x: x[2]): + print(f"{2*indent}{aid:20} {str(start).split(' ')[0]} -- {str(finish).split(' ')[0]}", file=fd) + diff --git a/milestones/cartoon_config.py b/milestones/cartoon_config.py new file mode 100644 index 0000000..9fc8b22 --- /dev/null +++ b/milestones/cartoon_config.py @@ -0,0 +1,70 @@ +# +# Configuration for the cartoon, imported by blockschedule.py +# +__all__ = ["categoryNrow", "categoryGrouping", "categoryColors", + "specials", "wrappedDescrip", "nRowOfMilestones", "milestoneHeight", + "legend_location", +] + + +nRowOfMilestones = 3 +milestoneHeight = 3 +legend_location = (0.85, 0.02) + +# +# Number of rows needed for boxes, and rotation (0 or 90 degrees; 90 is the default) +# +categoryNrow = dict( + AuxTel=(4, 0), + Calibration=15, + ComCam=13, + Commissioning=15, + Dome=(3, 0), + LSSTCam=30, + M1M3=30, + M2=30, + Refrigeration_PathFinder=(8, 0), + TMA_Verification=(2, 0), +) +# +# Tweak specific activities; (nrow, rotation, advanceRow) +# +specials = { + "Dome" : (None, None, -6), + "Light Windscreen" : (None, 0, 3), + "Ring Gear Install" : (None, None, 3), + "Calibration Screen" : (None, None, 9), +} +# +# Group categories onto single lines, and define the order that the categories are laid out (top to bottom) +# +categoryGrouping = [ + ("Dome",), + ("Calibration", "TMA_Verification",), + ("M1M3", "M2", "Commissioning",), + ("ComCam",), + ("Refrigeration_PathFinder",), + ("LSSTCam",), + ("AuxTel",), +] +# +# Colours for categories; when a tuple the second colour is the border colour +# +categoryColors = dict( + AuxTel="goldenrod", + Calibration="orchid", + ComCam=("cornflowerblue", "black"), + Commissioning=("moccasin", "black"), + Dome=("mistyrose", "brown"), + LSSTCam=("powderblue", "skyblue"), + M1M3=("indianred", "brown"), + M2=("lightgreen", "green"), + Refrigeration_PathFinder=("lavender", "black"), + TMA_Verification=("sandybrown", "black"), +) +# +# Override descriptions that the code wraps badly +# +wrappedDescrip = { + "ComCam Off TMA on Cart" : "ComCam Off\nTMA on Cart", +} From c0e0f38b6ce9693cfd6645a4456c42435b824923 Mon Sep 17 00:00:00 2001 From: Robert Lupton the Good Date: Sat, 10 Dec 2022 14:24:31 -0500 Subject: [PATCH 12/12] Make flake8 happy N.b. added exceptions for various things, but resolved the ones that didn't significantly affect the style --- milestones.py | 10 +- milestones/blockschedule.py | 23 ++-- milestones/cartoon.py | 205 ++--------------------------------- milestones/cartoon_config.py | 16 +-- setup.cfg | 3 + 5 files changed, 37 insertions(+), 220 deletions(-) diff --git a/milestones.py b/milestones.py index 4774d74..785fdf6 100644 --- a/milestones.py +++ b/milestones.py @@ -137,10 +137,12 @@ def parse_args(): ) blockschedule.add_argument("--output", help="Filename for output", default="blockschedule.pdf") - blockschedule.add_argument("--start-date", help="Starting date for cartoon (ISO 8601 format)") - blockschedule.add_argument("--end-date", help="Ending date for cartoon (ISO 8601 format)") - blockschedule.add_argument("--show-weeks", help="Show week boundaries", action='store_true', - default=False) + blockschedule.add_argument("--start-date", + help="Starting date for cartoon (ISO 8601 format)") + blockschedule.add_argument("--end-date", + help="Ending date for cartoon (ISO 8601 format)") + blockschedule.add_argument("--show-weeks", help="Show week boundaries", + action='store_true', default=False) blockschedule.set_defaults(func=milestones.blockschedule) args = parser.parse_args() diff --git a/milestones/blockschedule.py b/milestones/blockschedule.py index 1be1938..c4dbdfc 100644 --- a/milestones/blockschedule.py +++ b/milestones/blockschedule.py @@ -3,12 +3,11 @@ import os import sys -import textwrap import numpy as np import matplotlib.pyplot as plt -from .cartoon import * +from .cartoon import show_activities, add_legend, Activity, AdvanceRow, Milestone, Nrow, Rotation from .cartoon_config import \ categoryNrow, categoryColors, wrappedDescrip, categoryGrouping, specials, \ nRowOfMilestones, milestoneHeight, legend_location @@ -21,7 +20,7 @@ def blockschedule(args, milestones): blocks = create_blocks(activities, celebrations) - fig = plt.figure(figsize=(10, 8)) + plt.figure(figsize=(10, 8)) show_activities(blocks, height=1, fontsize=5, show_today=True, title=os.path.split(args.pmcs_data)[1], show_weeks=args.show_weeks, startDate=args.start_date, endDate=args.end_date) @@ -53,24 +52,23 @@ def create_blocks(activities, celebrations): row += 1 for descrip in sorted(activities[category]): - code = [code for (code, start, due) in activities[category][descrip]] start = np.min([start for (code, start, due) in activities[category][descrip]]) - due = np.max([due for (code, start, due) in activities[category][descrip]]) + due = np.max([due for (code, start, due) in activities[category][descrip]]) # noqa: E221,E272 if descrip in specials: - nrow, rot, advanceRow = specials[descrip] + nrow, rot, nadvance = specials[descrip] if nrow is not None: blocks[category].append(Nrow(nrow)) if rot is not None: blocks[category].append(Rotation(rot)) - if advanceRow is not None: - blocks[category].append(AdvanceRow(advanceRow)) + if nadvance is not None: + blocks[category].append(AdvanceRow(nadvance)) if descrip[0] == '"' and descrip[-1] == '"': descrip = descrip[1:-1] blocks[category].append(Activity(descrip, start, due, color, border, - wrappedDescrip=wrappedDescrip.get(descrip))) + wrappedDescrip=wrappedDescrip.get(descrip))) # # Order/group according to categoryGrouping[] @@ -78,7 +76,7 @@ def create_blocks(activities, celebrations): if categoryGrouping is None: grouping = [[cat] for cat in activities] else: - grouping = categoryGrouping + grouping = categoryGrouping _blocks = [] for cats in grouping: @@ -103,7 +101,7 @@ def create_blocks(activities, celebrations): milestones = sorted(milestones, key=lambda a: a.t0) for i, ml in enumerate(milestones): - ml.drow = i%nRowOfMilestones + ml.drow = i % nRowOfMilestones milestones.append(AdvanceRow((nRowOfMilestones - 1)*milestoneHeight)) @@ -111,6 +109,7 @@ def create_blocks(activities, celebrations): return blocks + def process_milestones(milestones): # Process Summary Chart activities/milestones and celebrations milestones @@ -166,5 +165,3 @@ def process_milestones(milestones): activities[category][descrip].append((code, start, due)) return activities, celebrations - - diff --git a/milestones/cartoon.py b/milestones/cartoon.py index 8b41861..38ce679 100644 --- a/milestones/cartoon.py +++ b/milestones/cartoon.py @@ -1,4 +1,3 @@ -import csv from datetime import datetime, timedelta import re import sys @@ -11,8 +10,7 @@ import matplotlib.patches as patches __all__ = ["show_activities", "add_legend", "print_details", - "Activity", "Milestone", "AdvanceRow", "Nrow", "Rotation", -] + "Activity", "Milestone", "AdvanceRow", "Nrow", "Rotation", ] class Activity: @@ -87,8 +85,7 @@ def __str__(self): def getData(self): return [self.descrip, self.t0.strftime('%Y-%m-%d'), (self.t0 + self.duration).strftime('%Y-%m-%d'), - self._color, self._border, self._markerWidth, self.drow, - ] + self._color, self._border, self._markerWidth, self.drow, ] def getNrow(self): return self.nrow if self._nrow is None else self._nrow @@ -141,7 +138,7 @@ def draw(self, totalDuration=0, startDate="1958-02-05", endDate="2099-12-31", ** width_pts = plt.gcf().get_size_inches()[1]*72 fiddleFactor = 1.5*width_pts/nrowTot - textwidth = int(fiddleFactor*nrow/self.fontsize) + 1 + textwidth = int(fiddleFactor*nrow/fontsize) + 1 else: if totalDuration == 0: fiddleFactor = 3 @@ -149,7 +146,7 @@ def draw(self, totalDuration=0, startDate="1958-02-05", endDate="2099-12-31", ** width_pts = plt.gcf().get_size_inches()[0]*72 fiddleFactor = width_pts/totalDuration.days - textwidth = int(fiddleFactor*self.duration.days/self.fontsize) + 1 + textwidth = int(fiddleFactor*self.duration.days/fontsize) + 1 if self.wrappedDescrip is None: text = textwrap.fill(self.descrip, width=textwidth, break_long_words=False) @@ -161,7 +158,7 @@ def draw(self, totalDuration=0, startDate="1958-02-05", endDate="2099-12-31", ** plt.text(t0 + 0.5*(t1 - t0), 0.5*(y[1] + y[2]), text, rotation=rotation, horizontalalignment='center', verticalalignment='center', - fontsize=self.fontsize) + fontsize=fontsize) return nrow @@ -252,6 +249,7 @@ class Manipulation: """Modify the state of the system, rather than describing an activity or milestone""" pass + class AdvanceRow(Manipulation): """A class used to advance the row counter""" @@ -372,13 +370,13 @@ def show_activities(activities, height=0.1, fontsize=7, Activity.fontsize = fontsize Milestone.axvline = show_milestone_vlines - if re.search("^[-+]?\d+$", startDate): # a date relative to now, in days + if re.search(r"^[-+]?\d+$", startDate): # a date relative to now, in days nday = int(startDate) startDate = datetime.now() + timedelta(days=nday) elif isinstance(startDate, str): startDate = datetime.fromisoformat(startDate) - if re.search("^[-+]?\d+$", endDate): # a date relative to now, in days + if re.search(r"^[-+]?\d+$", endDate): # a date relative to now, in days nday = int(endDate) endDate = datetime.now() + timedelta(days=nday) elif isinstance(endDate, str): @@ -418,7 +416,6 @@ def show_activities(activities, height=0.1, fontsize=7, # nrowTot = 0 for aa in activities: - nrowTot0 = nrowTot nrowTot += calculate_height(aa) Activity.row = 0 @@ -435,7 +432,7 @@ def show_activities(activities, height=0.1, fontsize=7, elif isinstance(a, Nrow): a.set_default_Nrow() else: - raise NotImplemented(a) + raise NotImplementedError(a) continue @@ -504,7 +501,7 @@ def add_legend(categoryColors, activities=None, categoryGrouping=None, legend_lo color, border = color if usedColors is None or color in usedColors: - handles.append(patches.Polygon([(0,0), (10,0), (0,-10)], + handles.append(patches.Polygon([(0, 0), (10, 0), (0, -10)], facecolor=color, edgecolor=border, label=f"{c.replace('_', ' ')}")) # and add it to the figure @@ -513,187 +510,6 @@ def add_legend(categoryColors, activities=None, categoryGrouping=None, legend_lo plt.tight_layout() -def p6dateStrToIso(dateStr): - """Convert a date string such as "5-Feb-58" to ISO standard "2058-02-05" - - Not Y2K compliant! - """ - if re.search(r"^\d{4}-\d{2}-\d{2}", dateStr): - return dateStr - - dateStr = re.sub("(\s+A|\*)$", "", dateStr) # "Actual"; Kevin Long says to ignore it - - day, monName, year2 = dateStr.split('-') - day = int(day) - year2 = int(year2) - - mon = dict(Jan=1, Feb=2, Mar=3, Apr=4, May=5, Jun=6, Jul=7, Aug=8, Sep=9, Oct=10, Nov=11, Dec=12)[monName] - - return f"20{year2}-{mon:02}-{day:02}" - - -def read_activities_from_P6(fileName, milestonesFileName=None, categoryGrouping=None, specials={}, - nRowOfMilestones=5, milestoneHeight=4, ignoreZeroLengthActivities=False, - categoryColors={}, categoryNrow={}, wrappedDescrip={}): - """Read Kevin Long's csv dump from P6 - -The file's in the format: -... - -Summary Chart,Activity ID,Start,Finish,Total Float,Original Duration -M1M3.Install M1M3 with Surrogate,Summit-K1310,17-Jan-23,30-Jan-23,0,10 -... -M1M3.Install M1M3 with Surrogate,SUMMIT-1923,7-Feb-23,13-Feb-23,0,5 -M1M3.FunctionalTesting with Surrogate,SUMMIT-1927,7-Mar-23,13-Mar-23,0,5 -... -M1M3.FunctionalTesting with Surrogate,SUMMIT-1929,14-Mar-23,15-Mar-23,0,2 -M1M3.Dynamic Testing with Surrogate,SUMMIT-1928,23-Mar-23,19-Apr-23,0,20 -... -M1M3.M1M3 on TMA + Thermal control t,SUMMIT-1909,5-Mar-24,15-Mar-24,0,9 -M2.M2 functional testing on TMA,SUMMIT-1916,24-Jul-23,4-Aug-23,0,10 -... -M2.M2 on TMA + reintallation,SUMMIT-3081,10-Jul-23,21-Jul-23,0,10 -Refrigeration PathFinder.prep & test on TMA,SUMMIT-3013,,22-Mar-23,352,0 -... - """ - - activities = {} - celebratory = [] - with open(fileName, newline='\n') as fd: - csvin = csv.reader(fd, skipinitialspace=True) - - for lineNo, line in enumerate(csvin, 1): - if False: - print(lineNo, line) - - if lineNo == 1: - continue - - summary_chart, aid, start, finish, celebrate = line - # - # handle dates - start = None if (start == 'None' or start is None) else start - finish = None if (finish == 'None' or finish is None) else finish - - if ignoreZeroLengthActivities: - if start is None or finish is None: - continue - - if start is None: - if finish is None: # HACK - print(f"Skipping {lineNo}: {line}") - continue - else: - start = finish - elif finish is None: - finish = start - - start = datetime.fromisoformat(p6dateStrToIso(start)) - finish = datetime.fromisoformat(p6dateStrToIso(finish)) - # - # Is it a celebratory milestone rather than an activity? - # - if celebrate.lower() in ("top", "y"): # it's a celebratory milestone - if celebrate.lower() == "y": - continue - - celebratory.append((aid, finish)) - continue - - if '.' not in summary_chart: - summary_chart = f"{summary_chart}.{summary_chart}" - - category, descrip = summary_chart.split(".") - category = category.replace(' ', '_') - - if "," in descrip: - descrip = f'"{descrip}"' - - if category not in activities: - activities[category] = {} - - if descrip not in activities[category]: - activities[category][descrip] = [] - - activities[category][descrip].append((aid, start, finish)) - - summaries = {} - row = 1 - for category in activities: - summaries[category] = [] - - nrow = categoryNrow.get(category, 5) - nrow, rot = (nrow, None) if isinstance(nrow, int) else nrow - - summaries[category] = [Nrow(nrow), Rotation(rot)] - - color = categoryColors.get(category) - if color is None: - print(f"No colour is defined for category {category}", file=sys.stderr) - color = ("white", "red") - color, border = (color, None) if isinstance(color, str) else color - - row += 1 - - for descrip in sorted(activities[category]): - aids = [aid for (aid, start, finish) in activities[category][descrip]] - start = np.min([start for (aid, start, finish) in activities[category][descrip]]) - finish = np.max([finish for (aid, start, finish) in activities[category][descrip]]) - - if descrip in specials: - nrow, rot, advanceRow = specials[descrip] - if nrow is not None: - summaries[category].append(Nrow(nrow)) - if rot is not None: - summaries[category].append(Rotation(rot)) - if advanceRow is not None: - summaries[category].append(AdvanceRow(advanceRow)) - - if descrip[0] == '"' and descrip[-1] == '"': - descrip = descrip[1:-1] - - summaries[category].append(Activity(descrip, start, finish, color, border, - wrappedDescrip=wrappedDescrip.get(descrip))) - - # - # Order/group according to grouping[] - if categoryGrouping is not None: - _summaries = [] - for cats in categoryGrouping: - sub = [] - for cat in cats: - for a in summaries[cat]: - sub.append(a) - - _summaries.append(sub) - - summaries = _summaries - - if False: - if milestonesFileName is not None: - milestones = read_celeb_milestones(milestonesFileName, - nRowOfMilestones=nRowOfMilestones, milestoneHeight=milestoneHeight) - summaries = [milestones] + summaries - else: - Milestone.height = milestoneHeight - Milestone.rotation = 0 - - milestones = [] - for c in celebratory: - name, start = c - milestones.append(Milestone(name, start)) - - milestones = sorted(milestones, key=lambda a: a.t0) - for i, ml in enumerate(milestones): - ml.drow = i%nRowOfMilestones - - milestones.append(AdvanceRow((nRowOfMilestones - 1)*milestoneHeight)) - - summaries = [milestones] + summaries - - return summaries, activities - - def print_details(inputs, systems=None, fd=None, indent=" "): """Print the details of cartoon boxes @@ -717,4 +533,3 @@ def print_details(inputs, systems=None, fd=None, indent=" "): print(f"{indent}{descrip}", file=fd) for (aid, start, finish) in sorted(cpts, key=lambda x: x[2]): print(f"{2*indent}{aid:20} {str(start).split(' ')[0]} -- {str(finish).split(' ')[0]}", file=fd) - diff --git a/milestones/cartoon_config.py b/milestones/cartoon_config.py index 9fc8b22..cc6b301 100644 --- a/milestones/cartoon_config.py +++ b/milestones/cartoon_config.py @@ -3,8 +3,7 @@ # __all__ = ["categoryNrow", "categoryGrouping", "categoryColors", "specials", "wrappedDescrip", "nRowOfMilestones", "milestoneHeight", - "legend_location", -] + "legend_location"] nRowOfMilestones = 3 @@ -30,13 +29,14 @@ # Tweak specific activities; (nrow, rotation, advanceRow) # specials = { - "Dome" : (None, None, -6), - "Light Windscreen" : (None, 0, 3), - "Ring Gear Install" : (None, None, 3), - "Calibration Screen" : (None, None, 9), + "Dome": (None, None, -6), + "Light Windscreen": (None, 0, 3), + "Ring Gear Install": (None, None, 3), + "Calibration Screen": (None, None, 9), } # -# Group categories onto single lines, and define the order that the categories are laid out (top to bottom) +# Group categories onto single lines, and define the order that the categories +# are laid out (top to bottom) # categoryGrouping = [ ("Dome",), @@ -66,5 +66,5 @@ # Override descriptions that the code wraps badly # wrappedDescrip = { - "ComCam Off TMA on Cart" : "ComCam Off\nTMA on Cart", + "ComCam Off TMA on Cart": "ComCam Off\nTMA on Cart", } diff --git a/setup.cfg b/setup.cfg index 603c9f5..0daadb3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,8 @@ [flake8] per-file-ignores = */__init__.py: F401, F403 + */blockschedule.py: E501 + */cartoon.py: E501, N802, N803, N806, N815 + */cartoon_config.py: N816 # Set the max line length to match Black max-line-length = 88