Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
ensembl-web
ensembl-client
Commits
7a328d5e
Unverified
Commit
7a328d5e
authored
Feb 06, 2020
by
Andrey Azov
Committed by
GitHub
Feb 06, 2020
Browse files
Add BasePairsRuler (#244)
This commit introduces d3 into our codebase
parent
f2a375ea
Pipeline
#59420
passed with stages
in 5 minutes and 42 seconds
Changes
11
Pipelines
1
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
948 additions
and
3 deletions
+948
-3
src/ensembl/package-lock.json
src/ensembl/package-lock.json
+503
-3
src/ensembl/package.json
src/ensembl/package.json
+2
-0
src/ensembl/src/content/app/entity-viewer/components/base-pairs-ruler/BasePairsRuler.scss
...ty-viewer/components/base-pairs-ruler/BasePairsRuler.scss
+16
-0
src/ensembl/src/content/app/entity-viewer/components/base-pairs-ruler/BasePairsRuler.test.tsx
...iewer/components/base-pairs-ruler/BasePairsRuler.test.tsx
+42
-0
src/ensembl/src/content/app/entity-viewer/components/base-pairs-ruler/BasePairsRuler.tsx
...ity-viewer/components/base-pairs-ruler/BasePairsRuler.tsx
+101
-0
src/ensembl/src/content/app/entity-viewer/components/base-pairs-ruler/basePairsRulerHelper.test.ts
.../components/base-pairs-ruler/basePairsRulerHelper.test.ts
+113
-0
src/ensembl/src/content/app/entity-viewer/components/base-pairs-ruler/basePairsRulerHelper.ts
...iewer/components/base-pairs-ruler/basePairsRulerHelper.ts
+95
-0
src/ensembl/stories/entity-viewer/base-pairs-ruler/BasePairsRuler.stories.scss
...ntity-viewer/base-pairs-ruler/BasePairsRuler.stories.scss
+23
-0
src/ensembl/stories/entity-viewer/base-pairs-ruler/BasePairsRuler.stories.tsx
...entity-viewer/base-pairs-ruler/BasePairsRuler.stories.tsx
+51
-0
src/ensembl/stories/entity-viewer/index.ts
src/ensembl/stories/entity-viewer/index.ts
+1
-0
src/ensembl/stories/index.tsx
src/ensembl/stories/index.tsx
+1
-0
No files found.
src/ensembl/package-lock.json
View file @
7a328d5e
This diff is collapsed.
Click to expand it.
src/ensembl/package.json
View file @
7a328d5e
...
...
@@ -54,6 +54,7 @@
"classnames"
:
"2.2.6"
,
"connected-react-router"
:
"6.6.1"
,
"core-js"
:
"3.6.4"
,
"d3"
:
"5.15.0"
,
"dotenv"
:
"8.2.0"
,
"ensembl-genome-browser"
:
"https://raw.githubusercontent.com/Ensembl/ensembl-genome-browser-assets/master/assets-80f51620ed443c640cdfd6b5aebd505b.tar.gz"
,
"koa-proxy"
:
"1.0.0-alpha.3"
,
...
...
@@ -91,6 +92,7 @@
"@storybook/react"
:
"5.3.6"
,
"@svgr/webpack"
:
"5.0.1"
,
"@types/classnames"
:
"2.2.9"
,
"@types/d3"
:
"5.7.2"
,
"@types/enzyme"
:
"3.10.4"
,
"@types/enzyme-adapter-react-16"
:
"1.0.5"
,
"@types/faker"
:
"4.1.9"
,
...
...
src/ensembl/src/content/app/entity-viewer/components/base-pairs-ruler/BasePairsRuler.scss
0 → 100644
View file @
7a328d5e
@import
'src/styles/common'
;
.containerSvg
{
overflow
:
visible
;
}
.axis
,
.tick
{
fill
:
$orange
;
}
.label
{
fill
:
white
;
font-size
:
10px
;
font-family
:
$font-family-monospace
;
}
src/ensembl/src/content/app/entity-viewer/components/base-pairs-ruler/BasePairsRuler.test.tsx
0 → 100644
View file @
7a328d5e
import
React
from
'
react
'
;
import
{
mount
,
render
}
from
'
enzyme
'
;
import
BasePairsRuler
from
'
./BasePairsRuler
'
;
const
defaultProps
=
{
length
:
80792
,
width
:
600
};
describe
(
'
<BasePairsRuler />
'
,
()
=>
{
describe
(
'
rendering
'
,
()
=>
{
it
(
'
renders inside an <svg> element if standalone
'
,
()
=>
{
const
wrapper
=
render
(
<
BasePairsRuler
{
...
defaultProps
}
standalone
=
{
true
}
/>
);
expect
(
wrapper
.
is
(
'
svg
'
)).
toBe
(
true
);
});
it
(
'
renders inside a <g> element (svg group) if not standalone
'
,
()
=>
{
const
wrapper
=
render
(<
BasePairsRuler
{
...
defaultProps
}
/>);
expect
(
wrapper
.
is
(
'
g
'
)).
toBe
(
true
);
});
});
describe
(
'
behaviour
'
,
()
=>
{
const
props
=
{
...
defaultProps
,
standalone
:
true
};
it
(
'
passes calculated ticks to the callback
'
,
()
=>
{
const
callback
=
jest
.
fn
();
mount
(<
BasePairsRuler
{
...
props
}
onTicksCalculated
=
{
callback
}
/>);
expect
(
callback
).
toHaveBeenCalledTimes
(
1
);
const
payload
=
callback
.
mock
.
calls
[
0
][
0
];
expect
(
payload
.
ticks
).
toBeDefined
();
expect
(
payload
.
labelledTicks
).
toBeDefined
();
});
});
});
src/ensembl/src/content/app/entity-viewer/components/base-pairs-ruler/BasePairsRuler.tsx
0 → 100644
View file @
7a328d5e
/*
This component is a ruler for displaying alongside visualisation of a nucleic acid
It follows the following rules for displaying labelled and unlabelled ticks
1. The ruler starts at 1 and ends at the length of the feature.
Both the start and the end positions of the ruler are labelled.
2. Apart from the start and the end positions, there should be at least one label, but no greater than 5 labels
3. There may also be some unlabelled ticks. The total number of ticks (both labelled and unlabelled)
between the start and the end positions should not be greater than 10.
4. Last tick cannot be labelled if it is at a less than 10% distance from the end of the ruler
5. Ticks can be either:
a) multiple of the same power of 10 as the length of the feature, or
b) half of this power of 10
*/
import
React
,
{
useEffect
}
from
'
react
'
;
import
{
scaleLinear
}
from
'
d3
'
;
import
{
getTicks
}
from
'
./basePairsRulerHelper
'
;
import
{
getCommaSeparatedNumber
}
from
'
src/shared/helpers/numberFormatter
'
;
import
styles
from
'
./BasePairsRuler.scss
'
;
type
Ticks
=
{
ticks
:
number
[];
labelledTicks
:
number
[];
};
type
Props
=
{
length
:
number
;
// number of biological building blocks (e.g. nucleotides) in the feature
width
:
number
;
// number of pixels allotted to the axis on the screen
onTicksCalculated
?:
(
ticks
:
Ticks
)
=>
void
;
// way to pass the ticks to the parent if it is interested in them
standalone
:
boolean
;
// wrap the component in an svg element if true
};
const
FeatureLengthAxis
=
(
props
:
Props
)
=>
{
const
domain
=
[
1
,
props
.
length
];
const
range
=
[
0
,
props
.
width
];
const
scale
=
scaleLinear
()
.
domain
(
domain
)
.
range
(
range
);
const
{
ticks
,
labelledTicks
}
=
getTicks
(
scale
);
useEffect
(()
=>
{
if
(
props
.
onTicksCalculated
)
{
props
.
onTicksCalculated
({
ticks
,
labelledTicks
});
}
},
[
props
.
length
]);
const
renderedAxis
=
(
<
g
>
<
rect
className
=
{
styles
.
axis
}
x
=
{
0
}
y
=
{
0
}
width
=
{
props
.
width
}
height
=
{
1
}
/>
<
g
>
<
rect
className
=
{
styles
.
tick
}
width
=
{
1
}
height
=
{
6
}
/>
<
text
className
=
{
styles
.
label
}
x
=
{
0
}
y
=
{
20
}
textAnchor
=
"end"
>
bp 1
</
text
>
</
g
>
{
ticks
.
map
((
tick
)
=>
(
<
g
key
=
{
tick
}
transform
=
{
`translate(
${
scale
(
tick
)}
)`
}
>
<
rect
className
=
{
styles
.
tick
}
width
=
{
1
}
height
=
{
6
}
/>
{
labelledTicks
.
includes
(
tick
)
&&
(
<
text
className
=
{
styles
.
label
}
x
=
{
0
}
y
=
{
20
}
textAnchor
=
"middle"
>
{
getCommaSeparatedNumber
(
tick
)
}
</
text
>
)
}
</
g
>
))
}
<
text
className
=
{
styles
.
label
}
x
=
{
0
}
y
=
{
20
}
textAnchor
=
"start"
transform
=
{
`translate(
${
scale
(
props
.
length
)}
)`
}
>
{
getCommaSeparatedNumber
(
props
.
length
)
}
</
text
>
</
g
>
);
return
props
.
standalone
?
(
<
svg
className
=
{
styles
.
containerSvg
}
width
=
{
props
.
width
}
>
{
renderedAxis
}
</
svg
>
)
:
(
renderedAxis
);
};
FeatureLengthAxis
.
defaultProps
=
{
standalone
:
false
};
export
default
FeatureLengthAxis
;
src/ensembl/src/content/app/entity-viewer/components/base-pairs-ruler/basePairsRulerHelper.test.ts
0 → 100644
View file @
7a328d5e
import
{
scaleLinear
}
from
'
d3
'
;
import
{
getTicks
}
from
'
./basePairsRulerHelper
'
;
type
Example
=
{
length
:
number
;
ticks
:
number
[];
labelledTicks
:
number
[];
};
// concrete test cases (because it's hard to come up with a random number generator for this)
const
examples
:
Example
[]
=
[
{
length
:
5
,
ticks
:
[
2
,
3
,
4
],
labelledTicks
:
[
2
,
3
,
4
]
},
{
length
:
7
,
ticks
:
[
2
,
3
,
4
,
5
,
6
],
labelledTicks
:
[
2
,
3
,
4
,
5
,
6
]
// 5 intermediate labels at most
},
{
length
:
8
,
ticks
:
[
2
,
3
,
4
,
5
,
6
,
7
],
labelledTicks
:
[
5
]
},
{
length
:
100
,
// edge case: length is equal to the power of ten that we use to filter the ticks
ticks
:
[
10
,
20
,
30
,
40
,
50
,
60
,
70
,
80
,
90
],
labelledTicks
:
[
50
]
},
{
length
:
101
,
// behaves the same as the previous case
ticks
:
[
10
,
20
,
30
,
40
,
50
,
60
,
70
,
80
,
90
],
labelledTicks
:
[
50
]
},
{
length
:
103
,
// as previous case, but includes the last tick
ticks
:
[
10
,
20
,
30
,
40
,
50
,
60
,
70
,
80
,
90
,
100
],
// notice the last tick is the same as the power of ten
labelledTicks
:
[
50
]
},
{
length
:
593
,
ticks
:
[
100
,
200
,
300
,
400
,
500
],
labelledTicks
:
[
100
,
200
,
300
,
400
,
500
]
// 500 is included in labelled ticks, because it's at more than 10% distance from 593
},
{
length
:
679
,
ticks
:
[
100
,
200
,
300
,
400
,
500
,
600
],
labelledTicks
:
[
500
]
// can't have more than 5 labels
},
{
length
:
1160
,
ticks
:
[
1000
],
labelledTicks
:
[
1000
]
},
{
length
:
3921
,
ticks
:
[
1000
,
2000
,
3000
],
labelledTicks
:
[
1000
,
2000
,
3000
]
},
{
length
:
5367
,
ticks
:
[
1000
,
2000
,
3000
,
4000
,
5000
],
labelledTicks
:
[
1000
,
2000
,
3000
,
4000
]
// notice that the last tick is not included in labelled ticks (less than 10% distance from length)
},
{
length
:
25623
,
ticks
:
[
10000
,
20000
],
labelledTicks
:
[
10000
,
20000
]
},
{
length
:
84792
,
ticks
:
[
10000
,
20000
,
30000
,
40000
,
50000
,
60000
,
70000
,
80000
],
labelledTicks
:
[
50000
]
},
{
length
:
304813
,
ticks
:
[
100000
,
200000
,
300000
],
labelledTicks
:
[
100000
,
200000
]
},
{
length
:
304813
,
ticks
:
[
100000
,
200000
,
300000
],
labelledTicks
:
[
100000
,
200000
]
},
{
length
:
2486000
,
ticks
:
[
1000000
,
2000000
],
labelledTicks
:
[
1000000
,
2000000
]
}
];
describe
(
'
featureLengthAxisHelper
'
,
()
=>
{
describe
(
'
getTicks
'
,
()
=>
{
const
width
=
600
;
const
generateScale
=
(
length
:
number
)
=>
scaleLinear
()
.
domain
([
1
,
length
])
.
range
([
0
,
width
]);
it
(
'
produces expected labelled ticks
'
,
()
=>
{
for
(
const
example
of
examples
)
{
const
scale
=
generateScale
(
example
.
length
);
const
{
ticks
,
labelledTicks
}
=
getTicks
(
scale
);
expect
(
ticks
).
toEqual
(
example
.
ticks
);
expect
(
labelledTicks
).
toEqual
(
example
.
labelledTicks
);
}
});
});
});
src/ensembl/src/content/app/entity-viewer/components/base-pairs-ruler/basePairsRulerHelper.ts
0 → 100644
View file @
7a328d5e
import
{
ScaleLinear
}
from
'
d3
'
;
export
const
getTicks
=
(
scale
:
ScaleLinear
<
number
,
number
>
)
=>
{
// use d3 scale to get 'approximately' 10 ticks (exact number not guaranteed)
// which are "human-readable" (i.e. are multiples of powers of 10)
// and are guaranteed to fall within the scale's domain
let
ticks
=
scale
.
ticks
();
const
length
=
scale
.
domain
()[
1
];
// get back the initial length value on which the scale is based
const
step
=
ticks
[
1
]
-
ticks
[
0
];
// choose only the "important" ticks for labelling
const
exponent
=
Math
.
floor
(
Math
.
log10
(
length
));
const
powerOfTen
=
10
**
exponent
;
// e.g. 100, 1000, 10000, etc.
if
(
length
>=
powerOfTen
&&
length
<
powerOfTen
+
step
)
{
return
handleLengthAsPowerOfTen
(
ticks
,
powerOfTen
,
step
,
length
);
}
ticks
=
ticks
.
filter
((
tick
)
=>
{
// do not add a tick if it is the beginning of the ruler (position 1)
// or in the end of the ruler (tick == length), because these cases are handled separately;
// and throw away all the possible 'inelegant' intermediate ticks, such as 50, etc.
return
tick
!==
1
&&
tick
!==
length
&&
tick
%
powerOfTen
===
0
;
});
let
labelledTicks
=
getLabelledTicks
(
ticks
,
powerOfTen
,
length
);
if
(
!
labelledTicks
.
length
)
{
// let's have at least one label, roughly in the middle of the ruler
const
halvedPowerOfTen
=
powerOfTen
/
2
;
ticks
=
[...
ticks
,
halvedPowerOfTen
].
sort
();
labelledTicks
=
[
halvedPowerOfTen
];
}
return
{
ticks
,
labelledTicks
};
};
const
handleLengthAsPowerOfTen
=
(
ticks
:
number
[],
powerOfTen
:
number
,
step
:
number
,
totalLength
:
number
)
=>
{
ticks
=
ticks
.
filter
((
tick
,
index
)
=>
{
if
(
index
===
ticks
.
length
-
1
)
{
return
totalLength
-
tick
>
step
*
0.2
;
// show last tick if it's more that 20% of step length removed from end of ruler
}
return
true
;
});
return
{
ticks
,
labelledTicks
:
[
powerOfTen
/
2
]
};
};
const
getLabelledTicks
=
(
ticks
:
number
[],
powerOfTen
:
number
,
totalLength
:
number
)
=>
{
let
filterForLabels
=
buildFilterForLabels
(
powerOfTen
,
totalLength
);
let
labelledTicks
=
ticks
.
filter
(
filterForLabels
);
if
(
labelledTicks
.
length
>
5
)
{
// that's too many labels; let's use the half of the next power of ten for labelling
const
nextPowerOfTen
=
powerOfTen
*
10
;
const
halvedNextPowerOfTen
=
nextPowerOfTen
/
2
;
filterForLabels
=
buildFilterForLabels
(
halvedNextPowerOfTen
,
totalLength
);
const
newLabelledTicks
=
ticks
.
filter
(
filterForLabels
);
if
(
newLabelledTicks
.
length
<
5
)
{
labelledTicks
=
newLabelledTicks
;
}
}
return
labelledTicks
;
};
const
buildFilterForLabels
=
(
powerOfTen
:
number
,
totalLength
:
number
)
=>
(
tick
:
number
,
index
:
number
,
ticks
:
number
[]
)
=>
{
const
lastIndex
=
ticks
.
length
-
1
;
if
(
tick
%
powerOfTen
!==
0
)
{
return
false
;
}
else
if
(
index
!==
lastIndex
)
{
return
true
;
}
else
{
return
totalLength
-
tick
>
totalLength
*
0.1
;
}
};
src/ensembl/stories/entity-viewer/base-pairs-ruler/BasePairsRuler.stories.scss
0 → 100644
View file @
7a328d5e
@import
'src/styles/common'
;
.container
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
height
:
100vh
;
background-color
:
$black
;
padding-top
:
20vh
;
}
.form
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
button
{
margin-top
:
1em
;
padding
:
0
.4em
;
background-color
:
white
;
border
:
black
;
}
}
src/ensembl/stories/entity-viewer/base-pairs-ruler/BasePairsRuler.stories.tsx
0 → 100644
View file @
7a328d5e
import
React
,
{
useState
,
useRef
}
from
'
react
'
;
import
{
storiesOf
}
from
'
@storybook/react
'
;
import
BasePairsRuler
from
'
src/content/app/entity-viewer/components/base-pairs-ruler/BasePairsRuler
'
;
import
styles
from
'
./BasePairsRuler.stories.scss
'
;
type
ContainerProps
=
{
value
:
number
;
onChange
:
(
length
:
number
)
=>
void
;
};
const
LengthInputForm
=
(
props
:
ContainerProps
)
=>
{
const
inputRef
=
useRef
<
HTMLInputElement
>
(
null
);
const
onSubmit
=
(
event
:
React
.
FormEvent
)
=>
{
event
.
preventDefault
();
const
value
=
inputRef
.
current
?.
value
;
const
parsedValue
=
value
?
parseInt
(
value
,
10
)
:
null
;
parsedValue
&&
props
.
onChange
(
parsedValue
);
};
return
(
<
form
className
=
{
styles
.
form
}
onSubmit
=
{
onSubmit
}
>
<
input
ref
=
{
inputRef
}
defaultValue
=
{
props
.
value
}
/>
<
button
>
Change length
</
button
>
</
form
>
);
};
storiesOf
(
'
Components|EntityViewer/FeatureLengthAxis
'
,
module
).
add
(
'
default
'
,
()
=>
{
const
initialLength
=
80792
;
const
[
length
,
setLength
]
=
useState
(
initialLength
);
const
handleLenghtChange
=
(
length
:
number
)
=>
{
setLength
(
length
);
};
return
(
<
div
className
=
{
styles
.
container
}
>
<
BasePairsRuler
length
=
{
length
}
width
=
{
800
}
standalone
=
{
true
}
/>
<
div
>
<
LengthInputForm
value
=
{
length
}
onChange
=
{
handleLenghtChange
}
/>
</
div
>
</
div
>
);
}
);
src/ensembl/stories/entity-viewer/index.ts
0 → 100644
View file @
7a328d5e
import
'
./base-pairs-ruler/BasePairsRuler.stories
'
;
src/ensembl/stories/index.tsx
View file @
7a328d5e
...
...
@@ -4,3 +4,4 @@ import './design-primitives/index.ts';
import
'
./shared-components/index.ts
'
;
import
'
./static-images/index.ts
'
;
import
'
./genome-browser/index.ts
'
;
import
'
./entity-viewer/index.ts
'
;
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment