Skip to content

Commit f89804b

Browse files
authored
Support conditional formatting based on other columns/cells (#162)
* Support conditional formatting based on other columns/cells * improve regex logic to allow compound targets / remove row indexing
2 parents 5d85a14 + c61ca30 commit f89804b

File tree

5 files changed

+342
-5
lines changed

5 files changed

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

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: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,16 @@ 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.data = (row: number, column: number) => {
165+
const columnIndex = this.columnNameToIndex(column.toString());
166+
return this.data('body', row, columnIndex);
167+
};
168+
}
169+
return md;
161170
}
162171

163172
/**

src/vegaexpr.ts

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

49+
_processRegex(match: string): string {
50+
const parsedMatch = match.match(/\[(.*?)\]/g)!;
51+
const column = parsedMatch[0];
52+
53+
// Column inxexing for regular element.
54+
if (parsedMatch.length === 1) {
55+
return `(cell.row, ${column.match(/\[(.*?)\]/)![1]})`;
56+
}
57+
58+
const rest = parsedMatch.splice(1);
59+
60+
// Column indexing for a compound element.
61+
return `(cell.row, ${column.match(/\[(.*?)\]/)![1]})${rest.join('')}`;
62+
}
63+
64+
/**
65+
* Augments transpiled JS code output from vega with
66+
* datamodel calls for condition validation.
67+
* @param parsedValue JS code (string) generated from a vega expression
68+
*/
69+
private _augmentExpression(parsedValue: ParsedVegaExpr): ParsedVegaExpr {
70+
let codeToProcess = parsedValue.code;
71+
codeToProcess = codeToProcess.replace(
72+
/(?<=cell.metadata.data)(\[(.*?)\])+(?=[==,>=,<=,!=,<,>])/g,
73+
this._processRegex,
74+
);
75+
parsedValue.code = codeToProcess;
76+
77+
return parsedValue;
78+
}
79+
4980
private updateFunction() {
50-
const parsedValue = this._codegen(vegaExpressions.parse(this.get('value')));
81+
let parsedValue: ParsedVegaExpr = this._codegen(
82+
vegaExpressions.parse(this.get('value')),
83+
);
84+
85+
parsedValue = this._augmentExpression(parsedValue);
5186

5287
this._function = Function(
5388
'cell',
@@ -75,3 +110,11 @@ export class VegaExprView extends WidgetView {
75110

76111
model: VegaExprModel;
77112
}
113+
114+
export interface ParsedVegaExpr {
115+
/**
116+
* A JavaScript soring literal describing
117+
* the converted vega expression
118+
*/
119+
code: string;
120+
}

0 commit comments

Comments
 (0)