Skip to content

Commit fde4e8e

Browse files
committed
Support conditional formatting based on other columns/cells
1 parent 5d85a14 commit fde4e8e

File tree

5 files changed

+303
-5
lines changed

5 files changed

+303
-5
lines changed
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Conditional formatting based on other column values"
8+
]
9+
},
10+
{
11+
"cell_type": "code",
12+
"execution_count": null,
13+
"metadata": {
14+
"scrolled": false
15+
},
16+
"outputs": [],
17+
"source": [
18+
"# Imports\n",
19+
"import json\n",
20+
"import numpy as np\n",
21+
"import pandas as pd\n",
22+
"from bqplot import DateScale, ColorScale\n",
23+
"from py2vega.functions.type_coercing import toDate\n",
24+
"from py2vega.functions.date_time import datetime\n",
25+
"from ipydatagrid import Expr, DataGrid, TextRenderer\n",
26+
"\n",
27+
"# Random data\n",
28+
"n = 10\n",
29+
"np.random.seed(0)\n",
30+
"df = pd.DataFrame({\n",
31+
" 'Column 0': np.random.randn(n),\n",
32+
" 'Column 1': np.random.randn(n),\n",
33+
" 'Column 2': np.random.randn(n),\n",
34+
"})\n",
35+
"\n",
36+
"\n",
37+
"# Formatting the values in column 0 based on the value the cell in row 1, column 1 has\n",
38+
"def format_based_on_other_cell_and_column_value(cell):\n",
39+
" return 'magenta' if cell.column == 0 and cell.metadata.data[1][1] > 1.45 else 'red'\n",
40+
"\n",
41+
"# Formatting the values in column 1 based on the value of the silbing row in column 2\n",
42+
"def format_based_on_other_column(cell):\n",
43+
" return 'green' if cell.column == 1 and cell.metadata.data[2] > 0.0 else 'yellow'\n",
44+
"\n",
45+
"column0_formatting = TextRenderer(\n",
46+
" text_color='black',\n",
47+
" background_color=Expr(format_based_on_other_cell_and_column_value),\n",
48+
")\n",
49+
"\n",
50+
"\n",
51+
"column1_formatting = TextRenderer(\n",
52+
" text_color='black',\n",
53+
" background_color=Expr(format_based_on_other_column),\n",
54+
")\n",
55+
"\n",
56+
"\n",
57+
"renderers = {\n",
58+
" 'Column 0': column0_formatting,\n",
59+
" 'Column 1': column1_formatting,\n",
60+
"}\n",
61+
"\n",
62+
"grid = DataGrid(df, base_row_size=30, base_column_size=300, renderers=renderers, \n",
63+
" layout={'height':'350px'})\n",
64+
"grid"
65+
]
66+
},
67+
{
68+
"cell_type": "markdown",
69+
"metadata": {},
70+
"source": [
71+
"## Example with nested columns"
72+
]
73+
},
74+
{
75+
"cell_type": "code",
76+
"execution_count": null,
77+
"metadata": {},
78+
"outputs": [],
79+
"source": [
80+
"import ipydatagrid as ipg\n",
81+
"import pandas as pd\n",
82+
"import numpy as np\n",
83+
"\n",
84+
"top_level = ['Value Factors', 'Value Factors', 'Momentum Factors', 'Momentum Factors']\n",
85+
"bottom_level = ['Factor A', 'Factor B', 'Factor C', 'Factor D']\n",
86+
"\n",
87+
"nested_df = pd.DataFrame(np.random.randn(4,4).round(2),\n",
88+
" columns=pd.MultiIndex.from_arrays([top_level, bottom_level]),\n",
89+
" index=pd.Index(['Security {}'.format(x) for x in ['A', 'B', 'C', 'D']], name='Ticker'))\n",
90+
"\n",
91+
"# Formatting Factor B rows based on their siblings in the Factor C column\n",
92+
"def format_based_on_other_column(cell):\n",
93+
" return 'green' if cell.column == 1 and cell.metadata.data[2] > 0.0 else 'yellow'\n",
94+
"\n",
95+
"nested_grid = ipg.DataGrid(nested_df,\n",
96+
" base_column_size=90,\n",
97+
" layout={'height':'140px'},\n",
98+
" default_renderer=ipg.TextRenderer(\n",
99+
" horizontal_alignment='right', \n",
100+
" background_color=Expr(format_based_on_other_column)))\n",
101+
"\n",
102+
"nested_grid"
103+
]
104+
},
105+
{
106+
"cell_type": "code",
107+
"execution_count": null,
108+
"metadata": {},
109+
"outputs": [],
110+
"source": [
111+
"def format_based_on_other_column(cell):\n",
112+
" return 'green' if cell.column == 0 and cell.metadata.data[1] == \"Buy\" else 'red'\n",
113+
"\n",
114+
"column0_formatting = TextRenderer(\n",
115+
" text_color='black',\n",
116+
" background_color=Expr(format_based_on_other_column),\n",
117+
")\n",
118+
"\n",
119+
"renderers = {\n",
120+
" 'Stock': column0_formatting,\n",
121+
"}\n",
122+
"\n",
123+
"grid = DataGrid(pd.DataFrame({\"Stock\":'A B C D'.split(), \"Signal\":['Buy', 'Sell', 'Buy', 'Sell']}), \n",
124+
" base_row_size=30, base_column_size=300, renderers=renderers, layout={'height':'150px'})\n",
125+
"grid"
126+
]
127+
},
128+
{
129+
"cell_type": "code",
130+
"execution_count": null,
131+
"metadata": {},
132+
"outputs": [],
133+
"source": [
134+
"def format_based_on_other_column(cell):\n",
135+
" return 'green' if cell.column == 0 and cell.metadata.data[2][1] == \"Really Important Cell\" else 'red'\n",
136+
"\n",
137+
"def format_really_important_cell(cell):\n",
138+
" return 'limegreen' if cell.row == 2 else 'white'\n",
139+
"\n",
140+
"column0_formatting = TextRenderer(\n",
141+
" text_color='black',\n",
142+
" background_color=Expr(format_based_on_other_column)\n",
143+
")\n",
144+
"\n",
145+
"column1_formatting = TextRenderer(\n",
146+
" text_color='black',\n",
147+
" background_color=Expr(format_really_important_cell)\n",
148+
")\n",
149+
"\n",
150+
"renderers = {\n",
151+
" 'Stock': column0_formatting,\n",
152+
" 'Sentiment': column1_formatting\n",
153+
"}\n",
154+
"\n",
155+
"grid = DataGrid(pd.DataFrame({\"Stock\":'A B C D'.split(), \n",
156+
" \"Sentiment\":['Not Important','Not Important', 'Really Important Cell', 'Not Important']}), \n",
157+
" base_row_size=30, base_column_size=300, renderers=renderers, layout={'height':'150px'})\n",
158+
"grid"
159+
]
160+
},
161+
{
162+
"cell_type": "markdown",
163+
"metadata": {},
164+
"source": [
165+
"## Comparing dates"
166+
]
167+
},
168+
{
169+
"cell_type": "code",
170+
"execution_count": null,
171+
"metadata": {},
172+
"outputs": [],
173+
"source": [
174+
"import json\n",
175+
"\n",
176+
"import numpy as np\n",
177+
"import pandas as pd\n",
178+
"from bqplot import DateScale, ColorScale\n",
179+
"from py2vega.functions.type_coercing import toDate\n",
180+
"from py2vega.functions.date_time import datetime\n",
181+
"\n",
182+
"from ipydatagrid import Expr, DataGrid, TextRenderer, BarRenderer\n",
183+
"\n",
184+
"n = 10\n",
185+
"np.random.seed(0)\n",
186+
"\n",
187+
"def format_based_on_date(cell):\n",
188+
" return 'magenta' if cell.column == 0 and cell.metadata.data[2] >= '2020-10-21' else 'yellow'\n",
189+
"\n",
190+
"df = pd.DataFrame({\n",
191+
" 'Value 1': np.random.randn(n),\n",
192+
" 'Value 2': np.random.randn(n),\n",
193+
" 'Dates': pd.date_range(end=pd.Timestamp('2020-10-25'), periods=n)\n",
194+
"})\n",
195+
"\n",
196+
"text_renderer = TextRenderer(\n",
197+
" text_color='black',\n",
198+
" background_color=Expr(format_based_on_date)\n",
199+
")\n",
200+
"\n",
201+
"def bar_color(cell):\n",
202+
" date = toDate(cell.value)\n",
203+
" return 'green' if date > datetime('2000') else 'red'\n",
204+
"\n",
205+
"\n",
206+
"renderers = {\n",
207+
" 'Value 1': text_renderer,\n",
208+
" 'Dates': BarRenderer(\n",
209+
" bar_value=DateScale(min=df['Dates'][0], max=df['Dates'][n-1]),\n",
210+
" bar_color=Expr(bar_color),\n",
211+
" format='%Y/%m/%d',\n",
212+
" format_type='time'\n",
213+
" ),\n",
214+
"}\n",
215+
"\n",
216+
"grid = DataGrid(df, base_row_size=30, base_column_size=300, renderers=renderers, layout={'height':'350px'})\n",
217+
"grid"
218+
]
219+
}
220+
],
221+
"metadata": {
222+
"kernelspec": {
223+
"display_name": "Python 3",
224+
"language": "python",
225+
"name": "python3"
226+
},
227+
"language_info": {
228+
"codemirror_mode": {
229+
"name": "ipython",
230+
"version": 3
231+
},
232+
"file_extension": ".py",
233+
"mimetype": "text/x-python",
234+
"name": "python",
235+
"nbconvert_exporter": "python",
236+
"pygments_lexer": "ipython3",
237+
"version": "3.8.5"
238+
}
239+
},
240+
"nbformat": 4,
241+
"nbformat_minor": 4
242+
}

ipydatagrid/cellrenderer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class Expr(VegaExpr):
4040

4141
@validate('value')
4242
def _validate_value(self, proposal):
43-
return py2vega(proposal['value'], [Variable('cell', ['value', 'row', 'column']), 'default_value'])
43+
return py2vega(proposal['value'], [Variable('cell', ['value', 'row', 'column', 'metadata']), 'default_value'])
4444

4545

4646
class CellRenderer(Widget):

src/core/view.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,9 @@ export class View {
7575
column: number,
7676
): DataModel.Metadata {
7777
if (region === 'body' || region === 'column-header') {
78-
return this._bodyFields[column];
78+
return { row: row, column: column, ...this._bodyFields[column] };
7979
}
80-
return this._headerFields[column];
80+
return { row: row, column: column, ...this._headerFields[column] };
8181
}
8282

8383
/**

src/core/viewbasedjsonmodel.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,17 @@ export class ViewBasedJSONModel extends MutableDataModel {
157157
row: number,
158158
column: number,
159159
): DataModel.Metadata {
160-
return this.currentView.metadata(region, row, column);
160+
const md = this.currentView.metadata(region, row, column);
161+
if (region == 'body') {
162+
md.row = row;
163+
md.column = column;
164+
md.formattingInfo = 4;
165+
md.model = this;
166+
md.data = (row: number, column: number) => {
167+
return this.data('body', row, column);
168+
};
169+
}
170+
return md;
161171
}
162172

163173
/**

src/vegaexpr.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,46 @@ export class VegaExprModel extends WidgetModel {
4646
return this._function(config, defaultValue, vegaFunctions.functionContext);
4747
}
4848

49+
/**
50+
* Augments transpiled JS code output from vega with
51+
* datamodel calls for condition validation.
52+
* @param parsedValue JS code (string) generated from a vega expression
53+
*/
54+
private _augmentExpression(parsedValue: ParsedVegaExpr): ParsedVegaExpr {
55+
const codeToProcess = parsedValue.code;
56+
if (codeToProcess.includes('cell.metadata.data')) {
57+
const localRegex = /\[(.*?)\]/; // For "abc[1]" returns ["[1]", "1"];
58+
const indices = parsedValue.code.match(/\[(.*?)\]/g)!;
59+
const oldSuffix = indices.join('');
60+
const stringToReplace = `cell.metadata.data${oldSuffix}`;
61+
let row, column;
62+
let newReplacement;
63+
64+
// Row and column passed - indexing based on given row and given column
65+
if (indices.length === 2) {
66+
[row, column] = indices;
67+
newReplacement = `cell.metadata.data(${row.match(localRegex)![1]}, ${
68+
column.match(localRegex)![1]
69+
})`;
70+
} else {
71+
// Only column passed - indexing based on given column and sibling row
72+
column = indices[0];
73+
newReplacement = `cell.metadata.data(cell.row, ${
74+
column.match(localRegex)![1]
75+
})`;
76+
}
77+
78+
parsedValue.code = codeToProcess.replace(stringToReplace, newReplacement);
79+
}
80+
81+
return parsedValue;
82+
}
83+
4984
private updateFunction() {
50-
const parsedValue = this._codegen(vegaExpressions.parse(this.get('value')));
85+
let parsedValue: ParsedVegaExpr = this._codegen(
86+
vegaExpressions.parse(this.get('value')),
87+
);
88+
parsedValue = this._augmentExpression(parsedValue);
5189

5290
this._function = Function(
5391
'cell',
@@ -75,3 +113,11 @@ export class VegaExprView extends WidgetView {
75113

76114
model: VegaExprModel;
77115
}
116+
117+
export interface ParsedVegaExpr {
118+
/**
119+
* A JavaScript soring literal describing
120+
* the converted vega expression
121+
*/
122+
code: string;
123+
}

0 commit comments

Comments
 (0)