diff --git a/.gitignore b/.gitignore index 026fc4e25..c34747f20 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.tmp *.log .DS_Store +junit.xml stats.json .idea/ .vscode/ diff --git a/docs/scriptappy.json b/docs/scriptappy.json index 424f49809..a82bfa7c9 100644 --- a/docs/scriptappy.json +++ b/docs/scriptappy.json @@ -148,6 +148,23 @@ } }, "definitions": { + "ArcSettings": { + "kind": "object", + "entries": { + "startAngle": { + "description": "Start of arc line, in radians", + "type": "number" + }, + "endAngle": { + "description": "End of arc line, in radians", + "type": "number" + }, + "radius": { + "description": "Radius of arc line", + "type": "number" + } + } + }, "Brush": { "description": "A brush context", "kind": "interface", @@ -1675,6 +1692,11 @@ "description": "Continuous axis settings", "kind": "object", "entries": { + "arc": { + "description": "Optional arc settings", + "optional": true, + "type": "#/definitions/ArcSettings" + }, "align": { "description": "Set the anchoring point of the axis. Available options are `auto/left/right/bottom/top`. In `auto` the axis determines the best option. The options are restricted based on the axis orientation, a vertical axis may only anchor on `left` or `right`", "optional": true, diff --git a/junit.xml b/junit.xml new file mode 100644 index 000000000..3b83516d4 --- /dev/null +++ b/junit.xml @@ -0,0 +1,5339 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/picasso.js/src/core/chart-components/axis/__tests__/axis-arc-label-node.spec.js b/packages/picasso.js/src/core/chart-components/axis/__tests__/axis-arc-label-node.spec.js new file mode 100644 index 000000000..a07db5fd5 --- /dev/null +++ b/packages/picasso.js/src/core/chart-components/axis/__tests__/axis-arc-label-node.spec.js @@ -0,0 +1,113 @@ +import { textBounds } from '../../../../web/text-manipulation'; +import buildArcLabels from '../axis-arc-label-node'; + +function createTick(position, label) { + return { + position, + label, + value: 1.23, + }; +} + +describe('Axis Arc Label Node', () => { + const innerRect = { + x: 0, + y: 0, + width: 0, + height: 0, + }; + const outerRect = { + x: 0, + y: 0, + width: 0, + height: 0, + }; + const textRect = { height: 16, width: 18 }; + const measureTextMock = ({ text }) => ({ width: text.length, height: 1 }); + + beforeEach(() => { + innerRect.width = 400; + innerRect.height = 425; + innerRect.x = 0; + innerRect.y = 425; + outerRect.width = 400; + outerRect.height = 425; + outerRect.x = 0; + outerRect.y = 0; + }); + + describe('Label', () => { + let buildOpts, tick; + + beforeEach(() => { + buildOpts = { + align: 'top', + radius: 0.85, + startAngle: (-2 * Math.PI) / 3, + endAngle: (2 * Math.PI) / 3, + innerRect, + outerRect, + padding: 3.5, + paddingEnd: 10, + stepSize: 0, + style: { + align: 0.5, + fontFamily: 'Arial', + fontSize: '12px', + margin: -5, + }, + textBounds: (node) => textBounds(node, measureTextMock), + textRect, + tickSize: 6, + }; + tick = createTick(0); + }); + + describe('Style align', () => { + it('the label should be placed on the left side of the tick', () => { + buildOpts.align = 'top'; + tick = createTick(1, '0'); + const result = buildArcLabels(tick, buildOpts); + expect(result.x).to.be.closeTo(40, 2); + expect(result.y).to.be.closeTo(305, 2); + expect(result.anchor).to.equal('end'); + }); + + it('label with different position, but should still be placed in the left side of the tick', () => { + buildOpts.align = 'top'; + tick = createTick(0.7, '100'); + const result = buildArcLabels(tick, buildOpts); + expect(result.x).to.be.closeTo(62, 2); + expect(result.y).to.be.closeTo(88, 2); + expect(result.anchor).to.equal('end'); + }); + + it('the label should be placed on the right side of the tick', () => { + buildOpts.align = 'top'; + tick = createTick(0.4, '250'); + const result = buildArcLabels(tick, buildOpts); + expect(result.x).to.be.closeTo(275, 2); + expect(result.y).to.be.closeTo(43, 2); + expect(result.anchor).to.equal('start'); + }); + it('the label should be centered on the tick', () => { + buildOpts.align = 'top'; + buildOpts.innerRect = { width: 213, height: 595 }; + tick = createTick(0.5, '300'); + const result = buildArcLabels(tick, buildOpts); + expect(result.x).to.be.closeTo(106.5, 2); + expect(result.y).to.be.closeTo(191, 2); + expect(result.anchor).to.equal('middle'); + }); + }); + + describe('Not text in tick label-property', () => { + it('tick.label is undefined', () => { + buildOpts.align = 'top'; + tick = createTick(0, undefined); + const result = buildArcLabels(tick, buildOpts); + expect(result.text).to.equal('-'); + }); + }); + }); +}); diff --git a/packages/picasso.js/src/core/chart-components/axis/__tests__/axis-arc-node.spec.js b/packages/picasso.js/src/core/chart-components/axis/__tests__/axis-arc-node.spec.js new file mode 100644 index 000000000..74f650a82 --- /dev/null +++ b/packages/picasso.js/src/core/chart-components/axis/__tests__/axis-arc-node.spec.js @@ -0,0 +1,71 @@ +import buildArcLine from '../axis-arc-node'; + +describe('Axis Arc Line Node', () => { + const innerRect = { + x: 0, + y: 0, + width: 0, + height: 0, + }; + const outerRect = { + x: 0, + y: 0, + width: 0, + height: 0, + }; + + describe('Arc', () => { + let buildOpts, expected; + + beforeEach(() => { + innerRect.width = 50; + innerRect.height = 100; + innerRect.x = 0; + innerRect.y = 0; + outerRect.width = 50; + outerRect.height = 100; + outerRect.x = 0; + outerRect.y = 0; + + buildOpts = { + style: { stroke: 'red', strokeWidth: 1 }, + align: 'bottom', + innerRect, + outerRect, + padding: 10, + startAngle: -Math.PI / 3, + endAngle: Math.PI / 3, + radius: 0.5, + }; + expected = { + visible: true, + type: 'path', + arcDatum: { startAngle: 0, endAngle: 0 }, + transform: `translate(0, 0) translate(${0}, ${0})`, + desc: { + share: 1, + slice: { + cornerRadius: 0, + innerRadius: 0, + outerRadius: 0, + }, + }, + stroke: 'red', + strokeWidth: 1, + ticks: [], + }; + }); + + it('Structure Properties', () => { + const rect = buildOpts.innerRect; + const centerPoint = { cx: rect.width / 2, cy: rect.height / 2 }; + const halfPlotSize = Math.min(rect.height, rect.width) / 2; + expected.transform = `translate(0, 0) translate(${centerPoint.cx}, ${centerPoint.cy})`; + expected.arcDatum.startAngle = buildOpts.startAngle; + expected.arcDatum.endAngle = buildOpts.endAngle; + expected.desc.slice.innerRadius = buildOpts.radius * halfPlotSize; + expected.desc.slice.outerRadius = buildOpts.radius * halfPlotSize + buildOpts.style.strokeWidth; + expect(buildArcLine(buildOpts)).to.deep.equal(expected); + }); + }); +}); diff --git a/packages/picasso.js/src/core/chart-components/axis/__tests__/axis-arc-tick-node.spec.js b/packages/picasso.js/src/core/chart-components/axis/__tests__/axis-arc-tick-node.spec.js new file mode 100644 index 000000000..d636fd886 --- /dev/null +++ b/packages/picasso.js/src/core/chart-components/axis/__tests__/axis-arc-tick-node.spec.js @@ -0,0 +1,87 @@ +import buildArcTicks from '../axis-arc-tick-node'; + +function createTick(position, value) { + return { + position, + value, + }; +} + +describe('Axis Arc Tick Node', () => { + const innerRect = { + x: 0, + y: 0, + width: 0, + height: 0, + }; + const outerRect = { + x: 0, + y: 0, + width: 0, + height: 0, + }; + describe('Tick', () => { + let buildOpts, tick, tickValue; + beforeEach(() => { + innerRect.width = 400; + innerRect.height = 417; + innerRect.x = 0; + innerRect.y = 417; + outerRect.width = 400; + outerRect.height = 417; + outerRect.x = 0; + outerRect.y = 0; + buildOpts = { + align: 'top', + radius: 0.85, + startAngle: (-2 * Math.PI) / 3, + endAngle: (2 * Math.PI) / 3, + innerRect, + outerRect, + padding: 2.5, + paddingEnd: 10, + stepSize: 0, + style: { + fontFamily: "'Source Sans Pro', Arial, sans-serif", + fontSize: '12px', + margin: 2, + strokeWidth: 1, + stroke: '#cdcdcd', + tickSize: 6, + }, + tickSize: 6, + }; + }); + it('Start Tick', () => { + tickValue = 0; + tick = createTick(1, tickValue); + const result = buildArcTicks(tick, buildOpts); + expect(result.x1).to.be.closeTo(45, 1); + expect(result.x2).to.be.closeTo(51, 1); + expect(result.y1).to.be.closeTo(298, 1); + expect(result.y2).to.be.closeTo(295, 1); + expect(result.value).to.equal(tickValue); + }); + it('End Tick', () => { + tickValue = 500; + tick = createTick(0, tickValue); + const result = buildArcTicks(tick, buildOpts); + expect(result.x1).to.be.closeTo(355, 1); + expect(result.x2).to.be.closeTo(349, 1); + expect(result.y1).to.be.closeTo(298, 1); + expect(result.y2).to.be.closeTo(295, 1); + expect(result.value).to.equal(tickValue); + }); + it('Middle Tick', () => { + tickValue = 250; + tick = createTick(0.5, tickValue); + buildOpts.innerRect = { width: 232, height: 390 }; + const result = buildArcTicks(tick, buildOpts); + expect(result.x1).to.be.closeTo(115.5, 1); + expect(result.x2).to.be.closeTo(115.5, 1); + expect(result.y1).to.be.closeTo(87.5, 1); + expect(result.y2).to.be.closeTo(93.5, 1); + expect(result.value).to.equal(tickValue); + }); + }); +}); diff --git a/packages/picasso.js/src/core/chart-components/axis/__tests__/axis.spec.js b/packages/picasso.js/src/core/chart-components/axis/__tests__/axis.spec.js index 21ebd243f..00bc84247 100644 --- a/packages/picasso.js/src/core/chart-components/axis/__tests__/axis.spec.js +++ b/packages/picasso.js/src/core/chart-components/axis/__tests__/axis.spec.js @@ -283,7 +283,68 @@ describe('Axis', () => { }); }); }); + describe('continuous arc axis', () => { + beforeEach(() => { + scale = linear(); + chart.scale.returns(scale); + opts = { + inner: { + x: 0, + y: 425, + width: 400, + height: 425, + }, + outer: { + x: 0, + y: 0, + width: 400, + height: 425, + }, + }; + config.settings.arc = { + radius: 0.5, + startAngle: (-2 * Math.PI) / 3, + endAngle: (2 * Math.PI) / 3, + }; + }); + it('should render arc axis', () => { + config.settings.labels = { show: true }; + config.settings.line = { show: true }; + componentFixture.simulateCreate(axisComponent, config); + componentFixture.simulateRender(opts); + verifyNumberOfNodes('text', 1); + verifyNumberOfNodes('line', 1); + }); + it('should not render arc axis labels when disabled', () => { + config.settings.labels = { show: false }; + config.settings.arc = { + radius: 0.5, + startAngle: (-2 * Math.PI) / 3, + endAngle: (2 * Math.PI) / 3, + }; + componentFixture.simulateCreate(axisComponent, config); + componentFixture.simulateRender(opts); + + verifyNumberOfNodes('text', 0); + verifyNumberOfNodes('line', 6); + }); + + it('should not render arc axis line when disabled', () => { + config.settings.labels = { show: true }; + config.settings.line = { show: false }; + config.settings.arc = { + radius: 0.5, + startAngle: (-2 * Math.PI) / 3, + endAngle: (2 * Math.PI) / 3, + }; + componentFixture.simulateCreate(axisComponent, config); + componentFixture.simulateRender(opts); + + verifyNumberOfNodes('text', 6); + verifyNumberOfNodes('line', 6); + }); + }); describe('continuous', () => { beforeEach(() => { scale = linear(); diff --git a/packages/picasso.js/src/core/chart-components/axis/axis-arc-label-node.js b/packages/picasso.js/src/core/chart-components/axis/axis-arc-label-node.js new file mode 100644 index 000000000..4906402f2 --- /dev/null +++ b/packages/picasso.js/src/core/chart-components/axis/axis-arc-label-node.js @@ -0,0 +1,101 @@ +function appendStyle(struct, buildOpts) { + ['fill', 'fontSize', 'fontFamily'].forEach((style) => { + struct[style] = buildOpts.style[style]; + }); +} + +function polarToCartesian(centerX, centerY, radius, angle) { + return { + x: centerX + radius * Math.cos(angle), + y: centerY + radius * Math.sin(angle), + }; +} + +function checkText(text) { + return typeof text === 'string' || typeof text === 'number' ? text : '-'; +} + +function calculateMaxWidth(buildOpts, side, innerPos) { + let maxWidth; + if (side === 'left') { + maxWidth = innerPos.x; + } else if (side === 'right') { + maxWidth = buildOpts.outerRect.width - innerPos.x; + } else { + maxWidth = Math.max(innerPos.x, buildOpts.outerRect.width - innerPos.x); + } + return maxWidth; +} + +function checkRadialOutOfBounds(buildOpts, innerPos, struct) { + let maxHeightTop; + let maxHeightBottom; + const textHeight = parseFloat(struct.fontSize) || 0; + maxHeightTop = innerPos.y - textHeight / 2; + maxHeightBottom = buildOpts.innerRect.height - (innerPos.y + textHeight / 2); + if (maxHeightTop < 0 || maxHeightBottom < 0) { + struct.text = ''; + } + return maxHeightBottom; +} + +function appendBounds(struct, buildOpts) { + struct.boundingRect = buildOpts.textBounds(struct); +} + +export default function buildArcLabels(tick, buildOpts) { + const rect = buildOpts.innerRect; + const centerPoint = { cx: rect.width / 2, cy: rect.height / 2 }; + const halfPlotSize = Math.min(rect.height, rect.width) / 2; + const innerRadius = halfPlotSize * buildOpts.radius; + const outerRadius = innerRadius + buildOpts.padding; + const startAngle = buildOpts.startAngle; + const endAngle = buildOpts.endAngle; + const tickLength = buildOpts.tickSize; + const angleRange = endAngle - startAngle; + const centerOffset = 0.2; + + let angle; + let side; + + if (buildOpts.align === 'top' || buildOpts.align === 'bottom') { + angle = endAngle - tick.position * angleRange; + } else { + angle = startAngle + tick.position * angleRange; + } + if (angle < 0 - centerOffset && angle > -Math.PI + centerOffset) { + side = 'left'; + } else if (angle > 0 + centerOffset && angle < Math.PI - centerOffset) { + side = 'right'; + } else { + side = 'center'; + } + angle -= Math.PI / 2; + const padding = 6; + const innerPos = polarToCartesian(centerPoint.cx, centerPoint.cy, outerRadius + tickLength + padding, angle); + let textAnchor; + if (side === 'left') { + textAnchor = 'end'; // Align text to the right of the x-coordinate + } else if (side === 'right') { + textAnchor = 'start'; // Align text to the left of the x-coordinate + } else { + textAnchor = 'middle'; // Center align the text + } + + const struct = { + type: 'text', + text: checkText(tick.label), + align: side, + x: innerPos.x, + y: innerPos.y, + maxHeight: buildOpts.maxHeight, + maxWidth: calculateMaxWidth(buildOpts, side, innerPos), + anchor: textAnchor, + baseline: 'middle', + }; + + appendStyle(struct, buildOpts); + appendBounds(struct, buildOpts); + checkRadialOutOfBounds(buildOpts, innerPos, struct); + return struct; +} diff --git a/packages/picasso.js/src/core/chart-components/axis/axis-arc-node.js b/packages/picasso.js/src/core/chart-components/axis/axis-arc-node.js new file mode 100644 index 000000000..36363549a --- /dev/null +++ b/packages/picasso.js/src/core/chart-components/axis/axis-arc-node.js @@ -0,0 +1,33 @@ +import extend from 'extend'; + +function appendStyle(struct, buildOpts) { + extend(struct, buildOpts.style); +} + +export default function buildArcLine(buildOpts) { + const rect = buildOpts.innerRect; + const centerPoint = { cx: rect.width / 2, cy: rect.height / 2 }; + const halfPlotSize = Math.min(rect.height, rect.width) / 2; + const innerRadius = halfPlotSize * buildOpts.radius; + const outerRadius = innerRadius + buildOpts.style.strokeWidth; + const startAngle = buildOpts.startAngle; + const endAngle = buildOpts.endAngle; + + const struct = { + visible: true, + type: 'path', + arcDatum: { startAngle, endAngle }, + transform: `translate(0, 0) translate(${centerPoint.cx}, ${centerPoint.cy})`, + desc: { + share: 1, + slice: { + cornerRadius: 0, + innerRadius, + outerRadius, + }, + }, + ticks: [], + }; + appendStyle(struct, buildOpts); + return struct; +} diff --git a/packages/picasso.js/src/core/chart-components/axis/axis-arc-tick-node.js b/packages/picasso.js/src/core/chart-components/axis/axis-arc-tick-node.js new file mode 100644 index 000000000..c4f389913 --- /dev/null +++ b/packages/picasso.js/src/core/chart-components/axis/axis-arc-tick-node.js @@ -0,0 +1,42 @@ +import extend from 'extend'; + +function appendStyle(struct, buildOpts) { + const styleClone = { ...buildOpts.style }; // Shallow clone + extend(struct, styleClone); +} + +function polarToCartesian(centerX, centerY, radius, angle) { + return { + x: centerX + radius * Math.cos(angle), + y: centerY + radius * Math.sin(angle), + }; +} + +export default function buildArcTicks(tick, buildOpts) { + const rect = buildOpts.innerRect; + const centerPoint = { cx: rect.width / 2, cy: rect.height / 2 }; + const halfPlotSize = Math.min(rect.height, rect.width) / 2; + const innerRadius = halfPlotSize * buildOpts.radius; + const outerRadius = innerRadius + buildOpts.padding; + const startAngle = buildOpts.startAngle; + const endAngle = buildOpts.endAngle; + const tickLength = buildOpts.tickSize; + const angleRange = endAngle - startAngle; + + let angle = endAngle - tick.position * angleRange; + + angle -= Math.PI / 2; + const innerPos = polarToCartesian(centerPoint.cx, centerPoint.cy, outerRadius + tickLength, angle); + const outerPos = polarToCartesian(centerPoint.cx, centerPoint.cy, outerRadius, angle); + + const struct = { + type: 'line', + x1: innerPos.x, + y1: innerPos.y, + x2: outerPos.x, + y2: outerPos.y, + value: tick.value, + }; + appendStyle(struct, buildOpts); + return struct; +} diff --git a/packages/picasso.js/src/core/chart-components/axis/axis-default-settings.js b/packages/picasso.js/src/core/chart-components/axis/axis-default-settings.js index 13c2bfe1b..4bbf2ce83 100644 --- a/packages/picasso.js/src/core/chart-components/axis/axis-default-settings.js +++ b/packages/picasso.js/src/core/chart-components/axis/axis-default-settings.js @@ -118,10 +118,18 @@ const DEFAULT_DISCRETE_SETTINGS = { align: 'auto', }; +/** + * @typedef {object} ArcSettings + * @property {number} startAngle - Start of arc line, in radians + * @property {number} endAngle - End of arc line, in radians + * @property {number} radius - Radius of arc line + */ + /** * Continuous axis settings * @typedef {object} * @alias ComponentAxis~ContinuousSettings + * @property {ArcSettings=} arc - Optional arc settings * @example * { * type: 'axis', diff --git a/packages/picasso.js/src/core/chart-components/axis/axis-node-builder.js b/packages/picasso.js/src/core/chart-components/axis/axis-node-builder.js index 67c36b5bb..287173c99 100644 --- a/packages/picasso.js/src/core/chart-components/axis/axis-node-builder.js +++ b/packages/picasso.js/src/core/chart-components/axis/axis-node-builder.js @@ -5,6 +5,9 @@ import { testRectRect } from '../../math/narrow-phase-collision'; import { getClampedValue } from './axis-label-size'; import getHorizontalContinuousWidth from './get-continuous-label-rect'; import { expandRect } from '../../geometry/util'; +import buildArcLine from './axis-arc-node'; +import buildArcTicks from './axis-arc-tick-node'; +import buildArcLabels from './axis-arc-label-node'; function tickSpacing(settings) { let spacing = 0; @@ -13,6 +16,12 @@ function tickSpacing(settings) { spacing += settings.ticks.show ? settings.ticks.margin : 0; return spacing; } +function arcTickSpacing(settings) { + let spacing = 0; + spacing += settings.line.show ? settings.line.strokeWidth / 2 : 0; + spacing += settings.ticks.show ? settings.ticks.margin : 0; + return spacing; +} function tickMinorSpacing(settings) { return settings.line.strokeWidth + settings.minorTicks.margin; @@ -24,6 +33,12 @@ function labelsSpacing(settings) { spacing += tickSpacing(settings) + settings.labels.margin; return spacing; } +function arcLabelSpacing(settings) { + let spacing = 0; + spacing += settings.ticks.show ? settings.ticks.tickSize : 0; + spacing += arcTickSpacing(settings) + settings.labels.margin; + return spacing; +} function calcActualTextRect({ style, measureText, tick }) { return measureText({ @@ -45,6 +60,10 @@ function tickBuilder(ticks, buildOpts) { return ticks.map((tick) => buildTick(tick, buildOpts)); } +function arcTickBuilder(ticks, buildOpts) { + return ticks.map((tick) => buildArcTicks(tick, buildOpts)); +} + function tickBandwidth(scale, tick) { return tick ? Math.abs(tick.end - tick.start) : scale.bandwidth(); } @@ -58,6 +77,15 @@ function labelBuilder(ticks, buildOpts, resolveTickOpts) { }); } +function arcLabelBuilder(ticks, buildOpts, resolveTickOpts) { + return ticks.map((tick, idx) => { + resolveTickOpts(tick, idx); + const label = buildArcLabels(tick, buildOpts); + label.data = tick.data; + return label; + }); +} + function layeredLabelBuilder(ticks, buildOpts, settings, resolveTickOpts) { const padding = buildOpts.padding; const spacing = labelsSpacing(settings); @@ -213,19 +241,30 @@ export default function nodeBuilder(isDiscrete) { const tilted = state.labels.activeMode === 'tilted'; const layered = state.labels.activeMode === 'layered'; let majorTickNodes; - + if (settings.arc) { + buildOpts.startAngle = settings.arc.startAngle; + buildOpts.endAngle = settings.arc.endAngle; + buildOpts.radius = settings.arc.radius; + } if (settings.line.show) { buildOpts.style = settings.line; buildOpts.padding = settings.paddingStart; - - nodes.push(buildLine(buildOpts)); + if (settings.arc) { + nodes.push(buildArcLine(buildOpts)); + } else { + nodes.push(buildLine(buildOpts)); + } } if (settings.ticks.show) { buildOpts.style = settings.ticks; buildOpts.tickSize = settings.ticks.tickSize; buildOpts.padding = tickSpacing(settings); - - majorTickNodes = tickBuilder(major, buildOpts); + if (settings.arc) { + buildOpts.padding = arcTickSpacing(settings); + majorTickNodes = arcTickBuilder(major, buildOpts); + } else { + majorTickNodes = tickBuilder(major, buildOpts); + } } if (settings.labels.show) { const padding = labelsSpacing(settings); @@ -260,9 +299,11 @@ export default function nodeBuilder(isDiscrete) { tick, }); }; - let labelNodes = []; - if (layered && (settings.align === 'top' || settings.align === 'bottom')) { + if (settings.arc) { + buildOpts.padding = arcLabelSpacing(settings); + labelNodes = arcLabelBuilder(major, buildOpts, resolveTickOpts); + } else if (layered && (settings.align === 'top' || settings.align === 'bottom')) { labelNodes = layeredLabelBuilder(major, buildOpts, settings, resolveTickOpts); } else { labelNodes = labelBuilder(major, buildOpts, resolveTickOpts); @@ -280,7 +321,6 @@ export default function nodeBuilder(isDiscrete) { buildOpts.style = settings.minorTicks; buildOpts.tickSize = settings.minorTicks.tickSize; buildOpts.padding = tickMinorSpacing(settings); - nodes.push(...tickBuilder(minor, buildOpts)); } diff --git a/packages/picasso.js/types/index.d.ts b/packages/picasso.js/types/index.d.ts index e8b9f4e2f..975c75806 100644 --- a/packages/picasso.js/types/index.d.ts +++ b/packages/picasso.js/types/index.d.ts @@ -67,6 +67,12 @@ declare namespace picassojs { } declare namespace picassojs { + type ArcSettings = { + startAngle: number; + endAngle: number; + radius: number; + }; + /** * A brush context */ @@ -552,6 +558,7 @@ declare namespace picassojs { namespace ComponentAxis { type ContinuousSettings = { + arc?: picassojs.ArcSettings; align?: string; labels: { align?: number;