Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
M
Metabase
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Iterations
Wiki
Requirements
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Locked files
Build
Pipelines
Jobs
Pipeline schedules
Test cases
Artifacts
Deploy
Releases
Package registry
Container Registry
Model registry
Operate
Environments
Terraform modules
Monitor
Incidents
Service Desk
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Code review analytics
Issue analytics
Insights
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Terms and privacy
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
Engineering Digital Service
Metabase
Commits
4d4ae44e
Commit
4d4ae44e
authored
6 years ago
by
Tom Robinson
Browse files
Options
Downloads
Patches
Plain Diff
Gauge: Various improvments and refactoring
parent
378cf72d
No related branches found
Branches containing commit
No related tags found
Tags containing commit
No related merge requests found
Changes
1
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
frontend/src/metabase/visualizations/visualizations/Gauge.jsx
+153
-74
153 additions, 74 deletions
...tend/src/metabase/visualizations/visualizations/Gauge.jsx
with
153 additions
and
74 deletions
frontend/src/metabase/visualizations/visualizations/Gauge.jsx
+
153
−
74
View file @
4d4ae44e
/* @flow */
import
React
,
{
Component
}
from
"
react
"
;
import
ReactDOM
from
"
react-dom
"
;
import
{
t
}
from
"
c-3po
"
;
import
d3
from
"
d3
"
;
import
cx
from
"
classnames
"
;
import
Scalar
from
"
./Scalar
"
;
import
colors
from
"
metabase/lib/colors
"
;
import
{
formatValue
}
from
"
metabase/lib/formatting
"
;
...
...
@@ -15,16 +14,36 @@ import ChartSettingRange from "metabase/visualizations/components/settings/Chart
import
type
{
VisualizationProps
}
from
"
metabase/meta/types/Visualization
"
;
const
OUTER_RADIUS
=
45
;
// within 100px
canvas
const
OUTER_RADIUS
=
45
;
// within 100px
SVG element
const
INNER_RADIUS_RATIO
=
4
/
5
;
const
INNER_RADIUS
=
OUTER_RADIUS
*
INNER_RADIUS_RATIO
;
const
ARROW_HEIGHT
=
(
OUTER_RADIUS
-
INNER_RADIUS
)
*
2
/
3
;
const
ARROW_BASE
=
ARROW_HEIGHT
/
Math
.
tan
(
60
/
180
*
Math
.
PI
);
// equilateral triangle
const
ARROW_THICKNESS
=
1.5
;
// arrow shape, currently an equilateral triangle
const
ARROW_HEIGHT
=
(
OUTER_RADIUS
-
INNER_RADIUS
)
*
3
/
4
;
// 2/3 of segment thickness
const
ARROW_BASE
=
ARROW_HEIGHT
/
Math
.
tan
(
60
/
180
*
Math
.
PI
);
const
ARROW_STROKE_THICKNESS
=
1.25
;
// colors
const
BACKGROUND_ARC_COLOR
=
colors
[
"
bg-medium
"
];
const
SEGMENT_LABEL_COLOR
=
colors
[
"
text-dark
"
];
const
CENTER_LABEL_COLOR
=
colors
[
"
text-dark
"
];
const
ARROW_FILL_COLOR
=
colors
[
"
text-dark
"
];
const
ARROW_STROKE_COLOR
=
"
white
"
;
// in ems, but within the scaled 100px SVG element
const
FONT_SIZE_SEGMENT_LABEL
=
0.15
;
const
FONT_SIZE_CENTER_LABEL_MIN
=
0.5
;
const
FONT_SIZE_CENTER_LABEL_MAX
=
1.25
;
// hide labels if SVG width is smaller than this
const
MIN_WIDTH_LABEL_THRESHOLD
=
400
;
// total degrees of the arc (180 = semicircle, etc)
const
ARC_DEGREES
=
180
+
45
*
2
;
// semicircle plus a bit
const
radians
=
degrees
=>
degrees
*
Math
.
PI
/
180
;
const
degrees
=
radians
=>
radians
*
180
/
Math
.
PI
;
export
default
class
Gauge
extends
Component
{
props
:
VisualizationProps
;
...
...
@@ -73,6 +92,36 @@ export default class Gauge extends Component {
componentDidMount
()
{
this
.
setState
({
mounted
:
true
});
this
.
_updateLabelSize
();
}
componentDidUpdate
()
{
this
.
_updateLabelSize
();
}
_updateLabelSize
()
{
// TODO: extract this into a component that resizes SVG <text> element to fit bounds
const
label
=
ReactDOM
.
findDOMNode
(
this
.
_label
);
if
(
label
)
{
const
{
width
:
currentWidth
}
=
label
.
getBBox
();
// maxWidth currently 95% of inner diameter, could be more intelligent based on text aspect ratio
const
maxWidth
=
INNER_RADIUS
*
2
*
0.95
;
const
currentFontSize
=
parseFloat
(
label
.
style
.
fontSize
.
replace
(
"
em
"
,
""
),
);
// scale the font based on currentWidth/maxWidth, within min and max
// TODO: if text is too big wrap or ellipsis?
const
desiredFontSize
=
Math
.
max
(
FONT_SIZE_CENTER_LABEL_MIN
,
Math
.
min
(
FONT_SIZE_CENTER_LABEL_MAX
,
currentFontSize
*
(
maxWidth
/
currentWidth
),
),
);
// don't resize if within 5% to avoid potential thrashing
if
(
Math
.
abs
(
1
-
currentFontSize
/
desiredFontSize
)
>
0.05
)
{
label
.
style
.
fontSize
=
desiredFontSize
+
"
em
"
;
}
}
}
render
()
{
...
...
@@ -85,8 +134,7 @@ export default class Gauge extends Component {
}
=
this
.
props
;
const
viewBoxHeight
=
(
ARC_DEGREES
>
180
?
50
:
0
)
+
Math
.
sin
(
ARC_DEGREES
/
2
/
180
*
Math
.
PI
)
*
50
;
(
ARC_DEGREES
>
180
?
50
:
0
)
+
Math
.
sin
(
radians
(
ARC_DEGREES
/
2
))
*
50
;
const
viewBoxWidth
=
100
;
const
svgAspectRatio
=
viewBoxHeight
/
viewBoxWidth
;
...
...
@@ -101,14 +149,12 @@ export default class Gauge extends Component {
svgHeight
=
width
/
svgAspectRatio
;
}
const
showLabels
=
svgWidth
>
MIN_WIDTH_LABEL_THRESHOLD
;
const
range
=
settings
[
"
gauge.range
"
];
const
segments
=
settings
[
"
gauge.segments
"
];
const
arc
=
d3
.
svg
.
arc
()
.
outerRadius
(
OUTER_RADIUS
)
.
innerRadius
(
OUTER_RADIUS
*
INNER_RADIUS_RATIO
);
// value to angle in radians, clamped
const
angle
=
d3
.
scale
.
linear
()
.
domain
(
range
)
// NOTE: confusing, but the "range" is the domain for the arc scale
...
...
@@ -128,8 +174,6 @@ export default class Gauge extends Component {
];
};
const
radiusCenter
=
OUTER_RADIUS
-
(
OUTER_RADIUS
-
INNER_RADIUS
)
/
2
;
// get unique min/max plus range endpoints
const
numberLabels
=
Array
.
from
(
new
Set
(
...
...
@@ -145,64 +189,71 @@ export default class Gauge extends Component {
}));
return
(
<
div
className
=
{
cx
(
className
,
"
flex layout-centered
"
)
}
>
<
div
className
=
{
cx
(
className
,
"
relative
"
)
}
>
<
div
className
=
"relative"
style
=
{
{
width
:
svgWidth
,
height
:
svgHeight
}
}
className
=
"absolute overflow-hidden"
style
=
{
{
width
:
svgWidth
,
height
:
svgHeight
,
top
:
(
height
-
svgHeight
)
/
2
,
left
:
(
width
-
svgWidth
)
/
2
,
}
}
>
<
Scalar
{
...
this
.
props
}
className
=
"spread"
style
=
{
{
top
:
0
}
}
/>
<
svg
viewBox
=
{
`0 0 100
${
viewBoxHeight
}
`
}
>
<
g
transform
=
{
`translate(50,50)`
}
>
{
/* BACKGROUND ARC */
}
<
path
d
=
{
arc
({
startAngle
:
angle
(
range
[
0
]),
endAngle
:
angle
(
range
[
1
]),
})
}
fill
=
{
colors
[
"
bg-medium
"
]
}
<
GaugeArc
start
=
{
angle
(
range
[
0
])
}
end
=
{
angle
(
range
[
1
])
}
fill
=
{
BACKGROUND_ARC_COLOR
}
/>
{
/* SEGMENT ARCS */
}
{
segments
.
map
((
segment
,
index
)
=>
(
<
path
d
=
{
arc
({
startAngle
:
angle
(
segments
[
index
].
min
),
endAngle
:
angle
(
segments
[
index
].
max
),
})
}
<
GaugeArc
key
=
{
index
}
start
=
{
angle
(
segment
.
min
)
}
end
=
{
angle
(
segment
.
max
)
}
fill
=
{
segment
.
color
}
/>
))
}
{
/* NEEDLE */
}
<
path
d
=
{
`M-
${
ARROW_BASE
}
0 L0 -
${
ARROW_HEIGHT
}
L
${
ARROW_BASE
}
0 Z`
}
stroke
=
"white"
strokeWidth
=
{
ARROW_THICKNESS
}
fill
=
"none"
transform
=
{
`translate(0,-
${
INNER_RADIUS
}
) rotate(
${
angle
(
this
.
state
.
mounted
?
value
:
0
,
)
*
180
/
Math
.
PI
}
, 0,
${
INNER_RADIUS
}
)`
}
style
=
{
{
transition
:
"
transform 0.5s ease-in-out
"
}
}
/>
<
GaugeNeedle
angle
=
{
angle
(
this
.
state
.
mounted
?
value
:
0
)
}
/>
{
/* NUMBER LABELS */
}
{
numberLabels
.
map
((
value
,
index
)
=>
(
<
GaugeLabel
position
=
{
valuePosition
(
value
,
OUTER_RADIUS
*
1.01
)
}
>
{
formatValue
(
value
,
{
column
})
}
</
GaugeLabel
>
))
}
{
showLabels
&&
numberLabels
.
map
((
value
,
index
)
=>
(
<
GaugeSegmentLabel
position
=
{
valuePosition
(
value
,
OUTER_RADIUS
*
1.01
)
}
>
{
formatValue
(
value
,
{
column
})
}
</
GaugeSegmentLabel
>
))
}
{
/* TEXT LABELS */
}
{
textLabels
.
map
(({
label
,
value
},
index
)
=>
(
<
GaugeLabel
position
=
{
valuePosition
(
value
,
OUTER_RADIUS
*
1.01
)
}
style
=
{
{
fill
:
colors
[
"
text-dark
"
],
}
}
>
{
label
}
</
GaugeLabel
>
))
}
{
showLabels
&&
textLabels
.
map
(({
label
,
value
},
index
)
=>
(
<
GaugeSegmentLabel
position
=
{
valuePosition
(
value
,
OUTER_RADIUS
*
1.01
)
}
style
=
{
{
fill
:
SEGMENT_LABEL_COLOR
,
}
}
>
{
label
}
</
GaugeSegmentLabel
>
))
}
{
/* CENTER LABEL */
}
{
/* NOTE: can't be a component because ref doesn't work? */
}
<
text
ref
=
{
label
=>
(
this
.
_label
=
label
)
}
x
=
{
0
}
y
=
{
0
}
style
=
{
{
fill
:
CENTER_LABEL_COLOR
,
fontSize
:
"
1em
"
,
textAnchor
:
"
middle
"
,
transform
:
"
translate(0,0.2em)
"
,
}
}
>
{
formatValue
(
value
,
{
column
})
}
</
text
>
</
g
>
</
svg
>
</
div
>
...
...
@@ -211,21 +262,49 @@ export default class Gauge extends Component {
}
}
const
GaugeLabel
=
({
position
:
[
x
,
y
],
style
=
{},
children
})
=>
{
const
GaugeArc
=
({
start
,
end
,
fill
})
=>
{
const
arc
=
d3
.
svg
.
arc
()
.
outerRadius
(
OUTER_RADIUS
)
.
innerRadius
(
OUTER_RADIUS
*
INNER_RADIUS_RATIO
);
return
(
<
text
x
=
{
x
}
y
=
{
y
}
style
=
{
{
fill
:
colors
[
"
text-medium
"
],
fontSize
:
"
0.15em
"
,
textAnchor
:
Math
.
abs
(
x
)
<
5
?
"
middle
"
:
x
>
0
?
"
start
"
:
"
end
"
,
// shift text in the lower half down a bit
transform
:
y
>
0
?
"
translate(0,0.15em)
"
:
undefined
,
...
style
,
}
}
>
{
children
}
</
text
>
<
path
d
=
{
arc
({
startAngle
:
start
,
endAngle
:
end
,
})
}
fill
=
{
fill
}
/>
);
};
const
GaugeNeedle
=
({
angle
})
=>
(
<
path
d
=
{
`M-
${
ARROW_BASE
}
0 L0 -
${
ARROW_HEIGHT
}
L
${
ARROW_BASE
}
0 Z`
}
transform
=
{
`translate(0,-
${
INNER_RADIUS
}
) rotate(
${
degrees
(
angle
,
)}
, 0,
${
INNER_RADIUS
}
)`
}
style
=
{
{
transition
:
"
transform 0.5s ease-in-out
"
}
}
stroke
=
{
ARROW_STROKE_COLOR
}
strokeWidth
=
{
ARROW_STROKE_THICKNESS
}
fill
=
{
ARROW_FILL_COLOR
}
/>
);
const
GaugeSegmentLabel
=
({
position
:
[
x
,
y
],
style
=
{},
children
})
=>
(
<
text
x
=
{
x
}
y
=
{
y
}
style
=
{
{
fill
:
colors
[
"
text-medium
"
],
fontSize
:
`
${
FONT_SIZE_SEGMENT_LABEL
}
em`
,
textAnchor
:
Math
.
abs
(
x
)
<
5
?
"
middle
"
:
x
>
0
?
"
start
"
:
"
end
"
,
// shift text in the lower half down a bit
transform
:
y
>
0
?
`translate(0,
${
FONT_SIZE_SEGMENT_LABEL
}
em)`
:
undefined
,
...
style
,
}
}
>
{
children
}
</
text
>
);
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment