-
Notifications
You must be signed in to change notification settings - Fork 121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(bullet): the tooltip shows up around the drawn part of the chart only #1278
Changes from all commits
81e2a81
7b2354a
b6f2804
2bfffa6
0d974db
e349071
48ba446
3dbbf20
6ac9de9
59fdc8b
97e78cd
54564fb
b15dd57
b2da302
59038ab
aa80f8a
92e819f
163928f
1923805
a54735c
13c01f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,8 +6,8 @@ | |
* Side Public License, v 1. | ||
*/ | ||
|
||
import { GOLDEN_RATIO } from '../../../../common/constants'; | ||
import { PointObject } from '../../../../common/geometry'; | ||
import { GOLDEN_RATIO, TAU } from '../../../../common/constants'; | ||
import { PointObject, Radian, Rectangle } from '../../../../common/geometry'; | ||
import { cssFontShorthand, Font } from '../../../../common/text_utils'; | ||
import { GoalSubtype } from '../../specs/constants'; | ||
import { Config } from '../types/config_types'; | ||
|
@@ -22,9 +22,12 @@ const marginRatio = 0.05; // same ratio on each side | |
const maxTickFontSize = 24; | ||
const maxLabelFontSize = 32; | ||
const maxCentralFontSize = 38; | ||
const arcBoxSamplePitch: Radian = (5 / 360) * TAU; // 5-degree pitch ie. a circle is 72 steps | ||
const capturePad = 16; // mouse hover is detected in the padding too; eg. for Fitts law | ||
|
||
/** @internal */ | ||
export interface Mark { | ||
boundingBoxes: (ctx: CanvasRenderingContext2D) => Rectangle[]; | ||
render: (ctx: CanvasRenderingContext2D) => void; | ||
} | ||
|
||
|
@@ -46,6 +49,22 @@ export class Section implements Mark { | |
this.strokeStyle = strokeStyle; | ||
} | ||
|
||
boundingBoxes() { | ||
// modifying with half the line width is a simple yet imprecise method for ensuring that the | ||
// entire ink is in the bounding box; depending on orientation and line ending, the bounding | ||
// box may overstate the data ink bounding box, which is preferable to understating it | ||
return this.lineWidth === 0 | ||
? [] | ||
: [ | ||
{ | ||
x0: Math.min(this.x, this.xTo) - this.lineWidth / 2 - capturePad, | ||
y0: Math.min(this.y, this.yTo) - this.lineWidth / 2 - capturePad, | ||
x1: Math.max(this.x, this.xTo) + this.lineWidth / 2 + capturePad, | ||
y1: Math.max(this.y, this.yTo) + this.lineWidth / 2 + capturePad, | ||
}, | ||
]; | ||
} | ||
|
||
render(ctx: CanvasRenderingContext2D) { | ||
ctx.beginPath(); | ||
ctx.lineWidth = this.lineWidth; | ||
|
@@ -56,13 +75,16 @@ export class Section implements Mark { | |
} | ||
} | ||
|
||
/** @internal */ | ||
export const initialBoundingBox = (): Rectangle => ({ x0: Infinity, y0: Infinity, x1: -Infinity, y1: -Infinity }); | ||
|
||
/** @internal */ | ||
export class Arc implements Mark { | ||
protected readonly x: number; | ||
protected readonly y: number; | ||
protected readonly radius: number; | ||
protected readonly startAngle: number; | ||
protected readonly endAngle: number; | ||
protected readonly startAngle: Radian; | ||
protected readonly endAngle: Radian; | ||
protected readonly anticlockwise: boolean; | ||
protected readonly lineWidth: number; | ||
protected readonly strokeStyle: string; | ||
|
@@ -87,6 +109,49 @@ export class Arc implements Mark { | |
this.strokeStyle = strokeStyle; | ||
} | ||
|
||
boundingBoxes() { | ||
if (this.lineWidth === 0) return []; | ||
|
||
const box = initialBoundingBox(); | ||
|
||
// instead of an analytical solution, we approximate with a GC-free grid sampler | ||
|
||
// full circle rotations such that `startAngle' and `endAngle` are positive | ||
const rotationCount = Math.ceil(Math.max(0, -this.startAngle, -this.endAngle) / TAU); | ||
const startAngle = this.startAngle + rotationCount * TAU; | ||
const endAngle = this.endAngle + rotationCount * TAU; | ||
|
||
// snapping to the closest `arcBoxSamplePitch` increment | ||
const angleFrom: Radian = Math.round(startAngle / arcBoxSamplePitch) * arcBoxSamplePitch; | ||
const angleTo: Radian = Math.round(endAngle / arcBoxSamplePitch) * arcBoxSamplePitch; | ||
const signedIncrement = arcBoxSamplePitch * Math.sign(angleTo - angleFrom); | ||
|
||
for (let angle: Radian = angleFrom; angle <= angleTo; angle += signedIncrement) { | ||
// unit vector for the angle direction | ||
const vx = Math.cos(angle); | ||
const vy = Math.sin(angle); | ||
const innerRadius = this.radius - this.lineWidth / 2; | ||
const outerRadius = this.radius + this.lineWidth / 2; | ||
|
||
// inner point of the sector | ||
const innerX = this.x + vx * innerRadius; | ||
const innerY = this.y + vy * innerRadius; | ||
|
||
// outer point of the sector | ||
const outerX = this.x + vx * outerRadius; | ||
const outerY = this.y + vy * outerRadius; | ||
|
||
box.x0 = Math.min(box.x0, innerX - capturePad, outerX - capturePad); | ||
box.y0 = Math.min(box.y0, innerY - capturePad, outerY - capturePad); | ||
box.x1 = Math.max(box.x1, innerX + capturePad, outerX + capturePad); | ||
box.y1 = Math.max(box.y1, innerY + capturePad, outerY + capturePad); | ||
|
||
if (signedIncrement === 0) break; // happens if fromAngle === toAngle | ||
} | ||
|
||
return Number.isFinite(box.x0) ? [box] : []; | ||
} | ||
|
||
render(ctx: CanvasRenderingContext2D) { | ||
ctx.beginPath(); | ||
ctx.lineWidth = this.lineWidth; | ||
|
@@ -124,11 +189,30 @@ export class Text implements Mark { | |
this.fontSize = fontSize; | ||
} | ||
|
||
render(ctx: CanvasRenderingContext2D) { | ||
ctx.beginPath(); | ||
setCanvasTextState(ctx: CanvasRenderingContext2D) { | ||
ctx.textAlign = this.textAlign; | ||
ctx.textBaseline = this.textBaseline; | ||
ctx.font = cssFontShorthand(this.fontShape, this.fontSize); | ||
} | ||
|
||
boundingBoxes(ctx: CanvasRenderingContext2D) { | ||
if (this.text.length === 0) return []; | ||
|
||
this.setCanvasTextState(ctx); | ||
const box = ctx.measureText(this.text); | ||
Comment on lines
+201
to
+202
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we reuse There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought about it, but these bounding box methods are mirrors of the actual rendering code ( It'd be a good idea to eventually migrate bullet graphs to that though: the user would specify the bounding box into which the bullet/goal titles must fit. It'd be a larger, API-involving task. Also, then the bounding box would be given, because that's what the user wants to fill. If you meant |
||
return [ | ||
{ | ||
x0: -box.actualBoundingBoxLeft + this.x - capturePad, | ||
y0: -box.actualBoundingBoxAscent + this.y - capturePad, | ||
x1: box.actualBoundingBoxRight + this.x + capturePad, | ||
y1: box.actualBoundingBoxDescent + this.y + capturePad, | ||
}, | ||
]; | ||
} | ||
|
||
render(ctx: CanvasRenderingContext2D) { | ||
this.setCanvasTextState(ctx); | ||
ctx.beginPath(); | ||
ctx.fillText(this.text, this.x, this.y); | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍🏼