IfcCSV can now create summary rows (e.g. total, average) at the end of the table (may or may not be in addition to group totals).

This commit is contained in:
Dion Moult
2023-08-22 21:04:59 +10:00
parent 5e36255efe
commit e6e364780b
4 changed files with 141 additions and 70 deletions
@@ -82,6 +82,7 @@ class ImportCsvAttributes(bpy.types.Operator):
new.header = attribute["header"] new.header = attribute["header"]
new.sort = attribute["sort"] new.sort = attribute["sort"]
new.group = attribute["group"] new.group = attribute["group"]
new.summary = attribute["summary"]
return {"FINISHED"} return {"FINISHED"}
def invoke(self, context, event): def invoke(self, context, event):
@@ -104,7 +105,8 @@ class ExportCsvAttributes(bpy.types.Operator):
data = { data = {
"query": tool.Search.export_filter_query(props.filter_groups), "query": tool.Search.export_filter_query(props.filter_groups),
"attributes": [ "attributes": [
{"name": a.name, "header": a.header, "sort": a.sort, "group": a.group} for a in props.csv_attributes {"name": a.name, "header": a.header, "sort": a.sort, "group": a.group, "summary": a.summary}
for a in props.csv_attributes
], ],
} }
@@ -152,11 +154,14 @@ class ExportIfcCsv(bpy.types.Operator):
sort = [] sort = []
groups = [] groups = []
summaries = []
for attribute in props.csv_attributes: for attribute in props.csv_attributes:
if attribute.sort != "NONE": if attribute.sort != "NONE":
sort.append({"name": attribute.name, "order": attribute.sort}) sort.append({"name": attribute.name, "order": attribute.sort})
if attribute.group != "NONE": if attribute.group != "NONE":
groups.append({"name": attribute.name, "type": attribute.group, "varies_value": attribute.varies_value}) groups.append({"name": attribute.name, "type": attribute.group, "varies_value": attribute.varies_value})
if attribute.summary != "NONE":
summaries.append({"name": attribute.name, "type": attribute.summary})
sep = props.csv_custom_delimiter if props.csv_delimiter == "CUSTOM" else props.csv_delimiter sep = props.csv_custom_delimiter if props.csv_delimiter == "CUSTOM" else props.csv_delimiter
ifc_csv.export( ifc_csv.export(
@@ -174,6 +179,7 @@ class ExportIfcCsv(bpy.types.Operator):
bool_false=props.false_value, bool_false=props.false_value,
sort=sort, sort=sort,
groups=groups, groups=groups,
summaries=summaries,
) )
return {"FINISHED"} return {"FINISHED"}
@@ -49,6 +49,15 @@ class CsvAttribute(PropertyGroup):
] ]
) )
varies_value: StringProperty(default="Varies", name="Varies Value") varies_value: StringProperty(default="Varies", name="Varies Value")
summary: EnumProperty(
items=[
("NONE", "None", ""),
("SUM", "Sum", "Sums the total value of all rows."),
("AVERAGE", "Average", "Averages the total value of all rows."),
("MIN", "Min", "Gets the minimum value of all rows."),
("MAX", "Max", "Gets the maximum value of all rows."),
]
)
class CsvProperties(PropertyGroup): class CsvProperties(PropertyGroup):
@@ -96,4 +105,5 @@ class CsvProperties(PropertyGroup):
should_show_settings: BoolProperty(default=False, name="Show Settings") should_show_settings: BoolProperty(default=False, name="Show Settings")
should_show_sort: BoolProperty(default=False, name="Show Sorting") should_show_sort: BoolProperty(default=False, name="Show Sorting")
should_show_group: BoolProperty(default=False, name="Show Grouping") should_show_group: BoolProperty(default=False, name="Show Grouping")
should_show_summary: BoolProperty(default=False, name="Show Summary")
should_load_from_memory: BoolProperty(default=False, name="Load from Memory") should_load_from_memory: BoolProperty(default=False, name="Load from Memory")
@@ -47,8 +47,6 @@ class BIM_PT_ifccsv(Panel):
row.alignment = "RIGHT" row.alignment = "RIGHT"
row.operator("bim.import_csv_attributes", icon="IMPORT", text="") row.operator("bim.import_csv_attributes", icon="IMPORT", text="")
row.operator("bim.export_csv_attributes", icon="EXPORT", text="") row.operator("bim.export_csv_attributes", icon="EXPORT", text="")
row.prop(props, "should_show_sort", icon="SORTSIZE", text="")
row.prop(props, "should_show_group", icon="OUTLINER_COLLECTION", text="")
row.prop(props, "should_show_settings", icon="PREFERENCES", text="") row.prop(props, "should_show_settings", icon="PREFERENCES", text="")
if not IfcStore.get_file() or not props.should_load_from_memory: if not IfcStore.get_file() or not props.should_load_from_memory:
@@ -85,17 +83,22 @@ class BIM_PT_ifccsv(Panel):
row = layout.row(align=True) row = layout.row(align=True)
row.operator("bim.add_csv_attribute", icon="ADD") row.operator("bim.add_csv_attribute", icon="ADD")
row.prop(props, "should_show_sort", icon="SORTSIZE", text="")
row.prop(props, "should_show_group", icon="OUTLINER_COLLECTION", text="")
row.prop(props, "should_show_summary", icon="SYNTAX_ON", text="")
for index, attribute in enumerate(props.csv_attributes): for index, attribute in enumerate(props.csv_attributes):
row = layout.row(align=True) row = layout.row(align=True)
row.prop(attribute, "name", text="") row.prop(attribute, "name", text="")
row.prop(attribute, "header", text="")
if props.should_show_sort: if props.should_show_sort:
row.prop(attribute, "sort", text="") row.prop(attribute, "sort", text="")
if props.should_show_group: if props.should_show_group:
row.prop(attribute, "group", text="") row.prop(attribute, "group", text="")
if attribute.group == "VARIES": if attribute.group == "VARIES":
row.prop(attribute, "varies_value", text="") row.prop(attribute, "varies_value", text="")
row.prop(attribute, "header", text="") if props.should_show_summary:
row.prop(attribute, "summary", text="")
row.operator("bim.remove_csv_attribute", icon="X", text="").index = index row.operator("bim.remove_csv_attribute", icon="X", text="").index = index
row = layout.row(align=True) row = layout.row(align=True)
+66 -14
View File
@@ -74,6 +74,7 @@ class IfcCsv:
bool_false="NO", bool_false="NO",
sort=None, sort=None,
groups=None, groups=None,
summaries=None,
): ):
self.ifc_file = ifc_file self.ifc_file = ifc_file
self.results = [] self.results = []
@@ -112,7 +113,23 @@ class IfcCsv:
else: else:
self.headers.append(attribute) self.headers.append(attribute)
if groups: self.group_results(groups, attributes)
self.summarise_results(summaries, attributes)
self.sort_results(sort, attributes)
if format == "csv":
self.export_csv(output, delimiter=delimiter)
elif format == "ods":
self.export_ods(output, should_preserve_existing=should_preserve_existing)
elif format == "xlsx":
self.export_xlsx(output, should_preserve_existing=should_preserve_existing)
elif format == "pd":
return self.export_pd()
def group_results(self, groups, attributes):
if not groups:
return
group_results = {} group_results = {}
group_indices = {} group_indices = {}
group_values = {} group_values = {}
@@ -171,6 +188,44 @@ class IfcCsv:
self.results = group_results.values() self.results = group_results.values()
def summarise_results(self, summaries, attributes):
self.summaries = [None] * len(attributes)
if not summaries:
return
summary_indices = {}
summary_values = {}
for summary in summaries:
index = attributes.index(summary["name"])
summary_indices.setdefault(summary["type"], [])
summary_indices[summary["type"]].append(index)
for row in self.results:
for summary_type, sis in summary_indices.items():
if summary_type in ("SUM", "AVERAGE", "MIN", "MAX"):
for si in sis:
summary_values.setdefault(si, [])
try:
value = float(row[si])
except:
continue
summary_values[si].append(value)
for summary_type, sis in summary_indices.items():
for si in sis:
if summary_type == "SUM":
self.summaries[si] = sum(summary_values[si])
elif summary_type == "AVERAGE":
self.summaries[si] = mean(summary_values[si])
elif summary_type == "MIN":
self.summaries[si] = min(summary_values[si])
elif summary_type == "MAX":
self.summaries[si] = max(summary_values[si])
self.summaries[si] = summary_type.title() + ": " + str(self.summaries[si])
def sort_results(self, sort, attributes):
if sort: if sort:
def natural_sort(value): def natural_sort(value):
if isinstance(value, str): if isinstance(value, str):
@@ -187,28 +242,24 @@ class IfcCsv:
else: else:
self.results = sorted(self.results, key=lambda x: x[1 if include_global_id else 0]) self.results = sorted(self.results, key=lambda x: x[1 if include_global_id else 0])
if format == "csv":
self.export_csv(output, delimiter=delimiter)
elif format == "ods":
self.export_ods(output, should_preserve_existing=should_preserve_existing)
elif format == "xlsx":
self.export_xlsx(output, should_preserve_existing=should_preserve_existing)
elif format == "pd":
return self.export_pd()
def export_csv(self, output, delimiter=None): def export_csv(self, output, delimiter=None):
with open(output, "w", newline="", encoding="utf-8") as f: with open(output, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f, delimiter=delimiter) writer = csv.writer(f, delimiter=delimiter)
writer.writerow(self.headers) writer.writerow(self.headers)
for row in self.results: for row in self.results:
writer.writerow(row) writer.writerow(row)
if any([s for s in self.summaries if s is not None]):
writer.writerow(self.summaries)
def export_ods(self, output, should_preserve_existing=False): def export_ods(self, output, should_preserve_existing=False):
df = self.export_pd()
if self.summaries:
df.loc[df.shape[0]] = self.summaries
if os.path.exists(output) and should_preserve_existing: if os.path.exists(output) and should_preserve_existing:
ods_document = load(output) ods_document = load(output)
first_table = ods_document.spreadsheet.getElementsByType(Table)[0] first_table = ods_document.spreadsheet.getElementsByType(Table)[0]
df = self.export_pd()
for col_index, col in enumerate(df.columns): for col_index, col in enumerate(df.columns):
# Assuming the first row of the table contains headers # Assuming the first row of the table contains headers
header_cell = self.get_col(first_table.getElementsByType(TableRow)[0], col_index) header_cell = self.get_col(first_table.getElementsByType(TableRow)[0], col_index)
@@ -232,7 +283,6 @@ class IfcCsv:
ods_document.save(output) ods_document.save(output)
else: else:
df = self.export_pd()
df.to_excel(output, index=False, engine="odf") df.to_excel(output, index=False, engine="odf")
def set_cell_value(self, cell, value): def set_cell_value(self, cell, value):
@@ -265,6 +315,10 @@ class IfcCsv:
return new_cell return new_cell
def export_xlsx(self, output, should_preserve_existing=False): def export_xlsx(self, output, should_preserve_existing=False):
df = self.export_pd()
if self.summaries:
df.loc[df.shape[0]] = self.summaries
if os.path.exists(output): if os.path.exists(output):
book = openpyxl.load_workbook(output) book = openpyxl.load_workbook(output)
with pd.ExcelWriter( with pd.ExcelWriter(
@@ -273,10 +327,8 @@ class IfcCsv:
mode="a", mode="a",
if_sheet_exists="overlay" if should_preserve_existing else "replace", if_sheet_exists="overlay" if should_preserve_existing else "replace",
) as writer: ) as writer:
df = self.export_pd()
df.to_excel(writer, sheet_name=book.sheetnames[0], index=False) df.to_excel(writer, sheet_name=book.sheetnames[0], index=False)
else: else:
df = self.export_pd()
df.to_excel(output, index=False, engine="openpyxl") df.to_excel(output, index=False, engine="openpyxl")
def export_pd(self): def export_pd(self):