Skip to content

Commit

Permalink
Merge pull request #14 from rogers-obrien-rad/feature/13/changelog
Browse files Browse the repository at this point in the history
Feature/13/changelog
  • Loading branch information
HagenFritz authored May 28, 2024
2 parents ce97d1e + c63627d commit 795b94d
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 7 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -217,4 +217,5 @@ replay_pid*
*.code-workspace

# Directories
*reference*
*reference*
*reports*
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
requests
pytest
python-dotenv
matplotlib
matplotlib
pandas
48 changes: 48 additions & 0 deletions smartpm/endpoints/changes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from smartpm.client import SmartPMClient
from smartpm.decorators import api_wrapper, utility
from smartpm.utils import plot_schedule_changes
from smartpm.logging_config import logger

class Changes:
def __init__(self, client: SmartPMClient):
self.client = client

@api_wrapper
def get_changes_summary(self, project_id, scenario_id):
"""
Retrieve summary information about the changes that have happened to a scenario over time: https://developers.smartpmtech.com/#operation/get-change-log-sumamry
Parameters
----------
project_id : str
The Project ID containing the scenario for which you would like to pull the changes summary from
scenario_id : str
The Scenario ID for which you would like to pull the changes summary from
Returns
-------
<response.json> : dict
Changes summary data as a JSON object
"""
logger.debug(f"Fetching changes summary for project_id: {project_id}, scenario_id: {scenario_id}")

endpoint = f'v1/projects/{project_id}/scenarios/{scenario_id}/change-log-summary'
response = self.client._get(endpoint=endpoint)

return response

@utility
def plot_changes_summary(self, project_id, scenario_id):
"""
Retrieve the changes summary data and plot it.
Reproduces this figure from SmartPM: https://help.smartpmtech.com/trends-schedule-changes-over-time
Parameters
----------
project_id : str
ID of the project containing the scenario
scenario_id : str
ID of the scenario to retrieve the percent complete curve for
"""
logger.debug(f"Plotting changes summary for project_id: {project_id}, scenario_id: {scenario_id}")
curve_data = self.get_changes_summary(project_id, scenario_id)
plot_schedule_changes(curve_data)
33 changes: 32 additions & 1 deletion smartpm/endpoints/projects.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import pandas as pd

from smartpm.client import SmartPMClient
from smartpm.decorators import api_wrapper, utility
from smartpm.logging_config import logger


class Projects:
def __init__(self, client: SmartPMClient):
self.client = client
Expand Down Expand Up @@ -94,4 +98,31 @@ def find_project_by_name(self, name):
return project

logger.info(f"Project with name '{name}' not found.")
return None
return None

@utility
def get_projects_dataframe(self):
"""
Get all projects and return as a DataFrame with selected columns.
Returns
-------
pd.DataFrame
DataFrame containing projects data with selected columns.
"""
projects = self.get_projects()

# Extract relevant fields and metadata
project_data = []
for project in projects:
project_data.append({
"id": project["id"],
"name": project["name"],
"startDate": project["startDate"],
"city": project["city"],
"projectNumber": project["metadata"].get("PROJECT_NUMBER"),
"region": project["metadata"].get("REGION")
})

df = pd.DataFrame(project_data, columns=["id", "name", "startDate", "city", "projectNumber", "region"])
return df
70 changes: 67 additions & 3 deletions smartpm/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,6 @@ def plot_earned_schedule_curve(json_data):
plt.tight_layout()
plt.show()

import matplotlib.pyplot as plt
import datetime

def plot_schedule_delay(json_data):
"""
Plot the schedule delay curve from the provided JSON data.
Expand Down Expand Up @@ -200,3 +197,70 @@ def plot_schedule_delay(json_data):

plt.tight_layout()
plt.show()

def plot_schedule_changes(json_data):
"""
Plot the schedule changes over time from the provided JSON data.
Parameters
----------
json_data : list of dict
List of dictionaries containing the schedule change data.
"""
# Extract dates and metrics
dates = [datetime.datetime.strptime(entry['dataDate'], '%Y-%m-%dT%H:%M:%S') for entry in json_data]

metrics = json_data[0]['metrics'].keys()

# Initialize a dictionary to hold lists of metric values
metric_values = {metric: [] for metric in metrics}

# Populate the metric values dictionary
for entry in json_data:
for metric in metrics:
metric_values[metric].append(entry['metrics'][metric])

# Define colors and linestyles for each metric
color_linestyle_dict = {
"CriticalChanges": {"color": 'red', "marker": 'o'},
"NearCriticalChanges": {"color": 'goldenrod', "marker": 'd'},
"ActivityChanges": {"color": 'seagreen', "marker": 's'},
"LogicChanges": {"color": 'blue', "marker": '^'},
"CalendarChanges": {"color": 'dodgerblue', "marker": 'v'},
"DurationChanges": {"color": 'steelblue', "marker": 'o'},
"DelayedActivityChanges": {"color": 'firebrick', "marker": 'd'},
}

plt.figure(figsize=(14, 10))
ms = 4 # marker size
lw = 2 # linewidth

for metric, style in color_linestyle_dict.items():
plt.plot(dates, metric_values[metric], label=metric, marker=style["marker"], markersize=ms, linewidth=lw, color=style["color"])

plt.title('Schedule Changes Over Time')

# Customize x-axis to show the first of every month with the format "mm/dd/yy"
plt.gca().xaxis.set_major_formatter(plt.matplotlib.dates.DateFormatter('%m/%d/%y'))
plt.gca().xaxis.set_major_locator(plt.matplotlib.dates.MonthLocator(bymonthday=1, interval=2))

# Rotate the x-axis labels by -30 degrees and align them to the left
plt.xticks(rotation=-30, ha='left')

# Remove top, right, and left spines
ax = plt.gca()
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False)

# Draw a vertical line for the current date if it is before the last date in data
current_date = datetime.datetime.now()
if current_date < max(dates):
plt.axvline(x=current_date, color='black', linestyle='-', linewidth=lw + 1, label='Current Date')

# Place the legend below the x-axis with no box around it on one line
plt.legend(loc='upper center', bbox_to_anchor=(0.5, -0.15), ncol=4, frameon=False)
plt.grid(True)

plt.tight_layout()
plt.show()
58 changes: 58 additions & 0 deletions snippets/explore_changes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import os
import sys
import json

# Add the package root directory to the sys.path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../')))

from dotenv import load_dotenv
load_dotenv() # Load environment variables from .env file

from smartpm.client import SmartPMClient
from smartpm.endpoints.projects import Projects # import projects to get project IDs
from smartpm.endpoints.scenarios import Scenarios
from smartpm.endpoints.changes import Changes

API_KEY = os.getenv("API_KEY")
COMPANY_ID = os.getenv("COMPANY_ID")

def main():
# Setup SDK
client = SmartPMClient(API_KEY, COMPANY_ID)
projects_api = Projects(client)
scenarios_api = Scenarios(client)
changes_api = Changes(client)

# Get Changes Summary
# -------------------
# Find project by name
name_to_find = "212096 - 401 FIRST STREET (College Station)" # replace with your project name
project = projects_api.find_project_by_name(name=name_to_find)
project_id = project["id"]

# Find scenario by name
scenario_to_find = "Full Schedule" # replace with your scenario name
matching_scenarios = scenarios_api.find_scenario_by_name(
project_id=project_id,
scenario_name=scenario_to_find
)
scenario_id = matching_scenarios[-1].get("id")

print("Get Summary Changes")
changes_summary = changes_api.get_changes_summary(
project_id=project_id,
scenario_id=scenario_id
)
print("Example changes summary entry:")
print(json.dumps(changes_summary[0], indent=4))
# -------------------

# Plot Changes Summary
# --------------------
changes_api.plot_changes_summary(
project_id=project_id,
scenario_id=scenario_id
)

if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion snippets/plot_progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def main():
)
print(json.dumps(complete_curve["data"][0], indent=4))

scenarios_api.plot_scenario_progress(
scenarios_api.plot_percent_complete_curve(
project_id=project_id,
scenario_id=scenario_id
)
Expand Down

0 comments on commit 795b94d

Please sign in to comment.