Skip to content

Commit 738203c

Browse files
committed
add categorical & range type legends
1 parent 8c83055 commit 738203c

File tree

8 files changed

+277
-25
lines changed

8 files changed

+277
-25
lines changed

src/EsnetMatrix.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,31 @@ import { useTheme2, CustomScrollbar } from '@grafana/ui';
66
// import { useTheme2 } from '@grafana/ui';
77

88
import * as Matrix from './matrix.js';
9+
// import * as Legend from 'matrixLegend.js';
910

1011
interface Props extends PanelProps<MatrixOptions> {}
1112

1213
export const EsnetMatrix: React.FC<Props> = ({ options, data, width, height, id }) => {
13-
let parsedData: any[] = [];
1414
const theme = useTheme2();
15+
const parsedData = parseData(data, options, theme);
1516
try {
16-
const tryData = parseData(data, options, theme);
17-
if (tryData === "too many inputs") {
17+
if (parsedData.data === 'too many inputs') {
1818
console.error('Too many data points.');
1919
return <svg width={width} height={height}></svg>;
20-
} else {
21-
parsedData = tryData;
2220
}
2321
} catch (error) {
2422
console.error('data error: ', error);
2523
}
26-
const rowNames = parsedData[0];
27-
const colNames = parsedData[1];
28-
const matrix = parsedData[2];
2924

30-
if (matrix == null) {
25+
if (parsedData.data == null) {
3126
return <div>No Data</div>;
3227
}
3328
// const divStyle = {
3429
// width: 'auto',
3530
// };
3631

37-
const ref = Matrix.matrix(rowNames, colNames, matrix, id, height, options);
32+
const ref = Matrix.matrix(parsedData.rows, parsedData.columns, parsedData.data, id, height, options, parsedData.legend);
33+
3834
return (
3935
<CustomScrollbar autoHeightMin="100%">
4036
<div ref={ref}></div>

src/dataParser.ts

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { DataFrameView } from '@grafana/data';
22

3+
// import { legend } from 'matrixLegend';
4+
35
/**
46
* this function creates an adjacency matrix to be consumed by the chord
57
* function returns the matrix + forward and reverse lookup Maps to go from
@@ -18,14 +20,14 @@ export function parseData(data: { series: any[] }, options: any, theme: any) {
1820
if (series === null || series === undefined) {
1921
// no data, bail
2022
console.log('no data');
21-
return [null, null, null];
23+
return { rows: null, columns: null, data: null, legend: null };
2224
}
2325

2426
const frame = new DataFrameView(series);
2527
if (frame === null || frame === undefined) {
2628
// no data, bail
2729
console.log('no data');
28-
return [null, null, null];
30+
return { rows: null, columns: null, data: null, legend: null };
2931
}
3032
// set fields
3133
let sourceKey = options.sourceField;
@@ -62,6 +64,8 @@ export function parseData(data: { series: any[] }, options: any, theme: any) {
6264
// Make Row and Column Lists
6365
let rows: any[] = [];
6466
let columns: any[] = [];
67+
// let uniqueVals: any[] = [];
68+
6569
// IF static list toggle is set, use input list
6670
if (options.inputList) {
6771
rows = options.staticRows.split(',');
@@ -80,14 +84,32 @@ export function parseData(data: { series: any[] }, options: any, theme: any) {
8084

8185
const numSquaresInMatrix = rowNames.length * colNames.length;
8286
if (numSquaresInMatrix > 50000) {
83-
return 'too many inputs';
87+
return { rows: null, columns: null, data: 'too many inputs', legend: null };
8488
}
89+
90+
//playground DELETE LATER ////////////////
91+
// let tempvals = frame.fields[valKey];
92+
// let min = 0;
93+
// let max = 0;
94+
// if (tempvals.state) {
95+
// if(tempvals.state.range) {
96+
// if(tempvals.state.range.min) {
97+
// min = tempvals.state.range.min;
98+
// }
99+
// if (tempvals.state.range.max) {
100+
// max = tempvals.state.range.max;
101+
// }
102+
// }
103+
// }
104+
// console.log(`min: ${min} max: ${max}`);
105+
106+
////////////////////////////
107+
85108
// create data matrix
86109
let dataMatrix: any[][] = [];
87110
for (let i = 0; i < rowNames.length; i++) {
88111
dataMatrix.push(new Array(colNames.length).fill(-1));
89112
}
90-
91113
frame.forEach((row) => {
92114
let r = rowNames.indexOf(String(row[sourceKey]));
93115
let c = colNames.indexOf(String(row[targetKey]));
@@ -102,5 +124,44 @@ export function parseData(data: { series: any[] }, options: any, theme: any) {
102124
};
103125
}
104126
});
105-
return [rowNames, colNames, dataMatrix];
127+
128+
// parse data for legend
129+
let legendData: any[] = [];
130+
if (options.showLegend) {
131+
// let allVals = frame.fields[valKey].values;
132+
let tempValues: any[] = [];
133+
if (options.legendType == 'range') {
134+
//get min & max, steps
135+
let allValues: number[] = Object.values(frame.fields[valKey].values);
136+
let thisMin = Math.min(...allValues);
137+
let thisMax = Math.max(...allValues);
138+
let step = (thisMax - thisMin) / 10;
139+
// push 10 steps to use for legend
140+
tempValues = [];
141+
for(let i = 0; i <= 10; i++) {
142+
tempValues.push(thisMin + (i*step));
143+
}
144+
} else {
145+
// get unique categories
146+
let allValues: string[] = Object.values(frame.fields[valKey].values);
147+
let unique = new Set(allValues);
148+
tempValues = [...unique];
149+
}
150+
tempValues.forEach((val) => {
151+
// find display values, unit & color for each
152+
// store in array
153+
let text = valueField[0].display(val).text;
154+
if (valueField[0].display(val).suffix) {
155+
text = text + ` ${valueField[0].display(val).suffix}`;
156+
}
157+
legendData.push({
158+
label: text,
159+
color: colorMap(val),
160+
});
161+
});
162+
}
163+
console.log(legendData);
164+
165+
var dataObject = { rows: rowNames, columns: colNames, data: dataMatrix, legend: legendData };
166+
return dataObject;
106167
}

src/matrix.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ export function matrix(
44
matrix: any,
55
id: number,
66
height: number,
7-
options: any
7+
options: any,
8+
legend: any
89
): LegacyRef<SVGSVGElement> | undefined;

src/matrix.js

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { useTheme2 } from '@grafana/ui';
1212
* @param {string} val The data series that will act as the value
1313
* @param {GrafanaTheme} theme
1414
*/
15-
function createViz(elem, id, height, rowNames, colNames, matrix, options, theme) {
15+
function createViz(elem, id, height, rowNames, colNames, matrix, options, theme, legend) {
1616
const srcText = options.sourceText,
1717
targetText = options.targetText,
1818
valText = options.valueText,
@@ -76,9 +76,9 @@ function createViz(elem, id, height, rowNames, colNames, matrix, options, theme)
7676
.style('font-color', theme.colors.text.primary)
7777
.style('box-shadow', '3px 3px 4px lightgray')
7878
.style('padding', '5px')
79-
.style('z-index','500')
80-
.style('position','absolute')
81-
.style('width','fit-content')
79+
.style('z-index', '500')
80+
.style('position', 'absolute')
81+
.style('width', 'fit-content')
8282
.style('opacity', 0);
8383

8484
// append the svg object to the body of the page
@@ -132,7 +132,7 @@ function createViz(elem, id, height, rowNames, colNames, matrix, options, theme)
132132
div.style('opacity', 0).style('left', '0px').style('top', '0px');
133133
});
134134

135-
//build the matrix
135+
//build the matrix /////////////////////////////////////////
136136

137137
//use d3's local stuff to record where we are in the outer loop
138138
var outer = d3.local();
@@ -185,7 +185,7 @@ function createViz(elem, id, height, rowNames, colNames, matrix, options, theme)
185185
return str;
186186
})
187187
.attr('fill', (d) => {
188-
if(d.color) {
188+
if (d.color) {
189189
return d.color;
190190
} else {
191191
return defaultColor;
@@ -231,6 +231,88 @@ function createViz(elem, id, height, rowNames, colNames, matrix, options, theme)
231231
.attr('height', y.bandwidth());
232232
div.style('opacity', 0).style('left', '0px').style('top', '0px');
233233
});
234+
235+
////// LEGEND ////////////
236+
if (options.showLegend) {
237+
var legendClass = `legend-${id}`;
238+
239+
var div = d3
240+
.select(elem)
241+
.append('div')
242+
.attr('class', 'matrix-legend')
243+
.attr('width', 'auto')
244+
.attr('style', 'height: 70px;')
245+
.append('svg')
246+
.attr('id', legendClass);
247+
248+
if (options.legendType == 'range') {
249+
var svg = d3.select(`#${legendClass}`);
250+
svg
251+
.append('g')
252+
.selectAll('legendBars')
253+
.data(legend)
254+
.enter()
255+
.append('rect')
256+
.attr('class', 'legend-bar')
257+
.attr('width', 10)
258+
.attr('height', 10)
259+
.attr('fill', function (d) {
260+
return d.color;
261+
})
262+
.attr('x', function (d, i) {
263+
return 25 + i * 10;
264+
})
265+
.attr('y', 20);
266+
svg
267+
.append('g')
268+
.selectAll('legendLabels')
269+
.data(legend)
270+
.enter()
271+
.append('text')
272+
.attr('x', function (d, i) {
273+
return 20 + i * 10;
274+
})
275+
.attr('y', 50)
276+
.text(function (d, i) {
277+
if ((i == 0) | (i == legend.length - 1)) {
278+
return d.label;
279+
} else {
280+
return;
281+
}
282+
})
283+
.attr('fill', theme.colors.text.primary);
284+
} else {
285+
// categorical
286+
var svg = d3.select(`#${legendClass}`);
287+
svg
288+
.append('g')
289+
.selectAll('legendCircles')
290+
.data(legend)
291+
.enter()
292+
.append('circle')
293+
.attr('class', 'legend-circle')
294+
.attr('r', 10)
295+
.attr('fill', function (d) {
296+
return d.color;
297+
})
298+
.attr('cx', function (d, i) {
299+
return 25 + i * 75;
300+
})
301+
.attr('cy', 20);
302+
svg
303+
.append('g')
304+
.selectAll('legendLabels')
305+
.data(legend)
306+
.enter()
307+
.append('text')
308+
.attr('x', function (d, i) {
309+
return 15 + i * 75;
310+
})
311+
.attr('y', 50)
312+
.text(function (d) { return d.label })
313+
.attr('fill', theme.colors.text.primary);
314+
}
315+
}
234316
}
235317

236318
function truncateLabel(text, width) {
@@ -253,10 +335,10 @@ function truncateLabel(text, width) {
253335
* @param {number} height Height of panel
254336
* @return {*} A d3 callback
255337
*/
256-
function matrix(rowNames, colNames, matrix, id, height, options) {
338+
function matrix(rowNames, colNames, matrix, id, height, options, legend) {
257339
const theme = useTheme2();
258340
const ref = useD3((svg) => {
259-
createViz(svg, id, height, rowNames, colNames, matrix, options, theme);
341+
createViz(svg, id, height, rowNames, colNames, matrix, options, theme, legend);
260342
});
261343
return ref;
262344
}

src/matrixLegend.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export function legend(
2+
matrix: any,
3+
id: number,
4+
height: number,
5+
options: any
6+
): LegacyRef<SVGSVGElement> | undefined;

src/matrixLegend.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { useD3 } from './useD3.js';
2+
import * as d3 from './d3.min.js';
3+
import { useTheme2 } from '@grafana/ui';
4+
5+
/** Create the legend using d3.
6+
* @param {*} elem The parent svg element that will house this legend
7+
* @param {*} id The panel id
8+
* @param {number} height The current height of the panel
9+
* @param {*} legend The data that will populate the legend. array of objects providing color: the color to use and label: the display values use text and suffix
10+
* @param {GrafanaTheme} theme
11+
*/
12+
function renderLegend(elem, id, height, legend, options, theme) {
13+
if (elem === null) {
14+
console.log('bailing after failing to find parent element');
15+
return;
16+
}
17+
// while (elem !== undefined && elem.firstChild) {
18+
// // clear out old contents
19+
// elem.removeChild(elem.lastChild);
20+
// }
21+
22+
var svgClass = `legend-${id}`;
23+
24+
var div = d3
25+
.select(elem)
26+
.append('div')
27+
.attr('class', 'matrix-legend')
28+
.attr('width', 'auto')
29+
.append('svg').attr('id', svgClass);
30+
31+
var svg = d3.select(`#${svgClass}`)
32+
svg.append('circle')
33+
34+
// append svg
35+
// var svgClass = `legend-${id}`;
36+
// var svg = d3
37+
// .select(elem)
38+
// .append('svg')
39+
// .attr('id', svgClass)
40+
// .attr('width', width + margin.left + margin.right)
41+
// .attr('height', '36px')
42+
// .append('g');
43+
44+
// svg.selectAll('mydots')
45+
// .data(legend)
46+
// .enter()
47+
// .append('circle')
48+
// .attr('cx', 100)
49+
// .attr('cy', function (d, i) {
50+
// return 100 + i * 25;
51+
// }) // 100 is where the first dot appears. 25 is the distance between dots
52+
// .attr('r', 7)
53+
// .style('fill', function (d) {
54+
// return d.color;
55+
// });
56+
57+
//if the legend is a range
58+
if (options.legendType == 'range') {
59+
}
60+
61+
//if the legend is categorical
62+
}
63+
64+
/**
65+
*
66+
* @param {*} data Data for the chord diagram
67+
* @param {*} id The panel id
68+
* @param {string} src The data series that will act as the source
69+
* @param {string} target The data series that will act as * the target
70+
* @param {string} val The data series that will act as the value
71+
* @param {number} height Height of panel
72+
* @return {*} A d3 callback
73+
*/
74+
function legend(legend, id, height, options) {
75+
const theme = useTheme2();
76+
const ref = useD3((svg) => {
77+
renderLegend(svg, id, height, legend, options, theme);
78+
});
79+
// const ref = 'div'
80+
return ref;
81+
}
82+
83+
export { legend, renderLegend };

0 commit comments

Comments
 (0)