I am using python-docx to create a table with borders on all cells. When I merge cells involving outer cells some outer borders disappear. I use a function from other stackoverflow question -link shown as comment in code below- to set cell borders. How to fix that so outer borders are shown in merged cells?
Wrong borders:
Good borders:
Working example:
from docx import Document
from docx.oxml.shared import OxmlElement, qn
# from https://stackoverflow.com/questions/33069697/how-to-setup-cell-borders-with-python-docx
def set_cell_edges(cell, edges, color, style, width):
"""
Parameter Type Definition
=========== ==================== ==========================================================================================
cell Cell Cell to apply edges
edges str, list, None Cell edges, options are 'top', 'bottom', 'start' and 'end'
color str Edge color
style str Edge style, options are 'single', 'dotted', 'dashed', 'dashdotted' and 'double',
width int, float Edge width in points
"""
kwargs = dict()
for edge in edges:
kwargs[edge] = {'sz': width, 'val': style, 'color': color}
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
# check for tag existance, if none found then create one
tcBorders = tcPr.first_child_found_in("w:tcBorders")
if tcBorders is None:
tcBorders = OxmlElement('w:tcBorders')
tcPr.append(tcBorders)
# list over all available tags
for edge in ('start', 'top', 'end', 'bottom', 'insideH', 'insideV'):
edge_data = kwargs.get(edge)
if edge_data:
tag = 'w:{}'.format(edge)
# check for tag existance, if none found, then create one
element = tcBorders.find(qn(tag))
if element is None:
element = OxmlElement(tag)
tcBorders.append(element)
# looks like order of attributes is important
for key in ["sz", "val", "color", "space", "shadow"]:
if key in edge_data:
element.set(qn('w:{}'.format(key)), str(edge_data[key]))
if __name__ == '__main__':
rows = 3
columns = 3
document = Document()
# create table
table = document.add_table(rows=rows, cols=columns)
# merge cells
scell = table.rows[1].cells[1]
ecell = table.rows[2].cells[2]
scell.merge(ecell)
# set 4 borders in all cells
for row in table.rows:
for cell in row.cells:
set_cell_edges(cell, ['top', 'bottom', 'start', 'end'], '#ff0000', 'single', 1)
document.save('test.docx')
Of course, I can set an extra column and row to set the specific borders. But it would be nice to fix it without that trick. Example with the trick.
Good borders with trick:
if __name__ == '__main__':
rows = 3
columns = 3
document = Document()
# create table
table = document.add_table(rows=rows+1, cols=columns+1)
# merge cells
scell = table.rows[1].cells[1]
ecell = table.rows[2].cells[2]
scell.merge(ecell)
# set 4 borders in all cells
for row in table.rows[:-1]:
for cell in row.cells[:-1]:
set_cell_edges(cell, ['top', 'bottom', 'start', 'end'], '#ff0000', 'single', 1)
# set top border in last row
for cell in table.rows[-1].cells[:-1]:
set_cell_edges(cell, ['top'], '#ff0000', 'single', 1)
# set left border in last column
for cell in table.columns[-1].cells[:-1]:
set_cell_edges(cell, ['start'], '#ff0000', 'single', 1)
document.save('test.docx')
Related
and thanks in advance for any assistance.
Based on the code snippet below, I'm trying to take the information from SUMMARY_LIST and add it to an xlsx worksheet. I'm then trying to colorize the cells based on 3 conditional formats,
green, text in cell in column A is equal to PASS
yellow, text in cell in column A contains the word PARTIAL and the value in column B of the same row is greater or equal to 6; or the text in cell in column B is equal to CRITICAL
red, text in cell in column A contains the word FAIL and the value in column B if the same row is greater than or equal to 6; or the text in cell in column B is equal to EMERGENCY.
If I hard code the cell in the condition to either $A1 or $B1, I get the desired result, which is that the if my condition passes, then both cells in the row that match either condition will get the appropriate background color.
If I substitute xl_rowcol_to_cell(0,0,col_abs=True) for $A1 and/or xl_rowcol_to_cell(0,1,col_abs=True) for $B1, the cells in my row doesn't get colorized.
In the snippet, I'm trying to colorize the background red if either my cell in column A contains the word "FAIL" and the value in column B is >= 6; OR the cell in column B contains the word "EMERGENCY".
When I run the script, neither the cells in the row containing the word FAIL or the row containing the word EMERGENCY are changed to a red background.
Is there an error in my conditional formatting statement for formatting in red or is it not possible to use the xl_rowcol_to_cell as an input to the condition?
import xlsxwriter
from xlsxwriter.utility import xl_range
from xlsxwriter.utility import xl_range_abs
from xlsxwriter.utility import xl_rowcol_to_cell
SUMMARY_LIST = [{'Item': 'Date', 'Value': '2023-01-11 14:09:16'}, {'Item': 'O/S Version', 'Value': '18.04.6 LTS (Bionic Beaver)'}, {'Item': 'CRITICAL EXPIRY THRESHOLD', 'Value': '6700'}, {'Item': 'EMERGENCY EXPIRY THRESHOLD', 'Value': '720'}, {'Item': 'TOTAL', 'Value': 6}, {'Item': 'PASS', 'Value': 6}, {'Item': 'PARTIAL PASS', 'Value': 6}, {'Item': 'FAIL', 'Value': 6}, {'Item': 'cert1', 'Value': 'EMERGENCY'}, {'Item': 'cert2', 'Value': 'CRITICAL'}]
wb = xlsxwriter.Workbook('myTest.xlsx')
ws = wb.add_worksheet()
row = 0
col = 0
inc = 0
formatEmerg = wb.add_format({'bg_color': 'red'})
formatCrit = wb.add_format({'bg_color': 'yellow'})
formatOk = wb.add_format({'bg_color': 'green'})
for item in SUMMARY_LIST:
inc = 0
for x in item.values():
ws.write(row,col + inc,x )
inc += 1
last_row = row
row += 1
col = 0
ws.conditional_format(xl_range(0,0,last_row,1),
{
'type':'formula',
'criteria':'=OR(and(left(xl_rowcol_to_cell(0,0,col_abs=True),4)="FAIL",(xl_rowcol_to_cell(0,1,col_abs=True)>=6)),left(xl_rowcol_to_cell(0,1,col_abs=True),9)="EMERGENCY")',
'format': formatEmerg
})
ws.conditional_format(xl_range(0,0,last_row,1),
{
'type':'formula',
'criteria':'=OR(and(left($A1,7)="PARTIAL",($B1>=6)),left($B1,8)="CRITICAL")',
'format': formatCrit
})
ws.conditional_format(xl_range(0,0,last_row,1),
{
'type':'formula',
'criteria':'=OR(and(left($A1,4)="PASS",($B1>=6)),left($B1,8)="CRITICAL")',
'format': formatOk
})
wb.close()
I haven't tried anything other than hardcoding the cell, or using the xl_rowcol_to_cell function.
My expectation is that the absolute column, represented by $A1, generated by xl_rowcol_to_cell(0,1,col_abs=True) would be substituted and the condition would evaluate the same as hardcoding the column as $A1. Same for column $B1.
Is it possible to create a multi label widget like a tabular column ? For, example as shown in the snapshot attached ?
Kindly let me know or provide some comments if there is any option to create widgets with multi label option ?
Thanks !
There is no built-in way to do that, but it's relatively easy to write your own using a canvas. For example, put one label in the upper-right corner and the other in the lower-left. Then, draw a line from upper-left to lower-right.
Example:
import tkinter as tk
class CustomLabel(tk.Frame):
def __init__(self, parent, label1, label2, **kwargs):
tk.Frame.__init__(self, parent, **kwargs)
self.canvas = tk.Canvas(self, borderwidth=0, highlightthickness=0, background=self.cget("background"))
self.canvas.pack(fill="both", expand=True)
l1 = tk.Label(self.canvas, text=label1, background=self.cget("background"))
l2 = tk.Label(self.canvas, text=label2, background=self.cget("background"))
l1.place(relx=.75, rely=.25, anchor="c")
l2.place(relx=.25, rely=.75, anchor="c")
# arrange for the line to be redrawn whenever the canvas
# changes size
self.canvas.bind("<Configure>", self.handle_configure)
# set the default size to be relative to the requested size
# of the labels plus some margin
width = l1.winfo_reqwidth() + l2.winfo_reqwidth() + 4
height = l1.winfo_reqheight() + l2.winfo_reqheight() + 4
self.canvas.configure(width=width, height=height)
def handle_configure(self, event):
self.canvas.delete("line")
self.canvas.create_line(0,0,event.width, event.height, tags=("line",))
Example usage:
root = tk.Tk()
colors = ("SteelBlue4", "SteelBlue3", "SkyBlue1")
for row in range(3):
for column in range(4):
if row == 0 and column == 0:
widget = CustomLabel(root, "Place", "Name", background=colors[row])
else:
widget = tk.Label(root, text="", background=colors[row])
widget.grid(row=row, column=column, sticky="nsew", padx=1, pady=1)
for row in range(3):
root.grid_rowconfigure(row, uniform="row")
for column in range(4):
root.grid_columnconfigure(column, uniform="column")
Screenshot:
I want to add colour to a row only if a row's cell's value is present in another datasource.
I mean, I have the list A, and the table B, so I want to colour the row X in B if a cell of the row contains a value from A...
I don't even know from where to start..
Pretty much you need to do what ChesuCR mentioned in their comment. To take it one step further, see below a small bokeh application.
If you edit values in the first table, a callback will run and check each 'y' value. An additional column is needed to keep track whether the 'y' values are contained in the seperate list/data source. The value of the additional column is then used to color the cell.
from bokeh.layouts import row
from bokeh.models import ColumnDataSource
from bokeh.models.widgets import DataTable, TableColumn, HTMLTemplateFormatter
from bokeh.io import curdoc
def update_included(attr, old, new):
list_a = [float(a) for a in source_1.data['a']]
ys = source_2.data['y']
y_in = []
for i, y in enumerate(ys):
if y in list_a:
y_in.append(1)
else:
y_in.append(0)
print(ys, y_in, list_a)
source_2.data['y_in'] = y_in
source_1 = ColumnDataSource(data={'a':[1001,1100]})
columns = [
TableColumn(field="a", title="Criteria list")
]
data_table1 = DataTable(source=source_1, columns=columns, width=400, editable=True)
dict1 = {'x':[0]*6,
'y':[0,10,12,13,200,2001],
'y_in':[0]*6}
source_2 = ColumnDataSource(data=dict1)
template="""
<div style="background:<%=
(function colorfromint(){
if(y_in == 1){
return("blue")}
else{return("red")}
}()) %>;
color: white">
<%= value %></div>
"""
formater = HTMLTemplateFormatter(template=template)
columns = [
TableColumn(field="x", title="x"),
TableColumn(field="y", title="y",formatter=formater)
]
data_table2 = DataTable(source=source_2, columns=columns, width=400)
source_1.on_change('data', update_included)
curdoc().add_root(row(data_table1, data_table2))
I have horizontal and vertical alignment set to center, but only the first two columns are centering the data.
The third column appears to be setting the data with a 'top' vertical alignment:
#!/usr/bin/env python
import xlsxwriter
# Create our spreadsheet.
workbook = xlsxwriter.Workbook('stats.xlsx')
worksheet = []
# Open our data file to extract data.
data = open('server_results','r')
# Create a bold centered font for our column headers.
format = workbook.add_format()
format.set_bold()
format.set_align('center')
format.set_align('vcenter')
format.set_text_wrap()
# Non-headers will be centered.
format1 = workbook.add_format()
format1.set_align('center')
format1.set_align('vcenter')
format1.set_text_wrap()
# Provider label/name for worksheet.
worksheet.append(workbook.add_worksheet('Stats'))
# Adjust column widths for data.
worksheet[0].set_column(0,2,25)
#worksheet[0].set_column(1,1,25)
#worksheet[0].set_column(1,2,25)
#worksheet[0].set_row(0,40)
# Start from the first cell. Rows and columns are zero indexed.
worksheet[0].write('A1', 'Column 1', format)
worksheet[0].write('B1', 'Column 2', format)
worksheet[0].write('C1', 'Column 3', format)
row = 1
col = 0
# Count lines
linelist = data.readlines()
count = len(linelist)
# Populate spreadsheet.
for num in range (0, count):
line = linelist[num]
splitline = line.split("\t")
worksheet[0].write(row, col, splitline[0], format1)
worksheet[0].write(row, col + 1, splitline[1], format1)
worksheet[0].write(row, col + 2, splitline[2], format1)
row += 1
#close workbook
workbook.close()
EOF
I've tried modifying how I format the data with following code:
format = workbook.add_format({'bold': True, 'bg_color': 'yellow', 'border': 5, 'align': 'center', 'valign': 'middle', 'text_wrap': True})
but when I do this, then NONE of the columns are centered -- with this format the first two columns are bottom aligned and third column is top aligned
The third column appears to be setting the data with a 'top' vertical alignment
I ran a version of your program with some sample data and I don't see that behaviour:
import xlsxwriter
# Create our spreadsheet.
workbook = xlsxwriter.Workbook('stats.xlsx')
worksheet = []
# Create a bold centered font for our column headers.
format = workbook.add_format()
format.set_bold()
format.set_align('center')
format.set_align('vcenter')
format.set_text_wrap()
# Non-headers will be centered.
format1 = workbook.add_format()
format1.set_align('center')
format1.set_align('vcenter')
format1.set_text_wrap()
# Provider label/name for worksheet.
worksheet.append(workbook.add_worksheet('Stats'))
# Adjust column widths for data.
worksheet[0].set_column(0,2,25)
# Start from the first cell. Rows and columns are zero indexed.
worksheet[0].write('A1', 'Column 1', format)
worksheet[0].write('B1', 'Column 2', format)
worksheet[0].write('C1', 'Column 3', format)
row = 1
col = 0
# Count lines
linelist = ['foo\tbar\tbaz\tbing'] * 5
count = len(linelist)
# Populate spreadsheet.
for num in range (0, count):
line = linelist[num]
splitline = line.split("\t")
worksheet[0].write(row, col, splitline[0], format1)
worksheet[0].write(row, col + 1, splitline[1], format1)
worksheet[0].write(row, col + 2, splitline[2], format1)
row += 1
#close workbook
workbook.close()
The output shows horizontal and vertical centering as expected:
Could you clarify what the issue is.
Using the following:
splitline = [l.rstrip() for l in line.split("\t")]
resolved the issue.
After grouping etc. I get a Series like in the example below. I would like to show the average numbers for each bar. The code below shows only one entry (of course, as I have only one "legend"). Could anyone one suggest a smart way of showing these numbers?
%matplotlib inline
import matplotlib.pyplot as plt
import matplotlib
matplotlib.style.use('ggplot')
import pandas
# create Series
dict_ = {"Business" : 104.04,"Economy":67.04, "Markets":58.56, "Companies":38.48}
s = pandas.Series(data=dict_)
# plot it
ax = s.plot(kind='bar', color='#43C6DB', stacked=True, figsize=(20, 10), legend=False)
plt.tick_params(axis='both', which='major', labelsize=14)
plt.xticks(rotation=30) #rotate labels
# Shrink current axis by 20%
box = ax.get_position()
ax.set_position([box.x0, box.y0, box.width * 0.8, box.height])
#create new legend
legend = ['%s (%.1f a day)' %(i, row/7) for i, row in s.iteritems()]
# Put the legend to the right of the current axis
L = ax.legend(legend, loc='center left', bbox_to_anchor=(1, 0.5), fontsize=18)
plt.show()
The legend only has a single entry. This is a handle of a blue bar. Therefore even if you set the labels to a longer list, only the first element of that list is used as label for the existing handle.
The idea can be to duplicate the legend handle to have the same size as the labels
legend = ['%s (%.1f a day)' %(i, row/7) for i, row in s.iteritems()]
h,l = ax.get_legend_handles_labels()
L = ax.legend(handles = h*len(legend), labels=legend, loc='center left',
bbox_to_anchor=(1, 0.5), fontsize=18)