I've created a table using ReportLab. I would like to conditionally color the cells, depending on their contents (in my case, I want negative numbers to be red). To be clear, I have the conditional code working, I can't figure out how to add color. What I've tried:
using the <font color="..."> tag. Instead, the tags is included verbatim in the output.
wrapping each cell in Paragraph(...) (suggested in this answer). In this case, the cell text is linewrapped after each letter.
wrapping the table in Paragraph(...). In this case, reportlab errors out (I believe the resulting error was TypeError: split() missing required positional argument: 'availHeight')
I found reportlab.platypus.tables.CellStyle in the reportlab source code, but can't figure out make use of it. Google turns up nothing useful and it's not mentioned in the reportlab documentation.
I guess TableStyle(...) rules could be used, but the cells aren't in a predetermined position within the table (which is what all the examples assume).
Help appreciated!
Using TableStyle() would be an acceptable solution. You could loop through the data and add a style command when the condition is met.
Here is an example:
import random
from reportlab.lib.pagesizes import letter
from reportlab.lib.colors import red
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle
# Generate random data with positive and negative values as list of lists.
data = []
for _ in range(20):
data.append(random.sample(range(-10, 10), 5))
table_style = TableStyle([('ALIGN', (0, 0), (-1, -1), 'RIGHT')])
# Loop through list of lists creating styles for cells with negative value.
for row, values, in enumerate(data):
for column, value in enumerate(values):
if value < 0:
table_style.add('TEXTCOLOR', (column, row), (column, row), red)
table = Table(data)
table.setStyle(table_style)
pdf = SimpleDocTemplate('example.pdf', pagesize=letter)
pdf.build([table])
Related
Is it possible to combine a selection and a predicate in a condition? I would like to color points on a scatterplot only if the group is selected and above a certain value.
import altair as alt
from vega_datasets import data
source = data.cars()
selection = alt.selection_multi(fields=['Origin'])
color = alt.condition(
selection & (alt.datum.Miles_per_Gallon > 18),
alt.Color('Origin:N'),
alt.value('lightgray')
)
alt.Chart(source).mark_circle().encode(
x='Horsepower',
y='Miles_per_Gallon',
color=color,
tooltip=['Name', 'Origin', 'Horsepower', 'Miles_per_Gallon']
).add_selection(
selection
)
Trying to compound the selection and the predicate raises:
Javascript Error: Cannot find a selection named "(datum.Miles_per_Gallon > 18)".
The code works with either just the selection or the condition, but not both. The only solution I can think of is layering a scatterplot on top with all the data points below the threshold colored gray. Appreciate any help, thanks!
It looks like the & operator does not work properly between a selection and an expression (tracked by this issue in the Altair repository). You can work around this by using the underlying schema object instead:
color = alt.condition(
alt.LogicalAndPredicate(**{'and': [selection, '(datum.Miles_per_Gallon > 18)']}),
alt.Color('Origin:N'),
alt.value('lightgray')
)
The resulting chart looks like this when the selection is empty:
Trying to color a bar chart using a condition based on a value that is not presented in the chart.
I got this dataframe:
I would like to color the bar green if row.presented_value > row.coloring_value , else color red.
I saw examples of conditions by constant values and by displayed values, but couldn't make it work for me.
In the code example below I would like both foo and bar to be red.
import pandas as pd
df = pd.DataFrame({'name':['bar','foo'],
'presented_value':[10,20],
'coloring_value':[15,25]})
(alt.Chart(df, height=250, width=375).mark_bar()
.encode(x='name', y=alt.Y('presented_value', axis=alt.Axis(orient='right')),
color=alt.condition(alt.datum['presented_value'] > df.loc[df.name==alt.datum.x,
'coloring_value'].values[0],
alt.value('lightgreen'),alt.value('darkred'))
)
)
Changing the first value of coloring_value to <10 both bars will be green even though I would expect only bar to be green.
df = pd.DataFrame({'name':['bar','foo'],
'presented_value':[10,20],
'coloring_value':[5,25]})
(alt.Chart(df, height=250, width=375).mark_bar()
.encode(x='name', y=alt.Y('presented_value', axis=alt.Axis(orient='right')),
color=alt.condition(alt.datum['presented_value'] > df.loc[df.name==alt.datum.x,
'coloring_value'].values[0],
alt.value('lightgreen'),alt.value('darkred'))))
Still not coloring by the correct values. Any idea on how to get it done?
Thanks in advance!
Condition expressions cannot use pandas constructs; they must map to vega expressions. Altair provides the alt.datum and alt.expr object as convenience wrappers for this.
In your case, when you want to compare two values in the row, the best way to do that is to compare them directly:
(alt.Chart(df, height=250, width=375).mark_bar()
.encode(
x='name',
y=alt.Y('presented_value', axis=alt.Axis(orient='right')),
color=alt.condition(
alt.datum.presented_value > alt.datum.coloring_value,
alt.value('lightgreen'),
alt.value('darkred')
)
)
)
I got a problem with changing the height of the Treeview.heading. I have found some answers about the dimensions of Treeview.column, but when I access Treeview.heading in the documentation, there is not a single word about changing the height of the heading dynamically when the text doesn't fit (and wrapping it) or even just hard-coding height of the heading in pixels.
I don't have to split the text to two rows, but when I just keep it that long the whole table (as it has many entries) takes up the whole screen. I want to keep it smaller, therefore I need to split longer entries.
Here is how it looks like:
I can't find any documentation to verify this but it looks like the height of the heading is determined by the heading in the first column.
Reproducing the problem
col_list = ('Name', 'Three\nLine\nHeader', 'Two\nline')
tree = Treeview(parent, columns=col_list[1:])
ix = -1
for col in col_list:
ix += 1
tree.heading(f'#{ix}', text=col)
The fix
col_list = ('Name\n\n', 'Three\nLine\nHeader', 'Two\nline')
or, if you want to make it look prettier
col_list = ('\nName\n', 'Three\nLine\nHeader', 'Two\nline')
The only problem is I haven't figured out how to centre the heading on a two line header
Edit
The newlines work if it is the top level window but not if it is a dialog. Another way of doing this is to set the style. I've got no idea why this works.
style = ttk.Style()
style.configure('Treeview.Heading', foreground='black')
you can use font size to increase the header height for sometimes;
style = ttk.Style()
style.configure('Treeview.Heading', foreground='black', background='white', font=('Arial',25),)
I'm trying to create a chart somewhat along the lines of the Multi-Line Tooltip example, but I'd like to format the string that is being printed to have some text added at the end. I'm trying to modify this part:
# Draw text labels near the points, and highlight based on selection
text = line.mark_text(align='left', dx=5, dy=-5).encode(
text=alt.condition(nearest, 'y:Q', alt.value(' '))
)
Specifically, rather than 'y:Q' I want something along the lines of 'y:Q' + " suffix". I've tried doing something like this:
# Draw text labels near the points, and highlight based on selection
text = line.mark_text(align='left', dx=5, dy=-5).encode(
text=alt.condition(nearest, 'y:Q', alt.value(' '), format=".2f inches")
)
Alternatively, I've tried:
# Draw text labels near the points, and highlight based on selection
y_fld = 'y'
text = line.mark_text(align='left', dx=5, dy=-5).encode(
text=alt.condition(nearest, f"{y_fld:.2f} inches", alt.value(' '))
)
I think I see why those don't work, but I can't figure out how to intercept the value of y and pass it through a format string. Thanks!
I think the easiest way to do this is to calculate a new field using transform_calculate to compute the label that you want.
Using the example from the documentation, I would change the text chart like this:
text = line.mark_text(align='left', dx=5, dy=-5).encode(
text=alt.condition(nearest, 'label:N', alt.value(' '))
).transform_calculate(label='datum.y + " inches"')
That leads to this chart:
If you want more control, you could change the dataset with pandas beforhand. Be sure to set the type to Nominal (and not Quantitative) otherwise you would get NaNs in the tooltips.
So column width is done using cell width on all cells in one column ike this:
from docx import Document
from docx.shared import Cm
file = /path/to/file/
doc = Document(file)
table = doc.add_table(4,2)
for cell in table.columns[0].cells:
cell.width = Cm(1.85)
however, the row height is done using rows, but I can't remember how I did it last week.
Now I managed to find a way to reference the rows in a table, but can't seem to get back to that way. It is possible to change the height by using the add_row method, but you can't create a table with no rows, so the the top row will always be the default height, which about 1.6cms.
There is a way to access paragraphs without using add_paragraph, does anyone know how to access the rows without using the add_row method because it was that that I used to set row height in a table as a default.
I have tried this but it doesn't work:
row = table.rows
row.height = Cm(0.7)
but although this does not give an error, it also has no effect on the height.
table.rows is a collection, in particular a sequence, so you need to access each row separately:
for row in table.rows:
row.height = Cm(0.7)
Also check out row.height_rule for some related behaviors you have access to:
https://python-docx.readthedocs.io/en/latest/api/table.html#row-objects
When you assign to table.rows.height, it just adds a .height instance attribute that does nothing. It's one of the side-effects of a dynamic language like Python that you encounter a mysterious behavior like this. It goes away as you gain more experience though, at least it has for me :)
Some additional information:
The answer here is correct, but this will give a minimum row height. Using WD_ROW_HEIGHT_RULE.EXACTLY will fix the cell height to the set row height ignoring the contents of the cell, this can result in cropping of the text in an undesirable way.
para = table.cell(0,0).add_paragrph('some text')
SOLUTION:
add_paragraph actually adds a blank line above the text.
Use the following instead to avoid using add_paragraph:
table.cell(0,0).paragraphs[0].text = 'some text'
or using add_run can make it easier to also work with the text:
run = table.cell(0,0).paragraphs[0].add_run('some text')
run.bold = True