diff --git a/app/assets/stylesheets/components/table.scss b/app/assets/stylesheets/components/table.scss
index f3fb8711a9..6c03be874d 100644
--- a/app/assets/stylesheets/components/table.scss
+++ b/app/assets/stylesheets/components/table.scss
@@ -2,7 +2,7 @@
border-collapse: collapse;
width: 100%;
border-spacing: 0;
-
+ box-sizing: border-box !important;
}
.table-auto-layout{
table-layout: auto !important;
diff --git a/app/assets/stylesheets/mappings.scss b/app/assets/stylesheets/mappings.scss
index 070fafea72..4e16155302 100644
--- a/app/assets/stylesheets/mappings.scss
+++ b/app/assets/stylesheets/mappings.scss
@@ -1,4 +1,265 @@
+.mappigs-page-container{
+ display: flex;
+ justify-content: center;
+ .file_uploader{
+ width: 700px;
+ }
+}
+.mappings-page-subcontainer{
+ width: 1248px;
+ padding: 20px 50px;
+}
+
+.mappings-page-title{
+ font-size: 25px;
+ font-weight: 700;
+}
+.mappings-page-title .line{
+ height: 2px;
+ width: 57px;
+ background-color: var(--primary-color);
+ border-radius: 10px;
+}
+.mappings-page-decription{
+ color: #888888;
+ margin: 20px 0;
+}
+
+.mappings-bubble{
+ cursor: pointer;
+}
+.mappings-bubble-view-frame{
+ width: 600px;
+ height: 600px;
+ overflow: auto;
+}
+
+.mappings-zoom-buttons{
+ display: flex;
+ margin-top: 20px;
+ justify-content: center;
+}
+.mappings-zoom-buttons svg{
+ width: 40px;
+ cursor: pointer;
+ path{
+ fill: var(--primary-color)
+ }
+ &:hover path{
+ fill: var(--hover-color)
+ }
+}
+.mappings-zoom-buttons div + div{
+ margin-left: 40px;
+}
+.mappings-bubble-view-container{
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+.mappings-table-view-container{
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin: 20px 0;
+}
+.mappings-ontologies-select{
+ width: 700px;
+}
+
+.mappings-table-container{
+ width: 678px;
+ margin: 20px 0;
+}
+.mappings-table-ontology-name{
+ font-weight: 600;
+}
+
+.bubble-tooltip {
+ position: absolute;
+ text-align: center;
+ padding: 6px;
+ font-size: 12px;
+ background: rgba(0, 0, 0, 0.7);
+ color: white;
+ border-radius: 4px;
+ pointer-events: none;
+ z-index: 9999;
+}
+
+.upload-mappings{
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-top: 20px;
+}
+
+.upload-mappings-example{
+ width: 700px;
+ margin-bottom: 20px;
+}
+.upload-mappings-example.ontologies{
+ width: 600px;
+}
+
+.mappings-page-ontologies-selector{
+ width: 100%;
+ padding: 0 30px 30px 30px;
+}
+
+.mappings-page-ontologies-selector .selector-button{
+ margin-top: 20px;
+}
+
+.upload-mappings-example .title-bar{
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ cursor: pointer;
+ padding: 15px;
+ color: var(--primary-color);
+ align-items: center;
+}
+.upload-mappings-example .title-bar svg{
+ margin-right: 10px;
+}
+
+.summary-mappigs-page-container{
+ margin-left: 90px;
+}
+.mapping-bubbles-loader{
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.summary-mappings-tab {
+ margin-top: 10px;
+}
+
+#ontology_viewermappings_content {
+ .summary-mappings-tab {
+ display: flex;
+ }
+
+ .summary-mappings-tab-table {
+ width: 400px;
+ }
+}
+
+.mappings-table-pagination a, .mappings-table-pagination em{
+ margin-left: 10px;
+}
+
+.mappings-bubble-view-legend{
+ width: 600px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ font-weight: 500;
+ padding: 20px;
+}
+.mappings-bubble-view-legend > .title {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.mappings-bubble-view-legend > .title .text{
+ font-size: 18px;
+ color: var(--primary-color);
+}
+.mappings-bubble-view-legend > .title .line{
+ background-color: var(--primary-color);
+ border-radius: 100%;
+ width: 40px;
+ height: 2px;
+}
+
+.content-container{
+ width: 100%;
+ margin-top: -35px;
+}
+.bubble-view-legend-item{
+ margin-top: 50px;
+}
+.bubble-view-legend-item .title{
+ font-weight: 500;
+}
+.bubble-view-legend-item .title span{
+ font-weight: 400;
+}
+
+.mappings-bubble-size-legend, .mappings-bubble-color-legend{
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin: 20px 0;
+}
+.mappings-bubble-size-legend .bubble{
+ border-radius: 100%;
+ border: 2px solid var(--primary-color);
+}
+.mappings-bubble-size-legend .bubble.bubble1{
+ width: 10px;
+ height: 10px;
+}
+.mappings-bubble-size-legend .bubble.bubble2{
+ width: 20px;
+ height: 20px;
+}
+.mappings-bubble-size-legend .bubble.bubble3{
+ width: 30px;
+ height: 30px;
+}
+.mappings-bubble-size-legend .bubble.bubble4{
+ width: 40px;
+ height: 40px;
+}
+.mappings-bubble-size-legend .bubble.bubble5{
+ width: 50px;
+ height: 50px;
+}
+.mappings-bubble-size-legend .bubble.bubble6{
+ width: 60px;
+ height: 60px;
+}
+.mappings-bubble-color-legend .bubble{
+ border-radius: 100%;
+ background-color: var(--primary-color);
+ width: 35px;
+ height: 35px;
+}
+.mappings-bubble-color-legend .bubble.bubble1{
+ opacity: 30%;
+}
+.mappings-bubble-color-legend .bubble.bubble2{
+ opacity: 50%;
+}
+.mappings-bubble-color-legend .bubble.bubble3{
+ opacity: 70%;
+}
+.mappings-bubble-color-legend .bubble.bubble4{
+ opacity: 80%;
+}
+.mappings-bubble-color-legend .bubble.bubble5{
+ opacity: 90%;
+}
+.mappings-bubble-color-legend .bubble.bubble6{
+ opacity: 100%;
+}
+.mappings-bubble-size-legend .bubble.yellow{
+ width: 35px;
+ height: 35px;
+ background-color: var(--secondary-color);
+}
+.mappings-legend-text{
+ color: var(--primary-color);
+ font-size: 15px;
+ font-weight: 500;
+}
@media (min-width: 1000px) {
#mappings_container{
@@ -76,3 +337,23 @@ div#map_from_concept_details_table, div#map_to_concept_details_table {
width: 100%;
}
+.summary-mappings-tab-table {
+ .dataTables_wrapper .dataTables_filter {
+ float: none;
+ text-align: unset;
+ }
+}
+
+.summary-mappings-tab-table label {
+ display: block;
+ font-size: 0;
+}
+
+.summary-mappings-tab-table label input[type="search"] {
+ font-size: 16px;
+ width: 100%;
+ border-radius: 8px;
+ padding: 10px;
+ outline: none;
+ margin-left: 0 !important;
+}
\ No newline at end of file
diff --git a/app/controllers/annotator_controller.rb b/app/controllers/annotator_controller.rb
index 6f1ec810b8..d0cd478007 100644
--- a/app/controllers/annotator_controller.rb
+++ b/app/controllers/annotator_controller.rb
@@ -201,6 +201,7 @@ def empty_advanced_options
params[:fast_context].nil? &&
params[:lemmatize].nil?
end
+
def remove_special_chars(input)
regex = /^[a-zA-Z0-9\s]*$/
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index ebcbcc6b36..d650f26460 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -193,19 +193,9 @@ def bp_config_json
def rest_url
- # Split the URL into protocol and path parts
- protocol, path = REST_URI.split("://", 2)
-
- # Remove duplicate "//"
- cleaned_url = REST_URI.gsub(/\/\//, '/')
-
- # Remove the last '/' in the path part
- cleaned_path = path.chomp('/')
- # Reconstruct the cleaned URL
- "#{protocol}://#{cleaned_path}"
+ helpers.rest_url
end
-
-
+
def check_http_file(url)
session = Net::HTTP.new(url.host, url.port)
session.use_ssl = true if url.port == 443
diff --git a/app/controllers/mappings_controller.rb b/app/controllers/mappings_controller.rb
index 2f262a6a38..e6c9fabd4b 100644
--- a/app/controllers/mappings_controller.rb
+++ b/app/controllers/mappings_controller.rb
@@ -15,32 +15,26 @@ class MappingsController < ApplicationController
INTERPORTAL_HASH = $INTERPORTAL_HASH ||= {}
def index
+ @ontologies_mapping_count = LinkedData::Client::HTTP.get("#{MAPPINGS_URL}/statistics/ontologies")
ontology_list = LinkedData::Client::Models::Ontology.all.select { |o| !o.summaryOnly }
- ontologies_mapping_count = LinkedData::Client::HTTP.get("#{MAPPINGS_URL}/statistics/ontologies")
ontologies_hash = {}
ontology_list.each do |ontology|
ontologies_hash[ontology.acronym] = ontology
end
- # TODO_REV: Views support for mappings
- # views_list.each do |view|
- # ontologies_hash[view.ontologyId] = view
- # end
-
@options = {}
- ontologies_mapping_count&.members&.each do |ontology_acronym|
- # Adding external and interportal mappings to the dropdown list
+ @ontologies_mapping_count&.members&.each do |ontology_acronym|
if ontology_acronym.to_s == EXTERNAL_MAPPINGS_GRAPH
- mapping_count = ontologies_mapping_count[ontology_acronym.to_s] || 0
+ mapping_count = @ontologies_mapping_count[ontology_acronym.to_s] || 0
select_text = t('mappings.external_mappings', number_with_delimiter: number_with_delimiter(mapping_count, delimiter: ',')) if mapping_count >= 0
ontology_acronym = EXTERNAL_URL_PARAM_STR
elsif ontology_acronym.to_s.start_with?(INTERPORTAL_MAPPINGS_GRAPH)
- mapping_count = ontologies_mapping_count[ontology_acronym.to_s] || 0
+ mapping_count = @ontologies_mapping_count[ontology_acronym.to_s] || 0
select_text = t('mappings.interportal_mappings', acronym: ontology_acronym.to_s.split("/")[-1].upcase, number_with_delimiter: number_with_delimiter(mapping_count, delimiter: ',')) if mapping_count >= 0
ontology_acronym = INTERPORTAL_URL_PARAM_STR + ontology_acronym.to_s.split("/")[-1]
else
ontology = ontologies_hash[ontology_acronym.to_s]
- mapping_count = ontologies_mapping_count[ontology_acronym] || 0
+ mapping_count = @ontologies_mapping_count[ontology_acronym] || 0
next unless ontology && mapping_count > 0
select_text = "#{ontology.name} - #{ontology.acronym} (#{number_with_delimiter(mapping_count, delimiter: ',')})"
end
@@ -48,16 +42,8 @@ def index
end
@options = @options.sort
- end
-
- def count
- @ontology = LinkedData::Client::Models::Ontology.find_by_acronym(params[:id]).first
- @ontology_acronym = @ontology&.acronym || params[:id]
- @mapping_counts = mapping_counts(@ontology_acronym)
- render partial: 'count'
- end
+ @options.unshift([])
- def loader
@example_code = [{
"classes": ["http://bioontology.org/ontologies/BiomedicalResourceOntology.owl#Image_Algorithm",
"http://purl.org/incf/ontology/Computational_Neurosciences/cno_alpha.owl#cno_0000202"],
@@ -74,9 +60,20 @@ def loader
"source_contact_info": 'orcid:1234,orcid:5678',
"date": '2020-05-30'
}]
- render partial: 'mappings/bulk_loader/loader'
end
+
+ def count
+ @ontology_acronym = params[:ontology] || params[:id]
+ @mapping_counts = mapping_counts(@ontology_acronym)
+
+ respond_to do |format|
+ format.html { render partial: 'mappings/count' }
+ format.json { render json: @mapping_counts }
+ end
+ end
+
+
def loader_process
response = LinkedData::Client::HTTP.post('/mappings/load', file: params[:file])
errors = response.errors
@@ -85,6 +82,7 @@ def loader_process
created = response.created
respond_to do |format|
format.turbo_stream do
+ # TO test
render turbo_stream: turbo_stream.replace('file_loader_result',
partial: 'mappings/bulk_loader/loaded_mappings',
locals: { errors: errors, created: created })
@@ -105,7 +103,7 @@ def show
def show_mappings
page = params[:page] || 1
@ontology = LinkedData::Client::Models::Ontology.find_by_acronym(params[:id]).first
- @target_ontology = LinkedData::Client::Models::Ontology.find(params[:target])
+ @target_ontology = LinkedData::Client::Models::Ontology.find(params[:target].split('/').last)
# Cases if ontology or target are interportal or external
if @ontology.nil?
@@ -277,7 +275,7 @@ def mapping_form(mapping: nil)
else
mapping = LinkedData::Client::Models::Mapping.new
@ontology_from = LinkedData::Client::Models::Ontology.find_by_acronym(params[:ontology_from].split('/').last).first
- @ontology_to = LinkedData::Client::Models::Ontology.find_by_acronym(params[:ontology_to]&.split('/')&.last).first
+ @ontology_to = params[:ontology_to].present? ? LinkedData::Client::Models::Ontology.find_by_acronym(params[:ontology_to].split('/').last).first : nil
@concept_from = @ontology_from.explore.single_class({ full: true }, params[:conceptid_from]) if @ontology_from
if @ontology_to
@concept_to = @ontology_to.explore.single_class({ full: true }, params[:conceptid_to])
@@ -343,4 +341,4 @@ def valid_values?(values)
end
errors
end
-end
+end
\ No newline at end of file
diff --git a/app/controllers/ontologies_controller.rb b/app/controllers/ontologies_controller.rb
index 69e6d04d37..981523bbfd 100644
--- a/app/controllers/ontologies_controller.rb
+++ b/app/controllers/ontologies_controller.rb
@@ -137,6 +137,7 @@ def edit
def mappings
@ontology_acronym = @ontology.acronym || params[:id]
@mapping_counts = mapping_counts(@ontology_acronym)
+ @ontologies_mapping_count = LinkedData::Client::HTTP.get("#{MAPPINGS_URL}/statistics/ontologies")
if request.xhr?
render partial: 'ontologies/sections/mappings', layout: false
else
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 669ce29f64..93a6742854 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -563,6 +563,16 @@ def prefix_properties(concept_properties)
modified_properties
end
+ def rest_url
+ # Split the URL into protocol and path parts
+ protocol, path = $REST_URL.split("://", 2)
+
+ # Remove the last '/' in the path part
+ cleaned_path = path.chomp('/')
+ # Reconstruct the cleaned URL
+ "#{protocol}://#{cleaned_path}"
+ end
+
def prefix_property_url(key_string, key = nil)
namespace_key, _ = RESOLVE_NAMESPACE.find { |_, value| key_string.include?(value) }
@@ -641,5 +651,7 @@ def cancel_button_component(class_name: nil, id: , value:, data: nil)
end
end
end
-
+
+
+
end
diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb
index 9824327927..eae423d829 100644
--- a/app/helpers/components_helper.rb
+++ b/app/helpers/components_helper.rb
@@ -139,6 +139,10 @@ def chart_component(title: '', type:, labels:, datasets:, index_axis: 'x', show_
content_tag(:canvas, nil, data: data)
end
+ def loader_component(type = 'pulsing')
+ render LoaderComponent.new(type: type)
+ end
+
def info_tooltip(text)
render Display::InfoTooltipComponent.new(text: text)
end
@@ -220,6 +224,14 @@ def properties_dropdown(id, title, tooltip, properties, is_open: false, &block)
end
end
+
+ def regular_button(id, value, variant: "secondary", state: "regular", size: "slim", &block)
+ render Buttons::RegularButtonComponent.new(id:id, value: value, variant: variant, state: state, size: size) do |btn|
+ capture(btn, &block) if block_given?
+ end
+ end
+
+
def form_save_button
render Buttons::RegularButtonComponent.new(id: 'save-button', value: t('components.save_button'), variant: "primary", size: "slim", type: "submit") do |btn|
btn.icon_left do
diff --git a/app/helpers/mappings_helper.rb b/app/helpers/mappings_helper.rb
index 7e0643e513..cee0168da4 100644
--- a/app/helpers/mappings_helper.rb
+++ b/app/helpers/mappings_helper.rb
@@ -1,12 +1,5 @@
module MappingsHelper
- RELATIONSHIP_URIS = {
- "http://www.w3.org/2004/02/skos/core" => "skos:",
- "http://www.w3.org/2000/01/rdf-schema" => "rdfs:",
- "http://www.w3.org/2002/07/owl" => "owl:",
- "http://www.w3.org/1999/02/22-rdf-syntax-ns" => "rdf:"
- }
-
# Used to replace the full URI by the prefixed URI
RELATIONSHIP_PREFIX = {
"http://www.w3.org/2004/02/skos/core#" => "skos:",
@@ -19,11 +12,6 @@ module MappingsHelper
INTERPORTAL_HASH = $INTERPORTAL_HASH
- def get_short_id(uri)
- split = uri.split("#")
- name = split.length > 1 && RELATIONSHIP_URIS.keys.include?(split[0]) ? RELATIONSHIP_URIS[split[0]] + split[1] : uri
- "#{name}"
- end
# a little method that returns true if the URIs array contain a gold:translation or gold:freeTranslation
def translation?(relation_array)
@@ -106,35 +94,6 @@ def get_inter_portal_ui_link(uri, process_name)
end
end
- def onts_and_views_for_select
- @onts_and_views_for_select = []
- ontologies = LinkedData::Client::Models::Ontology.all(include: "acronym,name", include_views: true)
- ontologies.each do |ont|
- next if (ont.acronym.nil? || ont.acronym.empty?)
- ont_acronym = ont.acronym
- ont_display_name = "#{ont.name.strip} (#{ont_acronym})"
- @onts_and_views_for_select << [ont_display_name, ont_acronym]
- end
- @onts_and_views_for_select.sort! { |a, b| a[0].downcase <=> b[0].downcase }
- return @onts_and_views_for_select
- end
-
- def get_concept_mappings(concept)
- mappings = concept.explore.mappings
- # Remove mappings where the destination class exists in an ontology that the logged in user doesn't have permissions to view.
- # Workaround for https://github.com/ncbo/ontologies_api/issues/52.
- mappings.delete_if do |mapping|
- #mapping.classes.reject! { |cls| (cls.id == concept.id) && (cls.links['ontology'] == concept.links['ontology']) }
- begin
- ont = mapping.classes[0].explore.ontology
- ont.errors && ont.errors.grep(/Access denied/).any?
- rescue => e
- Rails.logger.warn t('mappings.mapping_issue', mapping: mapping.inspect, message: e.message)
- false
- end
- end
- end
-
def internal_mapping?(cls)
cls.links['self'].to_s.start_with?(LinkedData::Client.settings.rest_url) || ($LOCAL_IP.present? && cls.links['self'].to_s.include?($LOCAL_IP))
end
@@ -212,4 +171,35 @@ def concept_mappings_loader(ontology_acronym: ,concept_id: )
end)
end
end
+
+ def client_filled_modal
+ link_to_modal "", ""
+ end
+
+ def mappings_bubble_view_legend
+ content_tag(:div, class: 'mappings-bubble-view-legend') do
+ mappings_legend_section('Bubble size:', 'The global number of mappings with all other ontologies.', 'mappings-bubble-size-legend') +
+ mappings_legend_section('Color degree:', 'The number of mappings with the selected ontology.', 'mappings-bubble-color-legend')
+ end
+ end
+
+ def mappings_legend_section(title_text, description_text, css_class)
+ content_tag(:div, class: 'content-container') do
+ content_tag(:div, class: 'bubble-view-legend-item') do
+ content_tag(:div, class: 'title') do
+ content_tag(:div, "#{title_text} ", class: 'd-inline') +
+ content_tag(:span, description_text)
+ end +
+ mappings_legend(css_class)
+ end
+ end
+ end
+
+ def mappings_legend(css_class)
+ content_tag(:div, class: css_class) do
+ content_tag(:div, "Less mappings", class: 'mappings-legend-text') +
+ (1..6).map { |i| content_tag(:div, "", class: "bubble bubble#{i}") }.join +
+ content_tag(:div, "More mappings", class: 'mappings-legend-text')
+ end
+ end
end
diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js
index b93b16d5e4..e490eb3a17 100644
--- a/app/javascript/controllers/index.js
+++ b/app/javascript/controllers/index.js
@@ -99,3 +99,7 @@ application.register('form-url', FormUrlController)
import OntologiesSelector from "./ontologies_selector_controller"
application.register("ontologies-selector", OntologiesSelector)
+
+
+import MappingsController from "./mappings_visualization_controller"
+application.register('mappings', MappingsController)
\ No newline at end of file
diff --git a/app/javascript/controllers/mappings_visualization_controller.js b/app/javascript/controllers/mappings_visualization_controller.js
new file mode 100644
index 0000000000..e75813d368
--- /dev/null
+++ b/app/javascript/controllers/mappings_visualization_controller.js
@@ -0,0 +1,259 @@
+import { Controller } from '@hotwired/stimulus'
+import { useMappingsDrawBubbles } from '../mixins/useMappingsBubbles'
+
+export default class extends Controller {
+
+ static values = {
+ mappingsList: Object,
+ zoomRatio: { type: Number, default: 1 },
+ acronym: String,
+ containerId: { type: String, default: 'mappings-bubbles-view' }
+ }
+
+ static targets = ['frame', 'bubbles', 'submit', 'modal', 'selector', 'ontologies', 'loader']
+
+ connect () {
+
+ this.drawBubbles = (mappingsList) => {
+ const zoomRatio = this.zoomRatioValue
+ const width = 600 * zoomRatio
+ const height = 600 * zoomRatio
+ const margin = 1
+ const logScaleFactor = 10
+ const normalization_ratio = this.#normalizationRatio(mappingsList)
+
+ const data = Object.entries(mappingsList).map(([key, value]) => ({
+ ontology_name: key.split('/').pop(),
+ ontology_mappings: value,
+ }))
+
+ useMappingsDrawBubbles(data, width, height, margin, this.bubblesTarget, normalization_ratio, logScaleFactor)
+
+ this.#centerScroll(this.frameTarget)
+ }
+
+ this.drawBubbles(this.mappingsListValue)
+
+ if (this.#selectionDisabled()) {
+ this.#clickOnSelectedAcronymBubble()
+ }
+
+ }
+
+ filterOntologies () {
+ const selectOptions = Array.from(this.ontologiesTarget.querySelector('select').selectedOptions)
+ const acronyms = selectOptions.map(option => option.value)
+
+ const filteredList = Object.fromEntries(
+ Object.entries(this.mappingsListValue).filter(([key]) => acronyms.includes(key))
+ )
+
+ this.drawBubbles(filteredList)
+ }
+
+ submit (event) {
+ const itemElement = event.currentTarget.querySelector('.item')
+ if (!itemElement) return
+
+ this.submitTarget.click()
+
+ const selectAcronym = event.currentTarget.querySelector('select').value
+
+ const bubblesContainer = document.getElementById(this.containerIdValue)
+ const selectedBubble = bubblesContainer.querySelector('[data-selected="true"]')
+ const currentBubble = bubblesContainer.querySelector(`[data-acronym="${selectAcronym}"]`)
+
+ if (selectedBubble && selectedBubble.dataset.acronym === selectAcronym) return
+
+ const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, view: window })
+
+ if (currentBubble && (currentBubble.getAttribute('data-enabled') === 'false' || currentBubble.getAttribute('data-highlighted') === 'true')) {
+ selectedBubble.dispatchEvent(clickEvent)
+ }
+
+ if (currentBubble) currentBubble.dispatchEvent(clickEvent)
+ }
+
+ zoomIn () {
+ this.zoomRatioValue++
+ this.drawBubbles(this.mappingsListValue)
+ }
+
+ zoomOut () {
+ if (this.zoomRatioValue > 1) {
+ this.zoomRatioValue--
+ this.drawBubbles(this.mappingsListValue)
+ }
+ }
+
+ selectBubble (event) {
+ const selected_bubble = event.currentTarget
+
+ if (selected_bubble.getAttribute('data-enabled') === 'false') {
+ // user clicks on a bubble that is disabled (has no mappings with the current bubble) do nothing
+ return
+ }
+
+ this.#toggleAnimation()
+
+ if (selected_bubble.getAttribute('data-highlighted') === 'true') {
+ // user clicks on a bubble that have mapping with the current highlighted bubble, should show a modal with the mappings
+ this.#showMappingsModal(selected_bubble)
+ this.#toggleAnimation()
+ } else if (selected_bubble.getAttribute('data-selected') === 'true') {
+ // user clicks on current bubble (should deselect it, but nothing happen if we're in ontology mappings section not the page)
+ this.#unSelectBubble(selected_bubble)
+ this.#toggleAnimation()
+ } else {
+ this.#selectBubble(selected_bubble)
+ }
+ }
+
+ #selectBubble (selected_bubble) {
+
+ const acronym = selected_bubble.getAttribute('data-acronym')
+ let url = '/mappings/count/' + acronym
+ selected_bubble.setAttribute('data-selected', 'true')
+
+ if (this.#selectionEnabled()) {
+ const input = this.selectorTarget.querySelector('input')
+ input.value = acronym
+ input.dispatchEvent(new Event('input', { bubbles: true }))
+
+ const selectValue = Array.from(this.selectorTarget.querySelectorAll('.option'))
+ .find(option => option.getAttribute('data-value') === acronym)
+
+ if (selectValue) selectValue.click()
+ }
+
+ this.#fetchMappingsDataAndSetBubblesColor(url)
+ }
+
+ #unSelectBubble (selected_bubble) {
+
+ if (this.#selectionDisabled()) return
+
+ selected_bubble.setAttribute('data-selected', 'false')
+
+ const selected_circle = selected_bubble.querySelector('circle')
+ selected_circle.style.fill = 'var(--primary-color)'
+
+ const leafs = this.bubblesTarget.querySelectorAll('.leaf')
+ leafs.forEach(leaf => {
+ const circle = leaf.querySelector('circle')
+ circle.style.fill = 'var(--primary-color)'
+ circle.style.opacity = '1'
+ leaf.setAttribute('data-enabled', 'true')
+ leaf.setAttribute('data-highlighted', 'false')
+ })
+ }
+
+ #showMappingsModal (selected_bubble) {
+ const selected_leaf = this.bubblesTarget.querySelector('[data-selected="true"]')
+ const acronym = selected_leaf.getAttribute('data-acronym')
+ const target_acronym = selected_bubble.getAttribute('data-acronym')
+ this.modalTarget.querySelector('a').href = `/mappings/show_mappings?id=${acronym}&target=${target_acronym}`
+ this.modalTarget.querySelector('a').click()
+ }
+
+ #fetchMappingsDataAndSetBubblesColor (url) {
+ fetch(url, {
+ method: 'GET',
+ headers: {
+ 'Accept': 'application/json'
+ },
+ })
+ .then(response => {
+ if (!response.ok) {
+ throw new Error('Network response was not ok')
+ }
+ return response.json()
+ })
+ .then(data => {
+ const mappings_list = data.map(item => ({
+ acronym: item.target_ontology.acronym,
+ count: item.count
+ }))
+
+ this.#setBubblesColors(mappings_list)
+
+ this.#toggleAnimation()
+ })
+ .catch(error => {
+ console.error('Error fetching or processing data:', error)
+ // Handle errors here
+ })
+ }
+
+ #setBubblesColors (mappings_list) {
+ const bubblesContainer = this.bubblesTarget
+ const leafs = bubblesContainer.querySelectorAll('.leaf')
+ const max_mappings_count = mappings_list.reduce((max, item) => Math.max(max, item.count), -Infinity)
+
+ leafs.forEach(leaf => {
+ const circle = leaf.querySelector('circle')
+ const acronym = leaf.getAttribute('data-acronym')
+
+ const matchingMapping = mappings_list.find(item => item.acronym === acronym)
+
+ if (matchingMapping) {
+ leaf.setAttribute('data-highlighted', 'true')
+ circle.style.fill = 'var(--primary-color)'
+
+ const opacity = (matchingMapping.count / max_mappings_count + Math.log(matchingMapping.count + 1)) / 10 + 0.3
+ circle.style.opacity = `${opacity}`
+ } else {
+ leaf.setAttribute('data-enabled', 'false')
+ circle.style.fill = 'var(--light-color)'
+ }
+ })
+
+ const selected_leaf = bubblesContainer.querySelector('[data-selected="true"]')
+ selected_leaf.setAttribute('data-enabled', 'true')
+
+ const selected_circle = selected_leaf.querySelector('circle')
+ selected_circle.style.fill = 'var(--secondary-color)'
+ }
+
+
+
+ #centerScroll (frame) {
+ frame.scrollTop = frame.scrollHeight / 2 - frame.clientHeight / 2
+ frame.scrollLeft = frame.scrollWidth / 2 - frame.clientWidth / 2
+ }
+
+ #normalizationRatio (ontologies_hash) { // try to find the biggest multiple of 10 inferior to the max mappings value
+ const maxValue = Math.max(...Object.values(ontologies_hash))
+ let normalization_ratio = 1
+ while (maxValue / normalization_ratio > 10) {
+ normalization_ratio *= 10
+ }
+ return normalization_ratio
+ }
+
+ #toggleAnimation () {
+ this.loaderTarget.classList.toggle('d-none')
+ this.bubblesTarget.classList.toggle('d-none')
+ }
+
+ #selectionEnabled () {
+ return !this.hasAcronymValue
+ }
+
+ #selectionDisabled () {
+ return !this.#selectionEnabled()
+ }
+
+ #clickOnSelectedAcronymBubble () {
+ setTimeout(() => {
+ const currentBubble = this.bubblesTarget.querySelector(`[data-acronym="${this.acronymValue}"]`)
+ let clickEvent = new MouseEvent('click', {
+ bubbles: true,
+ cancelable: true,
+ view: window
+ })
+ currentBubble.dispatchEvent(clickEvent)
+ }, 100)
+
+ }
+}
\ No newline at end of file
diff --git a/app/javascript/mixins/useMappingsBubbles.js b/app/javascript/mixins/useMappingsBubbles.js
new file mode 100644
index 0000000000..60c5a36830
--- /dev/null
+++ b/app/javascript/mixins/useMappingsBubbles.js
@@ -0,0 +1,110 @@
+import * as d3 from 'd3'
+
+class BubbleData {
+ constructor(ontology_name, ontology_mappings) {
+ this.ontology_name = ontology_name;
+ this.ontology_mappings = ontology_mappings;
+ }
+}
+
+/**
+ * Draws bubbles using D3.js based on the provided data.
+ * @param {Array} data - The array of BubbleData objects containing ontology names and mappings.
+ * @param {number} width - The width of the SVG container.
+ * @param {number} height - The height of the SVG container.
+ * @param {number} margin - The margin for the SVG container.
+ * @param {HTMLElement} bubblesTarget - The target HTML element to append the SVG container to.
+ * @param {number} normalization_ratio - The normalization ratio for bubble size calculation.
+ * @param {number} logScaleFactor - The logarithmic scale factor for bubble size calculation.
+ */
+
+export function useMappingsDrawBubbles(data, width, height, margin, bubblesTarget, normalization_ratio, logScaleFactor) {
+ // Define pack layout
+ const pack = d3.pack()
+ .size([width - margin, height - margin])
+ .padding(3);
+
+ // Create hierarchy and sum for bubble sizes
+ const root = d3.hierarchy({ children: data })
+ .sum(d => calculateBubbleSize(d));
+
+ // Create SVG container
+ const svg = d3.select(`#${bubblesTarget.id}`)
+ .append('svg')
+ .attr('width', width)
+ .attr('height', height)
+ .append('g')
+ .attr('transform', `translate(${margin}, ${margin})`)
+
+ // Create nodes and bind data
+ const node = svg.selectAll('.node')
+ .data(pack(root).descendants().slice(1)) // Exclude the root node
+ .enter().append('g')
+ .attr('class', d => d.children ? 'node mappings-bubble' : 'leaf mappings-bubble')
+ .attr('transform', d => `translate(${d.x},${d.y})`)
+ .attr('data-action', 'click->mappings#selectBubble')
+ .attr('data-acronym', d => d.data.ontology_name)
+ .attr('data-enabled', d => 'true');
+
+ // Create circles
+ const circle = node.append('circle')
+ .attr('r', d => d.r)
+ .style('fill', 'var(--primary-color)');
+
+ // Display ontology names and mappings
+ const textOntology = node.append('text')
+ .attr('dy', '.35em')
+ .style('text-anchor', 'middle')
+ .style('font-size', '16px')
+ .style('fill', 'white')
+ .style('font-weight', '600')
+ .text(d => displayOntologyName(d));
+
+ const textMappings = node.append('text')
+ .attr('dy', '1.5em')
+ .style('text-anchor', 'middle')
+ .style('font-size', '12px')
+ .style('fill', 'white')
+ .text(d => displayMappings(d));
+
+ // Show tooltips on hover
+ circle.on('mouseover', (event, d) => showTooltip(event, d))
+ .on('mouseout', () => hideTooltip());
+
+ // Function to calculate bubble size
+ function calculateBubbleSize(d) {
+ return d.ontology_mappings / normalization_ratio + Math.log(d.ontology_mappings + 1) / logScaleFactor;
+ }
+
+ // Function to display ontology name
+ function displayOntologyName(d) {
+ return (d.r > d.data.ontology_name.length * 5 && d.r > 20) ? d.data.ontology_name : '';
+ }
+
+ // Function to display mappings count
+ function displayMappings(d) {
+ return (d.r > d.data.ontology_name.length * 5 && d.r > 20) ? d.data.ontology_mappings : '';
+ }
+
+ // Function to show tooltip
+ function showTooltip(event, d) {
+ if (!(d.r > d.data.ontology_name.length * 5 && d.r > 20)) {
+ // Remove existing tooltip
+ d3.selectAll('.bubble-tooltip').remove();
+
+ // Calculate tooltip position based on mouse coordinates
+ const tooltip = d3.select('body')
+ .append('div')
+ .attr('class', 'bubble-tooltip')
+ .style('left', `${event.pageX + 10}px`) // Adjust position relative to mouse pointer
+ .style('top', `${event.pageY + 10}px`) // Adjust position relative to mouse pointer
+ .html(`${d.data.ontology_name}
${d.data.ontology_mappings}`);
+ }
+ }
+
+ // Function to hide tooltip
+ function hideTooltip() {
+ // Remove tooltip on mouseout
+ d3.selectAll('.bubble-tooltip').remove();
+ }
+}
diff --git a/app/views/mappings/_concept_mappings_selector.html.haml b/app/views/mappings/_concept_mappings_selector.html.haml
deleted file mode 100644
index c00a40fb44..0000000000
--- a/app/views/mappings/_concept_mappings_selector.html.haml
+++ /dev/null
@@ -1,21 +0,0 @@
-.card
- #headingTwo.card-header
- %h2.mb-0
- %button.btn.btn-link.btn-block.text-left.collapsed{"data-target" => "#collapseTwo", "data-toggle" => "collapse", :type => "button"}
- = t('mappings.find_mappings')
- = link_to(Rails.configuration.settings.links[:mappings], id: "mappings-help") do
- %i.fas.fa-question-circle.fa-lg{"aria-hidden": "true"}
- #collapseTwo.collapse{"data-parent" => "#accordionExample"}
- .card-body
- %div
- = render partial: 'shared/concept_picker', locals: {name: :concept_mapping_selector, concept_label: '', ontology_acronym: t('mappings.all'), include_definition: true }
- %div.mt-1
- = render TurboFrameComponent.new(id:'concept_mappings')
-:javascript
- const picker_name = 'concept_mapping_selector'
- const frame = document.getElementById('concept_mappings')
- $('input[name="concept_mapping_selector"]').on('selected', () => {
- const ontology_id = $(`input[name="${picker_name}_bioportal_ontology_id"]`).val()
- const concept_id = $(`input[name="${picker_name}_bioportal_concept_id"]`).val()
- frame.src = `/ajax/mappings/get_concept_table?ontologyid=${ontology_id}&conceptid=${encodeURIComponent(concept_id)}`
- })
diff --git a/app/views/mappings/_count.html.haml b/app/views/mappings/_count.html.haml
index 66b6d96e59..e3cd5f9336 100644
--- a/app/views/mappings/_count.html.haml
+++ b/app/views/mappings/_count.html.haml
@@ -1,18 +1,35 @@
-= render TableComponent.new(id: 'mapping_count_table') do |t|
- - t.header do |h|
- - h.th {t("mappings.count.ontology")}
- - h.th {t("mappings.count.mappings")}
+= turbo_frame_tag "mappings_table" do
+ .summary-mappings-tab
+ .summary-mappings-tab-table
+ = render TableComponent.new(id: 'summary-mappings-table', borderless: true, outline: true, searching: true, sort_column: '0', no_init_sort: true) do |t|
+ - t.header do |h|
+ - h.th {t("mappings.count.ontology")}
+ - h.th {t("mappings.count.mappings")}
- - if @mapping_counts.blank?
- - t.row do |r|
- - r.td {t("mappings.count.no_mappings")}
- - r.td {' '}
- - else
- - @mapping_counts.each do |mapping_count|
- - t.row do |r|
- - r.td do
- - title = mapping_count[:target_ontology].name
- = link_to_modal title, mappings_show_mappings_path(id: @ontology_acronym ,target: mapping_count[:target_ontology].id), data: { show_modal_title_value: title, show_modal_size_value: 'modal-xl'}
- - r.td do
- = number_with_delimiter(mapping_count[:count], delimiter: ',')
+ - if @mapping_counts.blank?
+ - t.row do |r|
+ - r.td {t("mappings.count.no_mappings")}
+ - r.td {' '}
+ - else
+ - @mapping_counts.each do |mapping_count|
+ - t.row do |r|
+ - r.td do
+ - title = mapping_count[:target_ontology].name
+ = link_to_modal title, mappings_show_mappings_path(id: @ontology_acronym ,target: mapping_count[:target_ontology].id), data: { show_modal_title_value: title, show_modal_size_value: 'modal-xl'}
+ - r.td do
+ = number_with_delimiter(mapping_count[:count], delimiter: ',')
+ - if @ontologies_mapping_count
+ .summary-mappigs-page-container{'data-controller': 'mappings',
+ 'data-mappings-mappings-list-value': "#{@ontologies_mapping_count.to_h.to_json}",
+ 'data-mappings-acronym-value': @ontology_acronym,
+ 'data-mappings-api-url-value': rest_url
+ }
+ .mappings-bubble-view-frame{'data-mappings-target': 'frame'}
+ #mappings-bubbles-view{'data-mappings-target': 'bubbles'}
+ .mapping-bubbles-loader.d-none{'data-mappings-target': 'loader'}
+ = render LoaderComponent.new(type: 'pulsing')
+ .d-none{'data-mappings-target': 'modal'}
+ = client_filled_modal
+ .d-flex.justify-content-center
+ = render Display::InfoTooltipComponent.new(text: mappings_bubble_view_legend)
\ No newline at end of file
diff --git a/app/views/mappings/_form.html.haml b/app/views/mappings/_form.html.haml
index 8bd7d159ea..264b680e70 100644
--- a/app/views/mappings/_form.html.haml
+++ b/app/views/mappings/_form.html.haml
@@ -38,5 +38,5 @@
= select("mapping", "relation", options_for_select(@mapping_relation_options, @selected_relation), {}, class: "form-control")
%div.form-group
- = submit_tag t("mappings.form.save") , class:'btn btn-success btn-block'
+ = submit_tag t("mappings.form.save") , class:'regular-button primary-button slim'
diff --git a/app/views/mappings/_mapping_table.html.haml b/app/views/mappings/_mapping_table.html.haml
index ad31dcd850..1d91b3b5bb 100644
--- a/app/views/mappings/_mapping_table.html.haml
+++ b/app/views/mappings/_mapping_table.html.haml
@@ -1,14 +1,3 @@
--# called from mappings_controller in several ways:
--# 1. mappings_controller::get_concept_table via /app/views/mappings/_concept_mappings.html.haml
--# 2. directly from mappings_controller::get_concept_table
--#NOTES on control over mapping deletion:
--#deleteMappings() is a callback that is called by "#delete_mappings_button" created below.
--#The appearance of that button is controlled by updateMappingDeletePermissions(), which
--#relies on @delete_mapping_permission in /app/views/mappings/_mapping_table.html.haml; which,
--#in turn, is set by /app/controllers/application_controller.check_delete_mapping_permission()
--#
--# The delete mappings button display is controlled by JS on page ready (see bp_mappings.js)
--# check_box_tag(name, value = "1", checked = false, options = {})
= check_box_tag "delete_mappings_permission", @delete_mapping_permission, @delete_mapping_permission, style: "display: none;"
%div#concept_mappings_tables_div
= render_alerts_container(MappingsController)
diff --git a/app/views/mappings/_ontology_mappings.html.haml b/app/views/mappings/_ontology_mappings.html.haml
deleted file mode 100644
index 9890a668cb..0000000000
--- a/app/views/mappings/_ontology_mappings.html.haml
+++ /dev/null
@@ -1,20 +0,0 @@
-.card
- #headingOne.card-header
- %h2.mb-0
- %button.btn.btn-link.btn-block.text-left{"data-target" => "#collapseOne", "data-toggle" => "collapse", :type => "button"}
- = t('mappings.intro').html_safe
- = link_to(Rails.configuration.settings.links[:mappings], id: "mappings-help", "aria-label": "View mappings help") do
- %i.fas.fa-question-circle.fa-lg{"aria-hidden": "true"}
- #collapseOne.collapse{"data-parent" => "#accordionExample"}
- .card-body
- %div#mappings_select
- - if @options.empty?
- = t('mappings.no_mappings_available')
- - else
- - @options.unshift(['',''])
- %div{onchange: "loadMappings(event.target.value)"}
- = select_input(name: 'search[ontologies]', id: 'search_ontologies', label: '', values: @options, placeholder: t('mappings.select_ontologies_list'))
- #mapping_load
- %img{src: asset_path("jquery.simple.tree/spinner.gif")}/
- = t('mappings.loading_mappings')
- #mappingCount{style:'min-height: 300px;'}
diff --git a/app/views/mappings/_show.html.haml b/app/views/mappings/_show.html.haml
index 62dbcd5018..6ca1cffe1f 100644
--- a/app/views/mappings/_show.html.haml
+++ b/app/views/mappings/_show.html.haml
@@ -1,11 +1,12 @@
= render_in_modal do
#mappings.paginate_ajax{:style => "overflow: auto; max-height: 600px;"}
#mapping_results
- = will_paginate @page_results, :update => 'mappings', :params => { :target => params[:target] }
+ .mappings-table-pagination
+ = will_paginate @page_results, :update => 'mappings', :params => { :target => params[:target] }
- if @mappings.nil? or @mappings.empty?
= t("mappings.show.no_mappings_found")
- - else
- %table.zebra.w-100
+ - else
+ %table.table-content.table-content-stripped
%thead
%th #{@ontology_name}
%th #{@target_ontology_name}
@@ -22,7 +23,8 @@
= ajax_to_external_cls(cls)
%td
#{map.source} #{(map.process || {})[:source_name]}
- = will_paginate @page_results, :update => 'mappings', :params => { :target => params[:target] }
+ .mappings-table-pagination
+ = will_paginate @page_results, :update => 'mappings', :params => { :target => params[:target] }
:javascript
jQuery(document).ready(function(){
diff --git a/app/views/mappings/bulk_loader/_loader.html.haml b/app/views/mappings/bulk_loader/_loader.html.haml
deleted file mode 100644
index fde3140323..0000000000
--- a/app/views/mappings/bulk_loader/_loader.html.haml
+++ /dev/null
@@ -1,19 +0,0 @@
-= render_in_modal do
- %div.d-flex.flex-column{:style => "overflow: auto; max-height: 600px;"}
- = render TurboFrameComponent.new(id: 'file_loader_result') do
- %div.my-2
- .card.mb-2
- %div
- %h2
- %button.btn.btn-link{"data-target" => "#collapseOne", "data-toggle" => "collapse", :type => "button"}
- = t("mappings.bulk_loader.loader.example_of_valid_file")
- #collapseOne.collapse
- .card-body
- %pre
- %code
- = JSON.pretty_generate @example_code
- = form_with url: '/mappings/loader', method: :post, multipart: true, data: { turbo: true} do
- %div
- = render Input::FileInputComponent.new(name: :file)
- %button.btn.btn-secondary.btn-block.mt-2{type:'submit'}
- = t("mappings.bulk_loader.loader.save")
diff --git a/app/views/mappings/index.html.haml b/app/views/mappings/index.html.haml
index 24a5d1dc9a..44b509894b 100644
--- a/app/views/mappings/index.html.haml
+++ b/app/views/mappings/index.html.haml
@@ -1,14 +1,23 @@
- @title= t('mappings.title')
-%div.container
- %div#mappings_container.container-fluid.py-4.flex-grow-1
- %h1.my-1= t('mappings.title')
-
- %div#mappings_uploader.my-2
- = link_to_modal t('mappings.upload_mappings') , "/mappings/loader", class: "btn btn-primary btn-block",
- data: { show_modal_title_value: t('mappings.mappings_bulk_load'), show_modal_size_value: 'modal-xl'}
- %hr.my-3.w-100
- #accordionExample.accordion
- = render partial: 'ontology_mappings'
- = render partial: 'concept_mappings_selector'
-
+.mappigs-page-container{'data-controller': 'mappings',
+ 'data-mappings-mappings-list-value': "#{@ontologies_mapping_count.to_h.to_json}",
+ 'data-mappings-api-url-value': rest_url
+ }
+ .mappings-page-subcontainer
+ .mappings-page-title
+ .text
+ = @title
+ .line
+ .mappings-page-decription
+ = t('mappings.description')
+ = render TabsContainerComponent.new do |c|
+ - c.item(title: t('mappings.tabs.bubble_view'), selected: true)
+ - c.item_content do
+ = render partial: '/mappings/tab_sections/bubble_view'
+ - c.item(title: t('mappings.tabs.table_view'))
+ - c.item_content do
+ = render partial: '/mappings/tab_sections/table_view'
+ - c.item(title: t('mappings.tabs.upload_mappings'))
+ - c.item_content do
+ = render partial: '/mappings/tab_sections/upload_mappings'
\ No newline at end of file
diff --git a/app/views/mappings/tab_sections/_bubble_view.html.haml b/app/views/mappings/tab_sections/_bubble_view.html.haml
new file mode 100644
index 0000000000..31a335fa1e
--- /dev/null
+++ b/app/views/mappings/tab_sections/_bubble_view.html.haml
@@ -0,0 +1,26 @@
+.mappings-bubble-view-container
+ .upload-mappings
+ .card.upload-mappings-example.ontologies
+ = render(Layout::RevealComponent.new(toggle: true, selected: false)) do |c|
+ - c.button do
+ .title-bar
+ = inline_svg_tag 'icons/settings.svg'
+ = t('mappings.filter_ontologies')
+ - c.container do
+ .mappings-page-ontologies-selector{'data-mappings-target': 'ontologies'}
+ = ontologies_selector(id:'mappings_page_ontologies' ,name: 'ontologies[]')
+ .selector-button{'data-action': 'click->mappings#filterOntologies'}
+ = regular_button('filter-bubbles', t('mappings.filter_bubbles'))
+
+ .mappings-bubble-view-frame{'data-mappings-target': 'frame'}
+ #mappings-bubbles-view{'data-mappings-target': 'bubbles'}
+ .mapping-bubbles-loader.d-none{'data-mappings-target': 'loader'}
+ = loader_component
+ = info_tooltip(mappings_bubble_view_legend)
+ .mappings-zoom-buttons
+ .in{'data-action': 'click->mappings#zoomIn'}
+ = inline_svg_tag 'icons/zoom-in.svg'
+ .out{'data-action': 'click->mappings#zoomOut'}
+ = inline_svg_tag 'icons/zoom-out.svg'
+ .d-none{'data-mappings-target': 'modal'}
+ = client_filled_modal
\ No newline at end of file
diff --git a/app/views/mappings/tab_sections/_table_view.html.haml b/app/views/mappings/tab_sections/_table_view.html.haml
new file mode 100644
index 0000000000..5ad3ac571e
--- /dev/null
+++ b/app/views/mappings/tab_sections/_table_view.html.haml
@@ -0,0 +1,8 @@
+.mappings-table-view-container
+ .mappings-ontologies-select
+ = form_tag('/mappings/count/fake_id', method: :get, novalidate: true, data: { turbo: true, turbo_frame: 'mappings_table' }) do
+ .mappings-selector{data: {action: 'change->mappings#submit', 'mappings-target': 'selector'}}
+ = select_input(name: "ontology", values: @options, placeholder: t('mappings.intro'))
+ %input.d-none{ type: 'submit', 'data-mappings-target': 'submit'}
+ %div
+ = render TurboFrameComponent.new(id:"mappings_table")
\ No newline at end of file
diff --git a/app/views/mappings/tab_sections/_upload_mappings.html.haml b/app/views/mappings/tab_sections/_upload_mappings.html.haml
new file mode 100644
index 0000000000..e10cb80645
--- /dev/null
+++ b/app/views/mappings/tab_sections/_upload_mappings.html.haml
@@ -0,0 +1,16 @@
+.upload-mappings
+ .card.upload-mappings-example
+ .title-bar{"data-target" => "#collapseOne", "data-toggle" => "collapse"}
+ = t("mappings.bulk_loader.loader.example_of_valid_file")
+ #collapseOne.collapse
+ .card-body
+ %pre
+ %code
+ = JSON.pretty_generate @example_code
+
+ = form_with url: '/mappings/loader', method: :post, multipart: true, data: { turbo: true, turbo_frame: 'file_loader_result'} do
+ %div.mb-3
+ = render Input::FileInputComponent.new(name: :file)
+ = render Buttons::RegularButtonComponent.new(id:'upload-mappings-button', value: "Save", variant: "primary", size: 'slim', type:'submit', state: "regular")
+ .mt-3
+ = render TurboFrameComponent.new(id: 'file_loader_result')
\ No newline at end of file
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 4130b985ee..cba920dd17 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -558,6 +558,13 @@ en:
ontology_visits: Ontology visits
mappings:
all: All
+ description: Dive into an overview of the mappings in the bubble view, efficiently locate a specific ontology in the table view or upload your own mappings.
+ tabs:
+ bubble_view: Bubbles view
+ table_view: Table view
+ upload_mappings: Upload mappings
+ filter_ontologies: Filter ontologies in the bubble view
+ filter_bubbles: Filter bubbles
external_mappings: "External Mappings (%{number_with_delimiter})"
interportal_mappings: "Interportal Mappings - %{acronym} (%{number_with_delimiter})"
test_bulk_load: This is the mappings produced to test the bulk load
@@ -570,7 +577,6 @@ en:
find_mappings: Find mappings of a class/concept
intro: Find all the mappings of an ontology
loading_mappings: Loading mappings...
- mappings_bulk_load: Upload mappings in bulk from a source file
no_mappings_available: No mappings available
title: Mappings
upload_mappings: Upload mappings
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 3dd03f4047..ba8dbc88f0 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -566,69 +566,75 @@ fr:
ontology_visits: Visites d'ontologies
mappings:
- select_ontologies_list: "Sélectionner des ontologies"
- external_mappings: "Alignements Externes (%{number_with_delimiter})"
- interportal_mappings: "Alignements Interportail - %{acronym} (%{number_with_delimiter})"
- test_bulk_load: Ceci est les alignements produits pour tester le chargement en masse
- mapping_created: Alignement créé
- mapping_updated: Alignement mis à jour
+ all: Tous
+ description: Plongez dans une vue d'ensemble des mappings dans la vue en bulles, localisez efficacement une ontologie spécifique dans la vue en tableau ou téléchargez vos propres mappings.
+ tabs:
+ bubble_view: Vue en bulles
+ table_view: Vue en tableau
+ upload_mappings: Télécharger des mappings
+ filter_ontologies: Filtrer les ontologies dans la vue en bulles
+ filter_bubbles: Filtrer les bulles
+ external_mappings: "Mappings externes (%{number_with_delimiter})"
+ interportal_mappings: "Mappings interportails - %{acronym} (%{number_with_delimiter})"
+ test_bulk_load: Ceci sont les mappings créés pour tester le chargement en masse
+ mapping_created: Mapping créé
+ mapping_updated: Mapping mis à jour
mapping_deleted: "%{map_id} supprimé avec succès"
- mapping_not_found: "Alignement %{id} non trouvé"
+ mapping_not_found: "Mapping %{id} non trouvé"
error_of_source_and_target: Les concepts source et cible doivent être spécifiés
- mapping_issue: "Problème d'alignement avec '%{mapping}' : %{message}"
- find_mappings: Trouver les alignements d'une classe/concept
- intro: Trouver tous les alignements d'une ontologie
- loading_mappings: Chargement des alignements...
- mappings_bulk_load: Télécharger des alignements en masse à partir d'un fichier source
- no_mappings_available: Aucun alignement disponible
- title: Alignements
- upload_mappings: Télécharger des alignements
- all: Tout
+ mapping_issue: "Problème de mapping avec '%{mapping}' : %{message}"
+ find_mappings: Trouver tous les mappings d'une classe/concept
+ intro: Trouver tous les mappings d'une ontologie
+ loading_mappings: Chargement des mappings...
+ no_mappings_available: Aucun mapping disponible
+ title: Mappings
+ upload_mappings: Télécharger des mappings
+ select_ontologies_list: Sélectionner les ontologies
count:
ontology: Ontologie
- mappings: Alignements
- no_mappings: Il n'y a aucun alignement vers ou depuis cette ontologie
+ mappings: Mappings
+ no_mappings: Il n'y a aucun mapping vers ou depuis cette ontologie
form:
source_class: Classe source
- mapping_name: Description du alignement (nom)
+ mapping_name: Description du mapping (nom)
contact_info: Informations de contact
- mapping_source_name: Nom de la source (ID de l'ensemble de alignement)
+ mapping_source_name: Nom source (ID de l'ensemble de mappings)
mapping_comment: Commentaire
- mapping_relation: Type de relation de alignement
- save: Sauvegarder
+ mapping_relation: Type de relation de mapping
+ save: Enregistrer
mapping_table:
- mapping_to: Alignement vers
+ mapping_to: Mapping vers
relations: Relations
source: Source
type: Type
actions: Actions
- no_mappings: Il n'y a actuellement aucun alignement pour cette classe.
+ no_mappings: Il n'y a actuellement aucun mapping pour cette classe.
mapping_type_selector:
- mapping_type: Type de alignement
+ mapping_type: Type de mapping
internal: Interne
- interportal: InterPortail
+ interportal: Interportail
external: Externe
target_class: Classe cible
details: Détails
ontology_acronym: Ontologie (acronyme)
class: Classe
- ontology_acronym_placeholder: Entrez l'acronyme de l'ontologie
+ ontology_acronym_placeholder: Entrez l'ACRONYM de l'ontologie
class_uri_placeholder: Entrez l'URI de la classe
ontology_uri_placeholder: Entrez l'URI de l'ontologie
show_line:
- edit_modal: Éditer
+ edit_modal: Modifier
delete_button: Supprimer
- turbo_confirm: Êtes-vous sûr ?
- edit_mapping: Éditer l'alignement pour %{preflabel}
+ turbo_confirm: Êtes-vous sûr(e) ?
+ edit_mapping: Modifier le mapping pour %{preflabel}
show:
- no_mappings_found: Aucun alignement trouvé
+ no_mappings_found: Aucun mapping trouvé
bulk_loader:
loader:
example_of_valid_file: Voir un exemple de fichier valide
- save: Sauvegarder
+ save: Enregistrer
loaded_mappings:
- mappings_created: "%{size} alignements créés avec succès"
- id: Id
+ mappings_created: "%{size} mappings créés avec succès"
+ id: ID
source: Source
target: Cible
relation: Relation
@@ -636,6 +642,7 @@ fr:
actions: Actions
see_other_properties: Voir d'autres propriétés
+
agents:
not_found_agent: Agent avec id %{id}
add_agent: Nouvel agent ajouté avec succès
diff --git a/config/routes.rb b/config/routes.rb
index 2759ffd627..b556a90685 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -18,6 +18,7 @@
post 'agents/:id/usages', to: 'agents#update_agent_usages', constraints: { id: /.+/ }
resources :agents, constraints: { id: /.+/ }
post 'agents/:id', to: 'agents#update', constraints: { id: /.+/ }
+
resources :ontolobridge do
post :save_new_term_instructions, on: :collection
end
@@ -29,7 +30,6 @@
get '/users/subscribe/:username', to: 'users#subscribe'
get '/users/un-subscribe/:email', to: 'users#un_subscribe'
- get '/mappings/loader', to: 'mappings#loader'
post '/mappings/loader', to: 'mappings#loader_process'
get 'mappings/count/:id', to: 'mappings#count', constraints: { id: /.+/ }
get 'mappings/show_mappings', to: 'mappings#show_mappings'
diff --git a/package.json b/package.json
index 3e4673d338..392e650aaa 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
"@hotwired/turbo-rails": "^7.1.1",
"@triply/yasgui": "^4.2.28",
"chart.js": "^4.4.1",
+ "d3": "^7.8.5",
"datatables.net-dt": "^1.13.8",
"debounce": "^1.2.1",
"esbuild": "^0.14.41",
diff --git a/yarn.lock b/yarn.lock
index 5cb3f6d1cc..131990647f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -265,6 +265,11 @@ combined-stream@^1.0.8:
dependencies:
delayed-stream "~1.0.0"
+commander@7:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
+ integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+
commander@^6.1.0:
version "6.2.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
@@ -280,6 +285,250 @@ cookiejar@^2.1.2:
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b"
integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==
+"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0:
+ version "3.2.4"
+ resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5"
+ integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
+ dependencies:
+ internmap "1 - 2"
+
+d3-axis@3:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-3.0.0.tgz#c42a4a13e8131d637b745fc2973824cfeaf93322"
+ integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==
+
+d3-brush@3:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c"
+ integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==
+ dependencies:
+ d3-dispatch "1 - 3"
+ d3-drag "2 - 3"
+ d3-interpolate "1 - 3"
+ d3-selection "3"
+ d3-transition "3"
+
+d3-chord@3:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-3.0.1.tgz#d156d61f485fce8327e6abf339cb41d8cbba6966"
+ integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==
+ dependencies:
+ d3-path "1 - 3"
+
+"d3-color@1 - 3", d3-color@3:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
+ integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
+
+d3-contour@4:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-4.0.2.tgz#bb92063bc8c5663acb2422f99c73cbb6c6ae3bcc"
+ integrity sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==
+ dependencies:
+ d3-array "^3.2.0"
+
+d3-delaunay@6:
+ version "6.0.4"
+ resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz#98169038733a0a5babbeda55054f795bb9e4a58b"
+ integrity sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==
+ dependencies:
+ delaunator "5"
+
+"d3-dispatch@1 - 3", d3-dispatch@3:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e"
+ integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
+
+"d3-drag@2 - 3", d3-drag@3:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba"
+ integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
+ dependencies:
+ d3-dispatch "1 - 3"
+ d3-selection "3"
+
+"d3-dsv@1 - 3", d3-dsv@3:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73"
+ integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==
+ dependencies:
+ commander "7"
+ iconv-lite "0.6"
+ rw "1"
+
+"d3-ease@1 - 3", d3-ease@3:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
+ integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
+
+d3-fetch@3:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-3.0.1.tgz#83141bff9856a0edb5e38de89cdcfe63d0a60a22"
+ integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==
+ dependencies:
+ d3-dsv "1 - 3"
+
+d3-force@3:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4"
+ integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==
+ dependencies:
+ d3-dispatch "1 - 3"
+ d3-quadtree "1 - 3"
+ d3-timer "1 - 3"
+
+"d3-format@1 - 3", d3-format@3:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
+ integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
+
+d3-geo@3:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.1.0.tgz#74fd54e1f4cebd5185ac2039217a98d39b0a4c0e"
+ integrity sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==
+ dependencies:
+ d3-array "2.5.0 - 3"
+
+d3-hierarchy@3:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6"
+ integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==
+
+"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
+ integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
+ dependencies:
+ d3-color "1 - 3"
+
+"d3-path@1 - 3", d3-path@3, d3-path@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526"
+ integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
+
+d3-polygon@3:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-3.0.1.tgz#0b45d3dd1c48a29c8e057e6135693ec80bf16398"
+ integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==
+
+"d3-quadtree@1 - 3", d3-quadtree@3:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f"
+ integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==
+
+d3-random@3:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4"
+ integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==
+
+d3-scale-chromatic@3:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#15b4ceb8ca2bb0dcb6d1a641ee03d59c3b62376a"
+ integrity sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==
+ dependencies:
+ d3-color "1 - 3"
+ d3-interpolate "1 - 3"
+
+d3-scale@4:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
+ integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
+ dependencies:
+ d3-array "2.10.0 - 3"
+ d3-format "1 - 3"
+ d3-interpolate "1.2.0 - 3"
+ d3-time "2.1.1 - 3"
+ d3-time-format "2 - 4"
+
+"d3-selection@2 - 3", d3-selection@3:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31"
+ integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
+
+d3-shape@3:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5"
+ integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
+ dependencies:
+ d3-path "^3.1.0"
+
+"d3-time-format@2 - 4", d3-time-format@4:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
+ integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
+ dependencies:
+ d3-time "1 - 3"
+
+"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7"
+ integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
+ dependencies:
+ d3-array "2 - 3"
+
+"d3-timer@1 - 3", d3-timer@3:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
+ integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
+
+"d3-transition@2 - 3", d3-transition@3:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f"
+ integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==
+ dependencies:
+ d3-color "1 - 3"
+ d3-dispatch "1 - 3"
+ d3-ease "1 - 3"
+ d3-interpolate "1 - 3"
+ d3-timer "1 - 3"
+
+d3-zoom@3:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3"
+ integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==
+ dependencies:
+ d3-dispatch "1 - 3"
+ d3-drag "2 - 3"
+ d3-interpolate "1 - 3"
+ d3-selection "2 - 3"
+ d3-transition "2 - 3"
+
+d3@^7.8.5:
+ version "7.8.5"
+ resolved "https://registry.yarnpkg.com/d3/-/d3-7.8.5.tgz#fde4b760d4486cdb6f0cc8e2cbff318af844635c"
+ integrity sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==
+ dependencies:
+ d3-array "3"
+ d3-axis "3"
+ d3-brush "3"
+ d3-chord "3"
+ d3-color "3"
+ d3-contour "4"
+ d3-delaunay "6"
+ d3-dispatch "3"
+ d3-drag "3"
+ d3-dsv "3"
+ d3-ease "3"
+ d3-fetch "3"
+ d3-force "3"
+ d3-format "3"
+ d3-geo "3"
+ d3-hierarchy "3"
+ d3-interpolate "3"
+ d3-path "3"
+ d3-polygon "3"
+ d3-quadtree "3"
+ d3-random "3"
+ d3-scale "4"
+ d3-scale-chromatic "3"
+ d3-selection "3"
+ d3-shape "3"
+ d3-time "3"
+ d3-time-format "4"
+ d3-timer "3"
+ d3-transition "3"
+ d3-zoom "3"
+
data-uri-to-buffer@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e"
@@ -338,6 +587,13 @@ define-data-property@^1.1.1:
gopd "^1.0.1"
has-property-descriptors "^1.0.0"
+delaunator@5:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.1.tgz#39032b08053923e924d6094fe2cde1a99cc51278"
+ integrity sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==
+ dependencies:
+ robust-predicates "^3.0.2"
+
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
@@ -590,6 +846,13 @@ highlight.js@^11.9.0:
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.9.0.tgz#04ab9ee43b52a41a047432c8103e2158a1b8b5b0"
integrity sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==
+iconv-lite@0.6:
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
+ integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3.0.0"
+
ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@@ -600,6 +863,11 @@ inherits@^2.0.3:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+"internmap@1 - 2":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
+ integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
+
jquery@>=1.7, jquery@^3.5.0:
version "3.7.1"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.1.tgz#083ef98927c9a6a74d05a6af02806566d16274de"
@@ -794,11 +1062,26 @@ remove-accents@^0.4.2:
resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.4.tgz#73704abf7dae3764295d475d2b6afac4ea23e4d9"
integrity sha512-EpFcOa/ISetVHEXqu+VwI96KZBmq+a8LJnGkaeFw45epGlxIZz5dhEEnNZMsQXgORu3qaMoLX4qJCzOik6ytAg==
+robust-predicates@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771"
+ integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==
+
+rw@1:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
+ integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==
+
safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+"safer-buffer@>= 2.1.2 < 3.0.0":
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+ integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
semver@^7.3.2:
version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"