UNPKG

@rfprodz/client-d3-charts

Version:

Angular chart components based on D3JS (https://d3js.org).

1 lines 195 kB
{"version":3,"file":"rfprodz-client-d3-charts.mjs","sources":["../../../../libs/client-d3-charts/src/lib/util/configuration.util.ts","../../../../libs/client-d3-charts/src/lib/util/bar-chart.util.ts","../../../../libs/client-d3-charts/src/lib/util/force-directed-chart.util.ts","../../../../libs/client-d3-charts/src/lib/util/gauge-chart.util.ts","../../../../libs/client-d3-charts/src/lib/util/line-chart.util.ts","../../../../libs/client-d3-charts/src/lib/util/pie-chart.util.ts","../../../../libs/client-d3-charts/src/lib/util/radar-chart.util.ts","../../../../libs/client-d3-charts/src/lib/providers/d3-chart-factory.provider.ts","../../../../libs/client-d3-charts/src/lib/components/_base/chart.base.ts","../../../../libs/client-d3-charts/src/lib/components/bar-chart/bar-chart.component.ts","../../../../libs/client-d3-charts/src/lib/components/bar-chart/bar-chart.component.html","../../../../libs/client-d3-charts/src/lib/components/line-chart/line-chart.component.ts","../../../../libs/client-d3-charts/src/lib/components/line-chart/line-chart.component.html","../../../../libs/client-d3-charts/src/lib/components/chart-examples-line/chart-examples-line.component.ts","../../../../libs/client-d3-charts/src/lib/components/chart-examples-line/chart-examples-line.component.html","../../../../libs/client-d3-charts/src/lib/components/radar-chart/radar-chart.component.ts","../../../../libs/client-d3-charts/src/lib/components/radar-chart/radar-chart.component.html","../../../../libs/client-d3-charts/src/lib/components/chart-examples-radar/chart-examples-radar.component.ts","../../../../libs/client-d3-charts/src/lib/components/chart-examples-radar/chart-examples-radar.component.html","../../../../libs/client-d3-charts/src/lib/components/chart-examples-bar/chart-examples-bar.component.ts","../../../../libs/client-d3-charts/src/lib/components/chart-examples-bar/chart-examples-bar.component.html","../../../../libs/client-d3-charts/src/lib/components/pie-chart/pie-chart.component.ts","../../../../libs/client-d3-charts/src/lib/components/pie-chart/pie-chart.component.html","../../../../libs/client-d3-charts/src/lib/components/chart-examples-pie/chart-examples-pie.component.ts","../../../../libs/client-d3-charts/src/lib/components/chart-examples-pie/chart-examples-pie.component.html","../../../../libs/client-d3-charts/src/lib/components/gauge-chart/gauge-chart.component.ts","../../../../libs/client-d3-charts/src/lib/components/gauge-chart/gauge-chart.component.html","../../../../libs/client-d3-charts/src/lib/components/chart-examples-gauge/chart-examples-gauge.component.ts","../../../../libs/client-d3-charts/src/lib/components/chart-examples-gauge/chart-examples-gauge.component.html","../../../../libs/client-d3-charts/src/lib/components/force-directed-chart/force-directed-chart.component.ts","../../../../libs/client-d3-charts/src/lib/components/force-directed-chart/force-directed-chart.component.html","../../../../libs/client-d3-charts/src/lib/components/chart-examples-force-directed/chart-examples-force-directed.component.ts","../../../../libs/client-d3-charts/src/lib/components/chart-examples-force-directed/chart-examples-force-directed.component.html","../../../../libs/client-d3-charts/src/lib/components/chart-examples/chart-examples.component.ts","../../../../libs/client-d3-charts/src/lib/components/chart-examples/chart-examples.component.html","../../../../libs/client-d3-charts/src/lib/d3-charts.module.ts","../../../../libs/client-d3-charts/src/rfprodz-client-d3-charts.ts"],"sourcesContent":["/**\n * Generates a configuration object based on a defaut configuration and an options object.\n * @param config the default object with all properties\n * @param options the input object\n * @param result the output object\n */\nexport const generateConfiguration = <T>(\n config: T,\n options: Partial<T & Record<string, unknown>> | undefined,\n result: Record<string, unknown>,\n) => {\n const defaultConfiguration = config as Record<string, unknown>;\n\n if (typeof options === 'undefined') {\n return config;\n }\n const keys = Object.keys(defaultConfiguration);\n for (const key of keys) {\n const defaultValue = defaultConfiguration[key];\n const value = options[key];\n const typedKey: keyof typeof defaultConfiguration = key;\n if (typeof defaultValue === 'string' || typeof defaultValue === 'number' || typeof defaultValue === 'boolean') {\n result[typedKey] = typeof value !== 'undefined' ? value : defaultValue;\n } else if (defaultValue instanceof Function) {\n result[typedKey] = defaultValue;\n } else if (typeof defaultValue === 'object' && defaultValue !== null) {\n const nestedDefaultObject = defaultValue as Record<string, unknown>;\n const nestedObject = value as Record<string, unknown>;\n result[typedKey] = generateConfiguration(nestedDefaultObject, nestedObject, {});\n }\n }\n return result as T;\n};\n","import type { ElementRef } from '@angular/core';\nimport * as d3 from 'd3';\n\nimport type { IBarChartDataNode, IBarChartOptions, TBarChartData } from '../interfaces/bar-chart.interface';\nimport { generateConfiguration } from './configuration.util';\n\n/**\n * The bar chart default configuration.\n */\nexport const defaultBarChartConfig: IBarChartOptions = Object.freeze({\n chartTitle: '',\n width: 350,\n height: 350,\n margin: {\n top: 70,\n right: 50,\n bottom: 50,\n left: 50,\n },\n transitionDuration: 400,\n xAxisPadding: 0.4,\n xAxisTitle: '',\n yAxisTitle: '',\n yAxisTicks: 10,\n displayAxisLabels: true,\n labelTextWrapWidth: 60, // the number of pixels after which a label needs to be given a new line\n color: d3.scaleOrdinal(d3.schemeCategory10),\n} as IBarChartOptions);\n\n/**\n * Creates a container for the bar chart.\n * @param container the chart container\n * @param config the chart configuration\n * @returns the object with the svg element and the g element\n */\nconst createContainer = (container: ElementRef<HTMLDivElement>, config: IBarChartOptions) => {\n const id = container.nativeElement.id ?? 'bar-0';\n\n d3.select(`#${id}`).select('svg').remove();\n const svg = d3\n .select(`#${id}`)\n .append('svg')\n .attr('width', config.width + config.margin.left + config.margin.right)\n .attr('height', config.height + config.margin.top + config.margin.bottom)\n .attr('class', id);\n const g = svg.append('g').attr('transform', `translate(${config.margin.left},${config.margin.top / 2})`);\n\n return { svg, g };\n};\n\n/**\n * Wraps the bar chart axis labels text.\n * @param svgText the svg text elements\n * @param width the chart axis label width\n */\nconst wrapSvgText = (svgText: d3.Selection<d3.BaseType, unknown, SVGGElement, unknown>, width: number) => {\n svgText.each(function (this: d3.BaseType) {\n const text = d3.select<d3.BaseType, string>(this);\n const words = text.text().split(/\\s+/).reverse();\n if (words.length > 1) {\n let line: string[] = [];\n let lineNumber = 0;\n const lineHeight = 1.4;\n const y = text.attr('y');\n const x = text.attr('x');\n const dy = parseFloat(text.attr('dy') ?? 0);\n let tspan = text.text(null).append('tspan').attr('x', x).attr('y', y).attr('dy', `${dy}em`); // axis label\n\n let word = words.pop();\n\n while (typeof word !== 'undefined') {\n line.push(word ?? '');\n tspan.text(line.join(' '));\n if ((tspan.node()?.getComputedTextLength() ?? 0) > width) {\n line.pop();\n tspan.text(line.join(' '));\n line = [word ?? ''];\n tspan = text\n .append('tspan')\n .attr('x', 0)\n .attr('y', y)\n .attr('dy', `${lineNumber * lineHeight + dy}em`)\n .text(word ?? '');\n lineNumber += 1;\n }\n word = words.pop();\n }\n }\n });\n};\n\n/**\n * Creates the legend.\n * @param g the svg g element\n * @param config the chart configuration\n */\nconst createLegend = (g: d3.Selection<SVGGElement, unknown, HTMLElement, unknown>, config: IBarChartOptions) => {\n if (config.displayAxisLabels && config.xAxisTitle !== '') {\n g.append('g')\n .attr('transform', `translate(0, ${config.height + config.margin.bottom})`)\n .append('text')\n .style('font-size', '12px')\n .attr('class', 'legend')\n .attr('dx', '1.5em')\n .attr('dy', '1em')\n .text(`x - ${config.xAxisTitle}`);\n }\n\n if (config.displayAxisLabels && config.yAxisTitle !== '') {\n g.append('g')\n .attr('transform', `translate(0, ${config.height + config.margin.bottom})`)\n .append('text')\n .style('font-size', '12px')\n .attr('class', 'legend')\n .attr('dx', '1.5em')\n .attr('dy', '2.5em')\n .text(`y - ${config.yAxisTitle}`);\n }\n\n if (config.chartTitle !== '') {\n g.append('g')\n .attr('transform', `translate(0, 0)`)\n .append('text')\n .style('font-size', '12px')\n .attr('class', 'legend')\n .attr('dx', '1.5em')\n .attr('dy', '-2em')\n .text(config.chartTitle);\n }\n};\n\n/**\n * Creates the x axis.\n * @param g the svg g element\n * @param x the x axis scale\n * @param config the chart configuration\n */\nconst createAxisX = (g: d3.Selection<SVGGElement, unknown, HTMLElement, unknown>, x: d3.ScaleBand<string>, config: IBarChartOptions) => {\n const xLabels = g.append('g').attr('transform', `translate(0, ${config.height})`).call(d3.axisBottom(x)).append('text');\n\n g.selectAll('text').call(wrapSvgText, config.labelTextWrapWidth);\n\n if (config.displayAxisLabels) {\n xLabels.attr('transform', `translate(${config.width}, 0)`).attr('class', 'legend').attr('dx', '1.5em').attr('dy', '0.7em').text('x');\n }\n};\n\n/**\n * Creates the y axis.\n * @param g the svg g element\n * @param y the y axis scale\n * @param config the chart configuration\n */\nconst createAxisY = (\n g: d3.Selection<SVGGElement, unknown, HTMLElement, unknown>,\n y: d3.ScaleLinear<number, number>,\n config: IBarChartOptions,\n) => {\n const yLabels = g\n .append('g')\n .call(\n d3\n .axisLeft(y)\n .tickFormat(function (d) {\n return `${d}`;\n })\n .ticks(config.yAxisTicks),\n )\n .append('text');\n\n if (config.displayAxisLabels) {\n yLabels.attr('dy', '-1.5em').attr('class', 'legend').text('y');\n }\n};\n\n/**\n * The mouse over event handler.\n * @param self an svg rect element\n * @param d the chart data node\n * @param g the svg g element\n * @param x the x axis scale\n * @param y the y axis scale\n * @param config the chart configuration\n */\nconst onMouseOver = (\n self: SVGRectElement,\n d: IBarChartDataNode,\n g: d3.Selection<SVGGElement, unknown, HTMLElement, unknown>,\n x: d3.ScaleBand<string>,\n y: d3.ScaleLinear<number, number>,\n config: IBarChartOptions,\n) => {\n const widthModifier = 5;\n d3.select(self)\n .transition()\n .duration(config.transitionDuration)\n .attr('width', x.bandwidth() + widthModifier)\n .attr('y', function () {\n const modifier = 10;\n return y(d.value) - modifier;\n })\n .attr('height', function () {\n const modifier = 10;\n return config.height - y(d.value) + modifier;\n });\n\n g.append('text')\n .attr('class', 'chart-tooltip')\n .style('font-size', '11px')\n .attr('x', () => x(d.title) ?? '')\n .attr('y', function () {\n const modifier = 15;\n return y(d.value) - modifier;\n })\n .text(() => d.value);\n};\n\n/**\n * The mouse out event handler.\n * @param self an svg rect element\n * @param d the chart data node\n * @param x the x axis scale\n * @param y the y axis scale\n * @param config the chart configuration\n */\nconst onMouseOut = (\n self: SVGRectElement,\n d: IBarChartDataNode,\n x: d3.ScaleBand<string>,\n y: d3.ScaleLinear<number, number>,\n config: IBarChartOptions,\n) => {\n d3.select(self).attr('class', 'bar');\n d3.select(self)\n .transition()\n .duration(config.transitionDuration)\n .attr('width', x.bandwidth())\n .attr('y', () => y(d.value) ?? 0)\n .attr('height', () => config.height - (y(d.value) ?? 0));\n\n d3.selectAll('.chart-tooltip').remove();\n};\n\n/**\n * Draws the chart bars, and sets the mouse pointer events.\n * @param g the svg g element\n * @param x the x axis scale\n * @param y the y axis scale\n * @param config the chart configuration\n * @param data the chart data\n */\nconst drawBarsAndSetPointerEvents = (\n g: d3.Selection<SVGGElement, unknown, HTMLElement, unknown>,\n x: d3.ScaleBand<string>,\n y: d3.ScaleLinear<number, number>,\n config: IBarChartOptions,\n data: TBarChartData,\n) => {\n const duration = 400;\n g.selectAll('.bar')\n .data(data)\n .enter()\n .append('rect')\n .attr('class', 'bar')\n .style('fill', (d, i) => config.color(i.toString()))\n .on('mouseover', function (this, event, d) {\n return onMouseOver(this, d, g, x, y, config);\n })\n .on('mouseout', function (this, event, d) {\n return onMouseOut(this, d, x, y, config);\n })\n .attr('x', d => x(d.title) ?? '')\n .attr('y', d => y(d.value))\n .attr('width', x.bandwidth())\n .transition()\n .ease(d3.easeLinear)\n .duration(duration)\n .delay(function (d, i) {\n const multiplier = 50;\n return i * multiplier;\n })\n .attr('height', d => config.height - y(d.value));\n};\n\n/**\n * Draws the bar chart.\n * @param container the chart container\n * @param data the chart data\n * @param options the chart options\n * @returns the chart configuration\n */\nexport const drawBarChart = (container: ElementRef<HTMLDivElement>, data: TBarChartData, options?: Partial<IBarChartOptions>) => {\n const config: IBarChartOptions = generateConfiguration<IBarChartOptions>(defaultBarChartConfig, options, {});\n\n const { g } = createContainer(container, config);\n\n const x = d3\n .scaleBand([0, config.width])\n .padding(config.xAxisPadding)\n .domain(data.map(d => d.title));\n const y = d3.scaleLinear([config.height, 0]).domain([0, d3.max(data, d => d.value) ?? 1]);\n\n createAxisX(g, x, config);\n\n createAxisY(g, y, config);\n\n createLegend(g, config);\n\n drawBarsAndSetPointerEvents(g, x, y, config, data);\n\n return config;\n};\n","import type { ElementRef } from '@angular/core';\nimport * as d3 from 'd3';\n\nimport type {\n IForceDirectedChartData,\n IForceDirectedChartDataNode,\n IForceDirectedChartOptions,\n} from '../interfaces/force-directed-chart.interface';\nimport { generateConfiguration } from './configuration.util';\n\n/**\n * The force directed chart default configuration.\n */\nexport const defaultForceDirectedChartConfig: IForceDirectedChartOptions = Object.freeze({\n chartTitle: '',\n width: 600,\n height: 600,\n centerCalcMod: 1.6,\n charge: {\n strength: -10,\n theta: 0.6,\n distanceMax: 2000,\n },\n distance: 75,\n fontSize: 10,\n collisionRadius: 30,\n margin: {\n top: 20,\n right: 20,\n bottom: 20,\n left: 20,\n },\n linkStrokeColor: 'lightgray',\n linkStrokeWidth: 1.5,\n labelTextWrapWidth: 60,\n color: d3.scaleOrdinal(d3.schemeCategory10),\n nodeColor: '#f00000',\n nodeStrokeColor: 'lightgray',\n nodeStrokeWidth: 1,\n});\n\n/**\n * The force durected chart tick handler.\n * @param link chart links\n * @param node chart nodes\n * @param text chart text\n * @returns rotation angle\n */\nconst ticked = (\n link?: d3.Selection<SVGLineElement, d3.SimulationLinkDatum<IForceDirectedChartDataNode>, SVGSVGElement, unknown>,\n node?: d3.Selection<SVGCircleElement, IForceDirectedChartDataNode, SVGSVGElement, unknown>,\n text?: d3.Selection<SVGTextElement, IForceDirectedChartDataNode, SVGGElement, unknown>,\n) => {\n if (typeof link !== 'undefined') {\n link\n .attr('x1', d => (d.source as { x: number; y: number }).x ?? 0)\n .attr('y1', d => (d.source as { x: number; y: number }).y ?? 0)\n .attr('x2', d => (d.target as { x: number; y: number }).x ?? 0)\n .attr('y2', d => (d.target as { x: number; y: number }).y ?? 0);\n }\n\n if (typeof node !== 'undefined') {\n node.attr('cx', d => d.x ?? 0).attr('cy', d => d.y ?? 0);\n }\n\n if (typeof text !== 'undefined') {\n const dx = 10;\n const dy = 5;\n text.attr('x', d => (d.x ?? 0) + dx).attr('y', d => (d.y ?? 0) - dy);\n }\n\n return 'rotate(0)';\n};\n\n/**\n * Creates a container for the force directed chart.\n * @param container the chart container\n * @param config the chart configuration\n * @returns the object with the svg element and the g element\n */\nconst createContainer = (container: ElementRef<HTMLDivElement>, config: IForceDirectedChartOptions) => {\n const id = container.nativeElement.id ?? 'force-directed-0';\n\n d3.select(`#${id}`).select('svg').remove();\n const svg = d3\n .select(`#${id}`)\n .append('svg')\n .attr('width', config.width + config.margin.left + config.margin.right)\n .attr('height', config.height + config.margin.top + config.margin.bottom)\n .attr('class', id);\n const g = svg\n .append('g')\n .attr('transform', `translate(${config.width / 2 + config.margin.left},${config.height / 2 + config.margin.top})`);\n\n return { svg, g };\n};\n\n/**\n * Applies the force directed chart data.\n * @param g the svg g element\n * @param data the chart data\n */\nconst applyChartData = (g: d3.Selection<SVGGElement, unknown, HTMLElement, unknown>, data: IForceDirectedChartData) => {\n const imageXY = 10;\n g.append('defs')\n .selectAll('pattern')\n .data(data.entities)\n .enter()\n .append('pattern')\n .attr('id', (val, i) => `img-${val.index}`)\n .attr('x', 0)\n .attr('y', 0)\n .attr('height', val => {\n const baseValue = 30;\n return baseValue + val.linksCount * 2;\n })\n .attr('width', val => {\n const baseValue = 30;\n return baseValue + val.linksCount * 2;\n })\n .append('image')\n .attr('x', imageXY)\n .attr('y', imageXY)\n .attr('height', val => {\n const baseValue = 30;\n return baseValue + val.linksCount * 2;\n })\n .attr('width', val => {\n const baseValue = 30;\n return baseValue + val.linksCount * 2;\n })\n .attr('xlink:href', val => val.img);\n};\n\n/**\n * Creates the force directed chart links.\n * @param svg the svg element\n * @param config the chart configuration\n * @param data the chart data\n * @returns the chart links\n */\nconst createLinks = (\n svg: d3.Selection<SVGSVGElement, unknown, HTMLElement, unknown>,\n config: IForceDirectedChartOptions,\n data: IForceDirectedChartData,\n) => {\n return svg\n .selectAll('.link')\n .data(data.links)\n .enter()\n .append('line')\n .attr('class', 'link')\n .style('stroke', config.linkStrokeColor)\n .style('stroke-width', config.linkStrokeWidth);\n};\n\n/**\n * Creates the force directed chart forces.\n * @param config the chart configuration\n * @param data the chart data\n * @returns the chart forces\n */\nconst createForces = (config: IForceDirectedChartOptions, data: IForceDirectedChartData) => {\n return d3\n .forceSimulation(data.nodes)\n .force(\n 'link',\n d3.forceLink().id(d => d.index ?? 0),\n )\n .force('charge', d3.forceManyBody().strength(config.charge.strength).theta(config.charge.theta).distanceMax(config.charge.distanceMax))\n .force('center', d3.forceCenter(config.width / config.centerCalcMod, config.height / config.centerCalcMod))\n .force(\n 'collision',\n d3.forceCollide().radius(d => config.collisionRadius),\n )\n .force(\n 'link',\n d3\n .forceLink(data.links)\n .id(d => d.index ?? 0)\n .distance(config.distance)\n .links(data.links),\n );\n};\n\n/**\n * The force directed chart node drag start handler.\n * @param event a drag event\n * @param datum the chart data\n * @param force the chart forces\n */\nconst nodeDragStartHandler = (\n event: d3.D3DragEvent<SVGCircleElement, IForceDirectedChartDataNode, unknown>,\n datum: IForceDirectedChartDataNode,\n force: d3.Simulation<IForceDirectedChartDataNode, undefined>,\n) => {\n if (!event.active && typeof force !== 'undefined') {\n const alphaTarget = 0.3;\n force.alphaTarget(alphaTarget).restart();\n }\n datum.fx = event.x;\n datum.fy = event.y;\n};\n\n/**\n * The force directed chart node drag handler.\n * @param event a drag event\n * @param datum the chart data\n * @param config the chart configuration\n */\nconst nodeDragHandler = (\n event: d3.D3DragEvent<SVGCircleElement, IForceDirectedChartDataNode, unknown>,\n datum: IForceDirectedChartDataNode,\n config: IForceDirectedChartOptions,\n) => {\n datum.fx = event.x > config.margin.left && event.x < config.width + config.margin.right ? event.x : datum.fx;\n datum.fy = event.y > config.margin.top && event.y < config.width + config.margin.bottom ? event.y : datum.fy;\n};\n\n/**\n * The force directed chart node drag end handler.\n * @param event a drag event\n * @param datum the chart data\n * @param force the chart forces\n */\nconst nodeDragEndHandler = (\n event: d3.D3DragEvent<SVGCircleElement, IForceDirectedChartDataNode, unknown>,\n datum: IForceDirectedChartDataNode,\n force: d3.Simulation<IForceDirectedChartDataNode, undefined>,\n) => {\n if (!event.active && typeof force !== 'undefined') {\n force.alphaTarget(0);\n }\n datum.fx = null;\n datum.fy = null;\n};\n\n/**\n * Creates the force directed chart nodes.\n * @param svg the svg element\n * @param data the chart data\n * @param force the chart forces\n * @param config the chart configuration\n * @returns the chart nodes\n */\nconst createNodes = (\n svg: d3.Selection<SVGSVGElement, unknown, HTMLElement, unknown>,\n data: IForceDirectedChartData,\n force: d3.Simulation<IForceDirectedChartDataNode, undefined>,\n config: IForceDirectedChartOptions,\n) => {\n const base = 5;\n return svg\n .selectAll('.node')\n .data(data.nodes)\n .enter()\n .append('circle')\n .attr('class', 'node')\n .attr('r', node => base + ((node.value ?? 1) + (node.linksCount ?? 1)))\n .style('stroke-width', config.nodeStrokeWidth)\n .style('stroke', config.nodeStrokeColor)\n .style('fill', node => (typeof node.img === 'undefined' || node.img === '' ? config.nodeColor : `url(${node.img})`))\n .call(\n d3\n .drag<SVGCircleElement, IForceDirectedChartDataNode>()\n .on(\n 'start',\n function (\n this: SVGCircleElement,\n event: d3.D3DragEvent<SVGCircleElement, IForceDirectedChartDataNode, unknown>,\n datum: IForceDirectedChartDataNode,\n ) {\n nodeDragStartHandler(event, datum, force);\n },\n )\n .on(\n 'drag',\n function (\n this: SVGCircleElement,\n event: d3.D3DragEvent<SVGCircleElement, IForceDirectedChartDataNode, unknown>,\n datum: IForceDirectedChartDataNode,\n ) {\n nodeDragHandler(event, datum, config);\n },\n )\n .on(\n 'end',\n function (\n this: SVGCircleElement,\n event: d3.D3DragEvent<SVGCircleElement, IForceDirectedChartDataNode, unknown>,\n datum: IForceDirectedChartDataNode,\n ) {\n nodeDragEndHandler(event, datum, force);\n },\n ),\n );\n};\n\n/**\n * Creates the force directed chart text labels.\n * @param svg the svg element\n * @param data the chart data\n * @returns the chart text labels\n */\nconst createText = (svg: d3.Selection<SVGSVGElement, unknown, HTMLElement, unknown>, data: IForceDirectedChartData) => {\n return svg\n .append('g')\n .selectAll('text')\n .data(data.nodes)\n .enter()\n .append('text')\n .attr('class', 'legend')\n .text(node => node.name ?? `N/A (id. ${node.index})`);\n};\n\n/**\n * Draws the force directed chart.\n * @param container the chart container\n * @param data the chart data\n * @param options the chart options\n * @returns the chart configuration\n */\nexport const drawForceDirectedChart = (\n container: ElementRef<HTMLDivElement>,\n data: IForceDirectedChartData,\n options?: Partial<IForceDirectedChartOptions>,\n) => {\n const config: IForceDirectedChartOptions = generateConfiguration<IForceDirectedChartOptions>(\n defaultForceDirectedChartConfig,\n options,\n {},\n );\n\n const { svg, g } = createContainer(container, config);\n\n applyChartData(g, data);\n\n const link = createLinks(svg, config, data);\n\n const force = createForces(config, data);\n\n const node = createNodes(svg, data, force, config);\n\n const text = createText(svg, data);\n\n force.on('tick', () => {\n ticked(link, node, text);\n });\n\n return config;\n};\n","import type { ElementRef } from '@angular/core';\nimport * as d3 from 'd3';\n\nimport type { IGaugeChartDataNode, IGaugeChartOptions } from '../interfaces/gauge-chart.interface';\nimport { generateConfiguration } from './configuration.util';\n\n/**\n * The gauge chart default configuration.\n */\nexport const defaultGaugeChartConfig: IGaugeChartOptions = Object.freeze({\n chartTitle: '',\n width: 600,\n height: 600,\n margin: {\n top: 20,\n right: 20,\n bottom: 20,\n left: 20,\n },\n innerRadius: 100, // increase inner radius to reduce thickness of the chart\n showLabels: true,\n showTooltips: true,\n labelRadiusModifier: 50,\n labelTextWrapWidth: 60,\n transitionDuration: 1000,\n color: 'green',\n defaultColor: 'lightgray',\n value: 10,\n padRad: 0.01,\n labelFontSize: 12,\n valueFontSize: 18,\n} as IGaugeChartOptions);\n\n/**\n * Creates a container for the gauge chart.\n * @param container the chart container\n * @param config the chart configuration\n * @returns the object with the svg element and the g element\n */\nconst createContainer = (container: ElementRef<HTMLDivElement>, config: IGaugeChartOptions) => {\n const id = container.nativeElement.id ?? 'gauge-0';\n\n d3.select(`#${id}`).select('svg').remove();\n const svg = d3\n .select(`#${id}`)\n .append('svg')\n .attr('width', config.width + config.margin.left + config.margin.right)\n .attr('height', config.height / 2 + config.margin.top + config.margin.bottom)\n .attr('class', id);\n const g = svg\n .append('g')\n .attr('transform', `translate(${config.width / 2 + config.margin.left},${config.height / 2 + config.margin.top})`);\n\n return { svg, g };\n};\n\n/**\n * Percentage to degrees converter.\n * @param perc percentage\n */\nconst percToDeg = (perc: number) => {\n const mod = 360;\n return perc * mod;\n};\n\n/**\n * Degrees to percentage converter.\n * @param deg degrees\n */\nconst degToRad = (deg: number) => {\n const mod = 180;\n return (deg * Math.PI) / mod;\n};\n\n/**\n * Repcentage to radians converter.\n * @param perc percentage\n */\nconst percToRad = (perc: number) => degToRad(percToDeg(perc));\n\n/**\n * Draws the gauge chart sections.\n * @param config the chart config\n * @param arc the charts's arc\n * @param arcs the chart's arc sections\n * @param data the chart's data\n */\nconst drawSections = (\n config: IGaugeChartOptions,\n arc: d3.Arc<unknown, d3.PieArcDatum<IGaugeChartDataNode>>,\n arcs: d3.Selection<SVGGElement, d3.PieArcDatum<IGaugeChartDataNode>, SVGGElement, unknown>,\n data: IGaugeChartDataNode[],\n) => {\n let startAt = 0.75; // Start at 270deg\n const sectionPercentage = 1 / data.length / 2;\n\n arcs\n .append('path')\n .attr('fill', d => (d.data.y <= config.value ? config.color : config.defaultColor))\n .attr('opacity', (d, i) => {\n const total = 100;\n const sections = data.length - 1;\n return ((i + 1) * sections) / total;\n })\n .attr('d', datum => {\n const arcStartRad = percToRad(startAt);\n const arcEndRad = arcStartRad + percToRad(sectionPercentage);\n startAt += sectionPercentage;\n\n const startPadRad = datum.index === 0 ? 0 : config.padRad / 2;\n const endPadRad = datum.index === data.length ? 0 : config.padRad / 2;\n return arc.startAngle(arcStartRad + startPadRad).endAngle(arcEndRad - endPadRad)(datum);\n });\n};\n\n/**\n * Draws the guage chart labels\n * @param config the chart config\n * @param arc the charts's arc\n * @param arcs the chart's arc sections\n * @param data the chart's data\n */\nconst drawLabels = (\n config: IGaugeChartOptions,\n arc: d3.Arc<unknown, d3.PieArcDatum<IGaugeChartDataNode>>,\n arcs: d3.Selection<SVGGElement, d3.PieArcDatum<IGaugeChartDataNode>, SVGGElement, unknown>,\n data: IGaugeChartDataNode[],\n) => {\n let startAt = 0.75; // Start at 270deg\n const sectionPercentage = 1 / data.length / 2;\n const textDy = 5;\n\n arcs\n .append('text')\n .attr('class', 'legend')\n .attr('text-anchor', 'middle')\n .attr('dy', textDy)\n .attr('transform', datum => {\n const arcStartRad = percToRad(startAt);\n const arcEndRad = arcStartRad + percToRad(sectionPercentage);\n startAt += sectionPercentage;\n\n const startPadRad = datum.index === 0 ? 0 : config.padRad / 2;\n const endPadRad = datum.index === data.length ? 0 : config.padRad / 2;\n const a = arc.startAngle(arcStartRad + startPadRad).endAngle(arcEndRad - endPadRad);\n return `translate(${a.centroid(datum)})`;\n })\n .style('font-size', `${config.labelFontSize}px`)\n .text(d => d.data.y);\n};\n\n/**\n * Draws the gauge chart value.\n * @param config the chart config\n * @param g the gauge chart container\n * @param radius the chart radius\n */\nconst drawValue = (config: IGaugeChartOptions, g: d3.Selection<SVGGElement, unknown, HTMLElement, unknown>, radius: number) => {\n const mod = 20;\n g.append('text')\n .attr('class', 'legend')\n .attr('text-anchor', 'middle')\n .attr('dx', radius / mod)\n .attr('dy', -(radius / mod))\n .style('font-size', `${config.valueFontSize}px`)\n .text(() => `${config.value}%`);\n};\n\n/**\n * Draws the gauge chart.\n * @param container the chart container\n * @param data the chart data\n * @param options the chart options\n * @returns the chart configuration\n */\nexport const drawGaugeChart = (\n container: ElementRef<HTMLDivElement>,\n data: IGaugeChartDataNode[],\n options?: Partial<IGaugeChartOptions>,\n) => {\n const config: IGaugeChartOptions = generateConfiguration<IGaugeChartOptions>(defaultGaugeChartConfig, options, {});\n\n const { g } = createContainer(container, config);\n\n const gauge = d3.pie<IGaugeChartDataNode>().value(datum => datum.y);\n\n const radius = Math.min(config.width, config.height) / 2;\n\n const arc = d3.arc<d3.PieArcDatum<IGaugeChartDataNode>>().innerRadius(config.innerRadius).outerRadius(radius);\n\n const arcs = g\n .selectAll('arc')\n .data(gauge(data))\n .enter()\n .append('g')\n .attr('class', 'arc')\n .on('mouseover', function (this, event: MouseEvent, d) {\n this.style.opacity = '0.8';\n\n const displayTooltip = d.data.y <= config.value && config.showTooltips;\n\n if (displayTooltip) {\n const tooltipText = `${d.data.key}: ${d.data.y}`;\n\n g.append('text')\n .attr('class', 'chart-tooltip')\n .style('opacity', 0)\n .attr('dx', -config.width / (2 * 2 * 2))\n .attr('dy', radius / 2 - config.margin.top - config.margin.bottom)\n .text(tooltipText)\n .transition()\n .duration(config.transitionDuration)\n .style('opacity', 1);\n }\n })\n .on('mouseout', function (this, event, d) {\n this.style.opacity = 'unset';\n d3.selectAll('.chart-tooltip')\n .transition()\n .duration(config.transitionDuration / 2)\n .style('opacity', 0)\n .remove();\n });\n\n drawSections(config, arc, arcs, data);\n\n if (config.showLabels) {\n drawLabels(config, arc, arcs, data);\n }\n\n drawValue(config, g, radius);\n\n return config;\n};\n","import type { ElementRef } from '@angular/core';\nimport * as d3 from 'd3';\n\nimport type { ILineChartDataNode, ILineChartOptions, TLineChartData } from '../interfaces/line-chart.interface';\nimport { generateConfiguration } from './configuration.util';\n\n/** The line chart default configuration. */\nexport const defaultLineChartConfig: ILineChartOptions = Object.freeze({\n chartTitle: '',\n width: 350,\n height: 350,\n margin: {\n top: 70,\n right: 50,\n bottom: 50,\n left: 50,\n },\n transitionDuration: 400,\n dotRadius: 3.5,\n xAxisTitle: '',\n yAxisTitle: '',\n ticks: {\n x: 5,\n y: 10,\n },\n displayAxisLabels: true,\n dateFormat: 'default',\n labelTextWrapWidth: 20, // the number of pixels after which a label needs to be given a new line\n color: d3.scaleOrdinal(d3.schemeCategory10),\n} as ILineChartOptions);\n\n/**\n * Creates a container for the line chart.\n * @param container the chart container\n * @param config the chart configuration\n * @returns the object with the svg element and the g element\n */\nconst createContainer = (container: ElementRef<HTMLDivElement>, config: ILineChartOptions) => {\n const id = container.nativeElement.id ?? 'line-0';\n\n d3.select(`#${id}`).select('svg').remove();\n const svg = d3\n .select(`#${id}`)\n .append('svg')\n .attr('width', config.width + config.margin.left + config.margin.right)\n .attr('height', config.height + config.margin.top + config.margin.bottom)\n .attr('class', id);\n const g = svg.append('g').attr('transform', `translate(${config.margin.left},${config.margin.top / 2})`);\n\n return { svg, g };\n};\n\n/**\n * Wraps the line chart axis labels text.\n * @param svgText the svg text elements\n * @param width the chart axis label width\n */\nconst wrapSvgText = (svgText: d3.Selection<d3.BaseType, unknown, SVGGElement, unknown>, width: number) => {\n svgText.each(function (this: d3.BaseType) {\n const text = d3.select<d3.BaseType, string>(this);\n const words = text.text().split(/\\s+/).reverse();\n if (words.length > 1) {\n let line: string[] = [];\n let lineNumber = 0;\n const lineHeight = 1.4;\n const y = text.attr('y');\n const x = text.attr('x');\n const dy = parseFloat(text.attr('dy') ?? 0);\n let tspan = text.text(null).append('tspan').attr('x', x).attr('y', y).attr('dy', `${dy}em`); // axis label\n\n let word = words.pop();\n\n while (typeof word !== 'undefined') {\n line.push(word ?? '');\n tspan.text(line.join(' '));\n if ((tspan.node()?.getComputedTextLength() ?? 0) > width) {\n line.pop();\n tspan.text(line.join(' '));\n line = [word ?? ''];\n tspan = text\n .append('tspan')\n .attr('x', 0)\n .attr('y', y)\n .attr('dy', `${lineNumber * lineHeight + dy}em`)\n .text(word ?? '');\n lineNumber += 1;\n }\n word = words.pop();\n }\n }\n });\n};\n\n/**\n * Creates the legend.\n * @param g the svg g element\n * @param config the chart configuration\n */\nconst createLegend = (g: d3.Selection<SVGGElement, unknown, HTMLElement, unknown>, config: ILineChartOptions) => {\n if (config.displayAxisLabels && config.xAxisTitle !== '') {\n g.append('g')\n .attr('transform', `translate(0, ${config.height + config.margin.bottom})`)\n .append('text')\n .style('font-size', '12px')\n .attr('class', 'legend')\n .attr('dx', '0.5em')\n .attr('dy', '0em')\n .text(`x - ${config.xAxisTitle}`);\n }\n\n if (config.displayAxisLabels && config.yAxisTitle !== '') {\n g.append('g')\n .attr('transform', `translate(0, ${config.height + config.margin.bottom})`)\n .append('text')\n .style('font-size', '12px')\n .attr('class', 'legend')\n .attr('dx', '0.5em')\n .attr('dy', '1.5em')\n .text(`y - ${config.yAxisTitle}`);\n }\n\n if (config.chartTitle !== '') {\n g.append('g')\n .attr('transform', `translate(0, 0)`)\n .append('text')\n .style('font-size', '12px')\n .attr('class', 'legend')\n .attr('dx', '1.5em')\n .attr('dy', '-2em')\n .text(config.chartTitle);\n }\n};\n\n/**\n * Creates the x axis.\n * @param g the svg g element\n * @param x the x axis scale\n * @param config the chart configuration\n */\nconst createAxisX = (\n g: d3.Selection<SVGGElement, unknown, HTMLElement, unknown>,\n x: d3.ScaleTime<number, number>,\n config: ILineChartOptions,\n) => {\n const xLabels = g\n .append('g')\n .attr('transform', `translate(0, ${config.height})`)\n .call(\n d3\n .axisBottom(x)\n .ticks(config.ticks.x)\n .tickFormat(d => {\n const date = new Date(d.valueOf());\n const formattingOffset = 10;\n const day = date.getDate();\n const dd = day < formattingOffset ? `0${day}` : day;\n const month = date.getMonth() + 1;\n const mm = month < formattingOffset ? `0${month}` : month;\n const year = date.getFullYear().toString();\n const yy = year.slice(2);\n const hours = date.getHours();\n const hour = hours < formattingOffset ? `0${hours}` : hours;\n const minutes = date.getMinutes();\n const minute = minutes < formattingOffset ? `0${minutes}` : minutes;\n let formattedDate = `${dd}/${mm}/${yy} ${hour}:${minute}`;\n switch (config.dateFormat) {\n case 'dd/mm/yyyy':\n formattedDate = `${dd}/${mm}/${year}`;\n break;\n case 'dd/mm/yy':\n formattedDate = `${dd}/${mm}/${yy}`;\n break;\n case 'mm/yyyy':\n formattedDate = `${mm}/${year}`;\n break;\n case 'yyyy':\n formattedDate = `${year}`;\n break;\n default:\n break;\n }\n return formattedDate;\n }),\n )\n .append('text');\n\n g.selectAll('text').call(wrapSvgText, config.labelTextWrapWidth);\n\n if (config.displayAxisLabels) {\n xLabels.attr('transform', `translate(${config.width}, 0)`).attr('class', 'legend').attr('dx', '1.5em').attr('dy', '0.7em').text('x');\n }\n};\n\n/**\n * Creates the y axis.\n * @param g the svg g element\n * @param y the y axis scale\n * @param config the chart configuration\n */\nconst createAxisY = (\n g: d3.Selection<SVGGElement, unknown, HTMLElement, unknown>,\n y: d3.ScaleLinear<number, number>,\n config: ILineChartOptions,\n) => {\n const yLabels = g\n .append('g')\n .call(\n d3\n .axisLeft(y)\n .ticks(config.ticks.y)\n .tickFormat(d => `${d}`),\n )\n .append('text');\n\n if (config.displayAxisLabels) {\n yLabels.attr('class', 'legend').attr('dy', '-1.5em').attr('class', 'legend').text('y');\n }\n};\n\n/**\n * The mouse over event handler.\n * @param self an svg circle element\n * @param d the chart data node\n * @param g the svg g element\n * @param config the chart configuration\n */\nconst onMouseOver = (\n self: SVGCircleElement,\n d: ILineChartDataNode,\n g: d3.Selection<SVGGElement, unknown, HTMLElement, unknown>,\n config: ILineChartOptions,\n) => {\n const duration = 400;\n d3.select(self)\n .transition()\n .duration(duration)\n .attr('r', config.dotRadius * 2);\n\n const tooltipShift = 4;\n const tooltipDy = -10;\n g.append('text')\n .attr('class', 'chart-tooltip')\n .style('font-size', '11px')\n .attr('dx', () => (config.width - config.margin.left - config.margin.right) / tooltipShift)\n .attr('dy', () => tooltipDy)\n .text(() => `${d.value} (${new Date(d.timestamp).toUTCString()})`)\n .transition()\n .duration(config.transitionDuration)\n .style('opacity', 1);\n};\n\n/**\n * The mouse out event handler.\n * @param self an svg circle element\n * @param config the chart configuration\n */\nconst onMouseOut = (self: SVGCircleElement, config: ILineChartOptions) => {\n const duration = 400;\n d3.select(self).attr('class', 'dot');\n d3.select(self).transition().duration(duration).attr('r', config.dotRadius);\n d3.selectAll('.chart-tooltip')\n .transition()\n .duration(config.transitionDuration / 2)\n .style('opacity', 0)\n .remove();\n};\n\n/**\n * Draws the chart lines, dots, and sets the mouse pointer events.\n * @param g the svg g element\n * @param x the x axis scale\n * @param y the y axis scale\n * @param config the chart configuration\n * @param data the chart data\n * @param datasetLabels the set of labels for the chart's dataset\n */\nconst drawLinesDotsAndSetPointerEvents = (\n g: d3.Selection<SVGGElement, unknown, HTMLElement, unknown>,\n x: d3.ScaleTime<number, number>,\n y: d3.ScaleLinear<number, number>,\n config: ILineChartOptions,\n data: TLineChartData[],\n datasetLabels: string[],\n) => {\n const line = d3\n .line<ILineChartDataNode>()\n .x(d => x(d.timestamp))\n .y(d => y(d.value))\n .curve(d3.curveMonotoneX);\n\n const flatData = data.flat();\n\n for (let c = 0, maxC = data.length; c < maxC; c += 1) {\n const chunk = data[c];\n\n g.append('path')\n .attr('id', `line-${c}`)\n .style('fill', 'none')\n .style('stroke', config.color(c.toString()))\n .style('stroke-width', '2px')\n .attr('d', line(chunk));\n\n const datasetLabelText = datasetLabels[c];\n const datasetLabel = g.append('g').attr('transform', `translate(0, ${config.height + config.margin.bottom})`);\n\n const markerShiftY = { multiplier: 12, modifier: 4 };\n datasetLabel\n .append('line')\n .attr('id', `legend-line-${c}`)\n .style('fill', 'none')\n .style('stroke', config.color(c.toString()))\n .style('stroke-width', '2px')\n .attr('x1', '6.75em')\n .attr('y1', c * markerShiftY.multiplier - markerShiftY.modifier)\n .attr('x2', '7.25em')\n .attr('y2', c * markerShiftY.multiplier - markerShiftY.modifier);\n\n datasetLabel\n .append('text')\n .style('font-size', '12px')\n .attr('class', 'legend')\n .attr('dx', '10em')\n .attr('dy', `${c}em`)\n .text(datasetLabelText);\n }\n\n g.selectAll('.dot')\n .data(flatData)\n .enter()\n .append('circle')\n .attr('class', 'dot')\n .style('pointer-events', 'all')\n .style('fill', (d, i) => config.color(i.toString()))\n .on('mouseover', function (this, event, d) {\n return onMouseOver(this, d, g, config);\n })\n .on('mouseout', function (this) {\n return onMouseOut(this, config);\n })\n .attr('cx', function (this, d) {\n return x(d.timestamp);\n })\n .attr('cy', function (this, d) {\n return y(d.value);\n })\n .attr('r', 0)\n .transition()\n .ease(d3.easeLinear)\n .duration(config.transitionDuration)\n .delay((d, i) => {\n const multiplier = 50;\n return i * multiplier;\n })\n .attr('r', config.dotRadius);\n};\n\n/**\n * Draws the line chart.\n * @param container the chart container\n * @param data the chart data\n * @param options the chart options\n * @returns the chart configuration\n */\nexport const drawLineChart = (\n container: ElementRef<HTMLDivElement>,\n data: TLineChartData[],\n datasetLabels: string[],\n options?: Partial<ILineChartOptions>,\n) => {\n const config: ILineChartOptions = generateConfiguration<ILineChartOptions>(defaultLineChartConfig, options, {});\n\n const { g } = createContainer(container, config);\n\n const range = data.reduce(\n (accumulator: { minTime: number; maxTime: number; maxValue: number }, arr) => {\n const timestamps = arr.map(item => item.timestamp);\n const minItem = Math.min(...timestamps);\n const maxItem = Math.max(...timestamps);\n const minTime = Math.min(minItem, accumulator.minTime);\n const maxTime = Math.max(maxItem, accumulator.maxTime);\n\n const values = arr.map(item => item.value);\n const maxItemValue = Math.max(...values);\n const maxValue = Math.max(maxItemValue, accumulator.maxValue);\n return { minTime, maxTime, maxValue };\n },\n { minTime: Number(Infinity), maxTime: -Infinity, maxValue: -Infinity },\n );\n\n const x = d3.scaleTime([0, config.width]).domain([range.minTime, range.maxTime]);\n const y = d3.scaleLinear([config.height, 0]).domain([0, range.maxValue ?? 1]);\n\n createAxisX(g, x, config);\n\n createAxisY(g, y, config);\n\n createLegend(g, config);\n\n drawLinesDotsAndSetPointerEvents(g, x, y, config, data, datasetLabels);\n\n return config;\n};\n","import type { ElementRef } from '@angular/core';\nimport * as d3 from 'd3';\n\nimport type { IPieChartDataNode, IPieChartOptions } from '../interfaces/pie-chart.interface';\nimport { generateConfiguration } from './configuration.util';\n\n/**\n * The pie chart default configuration.\n */\nexport const defaultPieChartConfig: IPieChartOptions = Object.freeze({\n chartTitle: '',\n width: 600,\n height: 600,\n margin: {\n top: 20,\n right: 20,\n bottom: 20,\n left: 20,\n },\n innerRadius: 0, // increase inner radius to render a donut chart\n showLabels: true,\n labelRadiusModifier: 50,\n labelTextWrapWidth: 60,\n transitionDuration: 1000,\n color: d3.scaleOrdinal(d3.schemeCategory10),\n} as IPieChartOptions);\n\n/**\n * Creates a container for the pie chart.\n * @param container the chart container\n * @param config the chart configuration\n * @returns the object with the svg element and the g element\n */\nconst createContainer = (container: ElementRef<HTMLDivElement>, config: IPieChartOptions) => {\n const id = container.nativeElement.id ?? 'pie-0';\n\n d3.select(`#${id}`).select('svg').remove();\n const svg = d3\n .select(`#${id}`)\n .append('svg')\n .attr('width', config.width + config.margin.left + config.margin.right)\n .attr('height', config.height + config.margin.top + config.margin.bottom)\n .attr('class', id);\n const g = svg\n .append('g')\n .attr('transform', `translate(${config.width / 2 + config.margin.left},${config.height / 2 + config.margin.top})`);\n\n return { svg, g };\n};\n\n/**\n * Draws the pie chart.\n * @param container the chart container\n * @param data the chart data\n * @param options the chart options\n * @returns the chart configuration\n */\nexport const drawPieChart = (container: ElementRef<HTMLDivElement>, data: IPieChartDataNode[], options?: Partial<IPieChartOptions>) => {\n const config: IPieChartOptions = generateConfiguration<IPieChartOptions>(defaultPieChartConfig, options, {});\n\n const { g } = createContainer(container, config);\n\n const pie = d3.pie<IPieChartDataNode>().value(datum => datum.y);\n\n const radius = Math.min(config.width, config.height) / 2;\n\n const arc = d3.arc<d3.PieArcDatum<IPieChartDataNode>>().innerRadius(config.innerRadius).outerRadius(radius);\n\n const arcs = g\n .selectAll('arc')\n .data(pie(data))\n .enter()\n .append('g')\n .attr('class', 'arc')\n .on('mouseover', function (this, event: MouseEvent, d) {\n this.style.opacity = '0.8';\n\n const tooltipText = `${d.data.key}: ${d.data.y}`;\n\n g.append('text')\n .attr('class', 'chart-tooltip')\n .style('opacity', 0)\n .attr('dx', -config.width / (2 * 2 * 2))\n .attr('dy', config.height / 2 + config.margin.top)\n .text(tooltipText)\n .transition()\n .duration(config.transitionDuration)\n .style('opacity', 1);\n })\n .on('mouseout', function (this, event, d) {\n this.style.opacity = 'unset';\n d3.selectAll('.chart-tooltip')\n .transition()\n .duration(config.transitionDuration / 2)\n .style('opacity', 0)\n .remove();\n });\n\n arcs\n .append('path')\n .attr('fill', (d, i) => config.color(i.toString()))\n .attr('d', arc);\n\n if (config.showLabels) {\n const label = d3\n .arc<d3.PieArcDatum<IPieChartDataNode>>()\n .innerRadius(radius)\n .outerRadius(radius + config.labelRadiusModifier);\n\n const textDy = 5;\n arcs\n .append('text')\n .attr('class', 'legend')\n .attr('text-anchor', 'middle')\n .attr('dy', textDy)\n .attr('transform', d => `translate(${label.centroid(d)})`)\n .style('font-size', '12px')\n .text(d => d.data.y);\n }\n\n return config;\n};\n","import type { ElementRef } from '@angular/core';\nimport * as d3 from 'd3';\n\nimport type { IRadarChartDataNode, IRadarChartOptions, TRadarChartData } from '../interfaces/radar-chart.interface';\nimport { generateConfiguration } from './configuration.util';\n\n/**\n * The radar chart default configuration.\n */\nexport const defaultRadarChartConfig: IRadarChartOptions = Object.freeze({\n chartTitle: '',\n width: 350,\n height: 350,\n margin: {\n top: 50,\n right: 50,\n bottom: 50,\n left: 50,\n },\n levels: 3, // how many levels or inner circles should there be drawn\n maxValue: 0, // what is the value that the biggest circle will represent\n lineFactor: 1.1, // how much farther than the radius of the outer circle should the lines be stretched\n labelFactor: 1.15, // how much farther than the radius of the outer circle should the labels be placed\n labelTextWrapWidth: 60, // the number of pixels after which a label needs to be given a new line\n opacityArea: 0.35, // the opacity of the area of the blob\n dotRadius: 4, // the size of the colored circles of each blog\n opacityCircles: 0.1, // the opacity of the circles of each blob\n strokeWidth: 2, // the width of the stroke around each blob\n roundStrokes: false, // if true the area and stroke will follow a round path (cardinal-closed)\n transitionDuration: 200,\n color: d3.scaleOrdinal(d3.schemeCategory10),\n} as IRadarChartOptions);\n\n/**\n * Creates a container for the radar chart.\n * @param container the