Skip to content

Commit

Permalink
[1255] Add Support for charts in the Form descriptions editor
Browse files Browse the repository at this point in the history
Bug: #1255
Signed-off-by: Florian Barbin <florian.barbin@obeo.fr>
  • Loading branch information
florianbarbin committed Jun 13, 2022
1 parent d183872 commit fbc4946
Show file tree
Hide file tree
Showing 15 changed files with 564 additions and 233 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
- https://github.com/eclipse-sirius/sirius-components/issues/1203[#1203] [charts] Add support for d3 bars data-structure and bar-chart representation
- https://github.com/eclipse-sirius/sirius-components/issues/1228[#1228] [charts] Add support for bar-chart in view DSL
- https://github.com/eclipse-sirius/sirius-components/issues/1248[#1248] [charts] Add support for pie-chart in Form representation
- https://github.com/eclipse-sirius/sirius-components/issues/1255[#1255] [form] Add support for charts in form descriptions editor

== v2022.5.0

Expand Down
1 change: 1 addition & 0 deletions frontend/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ module.exports = {
},
moduleNameMapper: {
'\\.(css|less)$': '<rootDir>/__mocks__/styleMock.js',
d3: '<rootDir>/node_modules/d3/dist/d3.min.js',
},
};
15 changes: 7 additions & 8 deletions frontend/src/charts/Charts.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,11 @@ export interface GQLRepresentationDescription {
}
export interface GQLRepresentation {
id: string;
}
export interface GQLChart extends GQLRepresentation {
metadata: GQLRepresentationMetadata;
}
export type GQLChart = GQLBarChart | GQLPieChart;

export interface GQLBarChart extends GQLChart {
export interface GQLBarChart extends GQLRepresentation {
label: string;
entries: GQLBarChartEntry[];
}
Expand All @@ -38,7 +37,7 @@ export interface GQLBarChartEntry {
value: number;
}

export interface GQLPieChart extends GQLChart {
export interface GQLPieChart extends GQLRepresentation {
label: string;
entries: GQLPieChartEntry[];
}
Expand All @@ -58,12 +57,12 @@ export interface RepresentationDescription {
}
export interface Representation {
id: string;
}
export interface Chart extends Representation {
metadata: RepresentationMetadata;
}

export interface BarChart extends Chart {
export type Chart = BarChart | PieChart;

export interface BarChart extends Representation {
label: string;
entries: BarChartEntry[];
}
Expand All @@ -72,7 +71,7 @@ export interface BarChartEntry {
value: number;
}

export interface PieChart extends Chart {
export interface PieChart extends Representation {
entries: PieChartEntry[];
}

Expand Down
105 changes: 105 additions & 0 deletions frontend/src/charts/barChart/BarChartComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*******************************************************************************
* Copyright (c) 2022 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
import * as d3 from 'd3';
import React, { useEffect, useRef } from 'react';
import { BarChartProps } from './BarChartComponent.types';

export const BarChartComponent = ({ width, height, chart }: BarChartProps) => {
const d3Container = useRef<SVGSVGElement | null>(null);
useEffect(() => {
const marginTop = 20;
const marginBottom = 30;
const marginRight = 0;
const marginLeft = 40;
const xRange = [marginLeft, width - marginRight]; // [left, right]
const yRange = [height - marginBottom, marginTop]; // [bottom, top]
const xPadding = 0.1;
const yType = d3.scaleLinear;
const yFormat = 'f';
if (d3Container.current && chart) {
const { entries: data, label: yLabel } = chart;
const x = (d) => d.key;
const y = (d) => d.value;
const color = 'steelblue';
// Compute values.
const X = d3.map(data, x);
const Y = d3.map(data, y);

// Compute default domains, and unique the x-domain.
const xDomain = new d3.InternSet(X);
const yDomain = [0, d3.max(Y)];

// Omit any data not present in the x-domain.
const I = d3.range(X.length).filter((i) => xDomain.has(X[i]));

// Construct scales, axes, and formats.
const xScale = d3.scaleBand(xDomain, xRange).padding(xPadding);
const yScale = yType(yDomain, yRange);
const xAxis = d3.axisBottom(xScale).tickSizeOuter(0);
const yAxis = d3.axisLeft(yScale).ticks(height / 40, yFormat);

// Compute titles.
const formatValue = yScale.tickFormat(100, yFormat);
const title = (i) => `${X[i]}\n${formatValue(Y[i])}`;

const selection = d3.select(d3Container.current);
selection.selectAll('*').remove(); // Remove existing content.
const svg = selection
.attr('viewBox', [0, 0, width, height])
.attr('style', `max-width: 100%; max-height: ${height}; height: auto; height: intrinsic;`);

svg
.append('g')
.attr('transform', `translate(${marginLeft},0)`)
.call(yAxis)
.call((g) => g.select('.domain').remove())
.call((g) =>
g
.selectAll('.tick line')
.clone()
.attr('x2', width - marginLeft - marginRight)
.attr('stroke-opacity', 0.1)
)
.call((g) =>
g
.append('text')
.attr('x', -marginLeft)
.attr('y', 10)
.attr('fill', 'currentColor')
.attr('text-anchor', 'start')
.text(yLabel)
);

const bar = svg
.append('g')
.attr('fill', color)
.selectAll('rect')
.data(I)
.join('rect')
.attr('x', (i) => xScale(X[i]))
.attr('y', (i) => yScale(Y[i]))
.attr('height', (i) => yScale(0) - yScale(Y[i]))
.attr('width', xScale.bandwidth());

if (title) {
bar.append('title').text(title);
}

svg
.append('g')
.attr('transform', `translate(0,${height - marginBottom})`)
.call(xAxis);
}
}, [width, height, chart, d3Container]);
return <svg ref={d3Container} width={width} height={height} />;
};
18 changes: 18 additions & 0 deletions frontend/src/charts/barChart/BarChartComponent.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*******************************************************************************
* Copyright (c) 2022 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
import { BarChart } from 'charts/Charts.types';
export interface BarChartProps {
width: number;
height: number;
chart: BarChart;
}
113 changes: 113 additions & 0 deletions frontend/src/charts/pieChart/PieChartComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*******************************************************************************
* Copyright (c) 2022 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
// This comment will be removed in the future but we should not have to wait to solve
// all d3 relatd typing issues in order to move forward with our TypeScript checks
// @ts-nocheck
import * as d3 from 'd3';
import React, { useEffect, useRef } from 'react';
import { PieChartProps } from './PieChartComponent.types';
export const PieChartComponent = ({ width, height, chart }: PieChartProps) => {
const d3Container = useRef<SVGSVGElement | null>(null);
useEffect(() => {
const { entries: data } = chart;
const name = (d) => d.key; // given d in data, returns the (ordinal) label
const value = (d) => d.value; // given d in data, returns the (quantitative) value
const innerRadius = 0; // inner radius of pie, in pixels (non-zero for donut)
const outerRadius = Math.min(width, height) / 2; // outer radius of pie, in pixels
const labelRadius = innerRadius * 0.2 + outerRadius * 0.8; // center radius of labels
const format = ','; // a format specifier for values (in the label)
const stroke = innerRadius > 0 ? 'none' : 'white'; // stroke separating widths
const strokeWidth = 1; // width of stroke separating wedges
const strokeLinejoin = 'round'; // line join of stroke separating wedges
const padAngle = stroke === 'none' ? 1 / outerRadius : 0; // angular separation between wedges
// Compute values.
const N = d3.map(data, name);
const V = d3.map(data, value);
const I = d3.range(N.length).filter((i) => !isNaN(V[i]));

// Unique the names.
const names = new d3.InternSet(N);

// Chose a default color scheme based on cardinality.
let colors = d3.schemeSpectral[names.size];
if (colors === undefined) colors = d3.quantize((t) => d3.interpolateSpectral(t * 0.8 + 0.1), names.size);

// Construct scales.
const color = d3.scaleOrdinal(names, colors);

// Compute titles.
const formatValue = d3.format(format);
const title = (i) => `${N[i]}\n${formatValue(V[i])}`;

const label = (d) => {
const lines = `${title(d.data)}`.split(/\n/);
let value: string[] = d.endAngle - d.startAngle > 0.25 ? lines : lines.slice(0, 1);
return value.map((v) => {
if (v.length > 13) {
return v.substring(0, 10).concat('...');
}
return v;
});
};

// Construct arcs.
const arcs = d3
.pie()
.padAngle(padAngle)
.sort(null)
.value((i) => V[i])(I);
const arc = d3.arc().innerRadius(innerRadius).outerRadius(outerRadius);
const arcLabel = d3.arc().innerRadius(labelRadius).outerRadius(labelRadius);

const selection = d3.select(d3Container.current);
selection.selectAll('*').remove(); // Remove existing content.
const svg = selection
.attr('width', width)
.attr('height', height)
.attr('viewBox', [-width / 2, -height / 2, width, height])
.attr('style', 'max-width: 100%; height: auto; height: intrinsic;');

svg
.append('g')
.attr('stroke', stroke)
.attr('stroke-width', strokeWidth)
.attr('stroke-linejoin', strokeLinejoin)
.selectAll('path')
.data(arcs)
.join('path')
.attr('fill', (d) => color(N[d.data]))
.attr('d', arc)
.append('title')
.text((d) => title(d.data));

svg
.append('g')
.attr('font-family', 'sans-serif')
.attr('font-size', 10)
.attr('text-anchor', 'middle')
.selectAll('text')
.data(arcs)
.join('text')
.attr('transform', (d) => `translate(${arcLabel.centroid(d)})`)
.selectAll('tspan')
.data(label)
.join('tspan')
.attr('x', 0)
.attr('y', (_, i) => `${i * 1.1}em`)
.attr('font-weight', (_, i) => (i ? null : 'bold'))
.text((d) => d);

Object.assign(svg.node(), { scales: { color } });
}, [width, height, chart, d3Container]);
return <svg ref={d3Container} />;
};
18 changes: 18 additions & 0 deletions frontend/src/charts/pieChart/PieChartComponent.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*******************************************************************************
* Copyright (c) 2022 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
import { PieChart } from 'charts/Charts.types';
export interface PieChartProps {
width: number;
height: number;
chart: PieChart;
}

0 comments on commit fbc4946

Please sign in to comment.