class ListElement(Element): style = { 'text': { PYDOCX_FONT_SIZE: DEFAULT_TABLE_FONT_SIZE, PYDOCX_FONT_NAME: get_chart_font(), PYDOCX_FONT_COLOR: DEFAULT_FONT_COLOR, PYDOCX_FONT_BOLD: False, }, 'title': { PYDOCX_FONT_NAME: get_chart_font(), PYDOCX_FONT_COLOR: DEFAULT_TITLE_COLOR, PYDOCX_FONT_SIZE: DEFAULT_TITLE_FONT_SIZE, PYDOCX_FONT_BOLD: False, } } def insert(self): if DEBUG: print("Adding list...") list_data = self.section.contents if isinstance(list_data, dict) and len(list_data) != 1: list_data = [list_data] if isinstance(list_data, dict) and len(list_data) == 1: # Create the parent title wrapper_table = self.cell_object.cell.add_table(rows=2, cols=1) title_cell = wrapper_table.cell(0, 0) title_text = list(list_data.keys())[0] insert_text(title_cell, title_text, self.style['title']) # Create a list in a list because this is a grouped list co = CellObject(wrapper_table.cell(1, 0)) table_data = list_data[title_text] invoke(co, Section('list', table_data, self.section.layout, {})) else: table.invoke(self.cell_object, Section('table', list_data, self.section.layout, {'list_style': True}))
class ColumnChartElement(Element): style = { 'title': { 'fontname': get_chart_font(), 'color': DEFAULT_FONT_COLOR, 'fontsize': DEFAULT_TITLE_FONT_SIZE } } @utils.plot def insert(self) -> None: """ This is a standing barchart (bar goes up) """ if DEBUG: print("Adding a column chart!") # Fix sizing size_w, size_h, dpi = utils.convert_plt_size(self.section) plt.figure(figsize=(size_w, size_h), dpi=dpi) data = self.section.contents objects = [i['name'] for i in data] y_axis = [i for i in range(len(objects))] x_axis = [i['data'][0] for i in data] colors = get_colors(self.section.layout, objects) rects = plt.bar(y_axis, x_axis, align='center', alpha=DEFAULT_BAR_ALPHA, width=DEFAULT_BAR_WIDTH, color=colors) ax = plt.gca() remove_plot_borders(ax) # Fix the legend values to be "some_value (some_number)" instead of # just "some_value" ledgend_keys = [CHART_LABEL_NONE_STRING if i == '' else i for i in objects] fixed_legends = [f'{v} ({x_axis[i]})' for i, v in enumerate(ledgend_keys)] # Move legend legend_location = 'upper center' legend_location_relative_to_graph = (0.5, -0.35) a = ax.legend(rects, fixed_legends, loc=legend_location, bbox_to_anchor=legend_location_relative_to_graph, handlelength=0.7) self.section = change_legend_vertical_alignment(self.section, top=3) set_legend_style(a, self.section.layout[LEGEND_STYLE]) ax.set_xlim(-len(objects), len(objects)) set_axis_font(ax) plt.xticks(y_axis, objects) plt.title(self.section.extra['title'], **self.style['title']) plt_b64 = utils.plt_t0_b64(plt) s = Section('image', plt_b64, {}, {'should_shrink': True}) image.invoke(self.cell_object, s)
class PieChartElement(Element): style = { 'title': { PYDOCX_FONT_NAME: get_chart_font(), PYDOCX_FONT_COLOR: DEFAULT_FONT_COLOR, PYDOCX_FONT_SIZE: DEFAULT_TITLE_FONT_SIZE } } @utils.plot def insert(self): if DEBUG: print('Adding pie chart: ...') size_w, size_h, dpi = utils.convert_plt_size(self.section) fig, ax = plt.subplots(figsize=(size_w, size_h), dpi=dpi, subplot_kw=dict(aspect="equal")) data = [int(i['data'][0]) for i in self.section.contents] objects = [i['name'] for i in self.section.contents] # Fix the unassigned key: objects = [i if i != "" else "Unassigned" for i in objects] # Generate the default colors colors = get_colors(self.section.layout, objects) unassigned_color = 'darkgrey' # If we have predefined colors, use them if 'legend' in self.section.layout and self.section.layout['legend']: for i in self.section.layout['legend']: if 'color' in i: colors.append(i['color']) elif 'fill' in i: colors.append(i['fill']) color_keys = {} for i, k in enumerate(objects): color_keys[k] = colors[i] if k == 'Unassigned': color_keys['Unassigned'] = unassigned_color final_colors = [color_keys[k] for k in objects] wedges, texts = ax.pie(data, colors=final_colors, startangle=90, pctdistance=0.85, textprops=dict(color="w"), radius=1) keys_with_numbers = ['{}: {}'.format(k, data[i]) for i, k in enumerate(objects)] # legend_location_relative_to_graph = (1, 0, 0.5, 1) # legend_location = self.section.layout['legendStyle'] legend_location = 'upper center' legend_location_relative_to_graph = (0.5, 0) legend = ax.legend(wedges, keys_with_numbers, title="", loc=legend_location, bbox_to_anchor=legend_location_relative_to_graph, handlelength=0.7 ) set_legend_style(legend, self.section.layout[LEGEND_STYLE]) set_axis_font(ax) ax.set_title(self.section.extra['title'], **self.style['title']) circle = plt.Circle((0, 0), 0.7, fc='white') ax.add_artist(circle) plt_b64 = utils.plt_t0_b64(plt) s = Section('image', plt_b64, {}, {'should_shrink': True}) image.invoke(self.cell_object, s)
class TableElement(Element): style = { 'text': { PYDOCX_FONT_SIZE: DEFAULT_TABLE_FONT_SIZE, PYDOCX_FONT_NAME: get_chart_font(), PYDOCX_FONT_COLOR: DEFAULT_FONT_COLOR, PYDOCX_FONT_BOLD: False, }, 'title': { PYDOCX_FONT_NAME: get_chart_font(), PYDOCX_FONT_COLOR: DEFAULT_TITLE_COLOR, PYDOCX_FONT_SIZE: DEFAULT_TITLE_FONT_SIZE, PYDOCX_FONT_BOLD: False, }, } def insert(self): if DEBUG: print("Adding table...") table_data = self.section.contents if isinstance(table_data, dict): table_data = table_data.get('data', table_data) # If table columns isn't present, use the dict values of the table data # as table columns (kind of like list). if 'tableColumns' not in self.section.layout: self.section.layout['tableColumns'] = list(table_data[0].keys()) # Use and order according to readableHeaders if present. if 'readableHeaders' in self.section.layout: ordered = self.section.layout['tableColumns'] readable_headers = self.section.layout['readableHeaders'] table_columns = fix_order(ordered, readable_headers) else: table_columns = self.section.layout['tableColumns'] # Quick fix, word crashes on more than MAX_MS_TABLE_COLS_LIMIT # (64 right now) columns. # See: https://stackoverflow.com/questions/36921010/docx-does-not-support-more-than-63-columns-in-a-table table_columns = table_columns[0:MAX_MS_TABLE_COLS_LIMIT] for i, row_title in enumerate(table_columns): if not isinstance(row_title, str): table_columns.remove(row_title) if 'title' in self.section.extra: table = self.cell_object.cell.add_table(rows=2, cols=len(table_columns)) title = table.cell(0, 0) title.merge(table.cell(0, len(table_columns) - 1)) insert_text(title, self.section.extra['title'], self.style['title']) hdr_cells = table.rows[1].cells else: table = self.cell_object.cell.add_table(rows=1, cols=len(table_columns)) hdr_cells = table.rows[0].cells table.style = DEFAULT_TABLE_STYLE if 'list_style' in self.section.extra and self.section.extra[ 'list_style']: table.style = None for i, row_title in enumerate(table_columns): insert_text(hdr_cells[i], row_title, self.style['text']) for row_item in table_data: row_cells = table.add_row().cells for i, row_title in enumerate(table_columns): if row_title not in row_item: continue # Old json format can have 'Avatars', which are images if isinstance(row_item[row_title], dict) and \ row_item[row_title]['type'] == 'image': insert_table_image(row_item, row_title, row_cells[i]) else: insert_text(row_cells[i], str(row_item[row_title]), self.style['text'])
class BarChartElement(Element): style = { 'title': { PYDOCX_FONT_NAME: get_chart_font(), PYDOCX_FONT_COLOR: DEFAULT_FONT_COLOR, PYDOCX_FONT_SIZE: DEFAULT_TITLE_FONT_SIZE } } @utils.plot def insert(self): """ This is a bar chart on the side (bar goes right) """ if DEBUG: print("Adding a bar chart") # Fix sizing size_w, size_h, dpi = utils.convert_plt_size(self.section, self.cell_object) plt.figure(figsize=(size_w, size_h), dpi=dpi) data = self.section.contents x_axis = None if any([True for i in data if 'groups' in i and i['groups']]): # Note for future maintainer, I really really... hate stacked # bar charts, it made this file look like hell. I hope you cope # with matplotlib's shitt* implementation. # May the force be with you :pray: # Create the stacks agg = [] y_axis = [i['name'] for i in data] max_labels_stacked = [] for v in data: names = [i['name'] for i in v['groups']] max_labels_stacked = list(set(max_labels_stacked) | set(names)) labels = sorted(max_labels_stacked) colors = get_colors(self.section.layout, labels) for v in data: current_labels = {i['name']: i['data'][0] for i in v['groups']} cols = [] for l in labels: if l in current_labels: cols.append(current_labels[l]) else: cols.append(0) agg.append(cols) stacked = [i for i in zip(*agg)] # Draw each stack rects = [ plt.barh(y_axis, stacked[0], DEFAULT_BAR_WIDTH, color=colors.pop(0)) ] for i in range(1, len(stacked)): left_padding = [sum(i) for i in zip(*stacked[:i])] rects.append( plt.barh(y_axis, stacked[i], DEFAULT_BAR_WIDTH, left=left_padding, color=colors.pop(0))) ax = plt.gca() legend_location = 'upper center' legend_location_relative_to_graph = (0.5, -0.35) a = ax.legend(rects, labels, loc=legend_location, bbox_to_anchor=legend_location_relative_to_graph, handlelength=0.7) else: objects = [i['name'] for i in data] colors = get_colors(self.section.layout, objects) y_axis = [i for i in range(len(objects))] x_axis = [i['data'][0] for i in data] # Fix height of bar when only one present if len(objects) == 1: plt.margins(y=2) rects = plt.barh(y_axis, width=x_axis, align='center', alpha=DEFAULT_BAR_ALPHA, color=colors, height=DEFAULT_BAR_WIDTH) # Fix the legend values to be "some_value (some_number)" instead of # just "some_value" ledgend_keys = [ CHART_LABEL_NONE_STRING if i == '' else i for i in objects ] fixed_legends = [ f'{v} ({x_axis[i]})' for i, v in enumerate(ledgend_keys) ] # Create and move the legend outside ax = plt.gca() legend_location = 'upper center' legend_location_relative_to_graph = (0.5, -0.35) a = ax.legend(rects, fixed_legends, loc=legend_location, bbox_to_anchor=legend_location_relative_to_graph, handlelength=0.7) ax.set_yticklabels([]) # Style the axis and labels self.section = change_legend_vertical_alignment(self.section, top=1) set_legend_style(a, self.section.layout[LEGEND_STYLE]) # Fix the axises set_axis_font(ax) ax.set_yticks(y_axis) ax.set_xlabel('') ax.invert_yaxis() # labels read top-to-bottom # Fix the xaxis ratio to fit biggest element if x_axis: ax.set_xlim(0, max(x_axis) + X_AXIS_PADDING) # Remove the bottom labels remove_plot_borders(ax) plt.tick_params(bottom='off') plt.title(self.section.extra['title'], **self.style['title']) plt_b64 = utils.plt_t0_b64(plt, (size_w, size_h), dpi) s = Section('image', plt_b64, {}, {'should_shrink': True}) image.invoke(self.cell_object, s)
class LineChartElement(Element): style = { 'title': { PYDOCX_FONT_NAME: get_chart_font(), PYDOCX_FONT_COLOR: DEFAULT_FONT_COLOR, PYDOCX_FONT_SIZE: DEFAULT_TITLE_FONT_SIZE } } @utils.plot def insert(self): if DEBUG: print("Adding line chart...") # Fix sizing size_w, size_h, dpi = utils.convert_plt_size(self.section, self.cell_object) figure(num=2, figsize=(size_w, size_h), dpi=dpi, constrained_layout=False) data = self.section.contents fix_data(data) # Make the groups look like: # groups = { # 'Type A': { # dates: ['2000', '2001', '2002'] # values: ['1', '2', '3'] # } # 'Type B': { # dates: ['2000', '2001', '2002'], # values : ['4', '5', '6'] # } groups = fix_data(data) # Fix empty key if '' in groups.keys(): groups['None'] = groups.pop('') # Generate the default colors colors = get_colors(self.section.layout, groups.keys()) unassigned_color = 'darkgrey' # If we have predefined colors, use them if 'legend' in self.section.layout and self.section.layout['legend']: for i in self.section.layout['legend']: if 'color' in i: colors.append(i['color']) elif 'fill' in i: colors.append(i['fill']) color_keys = {} for i, k in enumerate(groups.keys()): color_keys[k] = colors[i] if k == 'Unassigned': color_keys['Unassigned'] = unassigned_color final_colors = {k: color_keys[k] for k in groups.keys()} # Plot the lines for group, line in groups.items(): x_axis = line['dates'] y_axis = line['values'] plt.plot(x_axis, y_axis, marker='', color=final_colors[group], linewidth=2) # Create and move the legend outside ax = plt.gca() # Auto rotate the labels remove_plot_borders(ax) legend_location = 'upper center' legend_location_relative_to_graph = (0.5, -0.35) handles = [plt.Rectangle((0, 0), 1, 1, fc=final_colors[i]) for i in groups.keys()] legend = ax.legend(handles, [i for i in groups.keys()], loc=legend_location, bbox_to_anchor=legend_location_relative_to_graph, handlelength=0.7, handleheight=0.7, ncol=2) self.section = change_legend_vertical_alignment(self.section, top=1) # Set max ticks in xaxis to be MAX_AXIS_LABELS set_legend_max_count(ax, self.cell_object) set_legend_style(legend, self.section.layout[LEGEND_STYLE]) set_axis_font(ax) ax.set_title(self.section.extra['title'], **self.style['title']) # Add to docx as image plt_b64 = utils.plt_t0_b64(plt, (size_w, size_h), dpi) s = Section('image', plt_b64, {}, {'should_shrink': True}) image.invoke(self.cell_object, s)
class TableElement(Element): style = { 'text': { PYDOCX_FONT_SIZE: DEFAULT_TABLE_FONT_SIZE, PYDOCX_FONT_NAME: get_chart_font(), PYDOCX_FONT_COLOR: DEFAULT_FONT_COLOR, PYDOCX_FONT_BOLD: False, }, 'title': { PYDOCX_FONT_NAME: get_chart_font(), PYDOCX_FONT_COLOR: DEFAULT_TITLE_COLOR, PYDOCX_FONT_SIZE: DEFAULT_TITLE_FONT_SIZE, PYDOCX_FONT_BOLD: False, } } def insert(self): if DEBUG: print("Adding table...") table_data = self.section.contents if isinstance(table_data, dict): table_data = table_data.get('data', table_data) if 'tableColumns' not in self.section.layout: # Quick dirty fix for test - will be refactored in upcoming PR self.section.layout['tableColumns'] = list(table_data[0].keys()) # Fix new lists if isinstance(table_data, dict): wrapper_table = self.cell_object.cell.add_table( rows=2, cols=len(table_data.keys())) i = 0 # Add the wrapping headers for wrapper_header, table_contents in table_data.items(): hdr = wrapper_table.cell(0, i) insert_text(hdr, wrapper_header, self.style['title']) body = wrapper_table.cell(1, i) c = CellObject(body) # Hacky but will do the job invoke(c, Section('table', table_contents, {}, {})) i += 1 return if 'readableHeaders' in self.section.layout: ordered = self.section.layout['tableColumns'] readable_headers = self.section.layout['readableHeaders'] table_columns = fix_order(ordered, readable_headers) else: table_columns = self.section.layout['tableColumns'] # Quick fix, word crashes on more than 64 columns. # See: https://stackoverflow.com/questions/36921010/docx-does-not-support-more-than-63-columns-in-a-table table_columns = table_columns[0:63] for i, header_text in enumerate(table_columns): if not isinstance(header_text, str): table_columns.remove(header_text) if 'title' in self.section.extra: table = self.cell_object.cell.add_table(rows=2, cols=len(table_columns)) title = table.cell(0, 0) title.merge(table.cell(0, len(table_columns) - 1)) insert_text(title, self.section.extra['title'], self.style['title']) hdr_cells = table.rows[1].cells else: table = self.cell_object.cell.add_table(rows=2, cols=len(table_columns)) hdr_cells = table.rows[0].cells table.style = DEFAULT_TABLE_STYLE if 'list_style' in self.section.extra and self.section.extra[ 'list_style']: table.style = None for i, header_text in enumerate(table_columns): insert_text(hdr_cells[i], header_text, self.style['text']) if len(table_columns) > 63: # TODO: add error. pass for r in table_data: row_cells = table.add_row().cells for i, header_text in enumerate(table_columns): if header_text not in r: continue # Old json format can have 'Avatars', which are images if isinstance(r[header_text], dict) and \ r[header_text]['type'] == 'image': row_temp = r[header_text] s = Section(row_temp['type'], row_temp['data'], {}, {}) co = CellObject(row_cells[i], add_run=False) image.invoke(co, s) else: insert_text(row_cells[i], r[header_text], self.style['text'])
class TableElement(Element): style = { 'text': { PYDOCX_FONT_SIZE: DEFAULT_TABLE_FONT_SIZE, PYDOCX_FONT_NAME: get_chart_font(), PYDOCX_FONT_COLOR: DEFAULT_FONT_COLOR, PYDOCX_FONT_BOLD: False, }, 'title': { PYDOCX_FONT_NAME: get_chart_font(), PYDOCX_FONT_COLOR: DEFAULT_TITLE_COLOR, PYDOCX_FONT_SIZE: DEFAULT_TITLE_FONT_SIZE, PYDOCX_FONT_BOLD: False, } } def insert(self): if DEBUG: print("Adding table...") table_data = self.section.contents if 'tableColumns' not in self.section.layout: return if 'readableHeaders' in self.section.layout: ordered = self.section.layout['tableColumns'] readable_headers = self.section.layout['readableHeaders'].values() table_columns = fix_order(ordered, readable_headers) else: table_columns = self.section.layout['tableColumns'] for i, header_text in enumerate(table_columns): if not isinstance(header_text, str): table_columns.remove(header_text) if 'title' in self.section.extra: table = self.cell_object.cell.add_table(rows=2, cols=len(table_columns)) title = table.cell(0, 0) title.merge(table.cell(0, len(table_columns) - 1)) insert_text(title, self.section.extra['title'], self.style['title']) hdr_cells = table.rows[1].cells else: table = self.cell_object.cell.add_table(rows=2, cols=len(table_columns)) hdr_cells = table.rows[0].cells table.style = DEFAULT_TABLE_STYLE if 'list_style' in self.section.extra and self.section.extra[ 'list_style']: table.style = None for i, header_text in enumerate(table_columns): insert_text(hdr_cells[i], header_text, self.style['text']) for r in table_data: row_cells = table.add_row().cells for i, header_text in enumerate(table_columns): if header_text not in r: continue # Old json format can have 'Avatars', which are images if isinstance(r[header_text], dict) and \ r[header_text]['type'] == 'image': row_temp = r[header_text] s = Section(row_temp['type'], row_temp['data'], {}, {}) co = CellObject(row_cells[i], add_run=False) image.invoke(co, s) else: insert_text(row_cells[i], r[header_text], self.style['text'])