Skip to content

Commit 9695dd5

Browse files
Add labels_visibility support to FlexLine and fix legend rendering
- Add `labels_visibility` trait to FlexLine Python model and render inline curve labels at the end of each line in the JS view - Fix FlexLine.draw_legend to use the correct D3 enter selection and get_mark_color instead of get_colors - Fix TypeScript type errors in Lines.ts and FlexLine.ts with explicit casts Cleanup duplicate listeners for FlexLine Address review feedback: remove as-any churn and drop lock/notebook files docs: move Flexline notebook example into docs
1 parent 3459e0a commit 9695dd5

3 files changed

Lines changed: 152 additions & 41 deletions

File tree

bqplot/marks.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,8 @@ class FlexLine(Mark):
495495
stroke_width = Float(1.5).tag(sync=True, display_name='Stroke width')
496496
colors = List(trait=Color(default_value=None, allow_none=True),
497497
default_value=CATEGORY10).tag(sync=True)
498+
labels_visibility = Enum(['none', 'label'], default_value='none')\
499+
.tag(sync=True, display_name='Labels visibility')
498500
_view_name = Unicode('FlexLine').tag(sync=True)
499501
_model_name = Unicode('FlexLineModel').tag(sync=True)
500502

examples/Marks/Object Model/FlexLine.ipynb

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"cells": [
33
{
44
"cell_type": "code",
5-
"execution_count": null,
5+
"execution_count": 1,
66
"metadata": {
77
"scrolled": true
88
},
@@ -21,7 +21,7 @@
2121
},
2222
{
2323
"cell_type": "code",
24-
"execution_count": null,
24+
"execution_count": 3,
2525
"metadata": {
2626
"scrolled": true
2727
},
@@ -97,6 +97,87 @@
9797
"\n",
9898
"Figure(marks=[fl2], axes=[ax_x, ax_y])"
9999
]
100+
},
101+
{
102+
"cell_type": "code",
103+
"execution_count": 7,
104+
"metadata": {},
105+
"outputs": [
106+
{
107+
"data": {
108+
"application/vnd.jupyter.widget-view+json": {
109+
"model_id": "2a6a51dd45984cf1a192e9b145c08c2f",
110+
"version_major": 2,
111+
"version_minor": 0
112+
},
113+
"text/plain": [
114+
"VBox(children=(ToggleButtons(description='labels_visibility:', index=1, options=('none', 'label'), value='labe…"
115+
]
116+
},
117+
"execution_count": 7,
118+
"metadata": {},
119+
"output_type": "execute_result"
120+
}
121+
],
122+
"source": [
123+
"from ipywidgets import ToggleButtons, VBox\n",
124+
"from bqplot import DateScale, LinearScale, Axis, Figure, FlexLine\n",
125+
"\n",
126+
"# Reuse existing dates/spx from the notebook's earlier cell\n",
127+
"x_sc = DateScale()\n",
128+
"y_sc = LinearScale()\n",
129+
"\n",
130+
"ax_x = Axis(scale=x_sc, label=\"Date\")\n",
131+
"ax_y = Axis(scale=y_sc, orientation=\"vertical\", label=\"Index\")\n",
132+
"\n",
133+
"fl_label_test = FlexLine(\n",
134+
" x=dates,\n",
135+
" y=spx,\n",
136+
" scales={\"x\": x_sc, \"y\": y_sc},\n",
137+
" labels=[\"SPX\"],\n",
138+
" labels_visibility=\"label\",\n",
139+
" stroke_width=2.5,\n",
140+
")\n",
141+
"\n",
142+
"fig = Figure(\n",
143+
" marks=[fl_label_test],\n",
144+
" axes=[ax_x, ax_y],\n",
145+
" title=\"FlexLine labels_visibility test\",\n",
146+
" fig_margin={\"top\": 50, \"left\": 70, \"right\": 20, \"bottom\": 60},\n",
147+
")\n",
148+
"\n",
149+
"toggle = ToggleButtons(\n",
150+
" options=[\"none\", \"label\"],\n",
151+
" value=\"label\",\n",
152+
" description=\"labels_visibility:\",\n",
153+
")\n",
154+
"\n",
155+
"def _on_toggle(change):\n",
156+
" if change[\"name\"] == \"value\":\n",
157+
" fl_label_test.labels_visibility = change[\"new\"]\n",
158+
"\n",
159+
"toggle.observe(_on_toggle, names=\"value\")\n",
160+
"\n",
161+
"VBox([toggle, fig])"
162+
]
163+
},
164+
{
165+
"cell_type": "code",
166+
"execution_count": 5,
167+
"metadata": {},
168+
"outputs": [
169+
{
170+
"name": "stdout",
171+
"output_type": "stream",
172+
"text": [
173+
"/Library/Frameworks/Python.framework/Versions/3.14/bin/python3\n"
174+
]
175+
}
176+
],
177+
"source": [
178+
"import sys\n",
179+
"print(sys.executable)"
180+
]
100181
}
101182
],
102183
"metadata": {
@@ -115,7 +196,7 @@
115196
"name": "python",
116197
"nbconvert_exporter": "python",
117198
"pygments_lexer": "ipython3",
118-
"version": "3.7.6"
199+
"version": "3.14.0"
119200
}
120201
},
121202
"nbformat": 4,

js/src/FlexLine.ts

Lines changed: 66 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -38,50 +38,41 @@ export class FlexLine extends Lines {
3838

3939
create_listeners() {
4040
super.create_listeners();
41-
this.listenTo(
42-
this.model,
43-
'change:labels_visibility',
44-
this.update_legend_labels
45-
);
4641
}
4742

4843
draw_legend(elem, x_disp, y_disp, inter_x_disp, inter_y_disp) {
4944
const g_elements = elem
5045
.selectAll('.legend' + this.uuid)
51-
.data(this.model.mark_data, (d, i) => {
52-
return d.name;
53-
});
46+
.data(this.model.mark_data, (d: any) => d.name);
5447

5548
const that = this;
5649
const rect_dim = inter_y_disp * 0.8;
57-
g_elements
50+
51+
const g_enter = g_elements
5852
.enter()
5953
.append('g')
6054
.attr('class', 'legend' + this.uuid)
6155
.attr('transform', (d, i) => {
6256
return 'translate(0, ' + (i * inter_y_disp + y_disp) + ')';
63-
})
57+
});
58+
59+
g_enter
6460
.append('line')
65-
.style('stroke', (d, i) => {
66-
return that.get_colors(i);
67-
})
61+
.style('stroke', (d, i) => that.get_mark_color(d, i))
6862
.attr('x1', 0)
6963
.attr('x2', rect_dim)
7064
.attr('y1', rect_dim / 2)
7165
.attr('y2', rect_dim / 2);
7266

73-
g_elements
67+
g_enter
7468
.append('text')
7569
.attr('class', 'legendtext')
7670
.attr('x', rect_dim * 1.2)
7771
.attr('y', rect_dim / 2)
7872
.attr('dy', '0.35em')
79-
.text((d, i) => {
80-
return that.model.get('labels')[i];
81-
})
82-
.style('fill', (d, i) => {
83-
return that.get_colors(i);
84-
});
73+
.text((d, i) => that.model.get('labels')[i])
74+
.style('fill', (d, i) => that.get_mark_color(d, i));
75+
8576
const max_length = d3.max(this.model.get('labels'), (d: any[]) => {
8677
return d.length;
8778
});
@@ -117,23 +108,39 @@ export class FlexLine extends Lines {
117108

118109
draw() {
119110
this.set_ranges();
111+
const xScale = this.scales.x;
112+
const yScale = this.scales.y;
113+
120114
let curves_sel: d3.Selection<any, any, any, any> = this.d3el
121115
.selectAll('.curve')
122-
.data(this.model.mark_data, (d: any, i) => {
123-
return d.name;
124-
});
116+
.data(this.model.mark_data, (d: any, i) => d.name);
125117

126118
curves_sel
127119
.exit()
128120
.transition('draw')
129121
.duration(this.parent.model.get('animation_duration'))
130122
.remove();
131123

132-
curves_sel = curves_sel
133-
.enter()
134-
.append('g')
135-
.attr('class', 'curve')
136-
.merge(curves_sel);
124+
const newCurves = curves_sel.enter().append('g').attr('class', 'curve');
125+
newCurves
126+
.append('text')
127+
.attr('class', 'curve_label')
128+
.attr('text-anchor', 'end')
129+
.attr('dominant-baseline', 'middle');
130+
131+
curves_sel = newCurves.merge(curves_sel);
132+
133+
curves_sel.select('.curve_label')
134+
.attr('display', this.model.get('labels_visibility') === 'label' ? 'inline' : 'none')
135+
.text((d: any, i: number) => this.model.get('labels')[i] || d.name)
136+
.attr('x', (d: any) => {
137+
const last = d.values[d.values.length - 1];
138+
return last ? xScale.scale(last.x2) - 5 : 0;
139+
})
140+
.attr('y', (d: any) => {
141+
const last = d.values[d.values.length - 1];
142+
return last ? yScale.scale(last.y2) : 0;
143+
});
137144

138145
const x_scale = this.scales.x,
139146
y_scale = this.scales.y;
@@ -185,17 +192,38 @@ export class FlexLine extends Lines {
185192
.selectAll('.line-elem')
186193
.transition('relayout')
187194
.duration(this.parent.model.get('animation_duration'))
188-
.attr('x1', (d: any) => {
189-
return x_scale.scale(d.x1);
190-
})
191-
.attr('x2', (d: any) => {
192-
return x_scale.scale(d.x2);
193-
})
194-
.attr('y1', (d: any) => {
195-
return y_scale.scale(d.y1);
195+
.attr('x1', (d: any) => x_scale.scale(d.x1))
196+
.attr('x2', (d: any) => x_scale.scale(d.x2))
197+
.attr('y1', (d: any) => y_scale.scale(d.y1))
198+
.attr('y2', (d: any) => y_scale.scale(d.y2));
199+
200+
this.d3el
201+
.selectAll('.curve')
202+
.select('.curve_label')
203+
.attr('x', (d: any) => {
204+
const last = d.values[d.values.length - 1];
205+
return last ? x_scale.scale(last.x2) - 5 : 0;
196206
})
197-
.attr('y2', (d: any) => {
198-
return y_scale.scale(d.y2);
207+
.attr('y', (d: any) => {
208+
const last = d.values[d.values.length - 1];
209+
return last ? y_scale.scale(last.y2) : 0;
199210
});
200211
}
212+
213+
update_labels() {
214+
const labels = this.model.get('labels');
215+
this.d3el
216+
.selectAll('.curve')
217+
.select('.curve_label')
218+
.text((d: any, i: number) => labels[i] || d.name);
219+
}
220+
221+
update_legend_labels() {
222+
const labels_visibility = this.model.get('labels_visibility');
223+
if (labels_visibility === 'none') {
224+
this.d3el.selectAll('.curve_label').attr('display', 'none');
225+
} else if (labels_visibility === 'label') {
226+
this.d3el.selectAll('.curve_label').attr('display', 'inline');
227+
}
228+
}
201229
}

0 commit comments

Comments
 (0)