[ML] Remove all usage of font awesome icons in ML plugin (#84066) (#84130)

This commit is contained in:
Pete Harverson 2020-11-23 21:36:58 +00:00 committed by GitHub
parent 1223d666f4
commit 885af43ba1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 210 additions and 276 deletions

View file

@ -23,7 +23,6 @@
@import 'components/controls/index';
@import 'components/entity_cell/index';
@import 'components/field_title_bar/index';
@import 'components/field_type_icon/index';
@import 'components/influencers_list/index';
@import 'components/items_grid/index';
@import 'components/job_selector/index';

View file

@ -120,8 +120,6 @@ export const renderApp = (
urlGenerators: deps.share.urlGenerators,
});
deps.kibanaLegacy.loadFontAwesome();
appMountParams.onAppLeave((actions) => actions.default());
const mlLicense = setLicenseCache(deps.licensing, [

View file

@ -12,7 +12,7 @@
.field-type-icon {
vertical-align: middle;
padding-right: $euiSizeXS;
margin-bottom: -$euiSizeXS;
display: inline-block;
}

View file

@ -5,26 +5,22 @@
*/
import { mountWithIntl } from '@kbn/test/jest';
import React from 'react';
import { FieldTitleBar } from './field_title_bar';
// helper to let PropTypes throw errors instead of just doing console.error()
const error = console.error;
console.error = (warning, ...args) => {
if (/(Invalid prop|Failed prop type)/gi.test(warning)) {
throw new Error(warning);
}
error.apply(console, [warning, ...args]);
};
import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types';
describe('FieldTitleBar', () => {
test(`throws an error because card is a required prop`, () => {
expect(() => <FieldTitleBar />).toThrow();
});
test(`card prop is an empty object`, () => {
const props = { card: {} };
const props = {
card: {
type: ML_JOB_FIELD_TYPES.NUMBER,
existsInDocs: true,
loading: false,
aggregatable: true,
},
};
const wrapper = mountWithIntl(<FieldTitleBar {...props} />);
@ -36,29 +32,43 @@ describe('FieldTitleBar', () => {
});
test(`card.isUnsupportedType is true`, () => {
const testFieldName = 'foo';
const props = { card: { fieldName: testFieldName, isUnsupportedType: true } };
const props = {
card: {
type: ML_JOB_FIELD_TYPES.UNKNOWN,
fieldName: 'foo',
existsInDocs: true,
loading: false,
aggregatable: true,
isUnsupportedType: true,
},
};
const wrapper = mountWithIntl(<FieldTitleBar {...props} />);
const fieldName = wrapper.find({ className: 'field-name' }).text();
expect(fieldName).toEqual(testFieldName);
expect(fieldName).toEqual(props.card.fieldName);
const hasClassName = wrapper.find('EuiText').hasClass('type-other');
expect(hasClassName).toBeTruthy();
});
test(`card.fieldName and card.type is set`, () => {
const testFieldName = 'foo';
const testType = 'bar';
const props = { card: { fieldName: testFieldName, type: testType } };
const props = {
card: {
type: ML_JOB_FIELD_TYPES.KEYWORD,
fieldName: 'bar',
existsInDocs: true,
loading: false,
aggregatable: true,
},
};
const wrapper = mountWithIntl(<FieldTitleBar {...props} />);
const fieldName = wrapper.find({ className: 'field-name' }).text();
expect(fieldName).toEqual(testFieldName);
expect(fieldName).toEqual(props.card.fieldName);
const hasClassName = wrapper.find('EuiText').hasClass(testType);
const hasClassName = wrapper.find('EuiText').hasClass(props.card.type);
expect(hasClassName).toBeTruthy();
});
@ -66,11 +76,19 @@ describe('FieldTitleBar', () => {
// Use fake timers so we don't have to wait for the EuiToolTip timeout
jest.useFakeTimers();
const props = { card: { fieldName: 'foo', type: 'bar' } };
const props = {
card: {
type: ML_JOB_FIELD_TYPES.KEYWORD,
fieldName: 'bar',
existsInDocs: true,
loading: false,
aggregatable: true,
},
};
const wrapper = mountWithIntl(<FieldTitleBar {...props} />);
const container = wrapper.find({ className: 'field-name' });
expect(wrapper.find('EuiToolTip').children()).toHaveLength(1);
expect(wrapper.find('EuiToolTip').children()).toHaveLength(2);
container.simulate('mouseover');
@ -78,7 +96,7 @@ describe('FieldTitleBar', () => {
jest.runAllTimers();
wrapper.update();
expect(wrapper.find('EuiToolTip').children()).toHaveLength(2);
expect(wrapper.find('EuiToolTip').children()).toHaveLength(3);
container.simulate('mouseout');
@ -86,7 +104,7 @@ describe('FieldTitleBar', () => {
jest.runAllTimers();
wrapper.update();
expect(wrapper.find('EuiToolTip').children()).toHaveLength(1);
expect(wrapper.find('EuiToolTip').children()).toHaveLength(2);
// Clearing all mocks will also reset fake timers.
jest.clearAllMocks();

View file

@ -4,21 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
import PropTypes from 'prop-types';
import React from 'react';
import React, { FC } from 'react';
import { EuiText, EuiToolTip } from '@elastic/eui';
import { FieldTypeIcon } from '../field_type_icon';
import { getMLJobTypeAriaLabel } from '../../util/field_types_utils';
import { i18n } from '@kbn/i18n';
export function FieldTitleBar({ card }) {
// don't render and fail gracefully if card prop isn't set
if (typeof card !== 'object' || card === null) {
return null;
}
import { FieldTypeIcon } from '../field_type_icon';
import { FieldVisConfig } from '../../datavisualizer/index_based/common';
import { getMLJobTypeAriaLabel } from '../../util/field_types_utils';
interface Props {
card: FieldVisConfig;
}
export const FieldTitleBar: FC<Props> = ({ card }) => {
const fieldName =
card.fieldName ||
i18n.translate('xpack.ml.fieldTitleBar.documentCountLabel', {
@ -37,20 +37,23 @@ export function FieldTitleBar({ card }) {
}
if (card.isUnsupportedType !== true) {
cardTitleAriaLabel.unshift(getMLJobTypeAriaLabel(card.type));
// All the supported field types have aria labels.
cardTitleAriaLabel.unshift(getMLJobTypeAriaLabel(card.type)!);
}
return (
<EuiText className={classNames.join(' ')}>
<FieldTypeIcon type={card.type} tooltipEnabled={true} needsAria={false} />
<FieldTypeIcon
type={card.type}
fieldName={card.fieldName}
tooltipEnabled={true}
needsAria={false}
/>
<EuiToolTip position="left" content={fieldName}>
<div className="field-name" tabIndex="0" aria-label={`${cardTitleAriaLabel.join(', ')}`}>
<div className="field-name" tabIndex={0} aria-label={`${cardTitleAriaLabel.join(', ')}`}>
{fieldName}
</div>
</EuiToolTip>
</EuiText>
);
}
FieldTitleBar.propTypes = {
card: PropTypes.object.isRequired,
};

View file

@ -1,19 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FieldTypeIcon render component when type matches a field type 1`] = `
<FieldTypeIconContainer
ariaLabel="keyword type"
className="field-type-icon"
iconChar="t"
needsAria={true}
/>
`;
exports[`FieldTypeIcon update component 1`] = `
<FieldTypeIconContainer
ariaLabel="ip type"
className="field-type-icon kuiIcon fa-laptop"
iconChar=""
needsAria={true}
/>
`;

View file

@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FieldTypeIcon render component when type matches a field type 1`] = `
<EuiToolTip
content="keyword type"
delay="regular"
position="left"
>
<FieldTypeIconContainer
ariaLabel="keyword type"
color="euiColorVis0"
iconType="tokenText"
needsAria={false}
/>
</EuiToolTip>
`;

View file

@ -1,22 +0,0 @@
$icon-size: 20px;
.field-type-icon-container {
display: inline-block !important;
vertical-align: middle;
border: 1px solid;
border-radius: 4px;
width: $icon-size;
height: $icon-size;
line-height: $icon-size;
text-align: center;
position: relative;
.field-type-icon {
padding: 0;
display: inline-block !important;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}

View file

@ -1 +0,0 @@
@import 'field_type_icon';

View file

@ -11,18 +11,10 @@ import { FieldTypeIcon } from './field_type_icon';
import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types';
describe('FieldTypeIcon', () => {
test(`don't render component when type is undefined`, () => {
const typeIconComponent = shallow(<FieldTypeIcon />);
expect(typeIconComponent.isEmptyRender()).toBeTruthy();
});
test(`don't render component when type doesn't match a field type`, () => {
const typeIconComponent = shallow(<FieldTypeIcon type="foo" />);
expect(typeIconComponent.isEmptyRender()).toBeTruthy();
});
test(`render component when type matches a field type`, () => {
const typeIconComponent = shallow(<FieldTypeIcon type={ML_JOB_FIELD_TYPES.KEYWORD} />);
const typeIconComponent = shallow(
<FieldTypeIcon type={ML_JOB_FIELD_TYPES.KEYWORD} tooltipEnabled={true} needsAria={false} />
);
expect(typeIconComponent).toMatchSnapshot();
});
@ -31,9 +23,9 @@ describe('FieldTypeIcon', () => {
jest.useFakeTimers();
const typeIconComponent = mount(
<FieldTypeIcon type={ML_JOB_FIELD_TYPES.KEYWORD} tooltipEnabled={true} />
<FieldTypeIcon type={ML_JOB_FIELD_TYPES.KEYWORD} tooltipEnabled={true} needsAria={false} />
);
const container = typeIconComponent.find({ className: 'field-type-icon-container' });
const container = typeIconComponent.find({ 'data-test-subj': 'mlFieldTypeIcon' });
expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(1);
@ -56,11 +48,4 @@ describe('FieldTypeIcon', () => {
// Clearing all mocks will also reset fake timers.
jest.clearAllMocks();
});
test(`update component`, () => {
const typeIconComponent = shallow(<FieldTypeIcon />);
expect(typeIconComponent.isEmptyRender()).toBeTruthy();
typeIconComponent.setProps({ type: ML_JOB_FIELD_TYPES.IP });
expect(typeIconComponent).toMatchSnapshot();
});
});

View file

@ -4,63 +4,80 @@
* you may not use this file except in compliance with the Elastic License.
*/
import PropTypes from 'prop-types';
import React from 'react';
import React, { FC } from 'react';
import { EuiToolTip } from '@elastic/eui';
import { EuiToken, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { getMLJobTypeAriaLabel } from '../../util/field_types_utils';
import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types';
import { i18n } from '@kbn/i18n';
export const FieldTypeIcon = ({ tooltipEnabled = false, type, needsAria = true }) => {
interface FieldTypeIconProps {
tooltipEnabled: boolean;
type: ML_JOB_FIELD_TYPES;
fieldName?: string;
needsAria: boolean;
}
interface FieldTypeIconContainerProps {
ariaLabel: string | null;
iconType: string;
color: string;
needsAria: boolean;
[key: string]: any;
}
export const FieldTypeIcon: FC<FieldTypeIconProps> = ({
tooltipEnabled = false,
type,
fieldName,
needsAria = true,
}) => {
const ariaLabel = getMLJobTypeAriaLabel(type);
if (ariaLabel === null) {
// All ml job field types should have associated aria labels.
// Once it is missing, it means that the passed *type* is not a valid field type.
// if type doesn't match one of ML_JOB_FIELD_TYPES
// don't render the component at all
return null;
}
const iconClass = ['field-type-icon'];
let iconChar = '';
let iconType = 'questionInCircle';
let color = 'euiColorVis6';
switch (type) {
// icon class names
// Set icon types and colors
case ML_JOB_FIELD_TYPES.BOOLEAN:
iconClass.push('kuiIcon', 'fa-adjust');
iconType = 'tokenBoolean';
color = 'euiColorVis5';
break;
case ML_JOB_FIELD_TYPES.DATE:
iconClass.push('kuiIcon', 'fa-clock-o');
iconType = 'tokenDate';
color = 'euiColorVis7';
break;
case ML_JOB_FIELD_TYPES.GEO_POINT:
iconClass.push('kuiIcon', 'fa-globe');
iconType = 'tokenGeo';
color = 'euiColorVis8';
break;
case ML_JOB_FIELD_TYPES.TEXT:
iconClass.push('kuiIcon', 'fa-file-text-o');
iconType = 'document';
color = 'euiColorVis9';
break;
case ML_JOB_FIELD_TYPES.IP:
iconClass.push('kuiIcon', 'fa-laptop');
iconType = 'tokenIP';
color = 'euiColorVis3';
break;
// icon chars
case ML_JOB_FIELD_TYPES.KEYWORD:
iconChar = 't';
iconType = 'tokenText';
color = 'euiColorVis0';
break;
case ML_JOB_FIELD_TYPES.NUMBER:
iconChar = '#';
iconType = 'tokenNumber';
color = fieldName !== undefined ? 'euiColorVis1' : 'euiColorVis2';
break;
case ML_JOB_FIELD_TYPES.UNKNOWN:
iconChar = '?';
// Use defaults
break;
}
const containerProps = {
ariaLabel,
className: iconClass.join(' '),
iconChar,
iconType,
color,
needsAria,
};
@ -84,28 +101,27 @@ export const FieldTypeIcon = ({ tooltipEnabled = false, type, needsAria = true }
return <FieldTypeIconContainer {...containerProps} />;
};
FieldTypeIcon.propTypes = {
tooltipEnabled: PropTypes.bool,
type: PropTypes.string,
};
// If the tooltip is used, it will apply its events to its first inner child.
// To pass on its properties we apply `rest` to the outer `span` element.
function FieldTypeIconContainer({ ariaLabel, className, iconChar, needsAria, ...rest }) {
const wrapperProps = { className };
const FieldTypeIconContainer: FC<FieldTypeIconContainerProps> = ({
ariaLabel,
iconType,
color,
needsAria,
...rest
}) => {
const wrapperProps: { className: string; 'aria-label'?: string } = {
className: 'field-type-icon',
};
if (needsAria && ariaLabel) {
wrapperProps['aria-label'] = ariaLabel;
}
return (
<span className="field-type-icon-container" {...rest}>
{iconChar === '' ? (
<span {...wrapperProps} />
) : (
<span {...wrapperProps}>
<strong aria-hidden="true">{iconChar}</strong>
</span>
)}
<span data-test-subj="mlFieldTypeIcon" {...rest}>
<span {...wrapperProps}>
<EuiToken iconType={iconType} shape="square" size="s" color={color} />
</span>
</span>
);
}
};

View file

@ -15,83 +15,47 @@
.boolean {
color: $euiColorVis5;
border-color: $euiColorVis5;
.field-type-icon-container {
background-color: rgba($euiColorVis5, 0.2);
}
}
.date {
color: $euiColorVis7;
border-color: $euiColorVis7;
.field-type-icon-container {
background-color: rgba($euiColorVis7, 0.2);
}
}
.document_count {
color: $euiColorVis2;
border-color: $euiColorVis2;
.field-type-icon-container {
background-color: rgba($euiColorVis2, 0.2);
}
}
.geo_point {
color: $euiColorVis8;
border-color: $euiColorVis8;
.field-type-icon-container {
background-color: rgba($euiColorVis8, 0.2);
}
}
.ip {
color: $euiColorVis3;
border-color: $euiColorVis3;
.field-type-icon-container {
background-color: rgba($euiColorVis3, 0.2);
}
}
.keyword {
color: $euiColorVis0;
border-color: $euiColorVis0;
.field-type-icon-container {
background-color: rgba($euiColorVis0, 0.2);
}
}
.number {
color: $euiColorVis1;
border-color: $euiColorVis1;
.field-type-icon-container {
background-color: rgba($euiColorVis1, 0.2);
}
}
.text {
color: $euiColorVis9;
border-color: $euiColorVis9;
.field-type-icon-container {
background-color: rgba($euiColorVis9, 0.2);
}
}
.type-other,
.unknown {
color: $euiColorVis6;
border-color: $euiColorVis6;
.field-type-icon-container {
background-color: rgba($euiColorVis6, 0.2);
}
}
// Use euiPanel styling

View file

@ -5,7 +5,15 @@
*/
import React from 'react';
import { EuiSpacer, EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText, EuiProgress } from '@elastic/eui';
import {
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiPanel,
EuiProgress,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { FieldTypeIcon } from '../../../../components/field_type_icon';
@ -28,7 +36,7 @@ export function FieldStatsCard({ field }) {
<EuiPanel hasShadow={false} className="mlFieldDataCard">
<div className="ml-field-data-card" data-test-subj="mlPageFileDataVisFieldDataCard">
<div className={`ml-field-title-bar ${type}`}>
<FieldTypeIcon type={type} needsAria={false} />
<FieldTypeIcon type={type} needsAria={false} fieldName={field.name} />
<div className="field-name" tabIndex="0" aria-label={`${cardTitleAriaLabel.join(', ')}`}>
{field.name}
</div>
@ -38,29 +46,45 @@ export function FieldStatsCard({ field }) {
{field.count > 0 && (
<React.Fragment>
<div className="stats">
<div className="stat">
<i className="fa fa-files-o" aria-hidden="true" />
&nbsp;
<FormattedMessage
id="xpack.ml.fileDatavisualizer.fieldStatsCard.documentsCountDescription"
defaultMessage="{fieldCount, plural, zero {# document} one {# document} other {# documents}} ({fieldPercent}%)"
values={{
fieldCount: field.count,
fieldPercent: field.percent,
}}
/>
</div>
<div className="stat">
<i className="fa fa-cubes" aria-hidden="true" />
&nbsp;
<FormattedMessage
id="xpack.ml.fileDatavisualizer.fieldStatsCard.distinctCountDescription"
defaultMessage="{fieldCardinality} distinct {fieldCardinality, plural, zero {value} one {value} other {values}}"
values={{
fieldCardinality: field.cardinality,
}}
/>
</div>
<EuiFlexGroup
gutterSize="xs"
alignItems="center"
justifyContent="center"
className="stat"
>
<EuiFlexItem grow={false}>
<EuiIcon type="document" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<FormattedMessage
id="xpack.ml.fileDatavisualizer.fieldStatsCard.documentsCountDescription"
defaultMessage="{fieldCount, plural, zero {# document} one {# document} other {# documents}} ({fieldPercent}%)"
values={{
fieldCount: field.count,
fieldPercent: field.percent,
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
gutterSize="xs"
alignItems="center"
justifyContent="center"
className="stat"
>
<EuiFlexItem grow={false}>
<EuiIcon type="database" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<FormattedMessage
id="xpack.ml.fileDatavisualizer.fieldStatsCard.distinctCountDescription"
defaultMessage="{fieldCardinality} distinct {fieldCardinality, plural, zero {value} one {value} other {values}}"
values={{
fieldCardinality: field.cardinality,
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
{field.median_value && (
<React.Fragment>

View file

@ -7,83 +7,47 @@
.boolean {
color: $euiColorVis5;
border-color: $euiColorVis5;
.field-type-icon-container {
background-color: rgba($euiColorVis5, 0.2);
}
}
.date {
color: $euiColorVis7;
border-color: $euiColorVis7;
.field-type-icon-container {
background-color: rgba($euiColorVis7, 0.2);
}
}
.document_count {
color: $euiColorVis2;
border-color: $euiColorVis2;
.field-type-icon-container {
background-color: rgba($euiColorVis2, 0.2);
}
}
.geo_point {
color: $euiColorVis8;
border-color: $euiColorVis8;
.field-type-icon-container {
background-color: rgba($euiColorVis8, 0.2);
}
}
.ip {
color: $euiColorVis3;
border-color: $euiColorVis3;
.field-type-icon-container {
background-color: rgba($euiColorVis3, 0.2);
}
}
.keyword {
color: $euiColorVis0;
border-color: $euiColorVis0;
.field-type-icon-container {
background-color: rgba($euiColorVis0, 0.2);
}
}
.number {
color: $euiColorVis1;
border-color: $euiColorVis1;
.field-type-icon-container {
background-color: rgba($euiColorVis1, 0.2);
}
}
.text {
color: $euiColorVis9;
border-color: $euiColorVis9;
.field-type-icon-container {
background-color: rgba($euiColorVis9, 0.2);
}
}
.type-other,
.unknown {
color: $euiColorVis6;
border-color: $euiColorVis6;
.field-type-icon-container {
background-color: rgba($euiColorVis6, 0.2);
}
}
.mlFieldDataCard__content {

View file

@ -10,7 +10,6 @@ import React, { FC } from 'react';
import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types';
import { FieldVisConfig } from '../../common';
// @ts-ignore
import { FieldTitleBar } from '../../../../components/field_title_bar/index';
import {
BooleanContent,

View file

@ -1,8 +1,6 @@
// SASSTODO: There is a lot of very specific sizing in here that look brittle to touch
.explorer-charts {
ml-explorer-charts-container {
ml-explorer-charts-container {
.row {
padding: 10px;
}
@ -42,7 +40,7 @@
padding: 0px 0px 2px 0px;
}
table tr>td:first-child {
table tr > td:first-child {
padding-left: 2px;
vertical-align: top;
}
@ -54,20 +52,13 @@
td:nth-child(2) {
padding-left: 5px;
}
// Hide filtering icons until that functionality is available.
i.fa-search-plus, i.fa-search-minus {
display: none;
}
}
}
}
/* wrapper class for the top right alert icon and view button */
.ml-explorer-chart-icons {
float:right;
float: right;
padding-left: 5px;
/* counter-margin for EuiButtonEmpty's padding */
margin: 2px -8px 0 0;

View file

@ -166,7 +166,6 @@
}
.forecast {
.metric-value,
.metric-value:hover {
stroke: #cca300;
@ -201,7 +200,6 @@
a:focus {
text-decoration: underline;
}
}
}
@ -246,7 +244,6 @@
stroke-width: 0;
}
}
}
.swimlane .axis text {
@ -290,10 +287,8 @@
text-align: center;
cursor: ew-resize;
margin-top: 9px;
i.fa {
margin-top: 30px;
}
font-size: $euiFontSizeS;
fill: $euiColorDarkShade;
}
div.brush-handle-inner-left {

View file

@ -1237,19 +1237,23 @@ class TimeseriesChartIntl extends Component {
.attr('width', 10)
.attr('height', 90)
.attr('class', 'brush-handle')
.attr('x', contextXScale(handleBrushExtent[0]) - 10)
.html(
'<div class="brush-handle-inner brush-handle-inner-left"><i class="fa fa-caret-left"></i></div>'
);
.attr('x', contextXScale(handleBrushExtent[0]) - 10).html(`
<div class="brush-handle-inner brush-handle-inner-left" style="padding-top: 27px">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="6" height="9">
<polygon points="5,0 5,8 0,4" />
</svg>
</div>`);
const rightHandle = contextGroup
.append('foreignObject')
.attr('width', 10)
.attr('height', 90)
.attr('class', 'brush-handle')
.attr('x', contextXScale(handleBrushExtent[1]) + 0)
.html(
'<div class="brush-handle-inner brush-handle-inner-right"><i class="fa fa-caret-right"></i></div>'
);
.attr('x', contextXScale(handleBrushExtent[1]) + 0).html(`
<div class="brush-handle-inner brush-handle-inner-right" style="padding-top: 27px">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="6" height="9">
<polygon points="0,0 0,8 5,4" />
</svg>
</div>`);
function brushing() {
const brushExtent = brush.extent();