@gitlab/ui 137.2.0 → 137.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/charts/chart/chart.js +81 -20
- package/dist/components/charts/gauge/gauge.js +3 -13
- package/dist/components/charts/heatmap/heatmap.js +33 -13
- package/dist/components/charts/legend/legend.js +13 -1
- package/dist/tokens/color_mode.js +17 -0
- package/dist/utils/charts/color_mode.js +95 -0
- package/dist/utils/charts/css_values.js +105 -0
- package/dist/utils/charts/theme.js +72 -7
- package/package.json +3 -3
- package/src/components/charts/chart/chart.vue +68 -22
- package/src/components/charts/gauge/gauge.vue +1 -12
- package/src/components/charts/heatmap/heatmap.vue +34 -10
- package/src/components/charts/legend/legend.vue +13 -1
- package/src/tokens/color_mode.js +16 -0
- package/src/utils/charts/color_mode.js +83 -0
- package/src/utils/charts/css_values.js +59 -0
- package/src/utils/charts/theme.js +72 -5
|
@@ -2,6 +2,8 @@ import * as echarts from 'echarts';
|
|
|
2
2
|
import { merge } from 'lodash-es';
|
|
3
3
|
import { validRenderers, toolboxHeight, defaultWidth, defaultHeight } from '../../../utils/charts/config';
|
|
4
4
|
import { themeName, createTheme } from '../../../utils/charts/theme';
|
|
5
|
+
import { resolveCssValues } from '../../../utils/charts/css_values';
|
|
6
|
+
import { onColorModeChange } from '../../../utils/charts/color_mode';
|
|
5
7
|
import { GlResizeObserverDirective } from '../../../directives/resize_observer/resize_observer';
|
|
6
8
|
import { debounceByAnimationFrame } from '../../../utils/utils';
|
|
7
9
|
import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
|
|
@@ -126,32 +128,91 @@ var script = {
|
|
|
126
128
|
},
|
|
127
129
|
async mounted() {
|
|
128
130
|
await this.$nextTick();
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
width: defaultWidth,
|
|
132
|
-
height: defaultHeight
|
|
133
|
-
});
|
|
134
|
-
// FIXME: temporary workaround to ensure compatibility with @vue/compat
|
|
135
|
-
// eslint-disable-next-line no-underscore-dangle
|
|
136
|
-
chart.__v_skip = true;
|
|
137
|
-
this.chart = chart;
|
|
138
|
-
if (this.groupId.length) {
|
|
139
|
-
this.chart.group = this.groupId;
|
|
140
|
-
echarts.connect(this.groupId);
|
|
141
|
-
}
|
|
142
|
-
this.chart.on('click', this.handleClick);
|
|
143
|
-
/**
|
|
144
|
-
* Emitted after calling `echarts.init`
|
|
145
|
-
*/
|
|
146
|
-
this.$emit('created', this.chart);
|
|
147
|
-
this.draw();
|
|
148
|
-
this.setChartSize();
|
|
131
|
+
this.initChart();
|
|
132
|
+
this.observeColorMode();
|
|
149
133
|
},
|
|
150
134
|
beforeDestroy() {
|
|
135
|
+
var _this$unsubscribeColo;
|
|
151
136
|
if (!this.chart) return;
|
|
152
137
|
this.chart.off('click', this.handleClick);
|
|
138
|
+
(_this$unsubscribeColo = this.unsubscribeColorMode) === null || _this$unsubscribeColo === void 0 ? void 0 : _this$unsubscribeColo.call(this);
|
|
153
139
|
},
|
|
154
140
|
methods: {
|
|
141
|
+
initChart() {
|
|
142
|
+
const chart = echarts.init(this.$refs.chart, this.theme(), {
|
|
143
|
+
renderer: this.renderer,
|
|
144
|
+
width: defaultWidth,
|
|
145
|
+
height: defaultHeight
|
|
146
|
+
});
|
|
147
|
+
// FIXME: temporary workaround to ensure compatibility with @vue/compat
|
|
148
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
149
|
+
chart.__v_skip = true;
|
|
150
|
+
this.chart = chart;
|
|
151
|
+
if (this.groupId.length) {
|
|
152
|
+
this.chart.group = this.groupId;
|
|
153
|
+
echarts.connect(this.groupId);
|
|
154
|
+
}
|
|
155
|
+
this.chart.on('click', this.handleClick);
|
|
156
|
+
/**
|
|
157
|
+
* Emitted after calling `echarts.init`
|
|
158
|
+
*/
|
|
159
|
+
this.$emit('created', this.chart);
|
|
160
|
+
this.draw();
|
|
161
|
+
this.setChartSize();
|
|
162
|
+
},
|
|
163
|
+
/**
|
|
164
|
+
* The SVG renderer consumes the registered theme as-is, leaving its `var(--token)` colours for
|
|
165
|
+
* the browser to resolve. Canvas cannot, so it gets a copy resolved against this element.
|
|
166
|
+
*/
|
|
167
|
+
theme() {
|
|
168
|
+
if (this.renderer !== 'canvas') {
|
|
169
|
+
return themeName;
|
|
170
|
+
}
|
|
171
|
+
return resolveCssValues(createTheme(this.options), this.$refs.chart);
|
|
172
|
+
},
|
|
173
|
+
/**
|
|
174
|
+
* Rebuilding the chart discards its interaction state, so the legend selection and zoom range
|
|
175
|
+
* are carried over. Only these stateful fields are copied: the full option from getOption()
|
|
176
|
+
* holds colours resolved against the old colour mode.
|
|
177
|
+
*/
|
|
178
|
+
preservedState() {
|
|
179
|
+
const _this$chart$getOption = this.chart.getOption(),
|
|
180
|
+
_this$chart$getOption2 = _this$chart$getOption.legend,
|
|
181
|
+
legend = _this$chart$getOption2 === void 0 ? [] : _this$chart$getOption2,
|
|
182
|
+
_this$chart$getOption3 = _this$chart$getOption.dataZoom,
|
|
183
|
+
dataZoom = _this$chart$getOption3 === void 0 ? [] : _this$chart$getOption3;
|
|
184
|
+
return {
|
|
185
|
+
legend: legend.map(_ref => {
|
|
186
|
+
let selected = _ref.selected;
|
|
187
|
+
return {
|
|
188
|
+
selected
|
|
189
|
+
};
|
|
190
|
+
}),
|
|
191
|
+
dataZoom: dataZoom.map(_ref2 => {
|
|
192
|
+
let start = _ref2.start,
|
|
193
|
+
end = _ref2.end;
|
|
194
|
+
return {
|
|
195
|
+
start,
|
|
196
|
+
end
|
|
197
|
+
};
|
|
198
|
+
})
|
|
199
|
+
};
|
|
200
|
+
},
|
|
201
|
+
/**
|
|
202
|
+
* Colours resolved for canvas are frozen at init, so the chart has to be rebuilt when the
|
|
203
|
+
* colour mode changes. SVG re-resolves on its own and needs no subscription.
|
|
204
|
+
*/
|
|
205
|
+
observeColorMode() {
|
|
206
|
+
if (this.renderer !== 'canvas') {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
this.unsubscribeColorMode = onColorModeChange(this.$refs.chart, () => {
|
|
210
|
+
const state = this.preservedState();
|
|
211
|
+
this.chart.dispose();
|
|
212
|
+
this.initChart();
|
|
213
|
+
this.chart.setOption(state);
|
|
214
|
+
});
|
|
215
|
+
},
|
|
155
216
|
draw() {
|
|
156
217
|
this.chart.setOption(this.modifiedOptions);
|
|
157
218
|
/**
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import { merge, uniq, sortBy } from 'lodash-es';
|
|
2
|
-
import { gaugeSafeHues, gaugeWarningHue
|
|
2
|
+
import { gaugeSafeHues, gaugeWarningHue } from '../../../utils/charts/theme';
|
|
3
3
|
import Chart from '../chart/chart';
|
|
4
4
|
import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
|
|
5
5
|
|
|
6
|
-
const AXIS_LABEL_FONT_SIZE_PX = 14;
|
|
7
6
|
const ARC_RADIUS = '80%';
|
|
8
|
-
const DETAIL_FONT_SIZE_PX = 30;
|
|
9
|
-
const DETAIL_FONT_FAMILY = 'sans-serif';
|
|
10
|
-
const DETAIL_FONT_WEIGHT = 'bold';
|
|
11
7
|
const POINTER_LENGTH = '65%';
|
|
12
8
|
const POINTER_WIDTH_PX = 5;
|
|
13
9
|
const gaugeChartSeries = _ref => {
|
|
@@ -24,17 +20,11 @@ const gaugeChartSeries = _ref => {
|
|
|
24
20
|
formatter: () => {
|
|
25
21
|
const currentValue = Number.isFinite(value) ? value : null;
|
|
26
22
|
return text || (currentValue !== null && currentValue !== void 0 ? currentValue : '--');
|
|
27
|
-
}
|
|
28
|
-
color: `${gaugeNeutralHues[0]}`,
|
|
29
|
-
fontSize: DETAIL_FONT_SIZE_PX,
|
|
30
|
-
fontFamily: DETAIL_FONT_FAMILY,
|
|
31
|
-
fontWeight: DETAIL_FONT_WEIGHT
|
|
23
|
+
}
|
|
32
24
|
},
|
|
33
25
|
axisLabel: {
|
|
34
26
|
show: true,
|
|
35
|
-
|
|
36
|
-
formatter: theValue => Number.isFinite(theValue) ? theValue : '--',
|
|
37
|
-
color: `${gaugeNeutralHues[1]}`
|
|
27
|
+
formatter: theValue => Number.isFinite(theValue) ? theValue : '--'
|
|
38
28
|
},
|
|
39
29
|
axisLine: {
|
|
40
30
|
lineStyle: {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { merge } from 'lodash-es';
|
|
2
|
-
import { GL_COLOR_NEUTRAL_100, GL_COLOR_NEUTRAL_0 } from '../../../tokens/build/js/tokens';
|
|
3
2
|
import { getTooltipTitle, getTooltipContent } from '../../../utils/charts/config';
|
|
4
3
|
import { HEIGHT_AUTO_CLASSES } from '../../../utils/charts/constants';
|
|
5
|
-
import { heatmapHues } from '../../../utils/charts/theme';
|
|
4
|
+
import { heatmapHuesDark, heatmapHues, heatmapSurfaceDark, heatmapSurface } from '../../../utils/charts/theme';
|
|
5
|
+
import { isDarkMode, onColorModeChange } from '../../../utils/charts/color_mode';
|
|
6
6
|
import { engineeringNotation } from '../../../utils/number_utils';
|
|
7
7
|
import TooltipDefaultFormat from '../shared/tooltip/tooltip_default_format/tooltip_default_format';
|
|
8
8
|
import Chart from '../chart/chart';
|
|
@@ -61,10 +61,7 @@ function _unsupportedIterableToArray(r, a) {
|
|
|
61
61
|
|
|
62
62
|
const defaultOptions = {
|
|
63
63
|
visualMap: {
|
|
64
|
-
show: false
|
|
65
|
-
inRange: {
|
|
66
|
-
color: heatmapHues
|
|
67
|
-
}
|
|
64
|
+
show: false
|
|
68
65
|
},
|
|
69
66
|
series: {
|
|
70
67
|
type: 'heatmap'
|
|
@@ -201,10 +198,20 @@ var script = {
|
|
|
201
198
|
left: '0',
|
|
202
199
|
top: '0'
|
|
203
200
|
},
|
|
204
|
-
selectedFormatTooltipText: this.formatTooltipText || this.defaultFormatTooltipText
|
|
201
|
+
selectedFormatTooltipText: this.formatTooltipText || this.defaultFormatTooltipText,
|
|
202
|
+
// ECharts interpolates the visualMap stops numerically, so these colours cannot be CSS
|
|
203
|
+
// custom properties and have to be picked for the current colour mode instead. Resolved on
|
|
204
|
+
// mount, because the mode depends on where this chart sits in the document.
|
|
205
|
+
isDark: false
|
|
205
206
|
};
|
|
206
207
|
},
|
|
207
208
|
computed: {
|
|
209
|
+
hues() {
|
|
210
|
+
return this.isDark ? heatmapHuesDark : heatmapHues;
|
|
211
|
+
},
|
|
212
|
+
surface() {
|
|
213
|
+
return this.isDark ? heatmapSurfaceDark : heatmapSurface;
|
|
214
|
+
},
|
|
208
215
|
computedOptions() {
|
|
209
216
|
const _getRange = getRange(this.dataSeries),
|
|
210
217
|
min = _getRange.min,
|
|
@@ -219,11 +226,14 @@ var script = {
|
|
|
219
226
|
right: '32px',
|
|
220
227
|
show: true,
|
|
221
228
|
borderWidth: 0,
|
|
222
|
-
backgroundColor:
|
|
229
|
+
backgroundColor: this.surface.plate
|
|
223
230
|
},
|
|
224
231
|
visualMap: {
|
|
225
232
|
min,
|
|
226
|
-
max
|
|
233
|
+
max,
|
|
234
|
+
inRange: {
|
|
235
|
+
color: this.hues
|
|
236
|
+
}
|
|
227
237
|
},
|
|
228
238
|
xAxis: {
|
|
229
239
|
data: this.xAxisLabels,
|
|
@@ -243,7 +253,7 @@ var script = {
|
|
|
243
253
|
show: true,
|
|
244
254
|
interval: 0,
|
|
245
255
|
lineStyle: {
|
|
246
|
-
color:
|
|
256
|
+
color: this.surface.separator,
|
|
247
257
|
width: 2
|
|
248
258
|
}
|
|
249
259
|
},
|
|
@@ -270,7 +280,7 @@ var script = {
|
|
|
270
280
|
show: true,
|
|
271
281
|
interval: 0,
|
|
272
282
|
lineStyle: {
|
|
273
|
-
color:
|
|
283
|
+
color: this.surface.separator,
|
|
274
284
|
width: 2
|
|
275
285
|
}
|
|
276
286
|
}
|
|
@@ -290,8 +300,8 @@ var script = {
|
|
|
290
300
|
const _getRange2 = getRange(this.dataSeries),
|
|
291
301
|
min = _getRange2.min,
|
|
292
302
|
max = _getRange2.max;
|
|
293
|
-
const step = (max - min) /
|
|
294
|
-
return
|
|
303
|
+
const step = (max - min) / this.hues.length;
|
|
304
|
+
return this.hues.map((color, index) => {
|
|
295
305
|
const lowerBound = engineeringNotation(min + step * index);
|
|
296
306
|
const upperBound = engineeringNotation(min + step * (index + 1));
|
|
297
307
|
return {
|
|
@@ -305,6 +315,16 @@ var script = {
|
|
|
305
315
|
return this.height === 'auto';
|
|
306
316
|
}
|
|
307
317
|
},
|
|
318
|
+
mounted() {
|
|
319
|
+
this.isDark = isDarkMode(this.$el);
|
|
320
|
+
this.unsubscribeColorMode = onColorModeChange(this.$el, isDark => {
|
|
321
|
+
this.isDark = isDark;
|
|
322
|
+
});
|
|
323
|
+
},
|
|
324
|
+
beforeDestroy() {
|
|
325
|
+
var _this$unsubscribeColo;
|
|
326
|
+
(_this$unsubscribeColo = this.unsubscribeColorMode) === null || _this$unsubscribeColo === void 0 ? void 0 : _this$unsubscribeColo.call(this);
|
|
327
|
+
},
|
|
308
328
|
methods: {
|
|
309
329
|
defaultFormatTooltipText(params) {
|
|
310
330
|
this.tooltip.title = getTooltipTitle(params, this.computedOptions.xAxis.name);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as echarts from 'echarts';
|
|
2
2
|
import { defaultFontSize } from '../../../utils/charts/config';
|
|
3
|
+
import { defaultFontFamily } from '../../../utils/charts/theme';
|
|
3
4
|
import { LEGEND_AVERAGE_TEXT, LEGEND_CURRENT_TEXT, LEGEND_MIN_TEXT, LEGEND_MAX_TEXT, LEGEND_LAYOUT_INLINE, LEGEND_LAYOUT_TABLE } from '../../../utils/charts/constants';
|
|
4
5
|
import { engineeringNotation, average } from '../../../utils/number_utils';
|
|
5
6
|
import GlTruncate from '../../utilities/truncate/truncate';
|
|
@@ -144,7 +145,7 @@ var script = {
|
|
|
144
145
|
computed: {
|
|
145
146
|
fontStyle() {
|
|
146
147
|
return {
|
|
147
|
-
fontFamily: this.textStyle.fontFamily ||
|
|
148
|
+
fontFamily: this.textStyle.fontFamily || defaultFontFamily,
|
|
148
149
|
fontSize: `${this.textStyle.fontSize || defaultFontSize}px`
|
|
149
150
|
};
|
|
150
151
|
},
|
|
@@ -152,6 +153,17 @@ var script = {
|
|
|
152
153
|
return this.seriesInfo.length === 1;
|
|
153
154
|
}
|
|
154
155
|
},
|
|
156
|
+
watch: {
|
|
157
|
+
/**
|
|
158
|
+
* When the canvas renderer rebuilds the chart on a colour-mode change it disposes the old
|
|
159
|
+
* ECharts instance and creates a new one. The parent emits `created` with the new instance and
|
|
160
|
+
* updates this prop, so we must rebind our persistent listener to the new instance.
|
|
161
|
+
*/
|
|
162
|
+
chart(newChart, oldChart) {
|
|
163
|
+
oldChart.off('legendselectchanged', this.suppressLastActiveSeriesLabelToggle);
|
|
164
|
+
newChart.on('legendselectchanged', this.suppressLastActiveSeriesLabelToggle);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
155
167
|
created() {
|
|
156
168
|
this.chart.on('legendselectchanged', this.suppressLastActiveSeriesLabelToggle);
|
|
157
169
|
},
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The classes that decide which set of token values applies to an element.
|
|
3
|
+
*
|
|
4
|
+
* Dark mode is applied either to the whole document, by putting `gl-dark` on the root, or to a
|
|
5
|
+
* subtree, by putting `gl-dark-scope` on any ancestor. `gl-light-scope` flips a subtree back.
|
|
6
|
+
*
|
|
7
|
+
* These are the single source of truth: `bin/build_tokens.mjs` builds the CSS selectors from them,
|
|
8
|
+
* so the stylesheets and any JavaScript that has to reason about the colour mode cannot drift
|
|
9
|
+
* apart. Changing a name here changes both.
|
|
10
|
+
*/
|
|
11
|
+
const DARK_ROOT_CLASS = 'gl-dark';
|
|
12
|
+
const DARK_SCOPE_CLASS = 'gl-dark-scope';
|
|
13
|
+
const LIGHT_SCOPE_CLASS = 'gl-light-scope';
|
|
14
|
+
const LIGHT_SELECTOR = `:root, .${LIGHT_SCOPE_CLASS}`;
|
|
15
|
+
const DARK_SELECTOR = `:root.${DARK_ROOT_CLASS}, .${DARK_SCOPE_CLASS}`;
|
|
16
|
+
|
|
17
|
+
export { DARK_ROOT_CLASS, DARK_SCOPE_CLASS, DARK_SELECTOR, LIGHT_SCOPE_CLASS, LIGHT_SELECTOR };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { LIGHT_SCOPE_CLASS, DARK_SCOPE_CLASS, DARK_ROOT_CLASS } from '../../tokens/color_mode';
|
|
2
|
+
|
|
3
|
+
const SCOPE_CLASSES = [DARK_SCOPE_CLASS, LIGHT_SCOPE_CLASS, DARK_ROOT_CLASS];
|
|
4
|
+
const SCOPE_SELECTOR = SCOPE_CLASSES.map(scopeClass => `.${scopeClass}`).join(', ');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Whether the given element sits in dark mode.
|
|
8
|
+
*
|
|
9
|
+
* Colour mode is not a document-wide fact. Tokens are declared for the root dark class and for a
|
|
10
|
+
* dark scope class, so any subtree can be flipped independently, and a light scope can flip one
|
|
11
|
+
* back inside a dark document. The nearest scope wins, which is what `closest` gives us.
|
|
12
|
+
*
|
|
13
|
+
* This lives under `charts` rather than in the general utilities because charts are the only thing
|
|
14
|
+
* in the package that needs it. Everywhere else, colour follows tokens and CSS re-resolves them on
|
|
15
|
+
* its own. Charts cannot always do that: ECharts parses colours numerically for its visual maps,
|
|
16
|
+
* and canvas cannot read custom properties at all, so those values have to be chosen in JavaScript
|
|
17
|
+
* and re-chosen when the mode changes.
|
|
18
|
+
*/
|
|
19
|
+
const isDarkMode = element => {
|
|
20
|
+
var _element$closest;
|
|
21
|
+
const scope = element === null || element === void 0 ? void 0 : (_element$closest = element.closest) === null || _element$closest === void 0 ? void 0 : _element$closest.call(element, SCOPE_SELECTOR);
|
|
22
|
+
return Boolean(scope) && !scope.classList.contains(LIGHT_SCOPE_CLASS);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Keyed by subscription so that each subscribed element's last known mode is tracked separately.
|
|
26
|
+
const subscriptions = new Map();
|
|
27
|
+
let observer = null;
|
|
28
|
+
|
|
29
|
+
// Most class mutations (hover states, dropdowns opening and closing) have nothing to do with the
|
|
30
|
+
// colour mode. Only mutations that added or removed a scope class can change it, so anything else
|
|
31
|
+
// can be dismissed with this cheap check instead of re-checking every subscriber.
|
|
32
|
+
const touchesColorMode = _ref => {
|
|
33
|
+
let target = _ref.target,
|
|
34
|
+
oldValue = _ref.oldValue;
|
|
35
|
+
return SCOPE_CLASSES.some(scopeClass => {
|
|
36
|
+
var _target$classList;
|
|
37
|
+
return ((_target$classList = target.classList) === null || _target$classList === void 0 ? void 0 : _target$classList.contains(scopeClass)) || (oldValue === null || oldValue === void 0 ? void 0 : oldValue.includes(scopeClass));
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
const notify = () => {
|
|
41
|
+
subscriptions.forEach((_ref2, key) => {
|
|
42
|
+
let element = _ref2.element,
|
|
43
|
+
subscriber = _ref2.subscriber,
|
|
44
|
+
mode = _ref2.mode;
|
|
45
|
+
const current = isDarkMode(element);
|
|
46
|
+
if (current !== mode) {
|
|
47
|
+
subscriptions.set(key, {
|
|
48
|
+
element,
|
|
49
|
+
subscriber,
|
|
50
|
+
mode: current
|
|
51
|
+
});
|
|
52
|
+
subscriber(current);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Calls back when the colour mode of `element` changes, and returns a function that unsubscribes.
|
|
59
|
+
*
|
|
60
|
+
* Most colour can be left to CSS, which re-resolves custom properties on its own. This exists for
|
|
61
|
+
* the places that cannot: colours ECharts parses numerically, such as the interpolated stops of a
|
|
62
|
+
* continuous `visualMap`, and colours handed to a canvas context. Both need a concrete value picked
|
|
63
|
+
* in JavaScript, so both need telling when the mode flips.
|
|
64
|
+
*
|
|
65
|
+
* Because a scope class can live on any ancestor, this watches the whole document for class
|
|
66
|
+
* changes, then re-checks each subscribed element and only calls back when its own mode changed.
|
|
67
|
+
*/
|
|
68
|
+
const onColorModeChange = (element, subscriber) => {
|
|
69
|
+
if (!subscriptions.size && typeof MutationObserver !== 'undefined') {
|
|
70
|
+
observer = new MutationObserver(mutations => {
|
|
71
|
+
if (mutations.some(mutation => touchesColorMode(mutation))) notify();
|
|
72
|
+
});
|
|
73
|
+
observer.observe(document.documentElement, {
|
|
74
|
+
attributeFilter: ['class'],
|
|
75
|
+
attributeOldValue: true,
|
|
76
|
+
subtree: true
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const key = Symbol('colorModeSubscription');
|
|
80
|
+
subscriptions.set(key, {
|
|
81
|
+
element,
|
|
82
|
+
subscriber,
|
|
83
|
+
mode: isDarkMode(element)
|
|
84
|
+
});
|
|
85
|
+
return () => {
|
|
86
|
+
subscriptions.delete(key);
|
|
87
|
+
if (!subscriptions.size) {
|
|
88
|
+
var _observer;
|
|
89
|
+
(_observer = observer) === null || _observer === void 0 ? void 0 : _observer.disconnect();
|
|
90
|
+
observer = null;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export { isDarkMode, onColorModeChange };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { mapValues } from 'lodash-es';
|
|
2
|
+
|
|
3
|
+
function _arrayLikeToArray(r, a) {
|
|
4
|
+
(null == a || a > r.length) && (a = r.length);
|
|
5
|
+
for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
|
|
6
|
+
return n;
|
|
7
|
+
}
|
|
8
|
+
function _arrayWithHoles(r) {
|
|
9
|
+
if (Array.isArray(r)) return r;
|
|
10
|
+
}
|
|
11
|
+
function _iterableToArrayLimit(r, l) {
|
|
12
|
+
var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
|
|
13
|
+
if (null != t) {
|
|
14
|
+
var e,
|
|
15
|
+
n,
|
|
16
|
+
i,
|
|
17
|
+
u,
|
|
18
|
+
a = [],
|
|
19
|
+
f = !0,
|
|
20
|
+
o = !1;
|
|
21
|
+
try {
|
|
22
|
+
if (i = (t = t.call(r)).next, 0 === l) {
|
|
23
|
+
if (Object(t) !== t) return;
|
|
24
|
+
f = !1;
|
|
25
|
+
} else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
|
|
26
|
+
} catch (r) {
|
|
27
|
+
o = !0, n = r;
|
|
28
|
+
} finally {
|
|
29
|
+
try {
|
|
30
|
+
if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
|
|
31
|
+
} finally {
|
|
32
|
+
if (o) throw n;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return a;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function _nonIterableRest() {
|
|
39
|
+
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
|
|
40
|
+
}
|
|
41
|
+
function _slicedToArray(r, e) {
|
|
42
|
+
return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
|
|
43
|
+
}
|
|
44
|
+
function _unsupportedIterableToArray(r, a) {
|
|
45
|
+
if (r) {
|
|
46
|
+
if ("string" == typeof r) return _arrayLikeToArray(r, a);
|
|
47
|
+
var t = {}.toString.call(r).slice(8, -1);
|
|
48
|
+
return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const CSS_VAR = /^var\(\s*(--[\w-]+)\s*(?:,\s*([\s\S]*?)\s*)?\)$/;
|
|
53
|
+
const resolveString = (value, styles) => {
|
|
54
|
+
if (value.toLowerCase() === 'currentcolor') {
|
|
55
|
+
return styles.color;
|
|
56
|
+
}
|
|
57
|
+
const match = CSS_VAR.exec(value);
|
|
58
|
+
if (!match) {
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
const _match = _slicedToArray(match, 3),
|
|
62
|
+
name = _match[1],
|
|
63
|
+
fallback = _match[2];
|
|
64
|
+
const resolved = styles.getPropertyValue(name).trim();
|
|
65
|
+
if (resolved) {
|
|
66
|
+
return resolved;
|
|
67
|
+
}
|
|
68
|
+
return fallback ? resolveString(fallback, styles) : value;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Deeply replaces `var(--token)` and `currentcolor` strings with the values they resolve to for the
|
|
73
|
+
* given element.
|
|
74
|
+
*
|
|
75
|
+
* The SVG renderer needs none of this: it emits both forms into the DOM, where the browser resolves
|
|
76
|
+
* them and keeps them live across a colour-mode change. Canvas has no such context. `fillStyle` is
|
|
77
|
+
* a plain string setter, so an unparseable `var(--token)` is dropped on the floor and leaves the
|
|
78
|
+
* previous fill in place, while `currentcolor` has no element to inherit from and is defined to
|
|
79
|
+
* resolve to opaque black. Either way a canvas-rendered chart silently loses its themed colours.
|
|
80
|
+
*
|
|
81
|
+
* Resolution is relative to `element` rather than the document root so that `.gl-dark-scope`
|
|
82
|
+
* ancestors are honoured.
|
|
83
|
+
*
|
|
84
|
+
* Values are resolved once, so callers are responsible for resolving again when the colour mode
|
|
85
|
+
* changes.
|
|
86
|
+
*/
|
|
87
|
+
const resolveCssValues = (input, element) => {
|
|
88
|
+
const styles = window.getComputedStyle(element);
|
|
89
|
+
const walk = value => {
|
|
90
|
+
if (typeof value === 'string') {
|
|
91
|
+
return resolveString(value, styles);
|
|
92
|
+
}
|
|
93
|
+
if (Array.isArray(value)) {
|
|
94
|
+
return value.map(item => walk(item));
|
|
95
|
+
}
|
|
96
|
+
// Themes carry functions (formatters) which must pass through untouched.
|
|
97
|
+
if (value !== null && typeof value === 'object') {
|
|
98
|
+
return mapValues(value, walk);
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
};
|
|
102
|
+
return walk(input);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export { resolveCssValues };
|
|
@@ -1,10 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { GL_FONT_FAMILY_REGULAR, GL_COLOR_NEUTRAL_100, GL_COLOR_DATA_BLUE_500, GL_COLOR_DATA_BLUE_600, GL_COLOR_DATA_BLUE_800, GL_COLOR_DATA_BLUE_950, GL_COLOR_NEUTRAL_800, GL_COLOR_DATA_BLUE_400, GL_COLOR_DATA_BLUE_200, GL_COLOR_NEUTRAL_0, GL_COLOR_NEUTRAL_950, GL_COLOR_DATA_BLUE_900, GL_COLOR_DATA_ORANGE_500, GL_COLOR_DATA_ORANGE_600, GL_COLOR_DATA_AQUA_500, GL_COLOR_DATA_GREEN_600, GL_COLOR_DATA_MAGENTA_500, GL_COLOR_DATA_BLUE_700, GL_COLOR_DATA_ORANGE_800, GL_COLOR_DATA_AQUA_700, GL_COLOR_DATA_GREEN_800, GL_COLOR_DATA_MAGENTA_700, GL_COLOR_DATA_ORANGE_950, GL_COLOR_DATA_AQUA_900, GL_COLOR_DATA_GREEN_950, GL_COLOR_DATA_MAGENTA_900, GL_COLOR_DATA_ORANGE_700, GL_COLOR_DATA_AQUA_600, GL_COLOR_DATA_GREEN_700, GL_COLOR_DATA_MAGENTA_600, GL_COLOR_DATA_ORANGE_900, GL_COLOR_DATA_AQUA_800, GL_COLOR_DATA_GREEN_900, GL_COLOR_DATA_MAGENTA_800, GL_COLOR_DATA_AQUA_950, GL_COLOR_DATA_GREEN_500, GL_COLOR_DATA_MAGENTA_950, GL_COLOR_DATA_ORANGE_400, GL_COLOR_DATA_GREEN_400, GL_COLOR_DATA_BLUE_300, GL_COLOR_DATA_ORANGE_200, GL_COLOR_DATA_AQUA_300, GL_COLOR_DATA_GREEN_200, GL_COLOR_DATA_MAGENTA_300, GL_COLOR_DATA_BLUE_100, GL_COLOR_DATA_ORANGE_50, GL_COLOR_DATA_AQUA_100, GL_COLOR_DATA_GREEN_50, GL_COLOR_DATA_MAGENTA_100, GL_COLOR_DATA_ORANGE_300, GL_COLOR_DATA_AQUA_400, GL_COLOR_DATA_GREEN_300, GL_COLOR_DATA_MAGENTA_400, GL_COLOR_DATA_ORANGE_100, GL_COLOR_DATA_AQUA_200, GL_COLOR_DATA_GREEN_100, GL_COLOR_DATA_MAGENTA_200, GL_COLOR_DATA_BLUE_50, GL_COLOR_DATA_AQUA_50, GL_COLOR_DATA_MAGENTA_50, GL_COLOR_NEUTRAL_50 } from '../../tokens/build/js/tokens';
|
|
2
2
|
import { scrollHandleSvgPath, marqueeSelectionSvgPath, redoSvgPath, clearAllSvgPath, downloadSvgPath } from '../svgs/svg_paths';
|
|
3
|
+
import { defaultFontSize } from './config';
|
|
3
4
|
|
|
4
5
|
const GL_BORDER_RADIUS_BASE = '0.25rem';
|
|
5
6
|
const themeName = 'gitlab';
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sequential scales step away from the surface so that higher values gain contrast against it:
|
|
10
|
+
* darker on a light surface, lighter on a dark one. The lowest step is neutral rather than blue, so
|
|
11
|
+
* that "no data" reads as absence instead of as the bottom of the data range.
|
|
12
|
+
*
|
|
13
|
+
* https://design.gitlab.com/data-visualization/color#sequential-data
|
|
14
|
+
*/
|
|
15
|
+
const heatmapHues = [GL_COLOR_NEUTRAL_100, GL_COLOR_DATA_BLUE_500, GL_COLOR_DATA_BLUE_600, GL_COLOR_DATA_BLUE_800, GL_COLOR_DATA_BLUE_950];
|
|
16
|
+
const heatmapHuesDark = [GL_COLOR_NEUTRAL_800, GL_COLOR_DATA_BLUE_600, GL_COLOR_DATA_BLUE_500, GL_COLOR_DATA_BLUE_400, GL_COLOR_DATA_BLUE_200];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The plate behind the cells, and the separators between them. Separators match the surface so that
|
|
20
|
+
* neighbouring cells, which are only guaranteed to contrast with the surface and not with each
|
|
21
|
+
* other, stay distinguishable.
|
|
22
|
+
*
|
|
23
|
+
* https://design.gitlab.com/data-visualization/color#visual-separators
|
|
24
|
+
*/
|
|
25
|
+
const heatmapSurface = {
|
|
26
|
+
plate: GL_COLOR_NEUTRAL_100,
|
|
27
|
+
separator: GL_COLOR_NEUTRAL_0
|
|
28
|
+
};
|
|
29
|
+
const heatmapSurfaceDark = {
|
|
30
|
+
plate: GL_COLOR_NEUTRAL_800,
|
|
31
|
+
separator: GL_COLOR_NEUTRAL_950
|
|
32
|
+
};
|
|
8
33
|
const gaugeSafeHues = [GL_COLOR_DATA_BLUE_500, GL_COLOR_DATA_BLUE_900];
|
|
9
34
|
const gaugeWarningHue = GL_COLOR_DATA_ORANGE_500;
|
|
10
35
|
|
|
@@ -18,12 +43,32 @@ const colorPaletteDefault = [GL_COLOR_DATA_BLUE_500, GL_COLOR_DATA_ORANGE_600, G
|
|
|
18
43
|
const colorFromDefaultPalette = index => colorPaletteDefault[index % colorPaletteDefault.length];
|
|
19
44
|
const colorPaletteDark = [GL_COLOR_DATA_BLUE_500, GL_COLOR_DATA_ORANGE_400, GL_COLOR_DATA_AQUA_500, GL_COLOR_DATA_GREEN_400, GL_COLOR_DATA_MAGENTA_500, GL_COLOR_DATA_BLUE_300, GL_COLOR_DATA_ORANGE_200, GL_COLOR_DATA_AQUA_300, GL_COLOR_DATA_GREEN_200, GL_COLOR_DATA_MAGENTA_300, GL_COLOR_DATA_BLUE_100, GL_COLOR_DATA_ORANGE_50, GL_COLOR_DATA_AQUA_100, GL_COLOR_DATA_GREEN_50, GL_COLOR_DATA_MAGENTA_100, GL_COLOR_DATA_BLUE_400, GL_COLOR_DATA_ORANGE_300, GL_COLOR_DATA_AQUA_400, GL_COLOR_DATA_GREEN_300, GL_COLOR_DATA_MAGENTA_400, GL_COLOR_DATA_BLUE_200, GL_COLOR_DATA_ORANGE_100, GL_COLOR_DATA_AQUA_200, GL_COLOR_DATA_GREEN_100, GL_COLOR_DATA_MAGENTA_200, GL_COLOR_DATA_BLUE_50, GL_COLOR_DATA_ORANGE_500, GL_COLOR_DATA_AQUA_50, GL_COLOR_DATA_GREEN_500, GL_COLOR_DATA_MAGENTA_50];
|
|
20
45
|
const colorFromDarkPalette = index => colorPaletteDark[index % colorPaletteDark.length];
|
|
46
|
+
|
|
47
|
+
// ECharts lays out text by assigning a CSS font shorthand to a canvas context and calling
|
|
48
|
+
// `measureText`, even under the SVG renderer. Canvas does not substitute CSS custom properties,
|
|
49
|
+
// and an unparseable shorthand is rejected wholesale, leaving text measured at the canvas default
|
|
50
|
+
// of 10px sans-serif. So the chart font stack must be concrete: drop the leading
|
|
51
|
+
// `var(--default-regular-font, 'GitLab Sans')` consumer override, whose own fallback is the
|
|
52
|
+
// 'GitLab Sans' that follows it.
|
|
53
|
+
const defaultFontFamily = GL_FONT_FAMILY_REGULAR.filter(family => !family.startsWith('var(')).join(', ');
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Chart font sizes are concrete pixel numbers rather than the rem-based font.size tokens they
|
|
57
|
+
* mirror. zrender measures text on a detached canvas, which always resolves 1rem as 16px however
|
|
58
|
+
* the document root is set, so rem sizes render scaled while being laid out at the wrong width.
|
|
59
|
+
*
|
|
60
|
+
* `defaultFontSize` (12, font.size.100) is set per component in the theme below, because ECharts
|
|
61
|
+
* pins fontSize in its own component defaults (axis labels at 12, title at 18, gauge detail at 30)
|
|
62
|
+
* and those shadow `textStyle`.
|
|
63
|
+
*/
|
|
64
|
+
|
|
21
65
|
const axes = {
|
|
22
66
|
axisLabel: {
|
|
23
67
|
margin: 8,
|
|
24
68
|
show: true,
|
|
25
69
|
color: 'var(--gl-chart-axis-text-color)',
|
|
26
|
-
hideOverlap: true
|
|
70
|
+
hideOverlap: true,
|
|
71
|
+
fontSize: defaultFontSize
|
|
27
72
|
},
|
|
28
73
|
axisLine: {
|
|
29
74
|
show: false
|
|
@@ -46,7 +91,8 @@ const axes = {
|
|
|
46
91
|
},
|
|
47
92
|
nameGap: 30,
|
|
48
93
|
nameTextStyle: {
|
|
49
|
-
fontWeight: 'bold'
|
|
94
|
+
fontWeight: 'bold',
|
|
95
|
+
fontSize: defaultFontSize
|
|
50
96
|
},
|
|
51
97
|
splitLine: {
|
|
52
98
|
lineStyle: {
|
|
@@ -64,7 +110,26 @@ const createTheme = function () {
|
|
|
64
110
|
color: colorPaletteDefault,
|
|
65
111
|
backgroundColor: 'transparent',
|
|
66
112
|
textStyle: {
|
|
67
|
-
color: 'var(--gl-text-color-default)'
|
|
113
|
+
color: 'var(--gl-text-color-default)',
|
|
114
|
+
fontFamily: defaultFontFamily,
|
|
115
|
+
fontSize: defaultFontSize
|
|
116
|
+
},
|
|
117
|
+
title: {
|
|
118
|
+
textStyle: {
|
|
119
|
+
fontSize: 18 // font.size.500-fixed, 1.125rem
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
gauge: {
|
|
123
|
+
axisLabel: {
|
|
124
|
+
fontSize: 14,
|
|
125
|
+
// font.size.300, 0.875rem, the default body size
|
|
126
|
+
color: 'var(--gl-chart-axis-text-color)'
|
|
127
|
+
},
|
|
128
|
+
detail: {
|
|
129
|
+
fontSize: 30,
|
|
130
|
+
fontWeight: 'bold',
|
|
131
|
+
color: 'var(--gl-text-color-default)'
|
|
132
|
+
}
|
|
68
133
|
},
|
|
69
134
|
markLine: {
|
|
70
135
|
silent: true,
|
|
@@ -197,4 +262,4 @@ const createTheme = function () {
|
|
|
197
262
|
};
|
|
198
263
|
};
|
|
199
264
|
|
|
200
|
-
export { colorFromDarkPalette, colorFromDefaultPalette, colorPaletteDark, colorPaletteDefault, createTheme,
|
|
265
|
+
export { colorFromDarkPalette, colorFromDefaultPalette, colorPaletteDark, colorPaletteDefault, createTheme, defaultFontFamily, gaugeSafeHues, gaugeWarningHue, heatmapHues, heatmapHuesDark, heatmapSurface, heatmapSurfaceDark, themeName };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitlab/ui",
|
|
3
|
-
"version": "137.2.
|
|
3
|
+
"version": "137.2.1",
|
|
4
4
|
"description": "GitLab UI Components",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
"@gitlab/fonts": "^1.3.1",
|
|
110
110
|
"@gitlab/hybrid-vue": "npm:@vue/compat@3.5.34",
|
|
111
111
|
"@gitlab/svgs": "*",
|
|
112
|
-
"@jest/test-sequencer": "30.
|
|
112
|
+
"@jest/test-sequencer": "30.5.1",
|
|
113
113
|
"@rollup/plugin-commonjs": "^28.0.9",
|
|
114
114
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
115
115
|
"@rollup/plugin-replace": "^6.0.3",
|
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
"start-server-and-test": "^2.1.5",
|
|
176
176
|
"storybook": "^7.6.24",
|
|
177
177
|
"storybook-dark-mode": "4.0.2",
|
|
178
|
-
"style-dictionary": "^5.5.
|
|
178
|
+
"style-dictionary": "^5.5.3",
|
|
179
179
|
"style-loader": "^4",
|
|
180
180
|
"tailwindcss": "3.4.19",
|
|
181
181
|
"vue": "2.7.16",
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
toolboxHeight,
|
|
9
9
|
} from '../../../utils/charts/config';
|
|
10
10
|
import { createTheme, themeName } from '../../../utils/charts/theme';
|
|
11
|
+
import { resolveCssValues } from '../../../utils/charts/css_values';
|
|
12
|
+
import { onColorModeChange } from '../../../utils/charts/color_mode';
|
|
11
13
|
import { GlResizeObserverDirective } from '../../../directives/resize_observer/resize_observer';
|
|
12
14
|
import { debounceByAnimationFrame } from '../../../utils/utils';
|
|
13
15
|
|
|
@@ -122,35 +124,79 @@ export default {
|
|
|
122
124
|
async mounted() {
|
|
123
125
|
await this.$nextTick();
|
|
124
126
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
width: defaultWidth,
|
|
128
|
-
height: defaultHeight,
|
|
129
|
-
});
|
|
130
|
-
// FIXME: temporary workaround to ensure compatibility with @vue/compat
|
|
131
|
-
// eslint-disable-next-line no-underscore-dangle
|
|
132
|
-
chart.__v_skip = true;
|
|
133
|
-
this.chart = chart;
|
|
134
|
-
|
|
135
|
-
if (this.groupId.length) {
|
|
136
|
-
this.chart.group = this.groupId;
|
|
137
|
-
echarts.connect(this.groupId);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
this.chart.on('click', this.handleClick);
|
|
141
|
-
/**
|
|
142
|
-
* Emitted after calling `echarts.init`
|
|
143
|
-
*/
|
|
144
|
-
this.$emit('created', this.chart);
|
|
145
|
-
this.draw();
|
|
146
|
-
this.setChartSize();
|
|
127
|
+
this.initChart();
|
|
128
|
+
this.observeColorMode();
|
|
147
129
|
},
|
|
148
130
|
beforeDestroy() {
|
|
149
131
|
if (!this.chart) return;
|
|
150
132
|
|
|
151
133
|
this.chart.off('click', this.handleClick);
|
|
134
|
+
this.unsubscribeColorMode?.();
|
|
152
135
|
},
|
|
153
136
|
methods: {
|
|
137
|
+
initChart() {
|
|
138
|
+
const chart = echarts.init(this.$refs.chart, this.theme(), {
|
|
139
|
+
renderer: this.renderer,
|
|
140
|
+
width: defaultWidth,
|
|
141
|
+
height: defaultHeight,
|
|
142
|
+
});
|
|
143
|
+
// FIXME: temporary workaround to ensure compatibility with @vue/compat
|
|
144
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
145
|
+
chart.__v_skip = true;
|
|
146
|
+
this.chart = chart;
|
|
147
|
+
|
|
148
|
+
if (this.groupId.length) {
|
|
149
|
+
this.chart.group = this.groupId;
|
|
150
|
+
echarts.connect(this.groupId);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.chart.on('click', this.handleClick);
|
|
154
|
+
/**
|
|
155
|
+
* Emitted after calling `echarts.init`
|
|
156
|
+
*/
|
|
157
|
+
this.$emit('created', this.chart);
|
|
158
|
+
this.draw();
|
|
159
|
+
this.setChartSize();
|
|
160
|
+
},
|
|
161
|
+
/**
|
|
162
|
+
* The SVG renderer consumes the registered theme as-is, leaving its `var(--token)` colours for
|
|
163
|
+
* the browser to resolve. Canvas cannot, so it gets a copy resolved against this element.
|
|
164
|
+
*/
|
|
165
|
+
theme() {
|
|
166
|
+
if (this.renderer !== 'canvas') {
|
|
167
|
+
return themeName;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return resolveCssValues(createTheme(this.options), this.$refs.chart);
|
|
171
|
+
},
|
|
172
|
+
/**
|
|
173
|
+
* Rebuilding the chart discards its interaction state, so the legend selection and zoom range
|
|
174
|
+
* are carried over. Only these stateful fields are copied: the full option from getOption()
|
|
175
|
+
* holds colours resolved against the old colour mode.
|
|
176
|
+
*/
|
|
177
|
+
preservedState() {
|
|
178
|
+
const { legend = [], dataZoom = [] } = this.chart.getOption();
|
|
179
|
+
return {
|
|
180
|
+
legend: legend.map(({ selected }) => ({ selected })),
|
|
181
|
+
dataZoom: dataZoom.map(({ start, end }) => ({ start, end })),
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
/**
|
|
185
|
+
* Colours resolved for canvas are frozen at init, so the chart has to be rebuilt when the
|
|
186
|
+
* colour mode changes. SVG re-resolves on its own and needs no subscription.
|
|
187
|
+
*/
|
|
188
|
+
observeColorMode() {
|
|
189
|
+
if (this.renderer !== 'canvas') {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
this.unsubscribeColorMode = onColorModeChange(this.$refs.chart, () => {
|
|
194
|
+
const state = this.preservedState();
|
|
195
|
+
this.chart.dispose();
|
|
196
|
+
this.initChart();
|
|
197
|
+
this.chart.setOption(state);
|
|
198
|
+
});
|
|
199
|
+
},
|
|
154
200
|
draw() {
|
|
155
201
|
this.chart.setOption(this.modifiedOptions);
|
|
156
202
|
/**
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { merge, uniq, sortBy } from 'lodash-es';
|
|
3
|
-
import {
|
|
3
|
+
import { gaugeSafeHues, gaugeWarningHue } from '../../../utils/charts/theme';
|
|
4
4
|
import Chart from '../chart/chart.vue';
|
|
5
5
|
|
|
6
|
-
const AXIS_LABEL_FONT_SIZE_PX = 14;
|
|
7
|
-
|
|
8
6
|
const ARC_RADIUS = '80%';
|
|
9
|
-
const DETAIL_FONT_SIZE_PX = 30;
|
|
10
|
-
const DETAIL_FONT_FAMILY = 'sans-serif';
|
|
11
|
-
const DETAIL_FONT_WEIGHT = 'bold';
|
|
12
7
|
const POINTER_LENGTH = '65%';
|
|
13
8
|
const POINTER_WIDTH_PX = 5;
|
|
14
9
|
|
|
@@ -22,16 +17,10 @@ const gaugeChartSeries = ({ value, text, min, max, splitNumber, axisColor }) =>
|
|
|
22
17
|
|
|
23
18
|
return text || (currentValue ?? '--');
|
|
24
19
|
},
|
|
25
|
-
color: `${gaugeNeutralHues[0]}`,
|
|
26
|
-
fontSize: DETAIL_FONT_SIZE_PX,
|
|
27
|
-
fontFamily: DETAIL_FONT_FAMILY,
|
|
28
|
-
fontWeight: DETAIL_FONT_WEIGHT,
|
|
29
20
|
},
|
|
30
21
|
axisLabel: {
|
|
31
22
|
show: true,
|
|
32
|
-
fontSize: AXIS_LABEL_FONT_SIZE_PX,
|
|
33
23
|
formatter: (theValue) => (Number.isFinite(theValue) ? theValue : '--'),
|
|
34
|
-
color: `${gaugeNeutralHues[1]}`,
|
|
35
24
|
},
|
|
36
25
|
axisLine: {
|
|
37
26
|
lineStyle: {
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { merge } from 'lodash-es';
|
|
3
|
-
import { GL_COLOR_NEUTRAL_0, GL_COLOR_NEUTRAL_100 } from '../../../tokens/build/js/tokens';
|
|
4
3
|
import { getTooltipTitle, getTooltipContent } from '../../../utils/charts/config';
|
|
5
4
|
import { HEIGHT_AUTO_CLASSES } from '../../../utils/charts/constants';
|
|
6
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
heatmapHues,
|
|
7
|
+
heatmapHuesDark,
|
|
8
|
+
heatmapSurface,
|
|
9
|
+
heatmapSurfaceDark,
|
|
10
|
+
} from '../../../utils/charts/theme';
|
|
11
|
+
import { isDarkMode, onColorModeChange } from '../../../utils/charts/color_mode';
|
|
7
12
|
import { engineeringNotation } from '../../../utils/number_utils';
|
|
8
13
|
import TooltipDefaultFormat from '../shared/tooltip/tooltip_default_format/tooltip_default_format.vue';
|
|
9
14
|
import Chart from '../chart/chart.vue';
|
|
@@ -13,9 +18,6 @@ import ChartTooltip from '../shared/tooltip/tooltip.vue';
|
|
|
13
18
|
const defaultOptions = {
|
|
14
19
|
visualMap: {
|
|
15
20
|
show: false,
|
|
16
|
-
inRange: {
|
|
17
|
-
color: heatmapHues,
|
|
18
|
-
},
|
|
19
21
|
},
|
|
20
22
|
series: {
|
|
21
23
|
type: 'heatmap',
|
|
@@ -154,9 +156,19 @@ export default {
|
|
|
154
156
|
top: '0',
|
|
155
157
|
},
|
|
156
158
|
selectedFormatTooltipText: this.formatTooltipText || this.defaultFormatTooltipText,
|
|
159
|
+
// ECharts interpolates the visualMap stops numerically, so these colours cannot be CSS
|
|
160
|
+
// custom properties and have to be picked for the current colour mode instead. Resolved on
|
|
161
|
+
// mount, because the mode depends on where this chart sits in the document.
|
|
162
|
+
isDark: false,
|
|
157
163
|
};
|
|
158
164
|
},
|
|
159
165
|
computed: {
|
|
166
|
+
hues() {
|
|
167
|
+
return this.isDark ? heatmapHuesDark : heatmapHues;
|
|
168
|
+
},
|
|
169
|
+
surface() {
|
|
170
|
+
return this.isDark ? heatmapSurfaceDark : heatmapSurface;
|
|
171
|
+
},
|
|
160
172
|
computedOptions() {
|
|
161
173
|
const { min, max } = getRange(this.dataSeries);
|
|
162
174
|
return merge(
|
|
@@ -172,11 +184,14 @@ export default {
|
|
|
172
184
|
right: '32px',
|
|
173
185
|
show: true,
|
|
174
186
|
borderWidth: 0,
|
|
175
|
-
backgroundColor:
|
|
187
|
+
backgroundColor: this.surface.plate,
|
|
176
188
|
},
|
|
177
189
|
visualMap: {
|
|
178
190
|
min,
|
|
179
191
|
max,
|
|
192
|
+
inRange: {
|
|
193
|
+
color: this.hues,
|
|
194
|
+
},
|
|
180
195
|
},
|
|
181
196
|
xAxis: {
|
|
182
197
|
data: this.xAxisLabels,
|
|
@@ -196,7 +211,7 @@ export default {
|
|
|
196
211
|
show: true,
|
|
197
212
|
interval: 0,
|
|
198
213
|
lineStyle: {
|
|
199
|
-
color:
|
|
214
|
+
color: this.surface.separator,
|
|
200
215
|
width: 2,
|
|
201
216
|
},
|
|
202
217
|
},
|
|
@@ -223,7 +238,7 @@ export default {
|
|
|
223
238
|
show: true,
|
|
224
239
|
interval: 0,
|
|
225
240
|
lineStyle: {
|
|
226
|
-
color:
|
|
241
|
+
color: this.surface.separator,
|
|
227
242
|
width: 2,
|
|
228
243
|
},
|
|
229
244
|
},
|
|
@@ -240,9 +255,9 @@ export default {
|
|
|
240
255
|
},
|
|
241
256
|
seriesInfo() {
|
|
242
257
|
const { min, max } = getRange(this.dataSeries);
|
|
243
|
-
const step = (max - min) /
|
|
258
|
+
const step = (max - min) / this.hues.length;
|
|
244
259
|
|
|
245
|
-
return
|
|
260
|
+
return this.hues.map((color, index) => {
|
|
246
261
|
const lowerBound = engineeringNotation(min + step * index);
|
|
247
262
|
const upperBound = engineeringNotation(min + step * (index + 1));
|
|
248
263
|
|
|
@@ -257,6 +272,15 @@ export default {
|
|
|
257
272
|
return this.height === 'auto';
|
|
258
273
|
},
|
|
259
274
|
},
|
|
275
|
+
mounted() {
|
|
276
|
+
this.isDark = isDarkMode(this.$el);
|
|
277
|
+
this.unsubscribeColorMode = onColorModeChange(this.$el, (isDark) => {
|
|
278
|
+
this.isDark = isDark;
|
|
279
|
+
});
|
|
280
|
+
},
|
|
281
|
+
beforeDestroy() {
|
|
282
|
+
this.unsubscribeColorMode?.();
|
|
283
|
+
},
|
|
260
284
|
methods: {
|
|
261
285
|
defaultFormatTooltipText(params) {
|
|
262
286
|
this.tooltip.title = getTooltipTitle(params, this.computedOptions.xAxis.name);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import * as echarts from 'echarts';
|
|
3
3
|
import { defaultFontSize } from '../../../utils/charts/config';
|
|
4
|
+
import { defaultFontFamily } from '../../../utils/charts/theme';
|
|
4
5
|
import {
|
|
5
6
|
LEGEND_LAYOUT_INLINE,
|
|
6
7
|
LEGEND_LAYOUT_TABLE,
|
|
@@ -102,7 +103,7 @@ export default {
|
|
|
102
103
|
computed: {
|
|
103
104
|
fontStyle() {
|
|
104
105
|
return {
|
|
105
|
-
fontFamily: this.textStyle.fontFamily ||
|
|
106
|
+
fontFamily: this.textStyle.fontFamily || defaultFontFamily,
|
|
106
107
|
fontSize: `${this.textStyle.fontSize || defaultFontSize}px`,
|
|
107
108
|
};
|
|
108
109
|
},
|
|
@@ -110,6 +111,17 @@ export default {
|
|
|
110
111
|
return this.seriesInfo.length === 1;
|
|
111
112
|
},
|
|
112
113
|
},
|
|
114
|
+
watch: {
|
|
115
|
+
/**
|
|
116
|
+
* When the canvas renderer rebuilds the chart on a colour-mode change it disposes the old
|
|
117
|
+
* ECharts instance and creates a new one. The parent emits `created` with the new instance and
|
|
118
|
+
* updates this prop, so we must rebind our persistent listener to the new instance.
|
|
119
|
+
*/
|
|
120
|
+
chart(newChart, oldChart) {
|
|
121
|
+
oldChart.off('legendselectchanged', this.suppressLastActiveSeriesLabelToggle);
|
|
122
|
+
newChart.on('legendselectchanged', this.suppressLastActiveSeriesLabelToggle);
|
|
123
|
+
},
|
|
124
|
+
},
|
|
113
125
|
created() {
|
|
114
126
|
this.chart.on('legendselectchanged', this.suppressLastActiveSeriesLabelToggle);
|
|
115
127
|
},
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The classes that decide which set of token values applies to an element.
|
|
3
|
+
*
|
|
4
|
+
* Dark mode is applied either to the whole document, by putting `gl-dark` on the root, or to a
|
|
5
|
+
* subtree, by putting `gl-dark-scope` on any ancestor. `gl-light-scope` flips a subtree back.
|
|
6
|
+
*
|
|
7
|
+
* These are the single source of truth: `bin/build_tokens.mjs` builds the CSS selectors from them,
|
|
8
|
+
* so the stylesheets and any JavaScript that has to reason about the colour mode cannot drift
|
|
9
|
+
* apart. Changing a name here changes both.
|
|
10
|
+
*/
|
|
11
|
+
export const DARK_ROOT_CLASS = 'gl-dark';
|
|
12
|
+
export const DARK_SCOPE_CLASS = 'gl-dark-scope';
|
|
13
|
+
export const LIGHT_SCOPE_CLASS = 'gl-light-scope';
|
|
14
|
+
|
|
15
|
+
export const LIGHT_SELECTOR = `:root, .${LIGHT_SCOPE_CLASS}`;
|
|
16
|
+
export const DARK_SELECTOR = `:root.${DARK_ROOT_CLASS}, .${DARK_SCOPE_CLASS}`;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { DARK_ROOT_CLASS, DARK_SCOPE_CLASS, LIGHT_SCOPE_CLASS } from '../../tokens/color_mode';
|
|
2
|
+
|
|
3
|
+
const SCOPE_CLASSES = [DARK_SCOPE_CLASS, LIGHT_SCOPE_CLASS, DARK_ROOT_CLASS];
|
|
4
|
+
|
|
5
|
+
const SCOPE_SELECTOR = SCOPE_CLASSES.map((scopeClass) => `.${scopeClass}`).join(', ');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Whether the given element sits in dark mode.
|
|
9
|
+
*
|
|
10
|
+
* Colour mode is not a document-wide fact. Tokens are declared for the root dark class and for a
|
|
11
|
+
* dark scope class, so any subtree can be flipped independently, and a light scope can flip one
|
|
12
|
+
* back inside a dark document. The nearest scope wins, which is what `closest` gives us.
|
|
13
|
+
*
|
|
14
|
+
* This lives under `charts` rather than in the general utilities because charts are the only thing
|
|
15
|
+
* in the package that needs it. Everywhere else, colour follows tokens and CSS re-resolves them on
|
|
16
|
+
* its own. Charts cannot always do that: ECharts parses colours numerically for its visual maps,
|
|
17
|
+
* and canvas cannot read custom properties at all, so those values have to be chosen in JavaScript
|
|
18
|
+
* and re-chosen when the mode changes.
|
|
19
|
+
*/
|
|
20
|
+
export const isDarkMode = (element) => {
|
|
21
|
+
const scope = element?.closest?.(SCOPE_SELECTOR);
|
|
22
|
+
|
|
23
|
+
return Boolean(scope) && !scope.classList.contains(LIGHT_SCOPE_CLASS);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Keyed by subscription so that each subscribed element's last known mode is tracked separately.
|
|
27
|
+
const subscriptions = new Map();
|
|
28
|
+
let observer = null;
|
|
29
|
+
|
|
30
|
+
// Most class mutations (hover states, dropdowns opening and closing) have nothing to do with the
|
|
31
|
+
// colour mode. Only mutations that added or removed a scope class can change it, so anything else
|
|
32
|
+
// can be dismissed with this cheap check instead of re-checking every subscriber.
|
|
33
|
+
const touchesColorMode = ({ target, oldValue }) =>
|
|
34
|
+
SCOPE_CLASSES.some(
|
|
35
|
+
(scopeClass) => target.classList?.contains(scopeClass) || oldValue?.includes(scopeClass),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const notify = () => {
|
|
39
|
+
subscriptions.forEach(({ element, subscriber, mode }, key) => {
|
|
40
|
+
const current = isDarkMode(element);
|
|
41
|
+
|
|
42
|
+
if (current !== mode) {
|
|
43
|
+
subscriptions.set(key, { element, subscriber, mode: current });
|
|
44
|
+
subscriber(current);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Calls back when the colour mode of `element` changes, and returns a function that unsubscribes.
|
|
51
|
+
*
|
|
52
|
+
* Most colour can be left to CSS, which re-resolves custom properties on its own. This exists for
|
|
53
|
+
* the places that cannot: colours ECharts parses numerically, such as the interpolated stops of a
|
|
54
|
+
* continuous `visualMap`, and colours handed to a canvas context. Both need a concrete value picked
|
|
55
|
+
* in JavaScript, so both need telling when the mode flips.
|
|
56
|
+
*
|
|
57
|
+
* Because a scope class can live on any ancestor, this watches the whole document for class
|
|
58
|
+
* changes, then re-checks each subscribed element and only calls back when its own mode changed.
|
|
59
|
+
*/
|
|
60
|
+
export const onColorModeChange = (element, subscriber) => {
|
|
61
|
+
if (!subscriptions.size && typeof MutationObserver !== 'undefined') {
|
|
62
|
+
observer = new MutationObserver((mutations) => {
|
|
63
|
+
if (mutations.some((mutation) => touchesColorMode(mutation))) notify();
|
|
64
|
+
});
|
|
65
|
+
observer.observe(document.documentElement, {
|
|
66
|
+
attributeFilter: ['class'],
|
|
67
|
+
attributeOldValue: true,
|
|
68
|
+
subtree: true,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const key = Symbol('colorModeSubscription');
|
|
73
|
+
subscriptions.set(key, { element, subscriber, mode: isDarkMode(element) });
|
|
74
|
+
|
|
75
|
+
return () => {
|
|
76
|
+
subscriptions.delete(key);
|
|
77
|
+
|
|
78
|
+
if (!subscriptions.size) {
|
|
79
|
+
observer?.disconnect();
|
|
80
|
+
observer = null;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { mapValues } from 'lodash-es';
|
|
2
|
+
|
|
3
|
+
const CSS_VAR = /^var\(\s*(--[\w-]+)\s*(?:,\s*([\s\S]*?)\s*)?\)$/;
|
|
4
|
+
|
|
5
|
+
const resolveString = (value, styles) => {
|
|
6
|
+
if (value.toLowerCase() === 'currentcolor') {
|
|
7
|
+
return styles.color;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const match = CSS_VAR.exec(value);
|
|
11
|
+
if (!match) {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const [, name, fallback] = match;
|
|
16
|
+
const resolved = styles.getPropertyValue(name).trim();
|
|
17
|
+
|
|
18
|
+
if (resolved) {
|
|
19
|
+
return resolved;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return fallback ? resolveString(fallback, styles) : value;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Deeply replaces `var(--token)` and `currentcolor` strings with the values they resolve to for the
|
|
27
|
+
* given element.
|
|
28
|
+
*
|
|
29
|
+
* The SVG renderer needs none of this: it emits both forms into the DOM, where the browser resolves
|
|
30
|
+
* them and keeps them live across a colour-mode change. Canvas has no such context. `fillStyle` is
|
|
31
|
+
* a plain string setter, so an unparseable `var(--token)` is dropped on the floor and leaves the
|
|
32
|
+
* previous fill in place, while `currentcolor` has no element to inherit from and is defined to
|
|
33
|
+
* resolve to opaque black. Either way a canvas-rendered chart silently loses its themed colours.
|
|
34
|
+
*
|
|
35
|
+
* Resolution is relative to `element` rather than the document root so that `.gl-dark-scope`
|
|
36
|
+
* ancestors are honoured.
|
|
37
|
+
*
|
|
38
|
+
* Values are resolved once, so callers are responsible for resolving again when the colour mode
|
|
39
|
+
* changes.
|
|
40
|
+
*/
|
|
41
|
+
export const resolveCssValues = (input, element) => {
|
|
42
|
+
const styles = window.getComputedStyle(element);
|
|
43
|
+
|
|
44
|
+
const walk = (value) => {
|
|
45
|
+
if (typeof value === 'string') {
|
|
46
|
+
return resolveString(value, styles);
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(value)) {
|
|
49
|
+
return value.map((item) => walk(item));
|
|
50
|
+
}
|
|
51
|
+
// Themes carry functions (formatters) which must pass through untouched.
|
|
52
|
+
if (value !== null && typeof value === 'object') {
|
|
53
|
+
return mapValues(value, walk);
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return walk(input);
|
|
59
|
+
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
+
GL_COLOR_NEUTRAL_0,
|
|
2
3
|
GL_COLOR_NEUTRAL_50,
|
|
3
4
|
GL_COLOR_NEUTRAL_100,
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
GL_COLOR_NEUTRAL_800,
|
|
6
|
+
GL_COLOR_NEUTRAL_950,
|
|
6
7
|
GL_COLOR_DATA_AQUA_50,
|
|
7
8
|
GL_COLOR_DATA_AQUA_100,
|
|
8
9
|
GL_COLOR_DATA_AQUA_200,
|
|
@@ -58,6 +59,7 @@ import {
|
|
|
58
59
|
GL_COLOR_DATA_ORANGE_800,
|
|
59
60
|
GL_COLOR_DATA_ORANGE_950,
|
|
60
61
|
GL_COLOR_DATA_ORANGE_900,
|
|
62
|
+
GL_FONT_FAMILY_REGULAR,
|
|
61
63
|
} from '../../tokens/build/js/tokens';
|
|
62
64
|
import {
|
|
63
65
|
scrollHandleSvgPath,
|
|
@@ -66,20 +68,45 @@ import {
|
|
|
66
68
|
clearAllSvgPath,
|
|
67
69
|
downloadSvgPath,
|
|
68
70
|
} from '../svgs/svg_paths';
|
|
71
|
+
import { defaultFontSize } from './config';
|
|
69
72
|
|
|
70
73
|
const GL_BORDER_RADIUS_BASE = '0.25rem';
|
|
71
74
|
|
|
72
75
|
export const themeName = 'gitlab';
|
|
73
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Sequential scales step away from the surface so that higher values gain contrast against it:
|
|
79
|
+
* darker on a light surface, lighter on a dark one. The lowest step is neutral rather than blue, so
|
|
80
|
+
* that "no data" reads as absence instead of as the bottom of the data range.
|
|
81
|
+
*
|
|
82
|
+
* https://design.gitlab.com/data-visualization/color#sequential-data
|
|
83
|
+
*/
|
|
74
84
|
export const heatmapHues = [
|
|
75
85
|
GL_COLOR_NEUTRAL_100,
|
|
76
|
-
|
|
77
|
-
GL_COLOR_DATA_BLUE_400,
|
|
86
|
+
GL_COLOR_DATA_BLUE_500,
|
|
78
87
|
GL_COLOR_DATA_BLUE_600,
|
|
79
88
|
GL_COLOR_DATA_BLUE_800,
|
|
89
|
+
GL_COLOR_DATA_BLUE_950,
|
|
80
90
|
];
|
|
81
91
|
|
|
82
|
-
export const
|
|
92
|
+
export const heatmapHuesDark = [
|
|
93
|
+
GL_COLOR_NEUTRAL_800,
|
|
94
|
+
GL_COLOR_DATA_BLUE_600,
|
|
95
|
+
GL_COLOR_DATA_BLUE_500,
|
|
96
|
+
GL_COLOR_DATA_BLUE_400,
|
|
97
|
+
GL_COLOR_DATA_BLUE_200,
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The plate behind the cells, and the separators between them. Separators match the surface so that
|
|
102
|
+
* neighbouring cells, which are only guaranteed to contrast with the surface and not with each
|
|
103
|
+
* other, stay distinguishable.
|
|
104
|
+
*
|
|
105
|
+
* https://design.gitlab.com/data-visualization/color#visual-separators
|
|
106
|
+
*/
|
|
107
|
+
export const heatmapSurface = { plate: GL_COLOR_NEUTRAL_100, separator: GL_COLOR_NEUTRAL_0 };
|
|
108
|
+
export const heatmapSurfaceDark = { plate: GL_COLOR_NEUTRAL_800, separator: GL_COLOR_NEUTRAL_950 };
|
|
109
|
+
|
|
83
110
|
export const gaugeSafeHues = [GL_COLOR_DATA_BLUE_500, GL_COLOR_DATA_BLUE_900];
|
|
84
111
|
export const gaugeWarningHue = GL_COLOR_DATA_ORANGE_500;
|
|
85
112
|
|
|
@@ -159,12 +186,33 @@ export const colorPaletteDark = [
|
|
|
159
186
|
|
|
160
187
|
export const colorFromDarkPalette = (index) => colorPaletteDark[index % colorPaletteDark.length];
|
|
161
188
|
|
|
189
|
+
// ECharts lays out text by assigning a CSS font shorthand to a canvas context and calling
|
|
190
|
+
// `measureText`, even under the SVG renderer. Canvas does not substitute CSS custom properties,
|
|
191
|
+
// and an unparseable shorthand is rejected wholesale, leaving text measured at the canvas default
|
|
192
|
+
// of 10px sans-serif. So the chart font stack must be concrete: drop the leading
|
|
193
|
+
// `var(--default-regular-font, 'GitLab Sans')` consumer override, whose own fallback is the
|
|
194
|
+
// 'GitLab Sans' that follows it.
|
|
195
|
+
export const defaultFontFamily = GL_FONT_FAMILY_REGULAR.filter(
|
|
196
|
+
(family) => !family.startsWith('var('),
|
|
197
|
+
).join(', ');
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Chart font sizes are concrete pixel numbers rather than the rem-based font.size tokens they
|
|
201
|
+
* mirror. zrender measures text on a detached canvas, which always resolves 1rem as 16px however
|
|
202
|
+
* the document root is set, so rem sizes render scaled while being laid out at the wrong width.
|
|
203
|
+
*
|
|
204
|
+
* `defaultFontSize` (12, font.size.100) is set per component in the theme below, because ECharts
|
|
205
|
+
* pins fontSize in its own component defaults (axis labels at 12, title at 18, gauge detail at 30)
|
|
206
|
+
* and those shadow `textStyle`.
|
|
207
|
+
*/
|
|
208
|
+
|
|
162
209
|
const axes = {
|
|
163
210
|
axisLabel: {
|
|
164
211
|
margin: 8,
|
|
165
212
|
show: true,
|
|
166
213
|
color: 'var(--gl-chart-axis-text-color)',
|
|
167
214
|
hideOverlap: true,
|
|
215
|
+
fontSize: defaultFontSize,
|
|
168
216
|
},
|
|
169
217
|
axisLine: {
|
|
170
218
|
show: false,
|
|
@@ -188,6 +236,7 @@ const axes = {
|
|
|
188
236
|
nameGap: 30,
|
|
189
237
|
nameTextStyle: {
|
|
190
238
|
fontWeight: 'bold',
|
|
239
|
+
fontSize: defaultFontSize,
|
|
191
240
|
},
|
|
192
241
|
splitLine: {
|
|
193
242
|
lineStyle: {
|
|
@@ -208,6 +257,24 @@ export const createTheme = (options = {}) => ({
|
|
|
208
257
|
backgroundColor: 'transparent',
|
|
209
258
|
textStyle: {
|
|
210
259
|
color: 'var(--gl-text-color-default)',
|
|
260
|
+
fontFamily: defaultFontFamily,
|
|
261
|
+
fontSize: defaultFontSize,
|
|
262
|
+
},
|
|
263
|
+
title: {
|
|
264
|
+
textStyle: {
|
|
265
|
+
fontSize: 18, // font.size.500-fixed, 1.125rem
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
gauge: {
|
|
269
|
+
axisLabel: {
|
|
270
|
+
fontSize: 14, // font.size.300, 0.875rem, the default body size
|
|
271
|
+
color: 'var(--gl-chart-axis-text-color)',
|
|
272
|
+
},
|
|
273
|
+
detail: {
|
|
274
|
+
fontSize: 30,
|
|
275
|
+
fontWeight: 'bold',
|
|
276
|
+
color: 'var(--gl-text-color-default)',
|
|
277
|
+
},
|
|
211
278
|
},
|
|
212
279
|
markLine: {
|
|
213
280
|
silent: true,
|