NVD3:是否可以在图表上显示图标?
NVD3: Is it possible to display icons on the graph?
我正在使用 NVD3 的折线图来概述患者对治疗的反应。
以下是我要实现的目标的示例;在系列的某些条目上添加图标(在这里使用很棒的字体)(见笑脸):
在考虑 hack 之前,我希望有一个干净的解决方案 :-) 还没有找到。像使用 jQuery 来定位具有某些元的值并将图标附加到 dom 的 hack 听起来像一个...丑陋的 hack。
除非有一个干净的官方答案,否则我想可以考虑以下解决方案。
操作后得到的结果
让我开心。希望能帮助到你。为了避免我的特定实现需求的开销,我们只在图表的第一个系列的每个节点上方显示一个 "md-user" 图标以获得以下结果:
备注:
- 它使用 Anular5 的 renderer(2) 组件来操作 DOM。
- 项目本身使用 ionic3(一个 angular 包装器),但这应该不会有太大差异(除了 scss 文件中的 my-component 包装器)
假设有一个组件持有 NVD3 图形元素,例如:
我的-component.html
我的-component.scss
my-component {
nvd3{
position: relative; // do NOT forget this one
.illustrations-wrapper{
position: absolute;
left: 0px; // otherwise breaks on mobile safari
.point-illustration{
position: absolute;
margin-bottom: 22px;
> .vertical-line{
position: absolute;
top: 28px;
border-right: 1px solid #666;
height: 14px;
}
> i {
position: absolute;
pointer-events: none; // without this your mouse hovering the graph wouldnt trigger highlights / popup
&.fa-2x{
left: -10px; // this gets the line centered in case of icons using the font-awesome fa-2x size modifier
}
}
}
}
}
}
my-component.ts: 挂钩 "after graph rendered event"
private initGraphOptions(): void {
this.graphOptions = {
"chart": {
"type": "lineChart",
"callback": (chart) => {
if(!chart) return;
// Listen to click events
chart.lines.dispatch.on('elementClick', (e) => this.entryClicked(e[0].point.x) );
// Add some overlaying icons when rendering completes
chart.dispatch.on('renderEnd', () => this.onRenderComplete() );
// Customize tooltip
chart.interactiveLayer.tooltip.contentGenerator(this.tooltipContentGenerator);
return chart;
},
"height": 450,
"margin": { ... },
"useInteractiveGuideline": true,
"dispatch": {},
"xAxis": { ... }
"yDomain": [-100, 100],
"yAxis": { ... }
}
};
}
my-component.ts: post-渲染逻辑
private onRenderComplete(): void {
this.ensurePresenceOfIllustrationsLayer() && this.renderGraphIcons();
}
private ensurePresenceOfIllustrationsLayer() : boolean {
// We wish to create a child element to our NVD3 element.
// In this case, I can identify my element thanks to a class named "score-graph".
// This is the current component's DOM element
const hostElement = this.elementRef.nativeElement;
const nvd3Element = hostElement.getElementsByClassName('score-graph')[0];
if(!nvd3Element) return false; // in case something went wrong
// Add a wrapper div to that nvd3 element. Ensure it has the "illustrations-wrapper" class to apply propre positionning (see scss)
const wrapper = this.renderer.createElement('div');
this.renderer.addClass(wrapper, 'illustrations-wrapper');
this.renderer.appendChild(nvd3Element, wrapper); // woa angular is just awesome
// Our nvd3 element now has two children: <svg> and <div.illustrations-wrapper>
return true;
}
private renderGraphIcons(): void {
if(!(this.data && this.data[0]) ) return;
// This is the current component's DOM element
const hostElement = this.elementRef.nativeElement;
// The illustration wrapper will hold en entry per point to illustrate
const illustrationWrapper = hostElement.getElementsByClassName('illustrations-wrapper') && hostElement.getElementsByClassName('illustrations-wrapper')[0];
if(!illustrationWrapper) return;
// We are looking for a class named after the series we wish to process illustrate. In this case, we need "nv-series-0"
// However, there's two elements bearing that class:
// - under "nv-groups", where lines are drawn
// - under "nv-scatterWrap", where nodes (dots) are rendered.
// We need the second one. Let's even take its first most relevant child with a unique class under the hostElement: "nv-scatter"
const scatter = hostElement.getElementsByClassName('nv-scatter') && hostElement.getElementsByClassName('nv-scatter')[0];
if(!scatter && !scatter.getElementsByClassName) return; // in case something went wrong
// Now there will only be one possible element when looking for class "nv-series-0"
// NB. you can also use the "classed" property of a series when defining your graph data to assign a custom class.
const targetSeries = scatter.getElementsByClassName('nv-series-0') && scatter.getElementsByClassName('nv-series-0')[0];
if(!targetSeries && !targetSeries.getElementsByClassName) return; // in case something went wrong
// Now it gets nice. We get the array of points (as DOM "path" elements)
const points: any[] = targetSeries.getElementsByClassName('nv-point');
// You think this is a dirty hack so far? Well, keep reading. It gets worse.
const seriesData = this.data[0];
if(points.length !== seriesData.values.length) return; // not likely, but you never know.
for(let i = 0; i < points.length; i++){
const point = points[i];
const pointData = seriesData.values[i];
let translationX = 0;
let translationY = 0;
try{
// How far is this point from the left border?
let translation = point.getAttribute('transform'); // this wll get you a string like "translate(0,180)" and we're looking for both values
translation = translation.replace('translate(', '').replace(')', '').split(','); // Ok I might rot for this ugly regex-lazy approach
// What's the actual possibly scaled-down height of the graph area? Answer: Get the Y translation applied to the x-axis
const nvx = hostElement.getElementsByClassName('nv-x')[0];
const xAxisTranslate = nvx.getAttribute('transform').replace('translate(', '').replace(')', '').split(','); // Same comment
translationX = Number.parseInt(translation[0]);
translationY = Number.parseInt(xAxisTranslate[1]) / 2;
}
catch(err){
}
const translationOffsetX = 0;
const translationOffsetY = 52; // "trial and error" will tell what's best here
// Let's add an illustration entry here
const illustration = this.renderer.createElement('div');
this.renderer.addClass(illustration, 'point-illustration')
this.renderer.setStyle(illustration, 'left', (translationX + translationOffsetX) + 'px');
this.renderer.setStyle(illustration, 'bottom', (translationY + translationOffsetY) + 'px');
this.renderer.appendChild(illustrationWrapper, illustration);
// Optional: add a a visual connexion between the actual chart line and the icon (define this first so that the icon overlaps)
const verticalLine = this.renderer.createElement('div');
this.renderer.addClass(verticalLine, 'vertical-line');
this.renderer.appendChild(illustration, verticalLine);
// Add the icon
const icon = this.renderer.createElement('i');
this.renderer.addClass(icon, 'fa');
this.renderer.addClass(icon, 'fa-user-md');
this.renderer.addClass(icon, 'fa-2x');
this.renderer.appendChild(illustration, icon);
}
}
我正在使用 NVD3 的折线图来概述患者对治疗的反应。 以下是我要实现的目标的示例;在系列的某些条目上添加图标(在这里使用很棒的字体)(见笑脸):
在考虑 hack 之前,我希望有一个干净的解决方案 :-) 还没有找到。像使用 jQuery 来定位具有某些元的值并将图标附加到 dom 的 hack 听起来像一个...丑陋的 hack。
除非有一个干净的官方答案,否则我想可以考虑以下解决方案。
操作后得到的结果
让我开心。希望能帮助到你。为了避免我的特定实现需求的开销,我们只在图表的第一个系列的每个节点上方显示一个 "md-user" 图标以获得以下结果:
备注:
- 它使用 Anular5 的 renderer(2) 组件来操作 DOM。
- 项目本身使用 ionic3(一个 angular 包装器),但这应该不会有太大差异(除了 scss 文件中的 my-component 包装器)
假设有一个组件持有 NVD3 图形元素,例如:
我的-component.html
我的-component.scss
my-component {
nvd3{
position: relative; // do NOT forget this one
.illustrations-wrapper{
position: absolute;
left: 0px; // otherwise breaks on mobile safari
.point-illustration{
position: absolute;
margin-bottom: 22px;
> .vertical-line{
position: absolute;
top: 28px;
border-right: 1px solid #666;
height: 14px;
}
> i {
position: absolute;
pointer-events: none; // without this your mouse hovering the graph wouldnt trigger highlights / popup
&.fa-2x{
left: -10px; // this gets the line centered in case of icons using the font-awesome fa-2x size modifier
}
}
}
}
}
}
my-component.ts: 挂钩 "after graph rendered event"
private initGraphOptions(): void {
this.graphOptions = {
"chart": {
"type": "lineChart",
"callback": (chart) => {
if(!chart) return;
// Listen to click events
chart.lines.dispatch.on('elementClick', (e) => this.entryClicked(e[0].point.x) );
// Add some overlaying icons when rendering completes
chart.dispatch.on('renderEnd', () => this.onRenderComplete() );
// Customize tooltip
chart.interactiveLayer.tooltip.contentGenerator(this.tooltipContentGenerator);
return chart;
},
"height": 450,
"margin": { ... },
"useInteractiveGuideline": true,
"dispatch": {},
"xAxis": { ... }
"yDomain": [-100, 100],
"yAxis": { ... }
}
};
}
my-component.ts: post-渲染逻辑
private onRenderComplete(): void {
this.ensurePresenceOfIllustrationsLayer() && this.renderGraphIcons();
}
private ensurePresenceOfIllustrationsLayer() : boolean {
// We wish to create a child element to our NVD3 element.
// In this case, I can identify my element thanks to a class named "score-graph".
// This is the current component's DOM element
const hostElement = this.elementRef.nativeElement;
const nvd3Element = hostElement.getElementsByClassName('score-graph')[0];
if(!nvd3Element) return false; // in case something went wrong
// Add a wrapper div to that nvd3 element. Ensure it has the "illustrations-wrapper" class to apply propre positionning (see scss)
const wrapper = this.renderer.createElement('div');
this.renderer.addClass(wrapper, 'illustrations-wrapper');
this.renderer.appendChild(nvd3Element, wrapper); // woa angular is just awesome
// Our nvd3 element now has two children: <svg> and <div.illustrations-wrapper>
return true;
}
private renderGraphIcons(): void {
if(!(this.data && this.data[0]) ) return;
// This is the current component's DOM element
const hostElement = this.elementRef.nativeElement;
// The illustration wrapper will hold en entry per point to illustrate
const illustrationWrapper = hostElement.getElementsByClassName('illustrations-wrapper') && hostElement.getElementsByClassName('illustrations-wrapper')[0];
if(!illustrationWrapper) return;
// We are looking for a class named after the series we wish to process illustrate. In this case, we need "nv-series-0"
// However, there's two elements bearing that class:
// - under "nv-groups", where lines are drawn
// - under "nv-scatterWrap", where nodes (dots) are rendered.
// We need the second one. Let's even take its first most relevant child with a unique class under the hostElement: "nv-scatter"
const scatter = hostElement.getElementsByClassName('nv-scatter') && hostElement.getElementsByClassName('nv-scatter')[0];
if(!scatter && !scatter.getElementsByClassName) return; // in case something went wrong
// Now there will only be one possible element when looking for class "nv-series-0"
// NB. you can also use the "classed" property of a series when defining your graph data to assign a custom class.
const targetSeries = scatter.getElementsByClassName('nv-series-0') && scatter.getElementsByClassName('nv-series-0')[0];
if(!targetSeries && !targetSeries.getElementsByClassName) return; // in case something went wrong
// Now it gets nice. We get the array of points (as DOM "path" elements)
const points: any[] = targetSeries.getElementsByClassName('nv-point');
// You think this is a dirty hack so far? Well, keep reading. It gets worse.
const seriesData = this.data[0];
if(points.length !== seriesData.values.length) return; // not likely, but you never know.
for(let i = 0; i < points.length; i++){
const point = points[i];
const pointData = seriesData.values[i];
let translationX = 0;
let translationY = 0;
try{
// How far is this point from the left border?
let translation = point.getAttribute('transform'); // this wll get you a string like "translate(0,180)" and we're looking for both values
translation = translation.replace('translate(', '').replace(')', '').split(','); // Ok I might rot for this ugly regex-lazy approach
// What's the actual possibly scaled-down height of the graph area? Answer: Get the Y translation applied to the x-axis
const nvx = hostElement.getElementsByClassName('nv-x')[0];
const xAxisTranslate = nvx.getAttribute('transform').replace('translate(', '').replace(')', '').split(','); // Same comment
translationX = Number.parseInt(translation[0]);
translationY = Number.parseInt(xAxisTranslate[1]) / 2;
}
catch(err){
}
const translationOffsetX = 0;
const translationOffsetY = 52; // "trial and error" will tell what's best here
// Let's add an illustration entry here
const illustration = this.renderer.createElement('div');
this.renderer.addClass(illustration, 'point-illustration')
this.renderer.setStyle(illustration, 'left', (translationX + translationOffsetX) + 'px');
this.renderer.setStyle(illustration, 'bottom', (translationY + translationOffsetY) + 'px');
this.renderer.appendChild(illustrationWrapper, illustration);
// Optional: add a a visual connexion between the actual chart line and the icon (define this first so that the icon overlaps)
const verticalLine = this.renderer.createElement('div');
this.renderer.addClass(verticalLine, 'vertical-line');
this.renderer.appendChild(illustration, verticalLine);
// Add the icon
const icon = this.renderer.createElement('i');
this.renderer.addClass(icon, 'fa');
this.renderer.addClass(icon, 'fa-user-md');
this.renderer.addClass(icon, 'fa-2x');
this.renderer.appendChild(illustration, icon);
}
}